Files
momentum-system/.env.example
T
momentum devandClaude Opus 4.8 e74539d449 Resiliency + risk + monitoring: SIP backfill, freshness watchdog, gross cap, naked alarm
From the multi-agent audit (ship-now, low-overfit-risk cluster):

INGESTION RESILIENCY
- client._request: 429/5xx now retry with Retry-After + exponential backoff
  (previously hard-failed, feeding silent-stale ingest).
- SIP backfill feed (settings.backfill_feed='sip'): live realtime stays IEX, but
  backtest/backfill ingestion uses the full consolidated tape — IEX sees only
  ~2-5% of true volume (verified: AAPL 22,797 vs 452,704), which was silently
  corrupting every rvol/$-volume decision and backtest. eod_replay wired to it.
- step_ingest freshness watchdog: emits a STALE event + board note when the
  newest bar is older than INGEST_STALE_SECONDS during RTH (45% of passes
  historically returned 0 rows with no error). Optional hard arm-block
  (INGEST_STALE_BLOCK, off by default).

RISK
- Portfolio gross-notional cap (TRADING_MAX_GROSS_NOTIONAL_PCT=0.60): both sizing
  paths now cap each entry to the remaining gross budget, so 6 x 16% can't pile
  the book into ~96% gross of correlated small-caps. Blocks/shrinks late entries.
- Fixed the duplicate TRADING_MAX_CONCURRENT_POSITIONS in .env.example.

MONITORING (UI)
- Naked/unprotected-position alarm: _unprotected_positions joins the latest
  position + order snapshots (order sync now flattens legs) and the TUI board
  shows a red UNPROTECTED banner the instant a held long lacks stop coverage —
  the exact blind spot when APWC/NOWL went naked. Verified it flags today's
  3 overnight positions.

Tests: gross-notional cap blocks when budget full; 50 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 18:18:36 -04:00

162 lines
8.1 KiB
Bash

