Fix ORB intraday data pipeline and consolidate strategy configs

- 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 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 564bcba27c
commit 86419beeb0

@ -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:

@ -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

@ -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:309: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

@ -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

@ -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

@ -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:309: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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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",
}

@ -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)

@ -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).

Loading…
Cancel
Save