"""Integration tests for the full backtest pipeline (no DB/HTTP — uses SnapshotStore directly)."""
from __future__ import annotations

import datetime as dt
import json
from pathlib import Path
from zoneinfo import ZoneInfo

import pytest
import pyarrow as pa
import pyarrow.parquet as pq

_UTC = ZoneInfo("UTC")


def _build_synthetic_store() -> object:
    """Build a SnapshotStore with synthetic data for end-to-end testing."""
    from libs.backtest.snapshot_store import SnapshotStore

    # 5 trading days, 2 symbols
    dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8, 9]]

    candidates = {
        dt.date(2026, 1, 5): [
            {
                "event_id": "EVT::001",
                "symbol": "AAPL",
                "execution_date": dt.date(2026, 1, 5),
                "entry_date": "2026-01-05",
                "entry_price": 150.0,
                "score": 0.85,
                "sector": "Technology",
                "event_type": "earnings",
                "event_timestamp": "2026-01-02T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-02",
                "avg_dollar_volume": 5_000_000.0,
                "atr_14": 3.0,
            },
        ],
        dt.date(2026, 1, 6): [
            {
                "event_id": "EVT::002",
                "symbol": "MSFT",
                "execution_date": dt.date(2026, 1, 6),
                "entry_date": "2026-01-06",
                "entry_price": 300.0,
                "score": 0.70,
                "sector": "Technology",
                "event_type": "guidance",
                "event_timestamp": "2026-01-05T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-05",
                "avg_dollar_volume": 10_000_000.0,
                "atr_14": 5.0,
            },
        ],
    }

    bars = {
        "AAPL": {
            dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 150.0, "high": 160.0, "low": 148.0, "close": 158.0, "volume": 1_000_000},
            dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 158.0, "high": 170.0, "low": 155.0, "close": 165.0, "volume": 900_000},
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 165.0, "high": 175.0, "low": 160.0, "close": 170.0, "volume": 800_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 170.0, "high": 180.0, "low": 165.0, "close": 175.0, "volume": 750_000},
            dt.date(2026, 1, 9): {"date": dt.date(2026, 1, 9), "open": 175.0, "high": 185.0, "low": 170.0, "close": 180.0, "volume": 700_000},
        },
        "MSFT": {
            dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 300.0, "high": 305.0, "low": 280.0, "close": 282.0, "volume": 500_000},
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 282.0, "high": 290.0, "low": 270.0, "close": 272.0, "volume": 480_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 272.0, "high": 280.0, "low": 260.0, "close": 265.0, "volume": 450_000},
            dt.date(2026, 1, 9): {"date": dt.date(2026, 1, 9), "open": 265.0, "high": 270.0, "low": 255.0, "close": 258.0, "volume": 420_000},
        },
    }

    return SnapshotStore(
        candidates_by_exec_date=candidates,
        bars_by_symbol_date=bars,
    )


def _build_multi_engine_store() -> object:
    from libs.backtest.snapshot_store import SnapshotStore

    candidates = {
        dt.date(2026, 1, 7): [
            {
                "event_id": "EVT::SD::SHORT",
                "symbol": "NFLX",
                "execution_date": dt.date(2026, 1, 7),
                "entry_date": "2026-01-07",
                "event_date": "2026-01-06",
                "event_close": 400.0,
                "entry_price": 399.0,
                "score": 0.90,
                "sector": "Communication Services",
                "event_type": "earnings_release",
                "event_timestamp": "2026-01-06T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-06",
                "reaction_day_return": -0.12,
                "avg_dollar_volume": 8_000_000.0,
                "atr_14": 4.0,
            },
            {
                "event_id": "EVT::AC::LONG",
                "symbol": "AMD",
                "execution_date": dt.date(2026, 1, 7),
                "entry_date": "2026-01-07",
                "event_date": "2026-01-06",
                "event_close": 122.0,
                "entry_price": 123.0,
                "score": 0.88,
                "sector": "Technology",
                "event_type": "earnings_release",
                "event_timestamp": "2026-01-06T21:00:00+00:00",
                "filing_time_bucket": "post_market",
                "reaction_date": "2026-01-07",
                "reaction_day_return": 0.14,
                "avg_dollar_volume": 9_000_000.0,
                "atr_14": 3.0,
            },
        ],
    }

    bars = {
        "NFLX": {
            dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 395.0, "high": 405.0, "low": 390.0, "close": 400.0, "volume": 1_200_000},
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 390.0, "high": 392.0, "low": 380.0, "close": 382.0, "volume": 1_100_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 382.0, "high": 384.0, "low": 370.0, "close": 372.0, "volume": 1_000_000},
        },
        "AMD": {
            dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 123.0, "high": 130.0, "low": 122.0, "close": 129.0, "volume": 1_500_000},
            dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 129.0, "high": 135.0, "low": 128.0, "close": 134.0, "volume": 1_300_000},
            dt.date(2026, 1, 9): {"date": dt.date(2026, 1, 9), "open": 134.0, "high": 138.0, "low": 133.0, "close": 137.0, "volume": 1_250_000},
        },
    }

    return SnapshotStore(candidates_by_exec_date=candidates, bars_by_symbol_date=bars)


def _make_config(strategy_engines=None):
    from libs.backtest.domain import (
        BacktestConfig,
        ExecutionConfig,
        ReportingConfig,
        RiskConfig,
        SignalConfig,
        UniverseConfig,
    )

    return BacktestConfig(
        strategy_name="test_strategy",
        dataset_snapshot_id="test_snapshot",
        universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
        signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5),
        risk=RiskConfig(
            per_trade_risk_pct=0.01,
            max_daily_new_risk_pct=0.05,
            max_positions=10,
            max_positions_per_sector=5,
        ),
        execution=ExecutionConfig(
            entry_fill_model="next_open",
            exit_fill_model="daily_bar_approximation",
            slippage_bps_base=10.0,
            commission_per_share=0.005,
            same_bar_priority="stop_first_conservative",
            max_holding_days=10,
        ),
        reporting=ReportingConfig(
            write_trade_blotter=True,
            write_equity_curve=True,
            write_metrics_summary=True,
            generate_plots=False,
        ),
        strategy_engines=strategy_engines or [],
    )


