f2caca9175
ARCHITECTURE.md: system overview, data flow, SQLite schema, guard rails, risk gates, wheel flow, guide for adding new strategies. RUNBOOK.md: day-to-day ops, config reference, troubleshooting, safe shutdown, upgrade procedure. CHANGELOG.md: v0.1.0 + v0.1.1 (limit_price fix). .env.example: credentials removed, URL /v2 suffix stripped.
6.1 KiB
6.1 KiB
Architecture
Overview
┌─────────────────────────────────────────────────────┐
│ systemd loop service │
│ alpaclaudia loop (every 900s, market hours only) │
└───────────────────────────┬─────────────────────────┘
│ tick()
▼
┌─────────────────────────────────────────────────────┐
│ scheduler.tick() │
│ │
│ 1. snapshot account / positions / orders ─────────┼──► Alpaca REST
│ 2. record_tick() ─────────────────────────────────┼──► SQLite ticks
│ 3. plan_wheel() ──────────────────────────────────┼──► OrderIntent[]
│ 4. plan_trailing_stops() ─────────────────────────┼──► OrderIntent[]
│ 5. risk.check_*() per intent │
│ blocked → logged, dropped │
│ 6. executor.submit_intent() ──────────────────────┼──► Alpaca REST
│ (no-op if BOT_MODE=dry) ──────────────────────┼──► SQLite order_intents
└─────────────────────────────────────────────────────┘
┌─────────────────┐
│ Next.js dash │
│ :3030 │
│ │
│ reads SQLite ◄─┼── data/alpaclaudia.db
│ reads Alpaca ◄─┼── REST (read-only)
└─────────────────┘
┌─────────────────┐
│ systemd timer │
│ 22:30 Mon–Fri │
│ alpaclaudia │
│ report │
│ │ │
│ ▼ │
│ Discord webhook │
└─────────────────┘
Key files
| File | Purpose |
|---|---|
bot/alpaclaudia/config.py |
Load + validate all config from .env |
bot/alpaclaudia/client.py |
Alpaca SDK wrappers (account, positions, orders, options chain) |
bot/alpaclaudia/state.py |
SQLite store: ticks, order_intents, events |
bot/alpaclaudia/risk.py |
Pre-submit risk gates (pure functions, easily testable) |
bot/alpaclaudia/scheduler.py |
Orchestration: tick loop, clock check, risk enforcement |
bot/alpaclaudia/executor.py |
Translate OrderIntent → Alpaca request; dry-run vs live |
bot/alpaclaudia/strategies/wheel.py |
Wheel strategy: CSP + CC selection logic |
bot/alpaclaudia/strategies/trailing_stop.py |
Trailing-stop planner for long equity |
bot/alpaclaudia/reporter.py |
Build + post Discord daily summary |
bot/alpaclaudia/__main__.py |
Typer CLI: tick / loop / status / report / dump-state |
SQLite schema
ticks — portfolio snapshot per tick.
Fields: id, ts, equity, cash, buying_power, mode, note
order_intents — every order the bot considered, regardless of outcome.
Fields: id, ts, strategy, symbol, side, qty, order_type, limit_price, stop_price, trail_percent, details_json, submitted, alpaca_order_id, status
Status values: dry_run · blocked · error · accepted / Alpaca status string
events — tick metadata and heartbeats.
Fields: id, ts, kind, payload_json
Guard rails (two-layer)
BOT_MODE=dry → executor.submit_intent() records intent to DB only, no network call
BOT_MODE=live → executor submits to Alpaca
ALPACA_ENV=paper → TradingClient(paper=True) → paper-api.alpaca.markets
ALPACA_ENV=live → TradingClient(paper=False) → api.alpaca.markets (real money!)
Both must be explicitly set. The defaults are dry + paper.
Risk gates (bot/alpaclaudia/risk.py)
All gates are pure functions returning CheckResult(ok, reason).
scheduler.tick() calls the appropriate gate per intent before passing to executor.
| Gate | Invariant |
|---|---|
check_csp |
Collateral ≤ free cash − buffer; ≤ max_position_pct of equity; ≤ max concurrent short puts |
check_covered_call |
Strike ≥ cost basis; ≥ qty×100 shares held |
check_trailing_stop |
Long position exists with ≥ qty shares |
Strategy flow: Wheel
For each symbol in WHEEL_UNIVERSE:
shares_held = long stock qty
short_puts = count open short put positions
if shares_held >= 100:
uncovered_lots = shares_held // 100 − open short calls
if uncovered_lots > 0:
→ pick covered-call candidate (DTE window, target delta, above cost basis)
→ emit OrderIntent(strategy="wheel:covered_call", side="sell_to_open")
if short_puts < MAX_SHORT_PUTS_PER_SYMBOL:
→ list option contracts (DTE window)
→ get snapshots (greeks, quotes)
→ pick contract closest to TARGET_DELTA (fallback: closest to OTM_PCT below spot)
→ compute annualised yield; skip if < MIN_ANNUAL_YIELD
→ emit OrderIntent(strategy="wheel:cash_secured_put", side="sell_to_open")
Adding a new strategy
- Create
bot/alpaclaudia/strategies/my_strategy.pywith aplan_*()function returninglist[OrderIntent]. - Add a risk gate in
risk.pyif needed. - Wire it into
scheduler.tick()— addintents.extend(plan_my_strategy(...))and a_enforce_riskbranch. - Add tests in
bot/tests/.