You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6479 lines
263 KiB
Python

from __future__ import annotations
import datetime as dt
import pytest
from libs.intraday.domain import ORBStrategyParams
from libs.intraday.orb_simulator import (
ORBSimulationState,
_build_idle_sleeve_orders,
_entry_market_guard_scale,
_market_return_at_entry_open,
_orb_form4_size_scale,
_orb_liquid_leader_conviction_size_scale,
_orb_opening_burst_liquid_size_scale,
_orb_ownership_initial_size_scale,
_orb_sector_confirmation_size_scale,
_orb_soft_day_sector_confirmation_override_allows,
_orb_soft_day_sector_confirmation_override_base_sizing,
_orb_soft_day_setup_profile_allows,
_orb_soft_day_vwap_min_score_pct,
_orb_soft_day_vwap_reason_allowed,
_orb_soft_day_vwap_reason_param,
_orb_soft_day_vwap_size_scale,
_orb_unboosted_primary_fragility_size_scale,
_orb_unsupported_attention_size_scale,
compute_orb_candidates,
run_orb_simulation,
run_orb_simulation_with_state,
simulate_orb_day,
simulate_orb_trade,
)
def _enrichment_for(*tickers: str) -> dict[str, dict[str, dict]]:
return {
ticker: {
"2026-01-05": {
"atr_14": 1.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"avg_daily_vol_14d": 10_000.0,
"prev_close": 99.0,
"today_open": 100.0,
"entropy_20d": 0.5,
"atr_ratio_10_60": 0.8,
"range_compression_10_60": 0.7,
"gap_zscore_20d": 0.2,
}
}
for ticker in tickers
}
def _parse_test_ts(value: str) -> dt.datetime:
return dt.datetime.fromisoformat(value)
def _idle_bars(open_price: float, close_price: float) -> list[dict]:
return [
{
"timestamp": "2026-01-05T09:30:00-05:00",
"open": open_price,
"high": max(open_price, close_price),
"low": min(open_price, close_price),
"close": open_price,
"volume": 100_000,
},
{
"timestamp": "2026-01-05T15:55:00-05:00",
"open": close_price,
"high": close_price,
"low": close_price,
"close": close_price,
"volume": 100_000,
},
]
def _idle_reclaim_bars(open_price: float, checkpoint_close: float, close_price: float) -> list[dict]:
return [
{
"timestamp": "2026-01-05T09:30:00-05:00",
"open": open_price,
"high": max(open_price, checkpoint_close),
"low": min(open_price, checkpoint_close),
"close": open_price,
"volume": 100_000,
},
{
"timestamp": "2026-01-05T10:30:00-05:00",
"open": open_price,
"high": max(open_price, checkpoint_close),
"low": min(open_price, checkpoint_close),
"close": checkpoint_close,
"volume": 100_000,
},
{
"timestamp": "2026-01-05T15:55:00-05:00",
"open": checkpoint_close,
"high": max(open_price, checkpoint_close, close_price),
"low": min(open_price, checkpoint_close, close_price),
"close": close_price,
"volume": 100_000,
},
]
def test_pead_like_idle_parking_uses_tqqq_on_strong_market_close() -> None:
params = ORBStrategyParams(
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_parking_mode="pead_like",
orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"],
orb_idle_sleeve_weights={"parking": 1.0},
orb_idle_sleeve_market_ticker="QQQ",
orb_idle_sleeve_min_market_day_return_pct=-0.005,
orb_idle_sleeve_parking_overlay_min_market_return_pct=0.005,
orb_idle_sleeve_parking_overlay_min_market_close_location=0.70,
)
orders = _build_idle_sleeve_orders(
{
"QQQ": _idle_bars(100.0, 101.0),
"QQQM": _idle_bars(100.0, 100.4),
"TQQQ": _idle_bars(100.0, 103.0),
},
"2026-01-05",
params,
{},
)
assert [(order.ticker, order.sleeve) for order in orders] == [("TQQQ", "parking")]
def test_pead_like_idle_parking_uses_qqqm_on_ordinary_risk_on_close() -> None:
params = ORBStrategyParams(
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_parking_mode="pead_like",
orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"],
orb_idle_sleeve_weights={"parking": 1.0},
orb_idle_sleeve_market_ticker="QQQ",
orb_idle_sleeve_min_market_day_return_pct=-0.005,
orb_idle_sleeve_parking_overlay_min_market_return_pct=0.005,
orb_idle_sleeve_parking_overlay_min_market_close_location=0.70,
)
orders = _build_idle_sleeve_orders(
{
"QQQ": _idle_bars(100.0, 100.1),
"QQQM": _idle_bars(100.0, 100.2),
"TQQQ": _idle_bars(100.0, 100.6),
},
"2026-01-05",
params,
{},
)
assert [(order.ticker, order.sleeve) for order in orders] == [("QQQM", "parking")]
def test_pead_like_idle_parking_falls_back_to_sgov_when_risk_on_disallowed() -> None:
params = ORBStrategyParams(
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_parking_mode="pead_like",
orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"],
orb_idle_sleeve_risk_off_symbols=["GLD", "SGOV"],
orb_idle_sleeve_weights={"parking": 1.0},
orb_idle_sleeve_market_ticker="QQQ",
orb_idle_sleeve_min_market_day_return_pct=-0.005,
orb_idle_sleeve_force_defensive_fallback=True,
)
orders = _build_idle_sleeve_orders(
{
"QQQ": _idle_bars(100.0, 99.0),
"QQQM": _idle_bars(100.0, 99.5),
"TQQQ": _idle_bars(100.0, 97.0),
"GLD": _idle_bars(100.0, 101.0),
"SGOV": _idle_bars(100.0, 100.01),
},
"2026-01-05",
params,
{},
)
assert [(order.ticker, order.sleeve) for order in orders] == [("SGOV", "parking")]
def test_idle_sleeve_can_build_pead_like_five_sleeve_book() -> None:
params = ORBStrategyParams(
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_parking_mode="pead_like",
orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"],
orb_idle_sleeve_risk_off_symbols=["GLD", "SGOV"],
orb_idle_sleeve_weights={
"parking": 0.35,
"idle_alpha": 0.30,
"form4": 0.15,
"ownership": 0.10,
"risk_off_alpha": 0.10,
},
orb_idle_sleeve_max_positions=5,
orb_idle_sleeve_market_ticker="QQQ",
orb_idle_sleeve_min_market_day_return_pct=-0.005,
orb_idle_sleeve_min_market_day_return_for_stock_sleeves_pct=-0.005,
)
enrichment = _enrichment_for("ALPHA", "F4", "OWN")
enrichment["F4"]["2026-01-05"].update(
{
"form4_flag": True,
"form4_owner_count": 2,
"form4_c_suite_count": 1,
"form4_total_value": 1_000_000,
}
)
enrichment["OWN"]["2026-01-05"].update(
{
"ownership_13dg_flag": True,
"ownership_13dg_initial_flag": True,
"ownership_13dg_strength_score": 3.0,
}
)
orders = _build_idle_sleeve_orders(
{
"QQQ": _idle_bars(100.0, 99.8),
"QQQM": _idle_bars(100.0, 100.1),
"TQQQ": _idle_bars(100.0, 100.3),
"GLD": _idle_bars(100.0, 100.4),
"SGOV": _idle_bars(100.0, 100.01),
"ALPHA": _idle_bars(100.0, 102.0),
"F4": _idle_bars(100.0, 100.2),
"OWN": _idle_bars(100.0, 100.3),
},
"2026-01-05",
params,
enrichment,
)
assert {(order.ticker, order.sleeve) for order in orders} == {
("QQQM", "parking"),
("ALPHA", "idle_alpha"),
("F4", "form4"),
("OWN", "ownership"),
("GLD", "risk_off_alpha"),
}
def test_idle_sleeve_can_build_late_close_reclaim_book() -> None:
params = ORBStrategyParams(
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_weights={"close_reclaim": 1.0},
orb_idle_sleeve_reclaim_checkpoint_minutes=60,
orb_idle_sleeve_reclaim_min_day_return_pct=0.004,
orb_idle_sleeve_reclaim_max_early_return_pct=0.004,
orb_idle_sleeve_reclaim_min_late_return_pct=0.006,
orb_idle_sleeve_reclaim_min_close_location=0.70,
orb_idle_sleeve_reclaim_min_day_dollar_vol=20_000_000,
orb_idle_sleeve_reclaim_min_avg_dollar_vol=80_000_000,
)
orders = _build_idle_sleeve_orders(
{
"RECL": _idle_reclaim_bars(100.0, 99.2, 102.0),
"MORN": _idle_reclaim_bars(100.0, 102.0, 103.0),
},
"2026-01-05",
params,
_enrichment_for("RECL", "MORN"),
)
assert [(order.ticker, order.sleeve) for order in orders] == [("RECL", "close_reclaim")]
def test_idle_sleeve_can_build_sector_rotation_book() -> None:
params = ORBStrategyParams(
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_weights={"sector_rotation": 1.0},
orb_idle_sleeve_sector_rotation_symbols=["XLK", "XLE"],
orb_idle_sleeve_sector_rotation_min_day_return_pct=0.002,
orb_idle_sleeve_sector_rotation_min_close_location=0.60,
orb_idle_sleeve_min_market_day_return_pct=None,
)
orders = _build_idle_sleeve_orders(
{
"XLK": _idle_bars(100.0, 100.3),
"XLE": _idle_bars(100.0, 101.0),
},
"2026-01-05",
params,
{},
)
assert [(order.ticker, order.sleeve) for order in orders] == [("XLE", "sector_rotation")]
def test_idle_sleeve_opens_on_streaming_chunk_boundary() -> None:
params = ORBStrategyParams(
min_candidates_to_trade=99,
initial_capital=10_000.0,
daily_budget_reset=True,
compound_returns=False,
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_parking_mode="pead_like",
orb_idle_sleeve_parking_symbols=["QQQM"],
orb_idle_sleeve_weights={"parking": 1.0},
orb_idle_sleeve_market_ticker="QQQ",
orb_idle_sleeve_min_market_day_return_pct=-0.005,
)
day1 = "2026-01-05"
day2 = "2026-01-06"
all_intraday = {
day1: {
"QQQ": _idle_bars(100.0, 100.2),
"QQQM": _idle_bars(100.0, 100.5),
},
day2: {
"QQQM": [
{
"timestamp": f"{day2}T09:30:00-05:00",
"open": 101.0,
"high": 101.1,
"low": 100.9,
"close": 101.0,
"volume": 100_000,
}
],
},
}
chunk1, state = run_orb_simulation_with_state(
{day1: all_intraday[day1]},
[day1],
params,
{},
next_trading_day_after_window=day2,
)
chunk2, _ = run_orb_simulation_with_state(
{day2: all_intraday[day2]},
[day2],
params,
{},
state=state,
)
assert chunk1[0].entry_diagnostics["orb_idle_sleeve_symbols_opened"] == ["QQQM"]
assert len(chunk2[0].trades) == 1
assert chunk2[0].trades[0].ticker == "QQQM"
assert chunk2[0].trades[0].exit_reason == "idle_sleeve_next_open"
def test_idle_sleeve_can_enter_after_open_and_exit_same_day_close() -> None:
params = ORBStrategyParams(
min_candidates_to_trade=99,
initial_capital=10_000.0,
daily_budget_reset=True,
compound_returns=False,
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_entry_timing="minutes_after_open",
orb_idle_sleeve_entry_minutes_after_open=60,
orb_idle_sleeve_exit_timing="same_day_close",
orb_idle_sleeve_parking_mode="pead_like",
orb_idle_sleeve_parking_symbols=["QQQM"],
orb_idle_sleeve_weights={"parking": 1.0},
orb_idle_sleeve_market_ticker="QQQ",
orb_idle_sleeve_min_market_day_return_pct=-0.005,
)
day = "2026-01-05"
bars = [
{
"timestamp": f"{day}T09:30:00-05:00",
"open": 100.0,
"high": 100.2,
"low": 99.9,
"close": 100.1,
"volume": 100_000,
},
{
"timestamp": f"{day}T10:30:00-05:00",
"open": 100.1,
"high": 100.6,
"low": 100.0,
"close": 100.5,
"volume": 100_000,
},
{
"timestamp": f"{day}T15:55:00-05:00",
"open": 100.5,
"high": 101.1,
"low": 100.4,
"close": 101.0,
"volume": 100_000,
},
]
results = run_orb_simulation(
{day: {"QQQ": bars, "QQQM": bars}},
[day],
params,
{},
)
assert len(results[0].trades) == 1
trade = results[0].trades[0]
assert trade.ticker == "QQQM"
assert trade.entry_time == f"{day}T10:30:00-05:00"
assert trade.exit_time == f"{day}T15:55:00-05:00"
assert trade.exit_reason == "idle_sleeve_same_day_close"
assert trade.orb_idle_sleeve_overnight is False
def test_idle_sleeve_same_day_stop_loss_exits_before_close() -> None:
params = ORBStrategyParams(
min_candidates_to_trade=99,
initial_capital=10_000.0,
daily_budget_reset=True,
compound_returns=False,
orb_idle_sleeve_enabled=True,
orb_idle_sleeve_entry_timing="minutes_after_open",
orb_idle_sleeve_entry_minutes_after_open=60,
orb_idle_sleeve_exit_timing="same_day_close",
orb_idle_sleeve_same_day_stop_loss_pct=-0.03,
orb_idle_sleeve_parking_mode="pead_like",
orb_idle_sleeve_parking_symbols=["QQQM"],
orb_idle_sleeve_weights={"parking": 1.0},
orb_idle_sleeve_market_ticker="QQQ",
orb_idle_sleeve_min_market_day_return_pct=-0.005,
)
day = "2026-01-05"
bars = [
{
"timestamp": f"{day}T09:30:00-05:00",
"open": 100.0,
"high": 100.2,
"low": 99.8,
"close": 100.0,
"volume": 100_000,
},
{
"timestamp": f"{day}T10:30:00-05:00",
"open": 100.0,
"high": 102.2,
"low": 99.9,
"close": 102.0,
"volume": 100_000,
},
{
"timestamp": f"{day}T11:00:00-05:00",
"open": 102.0,
"high": 102.1,
"low": 98.5,
"close": 99.0,
"volume": 100_000,
},
{
"timestamp": f"{day}T15:55:00-05:00",
"open": 99.0,
"high": 105.0,
"low": 98.9,
"close": 105.0,
"volume": 100_000,
},
]
results = run_orb_simulation(
{day: {"QQQ": bars, "QQQM": bars}},
[day],
params,
{},
)
trade = results[0].trades[0]
assert trade.exit_reason == "idle_sleeve_same_day_stop_loss"
assert trade.exit_time == f"{day}T11:00:00-05:00"
assert trade.exit_price == 98.94
assert trade.orb_idle_sleeve_overnight is False
def test_soft_day_setup_profile_rejects_market_regime_only_fallback() -> None:
params = ORBStrategyParams(soft_day_setup_profile="skip_day_reclaim_v1")
cand = {
"score": 1.05,
"rvol": 10.0,
"gap_pct": 0.03,
"premarket_dollar_vol": 100_000_000,
"body_ratio": 0.8,
"close_location": 0.9,
}
enrich = {"ret_5d": 0.3}
assert not _orb_soft_day_setup_profile_allows(
params, "market_regime", cand, enrich
)
def test_soft_day_setup_profile_allows_breadth_gap_down_momentum() -> None:
params = ORBStrategyParams(soft_day_setup_profile="skip_day_reclaim_v1")
cand = {
"score": 0.70,
"rvol": 18.0,
"gap_pct": -0.04,
"premarket_dollar_vol": 50_000_000,
"body_ratio": 0.5,
"close_location": 0.65,
}
enrich = {"ret_5d": 0.20}
assert _orb_soft_day_setup_profile_allows(params, "breadth", cand, enrich)
def test_soft_day_vwap_reason_overrides_default_score_and_size() -> None:
params = ORBStrategyParams(
soft_day_vwap_reclaim_min_score_pct=0.85,
soft_day_vwap_reclaim_min_score_pct_market_regime=0.70,
soft_day_vwap_reclaim_min_score_pct_breadth=0.80,
soft_day_vwap_reclaim_min_score_pct_joint=0.90,
soft_day_vwap_reclaim_size_scale=0.03,
soft_day_vwap_reclaim_size_scale_market_regime=0.05,
soft_day_vwap_reclaim_size_scale_joint=0.02,
)
assert _orb_soft_day_vwap_min_score_pct(params, "market_regime") == 0.70
assert _orb_soft_day_vwap_min_score_pct(params, "breadth") == 0.80
assert _orb_soft_day_vwap_min_score_pct(params, "market_regime+breadth") == 0.90
assert _orb_soft_day_vwap_min_score_pct(params, None) == 0.85
assert _orb_soft_day_vwap_size_scale(params, "market_regime") == 0.05
assert _orb_soft_day_vwap_size_scale(params, "market_regime+breadth") == 0.02
assert _orb_soft_day_vwap_size_scale(params, "breadth") == 0.03
def test_soft_day_vwap_reason_allowlist_limits_auxiliary_sleeve() -> None:
params = ORBStrategyParams(
soft_day_vwap_reclaim_allowed_reason_parts=["hard_breadth"]
)
assert _orb_soft_day_vwap_reason_allowed(params, "market_regime+hard_breadth")
assert not _orb_soft_day_vwap_reason_allowed(params, "market_regime+breadth")
assert not _orb_soft_day_vwap_reason_allowed(params, "breadth")
def test_soft_day_vwap_reason_quality_overrides_default_gates() -> None:
params = ORBStrategyParams(
soft_day_vwap_reclaim_min_premarket_dollar_vol=20_000_000,
soft_day_vwap_reclaim_min_premarket_dollar_vol_hard_breadth=5_000_000,
soft_day_vwap_reclaim_min_body_ratio=0.15,
soft_day_vwap_reclaim_min_body_ratio_hard_breadth=0.0,
soft_day_vwap_reclaim_min_close_location=0.65,
soft_day_vwap_reclaim_min_close_location_hard_breadth=0.32,
soft_day_vwap_reclaim_min_rvol=4.0,
soft_day_vwap_reclaim_min_rvol_joint=5.0,
soft_day_vwap_reclaim_max_rvol=12.0,
soft_day_vwap_reclaim_max_rvol_breadth=35.0,
)
assert (
_orb_soft_day_vwap_reason_param(
params,
"market_regime+hard_breadth",
"soft_day_vwap_reclaim_min_premarket_dollar_vol",
)
== 5_000_000
)
assert (
_orb_soft_day_vwap_reason_param(
params,
"market_regime",
"soft_day_vwap_reclaim_min_premarket_dollar_vol",
)
== 20_000_000
)
assert (
_orb_soft_day_vwap_reason_param(
params,
"market_regime+hard_breadth",
"soft_day_vwap_reclaim_min_body_ratio",
)
== 0.0
)
assert (
_orb_soft_day_vwap_reason_param(
params,
"market_regime+hard_breadth",
"soft_day_vwap_reclaim_min_close_location",
)
== 0.32
)
assert (
_orb_soft_day_vwap_reason_param(
params,
"breadth",
"soft_day_vwap_reclaim_min_rvol",
)
== 4.0
)
assert (
_orb_soft_day_vwap_reason_param(
params,
"market_regime+breadth",
"soft_day_vwap_reclaim_min_rvol",
)
== 5.0
)
assert (
_orb_soft_day_vwap_reason_param(
params,
"breadth",
"soft_day_vwap_reclaim_max_rvol",
)
== 35.0
)
def test_broad_gapup_continuation_admits_bounded_high_gap_candidate() -> None:
bars = {
"HIGH": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.9, "high": 101.5, "low": 100.7, "close": 101.3, "volume": 5_000},
],
}
enrichment = _enrichment_for("HIGH")
enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0
enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08
enrichment["HIGH"]["2026-01-05"]["avg_daily_vol_14d"] = 1_000_000.0
base_params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=1000.0,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=10,
)
assert compute_orb_candidates(bars, "2026-01-05", base_params, enrichment) == []
params = base_params.model_copy(
update={
"broad_gapup_continuation_enabled": True,
"broad_gapup_continuation_min_gap_pct": 0.04,
"broad_gapup_continuation_max_gap_pct": 0.20,
"broad_gapup_continuation_min_rvol": None,
"broad_gapup_continuation_min_premarket_dollar_vol": 10_000_000,
"broad_gapup_continuation_min_first_bar_dollar_vol": 500_000,
"broad_gapup_continuation_min_body_ratio": 0.50,
"broad_gapup_continuation_min_close_location": 0.80,
}
)
candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment)
assert len(candidates) == 1
assert candidates[0]["ticker"] == "HIGH"
assert candidates[0]["broad_gapup_continuation"] is True
assert candidates[0]["rvol"] < base_params.min_rvol
liquid_only_params = params.model_copy(
update={"broad_gapup_continuation_min_avg_dollar_vol": 2_000_000_000}
)
assert compute_orb_candidates(bars, "2026-01-05", liquid_only_params, enrichment) == []
def test_broad_gapup_continuation_trades_with_dedicated_size_scale() -> None:
bars = {
"HIGH": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 5_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 5_000},
],
}
enrichment = _enrichment_for("HIGH")
enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0
enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
broad_gapup_continuation_enabled=True,
broad_gapup_continuation_min_gap_pct=0.04,
broad_gapup_continuation_max_gap_pct=0.20,
broad_gapup_continuation_min_rvol=1.0,
broad_gapup_continuation_min_premarket_dollar_vol=10_000_000,
broad_gapup_continuation_min_first_bar_dollar_vol=500_000,
broad_gapup_continuation_min_body_ratio=0.50,
broad_gapup_continuation_min_close_location=0.80,
broad_gapup_continuation_size_scale=0.25,
broad_gapup_continuation_max_trades=1,
)
result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0)
assert len(result.trades) == 1
trade = result.trades[0]
assert trade.trigger_type == "broad_gapup_continuation"
assert trade.broad_gapup_continuation is True
assert trade.broad_gapup_continuation_size_scale == 0.25
assert trade.shares == 2
def test_broad_gapup_continuation_opening_burst_enters_next_bar_open() -> None:
bars = {
"HIGH": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.7, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 5_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 5_000},
],
}
enrichment = _enrichment_for("HIGH")
enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0
enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
broad_gapup_continuation_enabled=True,
broad_gapup_continuation_entry_mode="opening_burst",
broad_gapup_continuation_min_gap_pct=0.04,
broad_gapup_continuation_max_gap_pct=0.20,
broad_gapup_continuation_min_rvol=1.0,
broad_gapup_continuation_min_premarket_dollar_vol=10_000_000,
broad_gapup_continuation_min_first_bar_dollar_vol=500_000,
broad_gapup_continuation_min_body_ratio=0.50,
broad_gapup_continuation_min_close_location=0.80,
broad_gapup_continuation_size_scale=0.25,
broad_gapup_continuation_max_trades=1,
)
result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0)
assert len(result.trades) == 1
assert result.trades[0].trigger_type == "broad_gapup_continuation"
assert result.trades[0].entry_price == 100.7
def test_entry_tie_break_rank_by_score_prioritizes_auxiliary_leader_on_same_bar() -> None:
bars = {
"BASE": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 10_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 10_000},
],
"HIGH": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 100_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 50_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 50_000},
],
}
enrichment = _enrichment_for("BASE", "HIGH")
enrichment["BASE"]["2026-01-05"]["prev_close"] = 98.0
enrichment["BASE"]["2026-01-05"]["ret_5d"] = 0.03
enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0
enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=1.0,
max_position_pct=1.0,
max_trades_per_day=2,
slippage_bps=0.0,
broad_gapup_continuation_enabled=True,
broad_gapup_continuation_min_gap_pct=0.04,
broad_gapup_continuation_max_gap_pct=0.20,
broad_gapup_continuation_min_rvol=1.0,
broad_gapup_continuation_min_premarket_dollar_vol=10_000_000,
broad_gapup_continuation_min_first_bar_dollar_vol=500_000,
broad_gapup_continuation_min_body_ratio=0.50,
broad_gapup_continuation_min_close_location=0.80,
broad_gapup_continuation_size_scale=1.0,
broad_gapup_continuation_max_trades=1,
)
legacy = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
available_cash=10_000.0,
)
ranked = simulate_orb_day(
bars,
"2026-01-05",
params.model_copy(update={"entry_tie_break_rank_by_score": True}),
enrichment,
equity=10_000.0,
available_cash=10_000.0,
)
assert legacy.trades[0].ticker == "BASE"
assert ranked.trades[0].ticker == "HIGH"
assert ranked.trades[0].trigger_type == "broad_gapup_continuation"
def test_broad_gapup_continuation_bypasses_generic_soft_day_profile() -> None:
bars = {
"HIGH": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 5_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 5_000},
],
}
enrichment = _enrichment_for("HIGH", "QQQ")
enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0
enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08
enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0
enrichment["QQQ"]["2026-01-05"]["today_open"] = 100.0
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
market_regime_spy_threshold=0.01,
market_regime_ticker="QQQ",
soft_day_fallback_on_regime_skip=True,
soft_day_regime_skip_size_scale=0.1,
soft_day_combined_size_scale_floor=1.0,
soft_day_setup_profile="skip_day_reclaim_v1",
soft_day_min_ret_5d=0.50,
broad_gapup_continuation_enabled=True,
broad_gapup_continuation_min_gap_pct=0.04,
broad_gapup_continuation_max_gap_pct=0.20,
broad_gapup_continuation_min_rvol=1.0,
broad_gapup_continuation_min_premarket_dollar_vol=10_000_000,
broad_gapup_continuation_min_first_bar_dollar_vol=500_000,
broad_gapup_continuation_min_body_ratio=0.50,
broad_gapup_continuation_min_close_location=0.80,
broad_gapup_continuation_size_scale=0.25,
)
result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0)
assert result.is_soft_day is True
assert result.soft_day_reason == "market_regime"
assert len(result.trades) == 1
assert result.trades[0].trigger_type == "broad_gapup_continuation"
def test_broad_gapup_continuation_scan_is_not_blocked_by_primary_sector_cap() -> None:
bars = {
"BASE": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 100.8, "low": 99.8, "close": 100.7, "volume": 20_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 10_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.1, "high": 101.3, "low": 100.9, "close": 101.2, "volume": 10_000},
],
"HIGH": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 5_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 5_000},
],
}
enrichment = _enrichment_for("BASE", "HIGH")
enrichment["BASE"]["2026-01-05"]["prev_close"] = 98.0
enrichment["BASE"]["2026-01-05"]["ret_5d"] = 0.03
enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0
enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=1,
max_candidates_per_sector=1,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
broad_gapup_continuation_enabled=True,
broad_gapup_continuation_min_gap_pct=0.04,
broad_gapup_continuation_max_gap_pct=0.20,
broad_gapup_continuation_min_rvol=1.0,
broad_gapup_continuation_min_premarket_dollar_vol=10_000_000,
broad_gapup_continuation_min_first_bar_dollar_vol=500_000,
broad_gapup_continuation_min_body_ratio=0.50,
broad_gapup_continuation_min_close_location=0.80,
broad_gapup_continuation_size_scale=0.25,
broad_gapup_continuation_max_candidates=1,
broad_gapup_continuation_max_trades=1,
)
result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
ticker_sectors={"BASE": "TECH", "HIGH": "TECH"},
)
assert "broad_gapup_continuation" in {trade.trigger_type for trade in result.trades}
def test_intraday_continuation_can_use_dedicated_low_atr_pct_gate() -> None:
bars = {
"LIQ": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.1, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.1, "high": 100.5, "low": 100.0, "close": 100.4, "volume": 10_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.4, "high": 100.9, "low": 100.3, "close": 100.8, "volume": 10_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 10_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.1, "high": 101.6, "low": 101.0, "close": 101.5, "volume": 10_000},
{"timestamp": "2026-01-05T09:55:00-05:00", "open": 101.5, "high": 102.1, "low": 101.4, "close": 102.0, "volume": 10_000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 102.1, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 10_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.2, "low": 102.6, "close": 103.0, "volume": 10_000},
],
}
enrichment = _enrichment_for("LIQ")
enrichment["LIQ"]["2026-01-05"]["atr_14"] = 2.0
enrichment["LIQ"]["2026-01-05"]["prev_close"] = 99.0
base_params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=100_000_000.0,
min_atr_14=0.1,
min_atr_pct=0.04,
min_rvol=1000.0,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=10,
)
assert compute_orb_candidates(bars, "2026-01-05", base_params, enrichment) == []
params = base_params.model_copy(
update={
"intraday_continuation_reclaim_enabled": True,
"intraday_continuation_reclaim_signal_minutes": 30,
"intraday_continuation_reclaim_min_signal_return_pct": 0.015,
"intraday_continuation_reclaim_min_signal_dollar_vol": 1_000_000,
"intraday_continuation_reclaim_min_signal_close_location": 0.80,
"intraday_continuation_reclaim_require_signal_above_vwap": False,
"intraday_continuation_reclaim_min_gap_pct": -0.02,
"intraday_continuation_reclaim_max_gap_pct": 0.02,
"intraday_continuation_reclaim_min_avg_dollar_vol": 100_000_000,
"intraday_continuation_reclaim_min_atr_pct": 0.01,
"intraday_continuation_reclaim_max_atr_pct": 0.03,
}
)
candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment)
assert len(candidates) == 1
assert candidates[0]["ticker"] == "LIQ"
assert candidates[0]["intraday_continuation_reclaim"] is True
assert candidates[0]["intraday_continuation_entry_price"] == 102.1
too_low_volatility_ceiling = params.model_copy(
update={"intraday_continuation_reclaim_max_atr_pct": 0.015}
)
assert compute_orb_candidates(
bars,
"2026-01-05",
too_low_volatility_ceiling,
enrichment,
) == []
def test_market_thrust_liquid_opening_burst_enters_without_breakout() -> None:
bars = {
"BURST": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.9, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.2, "high": 101.2, "low": 100.8, "close": 101.0, "volume": 5_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.0, "high": 102.2, "low": 100.9, "close": 102.0, "volume": 5_000},
],
}
enrichment = _enrichment_for("BURST")
enrichment["BURST"]["2026-01-05"]["prev_close"] = 98.0
enrichment["BURST"]["2026-01-05"]["ret_5d"] = 0.03
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=1000.0,
min_abs_gap_pct=0.01,
max_gap_pct=0.04,
max_candidates=10,
min_candidates_to_trade=1,
min_candidate_breadth=1.1,
soft_day_fallback_on_breadth_skip=True,
soft_day_breadth_skip_size_scale=0.1,
soft_day_combined_size_scale_floor=1.0,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
market_thrust_breadth_override_enabled=True,
market_thrust_liquid_continuation_enabled=True,
market_thrust_liquid_continuation_entry_mode="opening_burst",
market_thrust_liquid_continuation_min_gap_pct=0.02,
market_thrust_liquid_continuation_max_gap_pct=0.04,
market_thrust_liquid_continuation_min_first_bar_return_pct=0.007,
market_thrust_liquid_continuation_min_first_bar_dollar_vol=100_000,
market_thrust_liquid_continuation_min_avg_dollar_vol=100_000,
market_thrust_liquid_continuation_min_body_ratio=0.5,
market_thrust_liquid_continuation_min_close_location=0.8,
market_thrust_liquid_continuation_max_candidates=1,
market_thrust_liquid_continuation_max_trades=1,
market_thrust_liquid_continuation_size_scale=0.2,
)
result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0)
assert result.entry_diagnostics["market_thrust_opening_burst_candidates"] == 1
assert len(result.trades) == 1
trade = result.trades[0]
assert trade.trigger_type == "market_thrust_opening_burst"
assert trade.entry_time == "2026-01-05T09:35:00-05:00"
assert trade.entry_price == 101.2
assert trade.market_thrust_opening_burst is True
def test_iex_live_volume_multiplier_rescales_market_thrust_first_bar_gate() -> None:
bars = {
"IEX": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.9, "volume": 500},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.2, "high": 101.5, "low": 100.8, "close": 101.3, "volume": 500},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.3, "high": 102.0, "low": 101.0, "close": 101.8, "volume": 500},
],
}
enrichment = _enrichment_for("IEX")
enrichment["IEX"]["2026-01-05"]["prev_close"] = 98.0
enrichment["IEX"]["2026-01-05"]["avg_dollar_vol_30d"] = 300_000_000.0
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=None,
min_abs_gap_pct=0.01,
max_gap_pct=0.04,
max_candidates=10,
market_thrust_liquid_continuation_enabled=True,
market_thrust_liquid_continuation_min_gap_pct=0.02,
market_thrust_liquid_continuation_max_gap_pct=0.04,
market_thrust_liquid_continuation_min_first_bar_return_pct=0.007,
market_thrust_liquid_continuation_min_first_bar_dollar_vol=1_000_000,
market_thrust_liquid_continuation_min_avg_dollar_vol=100_000_000,
market_thrust_liquid_continuation_min_body_ratio=0.5,
market_thrust_liquid_continuation_min_close_location=0.8,
)
unscaled = compute_orb_candidates(
bars,
"2026-01-05",
params,
enrichment,
iex_live_mode=True,
)
scaled = compute_orb_candidates(
bars,
"2026-01-05",
params.model_copy(update={"iex_live_intraday_volume_multiplier": 30.0}),
enrichment,
iex_live_mode=True,
)
assert len(unscaled) == 1
assert unscaled[0]["market_thrust_liquid_continuation"] is False
assert len(scaled) == 1
assert scaled[0]["market_thrust_liquid_continuation"] is True
assert scaled[0]["first_bar_dollar_vol"] == 50_000.0
assert scaled[0]["filter_first_bar_dollar_vol"] == 1_500_000.0
def test_no_thrust_liquid_repair_can_rank_by_opening_impulse() -> None:
bars = {
"SCORE": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.0, "low": 100.0, "close": 100.0, "volume": 1_000_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.2, "low": 99.9, "close": 101.0, "volume": 20_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.1, "high": 101.5, "low": 100.8, "close": 101.2, "volume": 10_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.2, "high": 101.4, "low": 101.0, "close": 101.3, "volume": 10_000},
],
"IMPULSE": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.0, "low": 100.0, "close": 100.0, "volume": 10_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 103.8, "low": 99.9, "close": 103.5, "volume": 2_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 103.6, "high": 104.2, "low": 103.3, "close": 104.0, "volume": 2_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.0, "high": 104.5, "low": 103.8, "close": 104.4, "volume": 2_000},
],
}
enrichment = _enrichment_for("SCORE", "IMPULSE")
for ticker in enrichment:
enrichment[ticker]["2026-01-05"]["prev_close"] = 99.0
enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.02
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=1000.0,
min_abs_gap_pct=0.02,
max_gap_pct=0.04,
max_candidates=10,
min_candidates_to_trade=1,
min_candidate_breadth=0.0,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=1.0,
weight_momentum=0.0,
weight_obv_slope=0.0,
weight_event_catalyst=0.0,
weight_close_location=0.0,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
market_thrust_liquid_continuation_enabled=True,
market_thrust_liquid_continuation_require_market_thrust=False,
market_thrust_liquid_continuation_entry_mode="opening_burst",
market_thrust_liquid_continuation_no_thrust_min_gap_pct=0.0,
market_thrust_liquid_continuation_no_thrust_max_gap_pct=0.04,
market_thrust_liquid_continuation_no_thrust_min_first_bar_return_pct=0.005,
market_thrust_liquid_continuation_no_thrust_min_first_bar_dollar_vol=100_000,
market_thrust_liquid_continuation_no_thrust_min_avg_dollar_vol=100_000,
market_thrust_liquid_continuation_no_thrust_max_candidates=1,
market_thrust_liquid_continuation_no_thrust_max_trades=1,
market_thrust_liquid_continuation_no_thrust_rank_mode="opening_impulse",
market_thrust_liquid_continuation_no_thrust_size_scale=0.2,
)
result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0)
assert result.entry_diagnostics["market_thrust_opening_burst_candidates"] == 1
assert len(result.trades) == 1
assert result.trades[0].ticker == "IMPULSE"
assert result.trades[0].trigger_type == "market_thrust_opening_burst"
priority_params = params.model_copy(
update={
"market_thrust_liquid_continuation_no_thrust_rank_mode": None,
"min_candidate_breadth": 1.1,
"soft_day_fallback_on_breadth_skip": True,
"soft_day_combined_size_scale_floor": 1.0,
"soft_day_rank_before_time": True,
(
"market_thrust_liquid_continuation_no_thrust_"
"priority_min_first_bar_return_pct"
): 0.03,
}
)
priority_result = simulate_orb_day(
bars,
"2026-01-05",
priority_params,
enrichment,
equity=10_000.0,
)
assert len(priority_result.trades) == 1
assert priority_result.trades[0].ticker == "IMPULSE"
def test_nofill_vwap_reclaim_adds_trade_when_orb_never_breaks() -> None:
bars = {
"VWAP": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000},
]
}
enrichment = _enrichment_for("VWAP")
enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
nofill_vwap_reclaim_enabled=True,
nofill_vwap_reclaim_min_score_pct=0.0,
nofill_vwap_reclaim_size_scale=1.0,
vwap_reclaim_window_start_min=30,
vwap_reclaim_window_end_min=120,
vwap_reclaim_require_prior_dip=True,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert len(day_result.trades) == 1
assert day_result.trades[0].trigger_type == "vwap_reclaim"
assert day_result.trades[0].entry_time == "2026-01-05T10:00:00-05:00"
def test_reclaim_entry_attention_can_gate_weak_nofill_vwap_reclaim() -> None:
bars = {
"VWAP": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 1200},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000},
]
}
enrichment = _enrichment_for("VWAP")
enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0
base_params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
nofill_vwap_reclaim_enabled=True,
nofill_vwap_reclaim_min_score_pct=0.0,
nofill_vwap_reclaim_size_scale=1.0,
vwap_reclaim_window_start_min=30,
vwap_reclaim_window_end_min=120,
vwap_reclaim_require_prior_dip=True,
reclaim_entry_attention_enabled=True,
reclaim_entry_attention_trigger_types=["vwap_reclaim"],
)
pass_result = simulate_orb_day(
bars,
"2026-01-05",
base_params.model_copy(
update={"reclaim_entry_attention_min_entry_rel_volume": 1.0}
),
enrichment,
equity=10_000.0,
)
assert len(pass_result.trades) == 1
assert pass_result.trades[0].entry_rel_volume is not None
assert pass_result.trades[0].entry_rel_volume > 1.0
reject_result = simulate_orb_day(
bars,
"2026-01-05",
base_params.model_copy(
update={"reclaim_entry_attention_min_entry_rel_volume": 2.0}
),
enrichment,
equity=10_000.0,
)
assert reject_result.trades == []
assert (
reject_result.entry_diagnostics["entry_reject_stats"][
"reclaim_entry_attention_relvol"
]
== 1
)
def test_late_breakout_adds_trade_after_primary_timeout() -> None:
bars = {
"LATE": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.6, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.6, "high": 100.8, "low": 100.0, "close": 100.2, "volume": 800},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.2, "high": 100.7, "low": 99.9, "close": 100.5, "volume": 800},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.5, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 800},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.8, "high": 101.6, "low": 95.0, "close": 101.4, "volume": 3000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.4, "high": 102.0, "low": 101.1, "close": 101.8, "volume": 1000},
]
}
enrichment = _enrichment_for("LATE")
enrichment["LATE"]["2026-01-05"]["prev_close"] = 98.0
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
order_timeout_minutes=25,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=0.5,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
late_breakout_enabled=True,
late_breakout_min_score_pct=0.0,
late_breakout_size_scale=1.0,
late_breakout_window_start_min=30,
late_breakout_window_end_min=120,
late_breakout_confirm_rel_vol=1.2,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert day_result.entry_diagnostics["primary_timed_candidates"] == 0
assert day_result.entry_diagnostics["late_breakout_candidates"] == 1
assert len(day_result.trades) == 1
assert day_result.trades[0].trigger_type == "late_breakout"
assert day_result.trades[0].entry_time == "2026-01-05T10:00:00-05:00"
assert day_result.trades[0].exit_reason == "close"
def test_soft_day_vwap_reclaim_bypasses_market_regime_profile_block() -> None:
bars = {
"VWAP": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000},
]
}
enrichment = _enrichment_for("VWAP", "QQQ")
enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0
enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0
enrichment["QQQ"]["2026-01-05"]["today_open"] = 99.0
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
market_regime_spy_threshold=0.001,
market_regime_ticker="QQQ",
soft_day_fallback_on_regime_skip=True,
soft_day_regime_skip_size_scale=0.1,
soft_day_setup_profile="skip_day_reclaim_v1",
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
soft_day_vwap_reclaim_enabled=True,
soft_day_vwap_reclaim_min_score_pct=0.0,
soft_day_vwap_reclaim_min_premarket_dollar_vol=0.0,
soft_day_vwap_reclaim_size_scale=1.0,
vwap_reclaim_window_start_min=30,
vwap_reclaim_window_end_min=120,
vwap_reclaim_require_prior_dip=True,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert day_result.is_soft_day
assert day_result.soft_day_reason == "market_regime"
assert len(day_result.trades) == 1
assert day_result.trades[0].trigger_type == "soft_day_vwap_reclaim"
def test_soft_day_vwap_weak_participation_guard_rejects_low_rank_body() -> None:
bars = {
"ANCHOR": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 104.0, "low": 99.5, "close": 103.5, "volume": 20_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 103.5, "high": 104.0, "low": 103.0, "close": 103.7, "volume": 5_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.7, "high": 104.2, "low": 103.5, "close": 103.9, "volume": 5_000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 103.9, "high": 104.5, "low": 103.8, "close": 104.1, "volume": 5_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.1, "high": 104.5, "low": 103.9, "close": 104.2, "volume": 5_000},
],
"VWAP": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1_000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1_000},
],
}
enrichment = _enrichment_for("ANCHOR", "VWAP", "QQQ")
enrichment["ANCHOR"]["2026-01-05"]["prev_close"] = 98.0
enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0
enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0
enrichment["QQQ"]["2026-01-05"]["today_open"] = 99.0
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
market_regime_spy_threshold=0.001,
market_regime_ticker="QQQ",
soft_day_fallback_on_regime_skip=True,
soft_day_regime_skip_size_scale=0.1,
soft_day_setup_profile="skip_day_reclaim_v1",
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
soft_day_vwap_reclaim_enabled=True,
soft_day_vwap_reclaim_min_score_pct=0.0,
soft_day_vwap_reclaim_min_premarket_dollar_vol=0.0,
soft_day_vwap_reclaim_size_scale=1.0,
vwap_reclaim_window_start_min=30,
vwap_reclaim_window_end_min=120,
vwap_reclaim_require_prior_dip=True,
)
unguarded = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0)
guarded = simulate_orb_day(
bars,
"2026-01-05",
params.model_copy(
update={
"soft_day_vwap_reclaim_weak_participation_max_rvol_rank_pct": 0.65,
"soft_day_vwap_reclaim_weak_participation_max_body_ratio": 0.75,
}
),
enrichment,
equity=10_000.0,
)
assert len(unguarded.trades) == 1
assert unguarded.trades[0].ticker == "VWAP"
assert unguarded.trades[0].trigger_type == "soft_day_vwap_reclaim"
assert unguarded.trades[0].rvol_rank_pct == 0.0
assert len(guarded.trades) == 0
assert guarded.entry_diagnostics["entry_reject_stats"][
"soft_day_vwap_weak_participation"
] == 1
def test_soft_day_vwap_reclaim_can_be_limited_to_no_primary_trades() -> None:
bars = {
"ORB": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.0, "low": 101.6, "close": 101.9, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.9, "high": 102.1, "low": 101.7, "close": 102.0, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.6, "close": 101.9, "volume": 1000},
],
"VWAP": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000},
],
}
enrichment = _enrichment_for("ORB", "VWAP", "QQQ")
enrichment["ORB"]["2026-01-05"]["prev_close"] = 98.0
enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0
enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0
enrichment["QQQ"]["2026-01-05"]["today_open"] = 99.0
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
max_trades_per_day=10,
market_regime_spy_threshold=0.001,
market_regime_ticker="QQQ",
soft_day_fallback_on_regime_skip=True,
soft_day_regime_skip_size_scale=0.1,
soft_day_setup_profile="none",
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
soft_day_vwap_reclaim_enabled=True,
soft_day_vwap_reclaim_only_when_no_primary_trades=True,
soft_day_vwap_reclaim_min_score_pct=0.0,
soft_day_vwap_reclaim_min_premarket_dollar_vol=0.0,
soft_day_vwap_reclaim_size_scale=1.0,
vwap_reclaim_window_start_min=30,
vwap_reclaim_window_end_min=120,
vwap_reclaim_require_prior_dip=True,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert day_result.is_soft_day
assert day_result.entry_diagnostics["primary_timed_candidates"] == 1
assert day_result.entry_diagnostics["soft_day_vwap_candidates"] == 1
assert day_result.entry_diagnostics["timed_candidates"] == 2
assert day_result.entry_diagnostics["soft_day_vwap_trades"] == 0
assert day_result.entry_diagnostics["entry_reject_stats"] == {
"soft_day_vwap_primary_trade_exists": 1
}
assert len(day_result.trades) == 1
assert day_result.trades[0].trigger_type == "orb"
no_existing_params = params.model_copy(
update={
"soft_day_vwap_reclaim_only_when_no_primary_trades": False,
"soft_day_vwap_reclaim_only_when_no_existing_trades": True,
}
)
no_existing_result = simulate_orb_day(
bars,
"2026-01-05",
no_existing_params,
enrichment,
equity=10_000.0,
)
assert no_existing_result.entry_diagnostics["soft_day_vwap_trades"] == 0
assert no_existing_result.entry_diagnostics["entry_reject_stats"] == {
"soft_day_vwap_existing_trade": 1
}
def test_hard_breadth_soft_fallback_converts_breadth_skip_to_soft_day() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.2, "low": 101.6, "close": 102.0, "volume": 1000},
]
for ticker in ("AAA", "BBB")
}
enrichment = _enrichment_for("AAA", "BBB")
enrichment["AAA"]["2026-01-05"]["prev_close"] = 98.0
enrichment["AAA"]["2026-01-05"]["today_open"] = 100.0
enrichment["BBB"]["2026-01-05"]["prev_close"] = 102.0
enrichment["BBB"]["2026-01-05"]["today_open"] = 100.0
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
min_candidate_breadth=0.6,
breadth_skip_below=0.6,
hard_breadth_soft_fallback_enabled=True,
hard_breadth_soft_fallback_min_breadth=0.4,
hard_breadth_soft_fallback_size_scale=0.05,
soft_day_setup_profile="none",
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert day_result.skip_reason != "breadth"
assert day_result.is_soft_day
assert day_result.soft_day_reason == "hard_breadth"
assert day_result.breadth_scaler == 0.05
def test_synthetic_today_daily_breadth_uses_intraday_open() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 102.0, "low": 100.5, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 103.0, "low": 101.8, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.2, "low": 102.6, "close": 103.0, "volume": 1000},
]
for ticker in ("AAA", "BBB", "CCC")
}
enrichment = _enrichment_for("AAA", "BBB", "CCC")
for ticker in enrichment:
enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0
enrichment[ticker]["2026-01-05"]["today_open"] = 100.0
enrichment[ticker]["2026-01-05"]["synthetic_today_daily"] = True
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
min_candidate_breadth=0.6,
breadth_skip_below=0.25,
hard_breadth_soft_fallback_enabled=True,
hard_breadth_soft_fallback_min_breadth=0.0,
hard_breadth_soft_fallback_size_scale=0.05,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert day_result.skip_reason is None
assert day_result.soft_day_reason is None
assert day_result.breadth_scaler == 1.0
assert day_result.breadth_ratio == 1.0
assert day_result.breadth_positive_count == 3
assert day_result.breadth_total_count == 3
def test_market_thrust_override_clears_breadth_only_soft_day() -> None:
bars = {
"SPY": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.9, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.9, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 1000},
],
"QQQ": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 102.8, "high": 103.4, "low": 102.7, "close": 103.2, "volume": 1000},
],
"AAA": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 99.2, "low": 98.0, "close": 98.2, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 98.2, "high": 98.4, "low": 97.8, "close": 98.0, "volume": 1000},
],
"BBB": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 99.1, "low": 98.1, "close": 98.3, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 98.3, "high": 98.5, "low": 98.0, "close": 98.1, "volume": 1000},
],
"CCC": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 99.1, "low": 98.0, "close": 98.4, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 98.4, "high": 98.6, "low": 98.0, "close": 98.2, "volume": 1000},
],
}
enrichment = _enrichment_for("SPY", "QQQ", "AAA", "BBB", "CCC")
for ticker in ("SPY", "QQQ"):
enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0
enrichment[ticker]["2026-01-05"]["today_open"] = 101.0
for ticker in ("AAA", "BBB", "CCC"):
enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0
enrichment[ticker]["2026-01-05"]["today_open"] = 99.0
params = ORBStrategyParams(
engine_family="gainers_leader",
market_regime_ticker="QQQ",
market_regime_spy_threshold=0.001,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
min_candidate_breadth=0.6,
breadth_skip_below=0.25,
soft_day_fallback_on_breadth_skip=True,
soft_day_breadth_skip_size_scale=0.1,
market_thrust_breadth_override_enabled=True,
market_thrust_breadth_override_min_primary_close_location=0.85,
market_thrust_breadth_override_min_secondary_close_location=0.85,
market_thrust_breadth_override_min_primary_return_pct=0.001,
market_thrust_breadth_override_min_secondary_return_pct=0.001,
market_thrust_breadth_override_min_regime_gap_pct=0.001,
market_thrust_breadth_override_min_breadth_ratio=0.35,
market_thrust_breadth_override_size_scale_floor=1.0,
market_thrust_breadth_override_clear_soft_day=True,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert day_result.breadth_ratio == pytest.approx(0.4)
assert day_result.market_thrust_breadth_override_active
assert day_result.breadth_scaler == 1.0
assert day_result.is_soft_day is False
assert day_result.soft_day_reason is None
def test_max_trades_per_day_caps_later_orb_fills() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
for ticker in ("AAA", "BBB", "CCC")
}
enrichment = _enrichment_for("AAA", "BBB", "CCC")
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
max_trades_per_day=2,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert len(day_result.trades) == 2
def test_late_trade_size_scale_reduces_later_fills_without_skipping() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
for ticker in ("AAA", "BBB", "CCC")
}
enrichment = _enrichment_for("AAA", "BBB", "CCC")
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
late_trade_size_scale_after_n=2,
late_trade_size_scale=0.5,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert len(day_result.trades) == 3
assert day_result.trades[2].shares < day_result.trades[1].shares
assert day_result.trades[2].late_trade_size_scale == 0.5
def test_rank_rvol_pressure_size_scale_reduces_mid_rank_high_rvol_fills() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
for ticker in ("AAA", "BBB", "CCC")
}
enrichment = _enrichment_for("AAA", "BBB", "CCC")
for ticker in enrichment:
enrichment[ticker]["2026-01-05"]["ret_5d"] = -0.01
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
rank_rvol_pressure_min_rvol=0.1,
rank_rvol_pressure_max_score_rank_pct=0.5,
rank_rvol_pressure_max_ret_5d=0.0,
rank_rvol_pressure_size_scale=0.4,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert len(day_result.trades) == 3
assert day_result.trades[0].rank_rvol_pressure_size_scale is None
assert day_result.trades[1].rank_rvol_pressure_size_scale == 0.4
assert day_result.trades[2].rank_rvol_pressure_size_scale == 0.4
assert day_result.trades[1].shares < day_result.trades[0].shares
def test_mid_liquidity_fragility_scales_only_fragile_mid_premarket_profiles() -> None:
def bars_for(premarket_dollar_vol: float) -> list[dict]:
premarket_volume = int(premarket_dollar_vol / 100.0)
return [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.0, "low": 100.0, "close": 100.0, "volume": premarket_volume},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
bars = {
"FRAGILE": bars_for(30_000_000),
"LEADER": bars_for(90_000_000),
}
enrichment = _enrichment_for("FRAGILE", "LEADER")
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.0,
min_abs_gap_pct=0.0,
max_candidates=10,
max_trades_per_day=2,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
mid_liquidity_fragility_size_scale=0.25,
mid_liquidity_fragility_mid_min_premarket_dollar_vol=20_000_000,
mid_liquidity_fragility_mid_max_premarket_dollar_vol=75_000_000,
mid_liquidity_fragility_mid_min_body_ratio=0.5,
mid_liquidity_fragility_mid_max_body_ratio=0.75,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["FRAGILE"].mid_liquidity_fragility_active is True
assert trades["FRAGILE"].mid_liquidity_fragility_size_scale == 0.25
assert trades["LEADER"].mid_liquidity_fragility_active is False
assert trades["LEADER"].mid_liquidity_fragility_size_scale is None
assert trades["FRAGILE"].shares < trades["LEADER"].shares
def test_gap_exhaustion_pressure_scales_stretched_weak_orb_close() -> None:
bars = {
"STRONG": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.9, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
],
"WEAK": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.2, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.2, "high": 102.0, "low": 100.1, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
],
}
enrichment = _enrichment_for("STRONG", "WEAK")
for ticker in enrichment:
enrichment[ticker]["2026-01-05"]["prev_close"] = 98.0
enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.05
enrichment[ticker]["2026-01-05"]["gap_zscore_20d"] = 1.25
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
gap_exhaustion_pressure_min_gap_zscore=1.0,
gap_exhaustion_pressure_max_close_location=0.65,
gap_exhaustion_pressure_min_ret_5d=0.0,
gap_exhaustion_pressure_min_gap_pct=0.02,
gap_exhaustion_pressure_size_scale=0.25,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["STRONG"].gap_exhaustion_pressure_size_scale is None
assert trades["WEAK"].gap_exhaustion_pressure_size_scale == 0.25
assert trades["WEAK"].shares < trades["STRONG"].shares
def test_stale_obv_rvol_pressure_scales_low_rvol_stale_obv_fill() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
for ticker in ("FRESH", "STALE")
}
enrichment = _enrichment_for("FRESH", "STALE")
enrichment["FRESH"]["2026-01-05"]["obv_slope_20"] = 0.2
enrichment["STALE"]["2026-01-05"]["obv_slope_20"] = -0.3
for ticker in enrichment:
enrichment[ticker]["2026-01-05"]["ret_5d"] = -0.01
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
stale_obv_rvol_pressure_max_rvol=8.0,
stale_obv_rvol_pressure_max_obv_slope_20d=-0.2,
stale_obv_rvol_pressure_max_ret_5d=0.05,
stale_obv_rvol_pressure_max_score_rank_pct=1.0,
stale_obv_rvol_pressure_size_scale=0.4,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["FRESH"].stale_obv_rvol_pressure_size_scale is None
assert trades["STALE"].stale_obv_rvol_pressure_size_scale == 0.4
assert trades["STALE"].shares < trades["FRESH"].shares
def test_red_to_green_acceleration_boosts_only_clean_downside_reclaims() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
for ticker in ("BASE", "BOOST", "RISK")
}
enrichment = _enrichment_for("BASE", "BOOST", "RISK")
enrichment["BASE"]["2026-01-05"]["prev_close"] = 98.0
enrichment["BOOST"]["2026-01-05"]["prev_close"] = 106.0
enrichment["BOOST"]["2026-01-05"]["ret_5d"] = 0.0
enrichment["RISK"]["2026-01-05"]["prev_close"] = 106.0
enrichment["RISK"]["2026-01-05"]["ret_5d"] = 0.5
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
hot_reclaim_min_abs_gap_pct=0.04,
hot_reclaim_min_ret_5d=0.30,
hot_reclaim_max_premarket_dollar_vol=10_000_000_000,
hot_reclaim_min_body_ratio=0.50,
hot_reclaim_min_close_location=0.80,
hot_reclaim_action="scale",
hot_reclaim_size_scale=0.35,
red_to_green_acceleration_min_abs_gap_pct=0.04,
red_to_green_acceleration_min_orb_return=0.005,
red_to_green_acceleration_size_scale=1.5,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["BASE"].red_to_green_acceleration_size_scale is None
assert trades["BOOST"].red_to_green_acceleration_size_scale == 1.5
assert trades["RISK"].hot_reclaim_size_scale == 0.35
assert trades["RISK"].red_to_green_acceleration_size_scale is None
assert trades["BOOST"].shares > trades["BASE"].shares
assert trades["RISK"].shares < trades["BASE"].shares
def test_liquid_leader_conviction_boosts_only_unscaled_liquid_leaders() -> None:
bars = {}
for ticker, premarket_volume in (
("THIN", 1_000),
("LIQUID", 1_000_000),
("RISK", 1_000_000),
):
bars[ticker] = [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.1, "low": 99.9, "close": 100.0, "volume": premarket_volume},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
enrichment = _enrichment_for("THIN", "LIQUID", "RISK")
enrichment["THIN"]["2026-01-05"]["prev_close"] = 98.0
enrichment["LIQUID"]["2026-01-05"]["prev_close"] = 98.0
enrichment["RISK"]["2026-01-05"]["prev_close"] = 106.0
enrichment["RISK"]["2026-01-05"]["ret_5d"] = 0.5
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
hot_reclaim_min_abs_gap_pct=0.04,
hot_reclaim_min_ret_5d=0.30,
hot_reclaim_max_premarket_dollar_vol=200_000_000,
hot_reclaim_min_body_ratio=0.50,
hot_reclaim_min_close_location=0.80,
hot_reclaim_action="scale",
hot_reclaim_size_scale=0.35,
liquid_leader_conviction_min_premarket_dollar_vol=50_000_000,
liquid_leader_conviction_size_scale=1.5,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["THIN"].liquid_leader_conviction_size_scale is None
assert trades["LIQUID"].liquid_leader_conviction_size_scale == 1.5
assert trades["RISK"].hot_reclaim_size_scale == 0.35
assert trades["RISK"].liquid_leader_conviction_size_scale is None
assert trades["LIQUID"].shares > trades["THIN"].shares
assert trades["RISK"].shares < trades["THIN"].shares
trigger_blocked = simulate_orb_day(
bars,
"2026-01-05",
params.model_copy(
update={"liquid_leader_conviction_allowed_trigger_types": ["vwap_reclaim"]}
),
enrichment,
equity=10_000.0,
)
trigger_blocked_trades = {trade.ticker: trade for trade in trigger_blocked.trades}
assert trigger_blocked_trades["LIQUID"].liquid_leader_conviction_size_scale is None
def test_liquid_leader_secondary_boost_can_require_sector_confirmation() -> None:
params = ORBStrategyParams(
liquid_leader_conviction_size_scale=2.0,
liquid_leader_conviction_secondary_requires_sector_confirmation=True,
liquid_leader_conviction_secondary_min_body_ratio=0.20,
)
cand = {"sector_confirmation_active": False, "body_ratio": 0.50}
confirmed = {"sector_confirmation_active": True, "body_ratio": 0.50}
weak_body = {"sector_confirmation_active": True, "body_ratio": 0.10}
assert (
_orb_liquid_leader_conviction_size_scale(
params,
cand,
score_rank_pct=1.0,
trigger_type="soft_day_vwap_reclaim",
direction_str="long",
risk_size_scales=(1.0,),
)
== 1.0
)
assert (
_orb_liquid_leader_conviction_size_scale(
params,
confirmed,
score_rank_pct=1.0,
trigger_type="soft_day_vwap_reclaim",
direction_str="long",
risk_size_scales=(1.0,),
)
== 2.0
)
assert (
_orb_liquid_leader_conviction_size_scale(
params,
weak_body,
score_rank_pct=1.0,
trigger_type="soft_day_vwap_reclaim",
direction_str="long",
risk_size_scales=(1.0,),
)
== 1.0
)
assert (
_orb_liquid_leader_conviction_size_scale(
params,
cand,
score_rank_pct=1.0,
trigger_type="orb",
direction_str="long",
risk_size_scales=(1.0,),
)
== 2.0
)
def test_unsupported_attention_governor_scales_unconfirmed_liquid_laggards() -> None:
params = ORBStrategyParams(
unsupported_attention_size_scale=0.35,
unsupported_attention_allowed_trigger_types=["orb"],
unsupported_attention_liquid_min_premarket_dollar_vol=50_000_000,
unsupported_attention_liquid_max_candidate_score=0.65,
)
weak = {
"premarket_dollar_vol": 75_000_000.0,
"score": 0.60,
"sector_confirmation_active": False,
}
confirmed = {**weak, "sector_confirmation_active": True}
strong_score = {**weak, "score": 0.80}
assert (
_orb_unsupported_attention_size_scale(
params,
weak,
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0,),
)
== 0.35
)
assert (
_orb_unsupported_attention_size_scale(
params,
confirmed,
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0,),
)
== 1.0
)
assert (
_orb_unsupported_attention_size_scale(
params,
strong_score,
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0,),
)
== 1.0
)
def test_unsupported_attention_governor_scales_thin_positive_gap_spikes() -> None:
params = ORBStrategyParams(
unsupported_attention_size_scale=0.25,
unsupported_attention_thin_max_premarket_dollar_vol=10_000_000,
unsupported_attention_thin_min_gap_pct=0.025,
unsupported_attention_thin_min_rvol=20.0,
unsupported_attention_thin_max_rvol=25.0,
)
thin_spike = {
"premarket_dollar_vol": 8_000_000.0,
"gap_pct": 0.03,
"rvol": 22.0,
}
downside_reclaim = {**thin_spike, "gap_pct": -0.04}
high_conviction = {**thin_spike}
assert (
_orb_unsupported_attention_size_scale(
params,
thin_spike,
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0,),
)
== 0.25
)
assert (
_orb_unsupported_attention_size_scale(
params,
downside_reclaim,
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0,),
)
== 1.0
)
assert (
_orb_unsupported_attention_size_scale(
params,
high_conviction,
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.5,),
)
== 1.0
)
def test_thin_gap_up_loss_cap_marks_only_low_premarket_gap_ups() -> None:
bars = {}
for ticker, premarket_volume in (
("THIN", 1_000),
("LIQUID", 2_000_000),
):
bars[ticker] = [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.1, "low": 99.9, "close": 100.0, "volume": premarket_volume},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000},
]
enrichment = _enrichment_for("THIN", "LIQUID")
for ticker in ("THIN", "LIQUID"):
enrichment[ticker]["2026-01-05"]["prev_close"] = 98.0
enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.03
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
thin_gap_up_loss_cap_min_gap_pct=0.02,
thin_gap_up_loss_cap_min_ret_5d=-0.10,
thin_gap_up_loss_cap_max_ret_5d=0.10,
thin_gap_up_loss_cap_max_premarket_dollar_vol=10_000_000,
thin_gap_up_loss_cap_min_body_ratio=0.50,
thin_gap_up_loss_cap_min_close_location=0.80,
thin_gap_up_loss_cap_pct=0.02,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["THIN"].thin_gap_up_loss_cap_active
assert not trades["LIQUID"].thin_gap_up_loss_cap_active
def test_moderate_downside_loss_cap_marks_only_moderate_weak_reclaims() -> None:
bars = {}
for ticker in ("MOD", "DEEP", "STRONG"):
bars[ticker] = [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.1, "low": 99.9, "close": 100.0, "volume": 1000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.2, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 101.8, "low": 100.8, "close": 101.4, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.4, "high": 102.0, "low": 101.0, "close": 101.6, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 102.1, "low": 101.4, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.7, "volume": 1000},
]
enrichment = _enrichment_for("MOD", "DEEP", "STRONG")
enrichment["MOD"]["2026-01-05"]["prev_close"] = 106.0
enrichment["MOD"]["2026-01-05"]["ret_5d"] = 0.10
enrichment["DEEP"]["2026-01-05"]["prev_close"] = 116.0
enrichment["DEEP"]["2026-01-05"]["ret_5d"] = 0.10
enrichment["STRONG"]["2026-01-05"]["prev_close"] = 106.0
enrichment["STRONG"]["2026-01-05"]["ret_5d"] = 0.35
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
moderate_downside_loss_cap_min_abs_gap_pct=0.05,
moderate_downside_loss_cap_max_abs_gap_pct=0.10,
moderate_downside_loss_cap_min_ret_5d=0.0,
moderate_downside_loss_cap_max_ret_5d=0.25,
moderate_downside_loss_cap_max_premarket_dollar_vol=10_000_000,
moderate_downside_loss_cap_max_body_ratio=0.30,
moderate_downside_loss_cap_max_close_location=0.75,
moderate_downside_loss_cap_pct=0.01,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["MOD"].moderate_downside_loss_cap_active
assert not trades["DEEP"].moderate_downside_loss_cap_active
assert not trades["STRONG"].moderate_downside_loss_cap_active
def test_stale_obv_reversal_extra_gates_target_positive_gap_weak_open() -> None:
bars = {}
for ticker in ("WEAK", "NEG_GAP", "HIGH_RVOL", "STRONG_BODY"):
first_volume = 500_000 if ticker == "HIGH_RVOL" else 1_000
first_close = 100.8 if ticker == "STRONG_BODY" else 98.9
bars[ticker] = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 101.0, "low": 98.0, "close": first_close, "volume": first_volume},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": first_close, "high": 101.6, "low": first_close - 0.2, "close": 101.2, "volume": 1_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 102.0, "low": 101.0, "close": 101.6, "volume": 1_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 101.8, "low": 101.2, "close": 101.7, "volume": 1_000},
]
enrichment = _enrichment_for("WEAK", "NEG_GAP", "HIGH_RVOL", "STRONG_BODY")
for ticker in enrichment:
enrichment[ticker]["2026-01-05"]["prev_close"] = 96.0
enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.10
enrichment[ticker]["2026-01-05"]["obv_slope_20"] = 0.20
enrichment["NEG_GAP"]["2026-01-05"]["prev_close"] = 103.0
params = ORBStrategyParams(
engine_family="gainers_leader",
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.0,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_candidates=10,
min_candidates_to_trade=1,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
risk_per_trade_pct=0.001,
max_position_pct=0.25,
slippage_bps=0.0,
stale_obv_reversal_max_obv_slope_20d=0.35,
stale_obv_reversal_min_ret_5d=0.0,
stale_obv_reversal_max_ret_5d=0.35,
stale_obv_reversal_min_gap_pct=0.02,
stale_obv_reversal_max_rvol=15.0,
stale_obv_reversal_max_body_ratio=0.20,
stale_obv_reversal_max_close_location=0.40,
stale_obv_reversal_max_orb_return=0.0,
stale_obv_reversal_action="confirm_scale",
stale_obv_reversal_size_scale=0.35,
stale_obv_reversal_loss_cap_pct=0.015,
)
day_result = simulate_orb_day(
bars,
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
trades = {trade.ticker: trade for trade in day_result.trades}
assert trades["WEAK"].stale_obv_reversal_requires_confirmation
assert trades["WEAK"].stale_obv_reversal_size_scale == 0.35
assert not trades["NEG_GAP"].stale_obv_reversal_requires_confirmation
assert not trades["HIGH_RVOL"].stale_obv_reversal_requires_confirmation
assert not trades["STRONG_BODY"].stale_obv_reversal_requires_confirmation
def test_fixed_loss_can_preserve_trailing_stop_management() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 100.0, "low": 98.5, "close": 99.5, "volume": 1_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 101.0, "volume": 2_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.2, "low": 101.0, "close": 101.2, "volume": 2_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.0, "high": 104.2, "low": 103.8, "close": 104.0, "volume": 1_000},
]
base_params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=30,
atr_stop_multiplier=0.1,
breakeven_at_r=99.0,
trailing_at_r=1.0,
trailing_stop_atr_multiplier=0.15,
trailing_tighten_at_r=None,
fixed_loss_pct=0.05,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
)
legacy_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=10.0,
rvol=2.0,
gap_pct=0.03,
params=base_params,
equity=10_000.0,
date_str="2026-01-05",
ticker="LEGACY",
)
preserve_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=10.0,
rvol=2.0,
gap_pct=0.03,
params=base_params.model_copy(update={"fixed_loss_preserve_trailing": True}),
equity=10_000.0,
date_str="2026-01-05",
ticker="PRESERVE",
)
assert legacy_trade is not None
assert preserve_trade is not None
assert legacy_trade.exit_reason == "close"
assert preserve_trade.exit_reason == "trailing_stop"
assert preserve_trade.exit_price == pytest.approx(101.5)
def test_conviction_runner_trail_only_delays_qualified_orb_tightening() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 101.2, "low": 101.0, "close": 101.1, "volume": 2_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.6, "high": 102.7, "low": 102.3, "close": 102.6, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.4, "high": 102.6, "low": 102.4, "close": 102.5, "volume": 2_000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.0, "high": 104.1, "low": 103.9, "close": 104.0, "volume": 1_000},
]
base_params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=30,
atr_stop_multiplier=1.0,
breakeven_at_r=1.0,
trailing_at_r=1.0,
trailing_stop_atr_multiplier=0.6,
trailing_tighten_at_r=1.5,
trailing_stop_atr_multiplier_tight=0.2,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
slippage_bps=0.0,
)
runner_params = base_params.model_copy(
update={
"conviction_runner_trail_allowed_trigger_types": ["orb"],
"conviction_runner_trail_tighten_at_r": 2.0,
"conviction_runner_trail_gap_atr_multiplier": 0.8,
"conviction_runner_trail_min_abs_gap_pct": 0.035,
"conviction_runner_trail_min_candidate_score": 0.9,
"conviction_runner_trail_min_score_rank_pct": 0.9,
}
)
base_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.04,
params=base_params,
equity=10_000.0,
date_str="2026-01-05",
ticker="BASE",
score_rank_pct=1.0,
candidate_score=0.95,
)
runner_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.04,
params=runner_params,
equity=10_000.0,
date_str="2026-01-05",
ticker="RUNNER",
score_rank_pct=1.0,
candidate_score=0.95,
)
blocked_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.04,
params=runner_params,
equity=10_000.0,
date_str="2026-01-05",
ticker="BLOCKED",
score_rank_pct=1.0,
candidate_score=0.50,
)
assert base_trade is not None
assert runner_trade is not None
assert blocked_trade is not None
assert base_trade.exit_reason == "trailing_stop"
assert base_trade.exit_price == pytest.approx(102.4)
assert runner_trade.exit_reason == "close"
assert runner_trade.exit_price == pytest.approx(104.0)
assert blocked_trade.exit_reason == "trailing_stop"
assert blocked_trade.exit_price == pytest.approx(base_trade.exit_price)
def test_aggregated_bar_entry_fills_on_next_raw_bar_after_signal() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 103.0, "high": 103.2, "low": 102.8, "close": 103.0, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.0, "high": 103.0, "low": 102.9, "close": 102.95, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.95, "high": 103.1, "low": 102.9, "close": 103.0, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.0, "high": 103.1, "low": 102.8, "close": 102.9, "volume": 1000},
{"timestamp": "2026-01-05T09:55:00-05:00", "open": 102.9, "high": 103.0, "low": 102.7, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 102.8, "high": 103.3, "low": 102.7, "close": 103.1, "volume": 1000},
{"timestamp": "2026-01-05T10:05:00-05:00", "open": 104.5, "high": 104.8, "low": 104.4, "close": 104.7, "volume": 1000},
{"timestamp": "2026-01-05T10:10:00-05:00", "open": 104.7, "high": 104.9, "low": 104.6, "close": 104.8, "volume": 1000},
]
params = ORBStrategyParams(
sim_bar_minutes=30,
orb_minutes=5,
order_timeout_minutes=45,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
)
trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=2.0,
gap_pct=0.01,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="TEST",
)
assert trade is not None
assert trade.entry_time == "2026-01-05T10:05:00-05:00"
assert trade.entry_price == 104.5
def test_same_bar_stop_confirmation_delays_ambiguous_entry_bar_stop() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.4, "close": 101.1, "volume": 1500},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 102.0, "low": 101.0, "close": 101.8, "volume": 1600},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.1, "high": 101.4, "low": 100.9, "close": 101.2, "volume": 1000},
]
base_params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=30,
atr_stop_multiplier=0.5,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
)
default_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=base_params,
equity=10_000.0,
date_str="2026-01-05",
ticker="STOP",
)
delayed_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=base_params.model_copy(
update={
"same_bar_stop_confirmation_enabled": True,
"same_bar_stop_confirmation_allowed_trigger_types": ["orb"],
}
),
equity=10_000.0,
date_str="2026-01-05",
ticker="DELAY",
)
blocked_by_trigger = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=base_params.model_copy(
update={
"same_bar_stop_confirmation_enabled": True,
"same_bar_stop_confirmation_allowed_trigger_types": ["vwap_reclaim"],
}
),
equity=10_000.0,
date_str="2026-01-05",
ticker="ALLOWLIST",
)
assert default_trade is not None
assert default_trade.exit_reason == "stop_loss"
assert default_trade.entry_time == default_trade.exit_time
assert default_trade.total_capital_deployed > 0
assert delayed_trade is not None
assert delayed_trade.entry_time == "2026-01-05T09:40:00-05:00"
assert delayed_trade.entry_price == 101.8
assert blocked_by_trigger is not None
assert blocked_by_trigger.exit_reason == "stop_loss"
assert blocked_by_trigger.total_capital_deployed > 0
def test_early_failure_exit_cuts_failed_breakout() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 101.1, "low": 100.6, "close": 100.8, "volume": 1500},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.0, "high": 102.2, "low": 101.8, "close": 102.0, "volume": 1000},
]
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=30,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
early_failure_exit_minutes=15,
early_failure_exit_level="breakout",
early_failure_exit_max_peak_r=0.50,
)
trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="FAIL",
)
assert trade is not None
assert trade.exit_reason == "early_failure"
assert trade.exit_time == "2026-01-05T09:40:00-05:00"
assert trade.exit_price == 100.8
def test_early_failure_exit_allows_trade_after_positive_close_excursion() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 102.2, "low": 100.9, "close": 102.0, "volume": 1500},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 100.5, "close": 100.8, "volume": 1500},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.0, "high": 102.2, "low": 101.8, "close": 102.0, "volume": 1000},
]
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=30,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
early_failure_exit_minutes=15,
early_failure_exit_level="breakout",
early_failure_exit_max_peak_r=0.50,
)
trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="HOLD",
)
assert trade is not None
assert trade.exit_reason == "close"
assert trade.exit_price == 102.0
def test_pyramid_quality_gates_control_add_on() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 102.4, "low": 101.0, "close": 102.2, "volume": 1500},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 103.8, "high": 104.1, "low": 103.7, "close": 104.0, "volume": 1000},
]
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=30,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
risk_per_trade_pct=0.02,
max_position_pct=0.50,
pyramid_at_r=1.0,
pyramid_add_pct=0.50,
pyramid_max_adds=1,
pyramid_allowed_trigger_types=["orb"],
pyramid_min_score_rank_pct=0.80,
pyramid_min_rvol=6.0,
)
allowed_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="ALLOW",
score_rank_pct=0.90,
)
low_rank_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="LOWRANK",
score_rank_pct=0.70,
)
low_rvol_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=5.0,
gap_pct=0.03,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="LOWRVOL",
score_rank_pct=0.90,
)
blocked_trigger_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=params.model_copy(update={"pyramid_allowed_trigger_types": ["vwap_reclaim"]}),
equity=10_000.0,
date_str="2026-01-05",
ticker="BLOCKED",
score_rank_pct=0.90,
)
blocked_context_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="CTXBLOCK",
score_rank_pct=0.90,
pyramid_allowed=False,
)
assert allowed_trade is not None
assert low_rank_trade is not None
assert low_rvol_trade is not None
assert blocked_trigger_trade is not None
assert blocked_context_trade is not None
assert allowed_trade.pyramid_adds == 1
assert allowed_trade.pyramid_pnl > 0
assert low_rank_trade.pyramid_adds == 0
assert low_rvol_trade.pyramid_adds == 0
assert blocked_trigger_trade.pyramid_adds == 0
assert blocked_context_trade.pyramid_adds == 0
def test_pyramid_add_on_respects_available_cash() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 102.4, "low": 101.0, "close": 102.2, "volume": 1500},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 103.8, "high": 104.1, "low": 103.7, "close": 104.0, "volume": 1000},
]
base_params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=30,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
risk_per_trade_pct=0.02,
pyramid_at_r=1.0,
pyramid_add_pct=0.50,
pyramid_max_adds=1,
)
capped_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=base_params.model_copy(update={"max_position_pct": 0.50}),
equity=10_000.0,
date_str="2026-01-05",
ticker="CAPPED",
available_cash=6_000.0,
score_rank_pct=0.90,
)
no_room_trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=8.0,
gap_pct=0.03,
params=base_params.model_copy(update={"max_position_pct": 1.00}),
equity=10_000.0,
date_str="2026-01-05",
ticker="NOROOM",
available_cash=6_000.0,
score_rank_pct=0.90,
)
assert capped_trade is not None
assert no_room_trade is not None
assert capped_trade.pyramid_adds == 1
assert capped_trade.total_capital_deployed <= 6_000.0
assert no_room_trade.pyramid_adds == 0
assert no_room_trade.total_capital_deployed <= 6_000.0
def test_entry_market_guard_uses_entry_bar_open_without_close_lookahead() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.8, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.6, "high": 100.7, "low": 99.5, "close": 99.6, "volume": 12_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 99.4, "high": 99.6, "low": 99.0, "close": 99.2, "volume": 12_000},
]
entry_ts = _parse_test_ts("2026-01-05T09:35:00-05:00")
market_return = _market_return_at_entry_open(bars, entry_ts, "2026-01-05")
assert market_return == pytest.approx(0.006)
def test_entry_market_guard_scales_when_market_is_below_threshold() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 100.2, "low": 99.7, "close": 99.8, "volume": 10_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.7, "high": 99.8, "low": 99.0, "close": 99.1, "volume": 12_000},
]
params = ORBStrategyParams(
entry_market_guard_enabled=True,
entry_market_guard_min_return_pct=-0.001,
entry_market_guard_size_scale=0.4,
)
active, market_return, size_scale = _entry_market_guard_scale(
params,
bars,
_parse_test_ts("2026-01-05T09:35:00-05:00"),
"2026-01-05",
is_soft_day=False,
)
assert active is True
assert market_return == pytest.approx(-0.003)
assert size_scale == pytest.approx(0.4)
def test_daily_loss_limit_counts_only_losses_realized_before_next_breakout() -> None:
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=45,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=1.0,
max_position_pct=1.0,
daily_max_loss_pct=0.005,
max_stops_per_day=99,
)
early_close_loser = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.5, "close": 101.1, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.7, "high": 100.8, "low": 100.5, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.6, "high": 100.7, "low": 100.4, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.6, "high": 100.7, "low": 100.4, "close": 100.5, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.5, "high": 100.6, "low": 100.4, "close": 100.4, "volume": 1000},
]
late_winner = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 100.9, "low": 100.5, "close": 100.7, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.7, "high": 100.8, "low": 100.5, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.6, "high": 100.7, "low": 100.4, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.6, "high": 101.3, "low": 100.5, "close": 101.2, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.2, "high": 102.0, "low": 101.0, "close": 101.8, "volume": 1000},
]
day_result = simulate_orb_day(
{
"EARLY_CLOSE_LOSER": early_close_loser,
"LATE_WINNER": late_winner,
},
"2026-01-05",
params,
_enrichment_for("EARLY_CLOSE_LOSER", "LATE_WINNER"),
equity=10_000.0,
)
assert [trade.ticker for trade in day_result.trades] == ["EARLY_CLOSE_LOSER", "LATE_WINNER"]
assert day_result.trades[0].exit_time == "2026-01-05T15:55:00-05:00"
assert day_result.trades[1].entry_time == "2026-01-05T10:00:00-05:00"
def test_crowded_gap_reject_filters_extended_positive_gap_candidates() -> None:
bars = {
"CROWDED": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000},
],
"FRESH": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
],
}
enrichment = _enrichment_for("CROWDED", "FRESH")
enrichment["CROWDED"]["2026-01-05"]["prev_close"] = 97.0
enrichment["CROWDED"]["2026-01-05"]["ret_5d"] = 0.30
enrichment["FRESH"]["2026-01-05"]["prev_close"] = 97.0
enrichment["FRESH"]["2026-01-05"]["ret_5d"] = 0.05
stats: dict[str, int] = {}
params = ORBStrategyParams(
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
max_candidates=10,
crowded_gap_reject_min_gap_pct=0.02,
crowded_gap_reject_min_ret_5d=0.20,
crowded_gap_reject_min_body_ratio=0.60,
crowded_gap_reject_min_close_location=0.65,
)
candidates = compute_orb_candidates(
bars, "2026-01-05", params, enrichment, _stats_out=stats
)
assert [cand["ticker"] for cand in candidates] == ["FRESH"]
assert stats["crowded_gap"] == 1
def test_crowded_gap_confirm_keeps_candidate_with_confirmation_flag() -> None:
bars = {
"CROWDED": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000},
],
}
enrichment = _enrichment_for("CROWDED")
enrichment["CROWDED"]["2026-01-05"]["prev_close"] = 97.0
enrichment["CROWDED"]["2026-01-05"]["ret_5d"] = 0.30
stats: dict[str, int] = {}
params = ORBStrategyParams(
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
max_candidates=10,
crowded_gap_reject_min_gap_pct=0.02,
crowded_gap_reject_min_ret_5d=0.20,
crowded_gap_reject_min_body_ratio=0.60,
crowded_gap_reject_min_close_location=0.65,
crowded_gap_action="confirm",
)
candidates = compute_orb_candidates(
bars, "2026-01-05", params, enrichment, _stats_out=stats
)
assert [cand["ticker"] for cand in candidates] == ["CROWDED"]
assert candidates[0]["crowded_gap_requires_confirmation"] is True
assert stats["crowded_gap"] == 1
def test_crowded_gap_scale_keeps_candidate_with_size_scale() -> None:
bars = {
"CROWDED": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000},
],
}
enrichment = _enrichment_for("CROWDED")
enrichment["CROWDED"]["2026-01-05"]["prev_close"] = 97.0
enrichment["CROWDED"]["2026-01-05"]["ret_5d"] = 0.30
stats: dict[str, int] = {}
params = ORBStrategyParams(
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
max_candidates=10,
crowded_gap_reject_min_gap_pct=0.02,
crowded_gap_reject_min_ret_5d=0.20,
crowded_gap_reject_min_body_ratio=0.60,
crowded_gap_reject_min_close_location=0.65,
crowded_gap_action="scale",
crowded_gap_size_scale=0.4,
)
candidates = compute_orb_candidates(
bars, "2026-01-05", params, enrichment, _stats_out=stats
)
assert [cand["ticker"] for cand in candidates] == ["CROWDED"]
assert candidates[0]["crowded_gap_requires_confirmation"] is False
assert candidates[0]["crowded_gap_size_scale"] == 0.4
assert stats["crowded_gap"] == 1
def test_countertrend_gap_scale_keeps_candidate_with_size_scale() -> None:
bars = {
"BOUNCE": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.5, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.5, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000},
],
}
enrichment = _enrichment_for("BOUNCE")
enrichment["BOUNCE"]["2026-01-05"]["prev_close"] = 97.0
enrichment["BOUNCE"]["2026-01-05"]["ret_5d"] = -0.08
stats: dict[str, int] = {}
params = ORBStrategyParams(
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
max_candidates=10,
countertrend_gap_min_gap_pct=0.02,
countertrend_gap_max_ret_5d=0.0,
countertrend_gap_min_body_ratio=0.60,
countertrend_gap_min_close_location=0.65,
countertrend_gap_action="scale",
countertrend_gap_size_scale=0.45,
)
candidates = compute_orb_candidates(
bars, "2026-01-05", params, enrichment, _stats_out=stats
)
assert [cand["ticker"] for cand in candidates] == ["BOUNCE"]
assert candidates[0]["countertrend_gap_requires_confirmation"] is False
assert candidates[0]["countertrend_gap_size_scale"] == 0.45
assert stats["countertrend_gap"] == 1
def test_red_to_green_gap_weight_prioritizes_down_gap_reclaim_candidates() -> None:
bars = {
ticker: [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
]
for ticker in ("DOWN_RECLAIM", "UP_GAP")
}
enrichment = _enrichment_for("DOWN_RECLAIM", "UP_GAP")
enrichment["DOWN_RECLAIM"]["2026-01-05"]["prev_close"] = 103.0
enrichment["UP_GAP"]["2026-01-05"]["prev_close"] = 97.0
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=0.0,
weight_red_to_green_gap=1.0,
max_candidates=10,
)
candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment)
assert [cand["ticker"] for cand in candidates] == ["DOWN_RECLAIM", "UP_GAP"]
assert candidates[0]["score"] > candidates[1]["score"]
def test_red_to_green_reserved_slot_replaces_lowest_normal_candidate() -> None:
bars = {
"UP_HIGH": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 3000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
],
"UP_SECOND": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
],
"DOWN_RECLAIM": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000},
],
}
enrichment = _enrichment_for("UP_HIGH", "UP_SECOND", "DOWN_RECLAIM")
enrichment["UP_HIGH"]["2026-01-05"]["prev_close"] = 97.0
enrichment["UP_SECOND"]["2026-01-05"]["prev_close"] = 97.0
enrichment["DOWN_RECLAIM"]["2026-01-05"]["prev_close"] = 103.0
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=1.0,
weight_premarket_dollar_vol=0.0,
max_candidates=2,
red_to_green_reserved_slots=1,
red_to_green_min_abs_gap_pct=0.02,
red_to_green_min_close_location=0.60,
)
candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment)
assert [cand["ticker"] for cand in candidates] == ["UP_HIGH", "DOWN_RECLAIM"]
assert candidates[1]["red_to_green_reserved"] is True
def test_red_to_green_reserved_slot_can_require_opening_acceleration() -> None:
bars = {
"UP_HIGH": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 3000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000},
],
"DOWN_WEAK_RECLAIM": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000},
],
"DOWN_ACCEL": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 112.0, "low": 99.0, "close": 111.0, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 111.0, "high": 113.0, "low": 110.8, "close": 112.5, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 112.5, "high": 113.0, "low": 112.0, "close": 112.8, "volume": 1000},
],
}
enrichment = _enrichment_for("UP_HIGH", "DOWN_WEAK_RECLAIM", "DOWN_ACCEL")
enrichment["UP_HIGH"]["2026-01-05"]["prev_close"] = 97.0
enrichment["DOWN_WEAK_RECLAIM"]["2026-01-05"]["prev_close"] = 103.0
enrichment["DOWN_ACCEL"]["2026-01-05"]["prev_close"] = 113.0
params = ORBStrategyParams(
engine_family="gainers_leader",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=1.0,
weight_premarket_dollar_vol=0.0,
max_candidates=2,
red_to_green_reserved_slots=1,
red_to_green_min_abs_gap_pct=0.02,
red_to_green_min_close_location=0.60,
red_to_green_min_orb_return=0.08,
)
candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment)
assert [cand["ticker"] for cand in candidates] == ["UP_HIGH", "DOWN_ACCEL"]
assert candidates[1]["red_to_green_reserved"] is True
def test_daily_loss_limit_is_not_tightened_by_day_size_scalers() -> None:
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=45,
atr_stop_multiplier=0.5,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.06,
max_position_pct=1.0,
daily_max_loss_pct=0.05,
max_stops_per_day=99,
market_regime_ticker="SPY",
regime_size_scale_low=0.0,
regime_size_scale_high=0.02,
regime_size_scale_min=0.5,
)
early_loser = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 101.0, "low": 100.2, "close": 100.3, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 1000},
]
late_winner = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.6, "high": 100.9, "low": 100.4, "close": 100.7, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.7, "high": 100.8, "low": 100.5, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.6, "high": 100.7, "low": 100.5, "close": 100.6, "volume": 1000},
{"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.6, "high": 101.3, "low": 100.7, "close": 101.2, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.2, "high": 102.2, "low": 101.1, "close": 102.0, "volume": 1000},
]
enrichment = _enrichment_for("EARLY_LOSER", "LATE_WINNER")
enrichment["SPY"] = {
"2026-01-05": {
"prev_close": 100.0,
"today_open": 100.0,
}
}
day_result = simulate_orb_day(
{
"EARLY_LOSER": early_loser,
"LATE_WINNER": late_winner,
},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert day_result.regime_scaler == pytest.approx(0.5)
assert [trade.ticker for trade in day_result.trades] == ["EARLY_LOSER", "LATE_WINNER"]
def test_sparse_day_scaler_reduces_orb_day_budget_without_skipping_trade() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 102.0, "low": 101.4, "close": 101.9, "volume": 1000},
]
baseline = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
)
scaled = baseline.model_copy(update={
"full_size_positions_threshold": 2,
"sparse_day_size_floor": 0.5,
})
enrichment = _enrichment_for("AAA")
base_day = simulate_orb_day(
{"AAA": bars},
"2026-01-05",
baseline,
enrichment,
equity=10_000.0,
)
scaled_day = simulate_orb_day(
{"AAA": bars},
"2026-01-05",
scaled,
enrichment,
equity=10_000.0,
)
assert len(base_day.trades) == 1
assert len(scaled_day.trades) == 1
assert scaled_day.sparse_day_scaler == 0.5
assert scaled_day.capital_deployed < base_day.capital_deployed
assert scaled_day.trades[0].shares < base_day.trades[0].shares
def test_chunked_orb_simulation_matches_single_run_statefully() -> None:
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.01,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
ticker_cooldown_days=1,
settlement_days=1,
compound_returns=False,
)
def bars(day: str) -> list[dict]:
return [
{"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000},
{"timestamp": f"{day}T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000},
{"timestamp": f"{day}T15:55:00-05:00", "open": 101.5, "high": 102.0, "low": 101.4, "close": 101.9, "volume": 1000},
]
trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"]
all_intraday = {
day: {"AAA": bars(day)}
for day in trading_days
}
enrichment = {
"AAA": {
day: {
"atr_14": 1.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"avg_daily_vol_14d": 10_000.0,
"prev_close": 99.0,
"today_open": 100.0,
}
for day in trading_days
}
}
single = run_orb_simulation(all_intraday, trading_days, params, enrichment)
chunk1, state = run_orb_simulation_with_state(
{day: all_intraday[day] for day in trading_days[:2]},
trading_days[:2],
params,
enrichment,
)
chunk2, _ = run_orb_simulation_with_state(
{trading_days[2]: all_intraday[trading_days[2]]},
trading_days[2:],
params,
enrichment,
state=state,
)
combined = chunk1 + chunk2
assert [len(day.trades) for day in combined] == [len(day.trades) for day in single]
assert [round(day.daily_pnl, 6) for day in combined] == [round(day.daily_pnl, 6) for day in single]
assert [trade.ticker for day in combined for trade in day.trades] == [
trade.ticker for day in single for trade in day.trades
]
def test_chunked_orb_simulation_preserves_streak_sizing_state() -> None:
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
settlement_days=0,
daily_budget_reset=True,
streak_sizing_win_bonus=1.0,
streak_sizing_max=3.0,
single_trade_loss_cap_pct=0.5,
)
def bars(day: str) -> list[dict]:
return [
{"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000},
{"timestamp": f"{day}T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000},
{"timestamp": f"{day}T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 1000},
]
trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"]
all_intraday = {day: {"AAA": bars(day)} for day in trading_days}
enrichment = {
"AAA": {
day: {
"atr_14": 1.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"avg_daily_vol_14d": 10_000.0,
"prev_close": 99.0,
"today_open": 100.0,
}
for day in trading_days
}
}
single = run_orb_simulation(all_intraday, trading_days, params, enrichment)
chunk1, state = run_orb_simulation_with_state(
{day: all_intraday[day] for day in trading_days[:2]},
trading_days[:2],
params,
enrichment,
)
chunk2, _ = run_orb_simulation_with_state(
{trading_days[2]: all_intraday[trading_days[2]]},
trading_days[2:],
params,
enrichment,
state=state,
)
combined = chunk1 + chunk2
assert round(combined[2].capital_deployed, 6) == round(single[2].capital_deployed, 6)
assert round(combined[2].daily_pnl, 6) == round(single[2].daily_pnl, 6)
def test_daily_budget_reset_caps_buying_power_to_initial_capital() -> None:
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=1.0,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
settlement_days=1,
daily_budget_reset=True,
initial_capital=10_000.0,
streak_sizing_win_bonus=1.0,
streak_sizing_max=3.0,
)
def bars(day: str) -> list[dict]:
return [
{"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 102.0, "low": 100.7, "close": 101.8, "volume": 1000},
{"timestamp": f"{day}T09:40:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 1000},
{"timestamp": f"{day}T09:45:00-05:00", "open": 102.2, "high": 103.0, "low": 102.1, "close": 102.8, "volume": 1000},
{"timestamp": f"{day}T15:55:00-05:00", "open": 104.0, "high": 105.0, "low": 103.8, "close": 104.8, "volume": 1000},
]
trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"]
all_intraday = {
"2026-01-05": {"AAA": bars("2026-01-05")},
"2026-01-06": {"AAA": bars("2026-01-06")},
"2026-01-07": {
"AAA": bars("2026-01-07"),
"BBB": bars("2026-01-07"),
},
}
enrichment = {
ticker: {
day: {
"atr_14": 1.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"avg_daily_vol_14d": 10_000.0,
"prev_close": 99.0,
"today_open": 100.0,
}
for day in trading_days
}
for ticker in ("AAA", "BBB")
}
results = run_orb_simulation(all_intraday, trading_days, params, enrichment)
assert all(day.available_cash_start == params.initial_capital for day in results)
assert results[2].trades
assert results[2].capital_deployed <= params.initial_capital
assert results[2].daily_return_pct == pytest.approx(
results[2].daily_pnl / params.initial_capital
)
def test_single_trade_loss_cap_equity_basis_scales_with_compound_equity() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.0, "volume": 1000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.4, "low": 101.9, "close": 102.2, "volume": 1000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.2, "high": 102.7, "low": 102.0, "close": 102.4, "volume": 1000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.0, "high": 103.0, "low": 101.9, "close": 102.8, "volume": 1000},
]
all_intraday = {"2026-01-05": {"AAA": bars}}
enrichment = _enrichment_for("AAA")
base_kwargs = dict(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
compound_returns=True,
daily_budget_reset=False,
settlement_days=0,
single_trade_loss_cap_pct=0.05,
)
grown_state = ORBSimulationState(equity=20_000.0, peak_equity=20_000.0)
initial_results, _ = run_orb_simulation_with_state(
all_intraday,
["2026-01-05"],
ORBStrategyParams(**base_kwargs, single_trade_loss_cap_basis="initial"),
enrichment,
state=grown_state,
)
equity_results, _ = run_orb_simulation_with_state(
all_intraday,
["2026-01-05"],
ORBStrategyParams(**base_kwargs, single_trade_loss_cap_basis="equity"),
enrichment,
state=grown_state,
)
assert equity_results[0].capital_deployed > initial_results[0].capital_deployed * 1.8
def test_chunked_orb_simulation_preserves_drawdown_governor_peak() -> None:
params = ORBStrategyParams(
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
settlement_days=0,
daily_budget_reset=True,
drawdown_governor_threshold=0.05,
drawdown_governor_min_scale=0.5,
)
def winner_bars(day: str) -> list[dict]:
return [
{"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000},
{"timestamp": f"{day}T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000},
{"timestamp": f"{day}T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 1000},
]
def loser_bars(day: str) -> list[dict]:
return [
{"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000},
{"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.0, "low": 99.8, "close": 100.0, "volume": 1000},
{"timestamp": f"{day}T09:40:00-05:00", "open": 100.0, "high": 100.1, "low": 98.8, "close": 99.0, "volume": 1000},
{"timestamp": f"{day}T15:55:00-05:00", "open": 99.0, "high": 99.2, "low": 98.9, "close": 99.1, "volume": 1000},
]
trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"]
all_intraday = {
"2026-01-05": {"AAA": winner_bars("2026-01-05")},
"2026-01-06": {"AAA": loser_bars("2026-01-06")},
"2026-01-07": {"AAA": winner_bars("2026-01-07")},
}
enrichment = {
"AAA": {
day: {
"atr_14": 1.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"avg_daily_vol_14d": 10_000.0,
"prev_close": 99.0,
"today_open": 100.0,
}
for day in trading_days
}
}
single = run_orb_simulation(all_intraday, trading_days, params, enrichment)
chunk1, state = run_orb_simulation_with_state(
{day: all_intraday[day] for day in trading_days[:2]},
trading_days[:2],
params,
enrichment,
)
chunk2, _ = run_orb_simulation_with_state(
{trading_days[2]: all_intraday[trading_days[2]]},
trading_days[2:],
params,
enrichment,
state=state,
)
combined = chunk1 + chunk2
assert round(combined[2].capital_deployed, 6) == round(single[2].capital_deployed, 6)
assert round(combined[2].daily_pnl, 6) == round(single[2].daily_pnl, 6)
def test_market_orb_quality_scaler_adjusts_day_sizing() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
market_orb_quality_ticker="SPY",
market_orb_quality_size_scale_low=0.4,
market_orb_quality_size_scale_high=0.6,
market_orb_quality_size_scale_min=0.8,
market_orb_quality_size_scale_max=1.2,
allow_doji_breakout=True,
)
aaa_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.9, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.9, "high": 101.4, "low": 100.8, "close": 101.3, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.3, "high": 101.5, "low": 101.1, "close": 101.4, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.4, "high": 101.6, "low": 101.2, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000},
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000},
]
weak_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.8, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
strong = simulate_orb_day(
{"AAA": aaa_bars, "SPY": strong_spy},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
weak = simulate_orb_day(
{"AAA": aaa_bars, "SPY": weak_spy},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert strong.market_orb_quality_scaler == pytest.approx(1.2)
assert weak.market_orb_quality_scaler == pytest.approx(0.8)
assert strong.market_orb_quality_close_location == pytest.approx((500.9 - 499.5) / (501.0 - 499.5))
assert weak.market_orb_quality_close_location == pytest.approx((499.8 - 499.5) / (501.0 - 499.5))
assert strong.capital_deployed > weak.capital_deployed
assert strong.daily_pnl > weak.daily_pnl
def test_conditional_confirmation_activates_only_on_red_market_open() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
conditional_confirmation_ticker="QQQ",
conditional_confirmation_below_return_pct=0.0,
)
aaa_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.2, "low": 100.7, "close": 100.9, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.95, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.95, "high": 101.0, "low": 100.85, "close": 100.9, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.9, "volume": 2000},
]
green_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000},
]
red_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.8, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
green = simulate_orb_day(
{"AAA": aaa_bars, "QQQ": green_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
red = simulate_orb_day(
{"AAA": aaa_bars, "QQQ": red_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert green.conditional_confirmation_active is False
assert len(green.trades) == 1
assert red.conditional_confirmation_active is True
assert red.trades == []
def test_market_orb_quality_divergence_guard_reduces_day_sizing() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_primary_strong_above=0.75,
market_orb_quality_secondary_weak_below=0.5,
market_orb_quality_divergence_scale=0.5,
)
aaa_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000},
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000},
]
weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 400.1, "volume": 10000},
]
strong_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
aligned = simulate_orb_day(
{"AAA": aaa_bars, "SPY": strong_spy, "QQQ": strong_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
diverged = simulate_orb_day(
{"AAA": aaa_bars, "SPY": strong_spy, "QQQ": weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert aligned.market_orb_quality_divergence_active is False
assert diverged.market_orb_quality_divergence_active is True
assert diverged.market_orb_quality_scaler == pytest.approx(0.5)
assert diverged.capital_deployed < aligned.capital_deployed
def test_market_orb_quality_veto_can_feed_rolling_loss_governor() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
rolling_loss_days=1,
rolling_loss_threshold=-0.01,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_primary_strong_above=0.75,
market_orb_quality_secondary_weak_below=0.50,
market_orb_quality_divergence_scale=0.0,
market_orb_quality_veto_rolling_loss_pct=-0.05,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.5, "low": 101.0, "close": 101.4, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.4, "high": 101.6, "low": 101.2, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.7, "high": 102.0, "low": 101.2, "close": 101.8, "volume": 2000},
]
day2_bars = [
{**bar, "timestamp": bar["timestamp"].replace("2026-01-05", "2026-01-06")}
for bar in bars
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000},
]
weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.2, "volume": 10000},
]
calm_spy_day2 = [
{"timestamp": "2026-01-06T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.6, "volume": 10000},
]
calm_qqq_day2 = [
{"timestamp": "2026-01-06T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.6, "volume": 10000},
]
enrichment = {
"AAA": {
day: {
"atr_14": 1.0,
"avg_dollar_vol_30d": 1_000_000_000.0,
"avg_daily_vol_14d": 10_000.0,
"prev_close": 99.0,
"today_open": 100.0,
}
for day in ("2026-01-05", "2026-01-06")
}
}
results = run_orb_simulation(
{
"2026-01-05": {"AAA": bars, "SPY": strong_spy, "QQQ": weak_qqq},
"2026-01-06": {"AAA": day2_bars, "SPY": calm_spy_day2, "QQQ": calm_qqq_day2},
},
["2026-01-05", "2026-01-06"],
params,
enrichment,
)
assert results[0].daily_pnl == 0.0
assert results[0].rolling_loss_synthetic_pnl == pytest.approx(-500.0)
assert results[1].skip_reason == "rolling_loss"
def test_market_orb_quality_divergence_guard_can_cap_total_trades() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_primary_strong_above=0.75,
market_orb_quality_secondary_weak_below=0.5,
market_orb_quality_divergence_max_trades=1,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 2000},
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000},
]
weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 400.1, "volume": 10000},
]
strong_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000},
]
enrichment = _enrichment_for("AAA", "BBB")
aligned = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": strong_spy, "QQQ": strong_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
diverged = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": strong_spy, "QQQ": weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert aligned.market_orb_quality_divergence_max_trades_active is False
assert len(aligned.trades) == 2
assert diverged.market_orb_quality_divergence_max_trades_active is True
assert len(diverged.trades) == 1
def test_market_orb_quality_primary_weak_secondary_strong_guard_can_veto_day() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_primary_weak_above=0.20,
market_orb_quality_primary_weak_below=0.30,
market_orb_quality_secondary_strong_above=0.60,
market_orb_quality_primary_weak_secondary_strong_scale=0.0,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000},
]
neutral_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 500.6, "volume": 10000},
]
weak_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 499.5, "volume": 10000},
]
strong_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.0, "close": 400.4, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
aligned = simulate_orb_day(
{"AAA": bars, "SPY": neutral_spy, "QQQ": strong_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
split_tape = simulate_orb_day(
{"AAA": bars, "SPY": weak_spy, "QQQ": strong_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert aligned.market_orb_quality_primary_weak_secondary_strong_active is False
assert len(aligned.trades) == 1
assert split_tape.market_orb_quality_primary_weak_secondary_strong_active is True
assert split_tape.market_orb_quality_scaler == pytest.approx(0.0)
assert split_tape.trades == []
def test_market_orb_quality_primary_lag_secondary_lead_guard_can_veto_day() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_primary_lag_above=0.40,
market_orb_quality_primary_lag_below=0.50,
market_orb_quality_secondary_lead_above=0.70,
market_orb_quality_secondary_lead_below=0.80,
market_orb_quality_primary_lag_secondary_lead_scale=0.0,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.5, "low": 101.0, "close": 101.4, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.4, "high": 101.6, "low": 101.2, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.7, "high": 102.0, "low": 101.2, "close": 101.8, "volume": 2000},
]
aligned_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000},
]
aligned_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000},
]
lag_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 499.9, "volume": 10000},
]
lead_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.0, "close": 400.5, "volume": 10000},
]
enrichment = _enrichment_for("AAA", "BBB")
aligned = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": aligned_spy, "QQQ": aligned_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
split_tape = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": lag_spy, "QQQ": lead_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert aligned.market_orb_quality_primary_lag_secondary_lead_active is False
assert len(aligned.trades) == 2
assert split_tape.market_orb_quality_primary_lag_secondary_lead_active is True
assert split_tape.market_orb_quality_scaler == pytest.approx(0.0)
assert split_tape.trades == []
def test_market_orb_quality_divergence_guard_can_target_mild_split_tape() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_primary_strong_above=0.75,
market_orb_quality_secondary_weak_above=0.4,
market_orb_quality_secondary_weak_below=0.5,
market_orb_quality_divergence_scale=0.5,
)
aaa_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000},
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000},
]
mild_weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.9, "low": 400.0, "close": 400.41, "volume": 10000},
]
deep_weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 400.1, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
mild = simulate_orb_day(
{"AAA": aaa_bars, "SPY": strong_spy, "QQQ": mild_weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
deep = simulate_orb_day(
{"AAA": aaa_bars, "SPY": strong_spy, "QQQ": deep_weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert mild.market_orb_quality_divergence_active is True
assert deep.market_orb_quality_divergence_active is False
def test_market_orb_quality_joint_weak_guard_can_require_confirmation() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_joint_weak_primary_below=0.2,
market_orb_quality_joint_weak_secondary_below=0.2,
market_orb_quality_joint_weak_require_confirmation=True,
)
aaa_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.2, "low": 100.7, "close": 100.9, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.95, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.95, "high": 101.0, "low": 100.85, "close": 100.9, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.9, "volume": 2000},
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000},
]
strong_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000},
]
weak_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.6, "volume": 10000},
]
weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 399.95, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
aligned = simulate_orb_day(
{"AAA": aaa_bars, "SPY": strong_spy, "QQQ": strong_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
joint_weak = simulate_orb_day(
{"AAA": aaa_bars, "SPY": weak_spy, "QQQ": weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert aligned.market_orb_quality_joint_weak_active is False
assert aligned.conditional_confirmation_active is False
assert len(aligned.trades) == 1
assert joint_weak.market_orb_quality_joint_weak_active is True
assert joint_weak.conditional_confirmation_active is True
assert joint_weak.trades == []
def test_market_orb_quality_joint_weak_guard_can_reduce_day_sizing() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_joint_weak_primary_below=0.2,
market_orb_quality_joint_weak_secondary_below=0.2,
market_orb_quality_joint_weak_scale=0.5,
)
aaa_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000},
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000},
]
strong_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000},
]
weak_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.6, "volume": 10000},
]
weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 399.95, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
aligned = simulate_orb_day(
{"AAA": aaa_bars, "SPY": strong_spy, "QQQ": strong_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
joint_weak = simulate_orb_day(
{"AAA": aaa_bars, "SPY": weak_spy, "QQQ": weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert aligned.market_orb_quality_joint_weak_active is False
assert joint_weak.market_orb_quality_joint_weak_active is True
assert joint_weak.market_orb_quality_scaler == pytest.approx(0.5)
assert joint_weak.capital_deployed < aligned.capital_deployed
def test_market_orb_quality_joint_weak_guard_can_cap_total_trades() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_joint_weak_primary_below=0.2,
market_orb_quality_joint_weak_secondary_below=0.2,
market_orb_quality_joint_weak_max_trades=1,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 2000},
]
strong_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000},
]
strong_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000},
]
weak_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.6, "volume": 10000},
]
weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 399.95, "volume": 10000},
]
enrichment = _enrichment_for("AAA", "BBB")
aligned = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": strong_spy, "QQQ": strong_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
joint_weak = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": weak_spy, "QQQ": weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert aligned.market_orb_quality_joint_weak_max_trades_active is False
assert len(aligned.trades) == 2
assert joint_weak.market_orb_quality_joint_weak_active is True
assert joint_weak.market_orb_quality_joint_weak_max_trades_active is True
assert len(joint_weak.trades) == 1
def test_market_orb_quality_joint_weak_guard_can_target_mild_weak_band() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_joint_weak_primary_above=0.05,
market_orb_quality_joint_weak_primary_below=0.15,
market_orb_quality_joint_weak_secondary_above=0.10,
market_orb_quality_joint_weak_secondary_below=0.15,
market_orb_quality_joint_weak_max_trades=1,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 2000},
]
mild_weak_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 500.5, "low": 499.5, "close": 499.62, "volume": 10000},
]
mild_weak_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.4, "low": 399.8, "close": 399.88, "volume": 10000},
]
panic_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 500.5, "low": 499.5, "close": 499.50, "volume": 10000},
]
panic_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.4, "low": 399.8, "close": 399.82, "volume": 10000},
]
enrichment = _enrichment_for("AAA", "BBB")
mild = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": mild_weak_spy, "QQQ": mild_weak_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
panic = simulate_orb_day(
{"AAA": bars, "BBB": bars, "SPY": panic_spy, "QQQ": panic_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert mild.market_orb_quality_joint_weak_active is True
assert mild.market_orb_quality_joint_weak_max_trades_active is True
assert len(mild.trades) == 1
assert panic.market_orb_quality_joint_weak_active is False
assert panic.market_orb_quality_joint_weak_max_trades_active is False
assert len(panic.trades) == 2
def test_market_orb_quality_joint_panic_guard_can_require_confirmation() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
orb_minutes=5,
sim_bar_minutes=5,
order_timeout_minutes=20,
atr_stop_multiplier=1.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.0,
min_premarket_dollar_vol=0.0,
max_gap_pct=None,
max_candidates=20,
min_candidates_to_trade=1,
risk_per_trade_pct=0.1,
max_position_pct=1.0,
daily_max_loss_pct=1.0,
max_stops_per_day=99,
daily_budget_reset=True,
allow_doji_breakout=True,
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_joint_panic_primary_below=0.1,
market_orb_quality_joint_panic_secondary_below=0.1,
market_orb_quality_joint_panic_require_confirmation=True,
)
aaa_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.2, "low": 100.7, "close": 100.9, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.95, "volume": 2000},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.95, "high": 101.0, "low": 100.85, "close": 100.9, "volume": 2000},
]
calm_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000},
]
calm_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000},
]
panic_spy = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 499.05, "volume": 10000},
]
panic_qqq = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.0, "close": 399.1, "volume": 10000},
]
enrichment = _enrichment_for("AAA")
calm = simulate_orb_day(
{"AAA": aaa_bars, "SPY": calm_spy, "QQQ": calm_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
panic = simulate_orb_day(
{"AAA": aaa_bars, "SPY": panic_spy, "QQQ": panic_qqq},
"2026-01-05",
params,
enrichment,
equity=10_000.0,
)
assert calm.market_orb_quality_joint_panic_active is False
assert calm.conditional_confirmation_active is False
assert len(calm.trades) == 1
assert panic.market_orb_quality_joint_panic_active is True
assert panic.conditional_confirmation_active is True
assert panic.trades == []
def test_quality_breakout_min_body_ratio_filters_weak_candle() -> None:
params = ORBStrategyParams(
engine_family="quality_breakout",
min_body_ratio=0.3,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=10,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"STRONG": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
"WEAK": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 100.2, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.2, "high": 100.5, "low": 100.1, "close": 100.4, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.4, "high": 100.5, "low": 100.3, "close": 100.4, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.4, "high": 100.5, "low": 100.3, "close": 100.4, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.4, "high": 100.5, "low": 100.3, "close": 100.4, "volume": 2000},
],
}
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
{
**_enrichment_for("STRONG", "WEAK"),
},
)
assert [cand["ticker"] for cand in candidates] == ["STRONG"]
def test_compression_breakout_uses_entropy_weight_in_ranking() -> None:
params = ORBStrategyParams(
engine_family="compression_breakout",
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_entropy=-0.15,
weight_atr_ratio=0.0,
weight_gap_zscore=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=10,
min_candidates_to_trade=1,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
]
enrichment = _enrichment_for("LOW_ENT", "HIGH_ENT")
enrichment["LOW_ENT"]["2026-01-05"]["entropy_20d"] = 0.1
enrichment["HIGH_ENT"]["2026-01-05"]["entropy_20d"] = 0.9
candidates = compute_orb_candidates(
{"LOW_ENT": bars, "HIGH_ENT": bars},
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["LOW_ENT", "HIGH_ENT"]
def test_candidates_can_rank_on_premarket_dollar_volume() -> None:
params = ORBStrategyParams(
engine_family="compression_breakout",
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=1.0,
weight_entropy=0.0,
weight_atr_ratio=0.0,
weight_gap_zscore=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=10,
min_candidates_to_trade=1,
)
market_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
]
bars_by_ticker = {
"HIGH_PM": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.9, "close": 100.0, "volume": 10_000},
*market_bars,
],
"LOW_PM": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.9, "close": 100.0, "volume": 1_000},
*market_bars,
],
}
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
_enrichment_for("HIGH_PM", "LOW_PM"),
)
assert [cand["ticker"] for cand in candidates] == ["HIGH_PM", "LOW_PM"]
def test_stocks_in_play_can_gate_on_attention_and_sector_relative_strength() -> None:
params = ORBStrategyParams(
engine_family="stocks_in_play_dual_regime",
entry_direction="long_only",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_candidates_to_trade=1,
max_candidates=10,
require_event_flag=True,
attention_min_wiki_spike_10d=2.0,
attention_min_article_count_3d=3,
min_close_location=0.6,
min_sector_relative_strength=0.01,
require_vwap_confirmation=False,
)
strong_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 103.0, "low": 99.8, "close": 102.8, "volume": 2000, "vwap": 101.5},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 102.8, "high": 103.1, "low": 102.6, "close": 103.0, "volume": 2000, "vwap": 102.9},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.0, "high": 103.2, "low": 102.9, "close": 103.1, "volume": 2000, "vwap": 103.0},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.1, "high": 103.2, "low": 103.0, "close": 103.1, "volume": 2000, "vwap": 103.1},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.1, "high": 103.2, "low": 103.0, "close": 103.1, "volume": 2000, "vwap": 103.1},
]
weak_same_sector = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.2, "low": 99.8, "close": 100.7, "volume": 2000, "vwap": 100.5},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.7, "high": 100.9, "low": 100.6, "close": 100.8, "volume": 2000, "vwap": 100.8},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.8, "high": 100.9, "low": 100.7, "close": 100.8, "volume": 2000, "vwap": 100.8},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.8, "high": 100.9, "low": 100.7, "close": 100.8, "volume": 2000, "vwap": 100.8},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.8, "high": 100.9, "low": 100.7, "close": 100.8, "volume": 2000, "vwap": 100.8},
]
no_attention = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.5, "low": 99.8, "close": 102.0, "volume": 2000, "vwap": 101.5},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 2000, "vwap": 102.0},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.1, "high": 102.2, "low": 102.0, "close": 102.1, "volume": 2000, "vwap": 102.1},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.1, "high": 102.2, "low": 102.0, "close": 102.1, "volume": 2000, "vwap": 102.1},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.1, "high": 102.2, "low": 102.0, "close": 102.1, "volume": 2000, "vwap": 102.1},
]
enrichment = _enrichment_for("STRONG", "WEAK", "NO_ATTN")
enrichment["STRONG"]["2026-01-05"].update({
"event_flag": True,
"event_score": 1.0,
"attention_wiki_spike_10d": 3.0,
"attention_article_count_3d": 5,
})
enrichment["WEAK"]["2026-01-05"].update({
"event_flag": True,
"event_score": 1.0,
"attention_wiki_spike_10d": 2.5,
"attention_article_count_3d": 4,
})
enrichment["NO_ATTN"]["2026-01-05"].update({
"event_flag": True,
"event_score": 1.0,
"attention_wiki_spike_10d": 1.2,
"attention_article_count_3d": 0,
})
candidates = compute_orb_candidates(
{"STRONG": strong_bars, "WEAK": weak_same_sector, "NO_ATTN": no_attention},
"2026-01-05",
params,
enrichment,
ticker_sectors={"STRONG": "TECH", "WEAK": "TECH", "NO_ATTN": "HEALTH"},
)
assert [cand["ticker"] for cand in candidates] == ["STRONG"]
def test_gainers_leader_attention_news_weight_affects_ranking() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
entry_direction="long_only",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_candidates_to_trade=1,
max_candidates=2,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=0.0,
weight_attention_news=1.0,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.5, "high": 102.1, "low": 101.4, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.1, "high": 102.3, "low": 102.0, "close": 102.2, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.2, "high": 102.4, "low": 102.1, "close": 102.3, "volume": 2000},
]
enrichment = _enrichment_for("LOW_NEWS", "HIGH_NEWS")
enrichment["LOW_NEWS"]["2026-01-05"]["attention_article_count_3d"] = 0
enrichment["HIGH_NEWS"]["2026-01-05"]["attention_article_count_3d"] = 8
candidates = compute_orb_candidates(
{"LOW_NEWS": bars, "HIGH_NEWS": bars},
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["HIGH_NEWS", "LOW_NEWS"]
def test_gainers_leader_ownership_initial_weight_affects_ranking() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
entry_direction="long_only",
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_candidates_to_trade=1,
max_candidates=2,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=0.0,
ownership_13dg_lookback_days=180,
weight_ownership_initial_13dg=1.0,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.5, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.5, "high": 102.1, "low": 101.4, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.1, "high": 102.3, "low": 102.0, "close": 102.2, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.2, "high": 102.4, "low": 102.1, "close": 102.3, "volume": 2000},
]
enrichment = _enrichment_for("NO_OWNER", "OWNER")
enrichment["OWNER"]["2026-01-05"].update(
{
"ownership_13dg_flag": True,
"ownership_13dg_initial_flag": True,
"ownership_13dg_days_since": 20,
"ownership_13dg_score": 1.0,
"ownership_13dg_initial_score": 1.0,
}
)
candidates = compute_orb_candidates(
{"NO_OWNER": bars, "OWNER": bars},
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["OWNER", "NO_OWNER"]
def test_ownership_initial_size_scale_requires_clean_initial_owner_setup() -> None:
params = ORBStrategyParams(
ownership_initial_size_scale=1.2,
ownership_initial_min_score_rank_pct=0.8,
ownership_initial_allowed_trigger_types=["orb"],
)
cand = {"ownership_13dg_initial_flag": True}
assert _orb_ownership_initial_size_scale(params, cand, 0.9, "orb", "long", (1.0,)) == 1.2
assert _orb_ownership_initial_size_scale(params, cand, 0.7, "orb", "long", (1.0,)) == 1.0
assert _orb_ownership_initial_size_scale(params, cand, 0.9, "vwap_reclaim", "long", (1.0,)) == 1.0
assert _orb_ownership_initial_size_scale(params, cand, 0.9, "orb", "long", (0.5,)) == 1.0
def test_form4_size_scale_requires_cluster_or_csuite_setup() -> None:
params = ORBStrategyParams(
form4_size_scale=1.2,
form4_min_total_value=100_000.0,
form4_min_owner_count=2,
form4_min_c_suite_count=1,
form4_require_cluster_or_csuite=True,
form4_allowed_trigger_types=["orb"],
)
cluster = {
"form4_flag": True,
"form4_total_value": 150_000.0,
"form4_owner_count": 2,
"form4_c_suite_count": 0,
}
csuite = {
"form4_flag": True,
"form4_total_value": 150_000.0,
"form4_owner_count": 1,
"form4_c_suite_count": 1,
}
weak = {
"form4_flag": True,
"form4_total_value": 150_000.0,
"form4_owner_count": 1,
"form4_c_suite_count": 0,
}
assert _orb_form4_size_scale(params, cluster, "orb", "long", (1.0,)) == 1.2
assert _orb_form4_size_scale(params, csuite, "orb", "long", (1.0,)) == 1.2
assert _orb_form4_size_scale(params, weak, "orb", "long", (1.0,)) == 1.0
assert _orb_form4_size_scale(params, cluster, "vwap_reclaim", "long", (1.0,)) == 1.0
assert _orb_form4_size_scale(params, cluster, "orb", "long", (0.5,)) == 1.0
def test_stocks_in_play_can_allow_red_to_green_reclaim() -> None:
params = ORBStrategyParams(
engine_family="stocks_in_play_dual_regime",
entry_direction="long_only",
allow_doji_breakout=True,
allow_red_to_green_breakout=True,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_candidates_to_trade=1,
max_candidates=10,
require_event_flag=True,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 99.9, "volume": 2000, "vwap": 100.0},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.9, "high": 100.3, "low": 99.8, "close": 100.2, "volume": 2000, "vwap": 100.1},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.2, "high": 100.4, "low": 100.1, "close": 100.3, "volume": 2000, "vwap": 100.3},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 2000, "vwap": 100.3},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 2000, "vwap": 100.3},
]
enrichment = _enrichment_for("R2G")
enrichment["R2G"]["2026-01-05"].update({"event_flag": True, "event_score": 1.0})
candidates = compute_orb_candidates({"R2G": bars}, "2026-01-05", params, enrichment)
assert [cand["ticker"] for cand in candidates] == ["R2G"]
def test_candidates_can_filter_on_min_abs_gap_pct() -> None:
params = ORBStrategyParams(
engine_family="compression_breakout",
min_abs_gap_pct=0.02,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=10,
min_candidates_to_trade=1,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
]
enrichment = _enrichment_for("BIG_GAP", "SMALL_GAP")
enrichment["BIG_GAP"]["2026-01-05"]["prev_close"] = 95.0
enrichment["SMALL_GAP"]["2026-01-05"]["prev_close"] = 99.5
candidates = compute_orb_candidates(
{"BIG_GAP": bars, "SMALL_GAP": bars},
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["BIG_GAP"]
def test_candidates_can_cap_names_per_sector_after_ranking() -> None:
params = ORBStrategyParams(
weight_rvol=1.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=3,
max_candidates_per_sector=1,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"TECH1": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 4000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
"TECH2": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 3000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
"HEALTH1": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
}
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
_enrichment_for("TECH1", "TECH2", "HEALTH1"),
ticker_sectors={"TECH1": "Technology", "TECH2": "Technology", "HEALTH1": "Healthcare"},
)
assert [cand["ticker"] for cand in candidates] == ["TECH1", "HEALTH1"]
def test_sector_confirmation_can_promote_same_sector_cluster() -> None:
params = ORBStrategyParams(
weight_rvol=1.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=1,
min_candidates_to_trade=1,
sector_confirmation_enabled=True,
sector_confirmation_min_members=2,
sector_confirmation_score_weight=1.0,
)
def bars(volume: int) -> list[dict]:
return [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": volume},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
]
candidates = compute_orb_candidates(
{
"LONE": bars(5000),
"TECH1": bars(4600),
"TECH2": bars(4500),
},
"2026-01-05",
params,
_enrichment_for("LONE", "TECH1", "TECH2"),
ticker_sectors={
"LONE": "Healthcare",
"TECH1": "Technology",
"TECH2": "Technology",
},
)
assert [cand["ticker"] for cand in candidates] == ["TECH1"]
assert candidates[0]["sector_confirmation_active"] is True
assert candidates[0]["sector_confirmation_member_count"] == 2
def test_sector_confirmation_size_scale_supports_liquidity_tiers() -> None:
params = ORBStrategyParams(
sector_confirmation_enabled=True,
sector_confirmation_size_scale=1.10,
sector_confirmation_liquid_min_premarket_dollar_vol=20_000_000,
sector_confirmation_liquid_size_scale=1.20,
sector_confirmation_illiquid_size_scale=1.0,
sector_confirmation_unconfirmed_size_scale=0.95,
)
liquid = {
"sector_confirmation_active": True,
"premarket_dollar_vol": 25_000_000,
}
thin = {
"sector_confirmation_active": True,
"premarket_dollar_vol": 5_000_000,
}
unconfirmed = {
"sector_confirmation_active": False,
"premarket_dollar_vol": 25_000_000,
}
assert _orb_sector_confirmation_size_scale(params, liquid) == 1.20
assert _orb_sector_confirmation_size_scale(params, thin) == 1.0
assert _orb_sector_confirmation_size_scale(params, unconfirmed) == 0.95
def test_opening_burst_liquid_size_scale_requires_time_liquidity_and_clean_risk() -> None:
params = ORBStrategyParams(
opening_burst_liquid_size_scale=1.4,
opening_burst_liquid_max_entry_minutes_after_open=5,
opening_burst_liquid_min_premarket_dollar_vol=100_000_000,
opening_burst_liquid_min_gap_pct=0.0,
opening_burst_liquid_min_candidate_score=0.65,
opening_burst_liquid_allowed_trigger_types=["orb"],
)
cand = {"premarket_dollar_vol": 150_000_000, "gap_pct": 0.02, "score": 0.70}
entry_ts = dt.datetime(
2026,
1,
5,
9,
35,
tzinfo=dt.timezone(dt.timedelta(hours=-5)),
)
late_ts = entry_ts + dt.timedelta(minutes=5)
assert (
_orb_opening_burst_liquid_size_scale(
params,
{"premarket_dollar_vol": 150_000_000, "gap_pct": 0.02, "score": 0.60},
date_str="2026-01-05",
entry_ts=entry_ts,
trigger_type="orb",
score_rank_pct=0.5,
direction_str="long",
risk_size_scales=(1.0, 1.0),
)
== 1.0
)
assert (
_orb_opening_burst_liquid_size_scale(
params,
cand,
date_str="2026-01-05",
entry_ts=entry_ts,
trigger_type="orb",
score_rank_pct=0.5,
direction_str="long",
risk_size_scales=(1.0, 1.0),
)
== 1.4
)
assert (
_orb_opening_burst_liquid_size_scale(
params,
{"premarket_dollar_vol": 150_000_000, "gap_pct": -0.01, "score": 0.70},
date_str="2026-01-05",
entry_ts=entry_ts,
trigger_type="orb",
score_rank_pct=0.5,
direction_str="long",
risk_size_scales=(1.0, 1.0),
)
== 1.0
)
assert (
_orb_opening_burst_liquid_size_scale(
params,
cand,
date_str="2026-01-05",
entry_ts=late_ts,
trigger_type="orb",
score_rank_pct=0.5,
direction_str="long",
risk_size_scales=(1.0, 1.0),
)
== 1.0
)
assert (
_orb_opening_burst_liquid_size_scale(
params,
cand,
date_str="2026-01-05",
entry_ts=entry_ts,
trigger_type="soft_day_vwap_reclaim",
score_rank_pct=0.5,
direction_str="long",
risk_size_scales=(1.0, 1.0),
)
== 1.0
)
assert (
_orb_opening_burst_liquid_size_scale(
params,
cand,
date_str="2026-01-05",
entry_ts=entry_ts,
trigger_type="orb",
score_rank_pct=0.5,
direction_str="long",
risk_size_scales=(0.5, 1.0),
)
== 1.0
)
def test_unboosted_primary_fragility_scales_only_unboosted_fragile_orb() -> None:
params = ORBStrategyParams(
unboosted_primary_fragility_size_scale=0.25,
unboosted_primary_fragility_allowed_trigger_types=["orb"],
unboosted_primary_fragility_crowded_min_gap_pct=0.02,
unboosted_primary_fragility_crowded_min_premarket_dollar_vol=50_000_000,
unboosted_primary_fragility_crowded_max_close_location=0.85,
unboosted_primary_fragility_weak_max_rvol=5.0,
unboosted_primary_fragility_weak_max_ret_5d=0.05,
)
crowded = {
"gap_pct": 0.03,
"premarket_dollar_vol": 75_000_000,
"close_location": 0.70,
"rvol": 8.0,
"ret_5d": 0.20,
}
weak_attention = {
"gap_pct": -0.03,
"premarket_dollar_vol": 10_000_000,
"close_location": 0.95,
"rvol": 4.5,
"ret_5d": -0.02,
}
clean = {
"gap_pct": 0.03,
"premarket_dollar_vol": 75_000_000,
"close_location": 0.95,
"rvol": 8.0,
"ret_5d": 0.20,
}
assert (
_orb_unboosted_primary_fragility_size_scale(
params,
crowded,
{},
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0, 1.0),
)
== 0.25
)
assert (
_orb_unboosted_primary_fragility_size_scale(
params,
weak_attention,
{},
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0, 1.0),
)
== 0.25
)
assert (
_orb_unboosted_primary_fragility_size_scale(
params,
crowded,
{},
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.5, 1.0),
)
== 1.0
)
assert (
_orb_unboosted_primary_fragility_size_scale(
params,
crowded,
{},
trigger_type="soft_day_vwap_reclaim",
direction_str="long",
high_conviction_size_scales=(1.0, 1.0),
)
== 1.0
)
assert (
_orb_unboosted_primary_fragility_size_scale(
params,
clean,
{},
trigger_type="orb",
direction_str="long",
high_conviction_size_scales=(1.0, 1.0),
)
== 1.0
)
def test_soft_day_sector_confirmation_override_requires_sector_and_reason() -> None:
params = ORBStrategyParams(
soft_day_sector_confirmation_override_enabled=True,
soft_day_sector_confirmation_override_min_score_pct=0.70,
soft_day_sector_confirmation_override_min_premarket_dollar_vol=20_000_000,
soft_day_sector_confirmation_override_allowed_reason_parts=["breadth"],
soft_day_sector_confirmation_override_allowed_trigger_types=["orb"],
)
cand = {
"sector_confirmation_active": True,
"premarket_dollar_vol": 25_000_000,
}
assert _orb_soft_day_sector_confirmation_override_allows(
params,
"market_regime+breadth",
cand,
score_rank_pct=0.75,
trigger_type="orb",
)
assert not _orb_soft_day_sector_confirmation_override_allows(
params,
"market_regime",
cand,
score_rank_pct=0.75,
trigger_type="orb",
)
assert not _orb_soft_day_sector_confirmation_override_allows(
params,
"market_regime+breadth",
{**cand, "sector_confirmation_active": False},
score_rank_pct=0.75,
trigger_type="orb",
)
assert not _orb_soft_day_sector_confirmation_override_allows(
params,
"market_regime+breadth",
cand,
score_rank_pct=0.65,
trigger_type="orb",
)
assert not _orb_soft_day_sector_confirmation_override_allows(
params,
"market_regime+breadth",
cand,
score_rank_pct=0.75,
trigger_type="soft_day_vwap_reclaim",
)
def test_soft_day_sector_confirmation_override_base_sizing_floor_is_active_only() -> None:
params = ORBStrategyParams(
soft_day_sector_confirmation_override_min_day_size_scale=0.08,
)
assert _orb_soft_day_sector_confirmation_override_base_sizing(
params,
active=False,
sizing_cap=10_000,
adjusted_sizing=0.0,
) == (0.0, None)
assert _orb_soft_day_sector_confirmation_override_base_sizing(
params,
active=True,
sizing_cap=10_000,
adjusted_sizing=0.0,
) == (800.0, 0.08)
assert _orb_soft_day_sector_confirmation_override_base_sizing(
params,
active=True,
sizing_cap=10_000,
adjusted_sizing=1_200.0,
) == (1_200.0, 0.08)
def test_candidates_can_prune_weak_tail_with_relative_quality_floor() -> None:
params = ORBStrategyParams(
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=1.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=3,
min_candidates_to_trade=1,
basket_quality_relative_floor=0.6,
)
bars_by_ticker = {
"LEADER": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 4000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
"MID": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 3000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
"TAIL": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
}
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
_enrichment_for("LEADER", "MID", "TAIL"),
)
assert [cand["ticker"] for cand in candidates] == ["LEADER"]
def test_quality_floor_respects_min_count() -> None:
params = ORBStrategyParams(
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=1.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=3,
min_candidates_to_trade=1,
basket_quality_relative_floor=0.95,
basket_quality_min_count=2,
)
bars_by_ticker = {
"LEADER": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 4000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
"MID": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 3000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
"TAIL": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
],
}
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
_enrichment_for("LEADER", "MID", "TAIL"),
)
assert [cand["ticker"] for cand in candidates] == ["LEADER", "MID"]
def test_gainers_leader_prefers_premarket_attention_and_abs_gap() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
weight_rvol=0.50,
weight_gap=0.15,
weight_dollar_vol=0.10,
weight_premarket_dollar_vol=0.25,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
max_gap_pct=None,
max_candidates=10,
min_candidates_to_trade=1,
)
market_bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000},
]
bars_by_ticker = {
"LEADER": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 104.0, "high": 104.5, "low": 103.8, "close": 104.2, "volume": 20_000},
*market_bars,
],
"LAGGARD": [
{"timestamp": "2026-01-05T08:00:00-05:00", "open": 101.0, "high": 101.2, "low": 100.8, "close": 101.1, "volume": 1_000},
*market_bars,
],
}
enrichment = _enrichment_for("LEADER", "LAGGARD")
enrichment["LEADER"]["2026-01-05"]["prev_close"] = 95.0
enrichment["LAGGARD"]["2026-01-05"]["prev_close"] = 97.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["LEADER", "LAGGARD"]
def test_gainers_leader_can_allow_doji_followthrough_breakouts() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
entry_direction="long_only",
allow_doji_breakout=True,
weight_rvol=0.40,
weight_gap=0.20,
weight_dollar_vol=0.05,
weight_premarket_dollar_vol=0.35,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"DOJI": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.02, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.2, "high": 106.5, "low": 105.0, "close": 106.2, "volume": 6_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.2, "high": 106.4, "low": 105.9, "close": 106.1, "volume": 3_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.1, "high": 106.6, "low": 106.0, "close": 106.4, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.4, "high": 106.8, "low": 106.2, "close": 106.7, "volume": 2_000},
],
}
enrichment = _enrichment_for("DOJI")
enrichment["DOJI"]["2026-01-05"]["prev_close"] = 100.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["DOJI"]
def test_gainers_leader_can_allow_red_to_green_followthrough_breakouts() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
entry_direction="long_only",
allow_red_to_green_breakout=True,
weight_rvol=0.40,
weight_gap=0.20,
weight_dollar_vol=0.05,
weight_premarket_dollar_vol=0.35,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"RED_GREEN": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.3, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.4, "high": 105.8, "low": 104.2, "close": 105.6, "volume": 6_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.6, "high": 105.9, "low": 105.4, "close": 105.8, "volume": 3_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.8, "high": 106.0, "low": 105.6, "close": 105.9, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.9, "high": 106.2, "low": 105.7, "close": 106.1, "volume": 2_000},
],
}
enrichment = _enrichment_for("RED_GREEN")
enrichment["RED_GREEN"]["2026-01-05"]["prev_close"] = 100.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["RED_GREEN"]
def test_gainers_leader_can_conditionally_reject_high_gap_zscore_without_prior_trend_support() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
entry_direction="long_only",
allow_red_to_green_breakout=True,
conditional_gap_zscore_reject_above=2.0,
conditional_gap_zscore_reject_ret_5d_below=0.0,
weight_rvol=0.40,
weight_gap=0.20,
weight_dollar_vol=0.05,
weight_premarket_dollar_vol=0.35,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.02,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars = [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.4, "close": 105.8, "volume": 3_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.8, "high": 106.2, "low": 105.6, "close": 106.0, "volume": 2_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.0, "high": 106.2, "low": 105.8, "close": 106.1, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.1, "high": 106.3, "low": 106.0, "close": 106.2, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.2, "high": 106.4, "low": 106.1, "close": 106.3, "volume": 2_000},
]
enrichment = _enrichment_for("BAD_HIGH_NEG", "GOOD_HIGH_POS", "GOOD_LOW_NEG")
for ticker in ("BAD_HIGH_NEG", "GOOD_HIGH_POS", "GOOD_LOW_NEG"):
enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0
enrichment["BAD_HIGH_NEG"]["2026-01-05"].update({"gap_zscore_20d": 2.5, "ret_5d": -0.03})
enrichment["GOOD_HIGH_POS"]["2026-01-05"].update({"gap_zscore_20d": 2.5, "ret_5d": 0.08})
enrichment["GOOD_LOW_NEG"]["2026-01-05"].update({"gap_zscore_20d": 1.2, "ret_5d": -0.03})
candidates = compute_orb_candidates(
{"BAD_HIGH_NEG": bars, "GOOD_HIGH_POS": bars, "GOOD_LOW_NEG": bars},
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["GOOD_HIGH_POS", "GOOD_LOW_NEG"]
def test_dual_regime_requires_actual_event_flag_and_filters_event_types() -> None:
params = ORBStrategyParams(
engine_family="stocks_in_play_dual_regime",
entry_direction="candle",
require_event_flag=True,
allowed_event_types=["other_material_event"],
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_close_location=0.6,
weight_event_catalyst=1.0,
max_candidates=5,
min_candidates_to_trade=1,
)
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 107.0, "low": 104.9, "close": 106.8, "volume": 5000, "vwap": 106.1},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 106.8, "high": 107.2, "low": 106.7, "close": 107.0, "volume": 3000, "vwap": 106.9},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 107.0, "high": 107.2, "low": 106.8, "close": 107.1, "volume": 2000, "vwap": 107.0},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 107.1, "high": 107.3, "low": 106.9, "close": 107.2, "volume": 2000, "vwap": 107.1},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 107.2, "high": 107.4, "low": 107.0, "close": 107.3, "volume": 2000, "vwap": 107.2},
]
enrichment = _enrichment_for("GOOD", "WRONG_TYPE", "NO_EVENT")
for ticker in ("GOOD", "WRONG_TYPE", "NO_EVENT"):
enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0
enrichment["GOOD"]["2026-01-05"]["event_flag"] = True
enrichment["GOOD"]["2026-01-05"]["event_types"] = ["other_material_event"]
enrichment["GOOD"]["2026-01-05"]["event_score"] = 1.0
enrichment["WRONG_TYPE"]["2026-01-05"]["event_flag"] = True
enrichment["WRONG_TYPE"]["2026-01-05"]["event_types"] = ["management_change"]
enrichment["WRONG_TYPE"]["2026-01-05"]["event_score"] = 0.6
candidates = compute_orb_candidates(
{"GOOD": bars, "WRONG_TYPE": bars, "NO_EVENT": bars},
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["GOOD"]
def test_dual_regime_can_take_failed_gap_up_short_below_vwap() -> None:
params = ORBStrategyParams(
engine_family="stocks_in_play_dual_regime",
entry_direction="candle",
require_event_flag=True,
allow_failed_orb_short=True,
require_vwap_confirmation=True,
max_close_location_short=0.40,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"FAILED": [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.3, "low": 103.8, "close": 104.0, "volume": 6000, "vwap": 104.7},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 104.2, "low": 103.6, "close": 103.8, "volume": 3000, "vwap": 103.9},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.8, "high": 104.0, "low": 103.5, "close": 103.7, "volume": 2000, "vwap": 103.8},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.7, "high": 103.9, "low": 103.4, "close": 103.5, "volume": 2000, "vwap": 103.6},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.5, "high": 103.7, "low": 103.2, "close": 103.3, "volume": 2000, "vwap": 103.4},
],
}
enrichment = _enrichment_for("FAILED")
enrichment["FAILED"]["2026-01-05"]["prev_close"] = 100.0
enrichment["FAILED"]["2026-01-05"]["event_flag"] = True
enrichment["FAILED"]["2026-01-05"]["event_types"] = ["other_material_event"]
enrichment["FAILED"]["2026-01-05"]["event_score"] = 1.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["FAILED"]
assert candidates[0]["direction"] == "bearish"
def test_gainers_leader_can_override_min_gap_for_high_attention_small_gap_names() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
entry_direction="long_only",
allow_red_to_green_breakout=True,
min_abs_gap_pct=0.02,
small_gap_attention_override_premarket_dollar_vol=1_000_000.0,
small_gap_attention_override_rvol=1.5,
weight_rvol=0.40,
weight_gap=0.20,
weight_dollar_vol=0.05,
weight_premarket_dollar_vol=0.35,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"ATTN_SMALL_GAP": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 100.5, "high": 101.0, "low": 100.4, "close": 100.8, "volume": 20_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 101.1, "low": 99.8, "close": 100.4, "volume": 3_500},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.5, "high": 101.6, "low": 100.4, "close": 101.5, "volume": 3_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.5, "high": 101.7, "low": 101.3, "close": 101.6, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 101.8, "low": 101.5, "close": 101.7, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2_000},
],
}
enrichment = _enrichment_for("ATTN_SMALL_GAP")
enrichment["ATTN_SMALL_GAP"]["2026-01-05"]["prev_close"] = 100.5
enrichment["ATTN_SMALL_GAP"]["2026-01-05"]["avg_daily_vol_14d"] = 100_000.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["ATTN_SMALL_GAP"]
def test_gainers_leader_can_cap_small_gap_attention_override_names_per_day() -> None:
params = ORBStrategyParams(
engine_family="gainers_leader",
entry_direction="long_only",
allow_red_to_green_breakout=True,
min_abs_gap_pct=0.02,
small_gap_attention_override_premarket_dollar_vol=1_000_000.0,
small_gap_attention_override_rvol=1.5,
max_small_gap_attention_candidates=1,
weight_rvol=1.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=0.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"ATTN1": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 100.5, "high": 101.0, "low": 100.4, "close": 100.8, "volume": 20_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 101.1, "low": 99.8, "close": 100.4, "volume": 4_500},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.5, "high": 101.6, "low": 100.4, "close": 101.5, "volume": 3_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.5, "high": 101.7, "low": 101.3, "close": 101.6, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 101.8, "low": 101.5, "close": 101.7, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2_000},
],
"ATTN2": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 100.5, "high": 101.0, "low": 100.4, "close": 100.8, "volume": 20_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 101.1, "low": 99.8, "close": 100.4, "volume": 3_500},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.5, "high": 101.6, "low": 100.4, "close": 101.5, "volume": 3_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.5, "high": 101.7, "low": 101.3, "close": 101.6, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 101.8, "low": 101.5, "close": 101.7, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2_000},
],
"BIG_GAP": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.4, "close": 105.8, "volume": 3_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.8, "high": 106.2, "low": 105.6, "close": 106.0, "volume": 2_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.0, "high": 106.2, "low": 105.8, "close": 106.1, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.1, "high": 106.3, "low": 106.0, "close": 106.2, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.2, "high": 106.4, "low": 106.1, "close": 106.3, "volume": 2_000},
],
}
enrichment = _enrichment_for("ATTN1", "ATTN2", "BIG_GAP")
enrichment["ATTN1"]["2026-01-05"]["prev_close"] = 100.5
enrichment["ATTN1"]["2026-01-05"]["avg_daily_vol_14d"] = 100_000.0
enrichment["ATTN2"]["2026-01-05"]["prev_close"] = 100.5
enrichment["ATTN2"]["2026-01-05"]["avg_daily_vol_14d"] = 100_000.0
enrichment["BIG_GAP"]["2026-01-05"]["prev_close"] = 100.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["BIG_GAP", "ATTN1"]
def test_leader_followthrough_requires_strong_close_location_for_red_to_green() -> None:
params = ORBStrategyParams(
engine_family="leader_followthrough",
entry_direction="long_only",
allow_red_to_green_breakout=True,
min_close_location=0.55,
weight_rvol=0.30,
weight_gap=0.05,
weight_dollar_vol=0.10,
weight_premarket_dollar_vol=0.35,
weight_close_location=0.20,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.005,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"STRONG_RECLAIM": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.85, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.9, "high": 105.8, "low": 104.8, "close": 105.7, "volume": 6_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.7, "high": 106.0, "low": 105.5, "close": 105.9, "volume": 3_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.9, "high": 106.1, "low": 105.8, "close": 106.0, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.0, "high": 106.2, "low": 105.8, "close": 106.1, "volume": 2_000},
],
"WEAK_RECLAIM": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.2, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.2, "high": 104.6, "low": 104.0, "close": 104.3, "volume": 4_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 104.3, "high": 104.5, "low": 104.1, "close": 104.4, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 104.4, "high": 104.5, "low": 104.2, "close": 104.4, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 104.4, "high": 104.6, "low": 104.2, "close": 104.5, "volume": 2_000},
],
}
enrichment = _enrichment_for("STRONG_RECLAIM", "WEAK_RECLAIM")
enrichment["STRONG_RECLAIM"]["2026-01-05"]["prev_close"] = 104.0
enrichment["WEAK_RECLAIM"]["2026-01-05"]["prev_close"] = 104.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["STRONG_RECLAIM"]
def test_leader_followthrough_ranking_rewards_close_location() -> None:
params = ORBStrategyParams(
engine_family="leader_followthrough",
entry_direction="long_only",
allow_red_to_green_breakout=True,
min_close_location=0.0,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=0.0,
weight_close_location=1.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.005,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"HIGH_CLOSE": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 12_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.85, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.95, "high": 105.8, "low": 104.9, "close": 105.7, "volume": 5_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.7, "high": 105.9, "low": 105.5, "close": 105.8, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.8, "high": 105.9, "low": 105.6, "close": 105.8, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.8, "high": 106.0, "low": 105.6, "close": 105.9, "volume": 2_000},
],
"LOW_CLOSE": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 12_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.3, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.3, "high": 104.8, "low": 104.2, "close": 104.5, "volume": 5_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 104.5, "high": 104.7, "low": 104.3, "close": 104.6, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 104.6, "high": 104.7, "low": 104.4, "close": 104.6, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 104.6, "high": 104.8, "low": 104.4, "close": 104.7, "volume": 2_000},
],
}
enrichment = _enrichment_for("HIGH_CLOSE", "LOW_CLOSE")
enrichment["HIGH_CLOSE"]["2026-01-05"]["prev_close"] = 104.0
enrichment["LOW_CLOSE"]["2026-01-05"]["prev_close"] = 104.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["HIGH_CLOSE", "LOW_CLOSE"]
def test_vwap_reclaim_ranking_can_prefer_weaker_opening_structure() -> None:
params = ORBStrategyParams(
engine_family="vwap_reclaim_v1",
entry_direction="long_only",
allow_red_to_green_breakout=True,
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=0.0,
weight_close_location=-1.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.005,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"DEEP_PULLBACK": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.3, "low": 103.9, "close": 104.1, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.2, "high": 104.8, "low": 104.0, "close": 104.6, "volume": 4_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 104.6, "high": 104.8, "low": 104.4, "close": 104.7, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 104.7, "high": 104.9, "low": 104.6, "close": 104.8, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 104.8, "high": 105.0, "low": 104.7, "close": 104.9, "volume": 2_000},
],
"STRONG_OPEN": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.5, "low": 104.7, "close": 105.4, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.4, "high": 105.8, "low": 105.2, "close": 105.7, "volume": 4_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.7, "high": 105.9, "low": 105.5, "close": 105.8, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.8, "high": 106.0, "low": 105.7, "close": 105.9, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.9, "high": 106.1, "low": 105.8, "close": 106.0, "volume": 2_000},
],
}
enrichment = _enrichment_for("DEEP_PULLBACK", "STRONG_OPEN")
enrichment["DEEP_PULLBACK"]["2026-01-05"]["prev_close"] = 104.0
enrichment["STRONG_OPEN"]["2026-01-05"]["prev_close"] = 104.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["DEEP_PULLBACK", "STRONG_OPEN"]
def test_orb_pullback_ranking_can_use_event_catalyst_score() -> None:
params = ORBStrategyParams(
engine_family="orb_pullback_v1",
entry_direction="long_only",
weight_rvol=0.0,
weight_gap=0.0,
weight_dollar_vol=0.0,
weight_premarket_dollar_vol=0.0,
weight_event_catalyst=1.0,
min_price=10.0,
min_avg_dollar_volume=0.0,
min_atr_14=0.1,
min_rvol=0.1,
min_abs_gap_pct=0.005,
min_premarket_dollar_vol=100_000.0,
max_gap_pct=None,
max_candidates=5,
min_candidates_to_trade=1,
)
bars_by_ticker = {
"PLAIN": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.3, "low": 101.9, "close": 102.2, "volume": 12_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.5, "low": 104.4, "close": 105.2, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.2, "high": 105.8, "low": 105.1, "close": 105.6, "volume": 5_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.6, "high": 105.8, "low": 105.4, "close": 105.7, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.7, "high": 105.9, "low": 105.6, "close": 105.8, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.8, "high": 106.0, "low": 105.7, "close": 105.9, "volume": 2_000},
],
"EVENTFUL": [
{"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.3, "low": 101.9, "close": 102.2, "volume": 12_000},
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.5, "low": 104.4, "close": 105.2, "volume": 8_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.2, "high": 105.8, "low": 105.1, "close": 105.6, "volume": 5_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.6, "high": 105.8, "low": 105.4, "close": 105.7, "volume": 2_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.7, "high": 105.9, "low": 105.6, "close": 105.8, "volume": 2_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.8, "high": 106.0, "low": 105.7, "close": 105.9, "volume": 2_000},
],
}
enrichment = _enrichment_for("PLAIN", "EVENTFUL")
enrichment["PLAIN"]["2026-01-05"]["prev_close"] = 104.0
enrichment["EVENTFUL"]["2026-01-05"]["prev_close"] = 104.0
enrichment["PLAIN"]["2026-01-05"]["event_score"] = 0.0
enrichment["EVENTFUL"]["2026-01-05"]["event_flag"] = True
enrichment["EVENTFUL"]["2026-01-05"]["event_score"] = 3.0
candidates = compute_orb_candidates(
bars_by_ticker,
"2026-01-05",
params,
enrichment,
)
assert [cand["ticker"] for cand in candidates] == ["EVENTFUL", "PLAIN"]
def test_vwap_reclaim_trade_waits_for_prior_dip_then_enters_on_reclaim() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.2, "low": 103.8, "close": 104.0, "volume": 1_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 104.2, "low": 103.6, "close": 103.8, "volume": 1_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.8, "high": 104.0, "low": 103.5, "close": 103.7, "volume": 1_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.7, "high": 103.9, "low": 103.6, "close": 103.8, "volume": 1_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.8, "high": 104.8, "low": 103.7, "close": 104.7, "volume": 1_500},
{"timestamp": "2026-01-05T09:55:00-05:00", "open": 104.7, "high": 105.0, "low": 104.5, "close": 104.9, "volume": 1_200},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.9, "high": 105.1, "low": 104.8, "close": 105.0, "volume": 1_000},
]
params = ORBStrategyParams(
engine_family="vwap_reclaim_v1",
orb_minutes=5,
sim_bar_minutes=5,
vwap_reclaim_window_start_min=20,
vwap_reclaim_window_end_min=120,
vwap_reclaim_require_prior_dip=True,
vwap_reclaim_min_clearance_pct=0.0,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
)
trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=2.0,
gap_pct=0.05,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="VWAP",
)
assert trade is not None
assert trade.entry_time == "2026-01-05T09:50:00-05:00"
assert trade.entry_price == pytest.approx(104.7)
def test_vwap_reclaim_trade_can_require_orb_open_retake_and_rel_volume() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.2, "low": 103.8, "close": 104.0, "volume": 1_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 104.2, "low": 103.6, "close": 103.8, "volume": 1_000},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.8, "high": 104.0, "low": 103.5, "close": 103.7, "volume": 1_000},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.7, "high": 103.9, "low": 103.6, "close": 103.8, "volume": 1_000},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.8, "high": 104.9, "low": 103.7, "close": 104.7, "volume": 1_000},
{"timestamp": "2026-01-05T09:55:00-05:00", "open": 104.7, "high": 105.4, "low": 104.6, "close": 105.2, "volume": 1_500},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.2, "high": 105.4, "low": 105.1, "close": 105.3, "volume": 1_000},
]
params = ORBStrategyParams(
engine_family="vwap_reclaim_v1",
orb_minutes=5,
sim_bar_minutes=5,
vwap_reclaim_window_start_min=20,
vwap_reclaim_window_end_min=120,
vwap_reclaim_require_prior_dip=True,
vwap_reclaim_min_clearance_pct=0.0,
vwap_reclaim_require_orb_open_retake=True,
vwap_reclaim_confirm_rel_vol=1.2,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
)
trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=2.0,
gap_pct=0.05,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="VWAP",
)
assert trade is not None
assert trade.entry_time == "2026-01-05T09:55:00-05:00"
assert trade.entry_price == pytest.approx(105.2)
def test_pullback_trade_waits_for_breakout_retake_and_reclaim_volume() -> None:
bars = [
{"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.8, "close": 105.8, "volume": 1_000},
{"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.8, "high": 106.3, "low": 105.7, "close": 106.1, "volume": 1_500},
{"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.1, "high": 106.8, "low": 106.0, "close": 106.6, "volume": 1_300},
{"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.6, "high": 106.7, "low": 105.8, "close": 106.0, "volume": 700},
{"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.0, "high": 106.2, "low": 105.9, "close": 106.05, "volume": 700},
{"timestamp": "2026-01-05T09:55:00-05:00", "open": 106.05, "high": 106.5, "low": 106.0, "close": 106.35, "volume": 1_600},
{"timestamp": "2026-01-05T15:55:00-05:00", "open": 106.35, "high": 106.5, "low": 106.2, "close": 106.4, "volume": 1_000},
]
params = ORBStrategyParams(
engine_family="orb_pullback_v1",
orb_minutes=5,
sim_bar_minutes=5,
pullback_entry=True,
pullback_max_bars=6,
pullback_min_retracement_pct=0.25,
pullback_require_breakout_retake=True,
pullback_breakout_retake_clearance_pct=0.002,
pullback_reclaim_confirm_rel_vol=1.2,
atr_stop_multiplier=10.0,
trailing_at_r=99.0,
breakeven_at_r=99.0,
slippage_bps=0.0,
)
trade = simulate_orb_trade(
bars,
bars[0],
"long",
atr=1.0,
rvol=2.0,
gap_pct=0.05,
params=params,
equity=10_000.0,
date_str="2026-01-05",
ticker="PULL",
)
assert trade is not None
assert trade.entry_time == "2026-01-05T09:55:00-05:00"
assert trade.entry_price == pytest.approx(106.35)