Files
momentum-system/.env.example
T
momentum devandClaude Fable 5 07570fb0e4 operator-plane: fix dashboard POST NameError (import os/hmac)
Every POST endpoint (approve/reject/exit/watch) crashed with NameError:
do_POST references os.environ + hmac.compare_digest but neither was imported.
Adds the imports, a non-loopback regression test for the token gate (no token
-> routes; token set -> 401 without/with-wrong header, 200 with correct), and
plumbs DASHBOARD_TOKEN + PG_SKIP_SCHEMA=1 into both compose dashboard services
(+ .env.example docs for DASHBOARD_TOKEN / COMMAND_BUS_ENABLED).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:04:01 -04:00

239 lines
16 KiB
Bash

# ============================================================
# Momentum — environment configuration (copy to .env and fill)
# Regenerated 2026-07-09 from the live config. Values kept here
# are the current tuned defaults; <REDACTED> = secret, fill from
# your protected store. See deploy/docker/README.md for which
# layer (compose vs .env) each group belongs in.
# ============================================================
TOKEN_ENCRYPTION_KEY=<REDACTED>
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=<REDACTED>
ALPACA_SECRET_KEY=<REDACTED>
ALPACA_BASE_URL=https://paper-api.alpaca.markets/v2
# Polygon (optional fallback)
POLYGON_API_KEY=<REDACTED>
# Additional news and filing sources
FINNHUB_API_KEY=<REDACTED>
FINNHUB_EMAIL=fez5587@gmail.com
SEC_API_KEY=<REDACTED>
DATABENTO_API_KEY=<REDACTED>
# 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=<REDACTED>
DISCORD_TRADING_WEBHOOK_URL=<REDACTED>
DISCORD_DEVELOPER_WEBHOOK_URL=<REDACTED>
# Telegram notifier (bot @traitor_momentum_bot) — gated; no-op if unset
TELEGRAM_BOT_TOKEN=<REDACTED>
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=<REDACTED>
Twelve_Data_Api_Key=<REDACTED>
Marketstack_Api_Key=<REDACTED>
Financial_Modeling_Prep_Api_Key=<REDACTED>
EOD_Historical_Data_Api_Key=<REDACTED>
#News Data these are free plans so work within days/months capacity.
NewsAPI_Api_Key=<REDACTED>
NewsData.io_Api_Key=<REDACTED>
Marketaux_Api_Key=<REDACTED>
MediaStack_Api_Key=<REDACTED>
#ngrok key
ngrok_key=<REDACTED>
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
# 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=<REDACTED> # 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=<REDACTED>
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.