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.

1249 lines
44 KiB
Python

from __future__ import annotations
import asyncio
import pickle
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
from zoneinfo import ZoneInfo
import pyarrow as pa
import pyarrow.parquet as pq
import pytest
import apps.intraday_bt.run as run_mod
from apps.intraday_bt.run import (
_apply_orb_prior_event_decay_features,
_append_synthetic_today_daily_rows,
_augment_candidate_map_with_support_tickers,
_augment_momentum_seed_candidates_with_liquid_overlay,
_build_daily_cache,
_build_intraday_data_provenance,
_chunk_trading_days_by_pairs,
_fetch_vix_by_day,
_latest_backtest_date,
_latest_completed_trading_day,
_load_orb_form4_features,
_load_vix_from_local_macro_snapshots,
_merge_candidate_maps,
_normalize_candidate_map,
_orb_intraday_support_tickers,
_load_orb_ownership_13dg_features,
_momentum_intraday_seed_candidates,
_momentum_strategy_uses_candidate_stage_catalyst,
_momentum_strategy_uses_daily_enrichment,
_momentum_strategy_requires_regime_ticker_daily,
_momentum_strategy_uses_attention,
_momentum_strategy_uses_catalyst,
_momentum_strategy_uses_sector_labels,
_momentum_strategy_uses_sector_proxies,
_retain_recent_intraday_shortlist,
_recent_intraday_first_candidates,
_strategy_for_recent_live_scan,
_should_use_recent_live_scan,
_should_use_recent_intraday_first_scan,
apply_cli_overrides,
get_trading_days,
load_config,
)
from libs.intraday.catalyst import PriorEventFeatureSnapshotCache
from libs.intraday.cache import DailyBarCache, LayeredDailyBarCache, ReadOnlyDailyBarCache
from libs.intraday.domain import CacheParams, IntradayConfig, ORBStrategyParams, StrategyParams
def test_get_trading_days_uses_local_calendar_for_explicit_range() -> None:
days = asyncio.run(get_trading_days(None, "2026-01-01", "2026-01-10", lookback=0))
assert days == [
"2026-01-02",
"2026-01-05",
"2026-01-06",
"2026-01-07",
"2026-01-08",
"2026-01-09",
]
def test_get_trading_days_trims_to_lookback_when_start_not_pinned() -> None:
days = asyncio.run(get_trading_days(None, None, "2026-01-10", lookback=3))
assert days == [
"2026-01-07",
"2026-01-08",
"2026-01-09",
]
def test_chunk_trading_days_by_pairs_keeps_days_contiguous_and_bounded() -> None:
trading_days = [
"2026-01-05",
"2026-01-06",
"2026-01-07",
"2026-01-08",
]
candidates = {
"2026-01-05": ["A"] * 2000,
"2026-01-06": ["B"] * 2000,
"2026-01-07": ["C"] * 1500,
"2026-01-08": ["D"] * 2500,
}
chunks = _chunk_trading_days_by_pairs(trading_days, candidates, max_pairs_per_chunk=3500)
assert chunks == [
["2026-01-05"],
["2026-01-06", "2026-01-07"],
["2026-01-08"],
]
def test_normalize_candidate_map_unwraps_tuple_payload() -> None:
payload = ({"2026-01-05": ["A", "B"], "2026-01-06": []}, {"2026-01-05": {"A"}})
normalized = _normalize_candidate_map(payload)
assert normalized == {"2026-01-05": ["A", "B"]}
def test_orb_intraday_support_tickers_includes_market_quality_ticker() -> None:
params = ORBStrategyParams(
market_orb_quality_ticker="SPY",
market_orb_quality_secondary_ticker="QQQ",
market_orb_quality_size_scale_low=0.4,
market_orb_quality_size_scale_high=0.65,
conditional_confirmation_ticker="IWM",
)
support = _orb_intraday_support_tickers(params)
assert support == ["IWM", "QQQ", "SPY"]
def test_append_synthetic_today_daily_rows_enables_same_day_enrichment(monkeypatch) -> None:
monkeypatch.setattr(
run_mod,
"utc_now",
lambda: datetime(2026, 5, 1, 16, 0, tzinfo=ZoneInfo("UTC")),
)
daily_bars = {
"AAA": [
{"date": "2026-04-29", "open": 10.0, "high": 11.0, "low": 9.5, "close": 10.5, "volume": 1000.0},
{"date": "2026-04-30", "open": 11.0, "high": 12.0, "low": 10.5, "close": 11.5, "volume": 1200.0},
],
"BBB": [
{"date": "2026-05-01", "open": 20.0, "high": 21.0, "low": 19.5, "close": 20.5, "volume": 1500.0},
],
}
added = _append_synthetic_today_daily_rows(daily_bars, ["2026-05-01"])
assert added == 1
assert daily_bars["AAA"][-1] == {
"date": "2026-05-01",
"open": 11.5,
"high": 11.5,
"low": 11.5,
"close": 11.5,
"volume": 0.0,
"synthetic_today_daily": True,
}
assert daily_bars["BBB"][-1]["open"] == 20.0
def test_augment_candidate_map_with_support_tickers_appends_without_duplicates() -> None:
candidates = {
"2026-01-05": ["AAA", "SPY"],
"2026-01-06": ["BBB"],
}
augmented = _augment_candidate_map_with_support_tickers(candidates, ["SPY"])
assert augmented == {
"2026-01-05": ["AAA", "SPY"],
"2026-01-06": ["BBB", "SPY"],
}
def test_load_orb_ownership_13dg_features_is_pit_safe(tmp_path) -> None:
path = tmp_path / "ownership.parquet"
table = pa.table(
{
"symbol": ["AAA", "AAA", "BBB"],
"filing_date": ["2026-01-03", "2026-01-05", "2026-01-02"],
"form_type": ["SC 13G", "SC 13D", "SC 13G"],
"is_initial_for_owner": [True, True, False],
"is_13g_to_13d_transition": [False, True, False],
"activist_flag": [False, True, False],
"ownership_strength_score": [3, 5, 2],
}
)
pq.write_table(table, path)
params = ORBStrategyParams(
ownership_13dg_lookback_days=3,
ownership_13dg_reference_path=str(path),
weight_ownership_initial_13dg=0.1,
)
features = _load_orb_ownership_13dg_features(
["AAA", "BBB"],
["2026-01-05", "2026-01-06"],
params,
)
assert features["AAA"]["2026-01-05"]["ownership_13dg_initial_flag"] is True
assert features["AAA"]["2026-01-05"]["ownership_13dg_days_since"] == 2
assert features["AAA"]["2026-01-06"]["ownership_13dg_active_13d_flag"] is True
assert "2026-01-05" in features["BBB"]
assert "2026-01-06" not in features["BBB"]
def test_load_orb_form4_features_is_pit_safe_and_aggregates_clusters(tmp_path) -> None:
path = tmp_path / "form4.parquet"
table = pa.table(
{
"symbol": ["AAA", "AAA", "BBB"],
"filing_date": ["2026-01-03", "2026-01-05", "2026-01-02"],
"total_value": [100_000.0, 1_000_000.0, 50_000.0],
"owner_count": [2, 1, 1],
"transaction_count": [3, 1, 1],
"c_suite_count": [0, 1, 0],
"role_weight_score": [1.0, 2.5, 1.0],
"has_officer_or_director": [True, True, False],
}
)
pq.write_table(table, path)
params = ORBStrategyParams(
form4_lookback_days=3,
form4_reference_path=str(path),
form4_size_scale=1.2,
)
features = _load_orb_form4_features(
["AAA", "BBB"],
["2026-01-05", "2026-01-06"],
params,
)
assert features["AAA"]["2026-01-05"]["form4_days_since"] == 2
assert features["AAA"]["2026-01-06"]["form4_total_value"] == 1_100_000.0
assert features["AAA"]["2026-01-06"]["form4_owner_count"] == 2
assert features["AAA"]["2026-01-06"]["form4_c_suite_count"] == 1
assert "2026-01-05" in features["BBB"]
assert "2026-01-06" not in features["BBB"]
def test_merge_candidate_maps_unions_days_without_duplicates() -> None:
merged = _merge_candidate_maps(
[
{"2026-01-05": ["AAA", "BBB"], "2026-01-06": ["CCC"]},
{"2026-01-05": ["BBB", "SPY"], "2026-01-07": ["DDD"]},
]
)
assert merged == {
"2026-01-05": ["AAA", "BBB", "SPY"],
"2026-01-06": ["CCC"],
"2026-01-07": ["DDD"],
}
def test_latest_backtest_date_excludes_today_before_close() -> None:
now_et = datetime(2026, 4, 14, 12, 0, tzinfo=ZoneInfo("America/New_York"))
latest = _latest_backtest_date(now_et)
assert latest.isoformat() == "2026-04-13"
def test_latest_backtest_date_includes_today_after_close() -> None:
now_et = datetime(2026, 4, 14, 16, 1, tzinfo=ZoneInfo("America/New_York"))
latest = _latest_backtest_date(now_et)
assert latest.isoformat() == "2026-04-14"
def test_latest_backtest_date_uses_last_session_on_weekend() -> None:
now_et = datetime(2026, 4, 18, 10, 0, tzinfo=ZoneInfo("America/New_York"))
latest = _latest_backtest_date(now_et)
assert latest.isoformat() == "2026-04-18"
def test_latest_completed_trading_day_walks_back_on_weekend() -> None:
now_et = datetime(2026, 4, 18, 10, 0, tzinfo=ZoneInfo("America/New_York"))
latest = _latest_completed_trading_day(now_et)
assert latest.isoformat() == "2026-04-17"
def test_load_vix_from_local_macro_snapshots_uses_covering_file(tmp_path, monkeypatch) -> None:
parquet_dir = tmp_path / "parquet" / "sample"
parquet_dir.mkdir(parents=True, exist_ok=True)
payload = {
datetime(2025, 1, 2).date(): {"VIXCLS": 17.1},
datetime(2025, 1, 3).date(): {"VIXCLS": 18.2},
datetime(2025, 1, 6).date(): {"VIXCLS": 19.3},
}
path = parquet_dir / "macro_window_2025-01-01_2025-01-10.pkl"
with path.open("wb") as fh:
pickle.dump(payload, fh, protocol=pickle.HIGHEST_PROTOCOL)
monkeypatch.setattr(
run_mod,
"get_settings",
lambda: SimpleNamespace(data_root=str(tmp_path)),
)
result = _load_vix_from_local_macro_snapshots(["2025-01-02", "2025-01-03", "2025-01-06"])
assert result == {
"2025-01-02": 17.1,
"2025-01-03": 18.2,
"2025-01-06": 19.3,
}
def test_fetch_vix_by_day_uses_local_snapshot_when_health_check_fails(tmp_path, monkeypatch) -> None:
parquet_dir = tmp_path / "parquet" / "sample"
parquet_dir.mkdir(parents=True, exist_ok=True)
payload = {
datetime(2025, 1, 2).date(): {"VIXCLS": 17.1},
datetime(2025, 1, 3).date(): {"VIXCLS": 18.2},
}
path = parquet_dir / "macro_window_2025-01-01_2025-01-10.pkl"
with path.open("wb") as fh:
pickle.dump(payload, fh, protocol=pickle.HIGHEST_PROTOCOL)
monkeypatch.setattr(
run_mod,
"get_settings",
lambda: SimpleNamespace(data_root=str(tmp_path)),
)
class _Client:
async def health_check_fast(self, timeout: float = 3.0) -> bool:
return False
result = asyncio.run(_fetch_vix_by_day(_Client(), ["2025-01-02", "2025-01-03"]))
assert result == {
"2025-01-02": 17.1,
"2025-01-03": 18.2,
}
def test_prefetch_prior_event_features_db_reuses_local_snapshot(tmp_path, monkeypatch) -> None:
calls = {"count": 0}
cache = PriorEventFeatureSnapshotCache(str(tmp_path))
async def fake_fetch_live(
tickers: list[str],
trading_days: list[str],
lookback_calendar_days: int = 7,
event_types: tuple[str, ...] = ("earnings_release", "guidance_update"),
) -> dict[str, dict[str, dict]]:
calls["count"] += 1
assert tickers == ["ABC"]
assert trading_days == ["2026-01-05", "2026-01-06"]
assert lookback_calendar_days == 10
assert event_types == ("earnings_release", "guidance_update")
return {
"ABC": {
"2026-01-06": {
"event_flag": True,
"event_score": 1.0,
}
}
}
monkeypatch.setattr(run_mod, "_fetch_prior_event_features_db_live", fake_fetch_live)
first = asyncio.run(
run_mod._prefetch_prior_event_features_db(
["ABC"],
["2026-01-05", "2026-01-06"],
lookback_calendar_days=10,
event_types=("guidance_update", "earnings_release"),
snapshot_cache=cache,
)
)
second = asyncio.run(
run_mod._prefetch_prior_event_features_db(
["ABC"],
["2026-01-05", "2026-01-06"],
lookback_calendar_days=10,
event_types=("earnings_release", "guidance_update"),
snapshot_cache=cache,
)
)
assert first == second
assert calls["count"] == 1
def test_prefetch_prior_event_features_db_refetches_when_decay_needs_details(
tmp_path,
monkeypatch,
) -> None:
calls = {"count": 0}
cache = PriorEventFeatureSnapshotCache(str(tmp_path))
tickers = ["ABC"]
trading_days = ["2026-01-05", "2026-01-06"]
event_types = ("earnings_release",)
cache.put(
tickers,
trading_days,
10,
event_types,
{"ABC": {"2026-01-06": {"event_flag": True, "event_score": 1.0}}},
)
async def fake_fetch_live(
tickers: list[str],
trading_days: list[str],
lookback_calendar_days: int = 7,
event_types: tuple[str, ...] = ("earnings_release", "guidance_update"),
) -> dict[str, dict[str, dict]]:
calls["count"] += 1
return {
"ABC": {
"2026-01-06": {
"event_flag": True,
"event_score": 1.0,
"prior_event_candidates": [
{
"event_date": "2026-01-05",
"event_type": "earnings_release",
"days_ago": 1,
}
],
}
}
}
monkeypatch.setattr(run_mod, "_fetch_prior_event_features_db_live", fake_fetch_live)
result = asyncio.run(
run_mod._prefetch_prior_event_features_db(
tickers,
trading_days,
lookback_calendar_days=10,
event_types=event_types,
snapshot_cache=cache,
require_event_details=True,
)
)
assert calls["count"] == 1
assert result["ABC"]["2026-01-06"]["prior_event_candidates"][0]["days_ago"] == 1
def test_apply_orb_prior_event_decay_features_scores_recency_and_type() -> None:
params = ORBStrategyParams(
prior_event_decay_half_life_days=2,
prior_event_guidance_score_scale=0.5,
)
features = {
"ABC": {
"2026-01-08": {
"event_flag": True,
"event_score": 1.0,
"prior_event_candidates": [
{
"event_date": "2026-01-07",
"event_type": "guidance_update",
"days_ago": 1,
},
{
"event_date": "2026-01-06",
"event_type": "earnings_release",
"days_ago": 2,
},
],
}
}
}
result = _apply_orb_prior_event_decay_features(features, params)
payload = result["ABC"]["2026-01-08"]
assert payload["event_flag"] is True
assert payload["event_score"] == 0.707107
assert payload["prior_event_decay_score"] == 0.707107
assert payload["prior_event_best_event_type"] == "earnings_release"
assert payload["prior_event_best_days_ago"] == 2
def test_apply_orb_prior_event_decay_features_can_gate_weak_stale_events() -> None:
params = ORBStrategyParams(
prior_event_decay_half_life_days=2,
prior_event_decay_min_score=0.8,
)
features = {
"ABC": {
"2026-01-08": {
"event_flag": True,
"event_score": 1.0,
"prior_event_candidates": [
{
"event_date": "2026-01-05",
"event_type": "earnings_release",
"days_ago": 3,
}
],
}
}
}
result = _apply_orb_prior_event_decay_features(features, params)
payload = result["ABC"]["2026-01-08"]
assert payload["event_flag"] is False
assert payload["event_score"] == 0.0
assert payload["prior_event_decay_score"] == 0.5
def test_build_intraday_data_provenance_reports_prior_event_snapshot_id() -> None:
config = IntradayConfig(
strategy_mode="orb",
orb_strategy=ORBStrategyParams(
weight_event_catalyst=0.12,
prior_event_lookback_days=10,
prior_event_types=["earnings_release", "guidance_update"],
daily_bar_snapshot_id="orb_daily_v46_20260423",
prior_event_snapshot_id="orb_pead_d10_v46_20260423",
),
)
provenance = _build_intraday_data_provenance(config)
assert provenance == {
"daily_bars": {
"enabled": True,
"snapshot_id": "orb_daily_v46_20260423",
"overlay_enabled": False,
},
"prior_event": {
"enabled": True,
"snapshot_id": "orb_pead_d10_v46_20260423",
"lookback_calendar_days": 10,
"event_types": ["earnings_release", "guidance_update"],
}
}
def test_build_daily_cache_uses_read_only_snapshot_by_default(tmp_path) -> None:
config = IntradayConfig(
strategy_mode="orb",
orb_strategy=ORBStrategyParams(daily_bar_snapshot_id="snap1"),
cache=CacheParams(dir=str(tmp_path / "intraday")),
)
cache = _build_daily_cache(config)
assert isinstance(cache, ReadOnlyDailyBarCache)
assert not isinstance(cache, LayeredDailyBarCache)
assert str(cache._cache._root).endswith("daily_snapshots/snap1")
def test_build_daily_cache_uses_overlay_only_when_enabled(tmp_path) -> None:
config = IntradayConfig(
strategy_mode="orb",
orb_strategy=ORBStrategyParams(
daily_bar_snapshot_id="snap1",
daily_bar_snapshot_overlay_enabled=True,
),
cache=CacheParams(dir=str(tmp_path / "intraday")),
)
cache = _build_daily_cache(config)
assert isinstance(cache, LayeredDailyBarCache)
def test_load_ticker_sectors_with_oracle_backfills_missing_cache(tmp_path, monkeypatch) -> None:
cache_path = tmp_path / "cache" / "sector_cache.json"
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text('{"TSLA": "Consumer Cyclical"}')
monkeypatch.setattr(
run_mod,
"get_settings",
lambda: SimpleNamespace(data_root=str(tmp_path)),
)
class FakeCompanyService:
def __init__(self, client) -> None:
self.client = client
async def get_company(self, symbol: str):
if symbol == "CPRX":
return SimpleNamespace(
sector="Healthcare",
industry="Biotechnology",
exchange="NASDAQ",
market_cap=2_979_108_098.0,
)
raise AssertionError(f"unexpected symbol {symbol}")
monkeypatch.setattr(run_mod, "CompanyService", FakeCompanyService)
result = asyncio.run(
run_mod._load_ticker_sectors_with_oracle(["TSLA", "CPRX"], client=object())
)
assert result == {
"TSLA": "Consumer Cyclical",
"CPRX": "Healthcare",
}
assert cache_path.exists()
assert '"CPRX": "Healthcare"' in cache_path.read_text()
def test_load_ticker_sectors_with_oracle_skips_placeholder_metadata(tmp_path, monkeypatch) -> None:
cache_path = tmp_path / "cache" / "sector_cache.json"
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text("{}")
monkeypatch.setattr(
run_mod,
"get_settings",
lambda: SimpleNamespace(data_root=str(tmp_path)),
)
class FakeCompanyService:
def __init__(self, client) -> None:
self.client = client
async def get_company(self, symbol: str):
return SimpleNamespace(
sector="Technology",
industry="Software",
exchange=None,
market_cap=None,
)
monkeypatch.setattr(run_mod, "CompanyService", FakeCompanyService)
result = asyncio.run(
run_mod._load_ticker_sectors_with_oracle(["AVGO"], client=object())
)
assert result == {"AVGO": "UNKNOWN"}
assert cache_path.read_text() == "{}"
def test_should_use_recent_live_scan_only_for_small_recent_windows(monkeypatch) -> None:
monkeypatch.setattr(
run_mod,
"_latest_backtest_date",
lambda now_et=None: datetime(2026, 4, 16, tzinfo=ZoneInfo("America/New_York")).date(),
)
strategy = StrategyParams(recent_live_scan_days=5)
assert _should_use_recent_live_scan(strategy, ["2026-04-15", "2026-04-16"]) is True
assert _should_use_recent_live_scan(
strategy,
["2026-04-09", "2026-04-10", "2026-04-13", "2026-04-14", "2026-04-15", "2026-04-16"],
) is False
assert _should_use_recent_live_scan(strategy, ["2026-03-01"]) is False
def test_momentum_strategy_uses_intraday_event_weight_for_fetch_activation() -> None:
strategy = StrategyParams(candidate_intraday_weight_event_score=0.05)
assert _momentum_strategy_uses_catalyst(strategy) is True
def test_momentum_strategy_uses_seed_event_overlay_for_candidate_stage_catalyst() -> None:
strategy = StrategyParams(candidate_seed_event_overlay_slots=2)
assert _momentum_strategy_uses_catalyst(strategy) is True
assert _momentum_strategy_uses_candidate_stage_catalyst(strategy) is True
def test_momentum_strategy_uses_candidate_event_type_filter_for_fetch_activation() -> None:
strategy = StrategyParams(candidate_allowed_event_types=["earnings_release"])
assert _momentum_strategy_uses_catalyst(strategy) is True
assert _momentum_strategy_uses_candidate_stage_catalyst(strategy) is True
def test_momentum_strategy_uses_event_reserve_and_event_sleeve_for_fetch_activation() -> None:
reserve_strategy = StrategyParams(candidate_intraday_event_reserve_slots=1)
sleeve_strategy = StrategyParams(use_event_sleeve=True, event_weight=0.1)
event_day_liquid_strategy = StrategyParams(
use_event_day_liquid_sleeve=True,
event_day_liquid_capital_fraction=0.1,
event_day_liquid_max_positions=1,
)
assert _momentum_strategy_uses_catalyst(reserve_strategy) is True
assert _momentum_strategy_uses_candidate_stage_catalyst(reserve_strategy) is False
assert _momentum_strategy_uses_catalyst(sleeve_strategy) is True
assert _momentum_strategy_uses_catalyst(event_day_liquid_strategy) is True
def test_momentum_strategy_uses_intraday_attention_weight_for_fetch_activation() -> None:
strategy = StrategyParams(candidate_intraday_weight_attention_news=0.03)
assert _momentum_strategy_uses_attention(strategy) is True
def test_momentum_strategy_requires_regime_ticker_daily_for_gap_meta_layer() -> None:
strategy = StrategyParams(regime_size_scale_low=-0.01)
assert _momentum_strategy_requires_regime_ticker_daily(strategy) is True
def test_momentum_strategy_uses_sector_metadata_and_proxy_fetch_for_overlay_engines() -> None:
cluster_strategy = StrategyParams(use_liquid_cluster_engine=True)
etf_strategy = StrategyParams(
use_sector_etf_sleeve=True,
sector_etf_capital_fraction=0.2,
sector_etf_max_positions=1,
)
assert _momentum_strategy_uses_daily_enrichment(cluster_strategy) is True
assert _momentum_strategy_uses_sector_labels(cluster_strategy) is True
assert _momentum_strategy_uses_sector_proxies(cluster_strategy) is False
assert _momentum_strategy_uses_sector_labels(etf_strategy) is True
assert _momentum_strategy_uses_sector_proxies(etf_strategy) is True
def test_momentum_intraday_seed_candidates_only_apply_signal_filters_in_final_pass() -> None:
daily_bars = {
"AAA": [
{"date": "2026-01-05", "open": 10.0},
],
"BBB": [
{"date": "2026-01-05", "open": 10.0},
],
}
enrichment = {
"AAA": {
"2026-01-05": {
"gap_pct": 0.03,
"event_flag": True,
"event_score": 1.0,
"ret_5d": 0.01,
"entropy_20d": 0.4,
"avg_dollar_vol_30d": 1_000_000.0,
"atr_14": 1.0,
}
},
"BBB": {
"2026-01-05": {
"gap_pct": 0.04,
"event_flag": False,
"event_score": 0.0,
"ret_5d": 0.01,
"entropy_20d": 0.4,
"avg_dollar_vol_30d": 1_000_000.0,
"atr_14": 1.0,
}
},
}
strategy = StrategyParams(
candidate_source_mode="intraday_first",
candidate_seed_threshold=0.02,
candidate_seed_max_per_day=1,
candidate_require_event_flag=True,
candidate_weight_event_score=1.0,
)
preliminary = _momentum_intraday_seed_candidates(
daily_bars,
["2026-01-05"],
enrichment,
strategy,
default_threshold=0.02,
use_signal_features=False,
)
final_seed = _momentum_intraday_seed_candidates(
daily_bars,
["2026-01-05"],
enrichment,
strategy,
default_threshold=0.02,
use_signal_features=True,
)
assert preliminary == {"2026-01-05": ["BBB"]}
assert final_seed == {"2026-01-05": ["AAA"]}
def test_recent_intraday_first_candidates_uses_intraday_leaders_without_static_universe() -> None:
strategy = StrategyParams(
entry_minutes_after_open=10,
confirmation_minutes_after_entry=5,
min_entry_volume=250000,
min_entry_dollar_volume=2_000_000,
top_n=8,
recent_live_scan_max_candidates_per_day=10,
)
day = "2026-04-16"
bars = {
day: {
"XNDU": [
{"timestamp": "2026-04-16T13:30:00+00:00", "open": 2.00, "high": 2.06, "low": 1.98, "close": 2.05, "volume": 150000},
{"timestamp": "2026-04-16T13:35:00+00:00", "open": 2.05, "high": 2.12, "low": 2.04, "close": 2.11, "volume": 175000},
{"timestamp": "2026-04-16T13:40:00+00:00", "open": 2.11, "high": 2.15, "low": 2.10, "close": 2.14, "volume": 180000},
{"timestamp": "2026-04-16T13:45:00+00:00", "open": 2.14, "high": 2.20, "low": 2.13, "close": 2.19, "volume": 200000},
{"timestamp": "2026-04-16T13:50:00+00:00", "open": 2.19, "high": 2.24, "low": 2.18, "close": 2.22, "volume": 210000},
],
"SLOW": [
{"timestamp": "2026-04-16T13:30:00+00:00", "open": 20.00, "high": 20.01, "low": 19.95, "close": 19.98, "volume": 50000},
{"timestamp": "2026-04-16T13:35:00+00:00", "open": 19.98, "high": 20.00, "low": 19.90, "close": 19.95, "volume": 50000},
{"timestamp": "2026-04-16T13:40:00+00:00", "open": 19.95, "high": 19.99, "low": 19.92, "close": 19.97, "volume": 50000},
{"timestamp": "2026-04-16T13:45:00+00:00", "open": 19.97, "high": 19.98, "low": 19.94, "close": 19.96, "volume": 50000},
{"timestamp": "2026-04-16T13:50:00+00:00", "open": 19.96, "high": 19.97, "low": 19.93, "close": 19.95, "volume": 50000},
{"timestamp": "2026-04-16T13:55:00+00:00", "open": 19.95, "high": 19.96, "low": 19.92, "close": 19.94, "volume": 50000},
],
"LIQUID": [
{"timestamp": "2026-04-16T13:30:00+00:00", "open": 400.00, "high": 401.00, "low": 399.00, "close": 400.20, "volume": 80000},
{"timestamp": "2026-04-16T13:35:00+00:00", "open": 400.20, "high": 401.20, "low": 400.10, "close": 400.80, "volume": 90000},
{"timestamp": "2026-04-16T13:40:00+00:00", "open": 400.80, "high": 402.50, "low": 400.70, "close": 402.20, "volume": 120000},
{"timestamp": "2026-04-16T13:45:00+00:00", "open": 402.20, "high": 403.20, "low": 401.90, "close": 402.80, "volume": 130000},
{"timestamp": "2026-04-16T13:50:00+00:00", "open": 402.80, "high": 404.00, "low": 402.70, "close": 403.60, "volume": 140000},
{"timestamp": "2026-04-16T13:55:00+00:00", "open": 403.60, "high": 404.20, "low": 403.40, "close": 404.00, "volume": 150000},
],
"MEGA": [
{"timestamp": "2026-04-16T13:30:00+00:00", "open": 500.00, "high": 500.80, "low": 499.50, "close": 500.10, "volume": 120000},
{"timestamp": "2026-04-16T13:35:00+00:00", "open": 500.10, "high": 500.90, "low": 500.00, "close": 500.40, "volume": 140000},
{"timestamp": "2026-04-16T13:40:00+00:00", "open": 500.40, "high": 501.10, "low": 500.20, "close": 500.70, "volume": 150000},
{"timestamp": "2026-04-16T13:45:00+00:00", "open": 500.70, "high": 501.30, "low": 500.50, "close": 500.90, "volume": 160000},
{"timestamp": "2026-04-16T13:50:00+00:00", "open": 500.90, "high": 501.50, "low": 500.80, "close": 501.20, "volume": 180000},
{"timestamp": "2026-04-16T13:55:00+00:00", "open": 501.20, "high": 501.70, "low": 501.00, "close": 501.40, "volume": 190000},
],
}
}
candidates = _recent_intraday_first_candidates(bars, [day], strategy)
assert "XNDU" in candidates[day]
assert "LIQUID" in candidates[day]
assert "MEGA" in candidates[day]
assert "SLOW" not in candidates[day]
def test_recent_intraday_first_scan_only_applies_to_latest_safe_day(monkeypatch) -> None:
assert _should_use_recent_intraday_first_scan(["2026-04-16"]) is True
assert _should_use_recent_intraday_first_scan(["2026-04-15"]) is True
def test_recent_intraday_first_scan_treats_latest_completed_weekday_as_recent(monkeypatch) -> None:
assert _should_use_recent_intraday_first_scan(["2026-04-17"]) is True
def test_recent_intraday_first_scan_false_for_empty_window() -> None:
assert _should_use_recent_intraday_first_scan([]) is False
def test_liquid_seed_overlay_adds_liquid_name_without_replacing_base_seed() -> None:
strategy = StrategyParams(
candidate_seed_liquid_overlay_slots=1,
candidate_seed_liquid_min_gap_pct=0.005,
candidate_seed_liquid_max_gap_pct=0.03,
candidate_seed_liquid_min_avg_dollar_vol_30d=500_000_000.0,
candidate_seed_liquid_min_ret_5d=0.0,
candidate_seed_liquid_max_entropy_20d=0.90,
)
daily_bars = {
"BASE": [{"date": "2026-04-15", "open": 50.0}],
"TSLA": [{"date": "2026-04-15", "open": 250.0}],
"NOPE": [{"date": "2026-04-15", "open": 30.0}],
}
enrichment = {
"BASE": {"2026-04-15": {"gap_pct": 0.03, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.40}},
"TSLA": {"2026-04-15": {"gap_pct": 0.007, "avg_dollar_vol_30d": 1_200_000_000.0, "ret_5d": 0.05, "entropy_20d": 0.70}},
"NOPE": {"2026-04-15": {"gap_pct": 0.009, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.05, "entropy_20d": 0.50}},
}
candidates = {"2026-04-15": ["BASE"]}
augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay(
candidates,
daily_bars,
["2026-04-15"],
enrichment,
strategy,
)
assert augmented["2026-04-15"] == ["BASE", "TSLA"]
def test_strategy_for_recent_live_scan_applies_only_recent_overrides() -> None:
strategy = StrategyParams(
top_n=8,
min_morning_gain_pct=0.015,
max_morning_gain_pct=0.06,
min_confirmation_return_pct=0.005,
max_gap_pct=0.055,
use_slow_ignite_sleeve=False,
recent_live_scan_top_n=10,
recent_live_scan_min_morning_gain_pct=0.01,
recent_live_scan_max_morning_gain_pct=0.05,
recent_live_scan_min_confirmation_return_pct=0.001,
recent_live_scan_max_gap_pct=0.04,
recent_live_scan_max_entropy_20d=0.9,
recent_live_scan_use_slow_ignite_sleeve=True,
recent_live_scan_slow_ignite_weight=0.2,
recent_live_scan_use_liquid_largecap_sleeve=True,
recent_live_scan_liquid_largecap_weight=0.3,
recent_live_scan_liquid_largecap_min_gain_pct=0.004,
recent_live_scan_liquid_largecap_max_gain_pct=0.02,
recent_live_scan_liquid_largecap_min_confirmation_return_pct=0.0005,
recent_live_scan_liquid_largecap_min_entry_dollar_volume=50_000_000.0,
recent_live_scan_liquid_largecap_min_avg_dollar_vol_30d=500_000_000.0,
recent_live_scan_liquid_largecap_max_entropy_20d=0.9,
)
unchanged = _strategy_for_recent_live_scan(strategy, recent_live_scan=False)
recent = _strategy_for_recent_live_scan(strategy, recent_live_scan=True)
assert unchanged.top_n == 8
assert unchanged.min_morning_gain_pct == 0.015
assert unchanged.max_morning_gain_pct == 0.06
assert unchanged.max_gap_pct == 0.055
assert unchanged.use_slow_ignite_sleeve is False
assert recent.top_n == 10
assert recent.min_morning_gain_pct == 0.01
assert recent.max_morning_gain_pct == 0.05
assert recent.min_confirmation_return_pct == 0.001
assert recent.max_gap_pct == 0.04
assert recent.max_entropy_20d == 0.9
assert recent.use_slow_ignite_sleeve is True
assert recent.use_liquid_largecap_sleeve is True
assert recent.liquid_largecap_weight == 0.3
assert recent.liquid_largecap_min_gain_pct == 0.004
assert recent.liquid_largecap_max_gain_pct == 0.02
assert recent.liquid_largecap_min_confirmation_return_pct == 0.0005
assert recent.slow_ignite_weight == 0.2
def test_augment_momentum_seed_candidates_with_leader_overlay_adds_negative_gap_continuation_name() -> None:
strategy = StrategyParams(
candidate_seed_leader_overlay_slots=1,
candidate_seed_leader_min_gap_pct=-0.01,
candidate_seed_leader_max_gap_pct=0.01,
candidate_seed_leader_min_avg_dollar_vol_30d=500_000_000.0,
candidate_seed_leader_min_ret_5d=0.15,
candidate_seed_leader_min_atr_pct=0.05,
candidate_seed_leader_max_entropy_20d=0.75,
)
daily_bars = {
"BASE": [{"date": "2026-04-20", "open": 50.0}],
"CAR": [{"date": "2026-04-20", "open": 491.26}],
"NOPE": [{"date": "2026-04-20", "open": 300.0}],
}
enrichment = {
"BASE": {"2026-04-20": {"gap_pct": 0.03, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.40, "atr_14": 2.0}},
"CAR": {"2026-04-20": {"gap_pct": -0.005, "avg_dollar_vol_30d": 730_000_000.0, "ret_5d": 0.33, "entropy_20d": 0.44, "atr_14": 51.8}},
"NOPE": {"2026-04-20": {"gap_pct": 0.0, "avg_dollar_vol_30d": 450_000_000.0, "ret_5d": 0.18, "entropy_20d": 0.55, "atr_14": 5.0}},
}
candidates = {"2026-04-20": ["BASE"]}
augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay(
candidates,
daily_bars,
["2026-04-20"],
enrichment,
strategy,
)
assert augmented["2026-04-20"] == ["BASE", "CAR"]
def test_augment_momentum_seed_candidates_with_moderate_liquid_overlay_adds_followthrough_name() -> None:
strategy = StrategyParams(
candidate_seed_moderate_liquid_overlay_slots=1,
candidate_seed_moderate_liquid_min_gap_pct=0.005,
candidate_seed_moderate_liquid_max_gap_pct=0.025,
candidate_seed_moderate_liquid_min_avg_dollar_vol_30d=250_000_000.0,
candidate_seed_moderate_liquid_max_avg_dollar_vol_30d=2_000_000_000.0,
candidate_seed_moderate_liquid_max_entropy_20d=0.86,
)
daily_bars = {
"BASE": [{"date": "2026-03-13", "open": 50.0}],
"TER": [{"date": "2026-03-13", "open": 100.0}],
"MEGA": [{"date": "2026-03-13", "open": 250.0}],
}
enrichment = {
"BASE": {"2026-03-13": {"gap_pct": 0.04, "avg_dollar_vol_30d": 150_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.40}},
"TER": {"2026-03-13": {"gap_pct": 0.012, "avg_dollar_vol_30d": 750_000_000.0, "ret_5d": -0.02, "entropy_20d": 0.79}},
"MEGA": {"2026-03-13": {"gap_pct": 0.010, "avg_dollar_vol_30d": 5_000_000_000.0, "ret_5d": 0.01, "entropy_20d": 0.50}},
}
candidates = {"2026-03-13": ["BASE"]}
augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay(
candidates,
daily_bars,
["2026-03-13"],
enrichment,
strategy,
)
assert augmented["2026-03-13"] == ["BASE", "TER"]
assert overlay == {"2026-03-13": {"TER"}}
assert enrichment["TER"]["2026-03-13"]["candidate_seed_moderate_liquid_overlay"] is True
def test_augment_momentum_seed_candidates_with_event_overlay_adds_actual_catalyst_name() -> None:
strategy = StrategyParams(
candidate_seed_event_overlay_slots=1,
candidate_seed_event_min_score=0.95,
candidate_seed_event_min_gap_pct=-0.02,
candidate_seed_event_max_gap_pct=0.08,
candidate_seed_event_min_avg_dollar_vol_30d=100_000_000.0,
candidate_seed_event_max_entropy_20d=0.80,
)
daily_bars = {
"BASE": [{"date": "2026-02-10", "open": 50.0}],
"CAT": [{"date": "2026-02-10", "open": 42.0}],
"WEAK": [{"date": "2026-02-10", "open": 30.0}],
}
enrichment = {
"BASE": {"2026-02-10": {"gap_pct": 0.04, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.03, "entropy_20d": 0.40}},
"CAT": {"2026-02-10": {"event_flag": True, "event_score": 1.0, "gap_pct": 0.01, "avg_dollar_vol_30d": 350_000_000.0, "ret_5d": 0.08, "entropy_20d": 0.55}},
"WEAK": {"2026-02-10": {"event_flag": True, "event_score": 0.60, "gap_pct": 0.015, "avg_dollar_vol_30d": 120_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.50}},
}
candidates = {"2026-02-10": ["BASE"]}
augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay(
candidates,
daily_bars,
["2026-02-10"],
enrichment,
strategy,
)
assert augmented["2026-02-10"] == ["BASE", "CAT"]
assert overlay == {"2026-02-10": {"CAT"}}
def test_augment_momentum_seed_candidates_with_ownership_overlay_adds_pit_owner_name() -> None:
strategy = StrategyParams(
candidate_seed_ownership_overlay_slots=1,
candidate_seed_ownership_initial_only=True,
candidate_seed_ownership_min_strength_score=3.0,
candidate_seed_ownership_min_gap_pct=-0.02,
candidate_seed_ownership_max_gap_pct=0.08,
candidate_seed_ownership_min_avg_dollar_vol_30d=100_000_000.0,
candidate_seed_ownership_max_entropy_20d=0.85,
)
daily_bars = {
"BASE": [{"date": "2026-02-10", "open": 50.0}],
"OWNER": [{"date": "2026-02-10", "open": 42.0}],
"AMEND": [{"date": "2026-02-10", "open": 30.0}],
}
enrichment = {
"BASE": {
"2026-02-10": {
"gap_pct": 0.04,
"avg_dollar_vol_30d": 200_000_000.0,
"ret_5d": 0.03,
"entropy_20d": 0.40,
}
},
"OWNER": {
"2026-02-10": {
"ownership_13dg_flag": True,
"ownership_13dg_initial_flag": True,
"ownership_13dg_strength_score": 4.0,
"ownership_13dg_days_since": 12,
"gap_pct": 0.01,
"avg_dollar_vol_30d": 350_000_000.0,
"ret_5d": 0.08,
"entropy_20d": 0.55,
}
},
"AMEND": {
"2026-02-10": {
"ownership_13dg_flag": True,
"ownership_13dg_initial_flag": False,
"ownership_13dg_strength_score": 5.0,
"ownership_13dg_days_since": 3,
"gap_pct": 0.015,
"avg_dollar_vol_30d": 400_000_000.0,
"ret_5d": 0.02,
"entropy_20d": 0.50,
}
},
}
candidates = {"2026-02-10": ["BASE"]}
augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay(
candidates,
daily_bars,
["2026-02-10"],
enrichment,
strategy,
)
assert augmented["2026-02-10"] == ["BASE", "OWNER"]
assert overlay == {"2026-02-10": {"OWNER"}}
assert enrichment["OWNER"]["2026-02-10"]["candidate_seed_ownership_overlay"] is True
def test_retain_recent_intraday_shortlist_preserves_intraday_candidates() -> None:
candidates = {"2026-04-15": ["TSLA", "XNDU", "AXTI"]}
daily_bars = {"TSLA": [{"date": "2026-04-15"}], "AXTI": [{"date": "2026-04-15"}]}
retained = _retain_recent_intraday_shortlist(
candidates,
daily_bars,
require_daily_features=True,
)
assert retained == {"2026-04-15": ["TSLA", "AXTI"]}
def test_load_config_rejects_extends(tmp_path) -> None:
base = tmp_path / "base.yaml"
child = tmp_path / "child.yaml"
base.write_text(
"""
strategy_mode: orb
orb_strategy:
engine_family: gainers_leader
min_rvol: 3.0
nofill_vwap_reclaim_enabled: true
universe:
source: broad
backtest:
lookback_trading_days: 200
""",
encoding="utf-8",
)
child.write_text(
"""
extends: base.yaml
orb_strategy:
min_rvol: 4.0
soft_day_vwap_reclaim_allowed_reason_parts:
- hard_breadth
""",
encoding="utf-8",
)
with pytest.raises(ValueError, match="extends.*no longer supported"):
load_config(str(child))
def test_checked_in_intraday_strategies_do_not_use_extends() -> None:
strategy_dir = Path("configs/intraday/strategies")
offenders = []
for path in sorted(strategy_dir.glob("*.yaml")):
text = path.read_text(encoding="utf-8")
if any(line.startswith("extends:") for line in text.splitlines()):
offenders.append(path.name)
assert offenders == []
def test_momentum_strategy_defaults_to_simple_returns_and_cli_can_override() -> None:
config = load_config(
"configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml"
)
assert config.strategy.compound_returns is False
args = SimpleNamespace(
days=None,
start=None,
end=None,
universe=None,
top_n=None,
stop_loss=None,
entry_min=None,
exit_min=None,
min_gain=None,
no_cache=False,
verbose=False,
output_dir=None,
strategy="momentum",
compound_returns=True,
initial_capital=None,
)
updated = apply_cli_overrides(config, args)
assert updated.strategy.compound_returns is True
def test_orb_cli_compound_override_disables_daily_budget_reset_by_default() -> None:
config = IntradayConfig(
strategy_mode="orb",
strategy=StrategyParams(),
orb_strategy=ORBStrategyParams(
compound_returns=False,
daily_budget_reset=True,
),
)
args = SimpleNamespace(
days=None,
start=None,
end=None,
universe=None,
top_n=None,
stop_loss=None,
entry_min=None,
exit_min=None,
min_gain=None,
no_cache=False,
verbose=False,
output_dir=None,
strategy="orb",
compound_returns=True,
daily_budget_reset=None,
initial_capital=None,
)
updated = apply_cli_overrides(config, args)
assert updated.orb_strategy is not None
assert updated.orb_strategy.compound_returns is True
assert updated.orb_strategy.daily_budget_reset is False
assert updated.strategy.compound_returns is True
assert updated.strategy.daily_budget_reset is False
def test_orb_cli_daily_reset_override_disables_compound_by_default() -> None:
config = IntradayConfig(
strategy_mode="orb",
strategy=StrategyParams(),
orb_strategy=ORBStrategyParams(
compound_returns=True,
daily_budget_reset=False,
),
)
args = SimpleNamespace(
days=None,
start=None,
end=None,
universe=None,
top_n=None,
stop_loss=None,
entry_min=None,
exit_min=None,
min_gain=None,
no_cache=False,
verbose=False,
output_dir=None,
strategy="orb",
compound_returns=None,
daily_budget_reset=True,
initial_capital=None,
)
updated = apply_cli_overrides(config, args)
assert updated.orb_strategy is not None
assert updated.orb_strategy.compound_returns is False
assert updated.orb_strategy.daily_budget_reset is True
assert updated.strategy.compound_returns is False
assert updated.strategy.daily_budget_reset is True