triangular_arbitrage_bot/docs/signal-contract-v2.md

115 lines
5.3 KiB
Markdown

## Plan: Move volume computation from executor to fused_engine
### Goal
fused_engine computes per-leg order parameters (`funds`/`size`), precision-rounded.
Executor becomes a thin dispatcher — validate+simulate (paper) or place+record (live).
### New signal schema
```json
{
"type": "signal",
"live": false,
"legs": [
{"pair": "BTC-USDT", "side": "buy", "funds": "5.01", "base_increment": "0.00001", "quote_increment": "0.01", "base_min": "0.0001"},
{"pair": "ETH-BTC", "side": "sell", "size": "0.0495", "base_increment": "0.001", "quote_increment": "0.01", "base_min": "0.001"},
{"pair": "ETH-USDT", "side": "sell", "size": "0.0498", "base_increment": "0.001", "quote_increment": "0.01", "base_min": "0.001"}
],
"starting_volume": "5.01",
"predicted_bps": 12.5,
"books": [{"symbol": "BTC-USDT", "bids": [...], "asks": [...]}, ...],
"ts_ms": 1779400000000,
"book_ts_ms": 1779400000000,
"t_arrive_ms": 1779400000000,
"t_eval_ms": 1779400000000,
"triangle_key": ["USDT","BTC","ETH"],
"correlation_id": "abc123"
}
```
- `live=true`: no `books`, no `size`/`funds` — executor places leg 0 with `starting_volume`, KuCoin fills drive the chain
- `live=false`: `books` present for simulation, `funds`/`size` pre-computed, executor passes them straight to `order_test()`
### fused_engine changes
#### 1. Store symbol precision metadata (`symbols_api.c`)
Already fetches `/api/v2/symbols`. Add to `trading_pair_t`:
```
char base_increment[32];
char quote_increment[32];
char base_min_size[32];
```
Parse from `baseIncrement`, `quoteIncrement`, `baseMinSize` fields in the API response.
#### 2. Per-leg volume computation (`evaluate.c`)
After computing `max_volume` in evaluate_symbol, add a new function that runs the executor's current volume logic in C:
```
compute_leg_volumes(
triangle, books, max_volume, fee_rates,
out_leg_funds_or_size[3],
out_leg_side[3]
)
```
Steps (mirrors executor.py:550-608):
- `starting = cost_to_precision(max_volume, leg0.quote_increment)`
- For leg 0 (buy): `net = starting * (1 - fee)`, `base = floor(net / ask, base_inc)`, recompute `funds = cost_precision(base * ask, quote_inc)`
- For leg 1: `input = leg0.base_output`. Same buy/sell logic
- For leg 2: same
- Store `funds` for buys, `size` for sells
The computation is pure arithmetic (no I/O): Decimal-style operations using `strtod`/`sprintf` with integer math or floating point with epsilon guards. Or use a lightweight bignum lib.
#### 3. Signal JSON format (`evaluate.c` and `events.c`)
Update both `format_signal_json` (evaluate.c, unused today — keep for debugging) and `send_signal_to_executor` (events.c) to emit the new schema, including per-leg `funds`/`size` and increment fields.
Remove `fee_rate` and `exchange_rate` from leg JSON — executor no longer needs them.
#### 4. Live vs paper mode selection
Config: add `live_mode: bool` to config.yaml (default false). fused_engine reads it and conditionalizes signal content.
### Executor changes (`executor.py`)
#### 1. Remove dead code
- `get_symbol_meta()` and `SymbolMeta` (kucoin_api.py:226-228 + dataclass)
- Volume propagation (executor.py:550-558)
- Precision rounding per leg (executor.py:564-608)
- `_precheck_volume()` → simplify to just check `starting_volume >= min_size` per leg using the `base_min`/`base_increment` from the signal
- Fee computation in paper fill simulation (executor.py:823) → use fee_currency from signal leg
- `legs[i - 1].get("fee_rate")` → executor no longer knows fee rates, use from signal
#### 2. New signal parsing
Extract `live`, `legs` with pre-computed `funds`/`size`, increment fields, `starting_volume`.
#### 3. live=true path
- Leg 0: `order_place(funds=starting_volume)`, await fill
- Subsequent legs: `order_place(funds=actual_propagated)` — no, the actual fill drives the next leg. Keep current propagation (actual fill.filled_volume feeds next leg input) — this stays because KuCoin returns real fills.
#### 4. live=false path (paper)
- Each leg: `order_test(funds=leg.funds)` or `order_test(size=leg.size)` — pass through
- Paper fill simulation: use signal `books` + leg `base_increment`/`quote_increment` to compute `deal_funds` ← this is the only volume math remaining in the executor
- Simulate: apply fee to base_or_quote, round to precision, compute effective deal_funds
- profit = `fills[2].deal_funds - fills[0].deal_funds`
#### 5. Paper fill simulation (what stays)
The executor must still:
- Read `books[i].asks[0].price` / `bids[0].price`
- For buys: compute how much base you'd get for `funds` at that top-of-book price, after fee, rounded to `base_increment`
- For sells: compute how much quote you'd get for `size` at that top-of-book price, after fee, rounded to `quote_increment`
- This is ~20 lines per leg, no external dependencies
### Migration order
1. Add precision fields to `trading_pair_t` and parse from API
2. Add volume computation in `evaluate.c`
3. Update signal JSON format (both functions)
4. Strip executor down
5. Test paper mode, then live mode
### Risk: Decimal precision in C
The executor uses Python `Decimal` for exact precision. In C, we can:
- Use `double` with `ceil()`/`floor()` and `1e-10` epsilon guards (simple, adequate for crypto tick sizes)
- Or use integer math: multiply by 10^decimals, do integer rounding, convert back
For crypto tick sizes (0.00001, 0.01, 0.1), doubles are precise enough. No need for bignum.