Skip to content

Trading Bot: Build Plan

Author: Claude Code (Opus 4.7, trading-bot-architect) Date: 2026-04-21 Status: DRAFT FOR JUSTIN'S REVIEW, no code to ship, no accounts to open Repo path (target): /home/justinwieb/forge/tradingbot/


1. Executive Summary

  • What we build. A paper-first, autonomous equities trading bot that runs on UDev, uses Alpaca as broker, persists state in SQLite, and integrates into the existing Forge fleet (n8n triggers, task queue for Claude analysis, notify.sh for alerts, dashboard for status). Two brains: a deterministic Python execution engine (places and manages orders, enforces risk) and a Claude API analyst (produces structured signal proposals that feed a queue, never orders directly).
  • How Claude plugs in. As a tiered advisor: Opus 4.7 for daily regime analysis, Sonnet 4.6 for intraday signal refinement, Haiku 4.5 for headline classification. All outputs are JSON signals → policy filter → execution engine. The LLM cannot place an order. Projected cost: $80–$180/mo in full operation.
  • Guardrails. Hardcoded risk limits (max position, max daily loss, max drawdown), a file-based kill switch readable by the execution loop on every tick, ntfy and Home Assistant halt buttons, circuit breakers that flatten positions on breach, and a paper → shadow → live gating process with concrete pass/fail metrics. Trading capital sits in a dedicated Alpaca account; personal checking is not linked.
  • What Justin decides. Asset class (recommend Alpaca equities), initial live capital (recommend $5k), entity wrapper (personal or Wiebelhaus LLC), acceptable monthly Claude budget, tax-loss tolerance. Eleven numbered decisions at the bottom of this doc.
  • Expected timeline. ~4 months from greenlight to first live dollar. Phase 0–2 (paper + Claude integration): 8 weeks. Phase 3 (shadow, real tiny money): 4 weeks min. Phase 4 (live, small capital): 4 weeks min. Phase 5 (scale): month 5+.

2. Architecture Diagram

                              ┌──────────────────────────────────┐
                              │         FORGE FLEET              │
                              │ (existing infra — reuse, don't   │
                              │  rebuild)                        │
                              └──────────────────────────────────┘

                    ┌──────────────────────────────┐
                    │  n8n (192.168.86.82:5678)    │
                    │  cron triggers, webhooks     │
                    │  broker webhook receiver     │
                    │  credential vault (encrypted)│
                    └──────┬───────────────────────┘
                           │  SSH → task-creator.sh (for Claude analysis tasks)
  ┌────────────────────────────────────────────────────────────────────┐
  │  UDev (192.168.86.50) — bot host                                   │
  │                                                                    │
  │   ┌──────────────────┐       ┌────────────────────────────────┐    │
  │   │   ANALYST        │       │       EXECUTION ENGINE         │    │
  │   │  (Claude API)    │       │   (deterministic Python)       │    │
  │   │                  │       │                                │    │
  │   │ Opus 4.7 @ 8:30a │       │  Event loop (30s tick)         │    │
  │   │ regime analysis  │       │  ├─ read signal queue          │    │
  │   │                  │──────▶│  ├─ apply policy filter         │    │
  │   │ Sonnet 4.6 @     │signal │  │   - risk caps                │    │
  │   │ :00/:15/:30/:45  │ JSON  │  │   - kill-switch check        │    │
  │   │ intraday signals │       │  │   - dedupe vs open positions │    │
  │   │                  │       │  │   - cooling-off timer        │    │
  │   │ Haiku 4.5 ad-hoc │       │  ├─ size position (Kelly/fixed) │    │
  │   │ headline class   │       │  ├─ submit order → Alpaca API   │    │
  │   │                  │       │  ├─ reconcile fills             │    │
  │   └──────────────────┘       │  └─ log everything to SQLite    │    │
  │           ▲                   └───────┬────────────────────────┘    │
  │           │                           │                             │
  │           │ context                   ▼                             │
  │   ┌───────┴──────┐            ┌─────────────────┐                  │
  │   │  Data Layer  │◀───────────│  Broker Adapter │                  │
  │   │              │            │  (Alpaca REST + │                  │
  │   │ Alpaca feed  │            │  WebSocket)     │                  │
  │   │ Polygon (BT) │            └────────┬────────┘                  │
  │   │ yfinance     │                     │                           │
  │   │ news RSS     │                     │ HTTPS                     │
  │   └──────┬───────┘                     │                           │
  │          │                             │                           │
  │          ▼                             │                           │
  │   ┌──────────────────────┐             │                           │
  │   │  SQLite: trading.db  │             │                           │
  │   │ signals / orders /   │             │                           │
  │   │ fills / pnl /        │             │                           │
  │   │ llm_calls / audit    │             │                           │
  │   └──────────────────────┘             │                           │
  │                                        │                           │
  │   ┌──────────────────────┐             │                           │
  │   │  KILL switch file    │◀──checked──┼──(every tick)              │
  │   │  state/KILL          │             │                           │
  │   └──────────────────────┘             │                           │
  │          ▲                             │                           │
  │          │ touch                       │                           │
  │   ┌──────┴────────┐                    │                           │
  │   │ ntfy listener │                    │                           │
  │   │ HA shell_cmd  │                    │                           │
  │   └───────────────┘                    │                           │
  │                                        │                           │
  │   ┌──────────────────────┐             │                           │
  │   │  scripts/notify.sh   │◀────────────┘                           │
  │   │  ntfy / Discord /    │   fills, kill events, breaches          │
  │   │  Slack / file        │                                          │
  │   └──────────────────────┘                                          │
  │                                                                    │
  │   ┌──────────────────────┐                                         │
  │   │  Dashboard publisher │──writes──▶ comms/results/tradingbot.md  │
  │   │ (every 60s)          │        + state/dashboard.json           │
  │   └──────────────────────┘     (rendered by forge dashboard:8099)  │
  └────────────────────────────────────────────────────────────────────┘
                                         ▼ HTTPS (outbound only)
                               ┌─────────────────────┐
                               │  Alpaca Markets API │
                               │  paper or live env  │
                               └─────────────────────┘

