fix: marketable entries price from live last + time-of-day rvol arms fast path
Day-22 forensic (BIYA session): 6 entry submissions, 0 fills — IREX missed +1.7R, IRE +1.65R by one cent. Two root causes fixed: 1. Slow-path limit rested AT the trigger while price ran 2% past it, and retries reused the same stale price. Marketable model now prices from max(trigger, live last) via price_provider; the anti-chase extension gate gets the live ref too (was an entry_ref==trigger no-op on this path). TRADING_ENTRY_FILL_MODEL flipped to marketable. 2. Fast trigger path armed nothing all day: legacy rvol (cum-so-far / full-day MEAN) is ~0.1-0.3 minutes after the open for a normal heavy name and its mean is poisoned by reverse-split/prior-squeeze days (BIYA legacy 1.27 vs real activity 481x median). New rvol_tod = cum / (MEDIAN(20d) * elapsed-session fraction) drives arm eligibility and the scanner keep-filter; legacy rvol untouched for validated boundaries (gap-squeeze rvol>=5). Also: stagnation time-stop (TRADING_EXIT_STAGNATION_MIN/_R, default OFF) in simulate_exit + manage_live with sweep_exits.py variants for validation before any live enable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f10a8033a1
commit
5597250ff1
11 files changed
+894
-8
No files matched your search
@@ -0,0 +1,687 @@
|
||||
# TrueNAS SCALE custom-app compose - FLATTENED (no YAML anchors).
|
||||
# TrueNAS's app validator REJECTS anchors (x-app / <<: *app) with
|
||||
# 'Invalid YAML provided', so every service carries its own full env.
|
||||
# Anchored/DRY source of truth: deploy/docker/truenas-compose.yml.
|
||||
# Fill every <FILL-ME-*> before deploying (PG password repeats in all
|
||||
# DATABASE_URL blocks + postgres + db-backup). Regenerated? keep both in sync.
|
||||
#
|
||||
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:
|
||||
- 5433:5432
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready -U momentum -d momentum
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
loop:
|
||||
image: docker.io/fez5587/momentum-app:latest
|
||||
command:
|
||||
- python
|
||||
- -u
|
||||
- run_live_paper.py
|
||||
- --no-dashboard
|
||||
environment:
|
||||
ALPACA_API_KEY: <FILL-ME-ALPACA-KEY>
|
||||
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-ALPACA-SECRET>
|
||||
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-FMP-KEY>
|
||||
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: '1'
|
||||
STRATEGY_SETUPS: opening_range_break
|
||||
TELEGRAM_BOT_TOKEN: <FILL-ME-TELEGRAM-TOKEN>
|
||||
TELEGRAM_CHAT_ID: <FILL-ME-TELEGRAM-CHAT-ID>
|
||||
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: marketable
|
||||
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_REGIME_GATE_ENABLED: '1'
|
||||
TRADING_REGIME_GATE_P_MIN: '0.5'
|
||||
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-YOUTUBE-KEY>
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
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:
|
||||
image: docker.io/fez5587/momentum-app:latest
|
||||
command:
|
||||
- python
|
||||
- -u
|
||||
- dashboard_api.py
|
||||
environment:
|
||||
ALPACA_API_KEY: <FILL-ME-ALPACA-KEY>
|
||||
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-ALPACA-SECRET>
|
||||
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-FMP-KEY>
|
||||
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: '1'
|
||||
STRATEGY_SETUPS: opening_range_break
|
||||
TELEGRAM_BOT_TOKEN: <FILL-ME-TELEGRAM-TOKEN>
|
||||
TELEGRAM_CHAT_ID: <FILL-ME-TELEGRAM-CHAT-ID>
|
||||
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: marketable
|
||||
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_REGIME_GATE_ENABLED: '1'
|
||||
TRADING_REGIME_GATE_P_MIN: '0.5'
|
||||
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-YOUTUBE-KEY>
|
||||
DASHBOARD_HOST: 0.0.0.0
|
||||
DASHBOARD_PORT: '8765'
|
||||
DASHBOARD_TOKEN: <FILL-ME-DASHBOARD-TOKEN>
|
||||
COMMAND_BUS_ENABLED: '1'
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8765:8765
|
||||
qabot:
|
||||
image: docker.io/fez5587/momentum-app:latest
|
||||
command:
|
||||
- python
|
||||
- -u
|
||||
- telegram_bot.py
|
||||
environment:
|
||||
ALPACA_API_KEY: <FILL-ME-ALPACA-KEY>
|
||||
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-ALPACA-SECRET>
|
||||
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-FMP-KEY>
|
||||
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: '1'
|
||||
STRATEGY_SETUPS: opening_range_break
|
||||
TELEGRAM_BOT_TOKEN: <FILL-ME-TELEGRAM-TOKEN>
|
||||
TELEGRAM_CHAT_ID: <FILL-ME-TELEGRAM-CHAT-ID>
|
||||
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: marketable
|
||||
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_REGIME_GATE_ENABLED: '1'
|
||||
TRADING_REGIME_GATE_P_MIN: '0.5'
|
||||
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-YOUTUBE-KEY>
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
transcripts:
|
||||
image: docker.io/fez5587/momentum-app:latest
|
||||
command:
|
||||
- python
|
||||
- -u
|
||||
- -m
|
||||
- youtube_claims.run
|
||||
- run
|
||||
environment:
|
||||
ALPACA_API_KEY: <FILL-ME-ALPACA-KEY>
|
||||
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-ALPACA-SECRET>
|
||||
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-FMP-KEY>
|
||||
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: '1'
|
||||
STRATEGY_SETUPS: opening_range_break
|
||||
TELEGRAM_BOT_TOKEN: <FILL-ME-TELEGRAM-TOKEN>
|
||||
TELEGRAM_CHAT_ID: <FILL-ME-TELEGRAM-CHAT-ID>
|
||||
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: marketable
|
||||
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_REGIME_GATE_ENABLED: '1'
|
||||
TRADING_REGIME_GATE_P_MIN: '0.5'
|
||||
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-YOUTUBE-KEY>
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
admin:
|
||||
image: docker.io/fez5587/momentum-app:latest
|
||||
command:
|
||||
- python
|
||||
- -u
|
||||
- admin_api.py
|
||||
environment:
|
||||
ALPACA_API_KEY: <FILL-ME-ALPACA-KEY>
|
||||
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-ALPACA-SECRET>
|
||||
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-FMP-KEY>
|
||||
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: '1'
|
||||
STRATEGY_SETUPS: opening_range_break
|
||||
TELEGRAM_BOT_TOKEN: <FILL-ME-TELEGRAM-TOKEN>
|
||||
TELEGRAM_CHAT_ID: <FILL-ME-TELEGRAM-CHAT-ID>
|
||||
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: marketable
|
||||
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_REGIME_GATE_ENABLED: '1'
|
||||
TRADING_REGIME_GATE_P_MIN: '0.5'
|
||||
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-YOUTUBE-KEY>
|
||||
ADMIN_PORT: '8767'
|
||||
ADMIN_TOKEN: <FILL-ME-ADMIN-TOKEN>
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data:/app/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8767:8767
|
||||
scheduler:
|
||||
image: docker.io/fez5587/momentum-app:latest
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- "echo \"[scheduler] up; fires 20:00 ET Mon-Fri\"\nwhile true; do\n if [ \"$$(date +%H:%M)\" = \"20:00\" ] && [ \"$$(date +%u)\" -le 5 ]; then\n python -m research.premarket_eval || echo \"[scheduler] eval FAILED\"\n python -m research.ingestion.market_context || echo \"[scheduler] market_context FAILED\"\n python -m research.eod_reconcile || echo \"[scheduler] reconcile FAILED\"\n python eod_replay.py || echo \"[scheduler] eod_replay FAILED\"\n python nightly_tune.py || echo \"[scheduler] nightly_tune FAILED\"\n python -m research.regime build-features || echo \"[scheduler] regime build FAILED\"\n python -m research.regime score --model threshold_v1 || echo \"[scheduler] regime threshold FAILED\"\n python -m research.regime score --model meta_v1 || echo \"[scheduler] regime meta FAILED\"\n python -m research.experiments run || echo \"[scheduler] experiments run FAILED\"\n python -m research.experiments adjudicate || echo \"[scheduler] experiments adjudicate FAILED\"\n sleep 120\n fi\n sleep 20\ndone\n"
|
||||
environment:
|
||||
ALPACA_API_KEY: <FILL-ME-ALPACA-KEY>
|
||||
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-ALPACA-SECRET>
|
||||
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-FMP-KEY>
|
||||
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: '1'
|
||||
STRATEGY_SETUPS: opening_range_break
|
||||
TELEGRAM_BOT_TOKEN: <FILL-ME-TELEGRAM-TOKEN>
|
||||
TELEGRAM_CHAT_ID: <FILL-ME-TELEGRAM-CHAT-ID>
|
||||
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: marketable
|
||||
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_REGIME_GATE_ENABLED: '1'
|
||||
TRADING_REGIME_GATE_P_MIN: '0.5'
|
||||
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-YOUTUBE-KEY>
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
db-backup:
|
||||
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
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data/backups:/backups
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- "set -o pipefail\necho \"[backup] up; fires 20:10 ET Mon-Fri\"\nwhile true; do\n if [ \"$$(date +%H:%M)\" = \"20:10\" ] && [ \"$$(date +%u)\" -le 5 ]; then\n out=\"$$BACKUP_DIR/momentum-$$(date +%F).sql.gz\"\n mkdir -p \"$$BACKUP_DIR\" && pg_dump \"$$DATABASE_URL\" | gzip > \"$$out.tmp\" && gunzip -t \"$$out.tmp\" && mv \"$$out.tmp\" \"$$out\" && echo \"[backup] OK\" || echo \"[backup] FAILED\"\n sleep 120\n fi\n sleep 20\ndone\n"
|
||||
|
||||
@@ -24,6 +24,28 @@ class GapperCandidate:
|
||||
vwap: float | None
|
||||
prev_close: float | None = None # for the STATIC overnight gap (open vs prev close)
|
||||
rank: int = 0
|
||||
# Time-of-day rvol: cum volume vs MEDIAN(20d) prorated by session elapsed time.
|
||||
# The legacy relative_volume divides cum-so-far by the full-day MEAN, which is
|
||||
# (a) ~0.1-0.3 for a NORMAL heavy name minutes after the open (cum can't reach
|
||||
# a full-day average yet) and (b) poisoned by reverse-split / prior-squeeze
|
||||
# days in the mean (BIYA 07-20: legacy 1.27 vs tod 481 — median 1.5M, mean
|
||||
# 29.4M). None when no baseline. Legacy relative_volume is kept untouched —
|
||||
# validated boundaries (gap-squeeze rvol>=5) are calibrated in its units.
|
||||
relative_volume_tod: float | None = None
|
||||
|
||||
|
||||
def _session_elapsed_fraction(now: datetime | None = None) -> float:
|
||||
"""Fraction of the 390-min regular session elapsed, floored at 0.05 so a
|
||||
premarket/at-open scan doesn't divide by ~zero expected volume."""
|
||||
try:
|
||||
from zoneinfo import ZoneInfo
|
||||
et = (now or datetime.now(ZoneInfo("America/New_York")))
|
||||
if et.tzinfo is not None:
|
||||
et = et.astimezone(ZoneInfo("America/New_York"))
|
||||
elapsed = (et.hour - 9) * 60 + (et.minute - 30)
|
||||
except Exception: # noqa: BLE001 — fall back to full-day (legacy behaviour)
|
||||
return 1.0
|
||||
return min(1.0, max(0.05, elapsed / 390.0))
|
||||
|
||||
|
||||
def scan_gappers(
|
||||
@@ -34,6 +56,7 @@ def scan_gappers(
|
||||
price_min: float = 1.0,
|
||||
price_max: float = 20.0,
|
||||
limit: int = 25,
|
||||
now: datetime | None = None,
|
||||
) -> list[GapperCandidate]:
|
||||
"""Find symbols gapping up on elevated volume for a session."""
|
||||
rows = con.execute(
|
||||
@@ -56,7 +79,8 @@ def scan_gappers(
|
||||
WHERE trade_date < ?
|
||||
),
|
||||
avg_vol AS (
|
||||
SELECT symbol, AVG(volume) AS adv
|
||||
SELECT symbol, AVG(volume) AS adv,
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY volume) AS med_vol
|
||||
FROM (
|
||||
SELECT symbol, volume,
|
||||
ROW_NUMBER() OVER (
|
||||
@@ -69,7 +93,7 @@ def scan_gappers(
|
||||
GROUP BY symbol
|
||||
)
|
||||
SELECT l.symbol, l.close, l.vwap, l.cum_volume,
|
||||
p.prev_close, a.adv
|
||||
p.prev_close, a.adv, a.med_vol
|
||||
FROM latest l
|
||||
JOIN prev p ON p.symbol = l.symbol AND p.rn = 1
|
||||
LEFT JOIN avg_vol a ON a.symbol = l.symbol
|
||||
@@ -78,13 +102,21 @@ def scan_gappers(
|
||||
[session_date, session_date, session_date, price_min, price_max],
|
||||
).fetchall()
|
||||
|
||||
frac = _session_elapsed_fraction(now)
|
||||
candidates: list[GapperCandidate] = []
|
||||
for symbol, close, vwap, cum_volume, prev_close, adv in rows:
|
||||
for symbol, close, vwap, cum_volume, prev_close, adv, med_vol in rows:
|
||||
if not prev_close or prev_close <= 0:
|
||||
continue
|
||||
gap_pct = (float(close) - float(prev_close)) / float(prev_close) * 100.0
|
||||
rvol = (float(cum_volume) / float(adv)) if adv and adv > 0 else 0.0
|
||||
if gap_pct >= min_gap_pct and rvol >= min_relative_volume:
|
||||
rvol_tod = (float(cum_volume) / (float(med_vol) * frac)
|
||||
if med_vol and float(med_vol) > 0 else None)
|
||||
# either rvol flavour clears the bar: legacy is ~0.1-0.3 early in the
|
||||
# session even for a heavy name (cum-so-far vs full-day mean), so the
|
||||
# tod variant keeps real early movers from being silently dropped
|
||||
if gap_pct >= min_gap_pct and (
|
||||
rvol >= min_relative_volume
|
||||
or (rvol_tod or 0.0) >= min_relative_volume):
|
||||
candidates.append(
|
||||
GapperCandidate(
|
||||
symbol=symbol,
|
||||
@@ -94,6 +126,7 @@ def scan_gappers(
|
||||
cumulative_volume=int(cum_volume),
|
||||
relative_volume=rvol,
|
||||
vwap=float(vwap) if vwap is not None else None,
|
||||
relative_volume_tod=rvol_tod,
|
||||
)
|
||||
)
|
||||
candidates.sort(key=lambda c: (c.gap_pct * max(c.relative_volume, 0.1)), reverse=True)
|
||||
|
||||
@@ -960,6 +960,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"gap_pct": g.gap_pct,
|
||||
"gap_open_pct": gap_open_pct,
|
||||
"rvol": g.relative_volume,
|
||||
"rvol_tod": g.relative_volume_tod,
|
||||
"trigger": hi,
|
||||
"stop": stop,
|
||||
"range_pct": ((hi - lo) / hi) if (hi and lo and hi > 0) else 0.0,
|
||||
|
||||
+11
-1
@@ -56,6 +56,12 @@ class ArmedTrigger:
|
||||
gap_pct: float = 0.0 # DYNAMIC gap (latest vs prev close) — display/rank only
|
||||
gap_open_pct: float | None = None # STATIC overnight gap (session open vs prev close)
|
||||
rvol: float = 0.0
|
||||
# time-of-day rvol (cum vs median(20d) prorated by elapsed session) — the arm
|
||||
# eligibility signal. Legacy rvol is full-day-mean based: ~0.1-0.3 minutes
|
||||
# after the open for a NORMAL heavy name, so rvol_min=2 vs legacy armed almost
|
||||
# nothing early (07-20: IREX 0.76/IRE 0.58/BIYA 1.27 all stuck "weak" while
|
||||
# breaking out; tod = 16.8/11.9/481). None -> fall back to legacy rvol.
|
||||
rvol_tod: float | None = None
|
||||
trigger: float | None = None # opening-range high (entry breakout level)
|
||||
stop: float | None = None # opening-range low, cushioned (protective stop)
|
||||
range_pct: float = 0.0 # (high-low)/high — how wide the opening range is
|
||||
@@ -86,6 +92,7 @@ class ArmedTrigger:
|
||||
"rank": self.rank,
|
||||
"gap": round(self.gap_pct, 2),
|
||||
"rvol": round(self.rvol, 2),
|
||||
"rvol_tod": round(self.rvol_tod, 2) if self.rvol_tod is not None else None,
|
||||
"trigger": round(self.trigger, 4) if self.trigger else None,
|
||||
"stop": round(self.stop, 4) if self.stop else None,
|
||||
"price": round(self.price, 4) if self.price else None,
|
||||
@@ -140,7 +147,8 @@ class ArmedTriggerBook:
|
||||
and t.stop is not None
|
||||
and t.stop < t.trigger
|
||||
and self._gap_ok(t)
|
||||
and t.rvol >= self.rvol_min
|
||||
# tod rvol when the scanner provides it; legacy full-day-mean otherwise
|
||||
and (t.rvol_tod if t.rvol_tod is not None else t.rvol) >= self.rvol_min
|
||||
and t.range_pct >= self.min_range_pct
|
||||
and t.dollar_vol >= self.min_dollar_vol
|
||||
)
|
||||
@@ -172,6 +180,8 @@ class ArmedTriggerBook:
|
||||
_go = c.get("gap_open_pct")
|
||||
t.gap_open_pct = float(_go) if _go is not None else None
|
||||
t.rvol = float(c.get("rvol") or 0.0)
|
||||
_rt = c.get("rvol_tod")
|
||||
t.rvol_tod = float(_rt) if _rt is not None else None
|
||||
t.trigger = c.get("trigger")
|
||||
t.stop = c.get("stop")
|
||||
t.range_pct = float(c.get("range_pct") or 0.0)
|
||||
|
||||
+25
-1
@@ -89,6 +89,14 @@ class ExitConfig:
|
||||
# filled bracket's legs can attach first). 0 = off. The preventive complement
|
||||
# to the catastrophe stop (which is the -X% backstop).
|
||||
enforce_stop_grace_passes: int = 2
|
||||
# STAGNATION TIME-STOP: if after stagnation_minutes bars the trade's high-
|
||||
# water never reached entry + stagnation_min_r * risk, exit at market. The
|
||||
# dead-money case: bought the break, it neither ran nor stopped — capital and
|
||||
# attention sit parked in a name that has told us the breakout didn't follow
|
||||
# through. 0 = off (default; enable only after a sweep_exits.py validation —
|
||||
# exit levers here have a long refuted-lever history).
|
||||
stagnation_minutes: int = 0
|
||||
stagnation_min_r: float = 0.25
|
||||
# A held position at BREAKEVEN+ with no real R to trail from (a carried position, or
|
||||
# the bracket stop was stripped/invalid) has its +0.25R trail RE-ANCHORED to today's
|
||||
# opening range by the exit manager. This is the PRE-OPENING-RANGE FALLBACK R (a
|
||||
@@ -126,6 +134,8 @@ class ExitConfig:
|
||||
catastrophe_pct=f("TRADING_CATASTROPHE_STOP_PCT", "0.10"),
|
||||
catastrophe_risk_mult=f("TRADING_CATASTROPHE_RISK_MULT", "1.5"),
|
||||
enforce_stop_grace_passes=int(f("TRADING_ENFORCE_STOP_GRACE_PASSES", "2")),
|
||||
stagnation_minutes=int(f("TRADING_EXIT_STAGNATION_MIN", "0")),
|
||||
stagnation_min_r=f("TRADING_EXIT_STAGNATION_R", "0.25"),
|
||||
# clamp to [0, 1): a synthetic risk distance must be a positive fraction of
|
||||
# entry below it; >=1 or negative would put the synthetic stop at/above entry.
|
||||
default_trail_r_pct=min(max(f("TRADING_EXIT_DEFAULT_TRAIL_R_PCT", "0.10"), 0.0), 0.95),
|
||||
@@ -287,7 +297,15 @@ def simulate_exit(
|
||||
fills.append(ExitFill(remaining, close, "first_red"))
|
||||
return ExitResult(realized_r, "first_red", idx, fills)
|
||||
|
||||
# (5) ratchet the stop for the NEXT bar
|
||||
# (5) stagnation time-stop: N minutes in and the move never came
|
||||
if (cfg.stagnation_minutes and idx + 1 >= cfg.stagnation_minutes
|
||||
and reached_r < cfg.stagnation_min_r):
|
||||
r = (close - entry_price) / risk
|
||||
realized_r += remaining * r
|
||||
fills.append(ExitFill(remaining, close, "stagnation"))
|
||||
return ExitResult(realized_r, "stagnation", idx, fills)
|
||||
|
||||
# (6) ratchet the stop for the NEXT bar
|
||||
stop = _trail_stop(stop, entry_price, high_water, low, reached_r, cfg)
|
||||
prev_low = low
|
||||
|
||||
@@ -340,6 +358,12 @@ def manage_live(
|
||||
if last_close < prior_low and reached_r >= cfg.trail_after_r:
|
||||
return ExitDecision(stop, 0.0, True, "first_red")
|
||||
|
||||
# stagnation time-stop: N closed bars in and high-water never reached the
|
||||
# minimum R — dead money, exit outright (same rule as simulate_exit)
|
||||
if (cfg.stagnation_minutes and len(bars_since_entry) >= cfg.stagnation_minutes
|
||||
and reached_r < cfg.stagnation_min_r):
|
||||
return ExitDecision(stop, 0.0, True, "stagnation")
|
||||
|
||||
# scale-out: high reached the scale level and we haven't yet
|
||||
scale_frac = 0.0
|
||||
if (cfg.scale_out_r and not already_scaled
|
||||
|
||||
@@ -80,6 +80,11 @@ def variants() -> dict[str, ExitConfig]:
|
||||
profit_lock_tiers=[(.12, .05), (.25, .16), (.40, .30)])
|
||||
v["lock_only_mid"] = ExitConfig(target_r=10.0,
|
||||
profit_lock_tiers=[(.08, .03), (.15, .09), (.25, .18), (.40, .30)])
|
||||
# --- STAGNATION time-stop layered on the live trail: exit if high-water
|
||||
# never reached +min_r within N minutes (dead-money reclaim) ---
|
||||
for mins, min_r in ((15, 0.25), (30, 0.25), (45, 0.25), (60, 0.25), (30, 0.5)):
|
||||
v[f"stag{mins}_r{int(min_r*100)}"] = ExitConfig(
|
||||
**_trail, stagnation_minutes=mins, stagnation_min_r=min_r)
|
||||
return v
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ def test_gapper_scan_and_snapshot(con):
|
||||
candidates = scan_gappers(con, SESSION.date(), min_gap_pct=5, min_relative_volume=2)
|
||||
assert candidates and candidates[0].symbol == "GAPR"
|
||||
assert candidates[0].gap_pct > 20
|
||||
assert candidates[0].relative_volume_tod is not None
|
||||
assert store_scanner_snapshot(con, candidates) == len(candidates)
|
||||
rows = con.execute("SELECT symbol, rank FROM scanner_snapshots").fetchall()
|
||||
assert rows[0] == ("GAPR", 1)
|
||||
@@ -156,3 +157,18 @@ def test_research_watchlist_provider_feeds_watcher(con):
|
||||
assert candidates[0].previous_close == pytest.approx(4.1)
|
||||
bars = provider.get_bars("GAPR", SESSION.date())
|
||||
assert len(bars) == 15
|
||||
|
||||
|
||||
def test_gapper_scan_tod_rvol_keeps_early_mover(con):
|
||||
"""Early-session mover: cum volume is a fraction of the full-day mean (legacy
|
||||
rvol < 2 -> previously dropped) but far above the time-prorated median."""
|
||||
from datetime import datetime as _dt
|
||||
upsert_daily_bars(con, "EARL", [
|
||||
{"t": iso(SESSION - timedelta(days=2)), "o": 4, "h": 4.5, "l": 3.9, "c": 4.2, "v": 1_000_000},
|
||||
{"t": iso(SESSION - timedelta(days=1)), "o": 4.2, "h": 4.4, "l": 4.0, "c": 4.1, "v": 900_000},
|
||||
])
|
||||
# 10 minutes, 60k shares -> cum 600k = 0.63x mean (legacy fails min 2)
|
||||
upsert_minute_bars(con, "EARL", minute_payload(SESSION.replace(hour=13, minute=31), 10, base=5.0, volume=60_000))
|
||||
naive = scan_gappers(con, SESSION.date(), min_gap_pct=5, min_relative_volume=2,
|
||||
now=_dt(2026, 6, 10, 9, 40)) # 10 min into the session (ET, naive)
|
||||
assert any(c.symbol == "EARL" for c in naive)
|
||||
@@ -173,3 +173,35 @@ def test_manage_live_pct_trail():
|
||||
d = manage_live(10.0, 9.0, bars,
|
||||
ExitConfig(trail_mode=TRAIL_PCT, trail_pct=0.05, trail_after_r=1.0))
|
||||
assert round(d.desired_stop, 2) == round(12.0 * 0.95, 2)
|
||||
|
||||
|
||||
def test_stagnation_exit_fires_when_flat():
|
||||
# entry 10, stop 9 (risk 1). 5 flat bars: never reaches +0.25R by minute 5
|
||||
cfg = ExitConfig(target_r=0, stagnation_minutes=5, stagnation_min_r=0.25)
|
||||
bars = _bars([(10.1, 9.9, 10.05)] * 8)
|
||||
res = simulate_exit(10.0, 9.0, bars, cfg)
|
||||
assert res.reason == "stagnation"
|
||||
assert res.exit_index == 4 # fired ON minute 5
|
||||
assert abs(res.r_multiple - 0.05) < 1e-9 # left with the tiny drift
|
||||
|
||||
|
||||
def test_stagnation_not_triggered_when_move_came():
|
||||
cfg = ExitConfig(target_r=0, stagnation_minutes=5, stagnation_min_r=0.25)
|
||||
bars = _bars([(10.30, 9.95, 10.25)] + [(10.1, 9.9, 10.05)] * 10)
|
||||
res = simulate_exit(10.0, 9.0, bars, cfg) # +0.3R on bar 1 -> armed off
|
||||
assert res.reason != "stagnation"
|
||||
|
||||
|
||||
def test_stagnation_off_by_default():
|
||||
bars = _bars([(10.1, 9.9, 10.05)] * 30)
|
||||
res = simulate_exit(10.0, 9.0, bars, ExitConfig(target_r=0))
|
||||
assert res.reason == "session_end"
|
||||
|
||||
|
||||
def test_manage_live_stagnation_exits_flat_position():
|
||||
cfg = ExitConfig(target_r=0, stagnation_minutes=5, stagnation_min_r=0.25)
|
||||
bars = _bars([(10.1, 9.9, 10.05)] * 5)
|
||||
d = manage_live(10.0, 9.0, bars, cfg)
|
||||
assert d.exit_now and d.reason == "stagnation"
|
||||
d2 = manage_live(10.0, 9.0, _bars([(10.1, 9.9, 10.05)] * 3), cfg)
|
||||
assert not d2.exit_now # not enough bars yet
|
||||
@@ -375,6 +375,47 @@ def test_fill_model_marketable_lifts_above_trigger(store):
|
||||
assert req["stop_loss_price"] == 13.45 # R still measured from the level
|
||||
|
||||
|
||||
def test_fill_model_marketable_prices_from_live_last(store):
|
||||
# price already ran to 15.10 by the time the slow path submits: the limit
|
||||
# must chase from the LIVE price, not rest 2% behind the market at the level
|
||||
emit_signal(store, symbol="RUN", entry=14.0, stop=13.45, above_vwap=True)
|
||||
svc = make_service(store, entry_fill_model="marketable",
|
||||
price_provider=lambda s: 15.10)
|
||||
svc.request_approvals_for_ready_signals()
|
||||
req = query_approval_queue(store)[0]["execution_request"]
|
||||
assert req["entry_price"] == round(15.10 * 1.004, 2)
|
||||
assert req["stop_loss_price"] == 13.45 # R still from the level
|
||||
|
||||
|
||||
def test_fill_model_marketable_price_provider_error_falls_back(store):
|
||||
def boom(sym):
|
||||
raise RuntimeError("feed down")
|
||||
emit_signal(store, symbol="ERR", entry=14.0, stop=13.45, above_vwap=True)
|
||||
make_service(store, entry_fill_model="marketable",
|
||||
price_provider=boom).request_approvals_for_ready_signals()
|
||||
req = query_approval_queue(store)[0]["execution_request"]
|
||||
assert req["entry_price"] == round(14.0 * 1.004, 2) # trigger-based fallback
|
||||
|
||||
|
||||
def test_fill_model_marketable_live_below_trigger_uses_trigger(store):
|
||||
# live last dipped under the level -> never price the limit BELOW the trigger
|
||||
emit_signal(store, symbol="DIP", entry=14.0, stop=13.45, above_vwap=True)
|
||||
make_service(store, entry_fill_model="marketable",
|
||||
price_provider=lambda s: 13.80).request_approvals_for_ready_signals()
|
||||
req = query_approval_queue(store)[0]["execution_request"]
|
||||
assert req["entry_price"] == round(14.0 * 1.004, 2)
|
||||
|
||||
|
||||
def test_fill_model_marketable_over_extended_live_price_skipped(store):
|
||||
# price ran >15% past the trigger before we could submit -> anti-chase gate
|
||||
# (previously inert on the slow path because entry_ref == trigger)
|
||||
emit_signal(store, symbol="PARA", entry=14.0, stop=13.45, above_vwap=True)
|
||||
svc = make_service(store, entry_fill_model="marketable", unified_entry=True,
|
||||
price_provider=lambda s: 16.80)
|
||||
assert svc.request_approvals_for_ready_signals() == []
|
||||
assert query_approval_queue(store) == []
|
||||
|
||||
|
||||
def test_no_duplicate_requests_for_same_symbol(store):
|
||||
emit_signal(store)
|
||||
service = make_service(store)
|
||||
|
||||
@@ -401,3 +401,26 @@ def test_trigger_eligibility_uses_static_open_gap_not_dynamic():
|
||||
c2["gap_open_pct"] = 19.0 # true gapper stays armed
|
||||
b.arm([c2])
|
||||
assert b.triggers["BABU"].state == ARMED
|
||||
|
||||
|
||||
def test_book_arms_on_rvol_tod_when_legacy_rvol_low():
|
||||
"""The 07-20 miss: IREX legacy rvol 0.76 minutes after the open (cum-so-far
|
||||
vs full-day mean can't reach 2x yet) while the time-of-day rvol showed 16.8.
|
||||
tod must drive eligibility when present."""
|
||||
b = ArmedTriggerBook(gap_min=3, rvol_min=2)
|
||||
c = _cand("IREX", rvol=0.76); c["rvol_tod"] = 16.8
|
||||
b.arm([c])
|
||||
assert b.triggers["IREX"].state == ARMED
|
||||
|
||||
|
||||
def test_book_weak_when_rvol_tod_low_even_if_present():
|
||||
b = ArmedTriggerBook(gap_min=3, rvol_min=2)
|
||||
c = _cand("DEAD", rvol=3.0); c["rvol_tod"] = 0.2 # tod wins when provided
|
||||
b.arm([c])
|
||||
assert b.triggers["DEAD"].state == WEAK
|
||||
|
||||
|
||||
def test_book_falls_back_to_legacy_rvol_without_tod():
|
||||
b = ArmedTriggerBook(gap_min=3, rvol_min=2)
|
||||
b.arm([_cand("LEGACY", rvol=3.0)]) # no rvol_tod key at all
|
||||
assert b.triggers["LEGACY"].state == ARMED
|
||||
+16
-2
@@ -1205,12 +1205,26 @@ class TradingExecutionService:
|
||||
if not entry or not stop or stop >= entry:
|
||||
continue # price-sanity (silent; path-specific float coercion)
|
||||
entry_f0 = float(entry)
|
||||
# Marketable fill model prices from the LIVE last trade (the slow path
|
||||
# runs minutes behind the break — a limit at the level rests behind the
|
||||
# market and never fills on a runner; observed 6-for-6 unfilled 07-20).
|
||||
# live_ref also feeds entry_ref so the anti-chase extension gate bites
|
||||
# on the slow path instead of being an entry_ref==trigger no-op.
|
||||
live_ref = entry_f0
|
||||
if (self.settings.entry_fill_model == "marketable"
|
||||
and self.price_provider is not None):
|
||||
try:
|
||||
last = self.price_provider(symbol)
|
||||
except Exception: # noqa: BLE001 — feed hiccup: fall back to level
|
||||
last = None
|
||||
if last:
|
||||
live_ref = max(entry_f0, float(last))
|
||||
# ONE shared gate sequence (plan 005). The slow path rests a limit AT the
|
||||
# level, so entry_ref == trigger == the entry level. rank arrives from the
|
||||
# trigger book via rank_map (None -> factor 1.0, no-op).
|
||||
decision = self._evaluate_entry_gates(EntryContext(
|
||||
symbol=symbol, source="auto", entry=entry_f0, stop=float(stop),
|
||||
trigger=entry_f0, entry_ref=entry_f0,
|
||||
trigger=entry_f0, entry_ref=live_ref,
|
||||
gap_pct=signal.get("gap_pct"), price=entry_f0,
|
||||
cum_volume=signal.get("cum_volume"),
|
||||
rvol=signal.get("relative_volume"),
|
||||
@@ -1265,7 +1279,7 @@ class TradingExecutionService:
|
||||
# caps the fill price either way, so a gap-through can't be chased. R
|
||||
# (stop/target) is measured from the level (entry_f), not the limit.
|
||||
limit_price = (
|
||||
round(entry_f * (1.0 + self.settings.trigger_slippage_pct), 2)
|
||||
round(live_ref * (1.0 + self.settings.trigger_slippage_pct), 2)
|
||||
if self.settings.entry_fill_model == "marketable" else entry_f
|
||||
)
|
||||
request = ExecutionRequest(
|
||||
|
||||
Reference in new issue
Block a user