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.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# 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/`.
|
||||
Reference in New Issue
Block a user