# Live crypto execution layer (Binance USDⓈ-M perps) — design

**Date:** 2026-06-14
**Status:** Design approved (research-grounded), ready for implementation plan
**Parent:** the deployable combined book — crypto X-sectional momentum sleeve (83%) needs live execution

## Problem

The cockpit forward-validates the combined book on paper. To eventually trade the crypto alpha (dollar-neutral
cross-sectional momentum across ~135 Binance USDⓈ-M perps, daily rebalance) we need a production-grade
execution layer: turn target weights into real fills on a live venue, with the safety + cost discipline a
systematic desk requires. **Testnet first; real capital only after the Phase-1 forward gate clears.**

## Goals

- A hexagonal, fxhnt-native execution layer: a second `Broker`/exchange adapter (Binance USDM via **ccxt**)
  plus a pure, liquidity-aware **smart order router**, full **pre/post-trade risk controls**, and a
  reconciliation + transaction-cost-analysis (TCA) loop.
- **Testnet-first**: identical code path; live requires an explicit `allow_live` gate.
- Best realistic execution quality for a daily-rebalanced multi-perp book (maker-first + taker fallback +
  liquidity slicing), with measured fill quality.
- Honest cost + risk: rate-limit-safe, idempotent, capacity-aware, kill-switchable.

## Non-goals

- Not HFT/microsecond. The book rebalances once daily; fills over minutes are fine.
- No new alpha — execution only. The signal is the existing `xs_momentum_book`.
- Not coupling to foxhunt's Rust risk controllers — fxhnt gets a **Python** edge-decay×Kelly scaler.
- Spot, options, cross-exchange routing — out (single venue v1; ccxt keeps multi-exchange open).

## Research-grounded production concerns (do NOT underestimate)

Verified via research (Binance USDⓈ-M docs, ccxt, execution-algo literature):

1. **Rate limits.** Binance USDM: **2,400 request-weight/min per IP** and **1,200 orders/min per account**
   (plus short-window order caps). Exceeding → HTTP 429; repeated/no-backoff → **418 IP ban**. With 135
   symbols × (limit place → cancel → IOC cross) the order count is the binding constraint. **Mandatory:** a
   rate-limit governor — ccxt `enableRateLimit`, a local token bucket, reading `X-MBX-USED-WEIGHT-*` /
   `X-MBX-ORDER-COUNT-*` response headers, exponential backoff on 429, hard stop before 418.
2. **Idempotency.** Every order carries a deterministic `clientOrderId` (e.g. `fxhnt-<rebalance_id>-<symbol>-<leg>`)
   so a retry after a timeout never double-places. Reconcile by clientOrderId.
3. **Exchange filters.** Round quantity to `LOT_SIZE.stepSize`, price to `PRICE_FILTER.tickSize`, and skip
   anything below `MIN_NOTIONAL` (~5 USDT) and `MARKET_LOT_SIZE`. Cache `exchange_info` per rebalance.
4. **Position/margin setup.** Explicit **one-way** position mode; `reduceOnly` on reducing/closing legs;
   set leverage + margin mode (isolated default) per symbol once. Funding settles every 8h (track in PnL).
5. **Execution cost is real.** Pro desks cut 50–90 bps with maker-first + slicing vs pure taker. Measure it:
   a **TCA** record per rebalance (fill price vs arrival/decision mid = implementation shortfall) so we know
   whether execution is eating the alpha — especially on thin small-caps.
6. **Testnet specifics.** ccxt `set_sandbox_mode(True)` + **separate** testnet API keys
   (`testnet.binancefuture.com`). Live = same adapter, sandbox off, real keys, `allow_live=true`.

## Architecture (hexagonal, fxhnt)

```
ports/crypto_exchange.py        Perp exchange contract (Protocol) + DTOs:
                                  ExchangeInfo(stepSize,tickSize,minNotional per symbol) · OrderBook(bids/asks)
                                  · PerpPosition(symbol, signed_qty, entry, leverage) · AccountState(nlv,...)
                                  · ChildOrder(symbol, side, qty, type, price?, reduce_only, post_only, client_id)
                                  · Fill(client_id, symbol, side, qty, price, fee, ts)
                                  methods: exchange_info() · order_book(symbol) · positions() · account_state()
                                  · set_leverage/set_position_mode · place(ChildOrder)->Fill|status · cancel(id)
domain/execution/
  rounding.py       PURE: round_qty(stepSize), round_price(tickSize), meets_min_notional()
  smart_router.py   PURE: (target deltas + market snapshot) -> [ChildOrder] schedule
                          maker-first limit@touch -> IOC cross fallback; POV slice vs depth; slippage-abort
  risk_gates.py     PURE: pre-trade checks (allow_live, gross cap, per-symbol cap, leverage cap, kill-switch)
                          + dynamic scaler: edge-decay(θ, Page-Hinkley) × Kelly-fraction × capacity caps
  capacity.py       PURE: ADV-based per-symbol max notional (don't be >X% of a thin market)
  tca.py            PURE: implementation-shortfall per fill (vs arrival mid) -> cost bps
application/
  crypto_execution.py   ORCHESTRATES: book -> target$ -> scaler -> deltas -> gates -> router ->
                          rate-limited place loop -> collect fills -> reconcile -> persist (cockpit DB)
  reconciliation.py     intended vs exchange positions (by clientOrderId + net) -> drift -> log / halt
adapters/exchange/binance_ccxt.py   ccxt binanceusdm adapter (testnet via sandbox); rate-limit governor;
                                     header-aware backoff; filter cache; idempotent clientOrderId
cli.py:  crypto-rebalance (--dry-run | --execute) · crypto-flatten (kill-switch) · crypto-positions
config:  BinanceSettings (api_key/secret env, testnet=true, leverage, gross_cap, per_symbol_cap,
                          participation_rate, max_slippage_bps, allow_live=false, kill_switch=false)
```

