From paper to a live executor: why order placement is a separate service
The engine thinks. The executor acts. Keeping those apart is the difference between a research prototype and something you would trust near a real exchange.
Two very different jobs
Reasoning and execution have opposite failure modes. Reasoning is slow, deliberate and benefits from context. Execution is fast, unforgiving and benefits from discipline.
Semantic Signal splits them: the engine produces an atomic JSON decision — symbol, side, size, stop-loss, take-profit, reasoning. A separate service, llm_trader_executor, does everything on the exchange side: CCXT order placement, leverage, exchange-side stops, and a JSONL verdict journal that answers the only question that matters — what actually happened to this order?
The guard chain in the executor
Every decision is checked in order before a single order can leave the process (src/safety.py):
- Duplicate detection — a content hash of the decision, not a timestamp, persisted to disk so a bot restart cannot re-execute the same trade;
- Actionable signal — anything that is not a known entry, close or update is dropped;
- Confidence floor — entries below the configured minimum confidence are blocked, while exits and stop updates are deliberately exempt (you do not want a low-confidence signal keeping you in a losing trade);
- Quantity sanity — entries need a positive size;
- Leverage cap — anything above the configured maximum is rejected;
- Order type enforcement — with market entries configured, limit orders are refused rather than quietly resized;
- Max position notional —
quantity × entry pricehas to fit the USDC limit, re-priced from the exchange if the decision arrives without a price.
On the engine side the same decision already passed the configured pair (crypto_pair = BTC/USDC, 4h — the bot does not roam the market), a cooldown window, the shared R/R floor policy and the position cap. A spot SELL is capped to what is actually held, so a short can never be created by accident. Stop distances come from the regime risk profile (a tight 1.5× ATR in choppy markets, a wider 2× ATR in trends) instead of a fixed percentage.
If governance or risk validation cannot decide safely, the system fails closed: no order, no exception. Soft exits trigger at candle close; hard exits poll live prices every 15 minutes.
The verdict journal
A successful HTTP call is not proof that an order exists. Instead of assuming, the executor appends one line per processed decision to a JSONL journal — executed, blocked or error, with the reason, keyed by the bot's own order_id. The engine reads that file back to find out what actually happened to its order.
Friction is reported, not swallowed: if position size gets clamped or a decision is blocked, that shows up in the journal and on the dashboard. When a state genuinely cannot be determined, the dashboard says so instead of showing a green checkmark it cannot back up.
Where this actually stands (September 2026)
No overselling:
- The executor runs against an exchange testnet (
ENABLE_TESTNET=true, sandbox endpoints). It has placed test orders — the verdict journal has entries — and it has never touched real funds. - It was last exercised in August 2026. Since then the work went into the brain, the tests and the R/R policy, so the live path is the least-recently-certified part of the system.
- The part I trust least is reconciliation: proving that what the exchange holds matches what the journal claims. That is exactly what has to be boring and reliable before any real capital is considered — and it is not there yet.
- The executor is a separate local service and is not open-sourced yet; the engine is.
Capital stays simulated until the pipeline proves itself over a sustained period. That is a deliberate decision, not a limitation to hide: demo and live are different worlds, and a system should earn live trading the way it earns everything else — by surviving its own history.
The dashboard shows live position state and the audit trail behind every decision.
Live dashboard ↗GitHub ↗