Remote kill paths (Tailscale):
  Venus (iPhone) ──▶ ntfy topic ──▶ HA shell_cmd ──▶ ssh udev "touch KILL"
  Sol (MacBook)  ──▶ ssh udev "/tradingbot/scripts/kill-switch.sh"

Layer Recommendation Why Rejected
Broker Alpaca (paper + live) Free paper acct with identical API to live; generous free data; Python SDK; US-regulated; T+1 settlement; WebSocket for fills; no options/crypto complexity for v1. IBKR: best breadth but TWS/Gateway is a headache to keep alive on a headless VM. Coinbase Advanced: 24/7 & no PDT but every trade is a taxable event, and US crypto regulation shifts weekly. Robinhood: no official institutional API.
Asset class US equities + ETFs, long-only v1 Simplest settlement, deepest data, best Claude context (filings, news); skip margin & shorts for phase 1; PDT rule avoided by staying > $25k OR holding positions ≥ 1 day. Options: greeks + assignment risk, not for v1. Crypto: tax drag + exchange risk. Futures: leverage too hot.
Language Python 3.11 Pandas / NumPy / alpaca-py / vectorbt ecosystem; Claude SDK first-class; Justin already has it on UDev. Node/TypeScript: fewer quant libs. Rust: too heavy for v1 velocity.
Framework Custom minimal (~500 LOC) wrapping alpaca-py, plus VectorBT for offline backtests Keep execution simple and fully owned. VectorBT is the best backtest engine; using it only for research avoids backtest-framework-dictating-live problems. Backtrader: mature but live support brittle. Freqtrade: crypto-only opinions, fights you for equities. NautilusTrader: institutional; overkill and steep learning curve.
Market data, live Alpaca native feed (IEX free, SIP $99/mo optional) Same source as execution → no feed mismatch. Free tier is fine for paper and $5k live. Polygon live: great but duplicates what Alpaca gives. Yahoo/IEX Cloud: rate-limited.
Market data, historical (backtest) Polygon.io Starter ($29/mo) 5 years of 1-min bars, options chains, news API. Paid tier is worth it, free data is riddled with survivor bias and bad splits. yfinance: fine for research, bad for robust backtests (revisions, missing tickers). Alpha Vantage: rate-limited.
News Benzinga (via Alpaca addon) or Polygon news Tight integration with price data, Claude can correlate. RSS scraping: flaky.
Storage, state SQLite (tradingbot/state/trading.db) Bot is single-writer; SQLite is faster, simpler, durable on NVMe. Easy to inspect with sqlite3. Backups are a file copy. Postgres in new LXC: overkill for a solo bot. Reconsider in Phase 5 if multi-strategy.
Storage, reports Markdown in-repo + styled .docx via existing scripts/gdoc/to-drive.sh → weekly to /mnt/workspace/Google-Drive/Business/Trading/ Reuses the pipeline. Justin already reviews Drive from mobile. Custom HTML email: extra infra.
Scheduling systemd service for the execution loop (always-on during market hours); cron on UDev for Claude analysis triggers; n8n for market-hours gating + external webhooks Matches existing Forge pattern (forge-dispatcher.service, monitor cron). Long-running Python scheduler (APScheduler): reinvents what systemd + cron already do.
Claude integration Anthropic Python SDK called from the analyst module, with prompt caching on the system prompt and static context 5-minute cache TTL matches our analysis cadence perfectly. Cuts cost ~80% on the repeated context block. Raw HTTP: no reason when SDK handles retries/streaming.
Notifications Existing scripts/notify.sh Priority-routed; already plumbed to ntfy / Discord / Slack / file. Bot-native notification code: duplicate effort.
Kill switch File sentinel (state/KILL) checked every tick + ntfy command + HA shell_command + AdGuard sinkhole as nuclear Layered defense. File check is cheap and reliable. Broker-side trade disable alone: Alpaca's throttle is not a hard stop.
Observability SQLite event log (immutable append); logs/*.log structured JSON lines; dashboard JSON dropped into comms/results/ for the existing Forge dashboard Uses what's already running. Grep-friendly. Prometheus / Grafana: worth it at Phase 5 if strategy count grows.
Secrets .env in tradingbot/secrets/ (gitignored); n8n's encrypted DB for any webhook creds Matches existing Forge pattern. Vault / Doppler: infra bloat for a solo bot.
Remote kill Tailscale SSH from Venus (iPhone) + HA button + ntfy action Tailscale already in fleet. iPhone + HA give Justin one-tap halt. Public webhook: security risk.

4. Claude Integration Pattern

4.1 Model tiering

Tier Model Runs Job Cost per call (est., cached) Calls/day $ / day $ / mo
Deep Opus 4.7 8:30 AM CT (pre-market) Regime analysis + weekly strategy review ~$0.50 (8k in, 2k out) 1 weekday + 1 weekly ~$0.50 ~$12
Signal Sonnet 4.6 Every 15 min, 9:30a–4:00p CT Signal proposals on the watchlist ~$0.08 (3k in cached, 500 out) ~26 ~$2.10 ~$42
Tag Haiku 4.5 On news webhook or headline flood Headline classification (material/noise/direction) ~$0.003 Up to 200 ~$0.60 ~$12
Research Opus 4.7 ad-hoc When a new strategy is proposed Backtest results interpretation, drafts strategy spec ~$1.00 ~10/mo , ~$10
Total ~$76/mo baseline; ~$180/mo active

Costs assume prompt caching on the static system prompt + market-structure block (both ~3k tokens). Without caching, roughly 4x. Hard budget cap: $200/mo, implement as a daily counter that disables the Claude layer (falls back to deterministic signals only) when breached.

4.2 The analyst contract (signal JSON schema)

Every Claude call returns a JSON object conforming to this schema. Non-conforming output is discarded; the call counts against a daily malformed-response budget (3 strikes → Claude layer disabled for the day + notify warning).

{
  "version": "1.0",
  "generated_at": "2026-04-21T14:30:00Z",
  "model": "claude-sonnet-4-6",
  "regime": {
    "label": "risk_on | risk_off | chop | transition",
    "confidence": 0.0,
    "evidence": ["SPY above 50d and 200d", "VIX 14.2"]
  },
  "watchlist_view": [
    {
      "symbol": "AAPL",
      "action": "buy | sell | reduce | hold | flag",
      "side_confidence": 0.0,
      "timeframe": "intraday | swing | position",
      "rationale": "one sentence",
      "target_size_pct": 0.0,
      "stop_hint_pct": 0.0,
      "invalidates_on": "price below 180 on daily close"
    }
  ],
  "news_digest": [
    {"symbol": "AAPL", "headline": "...", "material": true, "direction": "bull | bear | neutral"}
  ],
  "caveats": ["What I could not see", "Known gaps"]
}

target_size_pct and stop_hint_pct are suggestions. The execution engine's risk module sizes the actual position per config/risk.yaml caps. The LLM's numbers get clamped, never trusted.

4.3 Prompt template (Sonnet intraday, abbreviated)

SYSTEM (cached):
You are the analyst for a US equities paper-trading bot. You ONLY produce
JSON matching the schema below. You NEVER produce prose outside the JSON.
You do not place trades. Your proposals go through a policy filter.

Risk posture: long-only, max 10 concurrent positions, holding period 1–10
days. Position sizes are suggestions; the engine enforces caps.

Schema: { ... signal JSON schema ... }

Market structure: { ... watchlist definitions, sector map, ETF universe ... }

USER (not cached — fresh per call):
Timestamp: 2026-04-21T14:30:00Z
Regime from this morning: risk_on (conf 0.7)
Positions open: AAPL (+2.1%, 3d), NVDA (-0.8%, 1d)
Watchlist quotes + 1d/5d/20d returns + RSI + 20d vol: { ... JSON ... }
News in last 30m: { ... headlines array ... }
Recent signals not yet acted on: { ... }

Produce the signal JSON. Be honest about uncertainty. Output caveats when
data is thin or contradicts your prior.

Key design choices: - Cached block: system prompt + schema + watchlist/market structure. Refreshed weekly or when the watchlist changes. - Fresh block: timestamps, quotes, positions, recent news. Small (~500 tokens). - No chain-of-thought asked: we want structured output, not reasoning trace (it inflates token use and adds no value once parsed). - Temperature 0.2 for signal tier; 0.4 for regime (broader exploration fine at daily cadence). - Deterministic parser in src/analyst/parser.py: json.loads → jsonschema validate → reject on any failure.

4.4 Signal → execution flow

analyst/run.py
  ├─ build_context()              reads DB for positions, quotes, news, last regime
  ├─ call_claude(tier, prompt)    SDK call with caching + retry
  ├─ parse_and_validate()         jsonschema; rejects non-conforming
  ├─ persist_to_db()              llm_calls + proposed_signals tables
  └─ emit signal to queue (SQLite signals table, status=proposed)

execution/loop.py (tick: every 30s)
  1. check_kill_switch()               → halt + flatten if KILL exists
  2. load_signals_where_status=proposed
  3. for each signal:
       a. policy_filter()              → approved | rejected | needs_approval
       b. if approved:  size + submit order (Alpaca)
       c. if rejected:  log reason, status=rejected
       d. if needs_approval: notify.sh high + status=awaiting_justin; Justin acks via file or ntfy
  4. reconcile_fills()                 WebSocket fills → positions + pnl
  5. risk_check()                      intraday drawdown, exposure, breach → kill-switch
  6. emit_heartbeat()                  state/heartbeat.json + dashboard.json

needs_approval is the escape hatch when the signal is big (>target_size cap), exotic (first-time symbol), or the regime confidence is low. Justin gets a ntfy with a deep link that resolves into either ack:signal_id or deny:signal_id. No action = auto-deny after 10 min.


5. Safety Model

5.1 Risk limits (hardcoded in config/risk.yaml, loaded at boot, not editable while running)

Limit Paper Shadow Live (initial) Live (scaled)
Allocated capital $100,000 fake $1,000 real $5,000 real up to $50,000 (increment +10%/mo clean)
Max concurrent positions 10 3 3 10
Max position size (% allocated) 10% 10% ($100) 5% ($250) 10%
Max position size (absolute) , $100 $250 $5,000
Max daily loss → halt 3% 3% ($30) 2% ($100) 3%
Max rolling 5-day loss → halt 5% 5% 4% 5%
Max drawdown from peak → halt + liquidate 15% 10% 7% 15%
Max orders / day 50 5 10 50
Min seconds between orders 30 300 60 30
Symbol whitelist SPY/QQQ + top 50 by liquidity same same expandable by review
Leverage 1x 1x 1x 1x (no margin v1)
Shorting no no no decide at Phase 5
Options no no no separate project
Max single-order slippage tolerance 1% 0.5% 0.3% 0.5%

On limit breach: src/risk/breaker.py writes state/KILL, sends notify.sh critical, and the execution loop flattens all positions at next tick with market orders.

5.2 Kill-switch mechanics (layered)

Layer Trigger Mechanism Latency
1 File sentinel Any process touches tradingbot/state/KILL Next tick (≤30s)
2 ntfy action Justin taps "HALT" on ntfy notif → HA webhook → shell_command touches KILL ~5s + next tick
3 HA dashboard button Home Assistant scene trading_haltshell_command: ssh udev "touch /home/justinwieb/forge/tradingbot/state/KILL" ~3s + next tick
4 SSH from Sol ssh udev "/home/justinwieb/forge/tradingbot/scripts/kill-switch.sh" Immediate + next tick
5 Circuit breaker Risk module auto-fires on daily-loss / drawdown / malformed-signal-flood Same tick
6 AdGuard sinkhole (nuclear) Justin adds api.alpaca.markets to AdGuard block list Immediate cut, all orders fail
7 Broker kill Disable API key in Alpaca dashboard Immediate at Alpaca side

Kill behavior is configurable per trigger: halt_only (stop opening new positions, keep existing) or flatten (market-order liquidate all). Default: circuit breakers and nuclear triggers flatten; user-initiated halts default to halt_only and require a second tap to liquidate.

Restart requires deleting state/KILL and running scripts/kill-switch.sh --clear --reason "...", which logs an unlock event. Bot won't re-open positions for 60 minutes after an unlock (cooling-off).

5.3 Paper → shadow → live gates

Each gate is a boolean of all conditions. Missing any → do not advance.

Gate Min duration Sharpe (ann.) Max DD # trades Other
Paper → Shadow 8 weeks ≥ 1.0 ≤ 10% ≥ 150 0 risk-limit breaches; 0 malformed-signal floods; dashboard green for 7 consecutive days; backtest on out-of-sample data shows Sharpe within 50% of live paper
Shadow → Live (tiny) 4 weeks ≥ 0.8 ≤ 7% ≥ 20 Real fills match paper direction ≥ 95% of the time; slippage < expected model + 50%; 0 kill-switch events; Justin has reviewed every trade once
Live (tiny) → Scale 4 weeks per +10% allocation ≥ 0.8 rolling 20 trades ≤ 7% ≥ 20 0 breaches; quarterly tax check done; no strategy changes in the last 30 days

Backtest harness gate on any new Claude-proposed strategy before it goes into paper: Sharpe ≥ 1.2 on 2020-2026 out-of-sample; max DD ≤ 12%; ≥ 300 simulated trades; survives regime-flip stress test (2020 Q1, 2022 Q3).

5.4 What kills the live tier permanently

Event Action
Single-day loss > 5% on live Halt, flatten, full postmortem required before re-enable
API key compromise suspected Rotate key, halt 72h, audit
Alpaca account flagged Halt until Alpaca issue resolved
Claude layer produces wrong JSON 3 days running Disable Claude layer, revert to deterministic baseline, debug
Two consecutive weeks below -2% PnL Pause + strategy review

6. Phased Build Plan

Each phase has a gate. No skipping.

Phase 0: Research & scaffolding (week 1–2)

  • Repo skeleton under tradingbot/ (directory tree in §7).
  • docs/ARCHITECTURE.md, docs/RUNBOOK.md stubs.
  • Backtest-only: install vectorbt, pull Polygon Starter historical data (no live API key yet, use the free sample + yfinance where needed).
  • Implement 1 baseline strategy (e.g. 20/50 SMA crossover on SPY top-50 liquid names) as a pure backtest, zero broker code, zero Claude.
  • Deliverable: backtest report in reports/phase-0-baseline.md with Sharpe, DD, trade count, equity curve PNG.
  • Gate: backtest shows any positive edge (Sharpe > 0.3 is fine at this stage, point is to validate the data pipeline, not the strategy).

Phase 1: Single-strategy paper (week 3–5)

  • Open Alpaca paper account (no live yet, ever). API keys to tradingbot/secrets/.env (gitignored).
  • Broker adapter (src/broker/alpaca.py): submit, cancel, list positions, stream fills.
  • Execution loop (src/execution/loop.py): 30s tick, reads signals from SQLite, submits orders, reconciles.
  • Risk module (src/risk/): position caps, daily-loss halt, kill-switch check.
  • Signal source: the Phase 0 deterministic strategy, no Claude yet.
  • SQLite schema + audit logging.
  • Kill-switch layers 1, 4, 5 (file, SSH script, circuit breakers).
  • systemd unit: tradingbot.service (started manually, not enabled-on-boot).
  • Gate: 2 weeks paper clean, no crashes, no missed fills, no off-by-one position bugs.

Phase 2: Claude analyst layer (week 6–8)

  • Analyst module (src/analyst/): prompt templates, SDK calls with caching, jsonschema validation, DB persistence of every call.
  • Scheduler: Opus pre-market (cron), Sonnet every 15m during RTH (cron), Haiku on news webhook (via n8n).
  • Signal queue: analyst proposals → policy filter → approved/rejected/needs_approval.
  • needs_approvalnotify.sh high with a ntfy action button that writes to state/approvals/.
  • Cost-meter + daily-budget cap ($10/day default; halts Claude layer on breach).
  • Kill-switch layers 2, 3 (ntfy → HA → SSH → touch KILL).
  • Dashboard JSON published to comms/results/tradingbot.md + state/dashboard.json (picked up by existing forge dashboard on port 8099).
  • Gate: 4 more weeks clean; gate math from §5.3 met; Claude cost under $180/mo.

Phase 3: Shadow (week 9–12)

  • First real money: $1,000 in a dedicated Alpaca live account (not linked to Justin's primary checking).
  • Same strategies, same code, flag flip in config from paper to shadow.
  • Tiny sizes ($100 max per position).
  • Daily report to Google Drive via scripts/gdoc/to-drive.sh.
  • Measure paper-vs-real slippage per fill; if > model + 50%, halt and tune.
  • Gate: §5.3 shadow gate.

Phase 4: Live tiny (week 13–16)

  • Capital: $5,000.
  • Position cap 5% ($250 max per name).
  • Weekly review with Justin, he reads every trade, signs off to continue.
  • Quarterly accountant touchpoint scheduled.
  • Gate: §5.3 live gate.

Phase 5: Scale (month 5+)

  • +10% allocation per clean month, cap $50k (re-evaluate if Justin wants to push higher, separate decision).
  • Add strategies one at a time, each through its own Phase 0–2 mini-cycle.
  • Consider: Postgres migration, multi-strategy portfolio allocator, options research sprint, crypto arm (would be a separate bot).

7. Directory Layout

Lives at /home/justinwieb/forge/tradingbot/. Nothing at forge root is touched. Nothing committed to git until Justin greenlights.

tradingbot/
├── README.md                       # quickstart, runbook link, status badge
├── .gitignore                      # secrets/, state/, logs/
├── config/
│   ├── bot.yaml                    # mode (paper|shadow|live), watchlist, cadence
│   ├── risk.yaml                   # risk limits (§5.1) — loaded at boot, immutable in-run
│   └── strategies/
│       ├── baseline_sma.yaml
│       └── claude_regime.yaml
├── secrets/
│   └── .env                        # ALPACA_KEY, ALPACA_SECRET, ANTHROPIC_API_KEY, POLYGON_KEY
├── src/
│   ├── __init__.py
│   ├── broker/
│   │   ├── alpaca.py               # REST + WebSocket adapter
│   │   └── paper_fake.py           # pure-local sim for unit tests
│   ├── execution/
│   │   ├── loop.py                 # main tick loop
│   │   ├── policy_filter.py
│   │   ├── sizing.py
│   │   └── reconcile.py
│   ├── analyst/
│   │   ├── client.py               # Anthropic SDK wrapper, caching, retries
│   │   ├── prompts/
│   │   │   ├── system_sonnet.md
│   │   │   ├── system_opus_regime.md
│   │   │   └── system_haiku_tag.md
│   │   ├── schema.json             # JSON schema for signal contract
│   │   ├── parser.py               # validate + normalize
│   │   └── run.py                  # orchestrator called by cron
│   ├── data/
│   │   ├── alpaca_feed.py
│   │   ├── polygon_hist.py
│   │   ├── news.py
│   │   └── cache.py                # short TTL in-memory cache
│   ├── risk/
│   │   ├── limits.py               # enforce §5.1
│   │   ├── breaker.py              # circuit breakers
│   │   └── kill_switch.py          # file + process checks
│   ├── strategies/
│   │   ├── baseline_sma.py
│   │   └── claude_signal_adapter.py  # converts JSON signals → trade intents
│   ├── backtest/
│   │   ├── harness.py              # VectorBT wrapper
│   │   └── gates.py                # Sharpe/DD gates per §5.3
│   ├── storage/
│   │   ├── db.py                   # SQLite init + migrations
│   │   └── schema.sql
│   └── monitor/
│       ├── heartbeat.py            # writes state/heartbeat.json
│       ├── dashboard.py            # writes state/dashboard.json + comms/results/tradingbot.md
│       └── report.py               # daily markdown → gdoc/to-drive.sh
├── state/
│   ├── trading.db                  # SQLite (never checked in)
│   ├── KILL                        # presence = halt
│   ├── heartbeat.json
│   ├── dashboard.json
│   └── approvals/                  # pending-approval signal files
├── logs/
│   ├── bot.log
│   ├── analyst-calls.jsonl         # structured log of every Claude call
│   ├── orders.log
│   └── breaker.log
├── scripts/
│   ├── start.sh                    # systemctl start tradingbot
│   ├── stop.sh
│   ├── kill-switch.sh              # touches KILL, sends notify, optionally flattens
│   ├── unlock.sh                   # clears KILL, logs reason, starts 60-min cooling-off
│   ├── backtest.sh                 # wraps src/backtest/harness.py
│   ├── daily-report.sh             # generates + uploads to Drive
│   └── install-systemd.sh
├── infra/
│   └── tradingbot.service          # systemd unit
├── tests/
│   ├── test_risk.py                # MUST cover every limit in §5.1
│   ├── test_policy_filter.py
│   ├── test_parser.py              # fuzz the schema validator
│   └── test_kill_switch.py
└── docs/
    ├── ARCHITECTURE.md
    ├── RUNBOOK.md
    ├── STRATEGIES.md
    └── POSTMORTEMS/                # one file per incident

SQLite schema highlights (in src/storage/schema.sql):

Table Purpose
llm_calls id, ts, tier, model, prompt_hash, input_tokens, output_tokens, cost_usd, latency_ms, malformed (bool), raw_response
proposed_signals id, source (tier), symbol, action, size_pct, stop_hint, rationale, regime_snapshot, status (proposed/approved/rejected/needs_approval/acted/expired), llm_call_id
orders id, signal_id, symbol, side, qty, order_type, submitted_at, broker_order_id, status, filled_qty, avg_fill_price, slippage_bps
positions_snapshot hourly snapshot for drift tracking
pnl_daily date, realized, unrealized, equity_close, peak_equity, drawdown_pct
events immutable audit log: kill events, breaches, regime flips, config reloads

8. Open Decisions for Justin

Each decision has my recommendation. Override as you see fit, these are the forks that need a human.

# Decision Recommendation Rationale
1 Asset class for v1 US equities + ETFs, long-only Cleanest data, best Claude context, no PDT if positions held overnight, no options greeks, no crypto tax drag.
2 Broker Alpaca (paper first, then live) Best Python API, free paper, one-key swap to live. IBKR is more powerful but the TWS/Gateway headless setup is a time sink.
3 Entity wrapper Personal account v1, revisit at Phase 5 for Wiebelhaus Enterprises LLC Opening a business brokerage is ~4 weeks of paperwork; personal is fastest to first paper account. If capital grows past $25k, the LLC question comes back.
4 Initial live capital $5,000 at Phase 4 Enough for meaningful stats (~20 trades/mo with 5% sizing); small enough that a 20% blowup is $1k, an expensive lesson, not a life event.
5 Dedicated broker email New throwaway email (e.g. [email protected] or a fresh Gmail, NOT personal Gmail) Hard security rule: no OAuth/password-reset on personal. Broker account gets its own identity, 2FA on a hardware key.
6 Market data subscription Polygon.io Starter ($29/mo) + Alpaca free feed Polygon for backtest quality; Alpaca native for live (matches execution). Skip Polygon live tier until Phase 5.
7 Strategy archetype Regime-gated trend/mean-reversion hybrid: deterministic core + Claude regime filter + Claude-tagged news overlay Claude adds value at regime classification and news synthesis; it adds very little to pure technical crossovers. Let it do what it's best at.
8 Claude monthly budget cap $200/mo hard ceiling with daily $10 soft cap (disables Claude tier for the day on breach) Predictable cost. If the bot earns 5%/mo on $5k = $250/mo, Claude has to stay ≤ 80% of that gross.
9 Paper duration before shadow 8 weeks min Industry lore says 3 months; we can compress with rigorous backtest + out-of-sample gate. 8 weeks is the floor I'd defend.
10 Kill-switch ergonomics Tier 1 (file) + tier 2 (ntfy tap from phone) + tier 5 (circuit breakers) live from day one; tier 3 (HA dashboard) + tier 6 (AdGuard sinkhole) after phase 2 Protects without over-engineering v1.
11 Tax strategy Quarterly 15-min check-in with your accountant from Phase 4 onward; enable Alpaca's tax loss harvesting in year 2 only Avoid wash sales by deduping symbols across strategies; consider Section 475(f) mark-to-market election only if day-trading volume warrants (unlikely at $5k).

Appendix A: Non-Negotiable Rules (per CLAUDE.md + this plan)

  1. Personal Gmail is off-limits for broker, Anthropic, Polygon, or anything touching this bot.
  2. No credentials in git. .env only, gitignored. Keys rotated if UDev is compromised.
  3. No live money until §5.3 paper gate met.
  4. No public webhooks without Cloudflare Access auth (reuse existing tunnel pattern; do NOT open router ports).
  5. Trading capital sits in a dedicated broker account, not linked to primary checking.
  6. Every order path runs through src/risk/limits.py. No direct broker.submit() calls from anywhere else.
  7. Claude outputs are suggestions. Execution is deterministic code. This is enforced by the policy filter, there is no code path where a signal skips the filter.
  8. Kill-switch file is read on every tick. If the file read itself fails (FS error), treat as kill present (fail-safe, not fail-open).

Appendix B: What we are NOT building in v1

Explicit non-goals keep scope honest.

  • Shorting, margin, options, futures, crypto.
  • Multi-broker routing.
  • Automated tax-loss harvesting.
  • Portfolio optimization across multiple strategies (Phase 5 consideration).
  • ML models trained on price data (Claude is the ML; no custom PyTorch).
  • Social / sentiment signals beyond Benzinga/Polygon news.
  • 24/7 operation (market hours + overnight position mgmt only).
  • Public dashboard / social sharing of results.
  • Copy-trading or signal selling.

Appendix C: What lives in the fleet vs what's new

Reused (no new infra): - UDev as the bot host. - n8n for cron triggers + webhook intake (broker callbacks, news feeds). - scripts/notify.sh for alerts. - scripts/gdoc/to-drive.sh for weekly reports. - Forge dashboard (port 8099) for status visibility. - systemd for persistent processes (match forge-dispatcher.service pattern). - Tailscale for remote kill from Venus/Sol. - AdGuard for nuclear-option DNS sinkhole.

New (purpose-built for the bot): - tradingbot/ directory tree (§7). - tradingbot.service systemd unit. - SQLite trading.db. - Claude analyst prompts + signal schema. - Broker adapter (Alpaca). - Home Assistant scene trading_halt (a single shell_command entry). - ntfy topic forge-trading (separate from existing operational topic).

Not touched: - The existing forge task queue / dispatcher, the bot runs its own loop, doesn't piggyback on tasks/pending/. Claude analysis calls go direct via SDK, not via claude -p workers (lower latency, better caching control). - Justin's personal Gmail, Google Drive personal folders, Home Assistant non-trading automations. - Any of the brand/website/media infra.


Sitting on this.

Plan ready for Justin's review. I will not write code, open accounts, or touch keys until he greenlights. The eleven open decisions in §8 are the gating conversation, answer those and I can start Phase 0 on the next session.

, Claude Code (Opus 4.7)