The **Broker** boundary stays the contract; this adds a richer perp-aware `crypto_exchange` port alongside it
(perps need exchange_info, order book, leverage, signed positions, reduce-only — beyond the equities `Broker`).

## Data flow

```
xs_momentum_book -> target weights (dollar-neutral, ~135 perps)
  -> risk scaler: gross *= clamp(edge_decay θ) × Kelly_fraction ; per-symbol *= capacity cap
  -> target $ per symbol  ->  deltas vs current exchange positions (signed)
  -> pre-trade gates (allow_live · kill_switch · gross cap · per-symbol cap · leverage · session drawdown CB)
  -> smart_router per symbol:
       skip if |Δ| < max(min_notional, hysteresis×NLV)
       size = min(target, participation_rate×ADV, depth_cap×book_depth) ; slice if target exceeds
       child = post-only limit @ touch (maker) ; wait T ; if unfilled -> cancel -> IOC cross (taker)
       abort symbol if fill price drifts > max_slippage_bps vs arrival mid ; reduce_only on closes
  -> rate-limited place loop (token bucket + X-MBX headers + 429 backoff; deterministic clientOrderId)
  -> collect Fills -> TCA (shortfall bps) -> reconciliation (intended vs actual; halt on drift>tol)
  -> persist fills + realized + TCA to the cockpit DB  (dashboard shows live execution + cost)
```

## Risk controls (full)

- **Pre-trade gates** (`risk_gates`): `allow_live` (default false — testnet/dry-run otherwise) · kill-switch
  flag (file/DB; `crypto-flatten` closes all reduce-only) · gross-exposure cap (×NLV) · per-symbol notional
  cap · leverage cap · session-drawdown circuit breaker (halt new opens if intraday PnL < −X%).
- **Dynamic sizing**: target gross scaled by **edge-decay θ** (Page-Hinkley on realized per-trade PnL — a
  Python port of the foxhunt concept) × **Kelly fraction** (capped, e.g. ≤0.25) × **capacity caps** (ADV).
  Edge decays → gross shrinks automatically; edge dies → θ→0 → flat.
- **Post-trade**: reconciliation compares intended vs exchange positions (and unfilled clientOrderIds); drift
  beyond tolerance → log + halt (no silent divergence). TCA flags execution eating > N bps of expected edge.

## Error handling

- 429 → exponential backoff honoring `Retry-After`; approach the 418 threshold → hard stop the batch.
- Network/timeout on place → look up by `clientOrderId` before any retry (never double-place).
- Order rejected (filter/margin) → log, skip that symbol, continue the batch (one bad symbol ≠ abort).
- Partial fill → router books the filled qty, re-sizes the remainder for the IOC leg.
- Any reconciliation drift > tolerance or kill-switch set → halt + alert; never "best-effort" past a risk gate.

## Testnet & safety

Everything runs first on **Binance futures testnet** (ccxt `set_sandbox_mode(True)`, separate testnet keys).
`crypto-rebalance --dry-run` prints the full child-order schedule + TCA estimate with **zero** orders. Live
requires `testnet=false` AND `allow_live=true` AND kill-switch clear — three independent conditions. Secrets
are k8s secrets, never logged.

## Testing (TDD)

- Pure units offline with fixtures: `rounding` (step/tick/minNotional edge cases), `smart_router`
  (maker→IOC, slicing vs depth, slippage abort, partial fill, dust skip), `risk_gates` (every gate + the
  edge-decay×Kelly×capacity scaler), `capacity` (ADV caps), `tca` (shortfall math), `reconciliation`
  (drift detection). No network.
- `binance_ccxt` adapter against **testnet** behind a pytest marker (real testnet fills): place/cancel/IOC,
  filter rounding, leverage/position-mode, rate-limit header parsing.
- End-to-end on testnet: rebalance a tiny book → fills → reconcile → cockpit DB rows.

## Milestones

- **M1 — Exchange port + ccxt adapter:** `crypto_exchange` port + DTOs; `binance_ccxt` (exchange_info,
  order_book, positions, account_state, set_leverage/position_mode, place/cancel) with the **rate-limit
  governor**, header-aware backoff, filter cache, idempotent clientOrderId; `set_sandbox_mode` testnet wiring.
- **M2 — Smart router (pure):** `rounding` + `smart_router` (maker-first → IOC, POV slicing, slippage abort).
- **M3 — Risk layer (pure):** `risk_gates` + `capacity` + edge-decay(θ)×Kelly scaler + kill-switch +
  session-drawdown CB.
- **M4 — Orchestrator + observability:** `crypto_execution` + `reconciliation` + `tca` + cockpit-DB
  persistence of fills/realized/TCA; CLI `crypto-rebalance`/`crypto-flatten`/`crypto-positions`.
- **M5 — Testnet end-to-end:** real testnet fills, reconciliation, TCA; validate against a tiny live book.

## Open follow-ups (post-v1)

- Limit-chase/iceberg refinements; cross-exchange routing (ccxt makes Bybit additive).
- Wire the cockpit to show live execution + TCA alongside the paper-forward track.
- Capacity/sizing calibration from realized testnet slippage before any real-capital sizing.
