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:
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:
- Inserts
trading.tradeswithON CONFLICT ON (trade_fingerprint) DO NOTHING. Thetrade_fingerprintis a content-hash of(symbol, side, qty, price, timestamp)— deduplicates Binance's at-least-once delivery. - Inserts
trading.fill_events(audit trail). - Inserts a
bus.outboxrow fortrading.fills.{bot_id}(theFillEventNATS payload). - 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:
- Reads all
trading.ordersin stateSubmittedorPartialFillfrom the last 48 hours. - For each open order, queries Binance REST to check current status.
- 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.
Related
- Event Flow — outbox pattern and intent subject topology.
- Risk Management — halt-latch and flatten command details.
- Strategies — how gordon-bot generates the OrderIntent.
- Backtesting —
BacktestExecutionsimulates the same execution path without touching the exchange.