Files
momentum-system/SYSTEM_FLOW.txt
T
momentum devandClaude Opus 4.8 007cd95c79 Baseline before Postgres migration (WS0)
Snapshot before the DuckDB->PostgreSQL migration. data/, archive/, the 232MB
rebuild zip, the Windows .venv, and .env are excluded via .gitignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:39:29 -04:00

406 lines
21 KiB
Plaintext

================================================================================
MOMENTUM TRADING SYSTEM - COMPLETE LOGICAL FLOW
================================================================================
ENTRY POINT: python run_live_paper.py [--once] [--symbols AAPL,SNDL] [--auto-approve]
================================================================================
STARTUP PHASE
================================================================================
1. INITIALIZE CONFIGURATION
├─ Load .env (ALPACA_API_KEY, ALPACA_SECRET_KEY, etc.)
├─ Parse CLI args
└─ Create settings objects: AlpacaPaperSettings, ExecutionSettings, TradingModeSettings
2. INITIALIZE STORAGE & DATABASES
├─ EventStore("data/momentum_events.duckdb")
│ └─ Append-only log of all events (canonical source of truth)
├─ ResearchConnection("data/research/market.duckdb")
│ └─ Minute bars + daily bars for all symbols
└─ AlpacaPaperClient (if API keys present)
└─ Connection to Alpaca paper trading account
3. INITIALIZE RUNTIME COMPONENTS
├─ Watcher (state machine for symbol discovery/evaluation)
│ └─ Provider: ResearchWatchlistProvider (pulls symbols from research DB)
├─ TradingExecutionService (signal → approval → order pipeline)
├─ AlpacaPaperSync (poll Alpaca account/positions/orders)
└─ DashboardAPI (optional HTTP server on port 8010)
└─ Serves JSON API + static UI + SSE event stream
================================================================================
MAIN LOOP (runs every tick, or once with --once)
================================================================================
TICK = 1 iteration (typically 60 seconds)
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 1: INGEST LATEST MARKET DATA │
└─────────────────────────────────────────────────────────────────────────┘
ingest_live_minute_bars()
├─ Call Alpaca REST API: get latest 1-minute bars
├─ Insert into research/market.duckdb (bars table)
└─ Return: list of symbols with new data
ingest_daily_history()
├─ On startup or when needed:
│ ├─ Alpaca REST API: get daily bars (past 30 days)
│ ├─ Insert into research/market.duckdb (daily_bars table)
│ └─ Used by: gap%, RVOL calculations
└─ Return: daily bar snapshots
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 2: DISCOVER & UPDATE WATCHLIST │
└─────────────────────────────────────────────────────────────────────────┘
ResearchWatchlistProvider.get_candidates(session_date)
├─ Check what symbols are in the watchlist for today
│ ├─ Manual symbols (from --symbols or WATCHER_SYMBOLS env)
│ ├─ Screener results (most-actives from Alpaca if --discover)
│ └─ RSS feed candidates (if configured)
├─ Return list of WatchCandidate objects (symbol, last_price, avg_volume)
└─ Watcher._states updated with new symbols
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 3: EVALUATE SYMBOLS (CORE STRATEGY LOGIC) │
└─────────────────────────────────────────────────────────────────────────┘
Watcher.tick(bar_data, candidates)
│
└─ For each symbol in candidates:
│
├─ 1. FETCH BARS
│ └─ ResearchWatchlistProvider.get_bars(symbol, session_date)
│ └─ Query research DB for all minute bars from 9:30 AM to now
│
├─ 2. EMIT SYMBOL DISCOVERED (if new)
│ └─ EventStore.emit(SymbolDiscoveredEvent(symbol, source, ...))
│ └─ Event recorded with timestamp, session_id, payload
│
├─ 3. EVALUATE SETUP (if ≥10 bars available)
│ │
│ └─ evaluate_setup(bars, symbol) ← PURE STRATEGY ENGINE
│ │
│ ├─ Gap Check
│ │ └─ (today_open - prev_close) / prev_close ≥ 2% ?
│ │
│ ├─ Relative Volume (RVOL)
│ │ └─ (current 5-min avg vol / 20-day avg) ≥ 1.5 ?
│ │
│ ├─ Bull Flag Structure
│ │ └─ Pullback from high + ascending support pattern?
│ │
│ ├─ Price Action Quality
│ │ └─ Higher highs + higher lows? Clean bars?
│ │
│ ├─ 5 Other Criteria (tunable)
│ │ └─ Momentum, volatility, trend confirmation, etc.
│ │
│ ├─ Calculate SCORE
│ │ └─ (criteria_passed / total_criteria) * 100 = X%
│ │
│ └─ Return: {score, details, setup_data}
│
├─ 4. EMIT CRITERIA EVALUATED
│ └─ EventStore.emit(CriteriaEvaluatedEvent(symbol, criteria=[...], score))
│ └─ Each criterion: {name, passed, reason}
│
├─ 5. CHECK SIGNAL READY THRESHOLD
│ └─ IF score ≥ ready_score_pct (default 60%) AND NOT already_ready_this_session:
│ │
│ ├─ Emit SIGNAL READY
│ │ └─ EventStore.emit(SignalReadyEvent(symbol, entry, stop, target, ...))
│ │
│ ├─ Update symbol state: watching → ready
│ │ └─ EventStore.emit(SymbolStateChangedEvent(symbol, "watching" → "ready"))
│ │
│ └─ Mark symbol as already signaled (no duplicate signals this session)
│
├─ 6. CHECK SIGNAL BLOCKED
│ └─ ELSE IF score < ready_score_pct OR quality < min_quality:
│ │
│ ├─ Emit SIGNAL BLOCKED
│ │ └─ EventStore.emit(SignalBlockedEvent(symbol, reason, ...))
│ │
│ ├─ Update symbol state: watching → blocked
│ │ └─ EventStore.emit(SymbolStateChangedEvent(symbol, "watching" → "blocked"))
│ │
│ └─ RESET: allow signal_ready again if it recovers above threshold
│
└─ END for each symbol
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 4: EXECUTION PIPELINE (signal_ready → approval → order) │
└─────────────────────────────────────────────────────────────────────────┘
TradingExecutionService.tick()
│
└─ Query event store for new signal_ready events (not yet processed)
│
└─ For each signal_ready:
│
├─ 1. CHECK RISK RULES
│ │
│ ├─ How many positions are open?
│ │ └─ IF open_positions ≥ max_concurrent_positions (e.g., 3):
│ │ └─ RiskRuleTriggeredEvent("max concurrent positions")
│ │ └─ SKIP this signal
│ │
│ ├─ Check daily loss circuit breaker
│ │ └─ IF realized_pnl_today ≤ -max_daily_loss_pct * account_equity:
│ │ └─ RiskRuleTriggeredEvent("daily loss exceeded")
│ │ └─ SKIP this signal (halt trading for the day)
│ │
│ └─ Max orders per tick
│ └─ Only submit max_orders_per_tick (e.g., 1) per tick to avoid order queue floods
│
├─ 2. CALCULATE POSITION SIZE
│ │
│ └─ calculate_position_size(entry, stop, account_equity, risk_per_trade_pct)
│ │
│ ├─ Risk per trade = account_equity * risk_per_trade_pct (e.g., 1%)
│ ├─ Risk per share = entry - stop
│ ├─ Shares = risk_per_trade / risk_per_share
│ └─ Return: {shares, entry, stop, target}
│
├─ 3. EMIT ORDER APPROVAL REQUESTED
│ └─ EventStore.emit(OrderApprovalRequestedEvent(
│ symbol, entry, stop, target, shares, risk_dollar, notional, ...
│ ))
│
├─ 4. AUTO-APPROVE OR QUEUE FOR MANUAL APPROVAL
│ │
│ ├─ IF --auto-approve or TRADING_AUTO_APPROVE=1:
│ │ │
│ │ ├─ Emit OrderApprovedEvent immediately
│ │ │ └─ EventStore.emit(OrderApprovedEvent(order_id, ...))
│ │ │
│ │ └─ Jump to Phase 5 (SUBMIT ORDER)
│ │
│ └─ ELSE (manual approval):
│ │
│ └─ Add to approval_queue in dashboard
│ └─ User sees: symbol, entry, stop, target, risk, share count, sparkline chart
│ └─ User clicks Approve or Reject
│ └─ Dashboard emits OrderApprovedEvent or OrderRejectedEvent
│
├─ 5. ON APPROVAL (if approved)
│ │
│ └─ TradingExecutionService detects OrderApprovedEvent
│ │
│ ├─ Create ExecutionRequest
│ │ └─ {symbol, entry, stop, target, shares, order_type: "limit"/"market", ...}
│ │
│ └─ Call AlpacaPaperExecutor.submit(request)
│ │
│ └─ AlpacaPaperExecutor.submit() [PHASE 5]
│
└─ END for each signal_ready
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 5: BROKER ORDER SUBMISSION │
└─────────────────────────────────────────────────────────────────────────┘
AlpacaPaperExecutor.submit(execution_request)
│
├─ Create bracket order:
│ │
│ ├─ ENTRY ORDER
│ │ └─ Type: limit @ entry_price (restful, will fill on touch, can be cancelled)
│ │ OR market (fills immediately at market)
│ │
│ ├─ PROFIT TARGET
│ │ └─ Type: limit @ target_price
│ │ (target = entry + (entry - stop) * reward_multiple)
│ │
│ └─ STOP LOSS
│ └─ Type: stop @ stop_price (becomes market order when stop is hit)
│
├─ Submit to Alpaca REST API
│ └─ Returns: Alpaca order_id
│
├─ Emit OrderSubmittedEvent
│ └─ EventStore.emit(OrderSubmittedEvent(symbol, order_id, entry, stop, target, ...))
│
├─ Track armed entry in TradingExecutionService._armed dict
│ └─ _armed[order_id] = {symbol, entry_price, broker_order_id, armed_at, checks}
│
└─ Monitor for:
│
├─ Entry Fill
│ └─ If entry fills → position open → wait for target or stop
│
├─ Entry Timeout (if not filled after entry_timeout_bars minutes)
│ └─ Call Alpaca to cancel unfilled entry order
│
├─ Price Break Invalidation
│ └─ If current price < (entry - entry_invalidate_pct * entry):
│ └─ Cancel entry order (price broke back below entry)
│
└─ Full Fill (target or stop hit)
└─ Position closes → emit OrderFilledEvent
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 6: ACCOUNT & POSITION SYNC │
└─────────────────────────────────────────────────────────────────────────┘
AlpacaPaperSync.sync_account()
│
├─ 1. GET ACCOUNT SUMMARY
│ └─ Alpaca REST: /account
│ └─ Returns: {equity, buying_power, multiplier, long_market_value, ...}
│
├─ 2. EMIT ACCOUNT SUMMARY UPDATED
│ └─ EventStore.emit(AccountSummaryUpdatedEvent(account_data))
│
├─ 3. GET OPEN POSITIONS
│ └─ Alpaca REST: /positions
│ └─ Returns: [{symbol, qty, avg_fill_price, current_price, unrealized_pl, ...}]
│
├─ 4. EMIT ACCOUNT POSITIONS UPDATED
│ └─ EventStore.emit(AccountPositionsUpdatedEvent(positions))
│
├─ 5. GET PENDING ORDERS
│ └─ Alpaca REST: /orders?status=open
│ └─ Returns: [{id, symbol, qty, type, status, filled_qty, ...}]
│
├─ 6. EMIT ACCOUNT ORDERS UPDATED
│ └─ EventStore.emit(AccountOrdersUpdatedEvent(orders))
│
└─ 7. CALCULATE SESSION P&L
└─ Query event store for all fills this session
├─ For each closed trade:
│ └─ realized_pnl = (exit_price - entry_price) * qty
├─ Aggregate:
│ └─ total_realized_pnl, total_unrealized_pnl, win_rate, avg_r, trade_count
└─ Used by dashboard P&L strip
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 7: DASHBOARD UPDATES (READ-ONLY PROJECTIONS) │
└─────────────────────────────────────────────────────────────────────────┘
DashboardAPI (runs in background thread)
│
├─ SSE Stream Handler (/api/events)
│ └─ Pushes new events to all connected clients in real-time
│ └─ Falls back to polling if connection drops
│
├─ JSON Endpoints:
│ │
│ ├─ GET /api/approval-queue
│ │ └─ query_approval_queue(event_store)
│ │ └─ Returns: [{symbol, entry, stop, target, shares, risk_pct, notional, ...}]
│ │
│ ├─ GET /api/positions
│ │ └─ query_account_positions_snapshot(event_store)
│ │ └─ Returns: [{symbol, qty, entry, current, unrealized_pl, ...}]
│ │
│ ├─ GET /api/pnl
│ │ └─ query_pnl_snapshot(event_store)
│ │ └─ Returns: {realized, unrealized, total, win_rate, avg_r, open_count, closed_count}
│ │
│ ├─ GET /api/activity-feed
│ │ └─ query_activity_feed(event_store, limit=100)
│ │ └─ Returns: [{timestamp, event_type, message, symbol, ...}]
│ │
│ ├─ GET /api/symbol/:symbol/bars
│ │ └─ query_minute_bars(research_db, symbol, session_date)
│ │ └─ Returns: [{timestamp, open, high, low, close, volume}]
│ │ (for sparkline chart rendering)
│ │
│ └─ POST /api/approval/:order_id/approve|reject
│ └─ Emits OrderApprovedEvent or OrderRejectedEvent
│
└─ UI Features:
├─ P&L Strip (realized, unrealized, total, win rate, R multiple, counts)
├─ Approval Queue (cards with entry/stop/target preview, Approve/Reject buttons)
├─ Open Positions (list with live sparkline charts, P&L per position)
├─ Activity Feed (chronological log of all events)
├─ Symbol Details (expand to see 9 criteria: pass/fail/not-evaluated)
└─ Keyboard Shortcuts: A=Approve, R=Reject, S=Sound Toggle, /=Add Symbol, ?=Help
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 8: CLEANUP & REPEAT │
└─────────────────────────────────────────────────────────────────────────┘
After all phases complete:
├─ Log tick summary (discovered, ready, blocked, evaluated, errors)
├─ IF --once: exit
└─ ELSE: sleep(60 seconds) and go back to PHASE 1
================================================================================
KEY CONCEPTS & INVARIANTS
================================================================================
EVENT SOURCING
• All state changes are recorded as immutable events in the append-only event store
• The current state is reconstructed by replaying events (projections)
• Every read operation queries the event store; no other state is authoritative
• Correlation ID = session_id (all events from one trading session have the same correlation_id)
SIGNAL DEBOUNCING
• Once a symbol emits signal_ready, it cannot emit another signal_ready until:
→ It falls back to signal_blocked (criteria fail), which resets the debounce
• Prevents duplicate signals from the same symbol in one session
STATE MACHINE (Watcher)
• discovered → watching → { ready | blocked } (can toggle between ready ↔ blocked)
• States are per-session; new session = fresh symbol states
BRACKET ORDER
• Entry (limit) + Profit Target (limit) + Stop Loss (stop) submitted as a single order
• If entry fills:
→ Two child orders activate (target and stop)
→ First one to fill = exit (other cancels automatically)
• If entry never fills:
→ Entry can timeout (entry_timeout_bars) or be cancelled on price break
RISK CONTROLS
• Max concurrent positions: once N positions open, no more entry orders until one closes
• Daily loss circuit breaker: once losses hit threshold, halt trading for the session
• Entry timeout: if entry rests >N minutes, cancel to avoid stale orders
• Price break invalidation: if price falls back >X% below entry, cancel entry (setup failed)
APPROVAL WORKFLOW
• Manual: user clicks Approve in dashboard → OrderApprovedEvent
• Auto: --auto-approve flag or TRADING_AUTO_APPROVE=1 env var → immediate approval
• Rejection: user clicks Reject or risk rule triggers → OrderRejectedEvent (queued signal removed)
================================================================================
TROUBLESHOOTING BY PHASE
================================================================================
Phase 1 (Ingest):
• No new bars? Check if Alpaca API keys are valid. Run --once in dry mode.
• Bars not in research DB? Check data/research/market.duckdb exists and is writable.
Phase 2 (Discover):
• No symbols in watchlist? Check --symbols / WATCHER_SYMBOLS env / most-actives screener.
Phase 3 (Evaluate):
• Symbols stuck in "watching"? Criteria too strict. Lower ready_score_pct or min_quality.
• Signals appear then disappear? They're being blocked. Check criteria_evaluated events.
• Check event store: SELECT * FROM events WHERE event_type = 'criteria_evaluated'
Phase 4 (Execution):
• Signal ready but no approval request? Risk rule triggered. Check RiskRuleTriggeredEvent.
• Approval request visible but user forgets to click? Use --auto-approve for testing.
Phase 5 (Broker):
• Order submitted but no fill? Entry price may be stale. Use market orders (entry_order_type).
• Entry stuck for >N minutes? Timeout will cancel it. Check entry_timeout_bars setting.
Phase 6 (Sync):
• Account sync fails? Alpaca API likely down. Dry mode still works.
• Positions don't match Alpaca? Event store is source of truth. Refresh browser.
Phase 7 (Dashboard):
• Dashboard not updating? Check browser console for errors. Try hard-refresh (Ctrl+Shift+R).
• SSE stream failing? Falls back to polling (/api/approval-queue, etc.). Check /api/events.
================================================================================
END
================================================================================