Multi-LLM algorithmic paper-trading system for US equities. Runs on a cron schedule during market hours, analyzes ~50 large-cap S&P 500 names with an ensemble of language models, and places paper orders through Alpaca.
Paper-only. All trading uses Alpaca's paper endpoint (
paper=True). No real money is at risk. No profitability is claimed — this is a research scaffold.
Each run (every 30 min, 9:30–16:00 ET, weekdays) executes a pipeline per ticker batch:
- Market data + news — price history via yfinance, headlines via Alpaca news API.
- Analyst (Gemini) — BUY/SELL/HOLD recommendation with confidence + reasoning.
Walks a model-priority chain (
gemini-3.5-flash→gemini-3.1-pro→ … →gemini-2.5-flash) so it degrades gracefully when a model is quota-gated. - Sentiment (Hugging Face) — BULLISH/BEARISH/NEUTRAL second opinion. It only blocks a trade when it directly contradicts the analyst (BUY vs BEARISH, or SELL vs BULLISH); NEUTRAL (quiet/empty news) does not veto.
- Risk (Claude) — final gate; vetoes unsafe trades. Uses Sonnet via the Claude Agent SDK — drawing on your Claude Pro plan's included usage — for the crucial exit decisions, and the Haiku API for the high-volume buy screen; falls back to Haiku when no subscription token is configured.
- Execution — Alpaca paper order; a hard stop-loss floor and a profit-protecting trailing stop are enforced independently of the LLMs.
- Exit analysis — open positions are re-evaluated by a separate Gemini API
exit analyst + a Claude exit-risk gate. (Google retired the individual-tier
gemini-clion 2026-06-18; that path is off by default — setUSE_GEMINI_EXIT_CLI=1only with a paid-key-backed CLI.)
A prompt rule forbids the analyst from inventing news when no headlines are supplied — absence of data must be stated, not hallucinated.
| Rail | Default | Env override |
|---|---|---|
| Hard stop-loss | 5% | STOP_LOSS_PCT |
| Trailing stop (pullback from peak) | 3.5% | TRAIL_STOP_PCT |
| Trailing-stop activation (gain before it arms) | 3% | TRAIL_ACTIVATE_PCT |
| Position size | 2% of equity | POSITION_SIZE_PCT |
| Max concurrent positions | 8 | MAX_POSITIONS |
| Tickers per batch | 10 | TICKER_BATCH_SIZE |
The trailing stop arms only after a position's running peak gains
TRAIL_ACTIVATE_PCT above entry, then exits on a TRAIL_STOP_PCT pullback from
that peak — locking in gains on winners while leaving the hard floor to govern
names that never ran up. Peaks persist in trade_logs/position_peaks.json and are
sampled each run.
pip install -r requirements.txt
cp .env.example ~/.env # fill in your keysKeys required in ~/.env: ALPACA_API_KEY, ALPACA_SECRET_KEY, GEMINI_API_KEY,
ANTHROPIC_API_KEY, HF_API_TOKEN. See .env.example.
Optional — Claude Sonnet via subscription. To run the risk/exit gate on Claude
Sonnet through the Claude Agent SDK — drawing on your Claude Pro plan's
included usage rather than metered Haiku API tokens — add CLAUDE_CODE_OAUTH_TOKEN
(from claude setup-token). Tunables: CLAUDE_SDK_FOR (exits [default] | all |
none) and CLAUDE_SDK_MODEL (default sonnet). With no token the bot runs
Haiku-only, exactly as before.
python3 trader.pyThe script enforces its own 9:30–16:00 ET market-hours window (and skips holidays via Alpaca's clock endpoint), so it exits early when the market is closed.
*/30 9-15 * * 1-5 flock -n /tmp/trader.lock /usr/bin/python3 /path/to/trader.py >> /path/to/trade_logs/cron.log 2>&1flock -n keeps overlapping runs from stacking up if one is slow. Adjust the hour
range to your server's timezone — the bot self-enforces the ET window regardless.
A small Flask app in dashboard/ shows positions, decisions, and history. The
overview page charts portfolio value over time with a high-water-mark line and
drawdown shading.
python3 dashboard/app.pytrader.py # main pipeline (data → analyst → sentiment → risk → execute → exit)
requirements.txt
dashboard/
app.py # Flask dashboard
templates/ # overview, positions, decisions, history
.env.example # credential template (real keys live in ~/.env, never committed)