# ============================================================
# Momentum — environment configuration (copy to .env and fill)
# ============================================================
# ---- Database (PostgreSQL) — the single datastore (event store + bar warehouse)
# Either a full URL:
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/momentum
# ...or individual vars (used only if DATABASE_URL is unset):
# PGHOST=127.0.0.1
# PGPORT=5432
# PGUSER=admin
# PGPASSWORD=
# PGDATABASE=momentum
# ---- Alpaca (market data + paper broker) — REQUIRED for live paper trading
ALPACA_API_KEY=
ALPACA_SECRET_KEY=
# Alpaca paper trading endpoint (default is fine)
APCA_API_BASE_URL=https://paper-api.alpaca.markets
# These two aliases are also read by the paper client:
APCA_API_KEY_ID=
APCA_API_SECRET_KEY=
# ---- Schwab (live broker, Milestone 3) — OPTIONAL for paper trading
SCHWAB_MARKET_DATA_APP_KEY=
SCHWAB_MARKET_DATA_APP_SECRET=
SCHWAB_BROKER_APP_KEY=
SCHWAB_BROKER_APP_SECRET=
SCHWAB_REDIRECT_URI=
SCHWAB_TOKEN_PATH=data/schwab_tokens.json
# ---- Optional research / news providers
POLYGON_API_KEY=
FINNHUB_API_KEY=
Alpha_Vantage_Api_Key=
# News-catalyst discovery (optional): RSS feeds as name=url, comma-separated.
# When set, tickers mentioned in recent news ($TICKER or (NASDAQ:TICKER)) are
# added to the watchlist. Unset = disabled (no effect on the loop).
# NEWS_FEEDS=prnewswire=https://www.prnewswire.com/rss/news-releases-list.rss,businesswire=https://feed.businesswire.com/rss/home/?rss=G1QFDERJXkJeGVtRWQ==
# ============================================================
# Live paper-trading orchestrator (run_live_paper.py)
# ============================================================
# Execution destination: alpaca_paper (safe) or schwab_live (REAL MONEY)
TRADING_EXECUTION_MODE=alpaca_paper
TRADING_EXECUTION_ENABLED=1
TRADING_EXECUTION_INTERVAL_SECONDS=30
TRADING_AUTO_APPROVE=0 # 0 = manual approval in the dashboard, 1 = auto-arm (no approval)
TRADING_MAX_ORDERS_PER_TICK=1
# NOTE: concurrency is set ONCE in the risk section below (was duplicated here).
TRADING_RISK_PER_TRADE_PCT=0.01 # fraction of equity risked per trade
# --- entry mechanism (Ross-Cameron-style auto-arm; all tunable) ---
TRADING_REWARD_MULTIPLE=2.0 # target = entry + this * (entry - stop); 2R default
TRADING_ENTRY_ORDER_TYPE=limit # limit = rest at trigger (cancellable); market = fill now
TRADING_ENTRY_TIMEOUT_BARS=2 # cancel an unfilled entry after N minutes resting (0 = off)
TRADING_ENTRY_INVALIDATE_PCT=0.0 # cancel if price breaks this far below entry (0 = any break; <0 = off)
TRADING_ENTRY_GUARD_INTERVAL_SECONDS=5 # how often to re-check armed entries vs the LIVE last-trade price
TRADING_MAX_DAILY_LOSS_PCT=0.03 # daily-loss circuit breaker
# Intraday watcher (signal engine)
WATCHER_ENABLED=1
WATCHER_INTERVAL_SECONDS=60
# WATCHER_EVENT_DB_PATH is legacy and ignored — the datastore is PostgreSQL (see DATABASE_URL above)
WATCHER_MAX_SYMBOLS=25
WATCHER_MIN_BARS=30
WATCHER_MIN_QUALITY=0.30
# Comma-separated symbols to follow all day. If empty, candidates are
# discovered from whatever has minute bars in market.duckdb for today.
WATCHER_SYMBOLS=AAPL,TSLA,NVDA,AMD,SOFI
# Price band for auto-discovered candidates (Ross Cameron default 1-20).
WATCHER_PRICE_MIN=1
WATCHER_PRICE_MAX=20
# Active strategy setups (comma-separated). The backtest sweep found the
# opening-range breakout is the only profitable one; empty = all setups.
STRATEGY_SETUPS=opening_range_break
# Live minute-bar ingestion (feeds the watcher with active market data)
LIVE_BARS_ENABLED=1
LIVE_BARS_INTERVAL_SECONDS=60
LIVE_BARS_LOOKBACK_MINUTES=120
# Data feed: live realtime uses iex (free, realtime, but ~2-5% of true volume).
# Backfill/backtest uses sip (full consolidated tape) so rvol/$-volume reflect
# REAL volume — the iex thinness silently corrupts every volume-based decision.
ALPACA_DATA_FEED=iex
ALPACA_BACKFILL_FEED=sip
# Freshness watchdog: warn (emit + display) when the newest bar is older than
# this during RTH. INGEST_STALE_BLOCK=1 also refuses to ARM on stale data
# (off by default so a mis-tuned threshold can't silently halt trading).
INGEST_STALE_SECONDS=180
INGEST_STALE_BLOCK=0
# Broker state sync
ALPACA_PAPER_SYNC_ENABLED=1
ALPACA_PAPER_SYNC_INTERVAL_SECONDS=60
ACCOUNT_SYNC_ENABLED=0 # Schwab live account sync
ORDER_SYNC_ENABLED=0 # Schwab live order sync
# Dashboard
DASHBOARD_HOST=0.0.0.0
DASHBOARD_PORT=8010
# ============================================================
# Armed-trigger fast path (runtime/triggers.py) + entry quality
# ============================================================
TRIGGER_ENABLED=1
TRIGGER_MAX_ARMED=10 # gappers kept ready to fire at once
TRIGGER_INTERVAL_SECONDS=4 # fast path: live-price check / fire cadence
TRIGGER_ARM_INTERVAL_SECONDS=20 # re-rank + recompute triggers cadence
TRIGGER_GAP_MIN=3.0 # min overnight gap %
TRIGGER_GAP_MAX=60 # skip blow-off gappers (they fade / buy-the-top)
TRIGGER_RVOL_MIN=2.0 # min RELATIVE volume to fire
# Absolute liquidity gate: rvol is relative, so 2x of near-nothing still
# qualifies and we fill at the top of a thin one-bar spike that reverses. This
# requires recent 5-bar $-volume >= the floor so thin names go WEAK, not armed.
TRIGGER_MIN_DOLLAR_VOL=8000
ORB_BARS=5 # opening range = first N regular-hours minutes
TRADING_TRIGGER_SLIP_PCT=0.004 # cap the chase above the trigger on the fill
# Account-aware sizing (matters most on a small REAL account):
TRADING_MAX_POSITION_PCT=0.16 # one position's $ value <= this fraction of equity
# PORTFOLIO gross-notional cap: total $ across ALL open+pending positions stays
# under this fraction of equity. Without it, 6 concurrent x 16% = ~96% gross in
# correlated small-cap gappers. With it, later entries shrink/blocked to fit.
TRADING_MAX_GROSS_NOTIONAL_PCT=0.60
# Liquidity-aware sizing: shares <= this fraction of the name's volume, so a
# thin name is sized DOWN automatically instead of taking a full position it
# can't absorb. 0 = off (fine at tiny size).
TRADING_LIQUIDITY_MAX_VOLUME_PCT=0.025
# ============================================================
# Risk discipline (added 2026-06-18 — see git history for the why)
# ============================================================
# Anti-thrash: a breakout that won't fill on a marketable limit used to re-arm
# every pass forever (observed: NEOV armed+backed-out 68x/session). Bench it.
TRADING_BACKOUT_COOLDOWN_SECONDS=180 # bench this long after an unfilled backout
TRADING_MAX_BACKOUTS_PER_SYMBOL=3 # after N backouts, bench for the session
# Re-entry discipline: don't buy back a name that already round-tripped today...
TRADING_BLOCK_REENTRY_AFTER_EXIT=1
# ...but only if it exited at a real LOSS (>1%), so a scratch/win can re-enter
# (the blunt version killed a +513 re-entry winner in replay). 0 = bench any close.
TRADING_REENTRY_MIN_LOSS_PCT=0.01
# End-of-day flatten — a day-trading book shouldn't carry overnight gap risk.
TRADING_EOD_FLATTEN_ENABLED=1
TRADING_EOD_FLATTEN_TIME=15:55 # flatten in the window [time, 16:00)
# ============================================================
# Managed exits (strategy/exits.py — swept + out-of-sample verified)
# The far take-profit is a backstop; the TRAILING STOP is the real exit.
# ============================================================
TRADING_REWARD_MULTIPLE=10.0 # far take-profit backstop (NOT 2.0; trail does the work)
TRADING_EXIT_TRAIL_MODE=prior_low # trail the stop under each prior bar's low
TRADING_EXIT_TRAIL_AFTER_R=1.0 # start trailing once the trade is +1R
TRADING_EXIT_BREAKEVEN_R=1.0 # lock breakeven once +1R (give-back safeguard)
TRADING_EXIT_PROFIT_TIERS=8:3,15:9,25:18,40:30 # +gain% -> lock% profit-lock ladder
# NOTE: several defaults above intentionally override the simpler values earlier
# in this file (e.g. MAX_CONCURRENT_POSITIONS, REWARD_MULTIPLE) — these reflect
# the current live-verified config. Set MAX_CONCURRENT_POSITIONS=6 for the live book.
TRADING_MAX_CONCURRENT_POSITIONS=6
# !!! Before REAL-money trading, set TRADING_MAX_DAILY_LOSS_PCT back to 0.03 !!!