================================================================================
  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
================================================================================
