diff --git a/README.md b/README.md index 6dcd17a..ac18044 100644 --- a/README.md +++ b/README.md @@ -6,648 +6,371 @@ real generated command centre is data/dashboard.html. # SpeedTrader AI -**An options trading agent where the AI can veto a trade but can never cause one.** +**An options trading agent where AI can challenge a trade, but deterministic +controls alone can authorize execution.** -Alpaca AI Trading Agents Hackathon · Paper trading only · 1122 tests +`Alpaca AI Trading Agents Hackathon` · `Paper trading only` · `Alpaca-native` · +`Options-first` · `Replayable` · `Auditable` ```bash pip install -e ".[dev]" python scripts/run_options_demo.py --replay --dashboard ``` -No API key. No network. No configuration. That runs the complete decision cycle, -re-derives every decision from its stored snapshot, and writes the command centre. - -**Full setup, live paper trading, and the strategy plugin system:** see -[`docs/SUBMISSION.md`](docs/SUBMISSION.md) for the complete engineering log — -what was built, what was rejected and why, and the exact commands to reproduce -every claim in this README. +No API key. No network. No configuration. That runs all six demo scenarios, +re-derives every decision from its stored snapshot, and writes the command +centre to `data/dashboard.html`. | | | |---|---| | **Team** | testerxma | -| **One-page write-up** | [`docs/ONE_PAGER.md`](docs/ONE_PAGER.md) — AI logic, risk gates, Alpaca infrastructure | -| **Engineering log** | [`docs/SUBMISSION.md`](docs/SUBMISSION.md) — what was built, what was rejected, and why | +| **One-page write-up** | [`docs/ONE_PAGER.md`](docs/ONE_PAGER.md) | +| **Engineering log** | [`docs/SUBMISSION.md`](docs/SUBMISSION.md) | | **Paper account** | `PA3FJIP2GIRB` · dedicated to this hackathon · $100,000 starting balance | | **License** | [MIT](LICENSE) | --- -## The idea +## Why SpeedTrader? -Most AI trading agents work like this: +Most AI trading agents propose first and rely on guardrails to catch mistakes +afterward. SpeedTrader inverts that order: -``` -LLM proposes a trade ──► guardrails try to catch bad ones ──► broker -``` +> **The AI does not create a trade.** A deterministic strategy produces the +> candidate, a deterministic risk engine sizes and approves it, and only then +> is a model consulted — to **confirm, abstain, or veto**. Never to originate, +> enlarge, or authorize. -The guardrails are the last line of defence, and the LLM's mistakes are the -thing they have to catch. SpeedTrader inverts it: +This is not a prompt-level convention. The AI's response is validated against a +JSON schema with `additionalProperties: false` and exactly four fields +(`verdict`, `confidence`, `reasoning`, `concerns`). A hostile model returning +`{"verdict":"CONFIRM","quantity":99999}` changes nothing, proven end-to-end by +test. ``` -S07 (ported quant strategy) - ──► deterministic risk engine decides IF and HOW MUCH - ──► options sizing converts risk budget to contracts - ──► AI adversarial review may only CONFIRM · ABSTAIN · VETO - ──► single-use execution licence - ──► Alpaca (paper) via its MCP server +AI MAY STOP a trade (advisory) +Deterministic MAY AUTHORIZE a trade (sole authority) +Broker EXECUTION OBSERVATION (never decides) ``` -By the time the model is consulted, the trade already exists and is already -approved. **The AI's entire vocabulary is three words**, and none of them can -enlarge a position, loosen a limit, change a strike, or authorize anything. - -This is not enforced by telling the model to behave — a prompt is a request, and -prompt injection is real. It is enforced by the **schema**: the review object has -four fields (`verdict`, `confidence`, `reasoning`, `concerns`) and -`additionalProperties: false`. A model returning `{"verdict": "CONFIRM", -"quantity": 99999}` changes nothing, and there is a test that proves exactly -that against the real pipeline. - -The practical consequence: **model capability is a reasoning decision, not a -safety one.** Swap in the strongest model available and its maximum authority is +The practical consequence: model capability is a reasoning decision, not a +safety one. Swap in the strongest model available and its maximum authority is still to cancel one trade. --- -## The failure mode is inverted, deliberately - -Because this layer can only *subtract*, "fail closed" here means **change -nothing** — not "block everything". +## What makes it different -| What happens | What the system does | +| | | |---|---| -| Model times out | ABSTAIN · the deterministic decision stands | -| Model returns malformed JSON | ABSTAIN · never guessed at | -| Model refuses | ABSTAIN | -| No API key at all | ABSTAIN · honest "no model was consulted" | -| Model says VETO with a reason | **Trade cancelled** | - -If an LLM outage vetoed instead, a vendor going down would silently halt all -trading. Mutation testing confirms it: `veto-on-failure` — making a model -failure veto instead of abstain — breaks 23 tests at this commit. +| **AI is subtractive** | Vocabulary is `CONFIRM · ABSTAIN · VETO`; it cannot resize, reprice, or authorize | +| **Exact maximum loss** | Long single-leg options only — max loss is the premium paid, known exactly at entry | +| **Deterministic risk authority** | 22 checks, all evaluated, never short-circuited; the sole source of execution authority | +| **Single-use authorization** | An HMAC licence bound by hash to the exact proposal and portfolio, non-serialisable | +| **Restart-safe execution** | Intent is fsynced to a write-ahead journal *before* the broker is contacted | +| **Reconciliation before retry** | `SUBMITTED` is never `FILLED`; `UNKNOWN` is resolved by querying the broker, never by resubmitting | +| **Deterministic replay** | A fingerprint that excludes the AI review by construction — the same market state hashes identically regardless of what the model said | --- -## Why long single-leg options - -Chosen for correctness, not convenience. - -> **Maximum loss = the premium paid, known exactly at entry, under every price path.** - -An equity stop-loss is an *intention* — price can gap through it overnight. A -long option cannot lose more than its debit. So the risk engine sizes against an -**exact** maximum loss rather than an assumed one: +## Architecture ``` -equities: shares = risk_money / stop_distance ← a price distance -options: contracts = risk_budget / (ask × 100) ← the entire amount at risk +Market Data + │ + ▼ +S07 Quant Signal deterministic + │ + ▼ +Options Selection + Sizing deterministic + │ + ▼ +AI Review advisory — CONFIRM / ABSTAIN / VETO + │ + ▼ +Deterministic Risk 22 checks — the sole execution authority + │ + ▼ +Single-use Authorization deterministic + │ + ▼ +Alpaca (paper) broker — observed, not decided + │ + ▼ +Reconciliation broker + │ + ▼ +Replay / Audit deterministic ``` -These are not the same formula with different names, and the options path never -reuses the equity one. Priced at the **ask**, never the mid — the mid understates -max loss by half the spread on every position. - -A bearish signal buys a **long put**, never a short call: risk stays defined in -both directions. +| Layer | Responsibility | Authority | +|---|---|---| +| Market / Data | Bars, quotes, account and portfolio state | data | +| Quant (S07) | Produces a candidate signal | deterministic | +| Options | Contract selection and sizing against risk budget | deterministic | +| AI Review | Bull / Bear / Judge — may only confirm, abstain, or veto | advisory | +| Risk | 22 checks evaluated in full; sole execution authority | deterministic | +| Authorization | Single-use, hash-bound execution licence | deterministic | +| Execution | Write-ahead intent, broker submission, reconciliation | broker (observed) | +| Replay / Audit | Fingerprint and replay of the deterministic decision | deterministic | + +No research-manager, technical-analyst, or sentiment-analyst agents exist in +this system. The dashboard reports absent stages as `NOT BUILT` rather than +implying they ran. --- -## What a judge can verify in 60 seconds +## 60-second demo ```bash -python scripts/run_options_demo.py # all six scenarios -python -m pytest -q # 1122 passed -python scripts/mutation_test.py # 78 mutants; every guard is load-bearing +pip install -e ".[dev]" +python scripts/run_options_demo.py --replay --dashboard ``` +Six scenarios, run offline against synthetic bars: + | Scenario | Demonstrates | |---|---| -| `breakout` | 3 contracts, **$960 max loss** inside a $1,000 budget | +| `breakout` | 3 contracts, $960 max loss inside a $1,000 budget | | `veto` | AI cancels an approved trade — nothing reaches the broker | | `no-signal` | S07 declines; the decision is still recorded | | `illiquid` | spread too wide → no tradeable contract | | `broke` | one contract costs $6,200 vs a $1,000 budget → rejected | -| `timeout` | broker times out → **UNKNOWN**, not retried, never "filled" | +| `timeout` | broker times out → `UNKNOWN`, not retried, never "filled" | -Then read one decision: +Inspect one recorded decision: ```bash cat data/decisions/decisions-*.jsonl | head -1 | python -m json.tool ``` -Every record carries the snapshot, the signal, **the cost assumptions behind its -EV**, all 22 deterministic checks, the contract chosen *and what was rejected and -why*, the AI review with the model that produced it, and the state the decision -ended in with the reason for it — including `UNKNOWN`, where an order may exist -and reconciliation, not a retry, is what resolves it. +## What the demo proves -### And against the real broker - -The demo above is deterministic and offline, which is what makes it a -*demonstration*. `run_live_paper.py` is the same pipeline with Alpaca supplying -the bars, the option chain, the quotes and the account balance: - -```bash -python scripts/run_live_paper.py --scan # decide across a watchlist -python scripts/run_live_paper.py --symbol SPY # one name, no order sent -python scripts/run_live_paper.py --symbol SPY --submit # actually place it -``` - -**The default sends nothing.** Without `--submit` the orchestrator is not given a -broker reference at all, so a dry run is structural rather than a flag someone -can forget. `--allow-stale` lets you watch the pipeline outside market hours and -is **refused together with `--submit`**: trading on a stale bar is the exact -thing the freshness gate exists to prevent. - -Running it is not a formality — three real defects in this repository were found -only this way, and each is now pinned by a mutant (`quote-batch-uncapped`, -`envelope-unbounded`, `lookback-calendar-units`). The third is the instructive -one: Alpaca's bar request is bounded by a wall-clock start time, but a strategy -asks in *bars*, and a calendar week holds 168 hours against ~32.5 market hours. -The window was computed in the wrong unit, so a request for the 891 hourly bars -an EMA200 needs returned 660 and the snapshot builder refused **every** hourly -symbol. It failed closed, correctly — and could never have failed any other way. -No fixture would have shown that, because a fixture returns the bars you wrote -into it. +| Proof | Evidence | +|---|---| +| The AI cannot originate a trade | schema has no field that could | +| A veto actually stops execution | `veto` scenario — nothing reaches the broker | +| Sizing respects the risk budget | `broke` scenario is rejected at $6,200 vs $1,000 | +| An ambiguous broker outcome is never claimed as a fill | `timeout` scenario ends `UNKNOWN`, not retried | +| Every decision — accepted or not — is recorded | one JSONL line per decision, `python -m json.tool` readable | +| The deterministic decision is reproducible | `--replay` re-derives it from the stored snapshot alone | --- -## The part competitors cannot do: replay +## Options intelligence -Every LLM trading agent shares one documented weakness, stated by its own -authors — model output is not reproducible, so a decision cannot be audited -afterwards. SpeedTrader's decisions *can* be re-derived, because the part that -decides is deterministic. +Long calls on a bullish signal, long puts on a bearish one — never a short +option, so risk stays defined in both directions. ``` -$ python scripts/run_options_demo.py --replay - - 315c42afbae355ad reproduced - 3cc211821bdfd44a reproduced (AI vetoed — the one thing it can change) - f85abf8442f2541e reproduced - - 6/6 decisions re-derived from their stored snapshot alone, with the AI - never consulted. +equities: shares = risk_money / stop_distance ← a price distance +options: contracts = risk_budget / (ask × 100) ← the entire amount at risk ``` -Six is what a fresh clone shows: the journal ships empty, this command writes -the six demo decisions and then replays them. The journal is append-only, so a -second run replays twelve — the count is every decision ever stored, not a fixed -figure. - -The 16-character fingerprint identifies **what the deterministic system -decided**, and it **excludes the AI review by construction**. So the same market -state hashes identically whether the model confirmed, abstained, timed out, -returned hostile output, or never ran. The project's central claim reduces to -comparing two strings — and a `CONFIRM` run and an `ABSTAIN` run producing the -same fingerprint is asserted end-to-end by test. - -A **veto** does change the outcome, so it is tracked *beside* the fingerprint -rather than folded into it. Merging them would destroy the ability to prove the -property above. - -If a replay diverges, the differing field is named. "Not reproducible" alone is -useless; an auditor needs to know which field moved. +Contracts are selected deterministically by moneyness and days-to-expiry, and +priced at the **ask**, never the mid. Max loss is shown all-in — premium plus +documented fees — and labelled `EXACT`. Max profit for a long option is +unbounded and is deliberately never printed as a number. --- -## Command centre +## AI governance -```bash -python scripts/build_dashboard.py --live # real paper account + journal -open data/dashboard.html ``` - -A single self-contained HTML file — no server, no build step, no network. It -renders the real pipeline: every figure comes from a persisted decision record, -from the Alpaca paper account, or from the runtime's own health object. - -It shows market state, the quantitative signal, the AI advisory lane, the -deterministic authority lane, the execution outcome lane, options contract and -sizing with **max loss labelled EXACT**, every deterministic check with its -observed value, rejected option candidates with the reason each was dropped, -the order lifecycle with intent phase beside broker state, and -**"why we did NOT trade"** attributing each declined decision to the layer that -stopped it. - -**There is no JavaScript on the page, and that is a security property rather -than a stylistic one.** The page renders text authored by a language model and -by a broker; with no `