# ============================================================ # Momentum — environment configuration (copy to .env and fill) # Regenerated 2026-07-09 from the live config. Values kept here # are the current tuned defaults; = secret, fill from # your protected store. See deploy/docker/README.md for which # layer (compose vs .env) each group belongs in. # ============================================================ TOKEN_ENCRYPTION_KEY= DASHBOARD_PORT=8765 # Dashboard POST auth + operator command bus (the :8765 kill switch). # DASHBOARD_TOKEN is REQUIRED-when-commands-on: /api/command returns 503 unless # it is set (commands never run unauthenticated); it also gates approve/reject/ # exit POSTs. Empty = read-only dashboard, no command bus. Keep it out of logs. DASHBOARD_TOKEN= # Enable the operator command bus (flatten-all / flatten-symbol / pause / resume). # Needs DASHBOARD_TOKEN set too; the loop consumes commands on its 5s guard. COMMAND_BUS_ENABLED=0 #curl.exe -H "APCA-API-KEY-ID: $key" -H "APCA-API-SECRET-KEY: $secret" https://paper-api.alpaca.markets/v2/account # Alpaca API (primary data provider) ALPACA_API_KEY= ALPACA_SECRET_KEY= ALPACA_BASE_URL=https://paper-api.alpaca.markets/v2 # Polygon (optional fallback) POLYGON_API_KEY= # Additional news and filing sources FINNHUB_API_KEY= FINNHUB_EMAIL=fez5587@gmail.com SEC_API_KEY= DATABENTO_API_KEY= # Data directory for parquet files and DuckDB DATA_DIR=./data # Boot warns (log + one Telegram note) if data/learned_params.json is older than # this many days — a stale file means the nightly tuner stopped running. Never blocks boot. LEARNED_PARAMS_MAX_AGE_DAYS=3 DISCORD_WEBHOOK_URL= DISCORD_TRADING_WEBHOOK_URL= DISCORD_DEVELOPER_WEBHOOK_URL= # Telegram notifier (bot @traitor_momentum_bot) — gated; no-op if unset TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID='2082795871' # Dead-man's switch (qabot container): the loop's own notifier dies WITH the # loop, so the always-up qabot watches max(events.timestamp) from outside. DEADMAN_STALE_MIN=5 # alert when no events for this many minutes during market hours DEADMAN_DAILY_PING=1 # nightly OK-ping after 20:30 ET — proves the watchdog itself is alive # Nightly pg_dump (db-backup compose service, 20:10 ET Mon-Fri) — the only # automated backup of the production Postgres. See deploy/docker/backup_db.sh. BACKUP_DIR=/data/backups # inside the db-backup container (app-data volume) BACKUP_KEEP_DAYS=14 # prune dumps older than this many days #Market Data these are free plans so work within days/months capacity. Alpha_Vantage_Api_Key= Twelve_Data_Api_Key= Marketstack_Api_Key= Financial_Modeling_Prep_Api_Key= EOD_Historical_Data_Api_Key= #News Data these are free plans so work within days/months capacity. NewsAPI_Api_Key= NewsData.io_Api_Key= Marketaux_Api_Key= MediaStack_Api_Key= #ngrok key ngrok_key= WATCHER_ENABLED=1 # WATCHER_MIN_BARS / WATCHER_READY_SCORE are left to the nightly tuner # (data/learned_params.json). Uncomment to pin a manual override. # WATCHER_MIN_BARS=5 # --- faster loop cadence: react sooner once a setup forms (helps at the margin; # the real lateness is the setup design — see learn_params.py) --- LIVE_BARS_INTERVAL_SECONDS=20 # Live ingest only re-pulls the last few minutes (closed bars never change); the # first pass still covers the whole session so the opening range is present. # This is what fixed the ~46s ingest that froze the single-threaded loop. LIVE_BARS_REFRESH_MINUTES=20 WATCHER_INTERVAL_SECONDS=15 DISCOVER_INTERVAL_SECONDS=120 # --- whole-market premarket squeeze scan (research/ingestion/snapshot_scan.py) --- # The movers/most-actives screeners only see the regular session, so premarket # squeezes were invisible until 09:30 (KITT 2026-09-25 ran at 09:00, found 09:35). # This sweeps every listed, tradable US equity through Alpaca snapshots # 04:00-16:00 ET on its own thread and adds qualifying names to the watchlist # (symbol_discovered source=snapshot_scan). Discovery only; all trading gates apply. # Price band = WATCHER_PRICE_MIN/MAX on the LATEST trade (not the prior close). DISCOVER_SNAPSHOT_SCAN_ENABLED=1 DISCOVER_SNAPSHOT_INTERVAL_SECONDS=45 # one full sweep (~7 requests) DISCOVER_SNAPSHOT_MIN_PCT=20 # % change vs prior close to qualify DISCOVER_SNAPSHOT_MAX_PER_SWEEP=5 # new names injected per sweep (top by %) DISCOVER_SNAPSHOT_MIN_BAR_VOLUME=5000 # shares in the latest minute bar DISCOVER_SNAPSHOT_MIN_CUM_VOLUME=100000 # shares traded today since 04:00 ET DISCOVER_SNAPSHOT_MAX_TRADE_AGE_SECONDS=180 DISCOVER_SNAPSHOT_MAX_BAR_AGE_SECONDS=300 DISCOVER_SNAPSHOT_BATCH=2000 # symbols per request (URL-length bound; 414 at ~12.7k) # Active setups. The backtest sweep found the opening-range breakout is the only # profitable one (others lose and fire later); empty = all setups. See learn_params.py. STRATEGY_SETUPS=opening_range_break # --- news / catalyst ranking ------------------------------------------------ # A fresh catalyst (PR/earnings/FDA/offering) is WHY a small-cap gaps and runs. # These RSS feeds are ingested each discover pass; tickers with recent news are # (1) folded into the watch universe (caught earlier) and (2) boosted up the # armed-trigger ranking. Format: name=url,name=url. Empty = news disabled. NEWS_FEEDS=prnewswire=https://www.prnewswire.com/rss/news-releases-list.rss,businesswire=https://feed.businesswire.com/rss/home/?rss=G1QFDERJXkJeGVtRWQ==,globenewswire=https://www.globenewswire.com/RssFeed/orgclass/1/feedTitle/GlobeNewswire%20-%20News%20about%20Public%20Companies NEWS_RANK_BOOST=2.0 # multiply a catalyst-backed gapper's rank score NEWS_LOOKBACK_HOURS=8 # how fresh the catalyst must be # Manual per-symbol priority nudge (a SMALL boost to names you want watched). # Keep modest (1.1-1.3) so it doesn't override the gap/volume logic. Empty = off. # e.g. SYMBOL_WEIGHTS=SOFI:1.2,RIVN:1.15 SYMBOL_WEIGHTS= # --- fast armed-trigger path (runtime/triggers.py) --------------------------- # Keep the N most-promising gappers "armed" with a pre-computed opening-range # trigger/stop and FIRE the instant live price crosses (a marketable order that # actually fills on a runner) — instead of waiting for a 1-min bar to close and # resting a limit that never fills. This is what catches the move in time. TRIGGER_ENABLED=1 TRIGGER_MAX_ARMED=10 # how many gappers to keep ready at once (batched price fetch scales fine) TRIGGER_INTERVAL_SECONDS=4 # how often to check live prices / fire TRIGGER_ARM_INTERVAL_SECONDS=20 # how often to re-rank + recompute triggers TRIGGER_GAP_MIN=10.0 # aligned with TRADING_MIN_ENTRY_GAP_PCT (population gate) 2026-07-03 TRIGGER_RVOL_MIN=2.0 # min relative volume to be eligible to fire # Entry-quality filters (diagnostic + out-of-sample verified, verify-entry-filters): # capping price at $5 and skipping blow-off gaps (>60%) ~DOUBLED held-out # expectancy (+0.76 vs +0.39R, win 86% vs 71%). $10-20 stocks were net losers; # >150% gaps fade. Fewer, much higher-quality trades. TRIGGER_GAP_MAX=35 # 35-150% band is dead (+0.02R year-scale) — cap earns its keep there # mega-gapper exception (2026-07-06, the LHSW/NXTS fix): re-admit REAL >150% squeezes. # Year replay n=2,879 w/ split artifacts removed: >150% & rvol>=5 ran +0.63R, 8/14 months >=0. TRIGGER_GAP_SQUEEZE_MIN=150 TRIGGER_GAP_SQUEEZE_RVOL=5.0 WATCHER_PRICE_MIN=2 # raised $1->$2 2026-07-09: $1-2 bucket -0.031R over 609 unbiased entries TRADING_PRICE_MIN=2.0 TRADING_PRICE_MAX=20.0 WATCHER_PRICE_MAX=20 # RESTORED to the original $1-20 (2026-06-22). Was narrowed to 5 by a prior backtest ("$10-20 net losers") that the LIVE record contradicts + is n=2 noise; and $5 is why the bot missed NXTS ($9.21, today's +250% winner). Downside is now $500/trade-capped so the wider net is a bounded experiment — measure by price band on the clean sample before re-narrowing. ORB_BARS=5 # opening range = first N regular-hours minutes TRADING_TRIGGER_SLIP_PCT=0.004 # cap chase above the trigger on the fill WATCHER_STATIC_SYMBOLS=TSLA,NVDA,AMD,MSFT,AAPL ACCOUNT_SYNC_ENABLED=0 ORDER_SYNC_ENABLED=0 # --- LIVE PAPER TRADING (enabled for market open) --- ALPACA_PAPER_SYNC_ENABLED=1 TRADING_EXECUTION_ENABLED=1 TRADING_AUTO_APPROVE=1 # 1 = auto-arm bracket orders; 0 = approve each in the dashboard TRADING_MAX_DAILY_LOSS_PCT=0.05 # RE-ARMED 2026-06-22 (was 0.50 testing): circuit breaker halts entries + flattens at -5% equity (~-$4.5k on $90k). Caps a runaway day; per-trade risk is already $500-capped so ~all-stop-out ≈ -5%. Original design was 0.03 — tighten if you want a harder stop. TRADING_RISK_PER_TRADE_PCT=0.01 # risk 1% of equity per trade TRADING_MAX_RISK_DOLLARS=500 # HARD dollar-risk ceiling/trade: risk = min(1% equity, $500). On the ~$90k paper acct this caps the wide-stop ~$900-1k losers from the loss diagnosis at $500. 0=off (small live acct uses the % only) TRADING_MAX_FRESH_ENTRIES_PER_DAY=10 # EXPERIMENT 2026-07-02 (owner-authorized): raised 5->10 real FILLED entries/day (behavior fix counts fills not submits). Data says all-day/more-trades is -EV (afternoon -0.94R); paper A/B only. C+ quality gate + re-entry-on-winners still in force. TRADING_MAX_CONCURRENT_POSITIONS=10 # raised 6->10; the 60% gross-notional cap below still hard-bounds TOTAL exposure (so this = more, smaller positions, not more risk) TRADING_REQUIRE_ABOVE_VWAP=1 # VWAP entry gate: only take breakouts trading >= session VWAP. The ONE validated selection signal (above-VWAP reaches +1R ~1.5x as often, n=3,110 from the labeler). 0=shadow-only (logs below-VWAP signals as rule_type=vwap_below but still enters). Set 0 to revert to pre-gate behavior. TRADING_UNIFIED_ENTRY=1 # run the fast path's anti-chase gates (day-extension: don't enter a name whose breakout level is >30% above the session open; halt) on the LIVE auto path too. Closes the gap where those guards only ran on the rarely-used fast trigger path. Fail-open on missing data. 0=pre-unification (auto path skips these gates). TRADING_GATE_RECEIPTS=1 # emit one entry_gate_evaluated event per gate evaluation (both paths) with the full pass/reject gate map — turns 'why didn't it trade X?' into a query. High-volume diagnostic, purged after 14 days like other observability spam. 0=off (no receipts). TRADING_ENTRY_FILL_MODEL=resting # auto-path fill: 'resting' = rest a limit AT the breakout level (fills only on a pullback, misses runners that gap up). 'marketable' = limit a hair above the trigger so it fills as price breaks UP through it (catches runners, the fast path's behavior; the limit still caps the fill so no gap-chase). VALIDATED LIVE 2026-06-24 open (NOK/SOFI/SSPC filled near-trigger, no chase) then reverted to resting (safe default). Flip to 'marketable' to use it. # Account-aware sizing: positions size off REAL broker equity (auto — so a $300 # live account sizes for $300, not the $100k default). One position's value is # capped to this fraction of equity so N positions fit buying power (~1/N). # 0.16 so 6 positions fit (6 x 16% ~ full account) — esp. the $300 cash account. TRADING_MAX_POSITION_PCT=0.16 # Liquidity cap (shares <= this % of the symbol's day volume) — OFF at small # size; enable (e.g. 0.005) only when scaling to real money to avoid slippage. TRADING_LIQUIDITY_MAX_VOLUME_PCT=0.025 # End-of-day auto-flatten: close everything ~5 min before the bell so the book # never carries overnight gap risk (a day-trading strategy shouldn't hold). TRADING_EOD_FLATTEN_ENABLED=1 TRADING_EOD_FLATTEN_TIME=15:55 # The process is one trading session: after a SUCCESSFUL EOD flatten the loop # exits 0 and compose restarts it fresh, so memory-only daily state (entry cap, # benches, session-closed flags) can never go stale across a date rollover. TRADING_EXIT_AFTER_EOD_FLATTEN=1 TRADING_MAX_ORDERS_PER_TICK=1 TRADING_ENTRY_ORDER_TYPE=limit # rest at the breakout trigger # --- managed exit (swept + out-of-sample verified: trailLow_a1) ------------- # The exit sweep over 187 sessions with a held-out test split found that # trailing the stop under the prior bar's low (after +1R) nearly DOUBLES the # out-of-sample expectancy vs the old fixed +2R target (+0.128R vs +0.067R, # win 49% vs 38%) and is robust, not overfit. So: a far backstop target and let # the trailing stop do the work. See sweep_exits.py / strategy/exits.py. TRADING_REWARD_MULTIPLE=10.0 # far take-profit backstop; the trail is the real exit 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 in profit # Profit-lock checkpoints (percentage of ENTRY): once a trade has been up gain%, # the stop is pinned so it can't close for less than lock% — you always keep a # minimum gain once it's working, and a fade back down sells you out in profit. # Format: gain:lock,gain:lock (whole %). Tunable; raise/lower to taste. TRADING_EXIT_PROFIT_TIERS=8:3,15:9,25:18,40:30 TRADING_EXIT_BREAKEVEN_R=1.0 # ON: lock breakeven once +1R (forward safeguard) TRADING_EXIT_TRAIL_R_STEP=0.25 # 0.25R STEP-TRAIL from the buy price (2026-06-24, supersedes a standalone +2% breakeven): once +0.25R is reached the stop moves to entry (breakeven), +0.5R->+0.25R, +0.75R->+0.5R, ... It locks a small green pop from round-tripping to -1R (the SOFI +$281->-$118 case) AND keeps climbing under a runner. Coexists with the prior-low trail (stop = max of all). Tradeoff: scratches some winners at breakeven on noise (sim ~+0.05R net). Widen to 0.5 if too tight. # TRADING_EXIT_SCALE_OUT_R / _PCT # (off) scaling didn't help out-of-sample # TRADING_EXIT_FIRST_RED=0 # (off) first-red exits were NEGATIVE expectancy here # How far price may dip below the entry trigger before we CANCEL an unfilled # entry. 0.0 (the old default) cancels on any sub-cent wobble -> a breakout that # oscillates around the level gets cancelled before it can fill = pure churn, # never holds a trade. Give it room; the bracket STOP (at the opening-range low) # is the real protection, and risk is capped at 1% per trade by sizing. TRADING_ENTRY_INVALIDATE_PCT=0.015 TRADING_ENTRY_TIMEOUT_BARS=2 # cancel an entry still unfilled after ~2 min # ---- Database (PostgreSQL) — single datastore via local port-proxy to the LAN instance DATABASE_URL= # direct to the LAN host; storage/db_pg.py auto-falls-back between 192.168.1.5 and the 127.0.0.1 netsh tunnel (whichever the WSL net can reach — it flips across reboots). # anti-thrash: bench a symbol after it backs out (unfilled entry) TRADING_BACKOUT_COOLDOWN_SECONDS=180 TRADING_MAX_BACKOUTS_PER_SYMBOL=3 # one round-trip per symbol per day: no re-entry after a position closes TRADING_BLOCK_REENTRY_AFTER_EXIT=1 # liquidity gate: skip breakouts on thin names (recent 5-bar $-volume floor) TRIGGER_MIN_DOLLAR_VOL=8000 # only bench re-entry after a real losing exit (>1%), not a scratch/win TRADING_REENTRY_MIN_LOSS_PCT=0.01 TRADING_MAX_GROSS_NOTIONAL_PCT=0.60 ALPACA_BACKFILL_FEED=sip INGEST_STALE_SECONDS=180 INGEST_STALE_BLOCK=0 TRIGGER_PRICE_MAX_AGE=12 # ============================================================ # Local LLM news/catalyst enrichment (Ollama) — TEST SPOT # Detected running at the Windows host (192.168.1.9:11434). # Phase-1 advisory only; trading behaviour does NOT change. If Ollama is down or # the model is missing it degrades to a no-op on its own thread (never impedes). # ============================================================ OLLAMA_ENABLED=1 OLLAMA_HOST=http://192.168.1.5:30068 # direct to host (WSL tunnel flips; client also falls back) OLLAMA_MODEL=qwen2.5:7b-instruct OLLAMA_TIMEOUT_SECONDS=60 NEWS_ENRICH_ENABLED=1 NEWS_ENRICH_INTERVAL_SECONDS=120 NEWS_ENRICH_LOOKBACK_HOURS=12 NEWS_ENRICH_BATCH_LIMIT=50 # Phase-2 automation stays OFF until you've watched a clean advisory run: NEWS_CATALYST_SCORE_ENABLED=1 NEWS_DILUTION_VETO_ENABLED=1 OLLAMA_TEMPERATURE=0.1 TRADING_MIN_ENTRY_GAP_PCT=0.10 # POPULATION gate (year-long +EV boundary, n=3008/14mo): only real gappers (open vs prev close >= 10%) are tradeable; all rank x timing segments +0.05..+0.15R under the BE@+5% ratchet TRADING_MIN_DAY_VOLUME=300000 # population volume floor (shares, consolidated SIP): thin names whipsaw; fail-open on missing data TRADING_ENTRY_CONFIRM_BY_MINUTE=0 # OFF 2026-07-03: year-long replay (n=3008, 14mo) REFUTED the 15-min gate — late crosses beat early 12/14 months; population is the real +EV boundary (see below) TRADING_MIN_QUALITY_SCORE=0.60 # C or higher (owner 2026-07-07 after F-tier experiment; grade map: A>=.9 B>=.75 C>=.6 D>=.4). Old 0.50 was mislabeled C+ — actually mid-D # YouTube claim-extraction pipeline YOUTUBE_API_KEY= YT_WATCHLIST= ALPACA_DATA_FEED=sip # Algo Trader Plus (paid 2026-07-02, entitlement verified 07-03): real-time consolidated tape. Flip back to iex if the plan lapses — the ratio estimator self-reactivates.