From 86419beeb045a809f66dc1727905bb3ab6041c14 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Tue, 14 Apr 2026 03:27:53 -0700 Subject: [PATCH] Fix ORB intraday data pipeline and consolidate strategy configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - screener: switch from non-existent single-ticker endpoint to multi-ticker /alpaca/intraday batch calls (grouped by date, chunk ≤ 75); fixes 0-trades - cache: bump version 2→3 to invalidate stale IEX Parquet files - oracle_client: add get_multi_intraday_bars_today() for IEX real-time feed - paper_trader: use /alpaca/intraday/today for live sessions, /alpaca/intraday for historical (SIP) - intraday.py: define _BUILTIN_STRATEGIES={} to fix /api/orb/strategies import - delete orb_p1–p10_winner + variant configs; add strategies/orb_default.yaml (Phase 10 params) as the single registered web strategy Co-Authored-By: Claude Sonnet 4.6 --- apps/paper_trader/alpaca_broker.py | 24 ++-- apps/web/routers/intraday.py | 2 + configs/intraday/orb_5min.yaml | 75 ----------- configs/intraday/orb_5min_aggressive.yaml | 75 ----------- configs/intraday/orb_aggressive.yaml | 78 ------------ configs/intraday/orb_p1_winner.yaml | 81 ------------ configs/intraday/orb_p2_winner.yaml | 85 ------------- configs/intraday/orb_p3_winner.yaml | 88 ------------- configs/intraday/orb_p4_winner.yaml | 90 ------------- configs/intraday/orb_p5_winner.yaml | 93 -------------- configs/intraday/orb_p6_winner.yaml | 90 ------------- configs/intraday/orb_p7_winner.yaml | 93 -------------- configs/intraday/orb_p8_winner.yaml | 91 -------------- configs/intraday/orb_p9_winner.yaml | 94 -------------- .../{orb_p9.yaml => orb_default.yaml} | 36 +++++- .../strategies/orb_p9_champion_copy.yaml | 50 -------- libs/intraday/cache.py | 2 +- libs/intraday/screener.py | 119 +++++++++--------- libs/oracle_client/alpaca.py | 36 ++++++ 19 files changed, 150 insertions(+), 1152 deletions(-) delete mode 100644 configs/intraday/orb_5min.yaml delete mode 100644 configs/intraday/orb_5min_aggressive.yaml delete mode 100644 configs/intraday/orb_aggressive.yaml delete mode 100644 configs/intraday/orb_p1_winner.yaml delete mode 100644 configs/intraday/orb_p2_winner.yaml delete mode 100644 configs/intraday/orb_p3_winner.yaml delete mode 100644 configs/intraday/orb_p4_winner.yaml delete mode 100644 configs/intraday/orb_p5_winner.yaml delete mode 100644 configs/intraday/orb_p6_winner.yaml delete mode 100644 configs/intraday/orb_p7_winner.yaml delete mode 100644 configs/intraday/orb_p8_winner.yaml delete mode 100644 configs/intraday/orb_p9_winner.yaml rename configs/intraday/strategies/{orb_p9.yaml => orb_default.yaml} (64%) delete mode 100644 configs/intraday/strategies/orb_p9_champion_copy.yaml diff --git a/apps/paper_trader/alpaca_broker.py b/apps/paper_trader/alpaca_broker.py index 773b3c0..f5d4a9c 100644 --- a/apps/paper_trader/alpaca_broker.py +++ b/apps/paper_trader/alpaca_broker.py @@ -290,20 +290,28 @@ class AlpacaBroker: ) -> dict[str, list[dict]]: """Fetch intraday OHLCV bars for a list of symbols via Oracle API. + - Today's date → /alpaca/intraday/today (IEX real-time, force_refresh) + - Historical dates → /alpaca/intraday (SIP, DB-cached) + Returns {symbol: [{timestamp: ISO8601, open, high, low, close, volume}, ...]}. """ if not symbols: return {} - from libs.oracle_client.alpaca import get_multi_intraday_bars - interval = f"{timeframe_minutes}min" - raw = get_multi_intraday_bars( - tickers=symbols, - start_date=start.date().isoformat(), - end_date=end.date().isoformat(), - interval=interval, - ) + today = dt.date.today() + + if start.date() >= today: + from libs.oracle_client.alpaca import get_multi_intraday_bars_today + raw = get_multi_intraday_bars_today(tickers=symbols, interval=interval) + else: + from libs.oracle_client.alpaca import get_multi_intraday_bars + raw = get_multi_intraday_bars( + tickers=symbols, + start_date=start.date().isoformat(), + end_date=end.date().isoformat(), + interval=interval, + ) result: dict[str, list[dict]] = {sym: [] for sym in symbols} for sym in symbols: diff --git a/apps/web/routers/intraday.py b/apps/web/routers/intraday.py index 1be7a6c..ffb2e7a 100644 --- a/apps/web/routers/intraday.py +++ b/apps/web/routers/intraday.py @@ -32,6 +32,8 @@ _tasks_initialized = False INTRADAY_OUTPUT_DIR = "runs/intraday_orb" # Built-in strategies (read-only presets shipped with the system) +# All strategies are now directory-based (configs/intraday/strategies/). +_BUILTIN_STRATEGIES: dict[str, Any] = {} # --------------------------------------------------------------------------- # Helpers diff --git a/configs/intraday/orb_5min.yaml b/configs/intraday/orb_5min.yaml deleted file mode 100644 index 0d92483..0000000 --- a/configs/intraday/orb_5min.yaml +++ /dev/null @@ -1,75 +0,0 @@ -# ORB Strategy — 5-min bars (highest backtest Sharpe but less realistic execution) -# Identical to orb_default except sim_bar_minutes: 5 (raw 5-min bars for breakout/stops) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 # 9:30–9:35 ET opening range - sim_bar_minutes: 5 # 5-min bars (raw, no aggregation) - - # Entry - entry_direction: long_only - order_timeout_minutes: 45 - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 - max_candidates: 20 - min_candidates_to_trade: 3 - - # Composite ranking weights - weight_rvol: 0.60 - weight_gap: 0.25 - weight_dollar_vol: 0.15 - - # ATR-based stop management - atr_stop_multiplier: 0.50 - breakeven_at_r: 1.0 - trailing_at_r: 2.0 - - # Risk-based position sizing (conservative) - risk_per_trade_pct: 0.0025 - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 - max_stops_per_day: 3 - - # Exit - exit_minutes_before_close: 5 - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account GFV constraint - settlement_days: 1 - - # Max opening gap filter - max_gap_pct: 0.03 - - # Market regime — ETF gap filter disabled - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - min_price: 10.0 - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - pre_screen_threshold: 0.01 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_5min_aggressive.yaml b/configs/intraday/orb_5min_aggressive.yaml deleted file mode 100644 index 070726c..0000000 --- a/configs/intraday/orb_5min_aggressive.yaml +++ /dev/null @@ -1,75 +0,0 @@ -# ORB Strategy — 5-min bars, Aggressive sizing -# Same signals as orb_5min but 8x position sizing (2% risk, 60% cap) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # 5-min bars (raw, no aggregation) - - # Entry - entry_direction: long_only - order_timeout_minutes: 45 - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 - max_candidates: 20 - min_candidates_to_trade: 3 - - # Composite ranking weights - weight_rvol: 0.60 - weight_gap: 0.25 - weight_dollar_vol: 0.15 - - # ATR-based stop management - atr_stop_multiplier: 0.50 - breakeven_at_r: 1.0 - trailing_at_r: 2.0 - - # AGGRESSIVE position sizing — 8x conservative - risk_per_trade_pct: 0.02 - max_position_pct: 0.60 - daily_max_loss_pct: 0.06 - max_stops_per_day: 5 - - # Exit - exit_minutes_before_close: 5 - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account GFV constraint - settlement_days: 1 - - # Max opening gap filter - max_gap_pct: 0.03 - - # Market regime — ETF gap filter disabled - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - min_price: 10.0 - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - pre_screen_threshold: 0.01 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_aggressive.yaml b/configs/intraday/orb_aggressive.yaml deleted file mode 100644 index 4e718e8..0000000 --- a/configs/intraday/orb_aggressive.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# ORB Strategy — Aggressive Configuration (high-return target) -# Same signals as orb_default (max_candidates=20, min_rvol=1.0) -# Only changes: 8x position sizing (2% risk, 60% cap) and tighter SPY filter - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 30 # 30-min bars for breakout/stop management (ORB candle stays 5-min) - - # Entry — identical to default - entry_direction: long_only - order_timeout_minutes: 45 - - # Universe quality filters — identical to default - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection — identical to default (keep the 51% WR edge) - min_rvol: 1.0 - max_candidates: 20 - min_candidates_to_trade: 3 - - # Composite ranking weights — identical to default - weight_rvol: 0.60 - weight_gap: 0.25 - weight_dollar_vol: 0.15 - - # ATR-based stop management — identical to default - atr_stop_multiplier: 0.50 - breakeven_at_r: 1.0 - trailing_at_r: 2.0 - - # AGGRESSIVE position sizing — 8x default - risk_per_trade_pct: 0.02 # 2% risk per trade (vs 0.25% default) - max_position_pct: 0.60 # 60% max per position (vs 20% default) - daily_max_loss_pct: 0.06 # 6% daily loss limit (vs 1.25% default) - max_stops_per_day: 5 # 5 stops (vs 3 default) - - # Exit — identical to default - exit_minutes_before_close: 5 - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account GFV constraint (same as orb_default) - settlement_days: 1 - - # Max opening gap filter (same as orb_default) - max_gap_pct: 0.03 - - # Market regime — ETF gap filter disabled (breadth filter below is superior) - market_regime_spy_threshold: null - - # Candidate breadth: disabled (per-trade risk controls sufficient, no filter = higher return) - min_candidate_breadth: null - -universe: - source: midlarge - min_price: 10.0 - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - pre_screen_threshold: 0.01 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p1_winner.yaml b/configs/intraday/orb_p1_winner.yaml deleted file mode 100644 index f36c2f2..0000000 --- a/configs/intraday/orb_p1_winner.yaml +++ /dev/null @@ -1,81 +0,0 @@ -# ORB Phase 1 Winner — Core Structure -# -# Phase 1 sweep (50 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 1.55 (sim_bar=5, atr_stop=1.0, long_only) -# OOS return: +26.8%, OOS max DD: -8.7%, OOS trades: 3532, OOS WR: 50.6% -# -# Parameters fixed here vs orb_default.yaml: -# sim_bar_minutes: 5 (was 30) — 5-min stop management dominates all larger bars -# atr_stop_multiplier: 1.00 (was 0.50) — wider stop, best OOS risk-adjusted -# entry_direction: long_only (unchanged) — confirmed better than 'both' - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 # 9:30–9:35 ET opening range - sim_bar_minutes: 5 # 5-min bars — Phase 1 winner - - # Entry - entry_direction: long_only # Phase 1 confirmed: long_only > both - order_timeout_minutes: 45 # cancel if no fill by 10:15 ET - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 # $25M 30-day avg daily dollar volume - min_atr_14: 0.50 # ATR(14) > $0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 - max_candidates: 20 - min_candidates_to_trade: 3 - - # Composite ranking weights - weight_rvol: 0.60 - weight_gap: 0.25 - weight_dollar_vol: 0.15 - - # ATR-based stop management (Phase 1 winner values) - atr_stop_multiplier: 1.00 # Phase 1 winner: 1.0 × ATR(14) from entry - breakeven_at_r: 1.0 # Phase 2 will sweep this - trailing_at_r: 2.0 # Phase 2 will sweep this - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 # 0.25% of sizing capital per trade - max_position_pct: 0.20 # cap at 20% per position - daily_max_loss_pct: 0.0125 # stop trading at -1.25% daily loss - max_stops_per_day: 3 # stop trading after 3 full-R stops - - # Exit - exit_minutes_before_close: 5 # time exit at 15:55 ET - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account settlement (T+1, US since May 2024) - settlement_days: 1 - - # Gap filter - max_gap_pct: 0.03 - - # Market regime filters (disabled) - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p2_winner.yaml b/configs/intraday/orb_p2_winner.yaml deleted file mode 100644 index 11e25f2..0000000 --- a/configs/intraday/orb_p2_winner.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# ORB Phase 2 Winner — Stop Management -# -# Phase 2 sweep (60 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 1.56 (breakeven=1.0, trailing_at=2.0, trailing_atr=0.3) -# OOS return: +27.1%, OOS max DD: -8.8%, OOS trades: 3533, OOS WR: 50.6% -# -# Note: Phase 2 showed minimal differentiation (~0.01 Sharpe spread across 60 combos, -# identical trade counts). Entry selection dominates exit management — Phase 3 focus. -# -# Parameters fixed here vs orb_p1_winner.yaml: -# breakeven_at_r: 1.0 (confirmed — 0.5 cuts winners too early) -# trailing_at_r: 2.0 (unchanged — all top 10 converged here) -# trailing_stop_atr_multiplier: 0.3 (slightly better than swing-low mode) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 1 winner - - # Entry - entry_direction: long_only # Phase 1 winner - order_timeout_minutes: 45 # Phase 3 will sweep this - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 # Phase 3 will sweep this - max_candidates: 20 # Phase 3 will sweep this - min_candidates_to_trade: 3 - - # Composite ranking weights - weight_rvol: 0.60 - weight_gap: 0.25 - weight_dollar_vol: 0.15 - - # ATR-based stop management (Phase 1+2 winner values) - atr_stop_multiplier: 1.00 # Phase 1 winner - breakeven_at_r: 1.0 # Phase 2 winner - trailing_at_r: 2.0 # Phase 2 winner - trailing_stop_atr_multiplier: 0.3 # Phase 2 winner - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 # Phase 3 will sweep this - max_stops_per_day: 3 # Phase 3 will sweep this - - # Exit - exit_minutes_before_close: 5 - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account settlement - settlement_days: 1 - - # Gap filter - max_gap_pct: 0.03 # Phase 3 will sweep this - - # Market regime filters (disabled) - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p3_winner.yaml b/configs/intraday/orb_p3_winner.yaml deleted file mode 100644 index 4fa9e9b..0000000 --- a/configs/intraday/orb_p3_winner.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# ORB Phase 3 Winner — Universe Filters & Entry Timeout -# -# Phase 3b sweep (20 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 1.663 (max_gap=0.05, order_timeout=20) -# OOS return: +29.0%, OOS max DD: -8.54%, OOS trades: 3447, OOS WR: 51.3% -# -# vs Phase 2 winner (max_gap=0.03, timeout=45): -# OOS Sharpe: 1.562, OOS return: +27.1%, OOS DD: -8.80% -# Improvement: +0.10 Sharpe (+6.5%), +1.9pp return, -0.26pp DD -# -# Key insight: max_gap=0.05 is the sweet spot (0.03 too tight, 0.10/null too loose). -# Note: IS Sharpe is slightly positive (0.067) vs Phase 2's -0.157 — better regime fit. -# -# Parameters changed vs orb_p2_winner.yaml: -# max_gap_pct: 0.05 (was 0.03 — broader gap filter admits better momentum candidates) -# order_timeout_minutes: 20 (was 45 — tighter timeout, fewer stale entries) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 1 winner - - # Entry - entry_direction: long_only # Phase 1 winner - order_timeout_minutes: 20 # Phase 3 winner (was 45) - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 # Phase 3 confirmed - max_candidates: 20 # Phase 3 confirmed - min_candidates_to_trade: 3 - - # Composite ranking weights - weight_rvol: 0.60 - weight_gap: 0.25 - weight_dollar_vol: 0.15 - - # ATR-based stop management (Phase 1+2 winner values) - atr_stop_multiplier: 1.00 # Phase 1 winner - breakeven_at_r: 1.0 # Phase 2 winner - trailing_at_r: 2.0 # Phase 2 winner - trailing_stop_atr_multiplier: 0.3 # Phase 2 winner - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 # Phase 3 confirmed (limit rarely binding) - max_stops_per_day: 3 # Phase 3 confirmed (limit rarely binding) - - # Exit - exit_minutes_before_close: 5 - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account settlement - settlement_days: 1 - - # Gap filter — Phase 3 winner - max_gap_pct: 0.05 # Phase 3 winner (was 0.03) - - # Market regime filters (disabled) - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p4_winner.yaml b/configs/intraday/orb_p4_winner.yaml deleted file mode 100644 index 2f79f08..0000000 --- a/configs/intraday/orb_p4_winner.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# ORB Phase 4 Winner — Composite Ranking Weights -# -# Phase 4 sweep (81 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 1.77 (weight_rvol=0.40, weight_gap=0.35) -# OOS return: +31%, OOS max DD: -7.95%, OOS WR: ~51% -# -# vs Phase 3 winner (weight_rvol=0.60, weight_gap=0.25): -# OOS Sharpe: 1.663, OOS return: +29.0%, OOS DD: -8.54% -# Improvement: +0.11 Sharpe (+6.6%), +2pp return, -0.59pp DD -# -# Key insight: Lower RVOL weight (0.40 vs 0.60) + higher gap weight (0.35 vs 0.25) -# - Less double-counting: RVOL and gap are correlated (both capture pre-market activity) -# - Gap weight increase gives more direct pre-market demand signal -# - weight_body_ratio=0.0 unchanged (no benefit from ORB candle body signal) -# -# Parameters changed vs orb_p3_winner.yaml: -# weight_rvol: 0.40 (was 0.60) -# weight_gap: 0.35 (was 0.25) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 1 winner - - # Entry - entry_direction: long_only # Phase 1 winner - order_timeout_minutes: 20 # Phase 3 winner - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 # Phase 3 confirmed - max_candidates: 20 # Phase 3 confirmed - min_candidates_to_trade: 3 - - # Composite ranking weights — Phase 4 winner - weight_rvol: 0.40 # Phase 4 winner (was 0.60) - weight_gap: 0.35 # Phase 4 winner (was 0.25) - weight_dollar_vol: 0.15 # unchanged - - # ATR-based stop management (Phase 1+2 winner values) - atr_stop_multiplier: 1.00 # Phase 1 winner - breakeven_at_r: 1.0 # Phase 2 winner - trailing_at_r: 2.0 # Phase 2 winner - trailing_stop_atr_multiplier: 0.3 # Phase 2 winner - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 # Phase 4 confirmed - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 # Phase 3 confirmed (limit rarely binding) - max_stops_per_day: 3 # Phase 3 confirmed (limit rarely binding) - - # Exit - exit_minutes_before_close: 5 - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account settlement - settlement_days: 1 - - # Gap filter — Phase 3 winner - max_gap_pct: 0.05 # Phase 3 winner - - # Market regime filters (disabled) - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p5_winner.yaml b/configs/intraday/orb_p5_winner.yaml deleted file mode 100644 index 098075a..0000000 --- a/configs/intraday/orb_p5_winner.yaml +++ /dev/null @@ -1,93 +0,0 @@ -# ORB Phase 5 Winner — Momentum Signal Weight -# -# Phase 5 sweep (5 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 2.253 (weight_momentum=0.0 — no momentum signal) -# OOS return: +39.2%, OOS max DD: -9.04%, OOS trades: 3349, OOS WR: 52.3% -# -# Result: momentum signal (5-day prior return) HURTS OOS performance. -# weight_momentum=0.00: OOS Sharpe 2.2529 (WINNER) -# weight_momentum=0.10: OOS Sharpe 2.2231 (-0.03) -# weight_momentum=0.20: OOS Sharpe 2.1633 (-0.09) -# weight_momentum=0.30: OOS Sharpe 2.1469 (-0.11) -# weight_momentum=0.50: OOS Sharpe 2.1398 (-0.11) -# -# Conclusion: Momentum signal adds noise — the ORB breakout direction itself -# is sufficient; pre-event momentum does not improve candidate ranking. -# weight_momentum remains 0.0 (disabled). -# -# Parameters unchanged vs orb_p4_winner.yaml: -# weight_momentum: 0.0 (confirmed, was default) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 1 winner - - # Entry - entry_direction: long_only # Phase 1 winner - order_timeout_minutes: 20 # Phase 3 winner - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 # Phase 3 confirmed - max_candidates: 20 # Phase 3 confirmed - min_candidates_to_trade: 3 - - # Composite ranking weights — Phase 4+5 winners - weight_rvol: 0.40 # Phase 4 winner - weight_gap: 0.35 # Phase 4 winner - weight_dollar_vol: 0.15 # unchanged - weight_body_ratio: 0.0 # Phase 4 confirmed (no benefit) - weight_momentum: 0.0 # Phase 5 confirmed (no benefit) - - # ATR-based stop management (Phase 1+2 winner values) - atr_stop_multiplier: 1.00 # Phase 1 winner - breakeven_at_r: 1.0 # Phase 2 winner - trailing_at_r: 2.0 # Phase 2 winner - trailing_stop_atr_multiplier: 0.3 # Phase 2 winner - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 # Phase 4 confirmed - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 # Phase 3 confirmed - max_stops_per_day: 3 # Phase 3 confirmed - - # Exit - exit_minutes_before_close: 5 - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account settlement - settlement_days: 1 - - # Gap filter — Phase 3 winner - max_gap_pct: 0.05 # Phase 3 winner - - # Market regime filters (disabled) - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p6_winner.yaml b/configs/intraday/orb_p6_winner.yaml deleted file mode 100644 index cc578a1..0000000 --- a/configs/intraday/orb_p6_winner.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# ORB Phase 6 Winner — Fine-Grained Parameter Tuning -# -# Phase 6 sweep (81 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 1.895 (min_rvol=1.0, atr_stop=1.25, max_gap=0.04, exit=10m) -# OOS return: +25.0%, OOS max DD: -6.03%, OOS trades: 3295, OOS WR: 51.9% -# -# Key parameter insights (avg OOS Sharpe by value): -# atr_stop: 0.75→1.747 | 1.00→1.768 | 1.25→1.815 (wider stop = trades breathe = better) -# exit_min: 3m→1.752 | 5m→1.752 | 10m→1.803 (exit earlier avoids close-auction noise) -# max_gap: 0.04→1.808 | 0.05→1.794 | 0.06→1.705 (tighter gap = cleaner breakouts) -# min_rvol: 0.70→1.747 | 1.00→1.765 | 1.30→1.795 (mild improvement with stricter RVOL) -# -# Parameters changed vs orb_p5_winner.yaml: -# atr_stop_multiplier: 1.25 (was 1.00 — wider stop reduces whipsaws) -# exit_minutes_before_close: 10 (was 5 — avoids late-day volatility) -# max_gap_pct: 0.04 (was 0.05 — tighter gap filter for cleaner candidates) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 1 winner - - # Entry - entry_direction: long_only # Phase 1 winner - order_timeout_minutes: 20 # Phase 3 winner - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 # Phase 6 confirmed - max_candidates: 20 # Phase 3 confirmed - min_candidates_to_trade: 3 - - # Composite ranking weights — Phase 4+5 winners - weight_rvol: 0.40 # Phase 4 winner - weight_gap: 0.35 # Phase 4 winner - weight_dollar_vol: 0.15 # unchanged - weight_body_ratio: 0.0 # Phase 4 confirmed (no benefit) - weight_momentum: 0.0 # Phase 5 confirmed (no benefit) - - # ATR-based stop management — Phase 6 winners - atr_stop_multiplier: 1.25 # Phase 6 winner (was 1.00) - breakeven_at_r: 1.0 # Phase 2 winner - trailing_at_r: 2.0 # Phase 2 winner - trailing_stop_atr_multiplier: 0.3 # Phase 2 winner - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 # Phase 4 confirmed - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 # Phase 3 confirmed - max_stops_per_day: 3 # Phase 3 confirmed - - # Exit — Phase 6 winner - exit_minutes_before_close: 10 # Phase 6 winner (was 5) - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 0 - - # Cash account settlement - settlement_days: 1 - - # Gap filter — Phase 6 winner - max_gap_pct: 0.04 # Phase 6 winner (was 0.05) - - # Market regime filters (disabled — Phase 7 will sweep these) - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p7_winner.yaml b/configs/intraday/orb_p7_winner.yaml deleted file mode 100644 index da2ef56..0000000 --- a/configs/intraday/orb_p7_winner.yaml +++ /dev/null @@ -1,93 +0,0 @@ -# ORB Phase 7 Winner — Market Regime Filter + Ticker Cooldown -# -# Phase 7 sweep (12 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 2.067 (ticker_cooldown_days=2, regime=any) -# OOS return: +23.0%, OOS max DD: -3.99%, OOS trades: 2669, OOS WR: 52.3% -# -# SPY regime filter: ZERO effect — all thresholds (null/-0.3%/-0.5%/-1%) identical -# → regime filter disabled (market_regime_spy_threshold: null) -# -# Ticker cooldown: STRONG effect (prevents chasing same stock repeatedly): -# cooldown=0d: OOS 1.895, DD -6.03%, 3295 trades -# cooldown=1d: OOS 1.968 (+3.8%), DD -4.18%, 2916 trades -# cooldown=2d: OOS 2.067 (+9.1%), DD -3.99%, 2669 trades ← WINNER -# -# Mechanism: cooldown=2 avoids mean-reversion trap (stock pulls back after -# initial breakout day). Also prevents overconcentration in popular names. -# -# Parameters changed vs orb_p6_winner.yaml: -# ticker_cooldown_days: 2 (was 0) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 1 winner (Phase 8 will sweep this) - - # Entry - entry_direction: long_only # Phase 1 winner - order_timeout_minutes: 20 # Phase 3 winner - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 # Phase 6 confirmed - max_candidates: 20 # Phase 3 confirmed - min_candidates_to_trade: 3 - - # Composite ranking weights — Phase 4+5 winners - weight_rvol: 0.40 # Phase 4 winner - weight_gap: 0.35 # Phase 4 winner - weight_dollar_vol: 0.15 # unchanged - weight_body_ratio: 0.0 # Phase 4 confirmed (no benefit) - weight_momentum: 0.0 # Phase 5 confirmed (no benefit) - - # ATR-based stop management - atr_stop_multiplier: 1.25 # Phase 6 winner - breakeven_at_r: 1.0 # Phase 2 winner - trailing_at_r: 2.0 # Phase 2 winner - trailing_stop_atr_multiplier: 0.3 # Phase 2 winner - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 # Phase 4 confirmed - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 # Phase 3 confirmed - max_stops_per_day: 3 # Phase 3 confirmed - - # Exit - exit_minutes_before_close: 10 # Phase 6 winner - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 2 # Phase 7 winner (was 0) - - # Cash account settlement - settlement_days: 1 - - # Gap filter - max_gap_pct: 0.04 # Phase 6 winner - - # Market regime filter — Phase 7: no effect, disabled - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p8_winner.yaml b/configs/intraday/orb_p8_winner.yaml deleted file mode 100644 index 7f84966..0000000 --- a/configs/intraday/orb_p8_winner.yaml +++ /dev/null @@ -1,91 +0,0 @@ -# ORB Phase 8 Winner — Bar Size Confirmation -# -# Phase 8a (15 combos): 30/60/90m bars produced 0 trades due to -# order_timeout_minutes=20 < sim_bar_minutes → order expires before first bar close. -# -# Phase 8b (8 combos, timeout=120): larger bars WORSE, not better: -# sim=5m: OOS 1.84 (best) -# sim=30m: OOS -3.26 (catastrophic) -# sim=60m: OOS -4.92 -# sim=90m: OOS -6.06 -# -# Root cause: atr_stop=1.25 (wide) + trailing_at_r=2.0 (tight) is incompatible -# with large bars. Trailing stop only updates at bar close — within a 30m bar, -# large reversals aren't caught. Original ORB worked with atr=0.30 (tight stop). -# -# Conclusion: sim_bar_minutes=5 is optimal for this strategy configuration. -# No parameter changes vs orb_p7_winner.yaml. - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 8 confirmed: 5m optimal for atr=1.25 - - # Entry - entry_direction: long_only - order_timeout_minutes: 20 # Phase 3 winner - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 - max_candidates: 20 - min_candidates_to_trade: 3 - - # Composite ranking weights - weight_rvol: 0.40 # Phase 4 winner - weight_gap: 0.35 # Phase 4 winner - weight_dollar_vol: 0.15 - weight_body_ratio: 0.0 # Phase 4 confirmed - weight_momentum: 0.0 # Phase 5 confirmed - - # ATR-based stop management - atr_stop_multiplier: 1.25 # Phase 6 winner - breakeven_at_r: 1.0 # Phase 2 winner (Phase 9 will fine-tune) - trailing_at_r: 2.0 # Phase 2 winner (Phase 9 will fine-tune) - trailing_stop_atr_multiplier: 0.3 - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 # Phase 4 confirmed - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 - max_stops_per_day: 3 - - # Exit - exit_minutes_before_close: 10 # Phase 6 winner - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 2 # Phase 7 winner - - # Cash account settlement - settlement_days: 1 - - # Gap filter - max_gap_pct: 0.04 # Phase 6 winner - - # Market regime filter - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/orb_p9_winner.yaml b/configs/intraday/orb_p9_winner.yaml deleted file mode 100644 index 00aa700..0000000 --- a/configs/intraday/orb_p9_winner.yaml +++ /dev/null @@ -1,94 +0,0 @@ -# ORB Phase 9 Winner — Stop Management Fine-Tune -# -# Phase 9 sweep (20 combos, IS 2022-2024 / OOS 2025-present): -# Best OOS Sharpe: 2.119 (breakeven_at_r=1.0, trailing_at_r=3.0) -# OOS return: +23.8%, OOS max DD: -3.86%, OOS trades: 2669, OOS WR: 52.3% -# -# Key findings: -# trailing=3.0R: avg 2.068 (BEST) — gives trades more room to run -# trailing=5.0R: avg 2.028 -# trailing=2.0R: avg 2.013 (was default) -# trailing=0.0R: avg 1.485 (catastrophic — no trailing = no profit lock) -# -# breakeven=1.0R: avg 1.931 (BEST) — protect against reversal after first R gain -# breakeven=0.0R: avg 1.837 (worst — no protection) -# -# Mechanism: ticker_cooldown=2 makes each trade precious → wider trailing (3R) -# lets winners run before locking profits. BE=1.0 guards against reversals. -# -# Parameters changed vs orb_p8_winner.yaml: -# trailing_at_r: 3.0 (was 2.0 — wider trailing to let trades run) - -strategy_mode: orb - -orb_strategy: - # ORB window - orb_minutes: 5 - sim_bar_minutes: 5 # Phase 8 confirmed optimal - - # Entry - entry_direction: long_only - order_timeout_minutes: 20 # Phase 3 winner - - # Universe quality filters - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.50 - - # RVOL-based candidate selection - min_rvol: 1.0 - max_candidates: 20 - min_candidates_to_trade: 3 - - # Composite ranking weights - weight_rvol: 0.40 # Phase 4 winner - weight_gap: 0.35 # Phase 4 winner - weight_dollar_vol: 0.15 - weight_body_ratio: 0.0 # Phase 4 confirmed - weight_momentum: 0.0 # Phase 5 confirmed - - # ATR-based stop management — Phase 9 winners - atr_stop_multiplier: 1.25 # Phase 6 winner - breakeven_at_r: 1.0 # Phase 9 confirmed (was already optimal) - trailing_at_r: 3.0 # Phase 9 winner (was 2.0) - trailing_stop_atr_multiplier: 0.3 - - # Risk-based position sizing - risk_per_trade_pct: 0.0025 # Phase 4 confirmed - max_position_pct: 0.20 - daily_max_loss_pct: 0.0125 - max_stops_per_day: 3 - - # Exit - exit_minutes_before_close: 10 # Phase 6 winner - - # Execution - slippage_bps: 5.0 - initial_capital: 10000 - ticker_cooldown_days: 2 # Phase 7 winner - - # Cash account settlement - settlement_days: 1 - - # Gap filter - max_gap_pct: 0.04 # Phase 6 winner - - # Market regime filter (no effect — Phase 7) - market_regime_spy_threshold: null - min_candidate_breadth: null - -universe: - source: midlarge - -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - -cache: - enabled: true - dir: data/cache/intraday - -output: - dir: runs/intraday_orb - verbose: false diff --git a/configs/intraday/strategies/orb_p9.yaml b/configs/intraday/strategies/orb_default.yaml similarity index 64% rename from configs/intraday/strategies/orb_p9.yaml rename to configs/intraday/strategies/orb_default.yaml index 5f027d9..cd21610 100644 --- a/configs/intraday/strategies/orb_p9.yaml +++ b/configs/intraday/strategies/orb_default.yaml @@ -1,43 +1,65 @@ _meta: id: 1 - name: "ORB P9 Champion" - description: "10-phase IS/OOS optimized: OOS Sharpe 2.12, MaxDD -3.86%, 2025 holdout." + name: "ORB Default" + description: "Opening Range Breakout — 5-min ORB, ATR stop, risk-based sizing. Phase 10 optimized." strategy_mode: orb orb_strategy: + # ORB window orb_minutes: 5 sim_bar_minutes: 5 + + # Entry entry_direction: long_only order_timeout_minutes: 20 + + # Universe quality filters min_price: 10.0 min_avg_dollar_volume: 25000000 min_atr_14: 0.50 + + # RVOL-based candidate selection min_rvol: 1.0 max_candidates: 20 min_candidates_to_trade: 3 + + # Composite ranking weights weight_rvol: 0.40 weight_gap: 0.35 weight_dollar_vol: 0.15 weight_body_ratio: 0.0 weight_momentum: 0.0 + + # ATR-based stop management atr_stop_multiplier: 1.25 breakeven_at_r: 1.0 trailing_at_r: 3.0 trailing_stop_atr_multiplier: 0.3 + + # Risk-based position sizing risk_per_trade_pct: 0.0025 max_position_pct: 0.20 daily_max_loss_pct: 0.0125 max_stops_per_day: 3 + + # Exit exit_minutes_before_close: 10 + + # Execution slippage_bps: 5.0 initial_capital: 10000 ticker_cooldown_days: 2 + + # Cash account settlement settlement_days: 1 + + # Gap filter max_gap_pct: 0.04 + + # Market regime filter market_regime_spy_threshold: null min_candidate_breadth: null - compound_returns: false universe: source: midlarge @@ -46,3 +68,11 @@ backtest: start_date: null end_date: null lookback_trading_days: 200 + +cache: + enabled: true + dir: data/cache/intraday + +output: + dir: runs/intraday_orb + verbose: false diff --git a/configs/intraday/strategies/orb_p9_champion_copy.yaml b/configs/intraday/strategies/orb_p9_champion_copy.yaml deleted file mode 100644 index 1f7d8fb..0000000 --- a/configs/intraday/strategies/orb_p9_champion_copy.yaml +++ /dev/null @@ -1,50 +0,0 @@ -_meta: - name: ORB P9 Champion (copy) - description: '10-phase IS/OOS optimized: OOS Sharpe 2.12, MaxDD -3.86%, 2025 holdout.' - id: 2 -strategy_mode: orb -orb_strategy: - orb_minutes: 5 - sim_bar_minutes: 5 - entry_direction: long_only - order_timeout_minutes: 20 - min_price: 10.0 - min_avg_dollar_volume: 25000000 - min_atr_14: 0.5 - min_rvol: 0.5 - max_candidates: 20 - min_candidates_to_trade: 3 - weight_rvol: 0.6 - weight_gap: 0.25 - weight_dollar_vol: 0.15 - atr_stop_multiplier: 1.25 - breakeven_at_r: 1.0 - trailing_at_r: 3.0 - trailing_stop_atr_multiplier: 0.3 - risk_per_trade_pct: 0.0025 - max_position_pct: 0.2 - daily_max_loss_pct: 0.0125 - max_stops_per_day: 3 - exit_minutes_before_close: 5 - slippage_bps: 5.0 - initial_capital: 10000.0 - ticker_cooldown_days: 0 - market_regime_spy_threshold: null - min_candidate_breadth: null - settlement_days: 1 - max_gap_pct: 0.04 - compound_returns: false -universe: - source: midlarge - min_price: 10.0 -backtest: - start_date: null - end_date: null - lookback_trading_days: 200 - pre_screen_threshold: 0.01 -cache: - enabled: true - dir: data/cache/intraday -output: - dir: runs/intraday_orb - verbose: false diff --git a/libs/intraday/cache.py b/libs/intraday/cache.py index 7538027..da3a9c3 100644 --- a/libs/intraday/cache.py +++ b/libs/intraday/cache.py @@ -16,7 +16,7 @@ import pyarrow.parquet as pq _CACHE_METADATA = { - b"intraday_cache_version": b"2", + b"intraday_cache_version": b"3", # v3: SIP data (v2 was IEX ~2.5% tape) b"intraday_cache_source": b"api_v1_alpaca_intraday", b"intraday_cache_interval": b"5min", } diff --git a/libs/intraday/screener.py b/libs/intraday/screener.py index 38c7b38..f68b0f2 100644 --- a/libs/intraday/screener.py +++ b/libs/intraday/screener.py @@ -304,86 +304,91 @@ async def fetch_intraday_bulk( client: Oracle API client. cache: Intraday Parquet cache. interval: Candle interval (default '5min'). - concurrency: Max concurrent API calls (Semaphore). Keep <= 10 to respect Alpaca rate limits. + concurrency: Max concurrent API calls (Semaphore). Keep <= 8 to respect rate limits. progress_callback: Called with (completed, total, cache_hits, api_calls). """ - svc = PriceService(client) - semaphore = asyncio.Semaphore(concurrency) + # Oracle endpoint uses "5m" format; config uses "5min" format + oracle_interval = interval.replace("min", "m") - # Build flat list of (date, ticker) pairs - pairs: list[tuple[str, str]] = [] - for day in sorted(candidates.keys()): - for ticker in candidates[day]: - pairs.append((day, ticker)) - - total = len(pairs) + # Build flat list and separate cache hits from misses + total = sum(len(tickers) for tickers in candidates.values()) completed = 0 cache_hits = 0 api_calls = 0 lock = asyncio.Lock() + semaphore = asyncio.Semaphore(concurrency) result: dict[str, dict[str, list[dict]]] = defaultdict(dict) + misses: dict[str, list[str]] = defaultdict(list) # {day: [ticker, ...]} - async def fetch_one(day: str, ticker: str) -> None: - nonlocal completed, cache_hits, api_calls - - # Check cache first - cached = cache.get(ticker, day) if cache else None - if cached is not None: - async with lock: + # Phase 1: resolve cache hits synchronously + for day in sorted(candidates.keys()): + for ticker in candidates[day]: + cached = cache.get(ticker, day) if cache else None + if cached is not None: result[day][ticker] = cached completed += 1 cache_hits += 1 - if progress_callback: - progress_callback(completed, total, cache_hits, api_calls) - return + else: + misses[day].append(ticker) + + if progress_callback: + progress_callback(completed, total, cache_hits, api_calls) - # Cache miss — fetch from API + # Phase 2: fetch cache misses via multi-ticker endpoint (batched by date, chunk ≤ 75) + CHUNK = 75 + + async def fetch_day_chunk(day: str, chunk: list[str]) -> None: + nonlocal completed, api_calls + fetched: dict[str, list[dict]] = {} async with semaphore: try: - # Call Alpaca intraday endpoint directly (PriceService.get_historical_intraday - # looks for "data" key but this endpoint returns "candles") raw = await client.get( - f"/api/v1/alpaca/intraday/{ticker}", + "/api/v1/alpaca/intraday", params={ - "interval": interval, + "tickers": ",".join(chunk), + "interval": oracle_interval, "start_date": day, "end_date": day, - "limit": 500, }, ) - bars = [ - { - "timestamp": b.get("timestamp", ""), - "open": float(b.get("open", 0)), - "high": float(b.get("high", 0)), - "low": float(b.get("low", 0)), - "close": float(b.get("close", 0)), - "volume": float(b.get("volume", 0)), - "vwap": float(b.get("vwap", 0) or 0), - } - for b in raw.get("candles", []) - ] - if cache: - cache.put(ticker, day, bars) - async with lock: - if bars: - result[day][ticker] = bars - completed += 1 - api_calls += 1 + bars_by_ticker = raw.get("bars", {}) + for ticker in chunk: + fetched[ticker] = [ + { + "timestamp": b.get("timestamp", ""), + "open": float(b.get("open", 0)), + "high": float(b.get("high", 0)), + "low": float(b.get("low", 0)), + "close": float(b.get("close", 0)), + "volume": float(b.get("volume", 0)), + "vwap": float(b.get("vwap", 0) or 0), + } + for b in bars_by_ticker.get(ticker, []) + ] except Exception: - async with lock: - completed += 1 - api_calls += 1 - finally: - if progress_callback: - async with lock: - ch, ac = cache_hits, api_calls - progress_callback(completed, total, ch, ac) - - # Small delay after each API call to respect rate limits (~160 calls/min max) - await asyncio.sleep(0.3) + pass - tasks = [asyncio.create_task(fetch_one(day, ticker)) for day, ticker in pairs] + async with lock: + for ticker in chunk: + bars = fetched.get(ticker, []) + if cache and bars: + cache.put(ticker, day, bars) + if bars: + result[day][ticker] = bars + completed += len(chunk) + api_calls += 1 + _c, _t, _ch, _ac = completed, total, cache_hits, api_calls + + if progress_callback: + progress_callback(_c, _t, _ch, _ac) + + tasks = [ + asyncio.create_task(fetch_day_chunk(day, chunk)) + for day, tickers in sorted(misses.items()) + for chunk in ( + tickers[i : i + CHUNK] for i in range(0, len(tickers), CHUNK) + ) + ] await asyncio.gather(*tasks) return dict(result) diff --git a/libs/oracle_client/alpaca.py b/libs/oracle_client/alpaca.py index 29927f1..e8fa5b2 100644 --- a/libs/oracle_client/alpaca.py +++ b/libs/oracle_client/alpaca.py @@ -147,6 +147,42 @@ def get_multi_intraday_bars( return result +def get_multi_intraday_bars_today( + tickers: list[str], + interval: str = "5min", + base_url: str | None = None, +) -> dict[str, list[dict]]: + """Fetch today's intraday bars (IEX real-time) via Oracle /alpaca/intraday/today. + + Uses IEX feed with force_refresh=True — suitable for live paper trading. + Returns {ticker: [{timestamp (ISO8601), open, high, low, close, volume}, ...]}. + """ + import httpx + + if not tickers: + return {} + + result: dict[str, list[dict]] = {} + url = (base_url or _base_url()) + "/api/v1/alpaca/intraday/today" + + chunk_size = 75 + for i in range(0, len(tickers), chunk_size): + chunk = tickers[i : i + chunk_size] + try: + resp = httpx.get( + url, + params={"tickers": ",".join(chunk), "interval": interval}, + timeout=90.0, + ) + resp.raise_for_status() + for ticker, bars in resp.json().get("bars", {}).items(): + result[ticker] = bars + except Exception as exc: + log.warning("Oracle intraday_today chunk %d failed: %s", i // chunk_size, exc) + + return result + + def get_snapshot(ticker: str, base_url: str | None = None) -> AlpacaSnapshot | None: """Fetch real-time snapshot for a single ticker (synchronous).