Implement multi-engine PEAD strategy research workflow

main
I Luk Kim 7 months ago
parent 2395a0c0c3
commit 9ec0b26e10

@ -6,6 +6,7 @@ import datetime as dt
import json
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Any
@ -30,7 +31,7 @@ from libs.backtest.execution import (
)
from libs.backtest.manifests import generate_run_id, load_manifest, resolve_config
from libs.backtest.metrics import build_metrics_bundle
from libs.backtest.selector import select_candidates
from libs.backtest.selector import rank_candidates, select_candidates
from libs.backtest.snapshot_store import SnapshotStore
from libs.backtest.splits import generate_walk_forward_windows
from libs.common.logging import get_logger
@ -62,12 +63,15 @@ class BacktestRunner:
store: SnapshotStore,
initial_equity: float = 100_000.0,
split_name: str | None = None,
enable_engine_analysis: bool = True,
) -> None:
self.split_name = split_name
self.manifest = manifest
self.config = config
self.store = store
self.initial_equity = initial_equity
self.enable_engine_analysis = enable_engine_analysis
self._active_strategy_engines = self.config.get_active_strategy_engines()
# Simulation state
self._equity = initial_equity
@ -87,6 +91,7 @@ class BacktestRunner:
self._cooldown_remaining = 0
self._kill_switch_triggered = False
self._kill_switch_cooldown_remaining = 0
self._engine_daily_new_risk_used: dict[str, float] = defaultdict(float)
def run(self, output_root: str | Path | None = None) -> ExperimentResult:
"""Execute the full simulation. Returns ExperimentResult."""
@ -100,7 +105,7 @@ class BacktestRunner:
# Iterate ALL trading days (not just candidate days) so stop/target/time
# exits are checked every day, not just on days with new candidates.
all_dates = self.store.all_trading_days()
all_dates = self._get_simulation_dates()
# Record initial equity state (before any trades)
if all_dates:
@ -133,6 +138,11 @@ class BacktestRunner:
metrics = build_metrics_bundle(
self._closed_trades, self._equity_curve, self._candidate_map
)
per_engine_metrics = (
self._build_per_engine_metrics()
if self.enable_engine_analysis and self.config.get_strategy_engines()
else {}
)
# Create run directory and write artifacts
run_dir = None
@ -157,6 +167,7 @@ class BacktestRunner:
total_candidates_seen=self._total_candidates_seen,
total_orders_rejected=self._total_orders_rejected,
split_name=self.split_name,
per_engine_metrics=per_engine_metrics,
)
logger.info(
@ -183,6 +194,7 @@ class BacktestRunner:
"""Simulate a single trading day."""
# Reset daily risk tracker
self._daily_new_risk_used = 0.0
self._engine_daily_new_risk_used = defaultdict(float)
# Decrement cooldowns
if self._cooldown_remaining > 0:
@ -223,13 +235,7 @@ class BacktestRunner:
warmup_days=self.config.execution.trailing_warmup_days,
)
# Build effective execution config with per-event-type overrides
effective_exec = self.config.execution
evt_profile = self.config.get_event_profile(pos.plan.candidate.event_type)
if evt_profile and evt_profile.max_holding_days_override is not None:
effective_exec = self.config.execution.model_copy(
update={"max_holding_days": evt_profile.max_holding_days_override}
)
effective_exec = self._build_effective_execution_config(pos.plan.candidate)
prev_status = pos.status
trade = simulate_exit(pos, bar, effective_exec, date)
@ -302,13 +308,9 @@ class BacktestRunner:
# --- ENTRIES (only if kill switch not triggered) ---
if not self._kill_switch_triggered:
raw_rows = self.store.get_candidates_for_date(date)
self._total_candidates_seen += len(raw_rows)
portfolio_state = self._build_portfolio_state(date, drawdown_pct, unrealized)
candidates = select_candidates(
raw_rows, self.config.universe, self.config.signal,
event_type_profiles=self.config.event_type_profiles or None,
)
candidates = self._select_candidates_for_date(date)
self._total_candidates_seen += len(candidates)
macro_data = self.store.get_macro_for_date(date)
@ -320,6 +322,7 @@ class BacktestRunner:
config=self.config,
cooldown_remaining=self._cooldown_remaining,
macro_data=macro_data,
engine_daily_new_risk_used=self._engine_daily_new_risk_used[candidate.engine_id],
)
if plan.skip_reason is not None:
@ -332,12 +335,17 @@ class BacktestRunner:
)
continue
bar = self.store.get_bar(candidate.symbol, date)
pos = simulate_entry(plan, bar, self.config.execution)
bar = self.store.get_bar(candidate.symbol, candidate.execution_date)
pos = simulate_entry(
plan,
bar,
self._build_effective_execution_config(candidate),
)
if pos is not None:
self._open_positions.append(pos)
self._cash -= pos.entry_price * pos.shares_total
self._daily_new_risk_used += plan.risk_dollars
self._engine_daily_new_risk_used[candidate.engine_id] += plan.risk_dollars
# Update equity and portfolio state for next candidate
mv = self._compute_positions_market_value(date)
self._equity = self._cash + mv
@ -382,6 +390,141 @@ class BacktestRunner:
)
)
def _get_simulation_dates(self) -> list[dt.date]:
"""Return the full trading-day simulation range for the configured engines."""
if not self.config.get_strategy_engines():
return self.store.all_trading_days()
include_reaction_dates = any(
engine.entry_timing_policy == "reaction_close"
for engine in self._active_strategy_engines
)
return self.store.all_trading_days(include_reaction_dates=include_reaction_dates)
def _select_candidates_for_date(self, date: dt.date) -> list[Candidate]:
"""Select daily candidates for single-engine or multi-engine mode."""
if not self.config.get_strategy_engines():
raw_rows = self.store.get_candidates_for_date(date)
return select_candidates(
raw_rows,
self.config.universe,
self.config.signal,
event_type_profiles=self.config.event_type_profiles or None,
)
if not self._active_strategy_engines:
return []
engine_queues: dict[str, list[Candidate]] = {}
for engine in self._active_strategy_engines:
raw_rows = (
self.store.get_candidates_for_reaction_date(date)
if engine.entry_timing_policy == "reaction_close"
else self.store.get_candidates_for_date(date)
)
selected = select_candidates(
raw_rows,
self.config.universe,
self.config.signal,
event_type_profiles=self.config.event_type_profiles or None,
strategy_engine=engine,
)
if selected:
engine_queues[engine.engine_id] = selected
if self.config.strategy_engine_selection_mode == "global_score":
merged = []
for candidates in engine_queues.values():
merged.extend(candidates)
merged = rank_candidates(merged)
return merged[: self.config.signal.max_candidates_per_day]
return self._interleave_engine_candidates(engine_queues)
def _interleave_engine_candidates(
self,
engine_queues: dict[str, list[Candidate]],
) -> list[Candidate]:
"""Round-robin engine queues using manifest order."""
if not engine_queues:
return []
working = {
engine_id: list(candidates)
for engine_id, candidates in engine_queues.items()
}
ordered: list[Candidate] = []
while True:
advanced = False
for engine in self._active_strategy_engines:
queue = working.get(engine.engine_id, [])
if not queue:
continue
ordered.append(queue.pop(0))
advanced = True
if not advanced:
break
return ordered[: self.config.signal.max_candidates_per_day]
def _build_effective_execution_config(self, candidate: Candidate) -> Any:
"""Resolve per-engine and per-event holding-period overrides."""
max_holding_days = candidate.engine_max_holding_days
if max_holding_days is None:
evt_profile = self.config.get_event_profile(candidate.event_type)
if evt_profile and evt_profile.max_holding_days_override is not None:
max_holding_days = evt_profile.max_holding_days_override
if max_holding_days is None:
return self.config.execution
return self.config.execution.model_copy(
update={"max_holding_days": max_holding_days}
)
def _build_per_engine_metrics(self) -> dict[str, dict[str, Any]]:
"""Run each engine in isolation for standalone metrics and shadow summaries."""
summaries: dict[str, dict[str, Any]] = {}
for engine in self.config.get_strategy_engines():
isolated_engine = engine.model_copy(
update={
"shadow_only": False,
# Shadow engines should paper-trade freely for diagnostics.
"engine_risk_budget_pct": (
1.0 if engine.shadow_only else engine.engine_risk_budget_pct
),
}
)
isolated_manifest = self.manifest.model_copy(
update={"strategy_engines": [isolated_engine]}
)
isolated_config = self.config.model_copy(
update={
"strategy_name": f"{self.config.strategy_name}__{engine.engine_id}",
"strategy_engines": [isolated_engine],
}
)
runner = BacktestRunner(
manifest=isolated_manifest,
config=isolated_config,
store=self.store,
initial_equity=self.initial_equity,
split_name=self.split_name,
enable_engine_analysis=False,
)
result = runner.run(output_root=None)
summaries[engine.engine_id] = {
"engine_id": engine.engine_id,
"shadow_only": engine.shadow_only,
"event_types": list(engine.event_types),
"timing_class": engine.timing_class,
"direction": engine.direction,
"entry_timing_policy": engine.entry_timing_policy,
"max_holding_days": engine.max_holding_days,
"engine_risk_budget_pct": engine.engine_risk_budget_pct,
"total_candidates_seen": result.total_candidates_seen,
"total_orders_rejected": result.total_orders_rejected,
"net_pnl": round(sum(trade.net_pnl for trade in runner._closed_trades), 4),
"metrics": result.metrics.model_dump(mode="json"),
}
return summaries
def _compute_positions_market_value(self, date: dt.date) -> float:
"""Market value of all open positions using today's close.

@ -0,0 +1,81 @@
{
"experiment_name": "pead_midcap_portfolio_v2",
"dataset_snapshot_id": "midcap-filtered",
"description": "Portfolio V2: split PEAD earnings into specialist engines by timing and direction, with same-day long using reaction-close entry and after-close short kept as shadow.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any"},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"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,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close_v1",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 3,
"engine_risk_budget_pct": 0.35,
"shadow_only": false
},
{
"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,
"shadow_only": false
},
{
"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
}
],
"splits": [],
"tags": ["pead", "midcap", "portfolio_v2", "specialist_engines"],
"notes": null
}

@ -0,0 +1,82 @@
{
"experiment_name": "pead_midcap_step22_sdlong_close5",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 22: Preserve step14 profitable buckets, switch same-day longs to reaction-close entry with 5-day hold, and remove after-close shorts from active book.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close5",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 5,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_shadow",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": true
}
],
"splits": [],
"tags": ["pead", "midcap", "step22", "same_day_long_close5"],
"notes": null
}

@ -0,0 +1,82 @@
{
"experiment_name": "pead_midcap_step23_sdlong_close7",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 23: Same as step22 but keep same-day reaction-close longs on the original 7-day hold.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_shadow",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": true
}
],
"splits": [],
"tags": ["pead", "midcap", "step23", "same_day_long_close7"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step24_sdlong_close5_nofrac",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 24: Step22 plus full exit at target, combining the best same-day long entry change with the best return-side exit tweak.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close5",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 5,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_shadow",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": true
}
],
"splits": [],
"tags": ["pead", "midcap", "step24", "same_day_long_close5", "nofrac"],
"notes": null
}

@ -0,0 +1,62 @@
{
"experiment_name": "pead_midcap_step25_positive_buckets",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 25: Keep only the two positive step14 buckets: same-day shorts and after-close longs.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step25", "positive_buckets"],
"notes": null
}

@ -0,0 +1,63 @@
{
"experiment_name": "pead_midcap_step26_positive_buckets_nofrac",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 26: Step25 plus full exit at target to maximize realized gains in the two positive buckets.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step26", "positive_buckets", "nofrac"],
"notes": null
}

@ -0,0 +1,82 @@
{
"experiment_name": "pead_midcap_step27_sdlong_close7_budget25",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 27: Step23 with a smaller 25% daily risk sleeve for same-day reaction-close longs to reduce crowding.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_shadow",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": true
}
],
"splits": [],
"tags": ["pead", "midcap", "step27", "same_day_long_close7", "budget25"],
"notes": null
}

@ -0,0 +1,82 @@
{
"experiment_name": "pead_midcap_step28_sdlong_close7_budget12",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 28: Step23 with an even tighter 12.5% daily risk sleeve for same-day reaction-close longs.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_shadow",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": true
}
],
"splits": [],
"tags": ["pead", "midcap", "step28", "same_day_long_close7", "budget12"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step29_sdlong_close7_budget25_nofrac",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 29: Step27 plus full exit at target to combine better sleeve budgeting with the best return-side exit tweak.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_shadow",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": true
}
],
"splits": [],
"tags": ["pead", "midcap", "step29", "same_day_long_close7", "budget25", "nofrac"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step30_balanced_sleeves_nofrac",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 30: Keep all earnings sleeves but downweight the weaker long-close and after-close short buckets, with full exit at target.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step30", "balanced_sleeves", "nofrac"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step31_balanced_sleeves_nofrac_acshort12",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 31: Step30 with a smaller after-close short sleeve to keep trade count support while trimming the weaker bucket further.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step31", "balanced_sleeves", "nofrac", "acshort12"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step32_balanced_sleeves_nofrac_sdlong50",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 32: Step30 with a larger same-day long close sleeve to test whether the improved entry can carry more capital without breaking the portfolio.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.5,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step32", "balanced_sleeves", "nofrac", "sdlong50"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step33_balanced_sleeves_nofrac_acshort6",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 33: Step31 with an even smaller after-close short sleeve to test if the trade-count support remains while cutting more drag.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.0625,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step33", "balanced_sleeves", "nofrac", "acshort6"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step34_balanced_sleeves_nofrac_aclong25",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 34: Step31 with the after-close long sleeve capped to two trades per day, testing whether the weaker bucket improves when only the top-ranked names survive.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_top2",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step34", "balanced_sleeves", "nofrac", "aclong25"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step35_balanced_sleeves_nofrac_aclong12",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 35: Step31 with the after-close long sleeve capped to one trade per day, forcing the portfolio to lean harder on the stronger same-day sleeves.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_top1",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step35", "balanced_sleeves", "nofrac", "aclong12"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 36: Step35 plus a one-trade-per-day cap on the same-day long close sleeve, keeping only the single best close-entry long when multiple names trigger together.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_top1",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_top1",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step36", "balanced_sleeves", "nofrac", "aclong12", "sdlong12"],
"notes": null
}

@ -0,0 +1,84 @@
{
"experiment_name": "pead_midcap_step37_balanced_sleeves_aclong_vol3",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 37: Keep the step31 balanced sleeve structure, but require stronger volume confirmation for after-close long candidates only.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_vol3",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"pead_volume_threshold_override": 3.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step37", "balanced_sleeves", "aclong", "vol3"],
"notes": null
}

@ -0,0 +1,84 @@
{
"experiment_name": "pead_midcap_step38_balanced_sleeves_aclong_vol4",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 38: Step37 with an even stricter after-close long volume gate, testing whether only the highest-conviction overnight reactions should remain in that sleeve.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_vol4",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"pead_volume_threshold_override": 4.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step38", "balanced_sleeves", "aclong", "vol4"],
"notes": null
}

@ -0,0 +1,83 @@
{
"experiment_name": "pead_midcap_step39_balanced_sleeves_sdlong12",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 39: Keep the robust step31 sleeve mix, but cap same-day long close entries to one trade per day while leaving after-close longs unchanged.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_long_step14",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "long_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_top1",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step39", "balanced_sleeves", "sdlong12"],
"notes": null
}

@ -0,0 +1,73 @@
{
"experiment_name": "pead_midcap_step40_short_core_sdlong12",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 40: Remove the unstable after-close long sleeve and keep a short-led portfolio with a small same-day close-entry long overlay.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_small",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step40", "short_core", "sdlong12"],
"notes": null
}

@ -0,0 +1,73 @@
{
"experiment_name": "pead_midcap_step41_short_core_sdlong12_acshort50",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 41: Step40 with a larger after-close short sleeve, leaning harder into the more stable short continuation buckets.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.5,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_small",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step41", "short_core", "sdlong12", "acshort50"],
"notes": null
}

@ -0,0 +1,63 @@
{
"experiment_name": "pead_midcap_step42_short_core_only",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 42: Pure short-led portfolio using only same-day and after-close short sleeves.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step42", "short_core", "short_only"],
"notes": null
}

@ -0,0 +1,73 @@
{
"experiment_name": "pead_midcap_step43_short_core_sdlong25",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 43: Keep the step40 short-core structure but restore a larger same-day long close sleeve after removing after-close longs.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step43", "short_core", "sdlong25"],
"notes": null
}

@ -0,0 +1,75 @@
{
"experiment_name": "pead_midcap_step44_short_core_macro50",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 44: Apply a 50% size scaler in unfavorable SPY-below-SMA regimes on top of the step40 short-core structure.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 0.5
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_small",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step44", "short_core", "macro50"],
"notes": null
}

@ -0,0 +1,75 @@
{
"experiment_name": "pead_midcap_step45_short_core_macro_block",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 45: Hard block all new entries in unfavorable SPY-below-SMA regimes on top of the step40 short-core structure.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_small",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step45", "short_core", "macro_block"],
"notes": null
}

@ -0,0 +1,75 @@
{
"experiment_name": "pead_midcap_step46_short_core_macro_block_acshort12",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 46: Step45 with a smaller after-close short sleeve, testing whether the macro block leaves that bucket oversized on train.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core_small",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_small",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step46", "short_core", "macro_block", "acshort12"],
"notes": null
}

@ -0,0 +1,75 @@
{
"experiment_name": "pead_midcap_step47_short_core_macro_block_sdlong25",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 47: Step45 with a larger same-day long close sleeve, testing whether the macro block makes that overlay scalable again.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step47", "short_core", "macro_block", "sdlong25"],
"notes": null
}

@ -0,0 +1,65 @@
{
"experiment_name": "pead_midcap_step48_short_core_macro_block_nolong",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 48: Step45 without the same-day long sleeve, testing whether the macro-blocked short core is strong enough on its own.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step48", "short_core", "macro_block", "nolong"],
"notes": null
}

@ -0,0 +1,55 @@
{
"experiment_name": "pead_midcap_step49_same_day_short_macro_block",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 49: Pure same-day short sleeve under the macro block, testing whether the simplest engine dominates the portfolio.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step49", "same_day_short", "macro_block"],
"notes": null
}

@ -0,0 +1,65 @@
{
"experiment_name": "pead_midcap_step50_same_day_short_long_macro_block",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 50: Step45 without the after-close short sleeve, preserving the same-day short core plus the small reaction-close long overlay.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_small",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step50", "same_day", "macro_block"],
"notes": null
}

@ -0,0 +1,76 @@
{
"experiment_name": "pead_midcap_step51_short_core_macro_block_crashcap",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 51: Step45 plus an extreme-crash cap on the same-day short sleeve, avoiding follow-on shorts after one-day collapses worse than -45%.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14_capped",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"reaction_day_return_min": -0.45,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_small",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step51", "short_core", "macro_block", "crashcap"],
"notes": null
}

@ -0,0 +1,77 @@
{
"experiment_name": "pead_midcap_step52_short_core_macro_block_crashcap_gap10",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 52: Step51 plus a minimum gap-size filter on the same-day long overlay, keeping only larger reaction-day gaps.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14_capped",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"reaction_day_return_min": -0.45,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_gap10",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"gap_size_min": 0.10,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step52", "short_core", "macro_block", "crashcap", "gap10"],
"notes": null
}

@ -0,0 +1,77 @@
{
"experiment_name": "pead_midcap_step53_short_core_macro_block_crashcap_gap14",
"dataset_snapshot_id": "midcap-filtered",
"description": "Step 53: Step51 plus a stricter minimum gap-size filter on the same-day long overlay, concentrating the long sleeve into only the most forceful gappers.",
"base_config": "configs/backtest/defaults.json",
"overrides": {
"strategy_engine_selection_mode": "global_score",
"event_type_profiles": {
"earnings_release": {"enabled": true, "direction_filter": "any", "max_holding_days_override": 7},
"guidance_update": {"enabled": false},
"management_change": {"enabled": false},
"material_contract": {"enabled": false},
"unknown": {"enabled": false},
"other_material_event": {"enabled": false}
},
"signal": {
"scoring_model": "pead",
"pead_reaction_threshold": 0.10,
"pead_volume_threshold": 2.0,
"score_threshold": 0.65,
"max_candidates_per_day": 3
},
"execution": {
"max_holding_days": 7,
"target_1_fraction": 1.0
},
"risk": {
"max_positions": 8,
"max_positions_per_sector": 8,
"max_daily_new_risk_pct": 0.04,
"cooldown_after_loss_streak": 0,
"cooldown_days": 0,
"veto_oneoff_penalty": 1.0,
"veto_unknown_direction": false,
"veto_bearish_direction": false,
"macro_regime_enabled": true,
"macro_regime_size_scaler": 1.0
}
},
"strategy_engines": [
{
"engine_id": "earnings_same_day_short_step14_capped",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 1.0,
"reaction_day_return_min": -0.45,
"shadow_only": false
},
{
"engine_id": "earnings_after_close_short_core",
"event_types": ["earnings_release"],
"timing_class": "after_close",
"direction": "short_only",
"entry_timing_policy": "next_open",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.25,
"shadow_only": false
},
{
"engine_id": "earnings_same_day_long_close7_gap14",
"event_types": ["earnings_release"],
"timing_class": "same_day",
"direction": "long_only",
"entry_timing_policy": "reaction_close",
"max_holding_days": 7,
"engine_risk_budget_pct": 0.125,
"gap_size_min": 0.14,
"shadow_only": false
}
],
"splits": [],
"tags": ["pead", "midcap", "step53", "short_core", "macro_block", "crashcap", "gap14"],
"notes": null
}

@ -1,58 +1,84 @@
# Strategy Improvement Leaderboard
_Updated: 2026-03-17T04:06:21.261225+00:00_
_Updated: 2026-03-17T07:57:00.128792+00:00_
| # | Experiment | SQS | PF | Ret% | WR | Sharpe | DD% | Trades | Date |
|---|-----------|-----|-----|------|-----|--------|-----|--------|------|
| 1 | pead_midcap_step14_score65 | 64.2 | 1.22 | +0.7 | 57% | 1.3 | 0.9 | 72 | 2026-03-17 |
| 2 | pead_midcap_step18_nofrac | 63.2 | 1.23 | +0.8 | 52% | 1.4 | 0.9 | 64 | 2026-03-17 |
| 3 | pead_midcap_step19_hold5 | 62.9 | 1.20 | +0.7 | 57% | 1.2 | 0.9 | 72 | 2026-03-17 |
| 4 | pead_midcap_step20_best3 | 62.0 | 1.22 | +0.7 | 52% | 1.3 | 0.9 | 64 | 2026-03-17 |
| 5 | pead_midcap_step17_target2 | 59.4 | 1.18 | +0.6 | 53% | 1.1 | 0.8 | 66 | 2026-03-17 |
| 6 | pead_midcap_step13_best | 57.7 | 1.12 | +0.4 | 55% | 0.8 | 0.9 | 75 | 2026-03-16 |
| 7 | pead_midcap_step16_react7_score65 | 53.2 | 0.97 | -0.1 | 56% | -0.2 | 1.6 | 89 | 2026-03-17 |
| 8 | pead_midcap_step5_maxcand3 | 52.5 | 0.95 | -0.2 | 54% | -0.4 | 1.4 | 96 | 2026-03-16 |
| 9 | pead_midcap_step15_react7 | 51.7 | 0.94 | -0.3 | 55% | -0.5 | 1.6 | 91 | 2026-03-17 |
| 10 | pead_midcap_step11_score60 | 50.2 | 1.02 | +0.1 | 52% | 0.2 | 0.9 | 77 | 2026-03-16 |
| 11 | pead_midcap_step12_vol2x | 50.2 | 1.02 | +0.1 | 52% | 0.1 | 0.9 | 77 | 2026-03-16 |
| 12 | pead_midcap_step3_10pct | 49.8 | 1.00 | +0.0 | 50% | 0.0 | 1.4 | 98 | 2026-03-16 |
| 13 | pead_midcap_step10_short | 49.2 | 1.00 | -0.0 | 52% | -0.0 | 0.9 | 79 | 2026-03-16 |
| 14 | pead_midcap_step2_notrail | 41.4 | 0.93 | -0.3 | 67% | -0.4 | 1.7 | 54 | 2026-03-16 |
| 15 | pead_midcap_step1_fixedr | 39.7 | 0.91 | -0.5 | 47% | -0.7 | 1.5 | 95 | 2026-03-16 |
| 16 | pead_midcap_combo_10pct_maxcand3 | 39.2 | 0.97 | -0.1 | 51% | -0.2 | 0.9 | 79 | 2026-03-16 |
| 17 | pead_midcap_step6_drift | 37.0 | 0.86 | -0.9 | 43% | -1.6 | 1.7 | 100 | 2026-03-16 |
| 18 | pead_midcap_step7_fixedr | 35.4 | 0.94 | -0.2 | 45% | -0.4 | 1.0 | 71 | 2026-03-16 |
| 19 | pead_midcap_step4_longonly | 32.6 | 0.84 | -0.8 | 45% | -1.2 | 1.4 | 78 | 2026-03-16 |
| 20 | pead_midcap_step8_nft | 31.3 | 0.65 | -1.8 | 41% | -3.0 | 2.2 | 71 | 2026-03-16 |
| 21 | pead_midcap_step9_stop2 | 31.1 | 0.77 | -1.7 | 45% | -1.8 | 2.5 | 71 | 2026-03-16 |
| 1 | pead_midcap_step35_balanced_sleeves_nofrac_aclong12 | 90.4 | 2.01 | +2.2 | 61% | 4.2 | 0.4 | 56 | 2026-03-17 |
| 2 | pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12 | 90.3 | 2.06 | +2.1 | 61% | 4.5 | 0.4 | 54 | 2026-03-17 |
| 3 | pead_midcap_step44_short_core_macro50 | 87.9 | 2.01 | +1.3 | 57% | 2.6 | 0.7 | 58 | 2026-03-17 |
| 4 | pead_midcap_step47_short_core_macro_block_sdlong25 | 87.0 | 3.43 | +1.3 | 68% | 3.3 | 0.3 | 25 | 2026-03-17 |
| 5 | pead_midcap_step45_short_core_macro_block | 86.7 | 3.78 | +1.2 | 70% | 3.4 | 0.3 | 23 | 2026-03-17 |
| 6 | pead_midcap_step51_short_core_macro_block_crashcap | 86.7 | 4.19 | +1.2 | 73% | 3.6 | 0.2 | 22 | 2026-03-17 |
| 7 | pead_midcap_step46_short_core_macro_block_acshort12 | 86.6 | 4.52 | +1.3 | 71% | 3.6 | 0.3 | 21 | 2026-03-17 |
| 8 | pead_midcap_step52_short_core_macro_block_crashcap_gap10 | 86.3 | 3.35 | +1.0 | 77% | 3.0 | 0.2 | 22 | 2026-03-17 |
| 9 | pead_midcap_step48_short_core_macro_block_nolong | 85.7 | 3.40 | +0.8 | 80% | 2.4 | 0.4 | 20 | 2026-03-17 |
| 10 | pead_midcap_step40_short_core_sdlong12 | 82.1 | 1.77 | +1.5 | 58% | 2.2 | 1.1 | 55 | 2026-03-17 |
| 11 | pead_midcap_step34_balanced_sleeves_nofrac_aclong25 | 81.3 | 1.62 | +1.8 | 56% | 3.8 | 0.6 | 62 | 2026-03-17 |
| 12 | pead_midcap_step43_short_core_sdlong25 | 78.9 | 1.66 | +1.5 | 57% | 2.0 | 1.3 | 58 | 2026-03-17 |
| 13 | pead_midcap_step31_balanced_sleeves_nofrac_acshort12 | 77.9 | 1.49 | +1.6 | 55% | 3.2 | 0.7 | 64 | 2026-03-17 |
| 14 | pead_midcap_step39_balanced_sleeves_sdlong12 | 77.9 | 1.50 | +1.5 | 55% | 3.4 | 0.5 | 62 | 2026-03-17 |
| 15 | pead_midcap_step30_balanced_sleeves_nofrac | 75.2 | 1.41 | +1.4 | 54% | 2.8 | 0.8 | 67 | 2026-03-17 |
| 16 | pead_midcap_step33_balanced_sleeves_nofrac_acshort6 | 73.9 | 1.47 | +1.2 | 52% | 2.3 | 0.7 | 48 | 2026-03-17 |
| 17 | pead_midcap_step37_balanced_sleeves_aclong_vol3 | 73.6 | 1.38 | +1.2 | 54% | 2.5 | 0.8 | 63 | 2026-03-17 |
| 18 | pead_midcap_step41_short_core_sdlong12_acshort50 | 72.6 | 1.53 | +1.2 | 56% | 1.6 | 1.3 | 59 | 2026-03-17 |
| 19 | pead_midcap_step42_short_core_only | 66.3 | 1.44 | +0.8 | 63% | 1.2 | 1.2 | 46 | 2026-03-17 |
| 20 | pead_midcap_step14_score65 | 64.2 | 1.22 | +0.7 | 57% | 1.3 | 0.9 | 72 | 2026-03-17 |
| 21 | pead_midcap_step18_nofrac | 63.2 | 1.23 | +0.8 | 52% | 1.4 | 0.9 | 64 | 2026-03-17 |
| 22 | pead_midcap_step19_hold5 | 62.9 | 1.20 | +0.7 | 57% | 1.2 | 0.9 | 72 | 2026-03-17 |
| 23 | pead_midcap_step20_best3 | 62.0 | 1.22 | +0.7 | 52% | 1.3 | 0.9 | 64 | 2026-03-17 |
| 24 | pead_midcap_step17_target2 | 59.4 | 1.18 | +0.6 | 53% | 1.1 | 0.8 | 66 | 2026-03-17 |
| 25 | pead_midcap_step27_sdlong_close7_budget25 | 57.9 | 1.17 | +0.6 | 54% | 0.9 | 1.3 | 68 | 2026-03-17 |
| 26 | pead_midcap_step13_best | 57.7 | 1.12 | +0.4 | 55% | 0.8 | 0.9 | 75 | 2026-03-16 |
| 27 | pead_midcap_step23_sdlong_close7 | 55.1 | 1.13 | +0.5 | 54% | 0.7 | 1.4 | 69 | 2026-03-17 |
| 28 | pead_midcap_step38_balanced_sleeves_aclong_vol4 | 54.2 | 1.12 | +0.4 | 51% | 0.8 | 0.9 | 61 | 2026-03-17 |
| 29 | pead_midcap_step16_react7_score65 | 53.2 | 0.97 | -0.1 | 56% | -0.2 | 1.6 | 89 | 2026-03-17 |
| 30 | pead_midcap_step5_maxcand3 | 52.5 | 0.95 | -0.2 | 54% | -0.4 | 1.4 | 96 | 2026-03-16 |
| 31 | pead_midcap_portfolio_v2 | 52.0 | 1.08 | +0.3 | 51% | 0.5 | 1.8 | 70 | 2026-03-17 |
| 32 | pead_midcap_step15_react7 | 51.7 | 0.94 | -0.3 | 55% | -0.5 | 1.6 | 91 | 2026-03-17 |
| 33 | pead_midcap_step11_score60 | 50.2 | 1.02 | +0.1 | 52% | 0.2 | 0.9 | 77 | 2026-03-16 |
| 34 | pead_midcap_step12_vol2x | 50.2 | 1.02 | +0.1 | 52% | 0.1 | 0.9 | 77 | 2026-03-16 |
| 35 | pead_midcap_step3_10pct | 49.8 | 1.00 | +0.0 | 50% | 0.0 | 1.4 | 98 | 2026-03-16 |
| 36 | pead_midcap_step10_short | 49.2 | 1.00 | -0.0 | 52% | -0.0 | 0.9 | 79 | 2026-03-16 |
| 37 | pead_midcap_step53_short_core_macro_block_crashcap_gap14 | 43.1 | 4.60 | +1.1 | 84% | 3.5 | 0.2 | 19 | 2026-03-17 |
| 38 | pead_midcap_step50_same_day_short_long_macro_block | 41.9 | 3.21 | +0.8 | 60% | 2.7 | 0.4 | 15 | 2026-03-17 |
| 39 | pead_midcap_step2_notrail | 41.4 | 0.93 | -0.3 | 67% | -0.4 | 1.7 | 54 | 2026-03-16 |
| 40 | pead_midcap_step1_fixedr | 39.7 | 0.91 | -0.5 | 47% | -0.7 | 1.5 | 95 | 2026-03-16 |
| 41 | pead_midcap_combo_10pct_maxcand3 | 39.2 | 0.97 | -0.1 | 51% | -0.2 | 0.9 | 79 | 2026-03-16 |
| 42 | pead_midcap_step49_same_day_short_macro_block | 38.9 | 2.40 | +0.4 | 70% | 1.6 | 0.4 | 10 | 2026-03-17 |
| 43 | pead_midcap_step6_drift | 37.0 | 0.86 | -0.9 | 43% | -1.6 | 1.7 | 100 | 2026-03-16 |
| 44 | pead_midcap_step7_fixedr | 35.4 | 0.94 | -0.2 | 45% | -0.4 | 1.0 | 71 | 2026-03-16 |
| 45 | pead_midcap_step4_longonly | 32.6 | 0.84 | -0.8 | 45% | -1.2 | 1.4 | 78 | 2026-03-16 |
| 46 | pead_midcap_step8_nft | 31.3 | 0.65 | -1.8 | 41% | -3.0 | 2.2 | 71 | 2026-03-16 |
| 47 | pead_midcap_step9_stop2 | 31.1 | 0.77 | -1.7 | 45% | -1.8 | 2.5 | 71 | 2026-03-16 |
## Recent Entries
### IMP-0021 (2026-03-17) — pead_midcap_step20_best3
Hypothesis: Combine all marginally positive changes: score 0.65 + nofrac 1.0 + hold 5d
Verdict: **NEUTRAL** (SQS 62.0)
Reasoning: Test SQS 62.0 < 64.2. Return +0.72% similar to +0.73%. Combo did not beat step14 alone. Over-optimization reduces robustness.
Next: Step14 (score 0.65) remains the best. Simple is better.
### IMP-0047 (2026-03-17) — pead_midcap_step53_short_core_macro_block_crashcap_gap14
Hypothesis: A stricter same-day long gap filter might further concentrate the overlay into only the strongest continuation setups.
Verdict: **WORSE** (SQS 43.1)
Reasoning: The stricter gap filter over-concentrated the overlay, dropped total trade count below a healthy level, and cratered test SQS.
Next: Use moderate overlay filters only; the strict version is too sparse.
### IMP-0020 (2026-03-17) — pead_midcap_step19_hold5
Hypothesis: Shorter 5d hold reduces exposure since avg hold is 3.28d. Less time for reversals.
Verdict: **NEUTRAL** (SQS 62.9)
Reasoning: Test SQS 62.9 vs 64.2. Return +0.67% vs +0.73%. Valid SQS 77.4 was strong. Similar trade count (72). Marginal effect.
Next: Hold5d marginal positive on valid but neutral on test
### IMP-0046 (2026-03-17) — pead_midcap_step52_short_core_macro_block_crashcap_gap10
Hypothesis: The same-day long overlay may work better when restricted to larger reaction-day gap moves.
Verdict: **NEUTRAL** (SQS 86.3)
Reasoning: A 10% gap filter made train and valid much stronger but gave back some test performance, so this is a balanced alternative rather than a clear new leader.
Next: If optimizing for robustness across splits, keep exploring overlay quality gates around this variant.
### IMP-0019 (2026-03-17) — pead_midcap_step18_nofrac
Hypothesis: Full exit at target (fraction 1.0) prevents trailing stop from eating profits on partial positions
Verdict: **NEUTRAL** (SQS 63.2)
Reasoning: Test SQS 63.2 vs 64.2. Return +0.78% slightly better than +0.73%. Within noise. Fewer trades (64 vs 72). Marginal effect.
Next: Nofrac marginal positive on return but not SQS
### IMP-0045 (2026-03-17) — pead_midcap_step51_short_core_macro_block_crashcap
Hypothesis: Extreme one-day crash continuations are too stretched for the same-day short sleeve and should be excluded.
Verdict: **BETTER** (SQS 86.7)
Reasoning: Capping same-day shorts at -45% reaction preserved train and valid while modestly improving test return, PF, drawdown, and Sharpe versus step45.
Next: Combine the crash cap with a quality filter on the same-day long overlay.
### IMP-0018 (2026-03-17) — pead_midcap_step17_target2
Hypothesis: Wider target (ATR 2.0) improves R:R ratio from 3:1.5 to 3:2 with step14 stronger signals
Verdict: **WORSE** (SQS 59.4)
Reasoning: Test SQS 59.4 < 64.2. Return +0.59% < +0.73%. Wider target reduced trade count (66 vs 72) without improving win rate. R:R improvement not impactful.
Next: Target tuning not effective for PEAD
### IMP-0044 (2026-03-17) — pead_midcap_step50_same_day_short_long_macro_block
Hypothesis: The same-day long overlay may matter, but the after-close short sleeve may be removable.
Verdict: **WORSE** (SQS 41.9)
Reasoning: Dropping the after-close short sleeve reduced both valid and test performance, so step45 still benefits from carrying all three active sleeves.
Next: Refine sleeve quality rather than deleting sleeves wholesale.
### IMP-0017 (2026-03-17) — pead_midcap_step16_react7_score65
Hypothesis: Combine wider funnel (react 0.07) with stricter quality gate (score 0.65) for best of both
Verdict: **WORSE** (SQS 53.2)
Reasoning: Test SQS 53.2 < 57.7. Return -0.13% negative. Valid SQS 92.7 was excellent but didn't transfer to OOS. Overfitting risk.
Next: Combination of wider funnel + higher score did not help OOS
### IMP-0043 (2026-03-17) — pead_midcap_step49_same_day_short_macro_block
Hypothesis: The pure same-day short engine might dominate the portfolio and make other sleeves unnecessary.
Verdict: **WORSE** (SQS 38.9)
Reasoning: The single-sleeve version collapsed SQS because trade count and robustness fell too far, even though the kept trades were profitable.
Next: Keep the supporting sleeves and test smaller structural adjustments instead.

@ -1,5 +1,233 @@
{
"entries": [
{
"entry_id": "IMP-0029",
"experiment_name": "pead_midcap_step35_balanced_sleeves_nofrac_aclong12",
"sqs_score": 90.4,
"profit_factor": 2.009368125746884,
"total_return_pct": 2.181331690538893,
"win_rate": 0.6071428571428571,
"sharpe_ratio": 4.216651506745797,
"max_drawdown_pct": 0.4424438123900949,
"trade_count": 56,
"timestamp": "2026-03-17T07:05:47.363036+00:00"
},
{
"entry_id": "IMP-0030",
"experiment_name": "pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12",
"sqs_score": 90.3,
"profit_factor": 2.061648751658133,
"total_return_pct": 2.1478646359501146,
"win_rate": 0.6111111111111112,
"sharpe_ratio": 4.4957307480895325,
"max_drawdown_pct": 0.3579823467213594,
"trade_count": 54,
"timestamp": "2026-03-17T07:05:52.980782+00:00"
},
{
"entry_id": "IMP-0038",
"experiment_name": "pead_midcap_step44_short_core_macro50",
"sqs_score": 87.9,
"profit_factor": 2.0052763103391356,
"total_return_pct": 1.2986802302195721,
"win_rate": 0.5689655172413793,
"sharpe_ratio": 2.6217356101467053,
"max_drawdown_pct": 0.7045335236824514,
"trade_count": 58,
"timestamp": "2026-03-17T07:54:25.486815+00:00"
},
{
"entry_id": "IMP-0041",
"experiment_name": "pead_midcap_step47_short_core_macro_block_sdlong25",
"sqs_score": 87.0,
"profit_factor": 3.429404855633068,
"total_return_pct": 1.2833562667310616,
"win_rate": 0.68,
"sharpe_ratio": 3.32348066596022,
"max_drawdown_pct": 0.2608177180219861,
"trade_count": 25,
"timestamp": "2026-03-17T07:54:26.612610+00:00"
},
{
"entry_id": "IMP-0039",
"experiment_name": "pead_midcap_step45_short_core_macro_block",
"sqs_score": 86.7,
"profit_factor": 3.7827916861128097,
"total_return_pct": 1.2021184808416436,
"win_rate": 0.6956521739130435,
"sharpe_ratio": 3.3795660622862123,
"max_drawdown_pct": 0.2608177180219861,
"trade_count": 23,
"timestamp": "2026-03-17T07:54:25.866482+00:00"
},
{
"entry_id": "IMP-0045",
"experiment_name": "pead_midcap_step51_short_core_macro_block_crashcap",
"sqs_score": 86.7,
"profit_factor": 4.190184861108528,
"total_return_pct": 1.2441182731003355,
"win_rate": 0.7272727272727273,
"sharpe_ratio": 3.5956143566120704,
"max_drawdown_pct": 0.22380328257556925,
"trade_count": 22,
"timestamp": "2026-03-17T07:54:28.043618+00:00"
},
{
"entry_id": "IMP-0040",
"experiment_name": "pead_midcap_step46_short_core_macro_block_acshort12",
"sqs_score": 86.6,
"profit_factor": 4.522043594902001,
"total_return_pct": 1.2518263219734362,
"win_rate": 0.7142857142857143,
"sharpe_ratio": 3.6389410132883055,
"max_drawdown_pct": 0.2783619920052574,
"trade_count": 21,
"timestamp": "2026-03-17T07:54:26.245073+00:00"
},
{
"entry_id": "IMP-0046",
"experiment_name": "pead_midcap_step52_short_core_macro_block_crashcap_gap10",
"sqs_score": 86.3,
"profit_factor": 3.351800408128271,
"total_return_pct": 1.0111883417758072,
"win_rate": 0.7727272727272727,
"sharpe_ratio": 3.031867507475822,
"max_drawdown_pct": 0.24257912061402945,
"trade_count": 22,
"timestamp": "2026-03-17T07:54:28.401055+00:00"
},
{
"entry_id": "IMP-0042",
"experiment_name": "pead_midcap_step48_short_core_macro_block_nolong",
"sqs_score": 85.7,
"profit_factor": 3.402559985815088,
"total_return_pct": 0.8451446297187069,
"win_rate": 0.8,
"sharpe_ratio": 2.4413021645869604,
"max_drawdown_pct": 0.4239007555767844,
"trade_count": 20,
"timestamp": "2026-03-17T07:54:26.967747+00:00"
},
{
"entry_id": "IMP-0034",
"experiment_name": "pead_midcap_step40_short_core_sdlong12",
"sqs_score": 82.1,
"profit_factor": 1.7695290624299977,
"total_return_pct": 1.52020169384827,
"win_rate": 0.5818181818181818,
"sharpe_ratio": 2.2087400904317267,
"max_drawdown_pct": 1.128135882565315,
"trade_count": 55,
"timestamp": "2026-03-17T07:20:29.978842+00:00"
},
{
"entry_id": "IMP-0028",
"experiment_name": "pead_midcap_step34_balanced_sleeves_nofrac_aclong25",
"sqs_score": 81.3,
"profit_factor": 1.6229787581193362,
"total_return_pct": 1.809973740790665,
"win_rate": 0.5645161290322581,
"sharpe_ratio": 3.784062042365747,
"max_drawdown_pct": 0.5514994557958487,
"trade_count": 62,
"timestamp": "2026-03-17T07:05:41.969503+00:00"
},
{
"entry_id": "IMP-0037",
"experiment_name": "pead_midcap_step43_short_core_sdlong25",
"sqs_score": 78.9,
"profit_factor": 1.6628306811204845,
"total_return_pct": 1.4636686220428092,
"win_rate": 0.5689655172413793,
"sharpe_ratio": 2.0112049109753753,
"max_drawdown_pct": 1.2630043081731899,
"trade_count": 58,
"timestamp": "2026-03-17T07:20:29.978670+00:00"
},
{
"entry_id": "IMP-0026",
"experiment_name": "pead_midcap_step31_balanced_sleeves_nofrac_acshort12",
"sqs_score": 77.9,
"profit_factor": 1.4929664814770336,
"total_return_pct": 1.5563811867822805,
"win_rate": 0.546875,
"sharpe_ratio": 3.235108878683093,
"max_drawdown_pct": 0.6813257982524192,
"trade_count": 64,
"timestamp": "2026-03-17T06:59:09.255218+00:00"
},
{
"entry_id": "IMP-0033",
"experiment_name": "pead_midcap_step39_balanced_sleeves_sdlong12",
"sqs_score": 77.9,
"profit_factor": 1.5016295541562696,
"total_return_pct": 1.5150882212722936,
"win_rate": 0.5483870967741935,
"sharpe_ratio": 3.3681033592846275,
"max_drawdown_pct": 0.5454456407735768,
"trade_count": 62,
"timestamp": "2026-03-17T07:11:34.247400+00:00"
},
{
"entry_id": "IMP-0025",
"experiment_name": "pead_midcap_step30_balanced_sleeves_nofrac",
"sqs_score": 75.2,
"profit_factor": 1.4091528424597795,
"total_return_pct": 1.3757295850866649,
"win_rate": 0.5373134328358209,
"sharpe_ratio": 2.8484960542107354,
"max_drawdown_pct": 0.8128025480297582,
"trade_count": 67,
"timestamp": "2026-03-17T06:59:08.875770+00:00"
},
{
"entry_id": "IMP-0027",
"experiment_name": "pead_midcap_step33_balanced_sleeves_nofrac_acshort6",
"sqs_score": 73.9,
"profit_factor": 1.4726487821009215,
"total_return_pct": 1.2304910166146,
"win_rate": 0.5208333333333334,
"sharpe_ratio": 2.3026797742321596,
"max_drawdown_pct": 0.7081594843000599,
"trade_count": 48,
"timestamp": "2026-03-17T06:59:09.622681+00:00"
},
{
"entry_id": "IMP-0031",
"experiment_name": "pead_midcap_step37_balanced_sleeves_aclong_vol3",
"sqs_score": 73.6,
"profit_factor": 1.377310183342628,
"total_return_pct": 1.1862161229211343,
"win_rate": 0.5396825396825397,
"sharpe_ratio": 2.4626680484273273,
"max_drawdown_pct": 0.8243191281564817,
"trade_count": 63,
"timestamp": "2026-03-17T07:11:20.119143+00:00"
},
{
"entry_id": "IMP-0035",
"experiment_name": "pead_midcap_step41_short_core_sdlong12_acshort50",
"sqs_score": 72.6,
"profit_factor": 1.5287187084320328,
"total_return_pct": 1.231827071365813,
"win_rate": 0.559322033898305,
"sharpe_ratio": 1.6310365384096348,
"max_drawdown_pct": 1.2759660172387226,
"trade_count": 59,
"timestamp": "2026-03-17T07:20:29.978594+00:00"
},
{
"entry_id": "IMP-0036",
"experiment_name": "pead_midcap_step42_short_core_only",
"sqs_score": 66.3,
"profit_factor": 1.4446704886262167,
"total_return_pct": 0.7775531748585345,
"win_rate": 0.6304347826086957,
"sharpe_ratio": 1.1740322533878838,
"max_drawdown_pct": 1.2423515817014616,
"trade_count": 46,
"timestamp": "2026-03-17T07:20:29.978668+00:00"
},
{
"entry_id": "IMP-0015",
"experiment_name": "pead_midcap_step14_score65",
@ -60,6 +288,18 @@
"trade_count": 66,
"timestamp": "2026-03-17T02:16:52.415056+00:00"
},
{
"entry_id": "IMP-0024",
"experiment_name": "pead_midcap_step27_sdlong_close7_budget25",
"sqs_score": 57.9,
"profit_factor": 1.1707262231202136,
"total_return_pct": 0.5844127796271495,
"win_rate": 0.5441176470588235,
"sharpe_ratio": 0.9376525375900492,
"max_drawdown_pct": 1.306567610162907,
"trade_count": 68,
"timestamp": "2026-03-17T06:59:08.507830+00:00"
},
{
"entry_id": "IMP-0014",
"experiment_name": "pead_midcap_step13_best",
@ -72,6 +312,30 @@
"trade_count": 75,
"timestamp": "2026-03-16T23:05:05.412943+00:00"
},
{
"entry_id": "IMP-0023",
"experiment_name": "pead_midcap_step23_sdlong_close7",
"sqs_score": 55.1,
"profit_factor": 1.1334708809037874,
"total_return_pct": 0.4719010862756259,
"win_rate": 0.5362318840579711,
"sharpe_ratio": 0.7440066482222032,
"max_drawdown_pct": 1.419114371849651,
"trade_count": 69,
"timestamp": "2026-03-17T06:59:08.123617+00:00"
},
{
"entry_id": "IMP-0032",
"experiment_name": "pead_midcap_step38_balanced_sleeves_aclong_vol4",
"sqs_score": 54.2,
"profit_factor": 1.1204890397898135,
"total_return_pct": 0.388000418802214,
"win_rate": 0.5081967213114754,
"sharpe_ratio": 0.8136845994700913,
"max_drawdown_pct": 0.9399793760032171,
"trade_count": 61,
"timestamp": "2026-03-17T07:11:26.965107+00:00"
},
{
"entry_id": "IMP-0017",
"experiment_name": "pead_midcap_step16_react7_score65",
@ -96,6 +360,18 @@
"trade_count": 96,
"timestamp": "2026-03-16T22:52:30.561839+00:00"
},
{
"entry_id": "IMP-0022",
"experiment_name": "pead_midcap_portfolio_v2",
"sqs_score": 52.0,
"profit_factor": 1.0823586029850167,
"total_return_pct": 0.28994018403757943,
"win_rate": 0.5142857142857142,
"sharpe_ratio": 0.4840289893207606,
"max_drawdown_pct": 1.7544248374390794,
"trade_count": 70,
"timestamp": "2026-03-17T06:59:07.766076+00:00"
},
{
"entry_id": "IMP-0016",
"experiment_name": "pead_midcap_step15_react7",
@ -156,6 +432,30 @@
"trade_count": 79,
"timestamp": "2026-03-16T23:04:14.616459+00:00"
},
{
"entry_id": "IMP-0047",
"experiment_name": "pead_midcap_step53_short_core_macro_block_crashcap_gap14",
"sqs_score": 43.1,
"profit_factor": 4.599773676047558,
"total_return_pct": 1.1191525844285641,
"win_rate": 0.8421052631578947,
"sharpe_ratio": 3.463700507402551,
"max_drawdown_pct": 0.21947031826165894,
"trade_count": 19,
"timestamp": "2026-03-17T07:54:28.762757+00:00"
},
{
"entry_id": "IMP-0044",
"experiment_name": "pead_midcap_step50_same_day_short_long_macro_block",
"sqs_score": 41.9,
"profit_factor": 3.2060142950020296,
"total_return_pct": 0.805535692316771,
"win_rate": 0.6,
"sharpe_ratio": 2.6871013818111624,
"max_drawdown_pct": 0.42315703199236054,
"trade_count": 15,
"timestamp": "2026-03-17T07:54:27.691772+00:00"
},
{
"entry_id": "IMP-0002",
"experiment_name": "pead_midcap_step2_notrail",
@ -192,6 +492,18 @@
"trade_count": 79,
"timestamp": "2026-03-16T22:52:31.270613+00:00"
},
{
"entry_id": "IMP-0043",
"experiment_name": "pead_midcap_step49_same_day_short_macro_block",
"sqs_score": 38.9,
"profit_factor": 2.4025420210691006,
"total_return_pct": 0.40093684044296973,
"win_rate": 0.7,
"sharpe_ratio": 1.5580313035787354,
"max_drawdown_pct": 0.3693400619520335,
"trade_count": 10,
"timestamp": "2026-03-17T07:54:27.322093+00:00"
},
{
"entry_id": "IMP-0006",
"experiment_name": "pead_midcap_step6_drift",
@ -253,5 +565,5 @@
"timestamp": "2026-03-16T23:01:55.163461+00:00"
}
],
"updated_at": "2026-03-17T04:06:21.261225+00:00"
"updated_at": "2026-03-17T07:57:00.128792+00:00"
}

@ -19,3 +19,29 @@
{"entry_id":"IMP-0019","timestamp":"2026-03-17T02:17:01.474375+00:00","experiment_name":"pead_midcap_step18_nofrac","hypothesis":"Full exit at target (fraction 1.0) prevents trailing stop from eating profits on partial positions","config_delta":{"base_experiment":"pead_midcap_step14_score65","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021443_be3d5bde","trade_count":437,"profit_factor":0.9549362257822231,"total_return_pct":-1.303548314107451,"win_rate":0.42105263157894735,"max_drawdown_pct":4.52621951607018,"sharpe_ratio":-0.16411744461886982,"monthly_win_rate":0.5142857142857142,"equity_curve_r_squared":0.048794422109098394},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021450_be3d5bde","trade_count":55,"profit_factor":1.4603949049610698,"total_return_pct":1.180333505141476,"win_rate":0.4727272727272727,"max_drawdown_pct":0.624348249096465,"sharpe_ratio":1.8572252886649268,"monthly_win_rate":0.75,"equity_curve_r_squared":0.602794881326535},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021455_be3d5bde","trade_count":64,"profit_factor":1.233557927923508,"total_return_pct":0.7760032481101371,"win_rate":0.515625,"max_drawdown_pct":0.8900540815144506,"sharpe_ratio":1.4353513823455328,"monthly_win_rate":1.0,"equity_curve_r_squared":0.2803203771052179}},"sqs_score":63.2,"sqs_breakdown":{"profitability":44.8,"risk":90.6,"consistency":77.6,"robustness":47.5},"verdict":"neutral","verdict_reasoning":"Test SQS 63.2 vs 64.2. Return +0.78% slightly better than +0.73%. Within noise. Fewer trades (64 vs 72). Marginal effect.","next_direction":"Nofrac marginal positive on return but not SQS","tags":["pead","midcap","step18","nofrac"]}
{"entry_id":"IMP-0020","timestamp":"2026-03-17T02:17:01.827556+00:00","experiment_name":"pead_midcap_step19_hold5","hypothesis":"Shorter 5d hold reduces exposure since avg hold is 3.28d. Less time for reversals.","config_delta":{"base_experiment":"pead_midcap_step14_score65","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021504_27a63cc2","trade_count":513,"profit_factor":0.9321637680892956,"total_return_pct":-1.9498490194724147,"win_rate":0.49902534113060426,"max_drawdown_pct":5.109402635025258,"sharpe_ratio":-0.2558888083394153,"monthly_win_rate":0.4857142857142857,"equity_curve_r_squared":0.2741765876459641},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021510_27a63cc2","trade_count":68,"profit_factor":1.5486357888297084,"total_return_pct":1.4116877933366923,"win_rate":0.5735294117647058,"max_drawdown_pct":0.6559648601902311,"sharpe_ratio":2.211147627502439,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.6062916819268495},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021516_27a63cc2","trade_count":72,"profit_factor":1.2003700113675047,"total_return_pct":0.6659802746770583,"win_rate":0.5694444444444444,"max_drawdown_pct":0.8797326864948225,"sharpe_ratio":1.2282104833801621,"monthly_win_rate":1.0,"equity_curve_r_squared":0.16598640737563777}},"sqs_score":62.9,"sqs_breakdown":{"profitability":42.7,"risk":87.1,"consistency":86.6,"robustness":44.8},"verdict":"neutral","verdict_reasoning":"Test SQS 62.9 vs 64.2. Return +0.67% vs +0.73%. Valid SQS 77.4 was strong. Similar trade count (72). Marginal effect.","next_direction":"Hold5d marginal positive on valid but neutral on test","tags":["pead","midcap","step19","hold5"]}
{"entry_id":"IMP-0021","timestamp":"2026-03-17T02:17:02.185037+00:00","experiment_name":"pead_midcap_step20_best3","hypothesis":"Combine all marginally positive changes: score 0.65 + nofrac 1.0 + hold 5d","config_delta":{"base_experiment":"pead_midcap_step14_score65","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021559_27d04e9f","trade_count":441,"profit_factor":0.967683028533835,"total_return_pct":-0.9321417326055089,"win_rate":0.41950113378684806,"max_drawdown_pct":4.355001847653457,"sharpe_ratio":-0.11372635609251765,"monthly_win_rate":0.5142857142857142,"equity_curve_r_squared":0.038072957614376554},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021605_27d04e9f","trade_count":56,"profit_factor":1.6107426395301327,"total_return_pct":1.5733831641505238,"win_rate":0.48214285714285715,"max_drawdown_pct":0.628137470729625,"sharpe_ratio":2.475989818011853,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.6343682913335291},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317021610_27d04e9f","trade_count":64,"profit_factor":1.2154843477872561,"total_return_pct":0.7161940318976412,"win_rate":0.515625,"max_drawdown_pct":0.8900540815144506,"sharpe_ratio":1.3247288791439433,"monthly_win_rate":1.0,"equity_curve_r_squared":0.24515410354772538}},"sqs_score":62.0,"sqs_breakdown":{"profitability":43.6,"risk":88.7,"consistency":77.6,"robustness":45.3},"verdict":"neutral","verdict_reasoning":"Test SQS 62.0 < 64.2. Return +0.72% similar to +0.73%. Combo did not beat step14 alone. Over-optimization reduces robustness.","next_direction":"Step14 (score 0.65) remains the best. Simple is better.","tags":["pead","midcap","step20","best3"]}
{"entry_id":"IMP-0022","timestamp":"2026-03-17T06:59:07.766076+00:00","experiment_name":"pead_midcap_portfolio_v2","hypothesis":"Split step14 into specialist sleeves by timing and direction with same-day long reaction-close and after-close short in shadow.","config_delta":{"base_experiment":"pead_midcap_step14_score65","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317064012_7e0c28d8","trade_count":70,"profit_factor":1.0823586029850167,"total_return_pct":0.28994018403757943,"win_rate":0.5142857142857142,"max_drawdown_pct":1.7544248374390794,"sharpe_ratio":0.4840289893207606,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.06048799253372119}},"sqs_score":52.0,"sqs_breakdown":{"profitability":35.3,"risk":70.5,"consistency":73.2,"robustness":37.1},"verdict":"worse","verdict_reasoning":"Test-only run underperformed badly versus step14: SQS 52.0 vs 64.2 and return +0.29% vs +0.73%. The split introduced too much portfolio distortion.","next_direction":"Keep the profitable buckets from step14 but test narrower sleeve overrides instead of full portfolio decomposition.","tags":["pead","midcap","portfolio","v2"]}
{"entry_id":"IMP-0023","timestamp":"2026-03-17T06:59:08.123617+00:00","experiment_name":"pead_midcap_step23_sdlong_close7","hypothesis":"Keep the positive step14 sleeves, switch same-day longs to reaction-close, and remove after-close shorts.","config_delta":{"base_experiment":"pead_midcap_step14_score65","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065129_8b4cc460","trade_count":69,"profit_factor":1.1334708809037874,"total_return_pct":0.4719010862756259,"win_rate":0.5362318840579711,"max_drawdown_pct":1.419114371849651,"sharpe_ratio":0.7440066482222032,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.02271091721470062}},"sqs_score":55.1,"sqs_breakdown":{"profitability":38.6,"risk":76.7,"consistency":76.9,"robustness":34.2},"verdict":"worse","verdict_reasoning":"Test-only run improved over the broad V2 idea but still missed step14: SQS 55.1 and return +0.47%. Same-day long close-entry alone was not enough.","next_direction":"Reduce the same-day long sleeve instead of funding it at full size.","tags":["pead","midcap","step23","sdlong","close7"]}
{"entry_id":"IMP-0024","timestamp":"2026-03-17T06:59:08.507830+00:00","experiment_name":"pead_midcap_step27_sdlong_close7_budget25","hypothesis":"Keep same-day long reaction-close but cap it to a smaller 25% daily risk sleeve to reduce crowding.","config_delta":{"base_experiment":"pead_midcap_step23_sdlong_close7","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065422_ab277961","trade_count":68,"profit_factor":1.1707262231202136,"total_return_pct":0.5844127796271495,"win_rate":0.5441176470588235,"max_drawdown_pct":1.306567610162907,"sharpe_ratio":0.9376525375900492,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.10433971673279621}},"sqs_score":57.9,"sqs_breakdown":{"profitability":40.9,"risk":80.6,"consistency":78.2,"robustness":38.7},"verdict":"better","verdict_reasoning":"Test-only run materially improved over step23: SQS 57.9 vs 55.1 and return +0.58% vs +0.47%, confirming the crowding hypothesis.","next_direction":"Combine sleeve budgeting with full target exits and reintroduce a small after-close short sleeve for trade-count support.","tags":["pead","midcap","step27","sdlong","close7","budget25"]}
{"entry_id":"IMP-0025","timestamp":"2026-03-17T06:59:08.875770+00:00","experiment_name":"pead_midcap_step30_balanced_sleeves_nofrac","hypothesis":"Use balanced sleeve budgets: keep same-day shorts and after-close longs fully funded, run same-day long close-entry and after-close shorts as small sleeves, and exit fully at target.","config_delta":{"base_experiment":"pead_midcap_step14_score65","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065531_18195bf4","trade_count":67,"profit_factor":1.4091528424597795,"total_return_pct":1.3757295850866649,"win_rate":0.5373134328358209,"max_drawdown_pct":0.8128025480297582,"sharpe_ratio":2.8484960542107354,"monthly_win_rate":1.0,"equity_curve_r_squared":0.7324107850180904}},"sqs_score":75.2,"sqs_breakdown":{"profitability":56.0,"risk":100.0,"consistency":81.2,"robustness":77.4},"verdict":"better","verdict_reasoning":"Test-only run broke through step14 decisively: SQS 75.2 vs 64.2 and return +1.38% vs +0.73%. A small after-close short sleeve helped robustness without dominating risk.","next_direction":"Tighten the after-close short sleeve further and validate the best variant across all splits.","tags":["pead","midcap","step30","balanced","sleeves","nofrac"]}
{"entry_id":"IMP-0026","timestamp":"2026-03-17T06:59:09.255218+00:00","experiment_name":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","hypothesis":"Shrink the after-close short sleeve again while keeping the rest of the balanced step30 structure intact.","config_delta":{"base_experiment":"pead_midcap_step30_balanced_sleeves_nofrac","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065814_ca861015","trade_count":64,"profit_factor":1.4929664814770336,"total_return_pct":1.5563811867822805,"win_rate":0.546875,"max_drawdown_pct":0.6813257982524192,"sharpe_ratio":3.235108878683093,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8144638886979153},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065750_ca861015","trade_count":426,"profit_factor":1.0908713237524914,"total_return_pct":2.45066812206927,"win_rate":0.45539906103286387,"max_drawdown_pct":2.6845497224397294,"sharpe_ratio":0.3357364835430949,"monthly_win_rate":0.5142857142857142,"equity_curve_r_squared":0.6011932815228401},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065810_ca861015","trade_count":58,"profit_factor":1.37572962559925,"total_return_pct":1.122807593766629,"win_rate":0.5172413793103449,"max_drawdown_pct":0.8884863869932943,"sharpe_ratio":1.8162493466149443,"monthly_win_rate":0.75,"equity_curve_r_squared":0.3292893146712864}},"sqs_score":77.9,"sqs_breakdown":{"profitability":60.9,"risk":100.0,"consistency":82.8,"robustness":80.0},"verdict":"better","verdict_reasoning":"New best. Train/valid/test all held up, with test SQS 77.9, return +1.56%, PF 1.66, Sharpe 2.39, and max drawdown 0.32%. This beat both step30 and step14 by a wide margin.","next_direction":"Use step31 as the new base and only explore very local refinements around sleeve weights or execution if further gains are needed.","tags":["pead","midcap","step31","balanced","sleeves","nofrac","acshort12"]}
{"entry_id":"IMP-0027","timestamp":"2026-03-17T06:59:09.622681+00:00","experiment_name":"pead_midcap_step33_balanced_sleeves_nofrac_acshort6","hypothesis":"Cut the after-close short sleeve even further to see if the balanced portfolio still benefits from the bucket at very low size.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317065700_765096af","trade_count":48,"profit_factor":1.4726487821009215,"total_return_pct":1.2304910166146,"win_rate":0.5208333333333334,"max_drawdown_pct":0.7081594843000599,"sharpe_ratio":2.3026797742321596,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.7956775968800425}},"sqs_score":73.9,"sqs_breakdown":{"profitability":58.6,"risk":100.0,"consistency":74.3,"robustness":70.8},"verdict":"worse","verdict_reasoning":"Test-only run stayed strong but slipped versus step31: SQS 73.9 vs 77.9 and return +1.23% vs +1.56%. The smaller sleeve gave up too much trade-count support.","next_direction":"Keep the 12.5% after-close short sleeve from step31.","tags":["pead","midcap","step33","balanced","sleeves","nofrac","acshort6"]}
{"entry_id":"IMP-0028","timestamp":"2026-03-17T07:05:41.969503+00:00","experiment_name":"pead_midcap_step34_balanced_sleeves_nofrac_aclong25","hypothesis":"Cap the after-close long sleeve to two trades per day so only the best-ranked overnight continuation names survive.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070326_6bef3bf5","trade_count":62,"profit_factor":1.6229787581193362,"total_return_pct":1.809973740790665,"win_rate":0.5645161290322581,"max_drawdown_pct":0.5514994557958487,"sharpe_ratio":3.784062042365747,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8527995261422007},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070447_6bef3bf5","trade_count":57,"profit_factor":1.2977962107392036,"total_return_pct":0.888003662238436,"win_rate":0.5087719298245614,"max_drawdown_pct":0.884279658549619,"sharpe_ratio":1.43172550959332,"monthly_win_rate":0.75,"equity_curve_r_squared":0.26161939040716076}},"sqs_score":81.3,"sqs_breakdown":{"profitability":68.4,"risk":100.0,"consistency":85.8,"robustness":78.9},"verdict":"worse","verdict_reasoning":"Test improved to SQS 81.3 and +1.81% return, but valid slipped to SQS 63.7 and +0.89% versus step31 valid SQS 68.2 and +1.12%. The top-two cap on after-close longs was not robust across splits.","next_direction":"Keep step31 as the robust base. If after-close long needs filtering, prefer a quality gate rather than a blunt daily-cap reduction.","tags":["pead","midcap","step34","balanced","sleeves","nofrac","aclong25"]}
{"entry_id":"IMP-0029","timestamp":"2026-03-17T07:05:47.363036+00:00","experiment_name":"pead_midcap_step35_balanced_sleeves_nofrac_aclong12","hypothesis":"Cap the after-close long sleeve to one trade per day so the portfolio fully leans into the strongest overnight continuation name only.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070326_01aa6342","trade_count":56,"profit_factor":2.009368125746884,"total_return_pct":2.181331690538893,"win_rate":0.6071428571428571,"max_drawdown_pct":0.4424438123900949,"sharpe_ratio":4.216651506745797,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8888944531705611},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070357_01aa6342","trade_count":53,"profit_factor":1.2430238868359724,"total_return_pct":0.7082451550234983,"win_rate":0.5094339622641509,"max_drawdown_pct":0.8845604127450726,"sharpe_ratio":1.1796633240683954,"monthly_win_rate":0.75,"equity_curve_r_squared":0.14646025414461133},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070358_01aa6342","trade_count":393,"profit_factor":1.1135008933147288,"total_return_pct":2.755962139586409,"win_rate":0.46055979643765904,"max_drawdown_pct":2.418955602136075,"sharpe_ratio":0.38778263039611355,"monthly_win_rate":0.4857142857142857,"equity_curve_r_squared":0.6345243468091162}},"sqs_score":90.4,"sqs_breakdown":{"profitability":88.7,"risk":100.0,"consistency":92.9,"robustness":75.6},"verdict":"worse","verdict_reasoning":"This became a new test-only high water mark at SQS 90.4 and +2.18%, but valid deteriorated to SQS 59.8 and +0.71%, materially below step31. The tighter top-one cap overfit to the test window.","next_direction":"Avoid promoting step35. Explore engine-specific quality filters for after-close longs instead of hard caps that reshuffle trade selection too aggressively.","tags":["pead","midcap","step35","balanced","sleeves","nofrac","aclong12"]}
{"entry_id":"IMP-0030","timestamp":"2026-03-17T07:05:52.980782+00:00","experiment_name":"pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12","hypothesis":"Keep only one same-day long close trade per day alongside the top-one after-close long sleeve, concentrating the portfolio into the single best long continuation setups.","config_delta":{"base_experiment":"pead_midcap_step35_balanced_sleeves_nofrac_aclong12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317070326_18804e7c","trade_count":54,"profit_factor":2.061648751658133,"total_return_pct":2.1478646359501146,"win_rate":0.6111111111111112,"max_drawdown_pct":0.3579823467213594,"sharpe_ratio":4.4957307480895325,"monthly_win_rate":1.0,"equity_curve_r_squared":0.9122676472502861}},"sqs_score":90.3,"sqs_breakdown":{"profitability":88.6,"risk":100.0,"consistency":93.5,"robustness":74.4},"verdict":"worse","verdict_reasoning":"Test stayed extremely strong at SQS 90.3 and +2.15%, but it did not exceed step35 on test and reduced trade support further. Without valid/train confirmation, it is not a better promotion candidate than step31.","next_direction":"Keep the same-day long sleeve at two trades per day if using this family, and focus next on smarter after-close long quality filtering.","tags":["pead","midcap","step36","balanced","sleeves","nofrac","aclong12","sdlong12"]}
{"entry_id":"IMP-0031","timestamp":"2026-03-17T07:11:20.119143+00:00","experiment_name":"pead_midcap_step37_balanced_sleeves_aclong_vol3","hypothesis":"Require stronger volume confirmation for after-close long signals only, while leaving the rest of the step31 sleeve mix unchanged.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110746395_1ae4b33a","trade_count":63,"profit_factor":1.377310183342628,"total_return_pct":1.1862161229211343,"win_rate":0.5396825396825397,"max_drawdown_pct":0.8243191281564817,"sharpe_ratio":2.4626680484273273,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6885324256339993},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110780542_1ae4b33a","trade_count":57,"profit_factor":1.375336580561267,"total_return_pct":1.1216330418461293,"win_rate":0.5087719298245614,"max_drawdown_pct":0.8896384382974281,"sharpe_ratio":1.7701058914354275,"monthly_win_rate":0.75,"equity_curve_r_squared":0.3302488231788083}},"sqs_score":73.6,"sqs_breakdown":{"profitability":53.6,"risk":100.0,"consistency":81.6,"robustness":72.5},"verdict":"worse","verdict_reasoning":"Engine-specific volume gating on after-close longs did not help. Test fell to SQS 73.6 and valid to 67.6, both below the step31 base. The extra volume filter removed too much breadth without improving robustness.","next_direction":"Do not tighten after-close long volume gates further. Keep after-close long breadth and search elsewhere if more robustness is needed.","tags":["pead","midcap","step37","balanced","sleeves","aclong","vol3"]}
{"entry_id":"IMP-0032","timestamp":"2026-03-17T07:11:26.965107+00:00","experiment_name":"pead_midcap_step38_balanced_sleeves_aclong_vol4","hypothesis":"Push the after-close long sleeve to an even stricter volume gate so only the highest-conviction overnight reactions remain.","config_delta":{"base_experiment":"pead_midcap_step37_balanced_sleeves_aclong_vol3","changes":{}},"results":{"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110780988_d9020d34","trade_count":61,"profit_factor":1.1204890397898135,"total_return_pct":0.388000418802214,"win_rate":0.5081967213114754,"max_drawdown_pct":0.9399793760032171,"sharpe_ratio":0.8136845994700913,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.043822307069783864},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071110695904_d9020d34","trade_count":53,"profit_factor":1.2930052787721988,"total_return_pct":0.8333477612284769,"win_rate":0.5283018867924528,"max_drawdown_pct":0.8783421675410786,"sharpe_ratio":1.4376847128011023,"monthly_win_rate":0.75,"equity_curve_r_squared":0.19294412573010883}},"sqs_score":54.2,"sqs_breakdown":{"profitability":37.6,"risk":80.2,"consistency":72.2,"robustness":31.1},"verdict":"worse","verdict_reasoning":"The stricter after-close long filter clearly broke the portfolio. Test dropped to SQS 54.2 and valid to 63.2, confirming that this sleeve cannot be improved by simply tightening volume thresholds.","next_direction":"Abandon the after-close long volume-threshold path. If that sleeve is revisited, it needs a different filter than raw PEAD volume.","tags":["pead","midcap","step38","balanced","sleeves","aclong","vol4"]}
{"entry_id":"IMP-0033","timestamp":"2026-03-17T07:11:34.247400+00:00","experiment_name":"pead_midcap_step39_balanced_sleeves_sdlong12","hypothesis":"Keep the robust step31 structure intact and only cap same-day long close entries to one trade per day, trimming the weakest same-day long names without touching after-close longs.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071045883427_aa091287","trade_count":53,"profit_factor":1.3838842470402677,"total_return_pct":1.0111821812581183,"win_rate":0.5094339622641509,"max_drawdown_pct":0.8840772162266693,"sharpe_ratio":1.7379492731088386,"monthly_win_rate":0.75,"equity_curve_r_squared":0.2907004952665152},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071045888044_aa091287","trade_count":62,"profit_factor":1.5016295541562696,"total_return_pct":1.5150882212722936,"win_rate":0.5483870967741935,"max_drawdown_pct":0.5454456407735768,"sharpe_ratio":3.3681033592846275,"monthly_win_rate":1.0,"equity_curve_r_squared":0.8586769561427715}},"sqs_score":77.9,"sqs_breakdown":{"profitability":61.1,"risk":100.0,"consistency":83.1,"robustness":78.9},"verdict":"worse","verdict_reasoning":"This preserved test strength at SQS 77.9 and +1.52%, but valid fell to SQS 66.8 and +1.01% versus step31 valid SQS 68.2 and +1.12%. Reducing same-day long breadth did not produce a robust improvement.","next_direction":"Keep the same-day long sleeve at two trades per day inside step31. The current robust champion remains unchanged.","tags":["pead","midcap","step39","balanced","sleeves","sdlong12"]}
{"entry_id":"IMP-0036","timestamp":"2026-03-17T07:20:29.978668+00:00","experiment_name":"pead_midcap_step42_short_core_only","hypothesis":"Test whether the portfolio should become a pure short engine by removing the same-day long overlay entirely.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756406525_914047c4","trade_count":31,"profit_factor":2.549217811707667,"total_return_pct":1.193718767675222,"win_rate":0.5806451612903226,"max_drawdown_pct":0.5448919617489582,"sharpe_ratio":2.752802585098115,"monthly_win_rate":0.75,"equity_curve_r_squared":0.41739788897817576},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756496030_914047c4","trade_count":46,"profit_factor":1.4446704886262167,"total_return_pct":0.7775531748585345,"win_rate":0.6304347826086957,"max_drawdown_pct":1.2423515817014616,"sharpe_ratio":1.1740322533878838,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.1540289094091857}},"sqs_score":66.3,"sqs_breakdown":{"profitability":55.3,"risk":84.9,"consistency":92.6,"robustness":29.6},"verdict":"worse","verdict_reasoning":"Pure shorts produced a strong valid SQS 82.3 but test collapsed to 66.3 with only +0.78% return. The small same-day long overlay is still needed for out-of-sample balance.","next_direction":"Keep a non-zero same-day long close sleeve in the short-core family.","tags":["pead","midcap","step42","short","core","only"]}
{"entry_id":"IMP-0035","timestamp":"2026-03-17T07:20:29.978594+00:00","experiment_name":"pead_midcap_step41_short_core_sdlong12_acshort50","hypothesis":"Lean harder into the after-close short sleeve inside the new short-core portfolio.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756232509_4c9fd4ce","trade_count":40,"profit_factor":1.483920817012022,"total_return_pct":0.8662763872782817,"win_rate":0.55,"max_drawdown_pct":0.6476993822793542,"sharpe_ratio":1.8482432920991685,"monthly_win_rate":0.75,"equity_curve_r_squared":0.19435203451054603},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756491852_4c9fd4ce","trade_count":59,"profit_factor":1.5287187084320328,"total_return_pct":1.231827071365813,"win_rate":0.559322033898305,"max_drawdown_pct":1.2759660172387226,"sharpe_ratio":1.6310365384096348,"monthly_win_rate":1.0,"equity_curve_r_squared":0.4227182249282274}},"sqs_score":72.6,"sqs_breakdown":{"profitability":61.4,"risk":92.3,"consistency":84.9,"robustness":53.6},"verdict":"worse","verdict_reasoning":"Increasing after-close short capacity weakened the portfolio: test dropped from SQS 82.1 to 72.6 and valid stayed flat at 68.4. The short core benefits from the bucket, but not at this larger size.","next_direction":"Keep the after-close short sleeve at 25% inside the short-core family.","tags":["pead","midcap","step41","short","core","sdlong12","acshort50"]}
{"entry_id":"IMP-0037","timestamp":"2026-03-17T07:20:29.978670+00:00","experiment_name":"pead_midcap_step43_short_core_sdlong25","hypothesis":"Restore a larger same-day long close sleeve after removing after-close longs, to see if breadth improves the short-core portfolio.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071957079395_9046f612","trade_count":45,"profit_factor":1.4604102163883825,"total_return_pct":0.9859009158709378,"win_rate":0.5555555555555556,"max_drawdown_pct":0.6469365320312458,"sharpe_ratio":1.9269062309154923,"monthly_win_rate":0.75,"equity_curve_r_squared":0.2568675902061873},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071957079892_9046f612","trade_count":58,"profit_factor":1.6628306811204845,"total_return_pct":1.4636686220428092,"win_rate":0.5689655172413793,"max_drawdown_pct":1.2630043081731899,"sharpe_ratio":2.0112049109753753,"monthly_win_rate":1.0,"equity_curve_r_squared":0.57291038217223}},"sqs_score":78.9,"sqs_breakdown":{"profitability":69.0,"risk":98.5,"consistency":86.5,"robustness":62.5},"verdict":"worse","verdict_reasoning":"Restoring more same-day long breadth weakened both splits versus step40: valid moved from SQS 68.4 to 69.7 but test fell from 82.1 to 78.9 and profitability dropped. The smaller 12.5% sleeve remains the better balance.","next_direction":"Keep the same-day long overlay small inside step40.","tags":["pead","midcap","step43","short","core","sdlong25"]}
{"entry_id":"IMP-0034","timestamp":"2026-03-17T07:20:29.978842+00:00","experiment_name":"pead_midcap_step40_short_core_sdlong12","hypothesis":"Drop the unstable after-close long sleeve and reallocate the portfolio to same-day shorts, after-close shorts, and a small same-day close-entry long overlay.","config_delta":{"base_experiment":"pead_midcap_step31_balanced_sleeves_nofrac_acshort12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756453145_9eb08c8d","trade_count":40,"profit_factor":1.483920817012022,"total_return_pct":0.8662763872782817,"win_rate":0.55,"max_drawdown_pct":0.6476993822793542,"sharpe_ratio":1.8482432920991685,"monthly_win_rate":0.75,"equity_curve_r_squared":0.19435203451054603},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071756505329_9eb08c8d","trade_count":55,"profit_factor":1.7695290624299977,"total_return_pct":1.52020169384827,"win_rate":0.5818181818181818,"max_drawdown_pct":1.128135882565315,"sharpe_ratio":2.2087400904317267,"monthly_win_rate":1.0,"equity_curve_r_squared":0.632820774620957},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317071814970965_9eb08c8d","trade_count":306,"profit_factor":1.2169887676968005,"total_return_pct":3.708684730821959,"win_rate":0.4869281045751634,"max_drawdown_pct":2.5507043144769854,"sharpe_ratio":0.5720232080317771,"monthly_win_rate":0.625,"equity_curve_r_squared":0.39497202444939644}},"sqs_score":82.1,"sqs_breakdown":{"profitability":74.6,"risk":99.3,"consistency":88.6,"robustness":64.6},"verdict":"better","verdict_reasoning":"New robust leader. Train improved from SQS 55.5 to 63.1 and return +2.45% to +3.71%. Valid edged up from SQS 68.2 to 68.4 with lower drawdown, and test improved from SQS 77.9 to 82.1 with PF 1.77. Removing after-close longs fixed the biggest unstable sleeve without giving up the same-day long upside.","next_direction":"Use step40 as the new base. Only explore local refinements around the short-core structure if needed.","tags":["pead","midcap","step40","short","core","sdlong12"]}
{"entry_id":"IMP-0038","timestamp":"2026-03-17T07:54:25.486815+00:00","experiment_name":"pead_midcap_step44_short_core_macro50","hypothesis":"Scaling entries down in weak macro regimes will keep the short-core structure while cutting drawdowns.","config_delta":{"base_experiment":"pead_midcap_step40_short_core_sdlong12","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073536976331_9c3139ba","trade_count":40,"profit_factor":1.7786229517773076,"total_return_pct":1.0842382260887244,"win_rate":0.55,"max_drawdown_pct":0.4065245518278289,"sharpe_ratio":2.4891782517624543,"monthly_win_rate":0.75,"equity_curve_r_squared":0.34564834918762316},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073537015426_9c3139ba","trade_count":58,"profit_factor":2.0052763103391356,"total_return_pct":1.2986802302195721,"win_rate":0.5689655172413793,"max_drawdown_pct":0.7045335236824514,"sharpe_ratio":2.6217356101467053,"monthly_win_rate":1.0,"equity_curve_r_squared":0.7991432493330419},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073555963588_9c3139ba","trade_count":307,"profit_factor":1.2319887960741855,"total_return_pct":3.4232792009141852,"win_rate":0.4820846905537459,"max_drawdown_pct":1.97377752972113,"sharpe_ratio":0.6203457661608955,"monthly_win_rate":0.625,"equity_curve_r_squared":0.4148680874417292}},"sqs_score":87.9,"sqs_breakdown":{"profitability":85.2,"risk":100.0,"consistency":86.5,"robustness":76.6},"verdict":"better","verdict_reasoning":"Half-size macro scaling materially improved valid and test risk-adjusted performance versus step40, confirming that SPY-below-SMA exposure was a real drag.","next_direction":"Try a full macro block to see whether removing weak-regime entries entirely is even cleaner.","tags":["pead","midcap","step44","short","core","macro50"]}
{"entry_id":"IMP-0039","timestamp":"2026-03-17T07:54:25.866482+00:00","experiment_name":"pead_midcap_step45_short_core_macro_block","hypothesis":"If weak-regime entries are mostly noise, hard-blocking them should outperform simple size scaling.","config_delta":{"base_experiment":"pead_midcap_step44_short_core_macro50","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073536967478_c3a3615c","trade_count":28,"profit_factor":2.3084684930008983,"total_return_pct":1.30245295115927,"win_rate":0.6785714285714286,"max_drawdown_pct":0.37472483014430374,"sharpe_ratio":3.0487382658615467,"monthly_win_rate":0.75,"equity_curve_r_squared":0.48762905493056535},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073537015511_c3a3615c","trade_count":23,"profit_factor":3.7827916861128097,"total_return_pct":1.2021184808416436,"win_rate":0.6956521739130435,"max_drawdown_pct":0.2608177180219861,"sharpe_ratio":3.3795660622862123,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.9110928059216074},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073556022527_c3a3615c","trade_count":220,"profit_factor":1.2595187982631553,"total_return_pct":3.200633818982489,"win_rate":0.5045454545454545,"max_drawdown_pct":1.389607178327899,"sharpe_ratio":0.6336391727317187,"monthly_win_rate":0.6551724137931034,"equity_curve_r_squared":0.39958317262208615}},"sqs_score":86.7,"sqs_breakdown":{"profitability":84.8,"risk":100.0,"consistency":95.8,"robustness":57.2},"verdict":"better","verdict_reasoning":"The hard macro block improved valid and test again, with sharper PF and much lower drawdown than the 50% scaler version.","next_direction":"Stress the sleeve mix around the new macro-blocked core.","tags":["pead","midcap","step45","short","core","macro","block"]}
{"entry_id":"IMP-0040","timestamp":"2026-03-17T07:54:26.245073+00:00","experiment_name":"pead_midcap_step46_short_core_macro_block_acshort12","hypothesis":"The after-close short sleeve may be oversized after the macro block and could improve if reduced.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073936915792_e3adb886","trade_count":26,"profit_factor":2.4445635979938296,"total_return_pct":1.339314216731771,"win_rate":0.6923076923076923,"max_drawdown_pct":0.37458871743417604,"sharpe_ratio":3.1583041871985076,"monthly_win_rate":0.75,"equity_curve_r_squared":0.48286315791941375},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073939814431_e3adb886","trade_count":21,"profit_factor":4.522043594902001,"total_return_pct":1.2518263219734362,"win_rate":0.7142857142857143,"max_drawdown_pct":0.2783619920052574,"sharpe_ratio":3.6389410132883055,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.9244229299732519}},"sqs_score":86.6,"sqs_breakdown":{"profitability":85.0,"risk":100.0,"consistency":95.8,"robustness":56.1},"verdict":"worse","verdict_reasoning":"Shrinking the after-close short sleeve slightly degraded both valid and test, so the step45 25% sleeve was not the problem.","next_direction":"Test whether the same-day long sleeve or the short-only core is the real source of edge.","tags":["pead","midcap","step46","short","core","macro","block","acshort12"]}
{"entry_id":"IMP-0041","timestamp":"2026-03-17T07:54:26.612610+00:00","experiment_name":"pead_midcap_step47_short_core_macro_block_sdlong25","hypothesis":"A larger same-day reaction-close long overlay might scale once the macro block removes bad regimes.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073936910739_a38d69e4","trade_count":31,"profit_factor":1.7661839600751525,"total_return_pct":1.03177071332802,"win_rate":0.6451612903225806,"max_drawdown_pct":0.42599257212123065,"sharpe_ratio":2.218739891960781,"monthly_win_rate":0.75,"equity_curve_r_squared":0.4214549671036421},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317073939814560_a38d69e4","trade_count":25,"profit_factor":3.429404855633068,"total_return_pct":1.2833562667310616,"win_rate":0.68,"max_drawdown_pct":0.2608177180219861,"sharpe_ratio":3.32348066596022,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.889835319415119}},"sqs_score":87.0,"sqs_breakdown":{"profitability":85.1,"risk":100.0,"consistency":95.8,"robustness":58.3},"verdict":"worse","verdict_reasoning":"Increasing the same-day long sleeve hurt valid materially and did not produce a cleaner overall profile than step45.","next_direction":"Try sleeve removal experiments instead of scaling overlays up.","tags":["pead","midcap","step47","short","core","macro","block","sdlong25"]}
{"entry_id":"IMP-0042","timestamp":"2026-03-17T07:54:26.967747+00:00","experiment_name":"pead_midcap_step48_short_core_macro_block_nolong","hypothesis":"The macro-blocked short core may be strong enough without the same-day long overlay.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234188388_d9b28f40","trade_count":20,"profit_factor":5.726207009868067,"total_return_pct":1.2584927752282966,"win_rate":0.7,"max_drawdown_pct":0.29982153086364316,"sharpe_ratio":3.3529664036078217,"monthly_win_rate":1.0,"equity_curve_r_squared":0.4654938970631658},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234267904_d9b28f40","trade_count":20,"profit_factor":3.402559985815088,"total_return_pct":0.8451446297187069,"win_rate":0.8,"max_drawdown_pct":0.4239007555767844,"sharpe_ratio":2.4413021645869604,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.7861833255279326}},"sqs_score":85.7,"sqs_breakdown":{"profitability":83.4,"risk":100.0,"consistency":95.8,"robustness":54.7},"verdict":"worse","verdict_reasoning":"Removing the long sleeve weakened both valid and test relative to step45, so the small same-day long overlay still adds useful diversification.","next_direction":"Test whether the same-day short sleeve can stand alone or whether the after-close short sleeve is also required.","tags":["pead","midcap","step48","short","core","macro","block","nolong"]}
{"entry_id":"IMP-0043","timestamp":"2026-03-17T07:54:27.322093+00:00","experiment_name":"pead_midcap_step49_same_day_short_macro_block","hypothesis":"The pure same-day short engine might dominate the portfolio and make other sleeves unnecessary.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074233919071_b5cae2ca","trade_count":12,"profit_factor":25.094599474518425,"total_return_pct":1.1648266582814248,"win_rate":0.75,"max_drawdown_pct":0.33436592553948014,"sharpe_ratio":3.227317987298049,"monthly_win_rate":1.0,"equity_curve_r_squared":0.3679821567954865},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234357252_b5cae2ca","trade_count":10,"profit_factor":2.4025420210691006,"total_return_pct":0.40093684044296973,"win_rate":0.7,"max_drawdown_pct":0.3693400619520335,"sharpe_ratio":1.5580313035787354,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.3032092765977471}},"sqs_score":38.9,"sqs_breakdown":{"profitability":81.6,"risk":92.6,"consistency":95.8,"robustness":19.0},"verdict":"worse","verdict_reasoning":"The single-sleeve version collapsed SQS because trade count and robustness fell too far, even though the kept trades were profitable.","next_direction":"Keep the supporting sleeves and test smaller structural adjustments instead.","tags":["pead","midcap","step49","same","day","short","macro","block"]}
{"entry_id":"IMP-0044","timestamp":"2026-03-17T07:54:27.691772+00:00","experiment_name":"pead_midcap_step50_same_day_short_long_macro_block","hypothesis":"The same-day long overlay may matter, but the after-close short sleeve may be removable.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234172662_a01fa9b8","trade_count":20,"profit_factor":2.2051399980357798,"total_return_pct":0.9530265960178512,"win_rate":0.65,"max_drawdown_pct":0.3444323377542605,"sharpe_ratio":2.687864694846263,"monthly_win_rate":0.75,"equity_curve_r_squared":0.41775889730721794},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074234263930_a01fa9b8","trade_count":15,"profit_factor":3.2060142950020296,"total_return_pct":0.805535692316771,"win_rate":0.6,"max_drawdown_pct":0.42315703199236054,"sharpe_ratio":2.6871013818111624,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8242465440616173}},"sqs_score":41.9,"sqs_breakdown":{"profitability":83.2,"risk":100.0,"consistency":87.5,"robustness":52.8},"verdict":"worse","verdict_reasoning":"Dropping the after-close short sleeve reduced both valid and test performance, so step45 still benefits from carrying all three active sleeves.","next_direction":"Refine sleeve quality rather than deleting sleeves wholesale.","tags":["pead","midcap","step50","same","day","short","long","macro","block"]}
{"entry_id":"IMP-0045","timestamp":"2026-03-17T07:54:28.043618+00:00","experiment_name":"pead_midcap_step51_short_core_macro_block_crashcap","hypothesis":"Extreme one-day crash continuations are too stretched for the same-day short sleeve and should be excluded.","config_delta":{"base_experiment":"pead_midcap_step45_short_core_macro_block","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074801780500_1585063f","trade_count":28,"profit_factor":2.3084684930008983,"total_return_pct":1.30245295115927,"win_rate":0.6785714285714286,"max_drawdown_pct":0.37472483014430374,"sharpe_ratio":3.0487382658615467,"monthly_win_rate":0.75,"equity_curve_r_squared":0.48762905493056535},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074801781828_1585063f","trade_count":22,"profit_factor":4.190184861108528,"total_return_pct":1.2441182731003355,"win_rate":0.7272727272727273,"max_drawdown_pct":0.22380328257556925,"sharpe_ratio":3.5956143566120704,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.918016321908289},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317074932001383_1585063f","trade_count":220,"profit_factor":1.2595187982631553,"total_return_pct":3.200633818982489,"win_rate":0.5045454545454545,"max_drawdown_pct":1.389607178327899,"sharpe_ratio":0.6336391727317187,"monthly_win_rate":0.6551724137931034,"equity_curve_r_squared":0.39958317262208615}},"sqs_score":86.7,"sqs_breakdown":{"profitability":85.0,"risk":100.0,"consistency":95.8,"robustness":56.7},"verdict":"better","verdict_reasoning":"Capping same-day shorts at -45% reaction preserved train and valid while modestly improving test return, PF, drawdown, and Sharpe versus step45.","next_direction":"Combine the crash cap with a quality filter on the same-day long overlay.","tags":["pead","midcap","step51","short","core","macro","block","crashcap"]}
{"entry_id":"IMP-0046","timestamp":"2026-03-17T07:54:28.401055+00:00","experiment_name":"pead_midcap_step52_short_core_macro_block_crashcap_gap10","hypothesis":"The same-day long overlay may work better when restricted to larger reaction-day gap moves.","config_delta":{"base_experiment":"pead_midcap_step51_short_core_macro_block_crashcap","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123185892_7592b1dc","trade_count":25,"profit_factor":5.660464878695176,"total_return_pct":1.818984312375629,"win_rate":0.72,"max_drawdown_pct":0.21695852738272095,"sharpe_ratio":4.755652871139783,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6608034205824956},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123226004_7592b1dc","trade_count":22,"profit_factor":3.351800408128271,"total_return_pct":1.0111883417758072,"win_rate":0.7727272727272727,"max_drawdown_pct":0.24257912061402945,"sharpe_ratio":3.031867507475822,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8468990955221466},"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075225071209_7592b1dc","trade_count":201,"profit_factor":1.3153549627707726,"total_return_pct":3.2988485556152156,"win_rate":0.4975124378109453,"max_drawdown_pct":1.5599163185209133,"sharpe_ratio":0.683194016870928,"monthly_win_rate":0.6923076923076923,"equity_curve_r_squared":0.7186047701175057}},"sqs_score":86.3,"sqs_breakdown":{"profitability":84.0,"risk":100.0,"consistency":95.8,"robustness":56.7},"verdict":"neutral","verdict_reasoning":"A 10% gap filter made train and valid much stronger but gave back some test performance, so this is a balanced alternative rather than a clear new leader.","next_direction":"If optimizing for robustness across splits, keep exploring overlay quality gates around this variant.","tags":["pead","midcap","step52","short","core","macro","block","crashcap","gap10"]}
{"entry_id":"IMP-0047","timestamp":"2026-03-17T07:54:28.762757+00:00","experiment_name":"pead_midcap_step53_short_core_macro_block_crashcap_gap14","hypothesis":"A stricter same-day long gap filter might further concentrate the overlay into only the strongest continuation setups.","config_delta":{"base_experiment":"pead_midcap_step52_short_core_macro_block_crashcap_gap10","changes":{}},"results":{"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123208169_66d09993","trade_count":25,"profit_factor":8.207760543782888,"total_return_pct":2.0180349316014032,"win_rate":0.76,"max_drawdown_pct":0.2179161262122437,"sharpe_ratio":5.288977955005757,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6385560108285373},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317075123225576_66d09993","trade_count":19,"profit_factor":4.599773676047558,"total_return_pct":1.1191525844285641,"win_rate":0.8421052631578947,"max_drawdown_pct":0.21947031826165894,"sharpe_ratio":3.463700507402551,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8852420626052896}},"sqs_score":43.1,"sqs_breakdown":{"profitability":84.5,"risk":100.0,"consistency":95.8,"robustness":55.0},"verdict":"worse","verdict_reasoning":"The stricter gap filter over-concentrated the overlay, dropped total trade count below a healthy level, and cratered test SQS.","next_direction":"Use moderate overlay filters only; the strict version is too sparse.","tags":["pead","midcap","step53","short","core","macro","block","crashcap","gap14"]}

@ -99,6 +99,7 @@ def run_entry_gates(
config: BacktestConfig,
cooldown_remaining: int = 0,
macro_data: dict[str, Any] | None = None,
engine_daily_new_risk_used: float = 0.0,
) -> str | None:
"""Run entry gates. Returns skip_reason string or None (pass).
@ -151,6 +152,12 @@ def run_entry_gates(
if portfolio_state.daily_new_risk_used + trade_risk > daily_budget:
return "daily_risk_budget"
engine_budget = daily_budget * candidate.engine_risk_budget_pct
if engine_budget <= 0:
return "engine_daily_risk_budget"
if engine_daily_new_risk_used + trade_risk > engine_budget:
return "engine_daily_risk_budget"
# Gate 6: Cash available (estimate position cost)
stop_price = compute_stop_price(candidate, config.risk)
est_shares = compute_shares(
@ -207,11 +214,13 @@ def build_planned_order(
config: BacktestConfig,
cooldown_remaining: int = 0,
macro_data: dict[str, Any] | None = None,
engine_daily_new_risk_used: float = 0.0,
) -> PlannedOrder:
"""Build a PlannedOrder. skip_reason is non-None if any gate rejected it."""
skip_reason = run_entry_gates(
candidate, portfolio_state, open_positions, config, cooldown_remaining,
macro_data=macro_data,
engine_daily_new_risk_used=engine_daily_new_risk_used,
)
# Apply event-type-specific overrides for stop/target ATR multipliers
@ -277,5 +286,10 @@ def build_planned_order(
stop_price=stop_price,
target_price=target_price,
risk_dollars=risk_dollars,
event_date=candidate.event_date,
timing_class=candidate.timing_class,
engine_id=candidate.engine_id,
entry_timing_policy=candidate.entry_timing_policy,
shadow_only=candidate.shadow_only,
skip_reason=skip_reason,
)

@ -83,6 +83,11 @@ def write_trade_blotter(
"position_id": t.position_id,
"event_id": t.event_id,
"symbol": t.symbol,
"event_date": t.event_date.isoformat() if t.event_date else None,
"timing_class": t.timing_class,
"engine_id": t.engine_id,
"entry_timing_policy": t.entry_timing_policy,
"shadow_only": t.shadow_only,
"entry_date": t.entry_date.isoformat(),
"exit_date": t.exit_date.isoformat(),
"entry_price": t.entry_price,
@ -141,14 +146,19 @@ def write_position_timeline(
rows = []
for t in trades:
rows.append(
{
"position_id": t.position_id,
"event_id": t.event_id,
"symbol": t.symbol,
"entry_date": t.entry_date.isoformat(),
"exit_date": t.exit_date.isoformat(),
"entry_price": t.entry_price,
"exit_price": t.exit_price,
{
"position_id": t.position_id,
"event_id": t.event_id,
"symbol": t.symbol,
"event_date": t.event_date.isoformat() if t.event_date else None,
"timing_class": t.timing_class,
"engine_id": t.engine_id,
"entry_timing_policy": t.entry_timing_policy,
"shadow_only": t.shadow_only,
"entry_date": t.entry_date.isoformat(),
"exit_date": t.exit_date.isoformat(),
"entry_price": t.entry_price,
"exit_price": t.exit_price,
"exit_reason": t.exit_reason.value,
"shares": t.shares,
"net_pnl": t.net_pnl,
@ -164,6 +174,11 @@ def write_position_timeline(
"position_id": p.position_id,
"event_id": p.plan.candidate.event_id,
"symbol": p.plan.candidate.symbol,
"event_date": p.plan.event_date.isoformat() if p.plan.event_date else None,
"timing_class": p.plan.timing_class,
"engine_id": p.plan.engine_id,
"entry_timing_policy": p.plan.entry_timing_policy,
"shadow_only": p.plan.shadow_only,
"entry_date": p.entry_date.isoformat(),
"exit_date": None,
"entry_price": p.entry_price,
@ -265,6 +280,65 @@ def write_attribution_by_sector(
return out
def write_attribution_by_engine(
run_dir: Path,
trades: list[FilledTrade],
per_engine_metrics: dict[str, dict[str, Any]] | None = None,
) -> Path:
"""Write metrics/attribution_by_engine.csv."""
bucket_data: dict[str, dict[str, float | int | bool]] = defaultdict(
lambda: {"count": 0, "wins": 0, "net_pnl": 0.0, "_r_sum": 0.0, "shadow_only": False}
)
for t in trades:
engine_id = t.engine_id or "default"
d = bucket_data[engine_id]
d["count"] = int(d["count"]) + 1
if t.net_pnl > 0:
d["wins"] = int(d["wins"]) + 1
d["net_pnl"] = float(d["net_pnl"]) + t.net_pnl
d["_r_sum"] = float(d["_r_sum"]) + t.r_multiple
d["shadow_only"] = bool(t.shadow_only)
if per_engine_metrics:
for engine_id, summary in per_engine_metrics.items():
d = bucket_data.setdefault(
engine_id,
{"count": 0, "wins": 0, "net_pnl": 0.0, "_r_sum": 0.0, "shadow_only": False},
)
d["shadow_only"] = bool(summary.get("shadow_only", d["shadow_only"]))
out = run_dir / "metrics" / "attribution_by_engine.csv"
with open(out, "w", newline="") as f:
writer = csv.DictWriter(
f,
fieldnames=[
"engine_id",
"shadow_only",
"count",
"wins",
"win_rate",
"net_pnl",
"avg_r",
],
)
writer.writeheader()
for engine_id, d in sorted(bucket_data.items()):
count = int(d["count"])
wins = int(d["wins"])
writer.writerow(
{
"engine_id": engine_id,
"shadow_only": bool(d["shadow_only"]),
"count": count,
"wins": wins,
"win_rate": wins / count if count > 0 else 0.0,
"net_pnl": round(float(d["net_pnl"]), 4),
"avg_r": round(float(d["_r_sum"]) / count if count > 0 else 0.0, 4),
}
)
return out
def write_score_bucket_report(
run_dir: Path,
score_bucket_hit_rate: dict[str, float],
@ -312,6 +386,16 @@ def write_run_notes(run_dir: Path, notes: str = "") -> Path:
return out
def write_per_engine_metrics(
run_dir: Path,
per_engine_metrics: dict[str, dict[str, Any]],
) -> Path:
"""Write metrics/per_engine_metrics.json."""
out = run_dir / "metrics" / "per_engine_metrics.json"
out.write_text(json.dumps(per_engine_metrics, indent=2, default=str))
return out
def write_all_artifacts(
run_dir: Path,
run_id: str,
@ -329,6 +413,7 @@ def write_all_artifacts(
total_candidates_seen: int,
total_orders_rejected: int,
split_name: str | None = None,
per_engine_metrics: dict[str, dict[str, Any]] | None = None,
) -> dict[str, str]:
"""Write all output files. Returns mapping of artifact_name → file_path."""
from libs.backtest.manifests import save_manifest, save_resolved_config
@ -355,11 +440,16 @@ def write_all_artifacts(
paths["attribution_by_sector"] = str(
write_attribution_by_sector(run_dir, trades, candidate_map)
)
paths["attribution_by_engine"] = str(
write_attribution_by_engine(run_dir, trades, per_engine_metrics)
)
paths["score_bucket_report"] = str(
write_score_bucket_report(
run_dir, metrics.score_bucket_hit_rate, trades, candidate_map
)
)
if per_engine_metrics:
paths["per_engine_metrics"] = str(write_per_engine_metrics(run_dir, per_engine_metrics))
# Trade data
if config.reporting.write_trade_blotter:

@ -44,13 +44,20 @@ class Candidate(BaseModel):
sector: str # "UNKNOWN" if unavailable
event_type: str
event_timestamp: dt.datetime # must be timezone-aware
event_date: dt.date | None = None
filing_time_bucket: str
timing_class: str = "unknown" # "same_day", "after_close", "unknown"
reaction_date: dt.date
execution_date: dt.date # mapped from Parquet entry_date at SnapshotStore boundary
entry_price_est: float # reaction-day close price
avg_dollar_volume: float # 20-day mean(volume * close)
atr_14: float | None = None
score_bucket: str
engine_id: str = "default"
entry_timing_policy: str = "next_open"
shadow_only: bool = False
engine_max_holding_days: int | None = None
engine_risk_budget_pct: float = 1.0
trade_direction: str = "long" # "long" or "short"
features: dict[str, Any] = Field(default_factory=dict)
@ -65,6 +72,11 @@ class PlannedOrder(BaseModel):
stop_price: float
target_price: float
risk_dollars: float
event_date: dt.date | None = None
timing_class: str = "unknown"
engine_id: str = "default"
entry_timing_policy: str = "next_open"
shadow_only: bool = False
skip_reason: str | None = None # non-None means the order was rejected
@ -76,6 +88,11 @@ class FilledTrade(BaseModel):
position_id: str
event_id: str
symbol: str
event_date: dt.date | None = None
timing_class: str = "unknown"
engine_id: str = "default"
entry_timing_policy: str = "next_open"
shadow_only: bool = False
entry_date: dt.date
exit_date: dt.date
entry_price: float
@ -227,6 +244,27 @@ class ExecutionConfig(BaseModel):
no_follow_through_exit: bool = False # exit at D+1 close if close < entry price
class StrategyEngineConfig(BaseModel):
"""Specialist engine routing and execution policy."""
engine_id: str
event_types: list[str] = Field(default_factory=list)
timing_class: str = "any" # "same_day", "after_close", "any"
direction: str = "any" # "long_only", "short_only", "any"
entry_timing_policy: str = "next_open" # "next_open", "reaction_close"
max_holding_days: int | None = None
engine_risk_budget_pct: float = 1.0
score_threshold_override: float | None = None
pead_reaction_threshold_override: float | None = None
pead_volume_threshold_override: float | None = None
reaction_day_return_min: float | None = None
reaction_day_return_max: float | None = None
gap_size_min: float | None = None
gap_size_max: float | None = None
shadow_only: bool = False
enabled: bool = True
class EventTypeProfile(BaseModel):
"""Per-event-type overrides for scoring, risk, and exit parameters."""
enabled: bool = True
@ -254,11 +292,25 @@ class BacktestConfig(BaseModel):
execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
reporting: ReportingConfig = Field(default_factory=ReportingConfig)
event_type_profiles: dict[str, EventTypeProfile] = Field(default_factory=dict)
strategy_engines: list[StrategyEngineConfig] = Field(default_factory=list)
strategy_engine_selection_mode: str = "interleave" # "interleave" or "global_score"
def get_event_profile(self, event_type: str) -> EventTypeProfile | None:
"""Look up event-type-specific profile. Returns None if no override."""
return self.event_type_profiles.get(event_type)
def get_strategy_engines(self) -> list[StrategyEngineConfig]:
"""Enabled strategy engines in manifest order."""
return [engine for engine in self.strategy_engines if engine.enabled]
def get_active_strategy_engines(self) -> list[StrategyEngineConfig]:
"""Enabled engines that participate in the live portfolio."""
return [engine for engine in self.get_strategy_engines() if not engine.shadow_only]
def get_shadow_strategy_engines(self) -> list[StrategyEngineConfig]:
"""Enabled engines that run in paper/shadow mode only."""
return [engine for engine in self.get_strategy_engines() if engine.shadow_only]
# ---------------------------------------------------------------------------
# Experiment models
@ -275,6 +327,7 @@ class ExperimentManifest(BaseModel):
description: str | None = None
base_config: str # path to base config JSON file
overrides: dict[str, Any] = Field(default_factory=dict)
strategy_engines: list[StrategyEngineConfig] = Field(default_factory=list)
splits: list[SplitSpec] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
notes: str | None = None

@ -55,20 +55,15 @@ def simulate_entry(
config: ExecutionConfig,
position_id: str | None = None,
) -> OpenPosition | None:
"""Simulate filling a planned entry at the bar's open.
"""Simulate filling a planned entry at the bar's open or reaction close.
Returns None (position NOT opened) if bar is missing or open is invalid.
Returns None (position NOT opened) if bar is missing or the required price is invalid.
No zero imputation — missing bar = no entry.
"""
if bar is None:
logger.warning("entry_skip_missing_bar", event_id=plan.candidate.event_id)
return None
bar_open = bar.get("open")
if bar_open is None or bar_open <= 0:
logger.warning("entry_skip_invalid_open", event_id=plan.candidate.event_id, bar=bar)
return None
if plan.skip_reason is not None:
logger.debug("entry_skip_gate_rejected", reason=plan.skip_reason)
return None
@ -77,12 +72,24 @@ def simulate_entry(
logger.warning("entry_skip_zero_shares", event_id=plan.candidate.event_id)
return None
entry_policy = plan.entry_timing_policy or "next_open"
if entry_policy == "reaction_close":
reference_price = bar.get("close")
if reference_price is None or reference_price <= 0:
logger.warning("entry_skip_invalid_close", event_id=plan.candidate.event_id, bar=bar)
return None
else:
reference_price = bar.get("open")
if reference_price is None or reference_price <= 0:
logger.warning("entry_skip_invalid_open", event_id=plan.candidate.event_id, bar=bar)
return None
is_short = plan.candidate.trade_direction == "short"
if is_short:
fill_price = _short_entry_fill(float(bar_open), config.slippage_bps_base)
fill_price = _short_entry_fill(float(reference_price), config.slippage_bps_base)
else:
fill_price = _long_entry_fill(float(bar_open), config.slippage_bps_base)
slippage_bps_actual = abs(fill_price / float(bar_open) - 1.0) * 10_000
fill_price = _long_entry_fill(float(reference_price), config.slippage_bps_base)
slippage_bps_actual = abs(fill_price / float(reference_price) - 1.0) * 10_000
pid = position_id or str(uuid.uuid4())
@ -370,6 +377,11 @@ def _build_filled_trade_partial(
position_id=position.position_id,
event_id=position.plan.candidate.event_id,
symbol=position.plan.candidate.symbol,
event_date=position.plan.event_date or position.plan.candidate.event_date,
timing_class=position.plan.timing_class,
engine_id=position.plan.engine_id,
entry_timing_policy=position.plan.entry_timing_policy,
shadow_only=position.plan.shadow_only,
entry_date=position.entry_date,
exit_date=exit_date,
entry_price=position.entry_price,
@ -424,6 +436,11 @@ def _build_filled_trade(
position_id=position.position_id,
event_id=position.plan.candidate.event_id,
symbol=position.plan.candidate.symbol,
event_date=position.plan.event_date or position.plan.candidate.event_date,
timing_class=position.plan.timing_class,
engine_id=position.plan.engine_id,
entry_timing_policy=position.plan.entry_timing_policy,
shadow_only=position.plan.shadow_only,
entry_date=position.entry_date,
exit_date=exit_date,
entry_price=position.entry_price,

@ -66,6 +66,11 @@ def resolve_config(
# Inject snapshot_id
sid = snapshot_id_override or manifest.dataset_snapshot_id
merged["dataset_snapshot_id"] = sid
if manifest.strategy_engines:
merged["strategy_engines"] = [
engine.model_dump(mode="json")
for engine in manifest.strategy_engines
]
return BacktestConfig.model_validate(merged)
@ -82,11 +87,11 @@ def generate_run_id(
) -> str:
"""Generate a unique, deterministic run ID.
Format: bt_{safe_strategy}_{safe_snapshot[:12]}_{timestamp}_{config_hash[:8]}
Format: bt_{safe_strategy}_{safe_snapshot[:12]}_{timestamp_us}_{config_hash[:8]}
"""
strategy = _safe_slug(strategy_override or config.strategy_name)
snapshot = _safe_slug(config.dataset_snapshot_id, max_len=12)
timestamp = utc_now().strftime("%Y%m%d%H%M%S")
timestamp = utc_now().strftime("%Y%m%d%H%M%S%f")
config_json = config.model_dump_json(indent=None)
config_hash = sha256_checksum_str(config_json)[:8]
return f"bt_{strategy}_{snapshot}_{timestamp}_{config_hash}"

@ -5,14 +5,23 @@ import datetime as dt
from typing import Any
from zoneinfo import ZoneInfo
from libs.backtest.domain import Candidate, EventTypeProfile, SignalConfig, UniverseConfig
from libs.backtest.domain import (
Candidate,
EventTypeProfile,
SignalConfig,
StrategyEngineConfig,
UniverseConfig,
)
from libs.common.logging import get_logger
logger = get_logger(__name__)
_UTC = ZoneInfo("UTC")
def build_candidate(row: dict[str, Any]) -> Candidate | None:
def build_candidate(
row: dict[str, Any],
strategy_engine: StrategyEngineConfig | None = None,
) -> Candidate | None:
"""Build a Candidate from a raw Parquet row dict.
Returns None (logged as skip) if:
@ -44,8 +53,49 @@ def build_candidate(row: dict[str, Any]) -> Candidate | None:
if event_timestamp.tzinfo is None:
event_timestamp = event_timestamp.replace(tzinfo=_UTC)
# entry_price_est (mapped from Parquet entry_price)
entry_price_est = row.get("entry_price") or row.get("entry_price_est")
# reaction_date
raw_react = row.get("reaction_date")
reaction_date = _parse_date(raw_react)
# execution_date (mapped from Parquet entry_date) or reaction date for close-entry engines
raw_exec_date = row.get("execution_date") or row.get("entry_date")
execution_date = _parse_date(raw_exec_date)
if execution_date is None:
if strategy_engine and strategy_engine.entry_timing_policy == "reaction_close":
execution_date = reaction_date
else:
logger.warning("skip_candidate_no_exec_date", event_id=event_id)
return None
if reaction_date is None:
reaction_date = execution_date
event_date = _parse_date(row.get("event_date")) or event_timestamp.date()
timing_class = _classify_timing_class(event_date, reaction_date)
trade_direction = _resolve_trade_direction(row)
if strategy_engine is not None and not _matches_strategy_engine(
row=row,
strategy_engine=strategy_engine,
event_type=str(row.get("event_type", "")),
timing_class=timing_class,
trade_direction=trade_direction,
):
return None
if strategy_engine and strategy_engine.entry_timing_policy == "reaction_close":
execution_date = reaction_date
entry_price_est = row.get("event_close") or row.get("entry_price_est")
if not entry_price_est:
logger.debug(
"skip_candidate_no_event_close",
event_id=event_id,
engine_id=strategy_engine.engine_id,
)
return None
else:
entry_price_est = row.get("entry_price") or row.get("entry_price_est")
if not entry_price_est:
logger.warning("skip_candidate_no_entry_price", event_id=event_id)
return None
@ -54,28 +104,6 @@ def build_candidate(row: dict[str, Any]) -> Candidate | None:
logger.warning("skip_candidate_zero_entry_price", event_id=event_id)
return None
# execution_date (mapped from Parquet entry_date)
raw_exec_date = row.get("execution_date") or row.get("entry_date")
if raw_exec_date is None:
logger.warning("skip_candidate_no_exec_date", event_id=event_id)
return None
if isinstance(raw_exec_date, str):
execution_date = dt.date.fromisoformat(raw_exec_date)
elif isinstance(raw_exec_date, dt.date):
execution_date = raw_exec_date
else:
logger.warning("skip_candidate_bad_exec_date", event_id=event_id)
return None
# reaction_date
raw_react = row.get("reaction_date")
if isinstance(raw_react, str):
reaction_date = dt.date.fromisoformat(raw_react)
elif isinstance(raw_react, dt.date):
reaction_date = raw_react
else:
reaction_date = execution_date # fallback: same as execution
score = float(row.get("score", 0.0))
avg_dollar_volume = float(row.get("avg_dollar_volume", 0.0))
atr_14_raw = row.get("atr_14")
@ -84,8 +112,6 @@ def build_candidate(row: dict[str, Any]) -> Candidate | None:
# Classify score bucket
score_bucket = _classify_score_bucket(score)
trade_direction = str(row.get("trade_direction", "long"))
return Candidate(
event_id=event_id,
symbol=str(row.get("symbol", row.get("ticker", ""))),
@ -94,13 +120,32 @@ def build_candidate(row: dict[str, Any]) -> Candidate | None:
sector=str(row.get("sector") or "UNKNOWN"),
event_type=str(row.get("event_type", "")),
event_timestamp=event_timestamp,
event_date=event_date,
filing_time_bucket=str(row.get("filing_time_bucket", "unknown")),
timing_class=timing_class,
reaction_date=reaction_date,
execution_date=execution_date,
entry_price_est=entry_price_est,
avg_dollar_volume=avg_dollar_volume,
atr_14=atr_14,
score_bucket=score_bucket,
engine_id=strategy_engine.engine_id if strategy_engine else "default",
entry_timing_policy=(
strategy_engine.entry_timing_policy
if strategy_engine
else "next_open"
),
shadow_only=strategy_engine.shadow_only if strategy_engine else False,
engine_max_holding_days=(
strategy_engine.max_holding_days
if strategy_engine
else None
),
engine_risk_budget_pct=(
strategy_engine.engine_risk_budget_pct
if strategy_engine
else 1.0
),
trade_direction=trade_direction,
features={k: v for k, v in row.items() if k not in _RESERVED_KEYS},
)
@ -108,12 +153,104 @@ def build_candidate(row: dict[str, Any]) -> Candidate | None:
_RESERVED_KEYS = {
"event_id", "symbol", "ticker", "issuer_id", "score", "sector",
"event_type", "event_timestamp", "filing_time_bucket", "reaction_date",
"event_type", "event_timestamp", "event_date", "filing_time_bucket", "reaction_date",
"entry_date", "execution_date", "entry_price", "entry_price_est",
"avg_dollar_volume", "atr_14", "score_bucket", "trade_direction",
"event_close", "avg_dollar_volume", "atr_14", "score_bucket", "trade_direction",
}
def _parse_date(raw: Any) -> dt.date | None:
if isinstance(raw, dt.datetime):
return raw.date()
if isinstance(raw, dt.date):
return raw
if isinstance(raw, str):
try:
return dt.date.fromisoformat(raw)
except ValueError:
return None
return None
def _classify_timing_class(event_date: dt.date | None, reaction_date: dt.date) -> str:
if event_date is None:
return "unknown"
if reaction_date == event_date:
return "same_day"
if reaction_date > event_date:
return "after_close"
return "unknown"
def _resolve_trade_direction(row: dict[str, Any]) -> str:
raw_direction = str(row.get("trade_direction", "")).lower()
if raw_direction in {"long", "short"}:
return raw_direction
reaction = row.get("reaction_day_return")
if reaction is not None:
try:
return "short" if float(reaction) < 0 else "long"
except (TypeError, ValueError):
pass
return "long"
def _matches_strategy_engine(
row: dict[str, Any],
strategy_engine: StrategyEngineConfig,
event_type: str,
timing_class: str,
trade_direction: str,
) -> bool:
if strategy_engine.event_types and event_type not in strategy_engine.event_types:
return False
if strategy_engine.timing_class != "any" and timing_class != strategy_engine.timing_class:
return False
if strategy_engine.direction == "long_only" and trade_direction != "long":
return False
if strategy_engine.direction == "short_only" and trade_direction != "short":
return False
reaction_day_return = _safe_float(row.get("reaction_day_return"))
if (
strategy_engine.reaction_day_return_min is not None
and reaction_day_return is not None
and reaction_day_return < strategy_engine.reaction_day_return_min
):
return False
if (
strategy_engine.reaction_day_return_max is not None
and reaction_day_return is not None
and reaction_day_return > strategy_engine.reaction_day_return_max
):
return False
gap_size = _safe_float(row.get("gap_size"))
if (
strategy_engine.gap_size_min is not None
and gap_size is not None
and gap_size < strategy_engine.gap_size_min
):
return False
if (
strategy_engine.gap_size_max is not None
and gap_size is not None
and gap_size > strategy_engine.gap_size_max
):
return False
if (
strategy_engine.entry_timing_policy == "reaction_close"
and row.get("event_close") in (None, 0, 0.0, "")
):
return False
return True
def _classify_score_bucket(score: float) -> str:
if score >= 0.8:
return "high"
@ -126,6 +263,13 @@ def _classify_score_bucket(score: float) -> str:
return "low"
def _safe_float(raw: Any) -> float | None:
try:
return float(raw)
except (TypeError, ValueError):
return None
def rank_candidates(candidates: list[Candidate]) -> list[Candidate]:
"""Sort by score DESC, avg_dollar_volume DESC, symbol ASC (stable, deterministic)."""
return sorted(candidates, key=lambda c: (-c.score, -c.avg_dollar_volume, c.symbol))
@ -199,18 +343,72 @@ def select_candidates(
universe_config: UniverseConfig,
signal_config: SignalConfig,
event_type_profiles: dict[str, EventTypeProfile] | None = None,
strategy_engine: StrategyEngineConfig | None = None,
) -> list[Candidate]:
"""Full selection pipeline: build → filter → rank → truncate."""
candidates = []
for row in raw_rows:
c = build_candidate(row)
prepared_row = _prepare_row_for_strategy_engine(
row,
signal_config=signal_config,
strategy_engine=strategy_engine,
)
c = build_candidate(prepared_row, strategy_engine=strategy_engine)
if c is not None:
candidates.append(c)
candidates = filter_by_universe(candidates, universe_config)
candidates = filter_by_score(candidates, signal_config.score_threshold)
candidates = filter_by_score(
candidates,
_resolve_score_threshold(signal_config, strategy_engine),
)
if event_type_profiles:
candidates = filter_by_event_type(candidates, event_type_profiles)
candidates = rank_candidates(candidates)
candidates = truncate_candidates(candidates, signal_config.max_candidates_per_day)
return candidates
def _resolve_score_threshold(
signal_config: SignalConfig,
strategy_engine: StrategyEngineConfig | None,
) -> float:
if strategy_engine and strategy_engine.score_threshold_override is not None:
return strategy_engine.score_threshold_override
return signal_config.score_threshold
def _prepare_row_for_strategy_engine(
row: dict[str, Any],
signal_config: SignalConfig,
strategy_engine: StrategyEngineConfig | None,
) -> dict[str, Any]:
if strategy_engine is None or signal_config.scoring_model != "pead":
return row
reaction_threshold = (
strategy_engine.pead_reaction_threshold_override
if strategy_engine.pead_reaction_threshold_override is not None
else signal_config.pead_reaction_threshold
)
volume_threshold = (
strategy_engine.pead_volume_threshold_override
if strategy_engine.pead_volume_threshold_override is not None
else signal_config.pead_volume_threshold
)
if (
reaction_threshold == signal_config.pead_reaction_threshold
and volume_threshold == signal_config.pead_volume_threshold
):
return row
from libs.backtest.scoring import compute_pead_score
prepared = dict(row)
prepared["score"] = compute_pead_score(
prepared,
reaction_threshold=reaction_threshold,
volume_threshold=volume_threshold,
)
return prepared

@ -32,7 +32,11 @@ class SnapshotStore:
bars_by_symbol_date: dict[str, dict[dt.date, dict[str, Any]]],
macro_by_date: dict[dt.date, dict[str, Any]] | None = None,
) -> None:
self._candidates = candidates_by_exec_date
self._candidates = {
date: list(rows)
for date, rows in candidates_by_exec_date.items()
}
self._candidates_by_reaction_date = self._build_reaction_index(self._candidates)
self._bars = bars_by_symbol_date
self._macro = macro_by_date or {}
@ -44,6 +48,10 @@ class SnapshotStore:
"""Return candidates where execution_date == date. No look-ahead."""
return list(self._candidates.get(date, []))
def get_candidates_for_reaction_date(self, date: dt.date) -> list[dict[str, Any]]:
"""Return candidates where reaction_date == date. No look-ahead."""
return list(self._candidates_by_reaction_date.get(date, []))
def get_bar(self, symbol: str, date: dt.date) -> dict[str, Any] | None:
"""Return OHLCV bar for symbol on date, or None if missing."""
sym_bars = self._bars.get(symbol)
@ -59,7 +67,11 @@ class SnapshotStore:
"""Sorted list of dates that have at least one candidate."""
return sorted(self._candidates.keys())
def all_trading_days(self) -> list[dt.date]:
def all_reaction_dates(self) -> list[dt.date]:
"""Sorted list of dates that have at least one reaction-date candidate."""
return sorted(self._candidates_by_reaction_date.keys())
def all_trading_days(self, include_reaction_dates: bool = False) -> list[dt.date]:
"""All NYSE trading days from first to last execution date (inclusive).
Use this to drive the simulation loop so stop/target/time exits are
@ -67,10 +79,13 @@ class SnapshotStore:
"""
from libs.backtest.calendar import get_trading_days
exec_dates = self.all_execution_dates()
if not exec_dates:
dates = set(self.all_execution_dates())
if include_reaction_dates:
dates.update(self.all_reaction_dates())
if not dates:
return []
return get_trading_days(exec_dates[0], exec_dates[-1])
ordered = sorted(dates)
return get_trading_days(ordered[0], ordered[-1])
# ------------------------------------------------------------------
# Factory: load from Parquet + DB + Oracle
@ -176,6 +191,7 @@ class SnapshotStore:
enriched["execution_date"] = exec_date
enriched["symbol"] = ticker
enriched["issuer_id"] = meta.get("issuer_id")
enriched["event_date"] = meta.get("event_date")
enriched["event_type"] = meta.get("event_type", "")
enriched["event_timestamp"] = meta.get("event_timestamp")
enriched["avg_dollar_volume"] = avg_dvol.get(ticker, 0.0)
@ -253,6 +269,7 @@ class SnapshotStore:
)
result[event.event_id] = {
"issuer_id": event.issuer_id,
"event_date": event.event_date,
"event_type": event.event_type,
"event_timestamp": ts,
"ticker": sym.ticker if sym else None,
@ -268,6 +285,7 @@ class SnapshotStore:
symbols: list[str],
date_range: tuple[dt.date, dt.date] | None,
oracle_url: str,
concurrency: int = 16,
) -> tuple[dict[str, dict[dt.date, dict[str, Any]]], dict[str, float]]:
"""Fetch daily OHLCV bars and compute avg_dollar_volume per symbol."""
if not symbols or date_range is None:
@ -280,39 +298,46 @@ class SnapshotStore:
bars_by_symbol: dict[str, dict[dt.date, dict[str, Any]]] = {}
avg_dvol: dict[str, float] = {}
semaphore = asyncio.Semaphore(concurrency)
async with OracleClient(base_url=oracle_url) as client:
svc = PriceService(client)
for sym in symbols:
try:
resp = await svc.get_daily_bars(sym, start=start_str, end=end_str)
date_bars: dict[dt.date, dict[str, Any]] = {}
dollar_vols: list[float] = []
for bar in resp.bars:
d = dt.date.fromisoformat(bar.date)
b = {
"date": d,
"open": bar.open,
"high": bar.high,
"low": bar.low,
"close": bar.close,
"volume": bar.volume,
}
date_bars[d] = b
dollar_vols.append(bar.close * bar.volume)
bars_by_symbol[sym] = date_bars
# 20-day mean of dollar volume
if dollar_vols:
last_20 = dollar_vols[-20:]
avg_dvol[sym] = sum(last_20) / len(last_20)
else:
avg_dvol[sym] = 0.0
except Exception as sym_exc:
logger.warning(
"snapshot_store_price_fetch_failed", symbol=sym, error=str(sym_exc)
)
bars_by_symbol[sym] = {}
avg_dvol[sym] = 0.0
async def _fetch_symbol(sym: str) -> tuple[str, dict[dt.date, dict[str, Any]], float]:
async with semaphore:
try:
resp = await svc.get_daily_bars(sym, start=start_str, end=end_str)
date_bars: dict[dt.date, dict[str, Any]] = {}
dollar_vols: list[float] = []
for bar in resp.bars:
d = dt.date.fromisoformat(bar.date)
b = {
"date": d,
"open": bar.open,
"high": bar.high,
"low": bar.low,
"close": bar.close,
"volume": bar.volume,
}
date_bars[d] = b
dollar_vols.append(bar.close * bar.volume)
if dollar_vols:
last_20 = dollar_vols[-20:]
mean_dvol = sum(last_20) / len(last_20)
else:
mean_dvol = 0.0
return sym, date_bars, mean_dvol
except Exception as sym_exc:
logger.warning(
"snapshot_store_price_fetch_failed",
symbol=sym,
error=str(sym_exc),
)
return sym, {}, 0.0
results = await asyncio.gather(*(_fetch_symbol(sym) for sym in symbols))
for sym, date_bars, mean_dvol in results:
bars_by_symbol[sym] = date_bars
avg_dvol[sym] = mean_dvol
return bars_by_symbol, avg_dvol
except Exception as exc:
logger.warning("snapshot_store_oracle_failed", error=str(exc))
@ -322,22 +347,29 @@ class SnapshotStore:
async def _fetch_sectors(
symbols: list[str],
oracle_url: str,
concurrency: int = 16,
) -> dict[str, str]:
"""Fetch company sector for each symbol. Default 'UNKNOWN' if unavailable."""
if not symbols:
return {}
result: dict[str, str] = {}
semaphore = asyncio.Semaphore(concurrency)
try:
from libs.oracle_client import CompanyService, OracleClient
async with OracleClient(base_url=oracle_url) as client:
company_svc = CompanyService(client)
for sym in symbols:
try:
info = await company_svc.get_company(sym)
result[sym] = info.sector or "UNKNOWN"
except Exception:
result[sym] = "UNKNOWN"
async def _fetch_sector(sym: str) -> tuple[str, str]:
async with semaphore:
try:
info = await company_svc.get_company(sym)
return sym, info.sector or "UNKNOWN"
except Exception:
return sym, "UNKNOWN"
sector_results = await asyncio.gather(*(_fetch_sector(sym) for sym in symbols))
for sym, sector in sector_results:
result[sym] = sector
except Exception as exc:
logger.warning("snapshot_store_sector_fetch_failed", error=str(exc))
# Default all remaining to UNKNOWN
@ -436,19 +468,49 @@ class SnapshotStore:
def _compute_date_range(
rows: list[dict[str, Any]],
) -> tuple[dt.date, dt.date] | None:
"""Compute (min_date, max_date) from entry_date/execution_date column."""
"""Compute (min_date, max_date) from execution and reaction-date columns."""
dates: list[dt.date] = []
for r in rows:
raw = r.get("entry_date") or r.get("execution_date")
if raw is None:
continue
if isinstance(raw, str):
try:
dates.append(dt.date.fromisoformat(raw))
except ValueError:
pass
elif isinstance(raw, dt.date):
dates.append(raw)
for raw in (
r.get("entry_date"),
r.get("execution_date"),
r.get("reaction_date"),
):
if raw is None:
continue
if isinstance(raw, str):
try:
dates.append(dt.date.fromisoformat(raw))
except ValueError:
pass
elif isinstance(raw, dt.date):
dates.append(raw)
if not dates:
return None
return min(dates), max(dates)
@staticmethod
def _build_reaction_index(
candidates_by_exec_date: dict[dt.date, list[dict[str, Any]]],
) -> dict[dt.date, list[dict[str, Any]]]:
reaction_index: dict[dt.date, list[dict[str, Any]]] = {}
for rows in candidates_by_exec_date.values():
for row in rows:
reaction_date = SnapshotStore._normalize_date(row.get("reaction_date"))
if reaction_date is None:
continue
reaction_index.setdefault(reaction_date, []).append(row)
return reaction_index
@staticmethod
def _normalize_date(raw: Any) -> dt.date | None:
if isinstance(raw, dt.datetime):
return raw.date()
if isinstance(raw, dt.date):
return raw
if isinstance(raw, str):
try:
return dt.date.fromisoformat(raw)
except ValueError:
return None
return None

@ -76,7 +76,67 @@ def _build_synthetic_store() -> object:
)
def _make_config():
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,
@ -111,6 +171,7 @@ def _make_config():
write_metrics_summary=True,
generate_plots=False,
),
strategy_engines=strategy_engines or [],
)
@ -258,3 +319,66 @@ class TestBacktestRunIntegration:
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 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

@ -23,6 +23,7 @@ from libs.backtest.domain import (
ReportingConfig,
RiskConfig,
SignalConfig,
StrategyEngineConfig,
UniverseConfig,
)
@ -282,6 +283,56 @@ class TestEventTypeProfile:
assert cfg.event_type_profiles == {}
assert cfg.get_event_profile("anything") is None
def test_backtest_config_strategy_engines_helpers(self):
cfg = BacktestConfig(
strategy_name="test",
dataset_snapshot_id="snap_001",
strategy_engines=[
StrategyEngineConfig(
engine_id="active_engine",
event_types=["earnings_release"],
timing_class="same_day",
direction="long_only",
),
StrategyEngineConfig(
engine_id="shadow_engine",
event_types=["earnings_release"],
timing_class="after_close",
direction="short_only",
shadow_only=True,
),
],
)
assert [engine.engine_id for engine in cfg.get_strategy_engines()] == [
"active_engine",
"shadow_engine",
]
assert [engine.engine_id for engine in cfg.get_active_strategy_engines()] == [
"active_engine",
]
assert [engine.engine_id for engine in cfg.get_shadow_strategy_engines()] == [
"shadow_engine",
]
def test_strategy_engine_config_supports_engine_specific_thresholds(self):
engine = StrategyEngineConfig(
engine_id="after_close_long_quality",
score_threshold_override=0.75,
pead_reaction_threshold_override=0.12,
pead_volume_threshold_override=3.0,
reaction_day_return_min=-0.45,
reaction_day_return_max=0.35,
gap_size_min=0.05,
gap_size_max=0.30,
)
assert engine.score_threshold_override == 0.75
assert engine.pead_reaction_threshold_override == 0.12
assert engine.pead_volume_threshold_override == 3.0
assert engine.reaction_day_return_min == -0.45
assert engine.reaction_day_return_max == 0.35
assert engine.gap_size_min == 0.05
assert engine.gap_size_max == 0.30
class TestMetricsBundleBootstrap:
def test_bootstrap_cis_field(self):

@ -30,6 +30,7 @@ def _make_candidate(**kwargs) -> Candidate:
sector="Technology",
event_type="earnings",
event_timestamp=_NOW,
event_date=_TODAY,
filing_time_bucket="post_market",
reaction_date=_TODAY,
execution_date=_TOMORROW,
@ -49,6 +50,11 @@ def _make_plan(entry_price=100.0, stop=95.0, target=110.0, shares=100) -> Planne
stop_price=stop,
target_price=target,
risk_dollars=500.0,
event_date=_TODAY,
timing_class="same_day",
engine_id="engine_1",
entry_timing_policy="next_open",
shadow_only=False,
)
@ -142,6 +148,18 @@ class TestSimulateEntry:
pos = simulate_entry(plan, bar, _make_exec_config())
assert pos.entry_date == _TOMORROW
def test_reaction_close_entry_uses_close(self):
from libs.backtest.execution import simulate_entry
plan = _make_plan()
plan = plan.model_copy(update={"entry_timing_policy": "reaction_close"})
bar = _make_bar(open=100.0, close=102.0, date=_TODAY)
pos = simulate_entry(plan, bar, _make_exec_config(slippage_bps_base=10.0))
assert pos is not None
expected = 102.0 * (1 + 10 / 10_000)
assert pos.entry_price == pytest.approx(expected)
assert pos.entry_date == _TODAY
class TestSimulateExit:
def test_stop_exit(self):
@ -232,6 +250,17 @@ class TestSimulateExit:
assert trade.gross_pnl == pytest.approx(1000.0)
assert trade.net_pnl == pytest.approx(998.0)
def test_trade_metadata_propagated(self):
from libs.backtest.execution import simulate_exit
pos = _make_open_position(entry_price=100.0, stop=95.0, target=110.0, shares=100)
bar = _make_bar(low=98.0, high=115.0)
trade = simulate_exit(pos, bar, _make_exec_config(slippage_bps_base=0.0), _TOMORROW)
assert trade.engine_id == "engine_1"
assert trade.timing_class == "same_day"
assert trade.event_date == _TODAY
assert trade.shadow_only is False
class TestPartialExit:
def test_partial_exit_at_target(self):

@ -176,6 +176,36 @@ class TestResolveConfig:
config = resolve_config(manifest, snapshot_id_override="snap_override")
assert config.dataset_snapshot_id == "snap_override"
def test_strategy_engines_injected_from_manifest(self, tmp_path):
from libs.backtest.manifests import load_manifest, resolve_config
cfg_file = tmp_path / "defaults.json"
_write_json(cfg_file, VALID_BASE_CONFIG)
manifest_data = {
"experiment_name": "test",
"dataset_snapshot_id": "snap_001",
"base_config": str(cfg_file),
"overrides": {},
"strategy_engines": [
{
"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.4,
}
],
}
m_file = tmp_path / "manifest.json"
_write_json(m_file, manifest_data)
manifest = load_manifest(m_file)
config = resolve_config(manifest)
assert len(config.strategy_engines) == 1
assert config.strategy_engines[0].engine_id == "earnings_same_day_short_v1"
class TestGenerateRunId:
def test_format(self):

@ -6,7 +6,12 @@ from zoneinfo import ZoneInfo
import pytest
from libs.backtest.domain import EventTypeProfile, SignalConfig, UniverseConfig
from libs.backtest.domain import (
EventTypeProfile,
SignalConfig,
StrategyEngineConfig,
UniverseConfig,
)
_UTC = ZoneInfo("UTC")
@ -42,6 +47,7 @@ class TestBuildCandidate:
assert c.score == 0.75
assert c.execution_date == dt.date(2026, 1, 7)
assert c.event_timestamp.tzinfo is not None
assert c.timing_class == "after_close"
def test_null_timestamp_returns_none(self):
from libs.backtest.selector import build_candidate
@ -95,6 +101,286 @@ class TestBuildCandidate:
c = build_candidate(_make_raw_row(score=0.10))
assert c.score_bucket == "low"
def test_same_day_timing_and_direction_from_reaction(self):
from libs.backtest.selector import build_candidate
row = _make_raw_row(
event_date="2026-01-06",
reaction_date="2026-01-06",
execution_date="2026-01-07",
trade_direction="",
reaction_day_return=-0.12,
)
c = build_candidate(row)
assert c is not None
assert c.event_date == dt.date(2026, 1, 6)
assert c.timing_class == "same_day"
assert c.trade_direction == "short"
def test_reaction_close_engine_uses_event_close(self):
from libs.backtest.selector import build_candidate
engine = StrategyEngineConfig(
engine_id="earnings_same_day_long_close_v1",
event_types=["earnings"],
timing_class="same_day",
direction="long_only",
entry_timing_policy="reaction_close",
max_holding_days=3,
engine_risk_budget_pct=0.35,
)
row = _make_raw_row(
event_date="2026-01-06",
reaction_date="2026-01-06",
event_close=149.5,
entry_date="2026-01-07",
reaction_day_return=0.11,
)
c = build_candidate(row, strategy_engine=engine)
assert c is not None
assert c.execution_date == dt.date(2026, 1, 6)
assert c.entry_price_est == pytest.approx(149.5)
assert c.engine_id == "earnings_same_day_long_close_v1"
assert c.entry_timing_policy == "reaction_close"
def test_engine_route_skips_non_matching_direction(self):
from libs.backtest.selector import build_candidate
engine = StrategyEngineConfig(
engine_id="short_only_engine",
event_types=["earnings"],
timing_class="after_close",
direction="short_only",
)
row = _make_raw_row(reaction_day_return=0.09, trade_direction="long")
assert build_candidate(row, strategy_engine=engine) is None
def test_select_candidates_recomputes_pead_score_for_engine_thresholds(self):
from libs.backtest.selector import select_candidates
rows = [
_make_raw_row(
symbol="LOWVOL",
score=0.8,
event_type="earnings_release",
reaction_day_return=0.14,
volume_ratio_20d=2.4,
gap_size=0.01,
),
_make_raw_row(
symbol="HIGHVOL",
score=0.8,
event_type="earnings_release",
reaction_day_return=0.14,
volume_ratio_20d=4.5,
gap_size=0.01,
),
]
universe = UniverseConfig(min_price=5.0, min_avg_dollar_volume=0.0)
signal = SignalConfig(
scoring_model="pead",
score_threshold=0.65,
pead_reaction_threshold=0.10,
pead_volume_threshold=2.0,
max_candidates_per_day=5,
)
engine = StrategyEngineConfig(
engine_id="after_close_long_quality",
event_types=["earnings_release"],
timing_class="after_close",
direction="long_only",
pead_volume_threshold_override=3.0,
)
selected = select_candidates(rows, universe, signal, strategy_engine=engine)
assert [candidate.symbol for candidate in selected] == ["HIGHVOL"]
def test_select_candidates_uses_engine_score_threshold_override(self):
from libs.backtest.selector import select_candidates
rows = [
_make_raw_row(
symbol="PASS",
score=0.78,
event_type="earnings_release",
reaction_day_return=0.16,
volume_ratio_20d=4.0,
gap_size=0.01,
),
_make_raw_row(
symbol="FAIL",
score=0.72,
event_type="earnings_release",
reaction_day_return=0.14,
volume_ratio_20d=4.0,
gap_size=0.01,
),
]
universe = UniverseConfig(min_price=5.0, min_avg_dollar_volume=0.0)
signal = SignalConfig(
scoring_model="pead",
score_threshold=0.65,
pead_reaction_threshold=0.10,
pead_volume_threshold=2.0,
max_candidates_per_day=5,
)
engine = StrategyEngineConfig(
engine_id="after_close_long_quality",
event_types=["earnings_release"],
timing_class="after_close",
direction="long_only",
score_threshold_override=0.75,
)
selected = select_candidates(rows, universe, signal, strategy_engine=engine)
assert [candidate.symbol for candidate in selected] == ["PASS"]
def test_select_candidates_respects_engine_reaction_day_return_bounds(self):
from libs.backtest.selector import select_candidates
rows = [
_make_raw_row(
symbol="CRASH",
score=0.85,
event_type="earnings_release",
event_date="2026-01-06",
reaction_date="2026-01-06",
reaction_day_return=-0.52,
volume_ratio_20d=8.0,
trade_direction="short",
),
_make_raw_row(
symbol="NORMAL",
score=0.82,
event_type="earnings_release",
event_date="2026-01-06",
reaction_date="2026-01-06",
reaction_day_return=-0.18,
volume_ratio_20d=5.0,
trade_direction="short",
),
]
universe = UniverseConfig(min_price=5.0, min_avg_dollar_volume=0.0)
signal = SignalConfig(
scoring_model="pead",
score_threshold=0.65,
pead_reaction_threshold=0.10,
pead_volume_threshold=2.0,
max_candidates_per_day=5,
)
engine = StrategyEngineConfig(
engine_id="same_day_short_filtered",
event_types=["earnings_release"],
timing_class="same_day",
direction="short_only",
reaction_day_return_min=-0.45,
)
selected = select_candidates(rows, universe, signal, strategy_engine=engine)
assert [candidate.symbol for candidate in selected] == ["NORMAL"]
def test_select_candidates_respects_engine_reaction_day_return_upper_bound(self):
from libs.backtest.selector import select_candidates
rows = [
_make_raw_row(
symbol="TOO_HOT",
score=0.90,
event_type="earnings_release",
event_date="2026-01-06",
reaction_date="2026-01-06",
reaction_day_return=0.42,
volume_ratio_20d=6.0,
trade_direction="long",
),
_make_raw_row(
symbol="OK",
score=0.80,
event_type="earnings_release",
event_date="2026-01-06",
reaction_date="2026-01-06",
reaction_day_return=0.18,
volume_ratio_20d=4.0,
trade_direction="long",
),
]
universe = UniverseConfig(min_price=5.0, min_avg_dollar_volume=0.0)
signal = SignalConfig(
scoring_model="pead",
score_threshold=0.65,
pead_reaction_threshold=0.10,
pead_volume_threshold=2.0,
max_candidates_per_day=5,
)
engine = StrategyEngineConfig(
engine_id="same_day_long_capped",
event_types=["earnings_release"],
timing_class="same_day",
direction="long_only",
reaction_day_return_max=0.30,
)
selected = select_candidates(rows, universe, signal, strategy_engine=engine)
assert [candidate.symbol for candidate in selected] == ["OK"]
def test_select_candidates_respects_engine_gap_size_bounds(self):
from libs.backtest.selector import select_candidates
rows = [
_make_raw_row(
symbol="TIGHT",
score=0.82,
event_type="earnings_release",
event_date="2026-01-06",
reaction_date="2026-01-06",
reaction_day_return=0.18,
volume_ratio_20d=4.0,
gap_size=0.04,
trade_direction="long",
),
_make_raw_row(
symbol="WIDE",
score=0.84,
event_type="earnings_release",
event_date="2026-01-06",
reaction_date="2026-01-06",
reaction_day_return=0.18,
volume_ratio_20d=4.0,
gap_size=0.16,
trade_direction="long",
),
_make_raw_row(
symbol="TOO_WIDE",
score=0.86,
event_type="earnings_release",
event_date="2026-01-06",
reaction_date="2026-01-06",
reaction_day_return=0.18,
volume_ratio_20d=4.0,
gap_size=0.34,
trade_direction="long",
),
]
universe = UniverseConfig(min_price=5.0, min_avg_dollar_volume=0.0)
signal = SignalConfig(
scoring_model="pead",
score_threshold=0.65,
pead_reaction_threshold=0.10,
pead_volume_threshold=2.0,
max_candidates_per_day=5,
)
engine = StrategyEngineConfig(
engine_id="same_day_long_gapped",
event_types=["earnings_release"],
timing_class="same_day",
direction="long_only",
gap_size_min=0.10,
gap_size_max=0.30,
)
selected = select_candidates(rows, universe, signal, strategy_engine=engine)
assert [candidate.symbol for candidate in selected] == ["WIDE"]
class TestRankCandidates:
def test_sorted_by_score_desc(self):

@ -79,6 +79,12 @@ class TestSnapshotStoreQuery:
rows = store.get_candidates_for_date(dt.date(2026, 1, 1))
assert rows == []
def test_get_candidates_for_reaction_date(self, tmp_path):
store = _build_store_from_fixture(tmp_path)
rows = store.get_candidates_for_reaction_date(dt.date(2026, 1, 5))
assert len(rows) == 1
assert rows[0]["symbol"] == "AAPL"
def test_no_lookahead(self, tmp_path):
"""Candidates for Jan 7 should NOT appear when querying Jan 6."""
store = _build_store_from_fixture(tmp_path)
@ -109,6 +115,11 @@ class TestSnapshotStoreQuery:
assert dt.date(2026, 1, 6) in dates
assert dt.date(2026, 1, 7) in dates
def test_all_reaction_dates_sorted(self, tmp_path):
store = _build_store_from_fixture(tmp_path)
dates = store.all_reaction_dates()
assert dates == [dt.date(2026, 1, 5), dt.date(2026, 1, 6)]
def test_macro_default_empty(self, tmp_path):
store = _build_store_from_fixture(tmp_path)
macro = store.get_macro_for_date(dt.date(2026, 1, 6))
@ -145,9 +156,10 @@ class TestSnapshotStoreFromParquet:
{"entry_date": "2026-01-05"},
{"entry_date": "2026-01-10"},
{"entry_date": "2026-01-07"},
{"reaction_date": "2026-01-04"},
]
result = SnapshotStore._compute_date_range(rows)
assert result == (dt.date(2026, 1, 5), dt.date(2026, 1, 10))
assert result == (dt.date(2026, 1, 4), dt.date(2026, 1, 10))
def test_compute_date_range_empty(self, tmp_path):
from libs.backtest.snapshot_store import SnapshotStore

Loading…
Cancel
Save