Files
admin f2caca9175 docs: add ARCHITECTURE, RUNBOOK, CHANGELOG; reset .env.example to placeholders
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.
2026-04-16 22:15:08 +02:00

6.1 KiB
Raw Permalink Blame History

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 MonFri   │
                    │ 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/.