The session-level metalabel P(our ORB profits today) — the one lever that survives out-of-sample. Backtested on 266 prod sessions (walk-forward): trade-always (trained subset): -0.111R/session, 36% hit meta_v1 GATED (trades best ~32% of days): -0.004R/session, 48% hit delta +0.107R in-sample, +0.128R OOS (recent 80 sessions), improves across folds It does NOT make the bot profitable — it takes a bleeding strategy to roughly flat by sitting out the worst ~2/3 of sessions. threshold_v1 rule gates too little to matter; meta_v1 (walk-forward GBM) is the signal. Leaky full-fit (+0.61R) >> walk-forward confirms the discipline was load-bearing; the OOS holding is the evidence the +0.078R is real, not look-ahead. Changes: - research/regime.py: batch minute-bar load per session (64k per-fire round-trips -> ~266 queries; fixed a >5min build to ~3min) + incremental label pass (skip already-labeled sessions; --rebuild forces full). Nightly cost now ~one new day. - docker-compose.yml + truenas-compose.yml: scheduler runs build-features + score (both models) before `experiments run`, nightly. ALL-SHADOW — does NOT gate live trading (that stays owner-gated Step 5). - admin_api.py + admin.html: POST /api/regime/run (detached) + GET /api/regime/status + a Run/Status button — the panel had no way to trigger this. - plans/011 + docs/research/regime_notes.md (6-book distillation) + plans 008/009 docs. Prod state (shadow, rollback = DROP regime_features/regime_scores + DELETE experiments WHERE id LIKE 'regime%'): 266 feature rows, 532 scores, 2 experiments registered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
263 lines
11 KiB
YAML
263 lines
11 KiB
YAML
# Momentum stack for TrueNAS SCALE (Apps -> Discover Apps -> Custom App -> YAML).
|
|
#
|
|
# PULL-ONLY: TrueNAS custom apps cannot `build:` — the image comes from GHCR
|
|
# (published by .github/workflows/docker.yml on every master push). Make the
|
|
# ghcr.io/fez5587/momentum-app package PUBLIC once (GitHub -> Packages ->
|
|
# momentum-app -> Package settings -> Change visibility), or add registry
|
|
# credentials in TrueNAS (Apps -> Settings -> Manage Container Images).
|
|
#
|
|
# BEFORE DEPLOYING:
|
|
# 1. Create a dataset for the stack, e.g. /mnt/<pool>/apps/momentum
|
|
# with subdirs: data/ pgdata/
|
|
# 2. Replace /mnt/tank/apps/momentum below with your dataset path.
|
|
# 3. Fill every <FILL-ME> in the environment block (these are the secrets
|
|
# you wanted to manage from a protected space — TrueNAS stores the YAML
|
|
# server-side, or mount an env file from a restricted dataset instead).
|
|
#
|
|
# TZ is America/New_York: the loop's market clock thinks in ET.
|
|
|
|
x-app: &app
|
|
image: docker.io/fez5587/momentum-app:latest # also published to ghcr.io/fez5587/momentum-app
|
|
environment: &app-env
|
|
ALPACA_API_KEY: '<FILL-ME>'
|
|
ALPACA_BACKFILL_FEED: 'sip'
|
|
ALPACA_BASE_URL: 'https://paper-api.alpaca.markets/v2'
|
|
ALPACA_DATA_FEED: 'sip'
|
|
ALPACA_PAPER_SYNC_ENABLED: '1'
|
|
ALPACA_SECRET_KEY: '<FILL-ME>'
|
|
DATABASE_URL: 'postgresql://momentum:<FILL-ME-PG-PASSWORD>@postgres:5432/momentum'
|
|
DEADMAN_DAILY_PING: '1'
|
|
DEADMAN_STALE_MIN: '5'
|
|
DISCOVER_INTERVAL_SECONDS: '120'
|
|
FMP_API_KEY: '<FILL-ME>'
|
|
INGEST_STALE_BLOCK: '0'
|
|
INGEST_STALE_SECONDS: '180'
|
|
LIVE_BARS_INTERVAL_SECONDS: '20'
|
|
LIVE_BARS_REFRESH_MINUTES: '20'
|
|
NEWS_CATALYST_SCORE_ENABLED: '1'
|
|
NEWS_DILUTION_VETO_ENABLED: '1'
|
|
NEWS_ENRICH_BATCH_LIMIT: '50'
|
|
NEWS_ENRICH_ENABLED: '1'
|
|
NEWS_ENRICH_INTERVAL_SECONDS: '120'
|
|
NEWS_ENRICH_LOOKBACK_HOURS: '12'
|
|
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_LOOKBACK_HOURS: '8'
|
|
NEWS_RANK_BOOST: '2.0'
|
|
OLLAMA_ENABLED: '1'
|
|
OLLAMA_HOST: 'http://192.168.1.5:30068'
|
|
OLLAMA_MODEL: 'qwen2.5:7b-instruct'
|
|
OLLAMA_TEMPERATURE: '0.1'
|
|
OLLAMA_TIMEOUT_SECONDS: '60'
|
|
ORB_BARS: '5'
|
|
PG_SKIP_SCHEMA: '0'
|
|
STRATEGY_SETUPS: 'opening_range_break'
|
|
TELEGRAM_BOT_TOKEN: '<FILL-ME>'
|
|
TELEGRAM_CHAT_ID: "'2082795871'"
|
|
TRADING_AUTO_APPROVE: '1'
|
|
TRADING_BACKOUT_COOLDOWN_SECONDS: '180'
|
|
TRADING_BLOCK_REENTRY_AFTER_EXIT: '1'
|
|
TRADING_ENTRY_CONFIRM_BY_MINUTE: '0'
|
|
TRADING_ENTRY_FILL_MODEL: 'resting'
|
|
TRADING_ENTRY_INVALIDATE_PCT: '0.015'
|
|
TRADING_ENTRY_ORDER_TYPE: 'limit'
|
|
TRADING_ENTRY_TIMEOUT_BARS: '2'
|
|
TRADING_EOD_FLATTEN_ENABLED: '1'
|
|
TRADING_EOD_FLATTEN_TIME: '15:55'
|
|
TRADING_EXECUTION_ENABLED: '1'
|
|
TRADING_EXIT_BREAKEVEN_R: '1.0'
|
|
TRADING_EXIT_PROFIT_TIERS: '8:3,15:9,25:18,40:30'
|
|
TRADING_EXIT_TRAIL_AFTER_R: '1.0'
|
|
TRADING_EXIT_TRAIL_MODE: 'prior_low'
|
|
TRADING_EXIT_TRAIL_R_STEP: '0.25'
|
|
TRADING_LIQUIDITY_MAX_VOLUME_PCT: '0.025'
|
|
TRADING_MAX_BACKOUTS_PER_SYMBOL: '3'
|
|
TRADING_MAX_CONCURRENT_POSITIONS: '10'
|
|
TRADING_MAX_DAILY_LOSS_PCT: '0.05'
|
|
TRADING_MAX_FRESH_ENTRIES_PER_DAY: '10'
|
|
TRADING_MAX_GROSS_NOTIONAL_PCT: '0.60'
|
|
TRADING_MAX_ORDERS_PER_TICK: '1'
|
|
TRADING_MAX_POSITION_PCT: '0.16'
|
|
TRADING_MAX_RISK_DOLLARS: '500'
|
|
TRADING_MIN_DAY_VOLUME: '300000'
|
|
TRADING_MIN_ENTRY_GAP_PCT: '0.10'
|
|
TRADING_MIN_QUALITY_SCORE: '0.60'
|
|
TRADING_PRICE_MAX: '20.0'
|
|
TRADING_PRICE_MIN: '2.0'
|
|
TRADING_REENTRY_MIN_LOSS_PCT: '0.01'
|
|
TRADING_REQUIRE_ABOVE_VWAP: '1'
|
|
TRADING_REWARD_MULTIPLE: '10.0'
|
|
TRADING_RISK_PER_TRADE_PCT: '0.01'
|
|
TRADING_SESSION_ONLY: '1'
|
|
TRADING_TRIGGER_SLIP_PCT: '0.004'
|
|
TRADING_UNIFIED_ENTRY: '1'
|
|
TRIGGER_ARM_INTERVAL_SECONDS: '20'
|
|
TRIGGER_ENABLED: '1'
|
|
TRIGGER_GAP_MAX: '35'
|
|
TRIGGER_GAP_MIN: '10.0'
|
|
TRIGGER_GAP_SQUEEZE_MIN: '150'
|
|
TRIGGER_GAP_SQUEEZE_RVOL: '5.0'
|
|
TRIGGER_INTERVAL_SECONDS: '4'
|
|
TRIGGER_MAX_ARMED: '10'
|
|
TRIGGER_MIN_DOLLAR_VOL: '8000'
|
|
TRIGGER_PRICE_MAX_AGE: '12'
|
|
TRIGGER_RVOL_MIN: '2.0'
|
|
TZ: 'America/New_York'
|
|
WATCHER_ENABLED: '1'
|
|
WATCHER_INTERVAL_SECONDS: '15'
|
|
WATCHER_PRICE_MAX: '20'
|
|
WATCHER_PRICE_MIN: '2'
|
|
YOUTUBE_API_KEY: '<FILL-ME>'
|
|
volumes:
|
|
- /mnt/volume/Apps/momentum/data:/app/data
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
restart: unless-stopped
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:18
|
|
environment:
|
|
POSTGRES_USER: momentum
|
|
POSTGRES_PASSWORD: <FILL-ME-PG-PASSWORD>
|
|
POSTGRES_DB: momentum
|
|
PGDATA: /var/lib/postgresql/data/pgdata
|
|
volumes:
|
|
- /mnt/volume/Apps/momentum/pgdata:/var/lib/postgresql/data
|
|
ports:
|
|
# published on 5433 so it can coexist with the EXISTING postgres (5432)
|
|
# during migration — pgAdmin/pg_restore reach the new instance on 5433.
|
|
# In-stack services still use postgres:5432 (the container port).
|
|
- "5433:5432"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U momentum -d momentum"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
restart: unless-stopped
|
|
|
|
loop:
|
|
<<: *app
|
|
command: ["python", "-u", "run_live_paper.py", "--no-dashboard"]
|
|
healthcheck:
|
|
# PG_SKIP_SCHEMA=1: a healthcheck must NOT re-apply schema DDL — the 60s
|
|
# CREATE-IF-NOT-EXISTS lock burst cancelled minute_bars autovacuum forever
|
|
# (2026-07-09 post-migration: stats rotted, dashboard queries crawled)
|
|
test: ["CMD", "python", "-c", "import os; os.environ['PG_SKIP_SCHEMA']='1'; import datetime,sys; from research.multi_schema import open_research_db; r=open_research_db('market').execute('SELECT max(timestamp) FROM events').fetchall()[0][0]; sys.exit(0 if r and (datetime.datetime.now()-r).total_seconds()<300 else 1)"]
|
|
interval: 60s
|
|
timeout: 20s
|
|
retries: 5
|
|
start_period: 180s
|
|
|
|
dashboard:
|
|
<<: *app
|
|
command: ["python", "-u", "dashboard_api.py"]
|
|
environment:
|
|
<<: *app-env
|
|
DASHBOARD_HOST: "0.0.0.0"
|
|
DASHBOARD_PORT: "8765"
|
|
# read-only viewer: never re-apply the 473-line schema (the SSE change
|
|
# detector holds one long-lived connection) — mirrors the loop healthcheck.
|
|
PG_SKIP_SCHEMA: '1'
|
|
# Shared secret required on every POST (approve/reject/exit + the command
|
|
# bus). Empty = commands disabled + POSTs unauthenticated; fill to arm the
|
|
# kill switch. Reference by name only — never inline the secret value.
|
|
DASHBOARD_TOKEN: <FILL-ME-DASHBOARD-TOKEN>
|
|
# Gate the operator command bus on (also needs DASHBOARD_TOKEN set).
|
|
COMMAND_BUS_ENABLED: '1'
|
|
|
|
qabot:
|
|
<<: *app
|
|
command: ["python", "-u", "telegram_bot.py"]
|
|
|
|
transcripts:
|
|
<<: *app
|
|
command: ["python", "-u", "-m", "youtube_claims.run", "run"]
|
|
|
|
admin:
|
|
<<: *app
|
|
command: ["python", "-u", "admin_api.py"]
|
|
environment:
|
|
<<: *app-env
|
|
ADMIN_PORT: "8767"
|
|
ADMIN_TOKEN: <FILL-ME-ADMIN-TOKEN>
|
|
ports:
|
|
- "8767:8767"
|
|
volumes:
|
|
- /mnt/volume/Apps/momentum/data:/app/data
|
|
- /var/run/docker.sock:/var/run/docker.sock # service status + restarts
|
|
|
|
scheduler:
|
|
<<: *app
|
|
command:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
echo "[scheduler] up; fires 20:00 ET Mon-Fri"
|
|
while true; do
|
|
if [ "$$(date +%H:%M)" = "20:00" ] && [ "$$(date +%u)" -le 5 ]; then
|
|
python -m research.premarket_eval || echo "[scheduler] eval FAILED"
|
|
# SPY/QQQ ingest -> forward source for the labeler's market-context features (plan 009)
|
|
python -m research.ingestion.market_context || echo "[scheduler] market_context ingest FAILED"
|
|
python -m research.eod_reconcile || echo "[scheduler] reconcile FAILED"
|
|
# Flywheel: grow the whole-market dataset first, then fit the tuner on
|
|
# it. 20:00 ET is after close, so replay of "today" is complete data —
|
|
# NOT look-ahead (replay is a labeling job, not a live gate).
|
|
python eod_replay.py || echo "[scheduler] eod_replay FAILED"
|
|
python nightly_tune.py || echo "[scheduler] nightly_tune FAILED"
|
|
# Regime gate (plan 011, ALL-SHADOW): a session-level metalabel
|
|
# P(our ORB profits today). build-features labels each session from its
|
|
# OWN realized expR; score writes the WALK-FORWARD favorable flag the
|
|
# experiments' regime_gate intervention reads. Must run BEFORE
|
|
# experiments run. Does NOT gate live trading (that is owner-gated).
|
|
python -m research.regime build-features || echo "[scheduler] regime build-features FAILED"
|
|
python -m research.regime score --model threshold_v1 || echo "[scheduler] regime score threshold FAILED"
|
|
python -m research.regime score --model meta_v1 || echo "[scheduler] regime score meta FAILED"
|
|
# Experiment OS (plan 006, ALL-SHADOW): replay running experiments over
|
|
# newly-logged fires, then adjudicate against pre-registered bars.
|
|
python -m research.experiments run || echo "[scheduler] experiments run FAILED"
|
|
python -m research.experiments adjudicate || echo "[scheduler] experiments adjudicate FAILED"
|
|
sleep 120
|
|
fi
|
|
sleep 20
|
|
done
|
|
|
|
db-backup:
|
|
# Nightly pg_dump at 20:10 ET Mon-Fri, on the postgres:18 image so the
|
|
# client always matches the server major (the app image has NO pg_dump).
|
|
# This host is pull-only (no repo checkout), so the backup logic is INLINE
|
|
# — keep it in sync with deploy/docker/backup_db.sh. Restore:
|
|
# gunzip -c momentum-<date>.sql.gz | psql "$DATABASE_URL"
|
|
image: postgres:18
|
|
restart: unless-stopped
|
|
environment:
|
|
DATABASE_URL: 'postgresql://momentum:<FILL-ME-PG-PASSWORD>@postgres:5432/momentum'
|
|
BACKUP_DIR: '/backups'
|
|
BACKUP_KEEP_DAYS: '14'
|
|
TZ: 'America/New_York' # postgres image defaults to UTC; the 20:10 gate is ET
|
|
volumes:
|
|
- /mnt/volume/Apps/momentum/data/backups:/backups
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
command:
|
|
- /bin/bash
|
|
- -c
|
|
- |
|
|
set -o pipefail
|
|
echo "[backup] up; fires 20:10 ET Mon-Fri"
|
|
while true; do
|
|
if [ "$$(date +%H:%M)" = "20:10" ] && [ "$$(date +%u)" -le 5 ]; then
|
|
out="$$BACKUP_DIR/momentum-$$(date +%F).sql.gz"
|
|
mkdir -p "$$BACKUP_DIR" \
|
|
&& pg_dump "$$DATABASE_URL" | gzip > "$$out.tmp" \
|
|
&& gunzip -t "$$out.tmp" && mv "$$out.tmp" "$$out" \
|
|
&& find "$$BACKUP_DIR" -name 'momentum-*.sql.gz*' -mtime +"$$BACKUP_KEEP_DAYS" -delete \
|
|
&& date > "$$BACKUP_DIR/last_backup_ok" \
|
|
&& echo "[backup] OK $$out" \
|
|
|| echo "[backup] FAILED"
|
|
sleep 120
|
|
fi
|
|
sleep 20
|
|
done
|