# ============================================================ # 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== # ---- Local LLM news/catalyst enrichment (Ollama) ---- # A local model classifies ingested headlines (catalyst type, sentiment, # conviction, dilution) into news_catalyst_cache — surfaced on the dashboard # approval queue. Runs off the hot path; degrades to a no-op if Ollama is down. # OLLAMA_ENABLED=1 # OLLAMA_HOST=http://localhost:11434 # OLLAMA_MODEL=qwen2.5:7b-instruct # fits 12GB; best small-model JSON. # # alts: llama3.1:8b-instruct-q4_K_M, mistral:7b-instruct # NEWS_ENRICH_ENABLED=1 # NEWS_ENRICH_INTERVAL_SECONDS=120 # NEWS_ENRICH_LOOKBACK_HOURS=12 # NEWS_ENRICH_BATCH_LIMIT=50 # max headlines classified per pass (GPU budget) # Phase 2 (automation — each ships OFF; separate from the advisory above so the # dashboard never silently changes scoring): # score blend — fold the catalyst into the setup quality score AND graded arming # rank (bullish catalyst boosts, confirmed dilution sinks below the armed cut). # NEWS_CATALYST_SCORE_ENABLED=0 # NEWS_RANK_BOOST_GAIN=2.0 # arming-rank multiplier = 1 + gain*score # NEWS_DILUTIVE_PENALTY=0.1 # arming-rank multiplier for a confirmed dilutive name # dilution veto — block confirmed dilutive offerings before sizing. # NEWS_DILUTION_VETO_ENABLED=0 # NEWS_DILUTION_VETO_CONVICTION=0.6 # # AI trade analysis (advisory): the local model reads the armed/weak setups, the # day's closed trades, and the whole session, writing a pursue/avoid verdict + # post-mortems + an EOD note to ai_trade_analysis_cache (shown on the dashboard). # OFF the hot path; never gates a trade. Needs OLLAMA_ENABLED=1. # TRADE_ANALYSIS_ENABLED=0 # TRADE_ANALYSIS_INTERVAL_SECONDS=180 # TRADE_ANALYSIS_BATCH_LIMIT=8 # max setups analyzed per pass (GPU budget) # ============================================================ # 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 % # Cap the gap: blow-off gappers (>35%) fade — they're the worst-expectancy # bucket. Train/test validated on 14 SIP-backfilled sessions: lowering 60->35 # lifted held-out TEST expectancy 0.19R -> 0.46R/trade AND raised total R. TRIGGER_GAP_MAX=35 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 # Do not fire a trigger on a price older than this (seconds) — a stalled feed # must not fire a breakout on a frozen quote into an already-reverted market. TRIGGER_PRICE_MAX_AGE=12 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 !!!