# 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 1. Create `bot/alpaclaudia/strategies/my_strategy.py` with a `plan_*()` function returning `list[OrderIntent]`. 2. Add a risk gate in `risk.py` if needed. 3. Wire it into `scheduler.tick()` — add `intents.extend(plan_my_strategy(...))` and a `_enforce_risk` branch. 4. Add tests in `bot/tests/`.