Case Study: An LLM Trading Loop in Production
Since mid-2026 we have run a small automated intraday SPY options loop in production in which a frontier multimodal LLM (Claude Opus) makes the discretionary calls — whether to enter, in which direction, and how to manage the position — inside a lattice of mechanical guardrails that it cannot override. This page is an engineering case study of that system: the architecture, the data problems we did not expect, and the guardrails that exist because something real went wrong. It is not a performance claim, and single-account results would be statistically meaningless anyway — what transfers is the design.
Part of the Agentic AI module: Build an LLM Trading Agent (the from-scratch architecture guide) · LLM Trading Research Landscape · Multi-Agent Trading Architectures · Do LLM Trading Agents Actually Work? · Commercial Agentic Trading Products.
The System's Shape
The loop is cron-driven, not event-driven — a deliberately boring choice that makes every decision reproducible and auditable:
- A decide grid runs every 10 minutes through the morning session. Each tick renders a 1-minute chart locally (matplotlib), assembles a numeric packet, and asks the model for a fresh directional read. One position at a time; no read, no trade; if nothing qualifies by late morning, the day is skipped.
- A 1-minute watch slot does exits only: it re-checks stops, targets, and trailing levels against real-time marks, so the maximum slippage window between the mechanical layers is one minute.
- A 10-minute manage slot runs only while a position is open: the model re-reads the chart and position state and may HOLD, TIGHTEN the stop (up-only), TAKE_PROFIT, CUT, or EXTEND the target (up-only). It cannot widen a stop or lower a target — de-risking is the only discretion it has.
- An end-of-day slot enforces the overnight policy mechanically.
- All state-mutating slots share a file lock so no two ever race the ledger.
In live mode the broker (Tradier) holds a native OTOCO bracket server-side, so the resting stop and target survive even if our box dies; the loop's job shrinks to trailing, time-based exits, and reconciliation.
Bounded Discretion: the Model Proposes, Code Disposes
The single most important design decision: the LLM never has authority, only influence.
- Its entry read returns a direction, a high-probability flag, a confidence, and suggested stop/target percentages — which are clamped to hard bands before they touch an order. Garbage or omitted values fall back to fixed defaults.
- Its packet is numbers it cannot fabricate: session VWAP, opening-range levels, time-based momentum, pre-market highs/lows, the ATM straddle's implied expected move, VIX, and a macro-event calendar (a high-impact release inside a blackout window hard-skips entry, no matter what the model thinks).
- Position sizing, per-trade risk caps (a fixed percentage of equity), contract selection, and position-count limits are pure code. The model is never asked “how much.”
- Underneath its discretion sit mechanical layers it cannot touch: hard stop/target, a profit-scaled trailing ratchet, an intraday time stop, and the end-of-day flatten.
The Tape Is the Weakest Link
Nothing in this project consumed more debugging than data quality, and every incident produced a defense that is now permanent:
- Delayed data corrupts everything downstream. Our first data source was ~15 minutes delayed (a sandbox feed). Marks, exits, and the model's chart were all quietly wrong. The fix was structural: real-time production market data even while the execution side was still simulated — and the discovery that this broker serves real-time quotes on unfunded accounts.
- A trickle feed with missing minutes blinded a read. Bars sourced from a lightweight collector had ~50% of minutes present; a breakdown happened inside a hole and the packet told the model the tape was flat. Fixes: a primary institutional-grade bars endpoint, time-based (not bar-count-based) indicator windows, and a hard freshness gate — a stale packet skips the tick and says so, rather than deciding on old data.
- Load-balanced endpoints can serve you the past. For a stretch, identical requests to the bars endpoint alternated roughly 50/50 between fresh data and a snapshot frozen half an hour old. The retry that fixed it varies the request window slightly as a cache-buster and keeps the freshest response. If your agent polls REST endpoints, staleness detection is not optional hygiene — it is the difference between a decision and a hallucination with extra steps.
Guardrails With a Birth Certificate
Every mechanical veto in the entry path exists because of a specific, logged failure — none were designed from imagination:
- Climax-chase veto. The loop once bought the ask one minute after a climactic volume bar (an order of magnitude above baseline) whose spike top held as the session high. Now a with-direction climax bar inside a short lookback vetoes the tick; the next grid tick re-reads — which is the wait-for-the-retest behavior, with no state to manage.
- Stop-geometry check. On a cheap, low-delta contract, a premium-percentage stop can map to an underlying move smaller than ordinary retest noise — the entire configured stop band can sit inside the market's wiggle. The guard converts the premium stop to its underlying-equivalent and requires it to clear the nearest structural level (VWAP, or a broken level between entry and VWAP) plus a buffer; it widens the stop to the minimum that survives, or vetoes the entry as too extended.
- Armed level-triggers, not standing orders. When the model's only objection is “the level is too close,” it can arm a level instead of entering. The 1-minute watch then fires one full decide tick — model, guards, and all — on a confirmed close through that level. It is a re-read trigger, never an auto-entry, and every arm, trigger, and rejection is a ledger event.
Falsifiability: Score Every Read, Not Every Trade
A system that trades a few times a week cannot be evaluated on its trades for months. The loop therefore grades every logged directional read — roughly forty per week — against what the underlying actually did 15, 30, and 60 minutes later: hit rate, signed edge, and calibration of the high-probability flag, computed nightly from the ledger.
This also disciplines the prompt itself. A context input earns a slot in the packet only if it is mechanical, scalp-timeframe-relevant, and scoreable after the fact; anything else is rationalization fuel for a language model that is very good at rationalizing. Several plausible-sounding inputs have been rejected under that razor, and at least one untested heuristic the model exhibits (refusing entries late in a trend as “exhausted”) is on the docket to be graded — not assumed — from the same logs.
Reconciliation, or: Live Trading Is a Distributed-Systems Problem
The paper-to-live transition surfaced a class of bugs that backtests cannot show, all of the flavor “two systems disagree about reality”:
- A fill can land after your cancel decision but before the cancel executes. The loop re-polls after every cancel and adopts late fills into the ledger instead of orphaning a real position.
- “The API returned nothing” and “you have no positions” are different facts. Position reads distinguish errors from emptiness, and a failed read aborts the tick rather than letting the loop conclude it is flat and book phantom closes.
- Before market-closing a bracketed position, the loop verifies the resting legs are actually dead — a close racing a live stop leg can double-fire.
- Once a day, the ledger's cash and equity are cross-checked against the broker's, and a broker position the ledger doesn't know about pages a human immediately.
What We'd Tell You to Steal
- Bounded discretion. Give the model a vote, never a veto over your risk layer. Clamp everything it suggests.
- Treat staleness as the default state of market data, and make every decision path prove its inputs are fresh.
- Log for falsifiability from day one. Counterfactually score every read; make every guard's rejection a ledger event. You will tune on evidence instead of vibes.
- Let guardrails be post-mortems, not speculation. Ship the minimal loop, then convert each real failure into exactly one mechanical rule.
- Do the live-execution engineering. Fill races, partial fills, cancel verification, and reconciliation are where paper systems die in production.
Frequently Asked Questions
- Does the LLM place trades directly?
- No. It returns a structured opinion; deterministic code applies clamps, vetoes, sizing, and risk caps before any order exists, and mechanical stops sit beneath everything it does.
- Why an LLM at all instead of pure rules?
- The multimodal read (chart + numeric packet) handles regime nuance that brittle rules encode poorly -- but only inside guardrails, and only because every read is counterfactually scored so its edge is measurable rather than assumed.
- What was the hardest engineering problem?
- Data integrity: delayed feeds, missing minutes, and load-balanced endpoints serving stale snapshots. Staleness detection and freshness gates mattered more than any prompt change.
- Is this a recommendation to run one?
- No. It is an engineering case study; short-dated options are a fast way to lose money, and nothing here is investment advice.
Build the same bounded-discretion pattern yourself on a paper account, or see what independent live benchmarks found when they tested agents like these.
Build Your Own → Do They Actually Work? →