@pytest.mark.integration
class TestBacktestRunIntegration:
    def test_run_completes(self, tmp_path):
        """Full run completes without error."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        store = _build_synthetic_store()
        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        assert result.run_id.startswith("bt_")
        assert result.total_trading_days >= 0
        assert result.metrics.trade_count >= 0

    def test_engine_allowed_macro_regimes_filters_engine_by_date(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore
        import pyarrow.parquet as pq

        store = SnapshotStore(
            candidates_by_exec_date={
                dt.date(2026, 1, 5): [
                    {
                        "event_id": "EVT::ROFF",
                        "symbol": "AAPL",
                        "execution_date": dt.date(2026, 1, 5),
                        "entry_date": "2026-01-05",
                        "entry_price": 100.0,
                        "score": 0.80,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-02T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-02",
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 3.0,
                        "reaction_day_return": 0.02,
                    },
                ],
                dt.date(2026, 1, 6): [
                    {
                        "event_id": "EVT::RON",
                        "symbol": "MSFT",
                        "execution_date": dt.date(2026, 1, 6),
                        "entry_date": "2026-01-06",
                        "entry_price": 110.0,
                        "score": 0.82,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-05",
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 3.0,
                        "reaction_day_return": 0.02,
                    },
                ],
            },
            bars_by_symbol_date={
                "AAPL": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 104.0, "low": 98.0, "close": 103.0, "volume": 1_000_000},
                    dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 103.0, "high": 105.0, "low": 102.0, "close": 104.0, "volume": 1_000_000},
                },
                "MSFT": {
                    dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 110.0, "high": 114.0, "low": 109.0, "close": 113.0, "volume": 1_000_000},
                    dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 113.0, "high": 115.0, "low": 111.0, "close": 114.0, "volume": 1_000_000},
                },
            },
            macro_by_date={
                dt.date(2026, 1, 5): {
                    "spy_close": 90.0,
                    "spy_sma_20": 100.0,
                    "qqq_close": 180.0,
                    "qqq_sma_20": 200.0,
                },
                dt.date(2026, 1, 6): {
                    "spy_close": 110.0,
                    "spy_sma_20": 100.0,
                    "qqq_close": 210.0,
                    "qqq_sma_20": 200.0,
                },
            },
        )

        manifest = ExperimentManifest(
            experiment_name="macro_gate_engine_test",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="macro_gated_engine",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    allowed_macro_regimes=["risk_on"],
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=0.0),
            signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.10,
                max_positions=5,
                max_positions_per_sector=5,
                macro_regime_enabled=True,
                macro_regime_mode="spy_qqq_scaler",
                macro_regime_neutral_size_scaler=1.0,
                macro_regime_risk_off_size_scaler=1.0,
                macro_regime_risk_off_a_tier_only=False,
                veto_unknown_direction=False,
                veto_bearish_direction=False,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=1,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            event_type_profiles={
                "earnings_release": EventTypeProfile(enabled=True),
            },
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)
        trade_blotter = pq.read_table(tmp_path / result.run_id / "artifacts" / "trade_blotter.parquet").to_pylist()

        assert [row["symbol"] for row in trade_blotter] == ["MSFT"]

    def test_output_files_created(self, tmp_path):
        """All expected output files are written."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        store = _build_synthetic_store()
        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        run_dir = tmp_path / result.run_id
        assert run_dir.exists()
        assert (run_dir / "metadata.json").exists()
        assert (run_dir / "manifest.json").exists()

    def test_same_day_cash_recycle_replaces_stale_core(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        d1 = dt.date(2026, 1, 5)
        d2 = dt.date(2026, 1, 6)
        d3 = dt.date(2026, 1, 7)

        store = SnapshotStore(
            candidates_by_exec_date={
                d1: [
                    {
                        "event_id": "EVT::OLD",
                        "symbol": "OLD",
                        "execution_date": d1,
                        "entry_date": d1.isoformat(),
                        "event_date": d1.isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.60,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": d1.isoformat(),
                        "reaction_day_return": 0.08,
                        "close_location": 0.74,
                        "volume_ratio_20d": 2.1,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
                d2: [
                    {
                        "event_id": "EVT::NEW",
                        "symbol": "NEW",
                        "execution_date": d2,
                        "entry_date": d2.isoformat(),
                        "event_date": d2.isoformat(),
                        "event_close": 50.0,
                        "entry_price": 50.0,
                        "score": 0.80,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": d2.isoformat(),
                        "reaction_day_return": 0.09,
                        "close_location": 0.76,
                        "volume_ratio_20d": 2.2,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
            },
            bars_by_symbol_date={
                "OLD": {
                    d1: {"date": d1, "open": 100.0, "high": 100.0, "low": 99.0, "close": 100.0, "volume": 1_000_000},
                    d2: {"date": d2, "open": 100.0, "high": 102.0, "low": 100.0, "close": 101.0, "volume": 1_000_000},
                    d3: {"date": d3, "open": 101.0, "high": 101.0, "low": 101.0, "close": 101.0, "volume": 1_000_000},
                },
                "NEW": {
                    d2: {"date": d2, "open": 50.0, "high": 50.0, "low": 50.0, "close": 50.0, "volume": 1_000_000},
                    d3: {"date": d3, "open": 50.0, "high": 55.0, "low": 50.0, "close": 55.0, "volume": 1_000_000},
                },
            },
        )

        engine = StrategyEngineConfig(
            engine_id="core",
            event_types=["earnings_release"],
            timing_class="same_day",
            direction="long_only",
            entry_timing_policy="reaction_close",
            score_threshold_override=0.5,
            recycle_on_cash_block=True,
            recycle_min_days_held=1,
            recycle_min_score_delta=0.05,
            recycle_allowed_victim_engine_ids=["core"],
            recycle_positive_pnl_only=True,
            enabled=True,
        )
        manifest = ExperimentManifest(
            experiment_name="test_same_day_recycle",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[engine],
        )
        config = BacktestConfig(
            strategy_name="test_strategy",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
            signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=1.0,
                max_daily_new_risk_pct=1.0,
                max_positions=10,
                max_positions_per_sector=5,
                max_position_value_pct=1.0,
            ),
            execution=ExecutionConfig(max_holding_days=10),
            reporting=ReportingConfig(),
            strategy_engines=[engine],
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=1_000.0)
        result = runner.run(output_root=tmp_path)
        run_dir = tmp_path / result.run_id

        recycle_trades = [trade for trade in runner._closed_trades if trade.exit_reason.value == "RECYCLE"]
        assert recycle_trades
        assert recycle_trades[0].symbol == "OLD"
        assert any(trade.symbol == "NEW" for trade in runner._closed_trades)
        assert result.metrics.trade_count >= 2
        assert (run_dir / "resolved_config.json").exists()
        assert (run_dir / "metrics" / "metrics_summary.json").exists()
        assert (run_dir / "plots").exists()  # empty dir

    def test_next_open_cash_recycle_replaces_stale_next_open_position(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        d1 = dt.date(2026, 1, 5)
        d2 = dt.date(2026, 1, 6)
        d3 = dt.date(2026, 1, 7)

        store = SnapshotStore(
            candidates_by_exec_date={
                d1: [
                    {
                        "event_id": "EVT::OLD::NEXT",
                        "symbol": "OLDN",
                        "execution_date": d1,
                        "entry_date": d1.isoformat(),
                        "event_date": d1.isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.60,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-05T13:00:00+00:00",
                        "filing_time_bucket": "pre_market",
                        "reaction_date": d1.isoformat(),
                        "reaction_day_return": 0.01,
                        "close_location": 0.65,
                        "volume_ratio_20d": 1.2,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
                d2: [
                    {
                        "event_id": "EVT::NEW::NEXT",
                        "symbol": "NEWN",
                        "execution_date": d2,
                        "entry_date": d2.isoformat(),
                        "event_date": d2.isoformat(),
                        "event_close": 50.0,
                        "entry_price": 50.0,
                        "score": 0.80,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-06T13:00:00+00:00",
                        "filing_time_bucket": "pre_market",
                        "reaction_date": d2.isoformat(),
                        "reaction_day_return": 0.01,
                        "close_location": 0.66,
                        "volume_ratio_20d": 1.2,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
            },
            bars_by_symbol_date={
                "OLDN": {
                    d1: {"date": d1, "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 1_000_000},
                    d2: {"date": d2, "open": 100.0, "high": 102.0, "low": 100.0, "close": 101.0, "volume": 1_000_000},
                    d3: {"date": d3, "open": 101.0, "high": 101.0, "low": 100.0, "close": 100.5, "volume": 1_000_000},
                },
                "NEWN": {
                    d2: {"date": d2, "open": 50.0, "high": 50.0, "low": 50.0, "close": 50.0, "volume": 1_000_000},
                    d3: {"date": d3, "open": 50.0, "high": 54.0, "low": 50.0, "close": 53.0, "volume": 1_000_000},
                },
            },
        )

        engine = StrategyEngineConfig(
            engine_id="next_open_guidance",
            event_types=["earnings_release"],
            timing_class="any",
            direction="long_only",
            entry_timing_policy="next_open",
            score_threshold_override=0.5,
            recycle_on_cash_block=True,
            recycle_min_days_held=1,
            recycle_min_score_delta=0.05,
            recycle_allowed_victim_engine_ids=["next_open_guidance"],
            recycle_positive_pnl_only=True,
            enabled=True,
        )
        manifest = ExperimentManifest(
            experiment_name="test_next_open_recycle",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[engine],
        )
        config = BacktestConfig(
            strategy_name="test_strategy",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
            signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=1.0,
                max_daily_new_risk_pct=1.0,
                max_positions=10,
                max_positions_per_sector=5,
                max_position_value_pct=1.0,
            ),
            execution=ExecutionConfig(max_holding_days=10),
            reporting=ReportingConfig(),
            strategy_engines=[engine],
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=1_000.0)
        result = runner.run(output_root=tmp_path)

        recycle_trades = [trade for trade in runner._closed_trades if trade.exit_reason.value == "RECYCLE"]
        assert recycle_trades
        assert recycle_trades[0].symbol == "OLDN"
        assert any(trade.symbol == "NEWN" for trade in runner._closed_trades)
        assert result.metrics.trade_count >= 2

    def test_next_open_cash_recycle_can_target_stale_cross_engine_victim(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        d1 = dt.date(2026, 1, 5)
        d2 = dt.date(2026, 1, 6)
        d3 = dt.date(2026, 1, 7)
        d4 = dt.date(2026, 1, 8)
        d5 = dt.date(2026, 1, 9)

        store = SnapshotStore(
            candidates_by_exec_date={
                d1: [
                    {
                        "event_id": "EVT::OLD::CROSS",
                        "symbol": "OLDX",
                        "execution_date": d1,
                        "entry_date": d1.isoformat(),
                        "event_date": d1.isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.55,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-05T13:00:00+00:00",
                        "filing_time_bucket": "pre_market",
                        "reaction_date": d1.isoformat(),
                        "reaction_day_return": 0.01,
                        "close_location": 0.60,
                        "volume_ratio_20d": 1.2,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
                d5: [
                    {
                        "event_id": "EVT::NEW::CROSS",
                        "symbol": "NEWX",
                        "execution_date": d5,
                        "entry_date": d5.isoformat(),
                        "event_date": d5.isoformat(),
                        "event_close": 50.0,
                        "entry_price": 50.0,
                        "score": 0.80,
                        "sector": "Technology",
                        "event_type": "other_material_event",
                        "event_direction": "bullish",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-09T13:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": d5.isoformat(),
                        "reaction_day_return": 0.01,
                        "close_location": 0.70,
                        "volume_ratio_20d": 1.2,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
            },
            bars_by_symbol_date={
                "OLDX": {
                    d1: {"date": d1, "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 1_000_000},
                    d2: {"date": d2, "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.2, "volume": 1_000_000},
                    d3: {"date": d3, "open": 100.2, "high": 100.8, "low": 99.8, "close": 100.3, "volume": 1_000_000},
                    d4: {"date": d4, "open": 100.3, "high": 100.7, "low": 100.0, "close": 100.4, "volume": 1_000_000},
                    d5: {"date": d5, "open": 100.4, "high": 100.8, "low": 100.2, "close": 100.5, "volume": 1_000_000},
                },
                "NEWX": {
                    d5: {"date": d5, "open": 50.0, "high": 50.0, "low": 50.0, "close": 50.0, "volume": 1_000_000},
                },
            },
        )

        old_engine = StrategyEngineConfig(
            engine_id="stale_next",
            event_types=["earnings_release"],
            timing_class="any",
            direction="long_only",
            entry_timing_policy="next_open",
            score_threshold_override=0.5,
            enabled=True,
        )
        new_engine = StrategyEngineConfig(
            engine_id="fresh_next",
            event_types=["other_material_event"],
            timing_class="any",
            direction="long_only",
            entry_timing_policy="next_open",
            score_threshold_override=0.5,
            recycle_on_cash_block=True,
            recycle_min_days_held=3,
            recycle_min_score_delta=0.05,
            recycle_allow_any_victim_engine=True,
            recycle_max_victim_fitness=0.45,
            recycle_max_victim_unrealized_r=0.30,
            recycle_positive_pnl_only=True,
            enabled=True,
        )
        manifest = ExperimentManifest(
            experiment_name="test_cross_engine_recycle",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[old_engine, new_engine],
        )
        config = BacktestConfig(
            strategy_name="test_strategy",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
            signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=1.0,
                max_daily_new_risk_pct=1.0,
                max_positions=10,
                max_positions_per_sector=5,
                max_position_value_pct=1.0,
            ),
            execution=ExecutionConfig(max_holding_days=5),
            reporting=ReportingConfig(),
            strategy_engines=[old_engine, new_engine],
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=1_000.0)
        result = runner.run(output_root=tmp_path)

        recycle_trades = [trade for trade in runner._closed_trades if trade.exit_reason.value == "RECYCLE"]
        assert recycle_trades
        assert recycle_trades[0].symbol == "OLDX"
        assert any(trade.symbol == "NEWX" for trade in runner._closed_trades)
        assert result.metrics.trade_count >= 2

    def test_rotation_can_skip_large_unrealized_winner(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        d1 = dt.date(2026, 1, 5)
        d2 = dt.date(2026, 1, 6)
        d3 = dt.date(2026, 1, 7)
        d4 = dt.date(2026, 1, 8)
        d5 = dt.date(2026, 1, 9)

        store = SnapshotStore(
            candidates_by_exec_date={
                d1: [
                    {
                        "event_id": "EVT::OLD::ROT",
                        "symbol": "OLDR",
                        "execution_date": d1,
                        "entry_date": d1.isoformat(),
                        "event_date": d1.isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.60,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": d1.isoformat(),
                        "reaction_day_return": 0.03,
                        "close_location": 0.70,
                        "volume_ratio_20d": 1.5,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
                d5: [
                    {
                        "event_id": "EVT::NEW::ROT",
                        "symbol": "NEWR",
                        "execution_date": d5,
                        "entry_date": d5.isoformat(),
                        "event_date": d5.isoformat(),
                        "event_close": 50.0,
                        "entry_price": 50.0,
                        "score": 0.85,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-09T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": d5.isoformat(),
                        "reaction_day_return": 0.04,
                        "close_location": 0.72,
                        "volume_ratio_20d": 1.6,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 2.0,
                    },
                ],
            },
            bars_by_symbol_date={
                "OLDR": {
                    d1: {"date": d1, "open": 100.0, "high": 100.0, "low": 99.0, "close": 100.0, "volume": 1_000_000},
                    d2: {"date": d2, "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.8, "volume": 1_000_000},
                    d3: {"date": d3, "open": 100.8, "high": 102.5, "low": 100.6, "close": 102.4, "volume": 1_000_000},
                    d4: {"date": d4, "open": 102.4, "high": 103.4, "low": 102.2, "close": 103.0, "volume": 1_000_000},
                    d5: {"date": d5, "open": 103.0, "high": 103.8, "low": 102.8, "close": 103.5, "volume": 1_000_000},
                },
                "NEWR": {
                    d5: {"date": d5, "open": 50.0, "high": 50.0, "low": 50.0, "close": 50.0, "volume": 1_000_000},
                },
            },
        )

        engine = StrategyEngineConfig(
            engine_id="rot_next",
            event_types=["earnings_release"],
            timing_class="any",
            direction="long_only",
            entry_timing_policy="next_open",
            score_threshold_override=0.5,
            target_1_r_override=6.0,
            rotation_enabled=True,
            rotation_min_days_held=3,
            rotation_fitness_threshold=0.45,
            rotation_min_candidate_score=0.8,
            rotation_max_unrealized_r=0.5,
            enabled=True,
        )
        manifest = ExperimentManifest(
            experiment_name="test_rotation_max_unrealized_r",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[engine],
        )
        config = BacktestConfig(
            strategy_name="test_strategy",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
            signal=SignalConfig(score_threshold=0.5, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=1.0,
                max_daily_new_risk_pct=1.0,
                max_positions=10,
                max_positions_per_sector=5,
                max_position_value_pct=1.0,
            ),
            execution=ExecutionConfig(max_holding_days=5),
            reporting=ReportingConfig(),
            strategy_engines=[engine],
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=1_000.0)
        result = runner.run(output_root=tmp_path)

        rotation_trades = [trade for trade in runner._closed_trades if trade.exit_reason.value == "ROTATION"]
        assert not rotation_trades
        assert not any(trade.symbol == "NEWR" for trade in runner._closed_trades)
        assert result.metrics.trade_count == 1

    def test_equity_curve_has_all_days(self, tmp_path):
        """Equity curve has one entry per candidate date."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        store = _build_synthetic_store()
        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        runner = BacktestRunner(manifest=manifest, config=config, store=store)
        result = runner.run()

        # Should have simulated days covering the range (all_trading_days between
        # first and last execution date), plus the initial equity state
        assert result.total_trading_days >= 2

    def test_deterministic_results(self, tmp_path):
        """Two runs with same inputs produce identical metrics."""
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest

        manifest = ExperimentManifest(
            experiment_name="test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        store1 = _build_synthetic_store()
        runner1 = BacktestRunner(manifest=manifest, config=config, store=store1)
        result1 = runner1.run()

        store2 = _build_synthetic_store()
        runner2 = BacktestRunner(manifest=manifest, config=config, store=store2)
        result2 = runner2.run()

        assert result1.metrics.trade_count == result2.metrics.trade_count
        assert result1.metrics.win_rate == result2.metrics.win_rate
        assert result1.metrics.total_return_pct == result2.metrics.total_return_pct
        assert result1.total_candidates_seen == result2.total_candidates_seen
        assert result1.total_orders_rejected == result2.total_orders_rejected

    def test_no_future_data_used(self, tmp_path):
        """Candidates for day D should not appear in a simulation of day D-1."""
        from libs.backtest.snapshot_store import SnapshotStore

        candidates = {
            dt.date(2026, 1, 5): [
                {
                    "event_id": "EVT::001",
                    "symbol": "AAPL",
                    "execution_date": dt.date(2026, 1, 5),
                    "entry_date": "2026-01-05",
                    "entry_price": 150.0,
                    "score": 0.85,
                    "sector": "Technology",
                    "event_type": "earnings",
                    "event_timestamp": "2026-01-02T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": "2026-01-02",
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 3.0,
                }
            ],
            dt.date(2026, 1, 6): [
                {
                    "event_id": "EVT::FUTURE",
                    "symbol": "FUTURE_TICKER",
                    "execution_date": dt.date(2026, 1, 6),
                    "entry_date": "2026-01-06",
                    "entry_price": 50.0,
                    "score": 0.99,
                    "sector": "Technology",
                    "event_type": "earnings",
                    "event_timestamp": "2026-01-05T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": "2026-01-05",
                    "avg_dollar_volume": 1_000_000.0,
                    "atr_14": 1.0,
                }
            ],
        }
        bars = {
            "AAPL": {dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 150.0, "high": 160.0, "low": 148.0, "close": 158.0, "volume": 1_000_000}},
            "FUTURE_TICKER": {dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 50.0, "high": 55.0, "low": 48.0, "close": 52.0, "volume": 500_000}},
        }
        store = SnapshotStore(candidates_by_exec_date=candidates, bars_by_symbol_date=bars)

        # Querying Jan 5 should NOT return FUTURE_TICKER candidate
        rows = store.get_candidates_for_date(dt.date(2026, 1, 5))
        symbols = [r["symbol"] for r in rows]
        assert "FUTURE_TICKER" not in symbols
        assert "AAPL" in symbols

    def test_multi_engine_run_writes_per_engine_metrics(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
        import json
        import pyarrow.parquet as pq

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="portfolio_v2",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="earnings_same_day_short_v1",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="short_only",
                    entry_timing_policy="next_open",
                    max_holding_days=3,
                    engine_risk_budget_pct=0.40,
                ),
                StrategyEngineConfig(
                    engine_id="earnings_after_close_long_v1",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    max_holding_days=5,
                    engine_risk_budget_pct=0.25,
                ),
                StrategyEngineConfig(
                    engine_id="earnings_after_close_short_v1",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="short_only",
                    entry_timing_policy="next_open",
                    max_holding_days=3,
                    engine_risk_budget_pct=0.10,
                    shadow_only=True,
                ),
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        run_dir = tmp_path / result.run_id
        per_engine_metrics = run_dir / "metrics" / "per_engine_metrics.json"
        attribution_by_engine = run_dir / "metrics" / "attribution_by_engine.csv"

        assert per_engine_metrics.exists()
        assert attribution_by_engine.exists()

        payload = per_engine_metrics.read_text()
        assert "earnings_same_day_short_v1" in payload
        assert "earnings_after_close_long_v1" in payload
        assert "earnings_after_close_short_v1" in payload
        assert result.metrics.trade_count >= 1

        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()
        engine_ids = {row["engine_id"] for row in trade_blotter}
        assert "earnings_after_close_short_v1" not in engine_ids

        equity_curve = pq.read_table(run_dir / "artifacts" / "daily_equity_curve.parquet").to_pylist()
        assert any(float(row["net_exposure"]) < 0 for row in equity_curve if float(row["gross_exposure"]) > 0)

        metrics_summary = json.loads((run_dir / "metrics" / "metrics_summary.json").read_text())
        assert "avg_gross_exposure_pct" in metrics_summary
        assert "avg_net_exposure_pct" in metrics_summary
        assert "days_in_market_pct" in metrics_summary

    def test_engine_execution_overrides_flow_into_effective_execution_config(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import Candidate, ExperimentManifest, StrategyEngineConfig

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="portfolio_exec_overrides",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="earnings_same_day_long_trend_v1",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=12,
                    engine_risk_budget_pct=0.25,
                    target_atr_multiplier_override=2.5,
                    target_1_r_override=3.25,
                    target_1_fraction_override=0.33,
                    trailing_model_override="pct_10",
                    trailing_warmup_days_override=2,
                ),
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)

        candidate = Candidate(
            event_id="EVT::SD::LONG",
            symbol="AMD",
            issuer_id="ISSUER::AMD",
            score=0.92,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            event_date=dt.date(2026, 1, 6),
            filing_time_bucket="post_market",
            timing_class="same_day",
            reaction_date=dt.date(2026, 1, 6),
            execution_date=dt.date(2026, 1, 6),
            entry_price_est=122.0,
            avg_dollar_volume=9_000_000.0,
            atr_14=3.0,
            score_bucket="high",
            engine_id="earnings_same_day_long_trend_v1",
            entry_timing_policy="reaction_close",
            shadow_only=False,
            engine_max_holding_days=12,
            engine_risk_budget_pct=0.25,
            engine_target_atr_multiplier=2.5,
            engine_target_1_r=3.25,
            engine_target_1_fraction=0.33,
            engine_trailing_model="pct_10",
            engine_trailing_warmup_days=2,
            trade_direction="long",
        )
        effective_exec = runner._build_effective_execution_config(candidate)

        assert candidate.engine_id == "earnings_same_day_long_trend_v1"
        assert effective_exec.max_holding_days == 12
        assert effective_exec.target_atr_multiplier == pytest.approx(2.5)
        assert effective_exec.target_1_r == pytest.approx(3.25)
        assert effective_exec.target_1_fraction == pytest.approx(0.33)
        assert effective_exec.trailing_model == "pct_10"
        assert effective_exec.trailing_warmup_days == 2

    def test_engine_target_overrides_take_precedence_over_tiered_targets(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import Candidate, ExecutionConfig, ExperimentManifest, SignalConfig, StrategyEngineConfig

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="portfolio_exec_target_override_precedence",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="earnings_same_day_long_trend_v1",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    target_1_r_override=3.5,
                    target_1_fraction_override=0.1,
                ),
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        config = config.model_copy(
            update={
                "signal": SignalConfig(score_threshold=0.5, max_candidates_per_day=5, a_tier_score_threshold=0.8),
                "execution": ExecutionConfig(
                    entry_fill_model="next_open",
                    exit_fill_model="daily_bar_approximation",
                    slippage_bps_base=10.0,
                    commission_per_share=0.005,
                    same_bar_priority="stop_first_conservative",
                    max_holding_days=10,
                    target_1_r=1.5,
                    target_1_fraction=0.5,
                    use_tiered_targets=True,
                    a_tier_target_1_r=2.0,
                    a_tier_target_1_fraction=0.25,
                    non_a_tier_target_1_r=1.25,
                    non_a_tier_target_1_fraction=0.6,
                ),
            }
        )
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)

        candidate = Candidate(
            event_id="EVT::SD::LONG::TIER",
            symbol="AMD",
            issuer_id="ISSUER::AMD",
            score=0.92,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            event_date=dt.date(2026, 1, 6),
            filing_time_bucket="post_market",
            timing_class="same_day",
            reaction_date=dt.date(2026, 1, 6),
            execution_date=dt.date(2026, 1, 6),
            entry_price_est=122.0,
            avg_dollar_volume=9_000_000.0,
            atr_14=3.0,
            score_bucket="high",
            engine_id="earnings_same_day_long_trend_v1",
            entry_timing_policy="reaction_close",
            shadow_only=False,
            engine_target_1_r=3.5,
            engine_target_1_fraction=0.1,
            trade_direction="long",
        )
        effective_exec = runner._build_effective_execution_config(candidate)

        assert effective_exec.target_1_r == pytest.approx(3.5)
        assert effective_exec.target_1_fraction == pytest.approx(0.1)

    def test_tail_exit_adjuster_tightens_execution_for_hot_candidate(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import Candidate, ExecutionConfig, ExperimentManifest, RiskConfig

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="portfolio_tail_exit_adjuster",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        config = config.model_copy(
            update={
                "risk": RiskConfig(
                    per_trade_risk_pct=0.01,
                    max_daily_new_risk_pct=0.05,
                    max_positions=10,
                    max_positions_per_sector=5,
                    tail_exit_adjuster_enabled=True,
                    tail_exit_threshold=0.60,
                    tail_exit_min_signals=2,
                    tail_exit_max_holding_days=8,
                    tail_exit_no_progress_days=1,
                    tail_exit_no_progress_r=0.25,
                    tail_exit_no_progress_fraction=1.0,
                ),
                "execution": ExecutionConfig(
                    entry_fill_model="next_open",
                    exit_fill_model="daily_bar_approximation",
                    slippage_bps_base=10.0,
                    commission_per_share=0.005,
                    same_bar_priority="stop_first_conservative",
                    max_holding_days=12,
                    early_failure_no_progress_days=2,
                    early_failure_no_progress_r=0.15,
                    early_failure_no_progress_fraction=0.5,
                ),
            }
        )
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)

        candidate = Candidate(
            event_id="EVT::TAIL::HOT",
            symbol="AMD",
            issuer_id="ISSUER::AMD",
            score=0.88,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            event_date=dt.date(2026, 1, 6),
            filing_time_bucket="post_market",
            timing_class="same_day",
            reaction_date=dt.date(2026, 1, 6),
            execution_date=dt.date(2026, 1, 6),
            entry_price_est=122.0,
            avg_dollar_volume=9_000_000.0,
            atr_14=3.0,
            score_bucket="high",
            engine_id="tail_hot",
            entry_timing_policy="reaction_close",
            shadow_only=False,
            engine_max_holding_days=12,
            trade_direction="long",
            features={
                "reaction_day_return": 0.14,
                "oneoff_penalty": 0.4,
                "pre_event_market_temperature": 1.4,
                "pre_event_entropy_60d": 2.0,
            },
        )
        effective_exec = runner._build_effective_execution_config(candidate)

        assert effective_exec.max_holding_days == 8
        assert effective_exec.early_failure_no_progress_days == 1
        assert effective_exec.early_failure_no_progress_r == pytest.approx(0.25)
        assert effective_exec.early_failure_no_progress_fraction == pytest.approx(1.0)

    def test_funding_optimizer_prefers_more_capital_efficient_candidate(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import Candidate, DailyPortfolioState, ExperimentManifest

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="portfolio_cap_efficiency_order",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()
        config = config.model_copy(update={"strategy_engine_selection_mode": "interleave_cap_efficiency_soft"})
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)

        expensive = Candidate(
            event_id="EVT::EXPENSIVE",
            symbol="EXP",
            issuer_id="ISSUER::EXP",
            score=0.9,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            event_date=dt.date(2026, 1, 6),
            filing_time_bucket="post_market",
            timing_class="same_day",
            reaction_date=dt.date(2026, 1, 6),
            execution_date=dt.date(2026, 1, 6),
            entry_price_est=100.0,
            avg_dollar_volume=20_000_000.0,
            atr_14=2.0,
            score_bucket="high",
            engine_id="core",
            entry_timing_policy="reaction_close",
            shadow_only=False,
            trade_direction="long",
            forced_shares=500,
        )
        cheap = Candidate(
            event_id="EVT::CHEAP",
            symbol="CHP",
            issuer_id="ISSUER::CHP",
            score=0.9,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            event_date=dt.date(2026, 1, 6),
            filing_time_bucket="post_market",
            timing_class="same_day",
            reaction_date=dt.date(2026, 1, 6),
            execution_date=dt.date(2026, 1, 6),
            entry_price_est=100.0,
            avg_dollar_volume=20_000_000.0,
            atr_14=2.0,
            score_bucket="high",
            engine_id="core",
            entry_timing_policy="reaction_close",
            shadow_only=False,
            trade_direction="long",
            forced_shares=50,
        )
        portfolio_state = DailyPortfolioState(
            date=dt.date(2026, 1, 6),
            equity=100_000.0,
            cash_available=100_000.0,
            gross_exposure=0.0,
            net_exposure=0.0,
            reserved_risk_budget=0.0,
            unrealized_pnl=0.0,
            realized_pnl=0.0,
            open_positions=[],
            daily_new_risk_used=0.0,
            peak_equity=100_000.0,
            current_drawdown_pct=0.0,
        )

        ordered = runner._reorder_candidates_for_funding([expensive, cheap], portfolio_state, macro_data=None)
        assert [candidate.symbol for candidate in ordered[:2]] == ["CHP", "EXP"]

    def test_attention_gate_filters_engine_candidates(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
        from libs.backtest.snapshot_store import SnapshotStore
        from libs.oracle_client.models import EntityInfo, EventAttentionResponse, NewsFeatures, WikiFeatures

        date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={
                date: [
                    {
                        "event_id": "EVT::ATTN::PASS",
                        "symbol": "PASS",
                        "execution_date": date,
                        "entry_date": "2026-01-07",
                        "event_date": "2026-01-06",
                        "event_close": 100.0,
                        "gap_size": 0.12,
                        "entry_price": 100.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": -0.12,
                        "avg_dollar_volume": 8_000_000.0,
                        "atr_14": 4.0,
                    },
                    {
                        "event_id": "EVT::ATTN::FAIL",
                        "symbol": "FAIL",
                        "execution_date": date,
                        "entry_date": "2026-01-07",
                        "event_date": "2026-01-06",
                        "event_close": 101.0,
                        "gap_size": 0.11,
                        "entry_price": 101.0,
                        "score": 0.89,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": -0.11,
                        "avg_dollar_volume": 7_500_000.0,
                        "atr_14": 4.0,
                    },
                ]
            },
            bars_by_symbol_date={
                "PASS": {date: {"date": date, "open": 100.0, "high": 100.0, "low": 95.0, "close": 96.0, "volume": 1_000_000}},
                "FAIL": {date: {"date": date, "open": 101.0, "high": 102.0, "low": 98.0, "close": 99.0, "volume": 900_000}},
            },
        )
        manifest = ExperimentManifest(
            experiment_name="attention_gate_test",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="same_day_short_attention",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="short_only",
                    attention_min_wiki_spike_10d=1.2,
                )
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store)

        def _fake_attention(candidate):
            spike = 1.5 if candidate.symbol == "PASS" else 0.9
            return EventAttentionResponse(
                ticker=candidate.symbol,
                event_date="2026-01-06",
                entity=EntityInfo(
                    ticker=candidate.symbol,
                    canonical_name=candidate.symbol,
                    resolver_confidence=0.9,
                ),
                wiki=WikiFeatures(spike_10d=spike, zscore_20d=1.0),
                news=NewsFeatures(),
                metadata={},
            )

        runner._attention_service._get_event_attention = _fake_attention  # type: ignore[method-assign]
        selected = runner._select_candidates_for_date(date)

        assert [candidate.symbol for candidate in selected] == ["PASS"]
        assert selected[0].features["attention_wiki_spike_10d"] == pytest.approx(1.5)

    def test_attention_max_gate_allows_missing_payload(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
        from libs.backtest.snapshot_store import SnapshotStore

        date = dt.date(2026, 1, 6)
        store = SnapshotStore(
            candidates_by_exec_date={
                date: [
                    {
                        "event_id": "EVT::ATTN::KNOWN",
                        "symbol": "KNOWN",
                        "execution_date": date,
                        "entry_date": "2026-01-06",
                        "event_date": "2026-01-06",
                        "event_close": 100.0,
                        "gap_size": 0.12,
                        "entry_price": 100.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": 0.15,
                        "avg_dollar_volume": 8_000_000.0,
                        "atr_14": 4.0,
                    },
                    {
                        "event_id": "EVT::ATTN::MISSING",
                        "symbol": "MISSING",
                        "execution_date": date,
                        "entry_date": "2026-01-06",
                        "event_date": "2026-01-06",
                        "event_close": 101.0,
                        "gap_size": 0.11,
                        "entry_price": 101.0,
                        "score": 0.89,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-06",
                        "reaction_day_return": 0.14,
                        "avg_dollar_volume": 7_500_000.0,
                        "atr_14": 4.0,
                    },
                ]
            },
            bars_by_symbol_date={
                "KNOWN": {date: {"date": date, "open": 100.0, "high": 105.0, "low": 99.0, "close": 103.0, "volume": 1_000_000}},
                "MISSING": {date: {"date": date, "open": 101.0, "high": 104.0, "low": 100.0, "close": 102.0, "volume": 900_000}},
            },
        )
        manifest = ExperimentManifest(
            experiment_name="attention_max_gate_test",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="same_day_long_attention_cap",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    gap_size_min=0.10,
                    attention_max_wiki_spike_10d=1.5,
                )
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        runner = BacktestRunner(manifest=manifest, config=config, store=store)

        def _fake_attention(candidate):
            if candidate.symbol == "KNOWN":
                from libs.oracle_client.models import EntityInfo, EventAttentionResponse, NewsFeatures, WikiFeatures

                return EventAttentionResponse(
                    ticker=candidate.symbol,
                    event_date="2026-01-06",
                    entity=EntityInfo(
                        ticker=candidate.symbol,
                        canonical_name=candidate.symbol,
                        resolver_confidence=0.9,
                    ),
                    wiki=WikiFeatures(spike_10d=1.4, zscore_20d=0.5),
                    news=NewsFeatures(),
                    metadata={},
                )
            return None

        runner._attention_service._get_event_attention = _fake_attention  # type: ignore[method-assign]
        selected = runner._select_candidates_for_date(date)

        assert [candidate.symbol for candidate in selected] == ["KNOWN", "MISSING"]
        assert selected[0].features["attention_wiki_spike_10d"] == pytest.approx(1.4)
        assert "attention_wiki_spike_10d" not in selected[1].features

    def test_attention_path_honors_custom_ranking_fields(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import ExperimentManifest, StrategyEngineConfig
        from libs.backtest.snapshot_store import SnapshotStore
        from libs.oracle_client.models import EntityInfo, EventAttentionResponse, NewsFeatures, WikiFeatures

        date = dt.date(2026, 1, 8)
        store = SnapshotStore(
            candidates_by_exec_date={
                date: [
                    {
                        "event_id": "EVT::ATTN::DOCWIN",
                        "symbol": "DOCWIN",
                        "execution_date": date,
                        "entry_date": date.isoformat(),
                        "event_date": date.isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-08T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": date.isoformat(),
                        "reaction_day_return": 0.09,
                        "close_location": 0.72,
                        "volume_ratio_20d": 1.8,
                        "gap_size": 0.01,
                        "parse_confidence_overall": 0.82,
                        "parse_confidence_event_direction": 0.82,
                        "parse_confidence_guidance": 0.82,
                        "document_quality_score": 0.95,
                        "signal_strength_score": 0.95,
                        "guidance_direction_score": 1.0,
                        "oneoff_penalty": 0.05,
                        "avg_dollar_volume": 8_000_000.0,
                        "atr_14": 4.0,
                    },
                    {
                        "event_id": "EVT::ATTN::CLOSEWIN",
                        "symbol": "CLOSEWIN",
                        "execution_date": date,
                        "entry_date": date.isoformat(),
                        "event_date": date.isoformat(),
                        "event_close": 101.0,
                        "entry_price": 101.0,
                        "score": 0.89,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-08T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": date.isoformat(),
                        "reaction_day_return": 0.08,
                        "close_location": 0.92,
                        "volume_ratio_20d": 1.8,
                        "gap_size": 0.01,
                        "parse_confidence_overall": 0.70,
                        "parse_confidence_event_direction": 0.70,
                        "parse_confidence_guidance": 0.70,
                        "document_quality_score": 0.60,
                        "signal_strength_score": 0.60,
                        "guidance_direction_score": 0.5,
                        "oneoff_penalty": 0.05,
                        "avg_dollar_volume": 7_500_000.0,
                        "atr_14": 4.0,
                    },
                ]
            },
            bars_by_symbol_date={},
        )
        manifest = ExperimentManifest(
            experiment_name="attention_ranking_fields_test",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="same_day_long_attention",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    attention_max_wiki_spike_10d=2.0,
                )
            ],
        )
        config = _make_config(strategy_engines=manifest.strategy_engines)
        config.signal.scoring_model = "return_max_long_v2"
        config.signal.score_threshold = 0.45
        config.signal.max_candidates_per_day = 1
        config.signal.ranking_fields = ["-close_location", "-score"]
        runner = BacktestRunner(manifest=manifest, config=config, store=store)

        def _fake_attention(candidate):
            return EventAttentionResponse(
                ticker=candidate.symbol,
                event_date=date.isoformat(),
                entity=EntityInfo(
                    ticker=candidate.symbol,
                    canonical_name=candidate.symbol,
                    resolver_confidence=0.9,
                ),
                wiki=WikiFeatures(spike_10d=1.0, zscore_20d=0.5),
                news=NewsFeatures(),
                metadata={},
            )

        runner._get_event_attention = _fake_attention  # type: ignore[method-assign]
        selected = runner._select_candidates_for_date(date)

        assert [candidate.symbol for candidate in selected] == ["CLOSEWIN"]

    def test_return_max_long_strategy_schedules_add_on_and_writes_benchmark_metrics(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore
        import json
        import pyarrow.parquet as pq

        dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8]]
        store = SnapshotStore(
            candidates_by_exec_date={
                dt.date(2026, 1, 5): [
                    {
                        "event_id": "EVT::LONG::001",
                        "symbol": "NVDA",
                        "execution_date": dt.date(2026, 1, 5),
                        "entry_date": "2026-01-05",
                        "event_date": "2026-01-05",
                        "event_close": 101.0,
                        "entry_price": 101.0,
                        "score": 0.82,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-05",
                        "reaction_day_return": 0.08,
                        "close_location": 0.82,
                        "volume_ratio_20d": 1.8,
                        "gap_size": 0.01,
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "parse_confidence_overall": 0.9,
                        "parse_confidence_event_direction": 0.85,
                        "parse_confidence_guidance": 0.8,
                        "document_quality_score": 0.8,
                        "guidance_direction_score": 1.0,
                        "oneoff_penalty": 0.1,
                        "avg_dollar_volume": 120_000_000.0,
                        "market_cap_proxy": 10_000_000_000.0,
                        "atr_14": 3.0,
                    }
                ],
                dt.date(2026, 1, 8): [],
            },
            bars_by_symbol_date={
                "NVDA": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2_000_000},
                    dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 102.0, "high": 108.0, "low": 101.0, "close": 107.0, "volume": 1_900_000},
                    dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 108.0, "high": 112.0, "low": 107.0, "close": 111.0, "volume": 1_800_000},
                    dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 112.0, "high": 115.0, "low": 111.0, "close": 114.0, "volume": 1_700_000},
                }
            },
            macro_by_date={
                dates[0]: {"spy_close": 500.0, "spy_sma_20": 495.0, "qqq_close": 420.0, "qqq_sma_20": 415.0},
                dates[1]: {"spy_close": 503.0, "spy_sma_20": 496.0, "qqq_close": 425.0, "qqq_sma_20": 416.0},
                dates[2]: {"spy_close": 505.0, "spy_sma_20": 497.0, "qqq_close": 430.0, "qqq_sma_20": 417.0},
                dates[3]: {"spy_close": 507.0, "spy_sma_20": 498.0, "qqq_close": 435.0, "qqq_sma_20": 418.0},
            },
        )

        manifest = ExperimentManifest(
            experiment_name="return_max_long_v1",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="reaction_close_long_core",
                    event_types=["earnings_release", "guidance_update"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=20,
                    engine_risk_budget_pct=0.5,
                    reaction_day_return_min=0.04,
                    reaction_day_return_max=0.12,
                    close_location_min=0.70,
                    volume_ratio_min=1.5,
                    score_threshold_override=0.62,
                ),
                StrategyEngineConfig(
                    engine_id="delayed_add_on_long",
                    event_types=["earnings_release", "guidance_update"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    synthetic_only=True,
                    max_holding_days=15,
                    engine_risk_budget_pct=1.0,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            universe=UniverseConfig(
                min_price=15.0,
                min_avg_dollar_volume=75_000_000.0,
                min_market_cap_proxy=2_000_000_000.0,
            ),
            signal=SignalConfig(
                score_threshold=0.62,
                max_candidates_per_day=6,
                scoring_model="return_max_long_v1",
                a_tier_score_threshold=0.75,
            ),
            risk=RiskConfig(
                per_trade_risk_pct=0.004,
                per_trade_risk_pct_a_tier=0.005,
                max_daily_new_risk_pct=0.015,
                max_positions=6,
                max_positions_per_sector=2,
                macro_regime_enabled=True,
                macro_regime_mode="spy_qqq_scaler",
                macro_regime_neutral_size_scaler=0.6,
                macro_regime_risk_off_size_scaler=0.35,
                macro_regime_risk_off_a_tier_only=True,
                stop_atr_multiplier=2.25,
                veto_oneoff_penalty=0.4,
                veto_parse_confidence_min=0.6,
                veto_unknown_direction=True,
                veto_bearish_direction=True,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                target_model="fixed_r",
                target_1_r=1.5,
                target_1_fraction=0.5,
                use_tiered_targets=True,
                a_tier_target_1_r=2.0,
                a_tier_target_1_fraction=0.25,
                non_a_tier_target_1_r=1.5,
                non_a_tier_target_1_fraction=0.5,
                trailing_model="pct_6",
                trailing_warmup_days=3,
                max_holding_days=15,
                early_failure_close_below_entry_and_reaction_close=True,
                early_failure_no_progress_days=2,
                early_failure_no_progress_r=0.5,
                early_failure_no_progress_fraction=0.5,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            event_type_profiles={
                "earnings_release": EventTypeProfile(enabled=True, max_holding_days_override=20),
                "guidance_update": EventTypeProfile(enabled=True, max_holding_days_override=12),
                "unknown": EventTypeProfile(enabled=False),
            },
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        run_dir = tmp_path / result.run_id
        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()
        assert any(bool(row["is_add_on"]) for row in trade_blotter)
        add_on_rows = [row for row in trade_blotter if row["engine_id"] == "delayed_add_on_long"]
        assert add_on_rows
        assert all(bool(row["is_add_on"]) for row in add_on_rows)
        assert all(row["parent_position_id"] is not None for row in add_on_rows)

        metrics_summary = json.loads((run_dir / "metrics" / "metrics_summary.json").read_text())
        assert metrics_summary["qqq_benchmark_return_pct"] is not None
        assert metrics_summary["excess_vs_qqq_pct"] is not None
        assert metrics_summary["long_pnl_contribution_pct"] is not None

    def test_residual_priority_engine_excludes_selected_names_from_core_queue(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        date = dt.date(2026, 1, 5)
        store = SnapshotStore(
            candidates_by_exec_date={
                date: [
                    {
                        "event_id": "EVT::A",
                        "symbol": "AAPL",
                        "execution_date": date,
                        "entry_date": "2026-01-05",
                        "event_date": "2026-01-05",
                        "event_close": 101.0,
                        "entry_price": 101.0,
                        "score": 0.60,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "regular_hours",
                        "reaction_date": "2026-01-05",
                        "reaction_day_return": 0.10,
                        "close_location": 0.75,
                        "volume_ratio_20d": 2.5,
                        "gap_size": 0.03,
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "parse_confidence_overall": 0.90,
                        "parse_confidence_event_direction": 0.85,
                        "parse_confidence_guidance": 0.80,
                        "document_quality_score": 0.80,
                        "guidance_direction_score": 1.0,
                        "oneoff_penalty": 0.1,
                        "avg_dollar_volume": 120_000_000.0,
                        "market_cap_proxy": 10_000_000_000.0,
                        "atr_14": 3.0,
                    },
                    {
                        "event_id": "EVT::B",
                        "symbol": "MSFT",
                        "execution_date": date,
                        "entry_date": "2026-01-05",
                        "event_date": "2026-01-05",
                        "event_close": 201.0,
                        "entry_price": 201.0,
                        "score": 0.58,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "regular_hours",
                        "reaction_date": "2026-01-05",
                        "reaction_day_return": 0.07,
                        "close_location": 0.72,
                        "volume_ratio_20d": 2.1,
                        "gap_size": 0.02,
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "parse_confidence_overall": 0.88,
                        "parse_confidence_event_direction": 0.84,
                        "parse_confidence_guidance": 0.79,
                        "document_quality_score": 0.79,
                        "guidance_direction_score": 1.0,
                        "oneoff_penalty": 0.1,
                        "avg_dollar_volume": 150_000_000.0,
                        "market_cap_proxy": 20_000_000_000.0,
                        "atr_14": 4.0,
                    },
                ],
            },
            bars_by_symbol_date={},
        )

        manifest = ExperimentManifest(
            experiment_name="residual_priority_test",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="priority",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    reaction_day_return_min=0.08,
                    reaction_day_return_max=0.18,
                    volume_ratio_min=2.0,
                    residual_reserve_selected=True,
                    score_threshold_override=0.50,
                ),
                StrategyEngineConfig(
                    engine_id="core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    reaction_day_return_min=0.03,
                    reaction_day_return_max=0.18,
                    volume_ratio_min=1.0,
                    score_threshold_override=0.50,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=15.0, min_avg_dollar_volume=75_000_000.0, min_market_cap_proxy=2_000_000_000.0),
            signal=SignalConfig(
                score_threshold=0.50,
                max_candidates_per_day=2,
                scoring_model="return_max_long_v2",
            ),
            risk=RiskConfig(max_positions=4, max_positions_per_sector=4),
            execution=ExecutionConfig(),
            reporting=ReportingConfig(generate_plots=False),
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )
        runner = BacktestRunner(manifest=manifest, config=config, store=store)
        selected = runner._select_candidates_for_date(date)

        assert [candidate.symbol for candidate in selected] == ["AAPL", "MSFT"]
        assert [candidate.engine_id for candidate in selected] == ["priority", "core"]

    def test_return_max_long_staged_add_on_can_scale_twice(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore
        import pyarrow.parquet as pq

        dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8, 9, 12]]
        store = SnapshotStore(
            candidates_by_exec_date={
                dt.date(2026, 1, 5): [
                    {
                        "event_id": "EVT::LONG::STAGED",
                        "symbol": "NVDA",
                        "execution_date": dt.date(2026, 1, 5),
                        "entry_date": "2026-01-05",
                        "event_date": "2026-01-05",
                        "event_close": 101.0,
                        "entry_price": 101.0,
                        "score": 0.84,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "regular_hours",
                        "reaction_date": "2026-01-05",
                        "reaction_day_return": 0.08,
                        "close_location": 0.82,
                        "volume_ratio_20d": 1.8,
                        "gap_size": 0.01,
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "parse_confidence_overall": 0.9,
                        "parse_confidence_event_direction": 0.85,
                        "parse_confidence_guidance": 0.8,
                        "document_quality_score": 0.8,
                        "guidance_direction_score": 1.0,
                        "oneoff_penalty": 0.1,
                        "avg_dollar_volume": 120_000_000.0,
                        "market_cap_proxy": 10_000_000_000.0,
                        "atr_14": 3.0,
                        "reaction_day_high": 102.0,
                    }
                ],
                dt.date(2026, 1, 12): [],
            },
            bars_by_symbol_date={
                "NVDA": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2_000_000},
                    dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 102.0, "high": 106.0, "low": 101.0, "close": 105.0, "volume": 1_900_000},
                    dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 106.0, "high": 109.0, "low": 105.0, "close": 108.0, "volume": 1_850_000},
                    dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 109.0, "high": 114.0, "low": 108.0, "close": 113.0, "volume": 1_800_000},
                    dt.date(2026, 1, 9): {"date": dt.date(2026, 1, 9), "open": 114.0, "high": 118.0, "low": 113.0, "close": 117.0, "volume": 1_750_000},
                    dt.date(2026, 1, 12): {"date": dt.date(2026, 1, 12), "open": 118.0, "high": 120.0, "low": 116.0, "close": 119.0, "volume": 1_700_000},
                }
            },
            macro_by_date={
                day: {"spy_close": 500.0 + idx, "spy_sma_20": 490.0, "qqq_close": 420.0 + idx, "qqq_sma_20": 410.0}
                for idx, day in enumerate(dates)
            },
        )

        manifest = ExperimentManifest(
            experiment_name="return_max_long_v1_staged_add_on",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="reaction_close_long_core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=20,
                    engine_risk_budget_pct=1.0,
                    reaction_day_return_min=0.04,
                    reaction_day_return_max=0.12,
                    close_location_min=0.70,
                    volume_ratio_min=1.5,
                    score_threshold_override=0.62,
                ),
                StrategyEngineConfig(
                    engine_id="delayed_add_on_long",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    synthetic_only=True,
                    max_holding_days=15,
                    engine_risk_budget_pct=1.0,
                    add_on_parent_score_min=0.75,
                    add_on_max_parent_days_held=4,
                    add_on_schedule_days=[1, 3],
                    add_on_progress_r_levels=[0.50, 1.50],
                    add_on_max_count=2,
                    add_on_size_fraction=0.25,
                    add_on_close_location_min=0.70,
                    add_on_require_above_reaction_high=True,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            universe=UniverseConfig(min_price=15.0, min_avg_dollar_volume=75_000_000.0, min_market_cap_proxy=2_000_000_000.0),
            signal=SignalConfig(score_threshold=0.62, max_candidates_per_day=6, scoring_model="return_max_long_v1", a_tier_score_threshold=0.75),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                per_trade_risk_pct_a_tier=0.01,
                max_daily_new_risk_pct=0.20,
                max_positions=6,
                max_positions_per_sector=3,
                macro_regime_enabled=True,
                macro_regime_mode="spy_qqq_scaler",
                macro_regime_neutral_size_scaler=1.0,
                macro_regime_risk_off_size_scaler=1.0,
                macro_regime_risk_off_a_tier_only=False,
                stop_atr_multiplier=2.25,
                veto_oneoff_penalty=0.4,
                veto_parse_confidence_min=0.6,
                veto_unknown_direction=False,
                veto_bearish_direction=True,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                target_model="fixed_r",
                target_1_r=10.0,
                target_1_fraction=0.0,
                trailing_model=None,
                trailing_warmup_days=20,
                max_holding_days=20,
            ),
            reporting=ReportingConfig(write_trade_blotter=True, write_equity_curve=True, write_metrics_summary=True, generate_plots=False),
            event_type_profiles={"earnings_release": EventTypeProfile(enabled=True, max_holding_days_override=20)},
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)
        run_dir = tmp_path / result.run_id
        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()
        add_on_rows = [row for row in trade_blotter if row["engine_id"] == "delayed_add_on_long"]
        assert len(add_on_rows) >= 2
        assert sorted(int(row["shares"]) for row in add_on_rows)[0] > 0
        assert all(bool(row["is_add_on"]) for row in add_on_rows)

    def test_return_max_long_add_on_parent_score_gate_blocks_synthetic_child(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore
        import pyarrow.parquet as pq

        dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8]]
        store = SnapshotStore(
            candidates_by_exec_date={
                dt.date(2026, 1, 5): [
                    {
                        "event_id": "EVT::LONG::002",
                        "symbol": "NVDA",
                        "execution_date": dt.date(2026, 1, 5),
                        "entry_date": "2026-01-05",
                        "event_date": "2026-01-05",
                        "event_close": 101.0,
                        "entry_price": 101.0,
                        "score": 0.70,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-05",
                        "reaction_day_return": 0.08,
                        "close_location": 0.82,
                        "volume_ratio_20d": 1.8,
                        "gap_size": 0.01,
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "parse_confidence_overall": 0.9,
                        "document_quality_score": 0.8,
                        "guidance_direction_score": 1.0,
                        "oneoff_penalty": 0.1,
                        "avg_dollar_volume": 120_000_000.0,
                        "market_cap_proxy": 10_000_000_000.0,
                        "atr_14": 3.0,
                    }
                ],
                dt.date(2026, 1, 8): [],
            },
            bars_by_symbol_date={
                "NVDA": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2_000_000},
                    dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 102.0, "high": 108.0, "low": 101.0, "close": 107.0, "volume": 1_900_000},
                    dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 108.0, "high": 112.0, "low": 107.0, "close": 111.0, "volume": 1_800_000},
                    dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 112.0, "high": 115.0, "low": 111.0, "close": 114.0, "volume": 1_700_000},
                }
            },
            macro_by_date={
                dates[0]: {"spy_close": 500.0, "spy_sma_20": 495.0, "qqq_close": 420.0, "qqq_sma_20": 415.0},
                dates[1]: {"spy_close": 503.0, "spy_sma_20": 496.0, "qqq_close": 425.0, "qqq_sma_20": 416.0},
                dates[2]: {"spy_close": 505.0, "spy_sma_20": 497.0, "qqq_close": 430.0, "qqq_sma_20": 417.0},
                dates[3]: {"spy_close": 507.0, "spy_sma_20": 498.0, "qqq_close": 435.0, "qqq_sma_20": 418.0},
            },
        )

        manifest = ExperimentManifest(
            experiment_name="return_max_long_v1_add_on_parent_gate",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="reaction_close_long_core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=20,
                    engine_risk_budget_pct=1.0,
                    reaction_day_return_min=0.04,
                    reaction_day_return_max=0.12,
                    close_location_min=0.70,
                    volume_ratio_min=1.5,
                    score_threshold_override=0.62,
                ),
                StrategyEngineConfig(
                    engine_id="delayed_add_on_long",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    synthetic_only=True,
                    max_holding_days=15,
                    engine_risk_budget_pct=1.0,
                    add_on_parent_score_min=0.80,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            universe=UniverseConfig(
                min_price=15.0,
                min_avg_dollar_volume=75_000_000.0,
                min_market_cap_proxy=2_000_000_000.0,
            ),
            signal=SignalConfig(
                score_threshold=0.62,
                max_candidates_per_day=6,
                scoring_model="return_max_long_v1",
                a_tier_score_threshold=0.75,
            ),
            risk=RiskConfig(
                per_trade_risk_pct=0.004,
                per_trade_risk_pct_a_tier=0.005,
                max_daily_new_risk_pct=0.05,
                max_positions=6,
                max_positions_per_sector=2,
                macro_regime_enabled=True,
                macro_regime_mode="spy_qqq_scaler",
                macro_regime_neutral_size_scaler=0.6,
                macro_regime_risk_off_size_scaler=0.35,
                macro_regime_risk_off_a_tier_only=False,
                stop_atr_multiplier=2.25,
                veto_oneoff_penalty=0.4,
                veto_parse_confidence_min=0.6,
                veto_unknown_direction=False,
                veto_bearish_direction=True,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                target_model="fixed_r",
                target_1_r=1.5,
                target_1_fraction=0.5,
                use_tiered_targets=True,
                a_tier_target_1_r=2.0,
                a_tier_target_1_fraction=0.25,
                non_a_tier_target_1_r=1.5,
                non_a_tier_target_1_fraction=0.5,
                trailing_model="pct_6",
                trailing_warmup_days=3,
                max_holding_days=15,
                early_failure_close_below_entry_and_reaction_close=True,
                early_failure_no_progress_days=2,
                early_failure_no_progress_r=0.5,
                early_failure_no_progress_fraction=0.5,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            event_type_profiles={
                "earnings_release": EventTypeProfile(enabled=True, max_holding_days_override=20),
                "unknown": EventTypeProfile(enabled=False),
            },
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        run_dir = tmp_path / result.run_id
        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()
        assert not any(bool(row["is_add_on"]) for row in trade_blotter)
        assert not any(row["engine_id"] == "delayed_add_on_long" for row in trade_blotter)

    def test_return_max_long_add_on_parent_engine_gate_blocks_non_matching_parent(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore
        import pyarrow.parquet as pq

        dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8]]
        store = SnapshotStore(
            candidates_by_exec_date={
                dt.date(2026, 1, 5): [
                    {
                        "event_id": "EVT::LONG::003",
                        "symbol": "NVDA",
                        "execution_date": dt.date(2026, 1, 5),
                        "entry_date": "2026-01-05",
                        "event_date": "2026-01-05",
                        "event_close": 101.0,
                        "entry_price": 101.0,
                        "score": 0.72,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": "2026-01-05",
                        "reaction_day_return": 0.08,
                        "close_location": 0.82,
                        "volume_ratio_20d": 1.8,
                        "gap_size": 0.01,
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "parse_confidence_overall": 0.9,
                        "document_quality_score": 0.8,
                        "guidance_direction_score": 1.0,
                        "oneoff_penalty": 0.1,
                        "avg_dollar_volume": 120_000_000.0,
                        "market_cap_proxy": 10_000_000_000.0,
                        "atr_14": 3.0,
                    }
                ],
                dt.date(2026, 1, 8): [],
            },
            bars_by_symbol_date={
                "NVDA": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2_000_000},
                    dt.date(2026, 1, 6): {"date": dt.date(2026, 1, 6), "open": 102.0, "high": 108.0, "low": 101.0, "close": 107.0, "volume": 1_900_000},
                    dt.date(2026, 1, 7): {"date": dt.date(2026, 1, 7), "open": 108.0, "high": 112.0, "low": 107.0, "close": 111.0, "volume": 1_800_000},
                    dt.date(2026, 1, 8): {"date": dt.date(2026, 1, 8), "open": 112.0, "high": 115.0, "low": 111.0, "close": 114.0, "volume": 1_700_000},
                }
            },
            macro_by_date={
                dates[0]: {"spy_close": 500.0, "spy_sma_20": 495.0, "qqq_close": 420.0, "qqq_sma_20": 415.0},
                dates[1]: {"spy_close": 503.0, "spy_sma_20": 496.0, "qqq_close": 425.0, "qqq_sma_20": 416.0},
                dates[2]: {"spy_close": 505.0, "spy_sma_20": 497.0, "qqq_close": 430.0, "qqq_sma_20": 417.0},
                dates[3]: {"spy_close": 507.0, "spy_sma_20": 498.0, "qqq_close": 435.0, "qqq_sma_20": 418.0},
            },
        )

        manifest = ExperimentManifest(
            experiment_name="return_max_long_v1_add_on_parent_engine_gate",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="next_open_controlled_long",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    max_holding_days=15,
                    engine_risk_budget_pct=1.0,
                    reaction_day_return_min=0.03,
                    reaction_day_return_max=0.10,
                    close_location_min=0.65,
                    volume_ratio_min=1.5,
                    score_threshold_override=0.60,
                ),
                StrategyEngineConfig(
                    engine_id="delayed_add_on_long",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    synthetic_only=True,
                    max_holding_days=15,
                    engine_risk_budget_pct=1.0,
                    add_on_parent_engine_ids=["reaction_close_long_core"],
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            universe=UniverseConfig(
                min_price=15.0,
                min_avg_dollar_volume=75_000_000.0,
                min_market_cap_proxy=2_000_000_000.0,
            ),
            signal=SignalConfig(
                score_threshold=0.55,
                max_candidates_per_day=6,
                scoring_model="return_max_long_v2",
                a_tier_score_threshold=0.68,
            ),
            risk=RiskConfig(
                per_trade_risk_pct=0.0045,
                per_trade_risk_pct_a_tier=0.0055,
                max_daily_new_risk_pct=0.04,
                max_positions=10,
                max_positions_per_sector=3,
                macro_regime_enabled=True,
                macro_regime_mode="spy_qqq_scaler",
                macro_regime_neutral_size_scaler=0.85,
                macro_regime_risk_off_size_scaler=0.60,
                macro_regime_risk_off_a_tier_only=False,
                stop_atr_multiplier=2.25,
                veto_oneoff_penalty=0.4,
                veto_parse_confidence_min=0.6,
                veto_unknown_direction=False,
                veto_bearish_direction=True,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                target_model="fixed_r",
                target_1_r=1.5,
                target_1_fraction=0.5,
                use_tiered_targets=True,
                a_tier_target_1_r=2.0,
                a_tier_target_1_fraction=0.25,
                non_a_tier_target_1_r=1.5,
                non_a_tier_target_1_fraction=0.5,
                trailing_model="pct_6",
                trailing_warmup_days=3,
                max_holding_days=15,
                early_failure_close_below_entry_and_reaction_close=True,
                early_failure_no_progress_days=2,
                early_failure_no_progress_r=0.5,
                early_failure_no_progress_fraction=0.5,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            event_type_profiles={
                "earnings_release": EventTypeProfile(enabled=True, max_holding_days_override=20),
                "unknown": EventTypeProfile(enabled=False),
            },
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)

        run_dir = tmp_path / result.run_id
        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()
        assert any(row["engine_id"] == "next_open_controlled_long" for row in trade_blotter)
        assert not any(bool(row["is_add_on"]) for row in trade_blotter)
        assert not any(row["engine_id"] == "delayed_add_on_long" for row in trade_blotter)

    def test_evaluate_pending_open_exit_can_schedule_early_pop_giveback(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            Candidate,
            ExecutionConfig,
            ExperimentManifest,
            OpenPosition,
            PlannedOrder,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        store = SnapshotStore(candidates_by_exec_date={}, bars_by_symbol_date={})
        manifest = ExperimentManifest(
            experiment_name="giveback_eval",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="midlarge-liquid-long-v1",
            universe=UniverseConfig(min_price=15.0, min_avg_dollar_volume=75_000_000.0),
            signal=SignalConfig(score_threshold=0.45, max_candidates_per_day=10, scoring_model="return_max_long_v2"),
            risk=RiskConfig(per_trade_risk_pct=0.05, max_daily_new_risk_pct=0.5, max_positions=20),
            execution=ExecutionConfig(
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                max_holding_days=25,
                early_pop_giveback_days_min=3,
                early_pop_giveback_days_max=5,
                early_pop_giveback_trigger_r=0.75,
                early_pop_giveback_min_r=0.40,
                early_pop_giveback_from_peak_pct=0.035,
                early_pop_giveback_fraction=1.0,
            ),
            reporting=ReportingConfig(generate_plots=False),
        )
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        candidate = Candidate(
            event_id="EVT::GIVEBACK",
            symbol="AAPL",
            score=0.7,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 5, 21, 0, tzinfo=_UTC),
            event_date=dt.date(2026, 1, 5),
            filing_time_bucket="regular_hours",
            reaction_date=dt.date(2026, 1, 5),
            execution_date=dt.date(2026, 1, 6),
            entry_price_est=100.0,
            avg_dollar_volume=120_000_000.0,
            atr_14=2.0,
            score_bucket="high",
            engine_id="reaction_close_long_core",
            entry_timing_policy="reaction_close",
            features={"event_direction": "bullish", "guidance_status": "raised"},
        )
        plan = PlannedOrder(
            candidate=candidate,
            shares=100,
            entry_price_limit=100.0,
            stop_price=95.0,
            target_price=112.0,
            risk_dollars=500.0,
        )
        position = OpenPosition(
            position_id="POS::1",
            plan=plan,
            entry_date=dt.date(2026, 1, 6),
            entry_price=100.0,
            entry_fill_slippage_bps=0.0,
            current_stop=95.0,
            target_price=112.0,
            peak_price=110.0,
            shares_open=100,
            shares_total=100,
            days_held=4,
        )
        payload = runner._evaluate_pending_open_exit(
            position=position,
            bar={"date": dt.date(2026, 1, 10), "open": 101.0, "high": 103.0, "low": 100.0, "close": 101.0},
            execution_config=config.execution,
            date=dt.date(2026, 1, 10),
        )
        assert payload is not None
        assert payload["reason"] == "GIVEBACK"
        assert payload["fraction"] == pytest.approx(1.0)

    def test_global_score_selection_uses_custom_ranking_fields(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        model_path = tmp_path / "ranker.json"
        model_path.write_text(
            json.dumps(
                {
                    "model_type": "bucket_blend_v1",
                    "global_mean": 0.0,
                    "features": [
                        {
                            "name": "direction_guidance_combo",
                            "weight": 1.0,
                            "values": {
                                "bullish|raised": 0.25,
                                "unknown|not_provided": 0.05,
                            },
                        }
                    ],
                }
            )
        )

        event_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={
                event_date: [
                    {
                        "event_id": "EVT::PRIORITY",
                        "symbol": "AAPL",
                        "execution_date": event_date,
                        "entry_date": event_date.isoformat(),
                        "event_date": event_date.isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.60,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-07T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": event_date.isoformat(),
                        "reaction_day_return": 0.10,
                        "close_location": 0.72,
                        "volume_ratio_20d": 2.2,
                        "gap_size": 0.03,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 3.0,
                    },
                    {
                        "event_id": "EVT::CORE",
                        "symbol": "MSFT",
                        "execution_date": event_date,
                        "entry_date": event_date.isoformat(),
                        "event_date": event_date.isoformat(),
                        "event_close": 200.0,
                        "entry_price": 200.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "unknown",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-07T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": event_date.isoformat(),
                        "reaction_day_return": 0.09,
                        "close_location": 0.74,
                        "volume_ratio_20d": 2.1,
                        "gap_size": 0.02,
                        "avg_dollar_volume": 8_000_000.0,
                        "atr_14": 4.0,
                    },
                ]
            },
            bars_by_symbol_date={},
        )
        manifest = ExperimentManifest(
            experiment_name="test_global_ranker",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="priority",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    enabled=True,
                ),
                StrategyEngineConfig(
                    engine_id="core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    enabled=True,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="test_strategy",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
            signal=SignalConfig(
                score_threshold=0.5,
                max_candidates_per_day=1,
                ranking_model_path=str(model_path),
                ranking_fields=["-learned_rank_score", "-score"],
            ),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(max_holding_days=5),
            reporting=ReportingConfig(),
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="global_score",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        candidates = runner._select_candidates_for_date(event_date)

        assert [candidate.symbol for candidate in candidates] == ["AAPL"]

    def test_interleave_head_score_prioritizes_stronger_core_head(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        event_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={
                event_date: [
                    {
                        "event_id": "EVT::PRIORITY",
                        "symbol": "AAPL",
                        "execution_date": event_date,
                        "entry_date": event_date.isoformat(),
                        "event_date": event_date.isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.60,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-07T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": event_date.isoformat(),
                        "reaction_day_return": 0.10,
                        "close_location": 0.72,
                        "volume_ratio_20d": 2.2,
                        "gap_size": 0.03,
                        "avg_dollar_volume": 5_000_000.0,
                        "atr_14": 3.0,
                    },
                    {
                        "event_id": "EVT::CORE",
                        "symbol": "MSFT",
                        "execution_date": event_date,
                        "entry_date": event_date.isoformat(),
                        "event_date": event_date.isoformat(),
                        "event_close": 200.0,
                        "entry_price": 200.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "unknown",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-07T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": event_date.isoformat(),
                        "reaction_day_return": 0.09,
                        "close_location": 0.74,
                        "volume_ratio_20d": 2.1,
                        "gap_size": 0.02,
                        "avg_dollar_volume": 8_000_000.0,
                        "atr_14": 4.0,
                    },
                ]
            },
            bars_by_symbol_date={},
        )
        manifest = ExperimentManifest(
            experiment_name="test_interleave_head_score",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="priority",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    enabled=True,
                ),
                StrategyEngineConfig(
                    engine_id="core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    enabled=True,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="test_strategy",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000),
            signal=SignalConfig(
                score_threshold=0.5,
                max_candidates_per_day=2,
            ),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(max_holding_days=5),
            reporting=ReportingConfig(),
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave_head_score",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        candidates = runner._select_candidates_for_date(event_date)

        assert [candidate.symbol for candidate in candidates] == ["MSFT", "AAPL"]


@pytest.mark.integration
class TestWalkForwardRunIntegration:
    def test_run_walk_forward_generates_train_and_test_folds(self, tmp_path, monkeypatch):
        from apps.backtester.run import run_walk_forward
        from libs.backtest.domain import ExperimentManifest
        from libs.backtest.snapshot_store import SnapshotStore

        trading_dates = [
            dt.date(2026, 1, 5),
            dt.date(2026, 1, 6),
            dt.date(2026, 1, 7),
            dt.date(2026, 1, 8),
            dt.date(2026, 1, 9),
            dt.date(2026, 1, 12),
            dt.date(2026, 1, 13),
        ]
        bars: dict[str, dict[dt.date, dict[str, float | dt.date | int]]] = {}
        candidates_by_date: dict[dt.date, list[dict[str, object]]] = {}
        for idx, exec_date in enumerate(trading_dates):
            symbol = f"SYM{idx}"
            candidates_by_date[exec_date] = [
                {
                    "event_id": f"EVT::{idx}",
                    "symbol": symbol,
                    "execution_date": exec_date,
                    "entry_date": exec_date.isoformat(),
                    "event_date": (exec_date - dt.timedelta(days=1)).isoformat(),
                    "event_close": 100.0,
                    "entry_price": 100.0,
                    "score": 0.9,
                    "sector": "Technology",
                    "event_type": "earnings_release",
                    "event_timestamp": f"{(exec_date - dt.timedelta(days=1)).isoformat()}T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": exec_date.isoformat(),
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 2.0,
                }
            ]
            bars[symbol] = {
                exec_date: {
                    "date": exec_date,
                    "open": 100.0,
                    "high": 106.0,
                    "low": 99.0,
                    "close": 105.0,
                    "volume": 1_000_000,
                },
                exec_date + dt.timedelta(days=1): {
                    "date": exec_date + dt.timedelta(days=1),
                    "open": 105.0,
                    "high": 110.0,
                    "low": 104.0,
                    "close": 109.0,
                    "volume": 900_000,
                },
            }

        store_map = {
            "train": SnapshotStore(
                candidates_by_exec_date={d: candidates_by_date[d] for d in trading_dates[:3]},
                bars_by_symbol_date=bars,
            ),
            "valid": SnapshotStore(
                candidates_by_exec_date={d: candidates_by_date[d] for d in trading_dates[3:5]},
                bars_by_symbol_date=bars,
            ),
            "test": SnapshotStore(
                candidates_by_exec_date={d: candidates_by_date[d] for d in trading_dates[5:]},
                bars_by_symbol_date=bars,
            ),
        }

        def _fake_build_store(manifest, config, split_name, snapshot_dir_override=None):
            return store_map[split_name]

        merged_store = SnapshotStore(
            candidates_by_exec_date=candidates_by_date,
            bars_by_symbol_date=bars,
        )

        monkeypatch.setattr("apps.backtester.run._build_store", _fake_build_store)
        monkeypatch.setattr("apps.backtester.run._build_merged_snapshot_store", lambda *args, **kwargs: merged_store)

        manifest = ExperimentManifest(
            experiment_name="wf_test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        summary = run_walk_forward(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path),
            train_days=3,
            test_days=2,
            step_days=2,
        )

        assert summary.fold_count == 2
        assert summary.folds[0].train_end < summary.folds[0].test_start
        assert summary.folds[0].train_run_id
        assert summary.folds[0].test_run_id
        assert summary.test_aggregate.mean_return_pct is not None
        assert (tmp_path / "walk_forward" / "walk_forward_summary.json").exists()

    def test_run_walk_forward_respects_explicit_date_window(self, tmp_path, monkeypatch):
        from apps.backtester.run import run_walk_forward
        from libs.backtest.domain import ExperimentManifest
        from libs.backtest.snapshot_store import SnapshotStore

        trading_dates = [
            dt.date(2026, 1, 5),
            dt.date(2026, 1, 6),
            dt.date(2026, 1, 7),
            dt.date(2026, 1, 8),
            dt.date(2026, 1, 9),
            dt.date(2026, 1, 12),
            dt.date(2026, 1, 13),
        ]
        bars: dict[str, dict[dt.date, dict[str, float | dt.date | int]]] = {}
        candidates_by_date: dict[dt.date, list[dict[str, object]]] = {}
        for idx, exec_date in enumerate(trading_dates):
            symbol = f"WFS{idx}"
            candidates_by_date[exec_date] = [
                {
                    "event_id": f"WFS::{idx}",
                    "symbol": symbol,
                    "execution_date": exec_date,
                    "entry_date": exec_date.isoformat(),
                    "event_date": (exec_date - dt.timedelta(days=1)).isoformat(),
                    "event_close": 100.0,
                    "entry_price": 100.0,
                    "score": 0.9,
                    "sector": "Technology",
                    "event_type": "earnings_release",
                    "event_timestamp": f"{(exec_date - dt.timedelta(days=1)).isoformat()}T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": exec_date.isoformat(),
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 2.0,
                }
            ]
            bars[symbol] = {
                exec_date: {
                    "date": exec_date,
                    "open": 100.0,
                    "high": 106.0,
                    "low": 99.0,
                    "close": 105.0,
                    "volume": 1_000_000,
                },
                exec_date + dt.timedelta(days=1): {
                    "date": exec_date + dt.timedelta(days=1),
                    "open": 105.0,
                    "high": 110.0,
                    "low": 104.0,
                    "close": 109.0,
                    "volume": 900_000,
                },
            }

        merged_store = SnapshotStore(
            candidates_by_exec_date=candidates_by_date,
            bars_by_symbol_date=bars,
        )

        monkeypatch.setattr("apps.backtester.run._build_merged_snapshot_store", lambda *args, **kwargs: merged_store)

        manifest = ExperimentManifest(
            experiment_name="wf_window_test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        full_summary = run_walk_forward(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path / "full"),
            train_days=3,
            test_days=2,
            step_days=2,
        )
        window_summary = run_walk_forward(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path / "window"),
            train_days=3,
            test_days=2,
            step_days=2,
            start_date=dt.date(2026, 1, 7),
            end_date=dt.date(2026, 1, 13),
        )

        assert window_summary.fold_count < full_summary.fold_count
        assert window_summary.folds[0].train_start >= dt.date(2026, 1, 7)
        assert window_summary.folds[-1].test_end <= dt.date(2026, 1, 13)


@pytest.mark.integration
class TestRobustnessMatrixRunIntegration:
    def test_run_robustness_matrix_generates_summary(self, tmp_path, monkeypatch):
        from apps.backtester.run import run_robustness_matrix
        from libs.backtest.domain import ExperimentManifest
        from libs.backtest.snapshot_store import SnapshotStore

        trading_dates = [
            dt.date(2026, 1, 5),
            dt.date(2026, 1, 6),
            dt.date(2026, 1, 7),
            dt.date(2026, 1, 8),
            dt.date(2026, 1, 9),
            dt.date(2026, 1, 12),
            dt.date(2026, 1, 13),
        ]
        bars: dict[str, dict[dt.date, dict[str, float | dt.date | int]]] = {}
        candidates_by_date: dict[dt.date, list[dict[str, object]]] = {}
        for idx, exec_date in enumerate(trading_dates):
            symbol = f"RM{idx}"
            candidates_by_date[exec_date] = [
                {
                    "event_id": f"RM::{idx}",
                    "symbol": symbol,
                    "execution_date": exec_date,
                    "entry_date": exec_date.isoformat(),
                    "event_date": (exec_date - dt.timedelta(days=1)).isoformat(),
                    "event_close": 100.0,
                    "entry_price": 100.0,
                    "score": 0.9,
                    "sector": "Technology",
                    "event_type": "earnings_release",
                    "event_timestamp": f"{(exec_date - dt.timedelta(days=1)).isoformat()}T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": exec_date.isoformat(),
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 2.0,
                }
            ]
            bars[symbol] = {
                exec_date: {
                    "date": exec_date,
                    "open": 100.0,
                    "high": 106.0,
                    "low": 99.0,
                    "close": 105.0,
                    "volume": 1_000_000,
                },
                exec_date + dt.timedelta(days=1): {
                    "date": exec_date + dt.timedelta(days=1),
                    "open": 105.0,
                    "high": 109.0,
                    "low": 104.0,
                    "close": 108.0,
                    "volume": 900_000,
                },
            }

        store_map = {
            "train": SnapshotStore(
                candidates_by_exec_date={d: candidates_by_date[d] for d in trading_dates[:3]},
                bars_by_symbol_date=bars,
            ),
            "valid": SnapshotStore(
                candidates_by_exec_date={d: candidates_by_date[d] for d in trading_dates[3:5]},
                bars_by_symbol_date=bars,
            ),
            "test": SnapshotStore(
                candidates_by_exec_date={d: candidates_by_date[d] for d in trading_dates[5:]},
                bars_by_symbol_date=bars,
            ),
        }

        def _fake_build_store(manifest, config, split_name, snapshot_dir_override=None):
            return store_map[split_name]

        monkeypatch.setattr("apps.backtester.run._build_store", _fake_build_store)
        monkeypatch.setattr("apps.backtester.run._build_merged_snapshot_store", lambda *args, **kwargs: merged_store)

        manifest = ExperimentManifest(
            experiment_name="rm_test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        summary = run_robustness_matrix(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path),
            horizons_days=[2, 4],
            step_days=2,
        )

        assert summary.overall_window_count > 0
        assert [item.horizon_days for item in summary.horizon_summaries] == [2, 4]
        assert all(item.window_count > 0 for item in summary.horizon_summaries)
        assert (tmp_path / "robustness_matrix" / "robustness_matrix_summary.json").exists()

    def test_run_robustness_matrix_respects_explicit_date_window(self, tmp_path, monkeypatch):
        from apps.backtester.run import run_robustness_matrix
        from libs.backtest.domain import ExperimentManifest
        from libs.backtest.snapshot_store import SnapshotStore

        trading_dates = [
            dt.date(2026, 1, 5),
            dt.date(2026, 1, 6),
            dt.date(2026, 1, 7),
            dt.date(2026, 1, 8),
            dt.date(2026, 1, 9),
            dt.date(2026, 1, 12),
            dt.date(2026, 1, 13),
        ]
        bars: dict[str, dict[dt.date, dict[str, float | dt.date | int]]] = {}
        candidates_by_date: dict[dt.date, list[dict[str, object]]] = {}
        for idx, exec_date in enumerate(trading_dates):
            symbol = f"RMW{idx}"
            candidates_by_date[exec_date] = [
                {
                    "event_id": f"RMW::{idx}",
                    "symbol": symbol,
                    "execution_date": exec_date,
                    "entry_date": exec_date.isoformat(),
                    "event_date": (exec_date - dt.timedelta(days=1)).isoformat(),
                    "event_close": 100.0,
                    "entry_price": 100.0,
                    "score": 0.9,
                    "sector": "Technology",
                    "event_type": "earnings_release",
                    "event_timestamp": f"{(exec_date - dt.timedelta(days=1)).isoformat()}T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": exec_date.isoformat(),
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 2.0,
                }
            ]
            bars[symbol] = {
                exec_date: {
                    "date": exec_date,
                    "open": 100.0,
                    "high": 106.0,
                    "low": 99.0,
                    "close": 105.0,
                    "volume": 1_000_000,
                },
                exec_date + dt.timedelta(days=1): {
                    "date": exec_date + dt.timedelta(days=1),
                    "open": 105.0,
                    "high": 109.0,
                    "low": 104.0,
                    "close": 108.0,
                    "volume": 900_000,
                },
            }

        merged_store = SnapshotStore(
            candidates_by_exec_date=candidates_by_date,
            bars_by_symbol_date=bars,
        )

        monkeypatch.setattr("apps.backtester.run._build_merged_snapshot_store", lambda *args, **kwargs: merged_store)

        manifest = ExperimentManifest(
            experiment_name="rm_window_test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        full_summary = run_robustness_matrix(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path / "full"),
            horizons_days=[2, 4],
            step_days=2,
        )
        window_summary = run_robustness_matrix(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path / "window"),
            horizons_days=[2, 4],
            step_days=2,
            start_date=dt.date(2026, 1, 7),
            end_date=dt.date(2026, 1, 13),
        )

        assert window_summary.overall_window_count < full_summary.overall_window_count
        assert all(item.window_count > 0 for item in window_summary.horizon_summaries)


@pytest.mark.integration
class TestRobustnessMatrixRunIntegration:
    def test_run_robustness_matrix_generates_summary(self, tmp_path, monkeypatch):
        from apps.backtester.run import run_robustness_matrix
        from libs.backtest.domain import ExperimentManifest
        from libs.backtest.snapshot_store import SnapshotStore

        trading_dates = [
            dt.date(2026, 1, 5),
            dt.date(2026, 1, 6),
            dt.date(2026, 1, 7),
            dt.date(2026, 1, 8),
            dt.date(2026, 1, 9),
            dt.date(2026, 1, 12),
            dt.date(2026, 1, 13),
        ]
        bars: dict[str, dict[dt.date, dict[str, float | dt.date | int]]] = {}
        candidates_by_date: dict[dt.date, list[dict[str, object]]] = {}
        for idx, exec_date in enumerate(trading_dates):
            symbol = f"RB{idx}"
            candidates_by_date[exec_date] = [
                {
                    "event_id": f"EVT::RB::{idx}",
                    "symbol": symbol,
                    "execution_date": exec_date,
                    "entry_date": exec_date.isoformat(),
                    "event_date": (exec_date - dt.timedelta(days=1)).isoformat(),
                    "event_close": 100.0,
                    "entry_price": 100.0,
                    "score": 0.9,
                    "sector": "Technology",
                    "event_type": "earnings_release",
                    "event_timestamp": f"{(exec_date - dt.timedelta(days=1)).isoformat()}T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": exec_date.isoformat(),
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 2.0,
                }
            ]
            bars[symbol] = {
                exec_date: {
                    "date": exec_date,
                    "open": 100.0,
                    "high": 106.0,
                    "low": 99.0,
                    "close": 105.0,
                    "volume": 1_000_000,
                },
                exec_date + dt.timedelta(days=1): {
                    "date": exec_date + dt.timedelta(days=1),
                    "open": 105.0,
                    "high": 110.0,
                    "low": 104.0,
                    "close": 109.0,
                    "volume": 900_000,
                },
            }

        merged_store = SnapshotStore(
            candidates_by_exec_date=candidates_by_date,
            bars_by_symbol_date=bars,
        )

        def _fake_build_store(manifest, config, split_name, snapshot_dir_override=None):
            return merged_store

        monkeypatch.setattr("apps.backtester.run._build_store", _fake_build_store)
        monkeypatch.setattr("apps.backtester.run._build_merged_snapshot_store", lambda *args, **kwargs: merged_store)

        manifest = ExperimentManifest(
            experiment_name="rb_test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        summary = run_robustness_matrix(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path),
            horizons_days=[2, 4],
            step_days=2,
        )

        assert summary.horizons_days == [2, 4]
        assert summary.step_days == 2
        assert summary.overall_window_count > 0
        assert len(summary.horizon_summaries) == 2
        assert all(item.window_count > 0 for item in summary.horizon_summaries)
        assert all(item.mean_return_pct is not None for item in summary.horizon_summaries)
        assert (tmp_path / "robustness_matrix" / "robustness_matrix_summary.json").exists()

    def test_run_robustness_matrix_respects_explicit_date_window(self, tmp_path, monkeypatch):
        from apps.backtester.run import run_robustness_matrix
        from libs.backtest.domain import ExperimentManifest
        from libs.backtest.snapshot_store import SnapshotStore

        trading_dates = [
            dt.date(2026, 1, 5),
            dt.date(2026, 1, 6),
            dt.date(2026, 1, 7),
            dt.date(2026, 1, 8),
            dt.date(2026, 1, 9),
            dt.date(2026, 1, 12),
            dt.date(2026, 1, 13),
        ]
        bars: dict[str, dict[dt.date, dict[str, float | dt.date | int]]] = {}
        candidates_by_date: dict[dt.date, list[dict[str, object]]] = {}
        for idx, exec_date in enumerate(trading_dates):
            symbol = f"RMWX{idx}"
            candidates_by_date[exec_date] = [
                {
                    "event_id": f"RMWX::{idx}",
                    "symbol": symbol,
                    "execution_date": exec_date,
                    "entry_date": exec_date.isoformat(),
                    "event_date": (exec_date - dt.timedelta(days=1)).isoformat(),
                    "event_close": 100.0,
                    "entry_price": 100.0,
                    "score": 0.9,
                    "sector": "Technology",
                    "event_type": "earnings_release",
                    "event_timestamp": f"{(exec_date - dt.timedelta(days=1)).isoformat()}T21:00:00+00:00",
                    "filing_time_bucket": "post_market",
                    "reaction_date": exec_date.isoformat(),
                    "avg_dollar_volume": 5_000_000.0,
                    "atr_14": 2.0,
                }
            ]
            bars[symbol] = {
                exec_date: {
                    "date": exec_date,
                    "open": 100.0,
                    "high": 106.0,
                    "low": 99.0,
                    "close": 105.0,
                    "volume": 1_000_000,
                },
                exec_date + dt.timedelta(days=1): {
                    "date": exec_date + dt.timedelta(days=1),
                    "open": 105.0,
                    "high": 109.0,
                    "low": 104.0,
                    "close": 108.0,
                    "volume": 900_000,
                },
            }

        merged_store = SnapshotStore(
            candidates_by_exec_date=candidates_by_date,
            bars_by_symbol_date=bars,
        )

        monkeypatch.setattr("apps.backtester.run._build_merged_snapshot_store", lambda *args, **kwargs: merged_store)

        manifest = ExperimentManifest(
            experiment_name="rm_window_test_exp",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = _make_config()

        full_summary = run_robustness_matrix(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path / "full"),
            horizons_days=[2, 4],
            step_days=2,
        )
        window_summary = run_robustness_matrix(
            manifest=manifest,
            config=config,
            snapshot_dir_override=None,
            initial_equity=100_000.0,
            output_root=str(tmp_path / "window"),
            horizons_days=[2, 4],
            step_days=2,
            start_date=dt.date(2026, 1, 7),
            end_date=dt.date(2026, 1, 13),
        )

        assert window_summary.overall_window_count < full_summary.overall_window_count
        assert all(item.window_count > 0 for item in window_summary.horizon_summaries)

    def test_delayed_entry_can_use_shadow_only_source_candidates(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        import pyarrow.parquet as pq
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExperimentManifest,
            ExecutionConfig,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8, 9]]
        store = SnapshotStore(
            candidates_by_exec_date={
                dates[0]: [
                    {
                        "event_id": "EVT::DRIFT::001",
                        "symbol": "AAPL",
                        "execution_date": dates[0],
                        "entry_date": dates[0].isoformat(),
                        "event_date": dates[0].isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.82,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": dates[0].isoformat(),
                        "reaction_day_return": 0.08,
                        "close_location": 0.82,
                        "volume_ratio_20d": 2.2,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 9_000_000.0,
                        "atr_14": 3.0,
                    }
                ],
                dates[1]: [],
                dates[2]: [],
                dates[3]: [],
                dates[4]: [],
            },
            bars_by_symbol_date={
                "AAPL": {
                    dates[0]: {"date": dates[0], "open": 98.0, "high": 101.0, "low": 97.0, "close": 100.0, "volume": 1_000_000},
                    dates[1]: {"date": dates[1], "open": 101.0, "high": 104.0, "low": 100.0, "close": 103.0, "volume": 900_000},
                    dates[2]: {"date": dates[2], "open": 103.0, "high": 106.0, "low": 102.0, "close": 105.0, "volume": 850_000},
                    dates[3]: {"date": dates[3], "open": 105.0, "high": 109.0, "low": 104.0, "close": 108.0, "volume": 800_000},
                    dates[4]: {"date": dates[4], "open": 109.0, "high": 111.0, "low": 108.0, "close": 110.0, "volume": 780_000},
                }
            },
        )

        manifest = ExperimentManifest(
            experiment_name="return_max_long_v1_shadow_delayed",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="reaction_close_long_core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=10,
                    engine_risk_budget_pct=1.0,
                    reaction_day_return_min=0.04,
                    reaction_day_return_max=0.20,
                    close_location_min=0.70,
                    volume_ratio_min=1.5,
                    score_threshold_override=0.60,
                    shadow_only=True,
                ),
                StrategyEngineConfig(
                    engine_id="delayed_primary_long_drift",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    max_holding_days=10,
                    engine_risk_budget_pct=1.0,
                    delayed_entry_lookback_days=3,
                    delayed_entry_source_engine_ids=["reaction_close_long_core"],
                    delayed_entry_min_drift_pct=0.05,
                    delayed_entry_close_location_min=0.50,
                    score_threshold_override=0.0,
                    per_trade_risk_pct_override=0.01,
                    synthetic_only=True,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=15.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.60, max_candidates_per_day=6, scoring_model="patient_drift"),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                per_trade_risk_pct_a_tier=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=6,
                max_positions_per_sector=3,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                target_model="fixed_r",
                target_1_r=99.0,
                target_1_fraction=0.0,
                trailing_model=None,
                trailing_warmup_days=10,
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            event_type_profiles={"earnings_release": EventTypeProfile(enabled=True, max_holding_days_override=10)},
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)
        run_dir = tmp_path / result.run_id
        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()

        engine_ids = [row["engine_id"] for row in trade_blotter]
        assert "reaction_close_long_core" not in engine_ids
        assert "delayed_primary_long_drift" in engine_ids

    def test_delayed_entry_preserves_parse_confidence_override(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        import pyarrow.parquet as pq
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExperimentManifest,
            ExecutionConfig,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        dates = [dt.date(2026, 1, d) for d in [5, 6, 7, 8, 9]]
        store = SnapshotStore(
            candidates_by_exec_date={
                dates[0]: [
                    {
                        "event_id": "EVT::PARSE::001",
                        "symbol": "AAPL",
                        "execution_date": dates[0],
                        "entry_date": dates[0].isoformat(),
                        "event_date": dates[0].isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.82,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-01-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": dates[0].isoformat(),
                        "reaction_day_return": 0.06,
                        "close_location": 0.70,
                        "volume_ratio_20d": 2.0,
                        "gap_size": 0.01,
                        "parse_confidence_overall": 0.30,
                        "avg_dollar_volume": 9_000_000.0,
                        "atr_14": 3.0,
                    }
                ],
                dates[1]: [],
                dates[2]: [],
                dates[3]: [],
                dates[4]: [],
            },
            bars_by_symbol_date={
                "AAPL": {
                    dates[0]: {"date": dates[0], "open": 98.0, "high": 101.0, "low": 97.0, "close": 100.0, "volume": 1_000_000},
                    dates[1]: {"date": dates[1], "open": 101.0, "high": 102.0, "low": 100.0, "close": 101.5, "volume": 900_000},
                    dates[2]: {"date": dates[2], "open": 101.5, "high": 104.0, "low": 101.0, "close": 103.2, "volume": 850_000},
                    dates[3]: {"date": dates[3], "open": 103.0, "high": 106.0, "low": 102.0, "close": 105.0, "volume": 800_000},
                    dates[4]: {"date": dates[4], "open": 105.0, "high": 107.0, "low": 104.0, "close": 106.0, "volume": 780_000},
                }
            },
        )

        manifest = ExperimentManifest(
            experiment_name="return_max_long_v1_shadow_delayed_parse",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="reaction_close_long_core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=10,
                    engine_risk_budget_pct=1.0,
                    reaction_day_return_min=0.04,
                    reaction_day_return_max=0.20,
                    close_location_min=0.60,
                    volume_ratio_min=1.5,
                    score_threshold_override=0.60,
                    shadow_only=True,
                ),
                StrategyEngineConfig(
                    engine_id="delayed_primary_long_parse",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    max_holding_days=10,
                    engine_risk_budget_pct=1.0,
                    delayed_entry_lookback_days=2,
                    delayed_entry_source_engine_ids=["reaction_close_long_core"],
                    delayed_entry_min_drift_pct=0.02,
                    delayed_entry_close_location_min=0.60,
                    score_threshold_override=0.0,
                    per_trade_risk_pct_override=0.01,
                    veto_parse_confidence_min_override=0.20,
                    synthetic_only=True,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=15.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.60, max_candidates_per_day=6, scoring_model="patient_drift"),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                per_trade_risk_pct_a_tier=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=6,
                max_positions_per_sector=3,
                veto_parse_confidence_min=0.40,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                target_model="fixed_r",
                target_1_r=99.0,
                target_1_fraction=0.0,
                trailing_model=None,
                trailing_warmup_days=10,
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            event_type_profiles={"earnings_release": EventTypeProfile(enabled=True, max_holding_days_override=10)},
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)
        run_dir = tmp_path / result.run_id
        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()

        assert len(trade_blotter) == 1
        assert trade_blotter[0]["engine_id"] == "delayed_primary_long_parse"

    def test_delayed_entry_preserves_no_progress_override(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        import pyarrow.parquet as pq
        from libs.backtest.domain import (
            BacktestConfig,
            EventTypeProfile,
            ExperimentManifest,
            ExecutionConfig,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        dates = [dt.date(2026, 2, d) for d in [2, 3, 4, 5, 6, 9, 10]]
        store = SnapshotStore(
            candidates_by_exec_date={
                dates[0]: [
                    {
                        "event_id": "EVT::NP::001",
                        "symbol": "AAPL",
                        "execution_date": dates[0],
                        "entry_date": dates[0].isoformat(),
                        "event_date": dates[0].isoformat(),
                        "event_close": 100.0,
                        "entry_price": 100.0,
                        "score": 0.82,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_timestamp": "2026-02-02T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": dates[0].isoformat(),
                        "reaction_day_return": 0.06,
                        "close_location": 0.70,
                        "volume_ratio_20d": 2.0,
                        "gap_size": 0.01,
                        "parse_confidence_overall": 0.80,
                        "avg_dollar_volume": 9_000_000.0,
                        "atr_14": 3.0,
                    }
                ],
                dates[1]: [],
                dates[2]: [],
                dates[3]: [],
                dates[4]: [],
                dates[5]: [],
                dates[6]: [],
            },
            bars_by_symbol_date={
                "AAPL": {
                    dates[0]: {"date": dates[0], "open": 98.0, "high": 101.0, "low": 97.0, "close": 100.0, "volume": 1_000_000},
                    dates[1]: {"date": dates[1], "open": 100.5, "high": 101.0, "low": 99.8, "close": 100.8, "volume": 900_000},
                    dates[2]: {"date": dates[2], "open": 100.7, "high": 101.2, "low": 100.1, "close": 100.9, "volume": 850_000},
                    dates[3]: {"date": dates[3], "open": 101.0, "high": 101.8, "low": 100.7, "close": 101.3, "volume": 820_000},
                    dates[4]: {"date": dates[4], "open": 101.2, "high": 102.0, "low": 100.9, "close": 101.6, "volume": 810_000},
                    dates[5]: {"date": dates[5], "open": 101.7, "high": 102.4, "low": 101.3, "close": 102.0, "volume": 800_000},
                    dates[6]: {"date": dates[6], "open": 102.0, "high": 102.5, "low": 101.6, "close": 102.2, "volume": 790_000},
                }
            },
        )

        manifest = ExperimentManifest(
            experiment_name="return_max_long_v1_shadow_delayed_np",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/return_max_long_v1.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="reaction_close_long_core",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="long_only",
                    entry_timing_policy="reaction_close",
                    max_holding_days=10,
                    engine_risk_budget_pct=1.0,
                    reaction_day_return_min=0.04,
                    reaction_day_return_max=0.20,
                    close_location_min=0.60,
                    volume_ratio_min=1.5,
                    score_threshold_override=0.60,
                    shadow_only=True,
                ),
                StrategyEngineConfig(
                    engine_id="delayed_primary_long_np",
                    event_types=["earnings_release"],
                    timing_class="any",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    max_holding_days=10,
                    engine_risk_budget_pct=1.0,
                    delayed_entry_lookback_days=2,
                    delayed_entry_source_engine_ids=["reaction_close_long_core"],
                    delayed_entry_min_drift_pct=0.005,
                    delayed_entry_close_location_min=0.55,
                    score_threshold_override=0.0,
                    per_trade_risk_pct_override=0.01,
                    early_failure_no_progress_days_override=5,
                    early_failure_no_progress_r_override=0.1,
                    early_failure_no_progress_fraction_override=1.0,
                    synthetic_only=True,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="return_max_long_v1",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=15.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.60, max_candidates_per_day=6, scoring_model="patient_drift"),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                per_trade_risk_pct_a_tier=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=6,
                max_positions_per_sector=3,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                target_model="fixed_r",
                target_1_r=99.0,
                target_1_fraction=0.0,
                trailing_model=None,
                trailing_warmup_days=10,
                max_holding_days=10,
                early_failure_no_progress_days=2,
                early_failure_no_progress_r=0.1,
                early_failure_no_progress_fraction=1.0,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            event_type_profiles={"earnings_release": EventTypeProfile(enabled=True, max_holding_days_override=10)},
            strategy_engines=manifest.strategy_engines,
            strategy_engine_selection_mode="interleave",
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        result = runner.run(output_root=tmp_path)
        run_dir = tmp_path / result.run_id
        trade_blotter = pq.read_table(run_dir / "artifacts" / "trade_blotter.parquet").to_pylist()

        assert len(trade_blotter) == 1
        assert trade_blotter[0]["engine_id"] == "delayed_primary_long_np"
        assert trade_blotter[0]["holding_days"] > 2

    def test_selected_candidates_are_annotated_with_daily_breadth_features(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            Candidate,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )

        store = _build_multi_engine_store()
        manifest = ExperimentManifest(
            experiment_name="breadth_annotation_smoke",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="same_day_short",
                    event_types=["earnings_release"],
                    timing_class="same_day",
                    direction="short_only",
                    entry_timing_policy="next_open",
                    reaction_day_return_max=-0.05,
                    score_threshold_override=0.50,
                ),
                StrategyEngineConfig(
                    engine_id="after_close_long",
                    event_types=["earnings_release"],
                    timing_class="after_close",
                    direction="long_only",
                    entry_timing_policy="next_open",
                    reaction_day_return_min=0.05,
                    score_threshold_override=0.50,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="breadth_annotation_smoke",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=5,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        selected = runner._select_candidates_for_date(dt.date(2026, 1, 7))

        assert len(selected) == 2
        for candidate in selected:
            assert candidate.features["daily_candidate_count_selected"] == 2
            assert candidate.features["daily_unique_sector_count_selected"] == 2
            assert candidate.features["daily_sector_candidate_count_selected"] == 1
            assert candidate.features["daily_engine_candidate_count_selected"] == 1

    def test_macro_bullish_engine_schedules_synthetic_qqq_candidate(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            Candidate,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        next_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={next_date: []},
            bars_by_symbol_date={
                "QQQ": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 102.0, "high": 107.0, "low": 101.5, "close": 106.0, "volume": 400},
                    next_date: {"date": next_date, "open": 106.5, "high": 108.0, "low": 105.0, "close": 107.5, "volume": 350},
                }
            },
            macro_by_date={signal_date: {"VIXCLS": 22.0}},
        )
        manifest = ExperimentManifest(
            experiment_name="macro_bullish_qqq_smoke",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="macro_bullish_qqq",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    max_holding_days=8,
                    engine_risk_budget_pct=0.25,
                    per_trade_risk_pct_override=0.01,
                    stop_atr_multiplier_override=2.0,
                    trailing_warmup_days_override=4,
                    macro_long_symbol="QQQ",
                    macro_long_reaction_day_return_min=0.015,
                    macro_long_volume_ratio_min=2.0,
                    macro_long_gap_size_min=0.01,
                    macro_long_close_location_min=0.75,
                    macro_long_min_daily_candidate_count=2,
                    macro_long_min_unique_sector_count=2,
                    macro_vix_max=30.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="macro_bullish_qqq_smoke",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, next_date]
        runner._next_trading_day = {signal_date: next_date}
        runner._recent_scored_candidates[signal_date] = [
            Candidate(
                event_id="BREADTH::1",
                symbol="AAPL",
                score=0.8,
                sector="Technology",
                event_type="earnings_release",
                event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
                filing_time_bucket="post_market",
                reaction_date=signal_date,
                execution_date=next_date,
                entry_price_est=100.0,
                avg_dollar_volume=1_000_000.0,
                atr_14=2.0,
                score_bucket="high",
                engine_id="core",
            ),
            Candidate(
                event_id="BREADTH::2",
                symbol="LLY",
                score=0.78,
                sector="Health Care",
                event_type="earnings_release",
                event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
                filing_time_bucket="post_market",
                reaction_date=signal_date,
                execution_date=next_date,
                entry_price_est=100.0,
                avg_dollar_volume=1_000_000.0,
                atr_14=2.0,
                score_bucket="high",
                engine_id="core",
            ),
        ]

        runner._schedule_macro_long_candidates(signal_date)

        scheduled = runner._scheduled_delayed_entries[next_date]
        assert len(scheduled) == 1
        candidate = scheduled[0]
        assert candidate.symbol == "QQQ"
        assert candidate.event_type == "macro_bullish_event"
        assert candidate.features["macro_long_daily_candidate_count"] == 2
        assert candidate.features["macro_long_daily_unique_sector_count"] == 2
        assert candidate.features["macro_long_reaction_day_return"] == pytest.approx(0.06)

    def test_leader_follower_engine_schedules_preentry_peer_candidate(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        entry_date = dt.date(2026, 1, 7)
        pre_event_date = dt.date(2026, 1, 8)
        follower_event_date = dt.date(2026, 1, 9)
        follower_exec_date = dt.date(2026, 1, 12)

        store = SnapshotStore(
            candidates_by_exec_date={
                entry_date: [
                    {
                        "event_id": "LEADER::NVDA",
                        "symbol": "NVDA",
                        "execution_date": entry_date,
                        "entry_date": str(entry_date),
                        "event_date": str(signal_date),
                        "event_close": 120.0,
                        "entry_price": 121.0,
                        "score": 0.86,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(signal_date),
                        "reaction_day_return": 0.14,
                        "close_location": 0.88,
                        "volume_ratio": 3.4,
                        "gap_size": 0.07,
                        "avg_dollar_volume": 50_000_000.0,
                        "market_cap_proxy": 100_000_000_000.0,
                        "document_quality_score": 0.80,
                        "parse_confidence_overall": 0.82,
                        "atr_14": 4.0,
                    }
                ],
                follower_exec_date: [
                    {
                        "event_id": "FOLLOWER::AMD",
                        "symbol": "AMD",
                        "execution_date": follower_exec_date,
                        "entry_date": str(follower_exec_date),
                        "event_date": str(follower_event_date),
                        "event_close": 51.5,
                        "entry_price": 52.0,
                        "score": 0.40,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "unknown",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-09T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(follower_event_date),
                        "reaction_day_return": 0.03,
                        "close_location": 0.55,
                        "volume_ratio": 1.4,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 30_000_000.0,
                        "market_cap_proxy": 60_000_000_000.0,
                        "document_quality_score": 0.60,
                        "parse_confidence_overall": 0.60,
                        "atr_14": 2.5,
                    }
                ],
            },
            bars_by_symbol_date={
                "NVDA": {
                    signal_date: {"date": signal_date, "open": 108.0, "high": 121.0, "low": 107.0, "close": 120.0, "volume": 4_000_000},
                    entry_date: {"date": entry_date, "open": 121.0, "high": 123.0, "low": 118.0, "close": 122.0, "volume": 3_000_000},
                },
                "AMD": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 49.5, "high": 50.5, "low": 49.0, "close": 50.0, "volume": 1_000_000},
                    signal_date: {"date": signal_date, "open": 50.2, "high": 51.6, "low": 49.8, "close": 51.0, "volume": 1_100_000},
                    entry_date: {"date": entry_date, "open": 51.1, "high": 52.0, "low": 50.8, "close": 51.7, "volume": 1_050_000},
                    pre_event_date: {"date": pre_event_date, "open": 51.9, "high": 52.6, "low": 51.4, "close": 52.3, "volume": 1_030_000},
                    follower_event_date: {"date": follower_event_date, "open": 52.4, "high": 53.4, "low": 51.8, "close": 53.0, "volume": 1_200_000},
                    follower_exec_date: {"date": follower_exec_date, "open": 54.5, "high": 55.0, "low": 53.5, "close": 54.0, "volume": 1_250_000},
                },
            },
        )
        manifest = ExperimentManifest(
            experiment_name="leader_follower_preentry_smoke",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="leader_follower_preentry",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    filing_time_buckets=["post_market"],
                    allowed_sectors=["Technology"],
                    direction="long_only",
                    max_holding_days=5,
                    engine_risk_budget_pct=0.05,
                    per_trade_risk_pct_override=0.01,
                    reaction_day_return_min=0.10,
                    close_location_min=0.75,
                    volume_ratio_min=2.0,
                    gap_size_min=0.04,
                    min_market_cap_proxy=40_000_000_000.0,
                    document_quality_score_min=0.5,
                    parse_confidence_overall_min=0.5,
                    leader_follower_lookahead_days=3,
                    leader_follower_min_days_to_event=2,
                    leader_follower_hold_buffer_days=1,
                    proxy_reaction_day_return_max=0.04,
                    proxy_gap_size_max=0.03,
                    proxy_close_location_max=0.80,
                    proxy_avg_dollar_volume_min=10_000_000.0,
                    next_open_gap_cap_pct=0.08,
                    stop_atr_multiplier_override=2.5,
                    trailing_warmup_days_override=3,
                    early_failure_no_progress_days_override=1,
                    early_failure_no_progress_r_override=0.0,
                    early_failure_no_progress_fraction_override=1.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="leader_follower_preentry_smoke",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, entry_date, pre_event_date, follower_event_date, follower_exec_date]
        runner._next_trading_day = {
            signal_date: entry_date,
            entry_date: pre_event_date,
            pre_event_date: follower_event_date,
            follower_event_date: follower_exec_date,
        }

        runner._schedule_leader_follower_candidates(signal_date)

        scheduled = runner._scheduled_delayed_entries[entry_date]
        assert len(scheduled) == 1
        candidate = scheduled[0]
        assert candidate.symbol == "AMD"
        assert candidate.source_symbol == "NVDA"
        assert candidate.event_type == "leader_follower_preearnings"
        assert candidate.engine_max_holding_days == 1
        assert candidate.features["leader_follower_days_to_event"] == 2
        assert candidate.features["leader_symbol"] == "NVDA"
        assert candidate.features["follower_symbol"] == "AMD"

    def test_leader_follower_prefers_calm_orderly_follower(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        entry_date = dt.date(2026, 1, 7)
        mid_date = dt.date(2026, 1, 8)
        crm_reaction_date = dt.date(2026, 1, 9)
        nvda_reaction_date = crm_reaction_date
        crm_exec_date = dt.date(2026, 1, 12)
        nvda_exec_date = dt.date(2026, 1, 13)
        store = SnapshotStore(
            candidates_by_exec_date={
                entry_date: [
                    {
                        "event_id": "LEADER::SNOW",
                        "symbol": "SNOW",
                        "execution_date": entry_date,
                        "entry_date": str(entry_date),
                        "event_date": str(signal_date),
                        "event_close": 116.0,
                        "entry_price": 116.5,
                        "score": 0.86,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(signal_date),
                        "reaction_day_return": 0.16,
                        "close_location": 0.87,
                        "volume_ratio": 3.2,
                        "gap_size": 0.06,
                        "avg_dollar_volume": 90_000_000.0,
                        "market_cap_proxy": 80_000_000_000.0,
                        "document_quality_score": 0.8,
                        "parse_confidence_overall": 0.8,
                        "atr_14": 4.0,
                    }
                ],
                crm_exec_date: [
                    {
                        "event_id": "FOLLOWER::CRM",
                        "symbol": "CRM",
                        "execution_date": crm_exec_date,
                        "entry_date": str(crm_exec_date),
                        "event_date": str(crm_reaction_date),
                        "event_close": 96.4,
                        "entry_price": 96.6,
                        "score": 0.4,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "unknown",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-09T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(crm_reaction_date),
                        "reaction_day_return": 0.03,
                        "close_location": 0.55,
                        "volume_ratio": 1.3,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 50_000_000.0,
                        "market_cap_proxy": 150_000_000_000.0,
                        "document_quality_score": 0.7,
                        "parse_confidence_overall": 0.7,
                        "atr_14": 3.0,
                    }
                ],
                nvda_exec_date: [
                    {
                        "event_id": "FOLLOWER::NVDA",
                        "symbol": "NVDA",
                        "execution_date": nvda_exec_date,
                        "entry_date": str(nvda_exec_date),
                        "event_date": str(nvda_reaction_date),
                        "event_close": 98.8,
                        "entry_price": 99.0,
                        "score": 0.4,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "unknown",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-09T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(nvda_reaction_date),
                        "reaction_day_return": 0.03,
                        "close_location": 0.55,
                        "volume_ratio": 1.3,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 50_000_000.0,
                        "market_cap_proxy": 300_000_000_000.0,
                        "document_quality_score": 0.7,
                        "parse_confidence_overall": 0.7,
                        "atr_14": 3.0,
                    }
                ],
            },
            bars_by_symbol_date={
                "SNOW": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 1_000_000},
                    signal_date: {"date": signal_date, "open": 106.0, "high": 117.0, "low": 105.5, "close": 116.0, "volume": 3_200_000},
                    entry_date: {"date": entry_date, "open": 116.5, "high": 118.0, "low": 114.0, "close": 117.0, "volume": 2_500_000},
                },
                "CRM": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 1_000_000},
                    signal_date: {"date": signal_date, "open": 98.4, "high": 101.0, "low": 95.5, "close": 96.4, "volume": 1_800_000},
                    entry_date: {"date": entry_date, "open": 96.0, "high": 97.0, "low": 95.0, "close": 96.5, "volume": 1_200_000},
                },
                "NVDA": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 1_000_000},
                    signal_date: {"date": signal_date, "open": 97.9, "high": 99.65, "low": 97.5, "close": 98.8, "volume": 950_000},
                    entry_date: {"date": entry_date, "open": 98.9, "high": 100.2, "low": 98.0, "close": 99.8, "volume": 1_100_000},
                },
            },
        )
        manifest = ExperimentManifest(
            experiment_name="leader_follower_prefers_orderly",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="leader_follower_preentry",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    filing_time_buckets=["post_market"],
                    allowed_sectors=["Technology"],
                    direction="long_only",
                    max_holding_days=5,
                    engine_risk_budget_pct=0.05,
                    per_trade_risk_pct_override=0.01,
                    reaction_day_return_min=0.10,
                    close_location_min=0.75,
                    volume_ratio_min=2.0,
                    gap_size_min=0.04,
                    min_market_cap_proxy=40_000_000_000.0,
                    document_quality_score_min=0.5,
                    parse_confidence_overall_min=0.5,
                    leader_follower_lookahead_days=3,
                    leader_follower_min_days_to_event=2,
                    leader_follower_hold_buffer_days=1,
                    proxy_reaction_day_return_max=0.05,
                    proxy_gap_size_max=0.03,
                    proxy_close_location_max=0.85,
                    proxy_avg_dollar_volume_min=10_000_000.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="leader_follower_prefers_orderly",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, entry_date, mid_date, crm_reaction_date, nvda_exec_date]
        runner._next_trading_day = {
            signal_date: entry_date,
            entry_date: mid_date,
            mid_date: crm_reaction_date,
            crm_reaction_date: nvda_exec_date,
        }

        runner._schedule_leader_follower_candidates(signal_date)

        scheduled = runner._scheduled_delayed_entries[entry_date]
        assert len(scheduled) == 2
        assert scheduled[0].symbol == "NVDA"
        assert scheduled[1].symbol == "CRM"
        assert scheduled[0].score > scheduled[1].score

    def test_leader_follower_engine_uses_pit_calendar_when_available(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.earnings_calendar import load_pit_earnings_calendar
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        entry_date = dt.date(2026, 1, 7)
        pre_event_date = dt.date(2026, 1, 8)
        follower_reaction_date = dt.date(2026, 1, 9)
        follower_exec_date = dt.date(2026, 1, 12)

        calendar_path = tmp_path / "earnings_calendar_pit.parquet"
        pq.write_table(
            pa.Table.from_pylist(
                [
                    {
                        "symbol": "AMD",
                        "as_of_date": signal_date.isoformat(),
                        "expected_reaction_date": follower_reaction_date.isoformat(),
                        "expected_event_date": pre_event_date.isoformat(),
                        "filing_time_bucket": "post_market",
                        "source": "unit_test",
                    }
                ]
            ),
            calendar_path,
        )
        load_pit_earnings_calendar.cache_clear()

        store = SnapshotStore(
            candidates_by_exec_date={
                entry_date: [
                    {
                        "event_id": "LEADER::NVDA",
                        "symbol": "NVDA",
                        "execution_date": entry_date,
                        "entry_date": str(entry_date),
                        "event_date": str(signal_date),
                        "event_close": 120.0,
                        "entry_price": 121.0,
                        "score": 0.86,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(signal_date),
                        "reaction_day_return": 0.14,
                        "close_location": 0.88,
                        "volume_ratio": 3.4,
                        "gap_size": 0.07,
                        "avg_dollar_volume": 50_000_000.0,
                        "market_cap_proxy": 100_000_000_000.0,
                        "document_quality_score": 0.80,
                        "parse_confidence_overall": 0.82,
                        "atr_14": 4.0,
                    }
                ],
            },
            bars_by_symbol_date={
                "NVDA": {
                    signal_date: {"date": signal_date, "open": 108.0, "high": 121.0, "low": 107.0, "close": 120.0, "volume": 4_000_000},
                    entry_date: {"date": entry_date, "open": 121.0, "high": 123.0, "low": 118.0, "close": 122.0, "volume": 3_000_000},
                },
                "AMD": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 49.5, "high": 50.5, "low": 49.0, "close": 50.0, "volume": 1_000_000},
                    signal_date: {"date": signal_date, "open": 50.2, "high": 51.6, "low": 49.8, "close": 51.0, "volume": 1_100_000},
                    entry_date: {"date": entry_date, "open": 51.1, "high": 52.0, "low": 50.8, "close": 51.7, "volume": 1_050_000},
                    pre_event_date: {"date": pre_event_date, "open": 51.9, "high": 52.6, "low": 51.4, "close": 52.3, "volume": 1_030_000},
                    follower_reaction_date: {"date": follower_reaction_date, "open": 52.4, "high": 53.4, "low": 51.8, "close": 53.0, "volume": 1_200_000},
                    follower_exec_date: {"date": follower_exec_date, "open": 54.5, "high": 55.0, "low": 53.5, "close": 54.0, "volume": 1_250_000},
                },
            },
        )
        manifest = ExperimentManifest(
            experiment_name="leader_follower_preentry_pit_smoke",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="leader_follower_preentry",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    filing_time_buckets=["post_market"],
                    allowed_sectors=["Technology"],
                    direction="long_only",
                    max_holding_days=5,
                    engine_risk_budget_pct=0.05,
                    per_trade_risk_pct_override=0.01,
                    reaction_day_return_min=0.10,
                    close_location_min=0.75,
                    volume_ratio_min=2.0,
                    gap_size_min=0.04,
                    min_market_cap_proxy=40_000_000_000.0,
                    document_quality_score_min=0.5,
                    parse_confidence_overall_min=0.5,
                    leader_follower_lookahead_days=3,
                    leader_follower_min_days_to_event=2,
                    leader_follower_hold_buffer_days=1,
                    leader_follower_calendar_mode="pit_calendar",
                    proxy_reaction_day_return_max=0.04,
                    proxy_gap_size_max=0.03,
                    proxy_close_location_max=0.80,
                    proxy_avg_dollar_volume_min=10_000_000.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="leader_follower_preentry_pit_smoke",
            dataset_snapshot_id="test_snapshot",
            earnings_calendar_pit_path=str(calendar_path),
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, entry_date, pre_event_date, follower_reaction_date, follower_exec_date]
        runner._next_trading_day = {
            signal_date: entry_date,
            entry_date: pre_event_date,
            pre_event_date: follower_reaction_date,
            follower_reaction_date: follower_exec_date,
        }

        runner._schedule_leader_follower_candidates(signal_date)

        scheduled = runner._scheduled_delayed_entries[entry_date]
        assert len(scheduled) == 1
        candidate = scheduled[0]
        assert candidate.symbol == "AMD"
        assert candidate.source_symbol == "NVDA"
        assert candidate.features["leader_follower_upcoming_reaction_date"] == follower_reaction_date.isoformat()

    def test_leader_follower_engine_uses_oracle_pit_calendar_without_local_file(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        class _FakeOracleCalendar:
            def get_known_upcoming_reaction_dates(self, as_of_date, allowed_reaction_dates, symbols=None):
                assert as_of_date == signal_date
                assert follower_reaction_date in allowed_reaction_dates
                assert "AMD" in (symbols or [])
                return {"AMD": follower_reaction_date}

        signal_date = dt.date(2026, 1, 6)
        entry_date = dt.date(2026, 1, 7)
        pre_event_date = dt.date(2026, 1, 8)
        follower_reaction_date = dt.date(2026, 1, 9)
        follower_exec_date = dt.date(2026, 1, 12)

        store = SnapshotStore(
            candidates_by_exec_date={
                entry_date: [
                    {
                        "event_id": "LEADER::NVDA",
                        "symbol": "NVDA",
                        "execution_date": entry_date,
                        "entry_date": str(entry_date),
                        "event_date": str(signal_date),
                        "event_close": 120.0,
                        "entry_price": 121.0,
                        "score": 0.86,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(signal_date),
                        "reaction_day_return": 0.14,
                        "close_location": 0.88,
                        "volume_ratio": 3.4,
                        "gap_size": 0.07,
                        "avg_dollar_volume": 50_000_000.0,
                        "market_cap_proxy": 100_000_000_000.0,
                        "document_quality_score": 0.80,
                        "parse_confidence_overall": 0.82,
                        "atr_14": 4.0,
                    }
                ],
            },
            bars_by_symbol_date={
                "NVDA": {
                    signal_date: {"date": signal_date, "open": 108.0, "high": 121.0, "low": 107.0, "close": 120.0, "volume": 4_000_000},
                    entry_date: {"date": entry_date, "open": 121.0, "high": 123.0, "low": 118.0, "close": 122.0, "volume": 3_000_000},
                },
                "AMD": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 49.5, "high": 50.5, "low": 49.0, "close": 50.0, "volume": 1_000_000},
                    signal_date: {"date": signal_date, "open": 50.2, "high": 51.6, "low": 49.8, "close": 51.0, "volume": 1_100_000},
                    entry_date: {"date": entry_date, "open": 51.1, "high": 52.0, "low": 50.8, "close": 51.7, "volume": 1_050_000},
                    pre_event_date: {"date": pre_event_date, "open": 51.9, "high": 52.6, "low": 51.4, "close": 52.3, "volume": 1_030_000},
                    follower_reaction_date: {"date": follower_reaction_date, "open": 52.4, "high": 53.4, "low": 51.8, "close": 53.0, "volume": 1_200_000},
                    follower_exec_date: {"date": follower_exec_date, "open": 54.5, "high": 55.0, "low": 53.5, "close": 54.0, "volume": 1_250_000},
                },
            },
        )
        manifest = ExperimentManifest(
            experiment_name="leader_follower_preentry_oracle_pit_smoke",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="leader_follower_preentry",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    filing_time_buckets=["post_market"],
                    allowed_sectors=["Technology"],
                    direction="long_only",
                    max_holding_days=5,
                    engine_risk_budget_pct=0.05,
                    per_trade_risk_pct_override=0.01,
                    reaction_day_return_min=0.10,
                    close_location_min=0.75,
                    volume_ratio_min=2.0,
                    gap_size_min=0.04,
                    min_market_cap_proxy=40_000_000_000.0,
                    document_quality_score_min=0.5,
                    parse_confidence_overall_min=0.5,
                    leader_follower_lookahead_days=3,
                    leader_follower_min_days_to_event=2,
                    leader_follower_hold_buffer_days=1,
                    leader_follower_calendar_mode="pit_calendar",
                    proxy_reaction_day_return_max=0.04,
                    proxy_gap_size_max=0.03,
                    proxy_close_location_max=0.80,
                    proxy_avg_dollar_volume_min=10_000_000.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="leader_follower_preentry_oracle_pit_smoke",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._pit_earnings_calendar = None
        runner._oracle_pit_earnings_calendar = _FakeOracleCalendar()
        runner._simulation_dates = [signal_date, entry_date, pre_event_date, follower_reaction_date, follower_exec_date]
        runner._next_trading_day = {
            signal_date: entry_date,
            entry_date: pre_event_date,
            pre_event_date: follower_reaction_date,
            follower_reaction_date: follower_exec_date,
        }

        runner._schedule_leader_follower_candidates(signal_date)

        scheduled = runner._scheduled_delayed_entries[entry_date]
        assert len(scheduled) == 1
        candidate = scheduled[0]
        assert candidate.symbol == "AMD"
        assert candidate.source_symbol == "NVDA"
        assert candidate.features["leader_follower_upcoming_reaction_date"] == follower_reaction_date.isoformat()

    def test_leader_follower_legacy_mode_ignores_pit_calendar(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.earnings_calendar import load_pit_earnings_calendar
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        entry_date = dt.date(2026, 1, 7)
        pre_event_date = dt.date(2026, 1, 8)
        follower_reaction_date = dt.date(2026, 1, 9)
        follower_exec_date = dt.date(2026, 1, 12)

        calendar_path = tmp_path / "earnings_calendar_pit.parquet"
        pq.write_table(
            pa.Table.from_pylist(
                [
                    {
                        "symbol": "BABA",
                        "as_of_date": signal_date.isoformat(),
                        "expected_reaction_date": follower_reaction_date.isoformat(),
                        "expected_event_date": pre_event_date.isoformat(),
                        "filing_time_bucket": "post_market",
                        "source": "unit_test",
                    }
                ]
            ),
            calendar_path,
        )
        load_pit_earnings_calendar.cache_clear()

        store = SnapshotStore(
            candidates_by_exec_date={
                entry_date: [
                    {
                        "event_id": "LEADER::NVDA",
                        "symbol": "NVDA",
                        "execution_date": entry_date,
                        "entry_date": str(entry_date),
                        "event_date": str(signal_date),
                        "event_close": 120.0,
                        "entry_price": 121.0,
                        "score": 0.86,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2026-01-06T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(signal_date),
                        "reaction_day_return": 0.14,
                        "close_location": 0.88,
                        "volume_ratio": 3.4,
                        "gap_size": 0.07,
                        "avg_dollar_volume": 50_000_000.0,
                        "market_cap_proxy": 100_000_000_000.0,
                        "document_quality_score": 0.80,
                        "parse_confidence_overall": 0.82,
                        "atr_14": 4.0,
                    }
                ],
                follower_exec_date: [
                    {
                        "event_id": "FOLLOWER::AMD",
                        "symbol": "AMD",
                        "execution_date": follower_exec_date,
                        "entry_date": str(follower_exec_date),
                        "event_date": str(follower_reaction_date),
                        "event_close": 51.5,
                        "entry_price": 52.0,
                        "score": 0.40,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "unknown",
                        "guidance_status": "not_provided",
                        "event_timestamp": "2026-01-09T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(follower_reaction_date),
                        "reaction_day_return": 0.03,
                        "close_location": 0.55,
                        "volume_ratio": 1.4,
                        "gap_size": 0.01,
                        "avg_dollar_volume": 30_000_000.0,
                        "market_cap_proxy": 60_000_000_000.0,
                        "document_quality_score": 0.60,
                        "parse_confidence_overall": 0.60,
                        "atr_14": 2.5,
                    }
                ],
            },
            bars_by_symbol_date={
                "NVDA": {
                    signal_date: {"date": signal_date, "open": 108.0, "high": 121.0, "low": 107.0, "close": 120.0, "volume": 4_000_000},
                    entry_date: {"date": entry_date, "open": 121.0, "high": 123.0, "low": 118.0, "close": 122.0, "volume": 3_000_000},
                },
                "AMD": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 49.5, "high": 50.5, "low": 49.0, "close": 50.0, "volume": 1_000_000},
                    signal_date: {"date": signal_date, "open": 50.2, "high": 51.6, "low": 49.8, "close": 51.0, "volume": 1_100_000},
                    entry_date: {"date": entry_date, "open": 51.1, "high": 52.0, "low": 50.8, "close": 51.7, "volume": 1_050_000},
                    pre_event_date: {"date": pre_event_date, "open": 51.9, "high": 52.6, "low": 51.4, "close": 52.3, "volume": 1_030_000},
                    follower_reaction_date: {"date": follower_reaction_date, "open": 52.4, "high": 53.4, "low": 51.8, "close": 53.0, "volume": 1_200_000},
                    follower_exec_date: {"date": follower_exec_date, "open": 54.5, "high": 55.0, "low": 53.5, "close": 54.0, "volume": 1_250_000},
                },
            },
        )
        manifest = ExperimentManifest(
            experiment_name="leader_follower_legacy_mode",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="leader_follower_preentry",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    filing_time_buckets=["post_market"],
                    allowed_sectors=["Technology"],
                    direction="long_only",
                    reaction_day_return_min=0.10,
                    close_location_min=0.75,
                    volume_ratio_min=2.0,
                    gap_size_min=0.04,
                    min_market_cap_proxy=40_000_000_000.0,
                    document_quality_score_min=0.5,
                    parse_confidence_overall_min=0.5,
                    leader_follower_lookahead_days=3,
                    leader_follower_min_days_to_event=2,
                    leader_follower_hold_buffer_days=1,
                    proxy_reaction_day_return_max=0.04,
                    proxy_gap_size_max=0.03,
                    proxy_close_location_max=0.80,
                    proxy_avg_dollar_volume_min=10_000_000.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="leader_follower_legacy_mode",
            dataset_snapshot_id="test_snapshot",
            earnings_calendar_pit_path=str(calendar_path),
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, entry_date, pre_event_date, follower_reaction_date, follower_exec_date]
        runner._next_trading_day = {
            signal_date: entry_date,
            entry_date: pre_event_date,
            pre_event_date: follower_reaction_date,
            follower_reaction_date: follower_exec_date,
        }

        runner._schedule_leader_follower_candidates(signal_date)
        scheduled = runner._scheduled_delayed_entries[entry_date]
        assert len(scheduled) == 1
        assert scheduled[0].symbol == "AMD"

    def test_leader_follower_engine_supports_engine_local_peer_overrides(self):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2025, 11, 5)
        entry_date = dt.date(2025, 11, 6)
        pre_event_date = dt.date(2025, 11, 7)
        follower_reaction_date = dt.date(2025, 11, 10)
        follower_exec_date = dt.date(2025, 11, 11)

        store = SnapshotStore(
            candidates_by_exec_date={
                entry_date: [
                    {
                        "event_id": "LEADER::NVDA",
                        "symbol": "NVDA",
                        "execution_date": entry_date,
                        "entry_date": str(entry_date),
                        "event_date": str(signal_date),
                        "event_close": 130.0,
                        "entry_price": 131.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2025-11-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(signal_date),
                        "reaction_day_return": 0.15,
                        "close_location": 0.86,
                        "volume_ratio": 3.0,
                        "gap_size": 0.06,
                        "avg_dollar_volume": 200_000_000.0,
                        "market_cap_proxy": 2_000_000_000_000.0,
                        "document_quality_score": 0.85,
                        "parse_confidence_overall": 0.90,
                        "atr_14": 4.0,
                    }
                ],
            },
            bars_by_symbol_date={
                "NVDA": {
                    signal_date: {"date": signal_date, "open": 120.0, "high": 132.0, "low": 118.0, "close": 130.0, "volume": 3_000_000},
                    entry_date: {"date": entry_date, "open": 131.0, "high": 133.0, "low": 128.0, "close": 132.0, "volume": 2_500_000},
                },
                "COHR": {
                    signal_date: {"date": signal_date, "open": 74.0, "high": 76.0, "low": 73.0, "close": 75.5, "volume": 500_000},
                    entry_date: {"date": entry_date, "open": 75.6, "high": 76.4, "low": 75.0, "close": 76.0, "volume": 480_000},
                    pre_event_date: {"date": pre_event_date, "open": 76.1, "high": 76.9, "low": 75.4, "close": 76.3, "volume": 470_000},
                    follower_reaction_date: {"date": follower_reaction_date, "open": 76.4, "high": 77.1, "low": 75.8, "close": 76.8, "volume": 510_000},
                    follower_exec_date: {"date": follower_exec_date, "open": 76.9, "high": 77.3, "low": 76.2, "close": 76.7, "volume": 450_000},
                },
            },
        )
        store._market_feature_cache[("COHR", signal_date)] = {
            "reaction_day_return": 0.01,
            "gap_size": 0.005,
            "close_location": 0.55,
            "volume_ratio_20d": 1.2,
            "avg_dollar_volume_20d": 120_000_000.0,
            "event_close": 75.5,
            "atr_14": 2.1,
        }

        class _FakeOracleCalendar:
            def get_known_upcoming_reaction_dates(self, *, as_of_date, allowed_reaction_dates, symbols):
                assert as_of_date == signal_date
                assert "COHR" in symbols
                return {"COHR": follower_reaction_date}

        manifest = ExperimentManifest(
            experiment_name="leader_follower_peer_override",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="leader_follower_preentry",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    filing_time_buckets=["post_market"],
                    allowed_sectors=["Technology"],
                    direction="long_only",
                    reaction_day_return_min=0.10,
                    close_location_min=0.75,
                    volume_ratio_min=2.0,
                    gap_size_min=0.04,
                    min_market_cap_proxy=40_000_000_000.0,
                    document_quality_score_min=0.5,
                    parse_confidence_overall_min=0.5,
                    leader_follower_lookahead_days=3,
                    leader_follower_min_days_to_event=2,
                    leader_follower_hold_buffer_days=1,
                    leader_follower_calendar_mode="pit_calendar",
                    leader_follower_extra_peer_symbols_by_leader={"NVDA": ["COHR"]},
                    leader_follower_allowed_peer_symbols=["COHR"],
                    proxy_reaction_day_return_max=0.04,
                    proxy_gap_size_max=0.03,
                    proxy_close_location_max=0.80,
                    proxy_avg_dollar_volume_min=10_000_000.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="leader_follower_peer_override",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._pit_earnings_calendar = None
        runner._oracle_pit_earnings_calendar = _FakeOracleCalendar()
        runner._simulation_dates = [signal_date, entry_date, pre_event_date, follower_reaction_date, follower_exec_date]
        runner._next_trading_day = {
            signal_date: entry_date,
            entry_date: pre_event_date,
            pre_event_date: follower_reaction_date,
            follower_reaction_date: follower_exec_date,
        }

        runner._schedule_leader_follower_candidates(signal_date)
        scheduled = runner._scheduled_delayed_entries[entry_date]
        assert len(scheduled) == 1
        assert scheduled[0].symbol == "COHR"
        assert scheduled[0].source_symbol == "NVDA"

    def test_leader_follower_engine_fetches_missing_peer_market_data(self, monkeypatch):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2025, 11, 5)
        entry_date = dt.date(2025, 11, 6)
        pre_event_date = dt.date(2025, 11, 7)
        follower_reaction_date = dt.date(2025, 11, 10)
        follower_exec_date = dt.date(2025, 11, 11)

        store = SnapshotStore(
            candidates_by_exec_date={
                entry_date: [
                    {
                        "event_id": "LEADER::NVDA",
                        "symbol": "NVDA",
                        "execution_date": entry_date,
                        "entry_date": str(entry_date),
                        "event_date": str(signal_date),
                        "event_close": 130.0,
                        "entry_price": 131.0,
                        "score": 0.90,
                        "sector": "Technology",
                        "event_type": "earnings_release",
                        "event_direction": "bullish",
                        "guidance_status": "raised",
                        "event_timestamp": "2025-11-05T21:00:00+00:00",
                        "filing_time_bucket": "post_market",
                        "reaction_date": str(signal_date),
                        "reaction_day_return": 0.15,
                        "close_location": 0.86,
                        "volume_ratio": 3.0,
                        "gap_size": 0.06,
                        "avg_dollar_volume": 200_000_000.0,
                        "market_cap_proxy": 2_000_000_000_000.0,
                        "document_quality_score": 0.85,
                        "parse_confidence_overall": 0.90,
                        "atr_14": 4.0,
                    }
                ],
            },
            bars_by_symbol_date={
                "NVDA": {
                    signal_date: {"date": signal_date, "open": 120.0, "high": 132.0, "low": 118.0, "close": 130.0, "volume": 3_000_000},
                    entry_date: {"date": entry_date, "open": 131.0, "high": 133.0, "low": 128.0, "close": 132.0, "volume": 2_500_000},
                },
            },
        )

        class _FakeOracleCalendar:
            def get_known_upcoming_reaction_dates(self, *, as_of_date, allowed_reaction_dates, symbols):
                assert as_of_date == signal_date
                assert "COHR" in symbols
                return {"COHR": follower_reaction_date}

        async def _fake_fetch_price_data(symbols, date_range, oracle_url, concurrency=8):
            assert "COHR" in symbols
            start_date, end_date = date_range
            current = start_date
            price = 60.0
            bars: dict[dt.date, dict[str, float | int | dt.date]] = {}
            while current <= end_date:
                if current.weekday() < 5:
                    bars[current] = {
                        "date": current,
                        "open": price,
                        "high": price * 1.01,
                        "low": price * 0.99,
                        "close": price * 1.002,
                        "volume": 2_000_000,
                    }
                    price += 0.2
                current += dt.timedelta(days=1)
            return {"COHR": bars}, {"COHR": 120_000_000.0}

        monkeypatch.setattr(SnapshotStore, "_fetch_price_data", staticmethod(_fake_fetch_price_data))

        manifest = ExperimentManifest(
            experiment_name="leader_follower_peer_fetch",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="leader_follower_preentry",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    event_types=["earnings_release"],
                    event_directions=["bullish"],
                    guidance_statuses=["raised"],
                    filing_time_buckets=["post_market"],
                    allowed_sectors=["Technology"],
                    direction="long_only",
                    reaction_day_return_min=0.10,
                    close_location_min=0.75,
                    volume_ratio_min=2.0,
                    gap_size_min=0.04,
                    min_market_cap_proxy=40_000_000_000.0,
                    document_quality_score_min=0.5,
                    parse_confidence_overall_min=0.5,
                    leader_follower_lookahead_days=3,
                    leader_follower_min_days_to_event=2,
                    leader_follower_hold_buffer_days=1,
                    leader_follower_calendar_mode="pit_calendar",
                    leader_follower_extra_peer_symbols_by_leader={"NVDA": ["COHR"]},
                    leader_follower_allowed_peer_symbols=["COHR"],
                    proxy_reaction_day_return_max=0.05,
                    proxy_gap_size_max=0.04,
                    proxy_close_location_min=0.3,
                    proxy_close_location_max=0.85,
                    proxy_avg_dollar_volume_min=10_000_000.0,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="leader_follower_peer_fetch",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._pit_earnings_calendar = None
        runner._oracle_pit_earnings_calendar = _FakeOracleCalendar()
        runner._simulation_dates = [signal_date, entry_date, pre_event_date, follower_reaction_date, follower_exec_date]
        runner._next_trading_day = {
            signal_date: entry_date,
            entry_date: pre_event_date,
            pre_event_date: follower_reaction_date,
            follower_reaction_date: follower_exec_date,
        }

        runner._schedule_leader_follower_candidates(signal_date)
        scheduled = runner._scheduled_delayed_entries[entry_date]
        assert len(scheduled) == 1
        assert scheduled[0].symbol == "COHR"
        assert store.get_latest_bar_on_or_before("COHR", signal_date) is not None

    def test_macro_bullish_engine_respects_breadth_gate(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            Candidate,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        next_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={next_date: []},
            bars_by_symbol_date={
                "QQQ": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 102.0, "high": 107.0, "low": 101.5, "close": 106.0, "volume": 400},
                    next_date: {"date": next_date, "open": 106.5, "high": 108.0, "low": 105.0, "close": 107.5, "volume": 350},
                }
            },
            macro_by_date={signal_date: {"VIXCLS": 22.0}},
        )
        manifest = ExperimentManifest(
            experiment_name="macro_bullish_qqq_breadth_block",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="macro_bullish_qqq",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    macro_long_symbol="QQQ",
                    macro_long_reaction_day_return_min=0.015,
                    macro_long_volume_ratio_min=2.0,
                    macro_long_gap_size_min=0.01,
                    macro_long_close_location_min=0.75,
                    macro_long_min_daily_candidate_count=3,
                    macro_long_min_unique_sector_count=2,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="macro_bullish_qqq_breadth_block",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, next_date]
        runner._next_trading_day = {signal_date: next_date}
        runner._recent_scored_candidates[signal_date] = [
            Candidate(
                event_id="BREADTH::1",
                symbol="AAPL",
                score=0.8,
                sector="Technology",
                event_type="earnings_release",
                event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
                filing_time_bucket="post_market",
                reaction_date=signal_date,
                execution_date=next_date,
                entry_price_est=100.0,
                avg_dollar_volume=1_000_000.0,
                atr_14=2.0,
                score_bucket="high",
                engine_id="core",
            ),
            Candidate(
                event_id="BREADTH::2",
                symbol="LLY",
                score=0.78,
                sector="Health Care",
                event_type="earnings_release",
                event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
                filing_time_bucket="post_market",
                reaction_date=signal_date,
                execution_date=next_date,
                entry_price_est=100.0,
                avg_dollar_volume=1_000_000.0,
                atr_14=2.0,
                score_bucket="high",
                engine_id="core",
            ),
        ]

        runner._schedule_macro_long_candidates(signal_date)

        assert runner._scheduled_delayed_entries[next_date] == []

    def test_macro_bullish_engine_uses_etf_basket_breadth(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        next_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={next_date: []},
            bars_by_symbol_date={
                "SPY": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 101.0, "high": 103.0, "low": 100.5, "close": 102.0, "volume": 130},
                },
                "QQQ": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 102.0, "high": 107.0, "low": 101.5, "close": 106.0, "volume": 180},
                    next_date: {"date": next_date, "open": 106.5, "high": 108.0, "low": 105.0, "close": 107.5, "volume": 170},
                },
                "XLK": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 200.0, "high": 201.0, "low": 199.0, "close": 200.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 202.0, "high": 209.0, "low": 201.5, "close": 208.0, "volume": 160},
                },
                "SMH": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 300.0, "high": 301.0, "low": 299.0, "close": 300.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 304.0, "high": 314.0, "low": 303.0, "close": 312.0, "volume": 175},
                },
                "IWM": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 150.0, "high": 151.0, "low": 149.0, "close": 150.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 149.5, "high": 151.0, "low": 148.0, "close": 149.0, "volume": 95},
                },
            },
            macro_by_date={signal_date: {"VIXCLS": 22.0}},
        )
        manifest = ExperimentManifest(
            experiment_name="macro_bullish_etf_breadth",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="macro_bullish_qqq_etf_breadth",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    max_holding_days=8,
                    engine_risk_budget_pct=0.25,
                    per_trade_risk_pct_override=0.01,
                    stop_atr_multiplier_override=2.0,
                    trailing_warmup_days_override=4,
                    macro_long_symbol="QQQ",
                    macro_long_reaction_day_return_min=0.015,
                    macro_long_gap_size_min=0.01,
                    macro_long_close_location_min=0.75,
                    macro_long_breadth_symbols=["QQQ", "XLK", "SMH", "IWM"],
                    macro_long_min_breadth_count=2,
                    macro_long_breadth_reaction_day_return_min=0.015,
                    macro_long_breadth_close_location_min=0.70,
                    macro_long_leadership_vs_spy_min=0.02,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="macro_bullish_etf_breadth",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, next_date]
        runner._next_trading_day = {signal_date: next_date}

        runner._schedule_macro_long_candidates(signal_date)

        scheduled = runner._scheduled_delayed_entries[next_date]
        assert len(scheduled) == 1
        candidate = scheduled[0]
        assert candidate.symbol == "QQQ"
        assert candidate.features["macro_long_breadth_count"] == 3
        assert candidate.features["macro_long_breadth_symbols"] == ["QQQ", "XLK", "SMH"]
        assert candidate.features["macro_long_leadership_vs_spy"] == pytest.approx(0.04)
        assert candidate.features["macro_long_event_candidate_count"] == 0

    def test_macro_bullish_engine_can_trade_strongest_breadth_etf(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            Candidate,
            DailyPortfolioState,
            ExecutionConfig,
            ExperimentManifest,
            OpenPosition,
            PlannedOrder,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        next_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={next_date: []},
            bars_by_symbol_date={
                "SPY": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 101.0, "high": 103.0, "low": 100.5, "close": 102.0, "volume": 130},
                },
                "QQQ": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 101.0, "high": 105.0, "low": 100.5, "close": 104.0, "volume": 150},
                    next_date: {"date": next_date, "open": 104.5, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 140},
                },
                "XLK": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 200.0, "high": 201.0, "low": 199.0, "close": 200.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 205.0, "high": 219.0, "low": 204.0, "close": 217.0, "volume": 190},
                    next_date: {"date": next_date, "open": 217.5, "high": 221.0, "low": 216.0, "close": 220.0, "volume": 180},
                },
                "SMH": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 300.0, "high": 301.0, "low": 299.0, "close": 300.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 303.0, "high": 309.0, "low": 302.0, "close": 307.0, "volume": 160},
                    next_date: {"date": next_date, "open": 307.5, "high": 310.0, "low": 306.0, "close": 309.0, "volume": 150},
                },
            },
            macro_by_date={signal_date: {"VIXCLS": 22.0}},
        )
        manifest = ExperimentManifest(
            experiment_name="macro_bullish_etf_breadth_leader",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="macro_bullish_qqq_etf_breadth_leader",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    macro_long_symbol="QQQ",
                    macro_long_trade_symbol_mode="leader",
                    macro_long_reaction_day_return_min=0.015,
                    macro_long_close_location_min=0.60,
                    macro_long_breadth_symbols=["QQQ", "XLK", "SMH"],
                    macro_long_min_breadth_count=2,
                    macro_long_breadth_reaction_day_return_min=0.015,
                    macro_long_breadth_close_location_min=0.60,
                    macro_long_leadership_vs_spy_min=0.01,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="macro_bullish_etf_breadth_leader",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, next_date]
        runner._next_trading_day = {signal_date: next_date}

        runner._schedule_macro_long_candidates(signal_date)

        scheduled = runner._scheduled_delayed_entries[next_date]
        assert len(scheduled) == 1
        candidate = scheduled[0]
        assert candidate.symbol == "XLK"
        assert candidate.source_symbol == "QQQ"
        assert candidate.features["macro_long_trade_symbol"] == "XLK"
        assert candidate.features["macro_long_trade_symbol_mode"] == "leader"

    def test_capital_bucket_reserves_cash_for_macro_sleeve(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            Candidate,
            DailyPortfolioState,
            ExecutionConfig,
            ExperimentManifest,
            OpenPosition,
            PlannedOrder,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={},
            bars_by_symbol_date={
                "XLK": {
                    date: {"date": date, "open": 210.0, "high": 212.0, "low": 209.0, "close": 211.0, "volume": 100},
                }
            },
        )
        manifest = ExperimentManifest(
            experiment_name="macro_capital_bucket_reserve",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(engine_id="core"),
                StrategyEngineConfig(
                    engine_id="macro_bullish_qqq_etf_breadth",
                    synthetic_only=True,
                    capital_bucket_id="macro_etf",
                    capital_bucket_allocation_pct=0.05,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="macro_capital_bucket_reserve",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        portfolio_state = DailyPortfolioState(
            date=date,
            equity=100_000.0,
            sizing_equity=100_000.0,
            cash_available=100_000.0,
            gross_exposure=0.0,
            net_exposure=0.0,
            reserved_risk_budget=0.0,
            unrealized_pnl=0.0,
            realized_pnl=0.0,
            open_positions=[],
            daily_new_risk_used=0.0,
            peak_equity=100_000.0,
            current_drawdown_pct=0.0,
        )
        core_candidate = Candidate(
            event_id="core::1",
            symbol="AAPL",
            score=0.7,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            filing_time_bucket="post_market",
            reaction_date=date,
            execution_date=date,
            entry_price_est=100.0,
            avg_dollar_volume=1_000_000.0,
            atr_14=2.0,
            score_bucket="high",
            engine_id="core",
        )
        macro_candidate = Candidate(
            event_id="macro::1",
            symbol="XLK",
            source_symbol="QQQ",
            score=0.7,
            sector="Technology",
            event_type="macro_bullish_event",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            filing_time_bucket="post_market",
            reaction_date=date,
            execution_date=date,
            entry_price_est=210.0,
            avg_dollar_volume=1_000_000.0,
            atr_14=4.0,
            score_bucket="high",
            engine_id="macro_bullish_qqq_etf_breadth",
            engine_capital_bucket_id="macro_etf",
            engine_capital_bucket_allocation_pct=0.05,
        )
        active_bucket_ids = runner._active_capital_bucket_ids_for_candidates([core_candidate, macro_candidate])

        core_state = runner._adjust_portfolio_state_for_candidate(
            date=date,
            candidate=core_candidate,
            portfolio_state=portfolio_state,
            active_bucket_ids=active_bucket_ids,
        )
        macro_state = runner._adjust_portfolio_state_for_candidate(
            date=date,
            candidate=macro_candidate,
            portfolio_state=portfolio_state,
            active_bucket_ids=active_bucket_ids,
        )

        assert core_state.cash_available == pytest.approx(95_000.0)
        assert macro_state.cash_available == pytest.approx(5_000.0)
        assert core_state.sizing_equity == pytest.approx(95_000.0)
        assert macro_state.sizing_equity == pytest.approx(5_000.0)

        macro_plan = PlannedOrder(
            candidate=macro_candidate,
            shares=10,
            entry_price_limit=210.0,
            stop_price=202.0,
            target_price=230.0,
            risk_dollars=80.0,
            event_date=date,
            timing_class="after_close",
            engine_id="macro_bullish_qqq_etf_breadth",
            entry_timing_policy="next_open",
        )
        runner._open_positions = [
            OpenPosition(
                position_id="macro-pos-1",
                plan=macro_plan,
                entry_date=date,
                entry_price=210.0,
                entry_fill_slippage_bps=0.0,
                current_stop=202.0,
                target_price=230.0,
                peak_price=211.0,
                shares_open=10,
                shares_total=10,
            )
        ]

        core_state_after_fill = runner._adjust_portfolio_state_for_candidate(
            date=date,
            candidate=core_candidate,
            portfolio_state=portfolio_state,
            active_bucket_ids=active_bucket_ids,
        )
        macro_state_after_fill = runner._adjust_portfolio_state_for_candidate(
            date=date,
            candidate=macro_candidate,
            portfolio_state=portfolio_state,
            active_bucket_ids=active_bucket_ids,
        )

        assert core_state_after_fill.cash_available == pytest.approx(97_100.0)
        assert macro_state_after_fill.cash_available == pytest.approx(2_900.0)
        assert core_state_after_fill.sizing_equity == pytest.approx(94_990.0)
        assert macro_state_after_fill.sizing_equity == pytest.approx(5_010.0)

    def test_capital_bucket_does_not_reserve_when_bucket_inactive(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            Candidate,
            DailyPortfolioState,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        date = dt.date(2026, 1, 7)
        store = SnapshotStore(candidates_by_exec_date={}, bars_by_symbol_date={})
        manifest = ExperimentManifest(
            experiment_name="macro_capital_bucket_inactive",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(engine_id="core"),
                StrategyEngineConfig(
                    engine_id="macro_bullish_qqq_etf_breadth",
                    synthetic_only=True,
                    capital_bucket_id="macro_etf",
                    capital_bucket_allocation_pct=0.05,
                ),
            ],
        )
        config = BacktestConfig(
            strategy_name="macro_capital_bucket_inactive",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )
        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        portfolio_state = DailyPortfolioState(
            date=date,
            equity=100_000.0,
            sizing_equity=100_000.0,
            cash_available=100_000.0,
            gross_exposure=0.0,
            net_exposure=0.0,
            reserved_risk_budget=0.0,
            unrealized_pnl=0.0,
            realized_pnl=0.0,
            open_positions=[],
            daily_new_risk_used=0.0,
            peak_equity=100_000.0,
            current_drawdown_pct=0.0,
        )
        core_candidate = Candidate(
            event_id="core::inactive",
            symbol="AAPL",
            score=0.7,
            sector="Technology",
            event_type="earnings_release",
            event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC),
            filing_time_bucket="post_market",
            reaction_date=date,
            execution_date=date,
            entry_price_est=100.0,
            avg_dollar_volume=1_000_000.0,
            atr_14=2.0,
            score_bucket="high",
            engine_id="core",
        )

        active_bucket_ids = runner._active_capital_bucket_ids_for_candidates([core_candidate])
        adjusted_state = runner._adjust_portfolio_state_for_candidate(
            date=date,
            candidate=core_candidate,
            portfolio_state=portfolio_state,
            active_bucket_ids=active_bucket_ids,
        )

        assert adjusted_state.cash_available == pytest.approx(100_000.0)
        assert adjusted_state.sizing_equity == pytest.approx(100_000.0)

    def test_parallel_sgov_marks_to_market_and_realizes_proportional_pnl(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        start = dt.date(2026, 1, 6)
        mark_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={},
            bars_by_symbol_date={},
            macro_by_date={
                start: {"sgov_close": 100.0},
                mark_date: {"sgov_close": 101.0},
            },
        )
        manifest = ExperimentManifest(
            experiment_name="parallel_sgov_mtm",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
        )
        config = BacktestConfig(
            strategy_name="parallel_sgov_mtm",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._cash = 95_000.0
        runner._parking_entry_date = start
        runner._allocate_parallel_sgov(date=start, amount=5_000.0, macro={"sgov_close": 100.0})

        assert runner._get_parking_value(mark_date) == pytest.approx(5_050.0)

        runner._liquidate_parking_for_cash(mark_date, 1_000.0)

        assert runner._cash == pytest.approx(96_000.0)
        assert runner._parking_sgov_value == pytest.approx(4_050.0)
        assert runner._parking_sgov_entry_value == pytest.approx(4_009.9009901)
        assert runner._realized_pnl == pytest.approx(9.9009901)

        runner._liquidate_parking(mark_date)

        assert runner._cash == pytest.approx(100_050.0)
        assert runner._realized_pnl == pytest.approx(50.0)

    def test_macro_bullish_engine_respects_leadership_vs_spy_gate(self, tmp_path):
        from apps.backtester.run import BacktestRunner
        from libs.backtest.domain import (
            BacktestConfig,
            ExecutionConfig,
            ExperimentManifest,
            ReportingConfig,
            RiskConfig,
            SignalConfig,
            StrategyEngineConfig,
            UniverseConfig,
        )
        from libs.backtest.snapshot_store import SnapshotStore

        signal_date = dt.date(2026, 1, 6)
        next_date = dt.date(2026, 1, 7)
        store = SnapshotStore(
            candidates_by_exec_date={next_date: []},
            bars_by_symbol_date={
                "SPY": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 102.0, "high": 105.0, "low": 101.5, "close": 104.0, "volume": 140},
                },
                "QQQ": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 101.5, "high": 104.0, "low": 101.0, "close": 103.0, "volume": 150},
                    next_date: {"date": next_date, "open": 103.5, "high": 105.0, "low": 102.0, "close": 104.0, "volume": 140},
                },
                "XLK": {
                    dt.date(2026, 1, 5): {"date": dt.date(2026, 1, 5), "open": 200.0, "high": 201.0, "low": 199.0, "close": 200.0, "volume": 100},
                    signal_date: {"date": signal_date, "open": 202.0, "high": 208.0, "low": 201.0, "close": 206.0, "volume": 150},
                },
            },
            macro_by_date={signal_date: {"VIXCLS": 22.0}},
        )
        manifest = ExperimentManifest(
            experiment_name="macro_bullish_etf_breadth_leadership_block",
            dataset_snapshot_id="test_snapshot",
            base_config="configs/backtest/defaults.json",
            overrides={},
            strategy_engines=[
                StrategyEngineConfig(
                    engine_id="macro_bullish_qqq_etf_breadth",
                    synthetic_only=True,
                    entry_timing_policy="next_open",
                    macro_long_symbol="QQQ",
                    macro_long_reaction_day_return_min=0.015,
                    macro_long_close_location_min=0.60,
                    macro_long_breadth_symbols=["QQQ", "XLK"],
                    macro_long_min_breadth_count=2,
                    macro_long_breadth_reaction_day_return_min=0.015,
                    macro_long_breadth_close_location_min=0.60,
                    macro_long_leadership_vs_spy_min=0.02,
                )
            ],
        )
        config = BacktestConfig(
            strategy_name="macro_bullish_etf_breadth_leadership_block",
            dataset_snapshot_id="test_snapshot",
            universe=UniverseConfig(min_price=5.0, min_avg_dollar_volume=100_000.0),
            signal=SignalConfig(score_threshold=0.50, max_candidates_per_day=5),
            risk=RiskConfig(
                per_trade_risk_pct=0.01,
                max_daily_new_risk_pct=0.05,
                max_positions=10,
                max_positions_per_sector=5,
            ),
            execution=ExecutionConfig(
                entry_fill_model="next_open",
                exit_fill_model="daily_bar_approximation",
                slippage_bps_base=0.0,
                commission_per_share=0.0,
                same_bar_priority="stop_first_conservative",
                max_holding_days=10,
            ),
            reporting=ReportingConfig(
                write_trade_blotter=True,
                write_equity_curve=True,
                write_metrics_summary=True,
                generate_plots=False,
            ),
            strategy_engines=manifest.strategy_engines,
        )

        runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0)
        runner._simulation_dates = [signal_date, next_date]
        runner._next_trading_day = {signal_date: next_date}

        runner._schedule_macro_long_candidates(signal_date)
        assert runner._scheduled_delayed_entries[next_date] == []
