triangular_arbitrage_bot/docs/signal-contract-v2.md

5.3 KiB

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

{
  "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.