## 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.