Architecture - kingabzpro/BetterTradingAgents GitHub Wiki
How the system is built. The README covers what a user sees and Usage how to drive it; this page explains the overall workflow and the interesting bits inside each feature.
One FastAPI process runs on your machine: it serves a vanilla HTML/CSS/JS frontend (no build step), the REST API, the SSE streams, and the background autopilot loop. SQLite is the only local storage; the paper account lives at Alpaca and is read fresh on every request. Any OpenAI-compatible LLM works, role by role: a cheap fast model for the researchers, a stronger one for the manager. The page shell is server-rendered from one template (app/pages.py fills the shared navigation), cross-page behavior lives in static/js/ui.js, and scripts/check_ui.py keeps helpers, nav markers, and design tokens from drifting; assets carry no version stamps because no-cache plus ETag revalidates everything.
flowchart LR
T["📈 Tickers<br/>up to five"] --> D["📡 Market data"]
D --> S["🔬 Researchers in parallel<br/>market · technical · fundamental · news · sentiment · forecast"]
S --> B["⚔️ Bull vs bear debate<br/>+ judge at Max · skipped at Fast"]
B --> PM["👔 Portfolio Manager"]
TR["📜 Every call graded vs SPY"] -.-> PM
PA["🏦 Paper account"] -.-> PM
PM --> RG["🛡️ Risk gate<br/>sizing · caps vs paper equity · forecast check"]
PA -.-> RG
RG --> OUT["✅ BUY · HOLD · SELL"]
OUT --> C["💬 Manager chat"]
OUT --> O["🧾 Paper order"]
What happens when you hit Analyze Stocks:
-
Run created. The API validates the tickers, persists a run row, and spins a coordinator that fans out events. Every state change is persisted, so History reopens anything and a server restart settles interrupted runs honestly instead of leaving them running forever; every run also carries a wall-clock deadline (
RUN_TIMEOUT_MINUTES, default 30), so a hung provider call fails the run instead of leaving it "running", and a finished run leaves memory after a short reconnect grace once its row is saved. SSE replays iterate a snapshot of the event list, so reconnecting tabs never see an event twice. Incognito runs are the deliberate exception: nothing is persisted at all. They skip the portfolio fetch, decision-memory reads and writes, past-run comparisons, and the shared analysis cache, live only in memory, are never attributed to a client id, and are dropped when their page closes (a beacon hits the discard endpoint); the manager prompt says the portfolio is "NOT SHARED" rather than "UNAVAILABLE", and server logs never record which ticker an incognito run analyzed. - Market data gathered. yfinance pulls six months of prices, Finnhub adds fundamentals and company news, Olostep runs the sentiment search, Nixtla supplies the 5-day forecast. Each has a built-in fallback, and the result card reports who answered and how fresh the data is.
- Researchers work in parallel. Six analysts exist (market regime, technical, fundamental, news, sentiment, forecast), and each tier picks its roster: the market analyst reads the S&P 500, Nasdaq, VIX and the 10-year yield so every call knows the weather it trades in. Each analyst is a single LLM call over deterministic, precomputed indicator data, with one classified retry per failure: 429/5xx waits and retries, a rejected JSON mode drops to plain, a rejected stream drops streaming, and malformed output is retried once with the validation error fed back. One agent failing never kills a run: its slot says so, and downstream agents are told the input is missing.
- Bull and bear debate (Pro and Max). Each side writes the strongest case from the same research. At Max, a neutral judge then cross-examines both cases against the data and returns calibrated strengths plus a verdict naming what survived; it replaces the old self-scored rebuttal round. Fast skips the debate entirely and the manager weighs the research directly.
| Tier | Agents | Roster |
|---|---|---|
| Fast | 3 | Technical + Forecast + Manager: price action and projection weighed directly, no debate, no news fetch |
| Pro | 7 | Market + Technical + News + Forecast researchers, then Bull/Bear and the Manager |
| Max | 10 | All six researchers, plus Bull/Bear, the Judge, and the Manager |
Old "medium" and "expert" values normalize to Pro and Max everywhere, so saved runs and old links keep working. Old runs without the judge or market rows show them as "Not recorded". 5. The manager decides. It weighs the debate, the current paper holdings, and the graded track record of past calls, and emits a pre-gate BUY/HOLD/SELL with confidence, suggested size, and change conditions. 6. The risk gate owns the money. Deterministic code scales the position by volatility and caps it against the paper account's per-ticker, invested, and cash limits; a forecast beyond the stock's own noise band downgrades the trade. Overrides render as "Manager: BUY -> Final: HOLD", never silently. 7. Execution, paper only. A BUY/SELL can become a paper order through the explicit review step, then the guarded Alpaca paper path with idempotent order ids, per-order and daily caps, and freshness checks. Everything lands in the paper account, which is the single holdings record. 8. Live and durable. SSE streams every agent state to the run view, completed runs are saved to SQLite with per-ticker chat available, and the per-run cost estimate is computed from real token usage.
A background asyncio loop runs whole sessions on a schedule: discover candidates (the momentum screen behind "I Am Feeling Lucky"), add every held ticker so SELL decisions can act, run the normal pipeline as one ordinary run, then submit the trades it agrees with through the same guarded broker path a human click uses. A session holds a real lock taken before any network call, so a schedule tick and a manual "Run now" can never trade at once; BUYs need a confidence bar, re-check positions at order time, and are never added to an existing position; the loop survives crashes, cancels sessions that run too long, and records every session with its decisions, orders, and errors. Candidate selection carries a memory: names analyzed in the last AUTOMATION_CANDIDATE_COOLDOWN_DAYS days are held out of the shortlist (backfilled from the ranking only if the fresh pool runs short), and held positions lead the session ticker list so the session cap can never drop an open position's sell-or-hold check.
Paper trading assumes the local order book and Alpaca agree, so they are checked: a sweep at startup (before autopilot may submit anything) and every 15 minutes refreshes every non-terminal local order against Alpaca by client order id, settling rows a crash left pending or unknown and pulling partial fills into the row. The same sweep diffs Alpaca's open orders against the local set; an order this app did not place is drift, shown as an out-of-sync banner on the portfolio page. Broker reads retry rate limits and 5xx with a short backoff; auth and validation errors fail immediately, and the order POST path retries nothing blindly.
A single field spec drives the Settings page and API: 53 settings, 10 of them secrets. Non-secret overrides live in SQLite; secrets never do: they go to the OS credential vault (Windows Credential Manager, macOS Keychain, Linux Secret Service). Precedence is keychain/DB override over .env default, values apply live with no restart, the API never returns a secret value, and Reset restores the .env defaults. Without an OS keychain, secrets fall back to the local database unencrypted and the UI says so.
The server binds to 127.0.0.1 only, and middleware closes the browser drive-by vectors that remain: non-loopback Host headers (DNS rebinding) are rejected, and any request whose Origin names a different site is rejected. Same-origin fetches, navigation, and plain curl are unaffected.
The Trades page polls the autopilot status while open; when a session reaches completed or failed, the page raises an opt-in desktop Notification with a short summary. Dedupe state lives in localStorage, enabling stamps what already finished so nothing replays, and skipped sessions stay silent.
Each analysis includes a five-trading-day price forecast from a hosted chain: Nixtla TimeGPT while its quota allows, then TSFM.ai Chronos-2, then a local 60-day log-linear trend; the cards and the run footer say which model answered (forecast_method). The app sends up to 512 historical daily closes and reports the predicted close at the run's outlook horizon (next session for day trades, five trading days for short term, about a month for long term). A dedicated analyst weighs the projection against the same ±1σ five-day noise band the risk gate uses - the band is precomputed into its payload, so the annualized volatility figure is never mistaken for a five-day band. Projections are supporting evidence, never a target. Backtests blank both hosted keys and grade the trend model.
The Sentiment Analyst runs a site-restricted search over Reddit and StockTwits and weighs the crowd skeptically. Honesty about thin data is built in: fewer than three posts reads as neutral with low confidence, never as a signal, and results link the underlying threads. Backtests have no point-in-time social archive, so replays run with the honest neutral instead of leaking present-day chatter into past dates.
Every completed run records its call plus calibration provenance. Past decisions are graded once their window closes (default 21 days) on realized return and alpha vs SPY; on the next run of the same ticker the manager sees the most recent graded calls and cross-ticker lessons, instructed to treat them as weak evidence. A scheduled settle pass grades every due decision on its own clock, so a call on a ticker never analyzed again still reaches the track record and the calibration. Lessons are deterministic sentences by default, LLM-written reflections are opt-in.
Deterministic code owns money and limits; LLMs summarize evidence. Every BUY is sized by volatility, then capped by max-position, max-invested, and min-cash fractions; a forecast beyond the stock's own plus-or-minus one sigma noise band (scaled to the run's outlook horizon) downgrades the trade. Downgrades are flagged with the exact reason. The caps read the Alpaca paper account (cash, equity, open positions), so a BUY is checked against what you actually hold; when the broker cannot be reached, the BUY still surfaces but carries an exposure caps skipped: portfolio unavailable flag instead of a fake pass.
The backend screens U.S.-listed companies (market cap 1B-50B dollars, real revenue with growth, liquid shares), then ranks candidates with a momentum score grounded in the published literature: 3-6 month formation momentum skipping the most recent month (Jegadeesh and Titman 1993), path smoothness (Da, Gurun and Warachka 2014), 52-week-high proximity (George and Hwang 2004), and volatility scaling (Barroso and Santa-Clara 2015). The selected outlook changes the formation weights; the screen is cached for an hour.
Every result carries a cost estimate computed from per-role token usage against a dated list-price table, with overrides for custom endpoints. An unpriced model reports a floor or "cost unknown", never a confident wrong number. Mock mode records no tokens and shows nothing.