Skip to content

Execution

The execution layer covers everything from a strategy decision to a confirmed fill on Binance. gordon-bot emits intents; gordon-executor is the only service that touches the exchange. No other service holds trading-capable keys.

Intent-to-fill sequence

OrderIntent

gordon-bot emits an OrderIntent when Strategy::evaluate returns a tradeable signal and the overlay pipeline does not veto it. The intent is written inside a single database transaction:

sql
INSERT INTO trading.order_intents (intent_id, bot_id, symbol, side, qty, sl_price, ...);
INSERT INTO bus.outbox (subject, payload) VALUES ('intents.executor', <OrderIntentEvent JSON>);

The outbox publisher (leader-elected in gordon-data) drains the bus.outbox row and publishes to NATS JetStream. See Event Flow for outbox mechanics.

Pre-trade checks in gordon-executor

Before submitting any order, gordon-executor runs these checks in sequence:

1. Halt-latch check. Reads trading.risk_state.halted. If TRUE, the intent is NACK'd (not acked to JetStream — it will be redelivered on resume). No order is submitted.

2. Daily notional cap (DP-01). Checks the total notional submitted today against trading.daily_notional_caps. If the cap is reached, the intent is rejected and an audit row is written. The cap is per-bot and per-currency.

3. Idempotency check. Checks trading.order_intents by intent_id. If the intent has already been processed (order exists), the message is acked as a no-op. This handles JetStream redelivery without double-orders.

4. Lease fence. Verifies the bot's lease is current before submitting. A bot whose lease has expired (crashed without releasing) cannot place new orders.

Order placement

Orders are submitted via gordon_exchange::Client (Binance REST + rate limiter). gordon-executor does not silently retry a failed order placement. A placement failure is logged with its error code, the intent is left unacked (redelivered by JetStream after ack_wait = 30s), and the failure counter is incremented. An operator or a subsequent successful retry resolves it.

Fill tracking

Binance fills arrive on the user-data WebSocket stream. gordon-executor:

  1. Inserts trading.trades with ON CONFLICT ON (trade_fingerprint) DO NOTHING. The trade_fingerprint is a content-hash of (symbol, side, qty, price, timestamp) — deduplicates Binance's at-least-once delivery.
  2. Inserts trading.fill_events (audit trail).
  3. Inserts a bus.outbox row for trading.fills.{bot_id} (the FillEvent NATS payload).
  4. Fires pg_notify('bot_events', fill_id) — picked up by gordon-manager's LISTEN multiplexer for the console fan-out.

All four writes are in the same transaction.

Reconcile-on-startup

Every time gordon-executor starts, it runs a reconciliation scan:

  1. Reads all trading.orders in state Submitted or PartialFill from the last 48 hours.
  2. For each open order, queries Binance REST to check current status.
  3. Any divergence (order filled on exchange but not in DB, order cancelled on exchange) is resolved: missing fills are inserted, stale orders are closed.

This handles the failure mode where gordon-executor crashed between placing an order and processing the fill. Reconciliation is idempotent — running it multiple times produces the same result.

Risk command consumer

gordon-executor also maintains the executor-risk durable consumer on risk.commands. When a RiskCommandEvent arrives:

  • flatten_all — closes all open positions via Binance REST market orders, sets halt latch.
  • pause_bot <id> — stops accepting new intents from the specified bot (checked at pre-trade step).

Risk commands take priority over intent processing. The executor processes executor-risk messages before executor-default in its select loop.

Invariants

  • gordon-executor is the sole holder of trading-capable Binance keys. No other service can submit orders.
  • Every intent submission is preceded by halt-latch, daily cap, idempotency, and lease checks — in that order.
  • Fills are inserted idempotent on trade_fingerprint. Duplicate delivery from Binance is a no-op.
  • Reconcile-on-startup runs before accepting any new intents. A service that skips reconcile is a correctness bug.
  • No silent retries on order placement failure. Failure is logged, metered, and left for redelivery.
  • All four fill writes (trades, fill_events, bus.outbox, pg_notify) are in the same transaction.
  • Event Flow — outbox pattern and intent subject topology.
  • Risk Management — halt-latch and flatten command details.
  • Strategies — how gordon-bot generates the OrderIntent.
  • BacktestingBacktestExecution simulates the same execution path without touching the exchange.

Gordon — keep compounding without blowing up