Files
alpaclaudia/docs/ARCHITECTURE.md
T
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

122 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`.