Ask how an AI trading agent fails and most answers gesture at “hallucination” and move on. That is not an engineering answer. An agent with order access fails in specific, mechanical, reproducible ways, the same ways distributed systems fail, plus a few the model contributes, and each one can be named, provoked on a simulation account, and caught by a specific control. This piece is that catalog. The companion piece, risk controls an LLM cannot override, walks the controls first; this one walks the failures first, because that is the direction you actually experience them in. Everything below describes how PitBridge is really built, in the open core you can read, and none of it makes trading safe or profitable. It makes specific failures non-fatal.
The catalog at a glance
| Failure mode | What catches it | Reason code or mechanism |
|---|---|---|
| Wrong size, side, or instrument | schema check, allowlist, position caps | INVALID_QTY, INSTRUMENT_NOT_ALLOWED, MAX_CONTRACTS_PER_ORDER, MAX_POSITION |
| Duplicate order on retry | duplicate window, then idempotent client_order_id | DUPLICATE_ORDER; duplicate ids refused, duplicate events no-op |
| Deciding on stale data | link and feed freshness guards, fail closed | LINK_DOWN, STALE_ACCOUNT_STATE |
| Runaway order loop | rate limits and cooldowns | MAX_ORDERS_PER_MINUTE / _HOUR / _DAY, COOLDOWN_AFTER_LOSS, COOLDOWN_AFTER_ORDER |
| Prompt injection via market text | deterministic engine outside the model, absent tools | no tool can lift a limit, release the kill, or arm live |
| Position drift after reconnect | broker-truth reconcile, in-flight-aware caps, persistent ledger | position_mismatch audit entries, CANCELLED on absence |
| All of the above at once | kill switch with reduce-only carve-out | KILL_SWITCH, release is human-only |
Two properties hold for every row. The control runs in the daemon, a separate process the model has no path into, so no output the model produces can route around it. And every decision, allow or block, lands in an append-only, hash-chained audit log, so each failure mode below is also inspectable after the fact with pitbridge audit why <order_id>.
Wrong size, wrong side, wrong instrument
The base failure. A language model produces a distribution of outputs, and the tail of that distribution includes qty=10 where you meant 1, SELL where it read BUY, or NQ when your plan says MES. No malice required, just one bad sample at the wrong moment.
The catch is layered before any account state is consulted. The schema stage rejects a malformed request outright, including a zero or negative quantity, which matters more than it looks: a negative BUY is a disguised SELL that would sail past a positive per-order cap if it were allowed to exist. The instrument_allowlist refuses any contract you did not explicitly list. The per-order cap blocks a single oversized order with MAX_CONTRACTS_PER_ORDER, and max_position blocks any order whose projected net position would breach your cap. All of it is deterministic: the same order and the same config produce the same decision every time, with no model anywhere in the adjudication. The full set of twelve controls and their frozen evaluation order is in the guardrails piece; this article stays on the failure anatomy.
Duplicate orders on retry
The classic. The agent, or the client library under it, submits an order and hears nothing back inside its timeout. It concludes failure and retries. But the first order was not dead, it was slow, and now two live orders express one intent. Retry-on-ambiguity is correct behavior for idempotent APIs and catastrophic for order entry, and an LLM agent makes it worse: models are literally trained to try again when something looks like it failed.
PitBridge treats this as two failures needing two lines of defense at different depths. The first is the duplicate_protection guardrail: a repeat of the same instrument, side, and quantity on the same account inside duplicate_window_s seconds blocks with DUPLICATE_ORDER. That catches the plain double-submit before it exists.
place_order account=sim instrument="MES 09-26" side=BUY qty=2 -> SUBMITTED, then FILLED filled_qty=2
# the agent's client times out waiting and replays the same intent
place_order account=sim instrument="MES 09-26" side=BUY qty=2 -> BLOCKED reason_code=DUPLICATE_ORDERReal tool name and reason code; the window is duplicate_window_s in your config (the scaffolded config uses 30 seconds). Fills are synthetic simulation fills.
The second line is idempotency at the order’s identity. Every order intent gets a daemon-generated client_order_id, a UUID that becomes the NinjaTrader Order.Name and serves as the single idempotency key for the order’s whole lifecycle. The executor refuses a duplicate id outright. Inbound broker events are applied idempotently: a duplicate delivery of the same order update is a no-op, a fill is deduplicated by its execution id, and an order that reached a terminal state is never resurrected by a late or repeated frame.
The subtle case is the timeout itself. If no broker ack arrives within the ack window (10 seconds by default), the order is marked TIMED_OUT, and that status is deliberately not treated as final. A timeout means we did not hear an ack, not that the order died; it may be live at the broker. So a TIMED_OUT order stays eligible for reconciliation, and the next snapshot either adopts it in its true broker state or confirms it is gone. The honest engineering move is to keep the ambiguity visible and resolve it against broker truth, not to guess.
Deciding on stale data
An agent reads day_pnl: 0.0 and sizes up. The number was true four minutes ago; the feed died, the loss since then is real, and every control keyed on P&L is now reasoning about a world that no longer exists. Stale data does not look like a failure. That is what makes it one.
The control is freshness enforcement that fails closed, in two layers. Heartbeats run every 5 seconds in both directions on the AddOn link, and after 15 seconds of silence the link is DOWN: every order hard-rejects with LINK_DOWN, because queueing orders against a dead link is how you end up exposed when you thought you were covered. Subtler: the link can be up, heartbeats flowing, while a single account’s account_state feed, the source of its unrealized P&L, has gone silent. Past account_state_staleness_s (default 60 seconds, floor 5) new entries block with STALE_ACCOUNT_STATE, so the daily loss halt and profit lock fail closed instead of deciding on frozen numbers. De-risking with cancel, close, and flatten stays allowed in both cases. And after any reconnect, the link is not considered fresh until the first account_state actually arrives for the account; a snapshot alone does not unfreeze order flow. The 0.2.1 changelog entry records why this guard was added: the failure was found by review, not by a blown account, which is the cheap time to find it.
Runaway loops
Agents run loops: plan, act, observe, repeat. A loop with a bad termination condition, a strategy that re-fires on every tick of the same signal, or a model that keeps concluding “that did not work, try again” produces order flow no human would. This is the failure that turns one mistake into forty.
The controls are rate and rhythm caps that no loop can reason its way past. max_orders caps order count per minute and per hour on rolling windows, and per day on the ET calendar date, the same day boundary the ledger uses. cooldown_after_order_s enforces minimum spacing between orders; cooldown_after_loss_s blocks the revenge-trade rhythm by enforcing a pause after any losing execution. Each returns its own reason code (MAX_ORDERS_PER_MINUTE, COOLDOWN_AFTER_LOSS, and kin), so a blocked loop is also a legible loop: the agent reads why it stopped instead of interpreting the block as one more failure to retry. And if a loop finds a rhythm that fits inside all of those caps, the kill switch stops everything at once, from outside the loop.
Prompt injection through market text
A trading agent reads text as part of the job: headlines, economic calendar notes, news summaries, a research file you gave it. Any of that can carry adversarial instructions, and the model has no reliable way to separate untrusted text from your intent, because both travel the same channel. This is not the coding-agent edge case where a poisoned README is a curiosity; here the untrusted text arrives through the data pipeline you built on purpose, at market speed. Prompt hygiene helps and is not a control: every prompt-level defense is probabilistic, and the run where injected text wins is the run that counts.
The control is structural. Whatever the model was talked into proposing, the proposal lands in a deterministic engine in a separate process, checked against limits from a config file the agent has no tool to edit. And the tool surface is the stronger half of the argument: the agent’s nine tools contain no function to raise a limit, release the kill switch, or arm live trading, so the worst case of a fully hijacked session is a proposal that still has to clear every check. The blast radius is a BLOCKED result and an audit entry. How that absence-of-capability design works end to end is covered in risk controls an LLM cannot override.
Losing track of the position after a disconnect
The deepest failure on the list, and the one bot frameworks most often get wrong. A bot that builds its position view purely from the events it observes is running dead reckoning: every dropped frame is silent drift between its book and reality. Disconnect for thirty seconds during a fill, and the bot believes it is flat while holding two contracts. Restart the process mid-session and the day’s realized loss resets to zero, which quietly disarms the daily loss halt. The agent then acts, confidently, on fiction.
PitBridge’s rule is that the broker is the source of truth and the daemon’s book is a cache. On every connect and reconnect, the daemon requests a snapshot and adopts the broker’s positions over its own book, flattening any instrument the book thought it held that the snapshot omits. Every divergence is written to the audit log as a position_mismatch entry with both quantities, so drift is loud instead of silent. In-flight orders are reconciled against the broker’s working orders: an order the broker knows about is adopted in its broker state, and an order the broker does not know about is marked CANCELLED with the reason recorded. Nothing is ever auto-resubmitted, because a resubmit is a guess, and snapshots are applied per account, so one account’s snapshot can never cancel another account’s in-flight orders.
{"type":"position_mismatch","account":"Sim101","instrument":"MES 09-26","book_qty":3,"broker_qty":2}
{"type":"reconcile","account":"Sim101","changes":{"ord_7f3a":"CANCELLED"}}Real entry types and fields as the daemon writes them, abridged from the full hash-chained record. The mismatch is booked and visible, never silently corrected.
Two more pieces close the loop. The max_position cap is in-flight-aware: it counts the unfilled remainder of the account’s own working and pending orders, signed per instrument, alongside the filled position, so two orders that each fit the cap cannot sum past it by racing each other, and a reducing order is netted rather than double-counted into a false block. And the day ledger, the day’s realized P&L, the sticky halt latch, and the set of booked execution ids, persists to disk and is restored on startup for the current trading date, so a mid-session restart no longer forgets the day’s losses; a prior day’s ledger rolls over and is never resurrected. Both hardenings are recorded plainly in the 0.2.1 and 0.2.2 changelog entries, including what was wrong before them.
The backstop: one switch that stops all of it
Every control above targets one failure class. The kill switch targets the situation where you have stopped caring which class this is. Engaged, it blocks every new entry with KILL_SWITCH, checked first in the chain, before any other logic runs. It is file-backed, so it survives a daemon restart and can be engaged with a bare file touch even if the daemon is wedged. Cancel, close, and flatten keep working under it, so stopping new risk never traps you in existing risk. And release is a human act at the CLI, deliberately absent from the agent’s tools and from the REST surface. What that switch must guarantee to deserve the name has its own deep dive.
What this catalog does not claim
Honesty about scope is part of the engineering. Each control above reduces one named failure class: duplicates, stale-data decisions, runaway loops, oversized or off-plan orders, silent position drift. Here is what none of them do. They do not evaluate whether a trade is wise. They do not predict markets, and an order that passes every check can lose money, because being inside your limits and being right are different properties. They do not run unless you configure them: a guardrail with no value set does not check anything, and the values are your judgment, not ours. And the catalog is not finished. The in-flight race and the stale-feed hole were found by review after v0 shipped and closed in 0.2.1; the restart amnesia in 0.2.2. There will be a next one, which is exactly why every decision is audited and the changelog names what was wrong.
Honest status: paper by default, live gated
Everything in this piece runs against paper and simulation accounts today. In the open core mode = "live" is intentionally out of reach: arm-live refuses, because live execution ships separately as a paid, closed component, and there is no agent tool or API route to change that. Live execution has been in production on a funded futures account since 30 July 2026, behind an operator-run arm step. For an engineer evaluating the failure modes above, that is the useful part: every reason code and every reconcile path in this article can be provoked and observed on a Mac with the bundled fake AddOn, with nothing at risk, before any real account is in scope.
If you are building an agent with order access, the trading AI guardrails pillar covers why these controls must live outside the model, the guardrails page lists the full set with reason codes, and the security model shows where each part runs. If you want this failure catalog standing between your agent and your account, tell us your platform on the waitlist. PitBridge is trading infrastructure, not financial advice: it enforces the limits you configure and does not promise any trading outcome. Futures trading carries a substantial risk of loss.