From 5056295cb626a72c0fe27ce968adfeaea9dbe782 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Tue, 7 Apr 2026 14:39:03 -0700 Subject: [PATCH] Add lookback entry feature for bounded backtests When a backtest starts mid-stream (via --start), events that fired before the start date but are still within their max_holding_days window can now be entered on the first simulation day. - Add `lookback_entry_enabled: bool = False` to ExecutionConfig - On first sim day, _collect_lookback_candidates() gathers pre-start events, runs them through the same select_candidates() pipeline, and injects them before normal candidates - Entry fills at the first day's open price; gap-cap check is skipped since the event is multi-days old - days_held is initialized to the elapsed trading days so TIME exits fire at the correct time relative to the original event date - Store slice is extended backward by max_mhd calendar buffer so pre-start rows survive slice_by_date_range when feature is enabled - Enabled in return_max_long_v7.119 for testing Co-Authored-By: Claude Sonnet 4.6 --- apps/backtester/run.py | 174 +++- .../experiments/return_max_long_v7.119.json | 582 +++++++++++ libs/backtest/domain.py | 935 +++++++++++++++++- 3 files changed, 1674 insertions(+), 17 deletions(-) create mode 100644 configs/experiments/return_max_long_v7.119.json diff --git a/apps/backtester/run.py b/apps/backtester/run.py index 6920deb..bb4f055 100644 --- a/apps/backtester/run.py +++ b/apps/backtester/run.py @@ -248,6 +248,8 @@ class BacktestRunner: self._dividend_capture_trade_counter: int = 0 self._form4_capture_trade_counter: int = 0 self._ownership_capture_trade_counter: int = 0 + self._lookback_entry_enabled: bool = config.execution.lookback_entry_enabled + self._lookback_injected: bool = False def _get_known_upcoming_earnings_by_symbol( self, @@ -840,6 +842,14 @@ class BacktestRunner: if not self._kill_switch_triggered: portfolio_state = self._build_portfolio_state(date, drawdown_pct, unrealized) candidates = self._select_candidates_for_date(date) + + # Lookback: on the first simulation day inject pre-start events still within mhd + if self._lookback_entry_enabled and not self._lookback_injected: + lookback = self._collect_lookback_candidates(date) + if lookback: + candidates = list(lookback) + list(candidates) + self._lookback_injected = True + shadow_candidates = self._select_shadow_candidates_for_date(date) # Store scored candidates for delayed entry lookback @@ -1527,6 +1537,113 @@ class BacktestRunner: ) return self.store.all_trading_days(include_reaction_dates=include_reaction_dates) + def _collect_lookback_candidates(self, first_sim_date: dt.date) -> list[Candidate]: + """Return candidates from before first_sim_date that are still within their holding window. + + Used on the first day of a bounded backtest so that events which fired before + the requested start date — but whose max_holding_days has not yet expired — + can still be entered at today's open price. + """ + from libs.backtest.calendar import get_trading_days + + # Compute the widest possible holding window across all engines / event profiles + max_mhd = self.config.execution.max_holding_days + if self.config.execution.dynamic_hold_enabled: + max_mhd = max(max_mhd, self.config.execution.dynamic_hold_extend_to) + for engine in self.config.get_strategy_engines(): + if engine.max_holding_days is not None: + engine_mhd = engine.max_holding_days + if engine.dynamic_hold_extend_to_override is not None: + engine_mhd = max(engine_mhd, engine.dynamic_hold_extend_to_override) + max_mhd = max(max_mhd, engine_mhd) + for profile in (self.config.event_type_profiles or {}).values(): + if profile.max_holding_days_override is not None: + max_mhd = max(max_mhd, profile.max_holding_days_override) + + # Collect raw rows for all execution dates before first_sim_date + # Use a calendar-day buffer of 2x to cover weekends/holidays + lookback_rows: list[dict[str, Any]] = [] + for exec_date in sorted(self.store._candidates.keys()): + if exec_date >= first_sim_date: + break + lookback_rows.extend(self.store._candidates[exec_date]) + + if not lookback_rows: + return [] + + # Cache trading-day counts per execution_date for efficiency + _elapsed_cache: dict[dt.date, int] = {} + + def _trading_days_elapsed(exec_date: dt.date) -> int: + if exec_date not in _elapsed_cache: + tdays = get_trading_days(exec_date, first_sim_date) + # Inclusive on both ends; elapsed = days the position has been "in play" + # (exec_date is day 0; first_sim_date adds another day beyond that) + _elapsed_cache[exec_date] = max(0, len(tdays) - 1) + return _elapsed_cache[exec_date] + + # Run through the same selection pipeline as normal candidates + all_lookback: list[Candidate] = [] + engine_list = list(self._primary_strategy_engines) + + if not engine_list: + # No-engine (legacy single-engine) mode + selected = select_candidates( + lookback_rows, + self.config.universe, + self.config.signal, + event_type_profiles=self.config.event_type_profiles or None, + ) + all_lookback.extend(selected) + else: + seen_event_ids: set[str] = set() + for engine in engine_list: + if not self._engine_allowed_for_date(engine, first_sim_date): + continue + if not self._engine_uses_snapshot_candidates(engine): + continue + effective_engine = self._effective_engine_for_date(engine, first_sim_date) + selected = select_candidates( + lookback_rows, + self.config.universe, + self.config.signal, + event_type_profiles=self.config.event_type_profiles or None, + strategy_engine=effective_engine, + engine_lookup=self._strategy_engine_lookup, + excluded_event_ids=seen_event_ids, + ) + all_lookback.extend(selected) + seen_event_ids.update(cand.event_id for cand in selected) + + # Filter by elapsed holding days and annotate surviving candidates + result: list[Candidate] = [] + for candidate in all_lookback: + elapsed = _trading_days_elapsed(candidate.execution_date) + # Get the effective mhd for this specific candidate + eff_exec = self._build_effective_execution_config(candidate) + candidate_mhd = eff_exec.max_holding_days + if eff_exec.dynamic_hold_enabled: + candidate_mhd = max(candidate_mhd, eff_exec.dynamic_hold_extend_to) + + if elapsed >= candidate_mhd: + continue # would have timed out by now + + candidate.features["is_lookback_entry"] = True + candidate.features["lookback_days_elapsed"] = elapsed + candidate.features["lookback_original_execution_date"] = ( + candidate.execution_date.isoformat() + ) + result.append(candidate) + + logger.info( + "lookback_entry_candidates", + first_sim_date=str(first_sim_date), + max_mhd=max_mhd, + rows_scanned=len(lookback_rows), + candidates_selected=len(result), + ) + return result + def _select_candidates_for_date( self, date: dt.date, @@ -1913,19 +2030,21 @@ class BacktestRunner: ) continue - bar = self.store.get_bar(candidate.symbol, candidate.execution_date) - gap_skip_reason = self._check_next_open_gap_cap(candidate, bar) - if gap_skip_reason is not None: - self._total_orders_rejected += 1 - self._release_add_on_reservation(candidate) - logger.debug( - "order_rejected", - engine_id=candidate.engine_id, - symbol=candidate.symbol, - reason=gap_skip_reason, - date=str(date), - ) - continue + is_lookback = bool(candidate.features.get("is_lookback_entry", False)) + bar = self.store.get_bar(candidate.symbol, date if is_lookback else candidate.execution_date) + if not is_lookback: + gap_skip_reason = self._check_next_open_gap_cap(candidate, bar) + if gap_skip_reason is not None: + self._total_orders_rejected += 1 + self._release_add_on_reservation(candidate) + logger.debug( + "order_rejected", + engine_id=candidate.engine_id, + symbol=candidate.symbol, + reason=gap_skip_reason, + date=str(date), + ) + continue pos = simulate_entry( plan, @@ -1938,6 +2057,8 @@ class BacktestRunner: pos.parent_position_id = candidate.parent_position_id pos.is_add_on = candidate.is_add_on + if is_lookback: + pos.days_held = int(candidate.features.get("lookback_days_elapsed", 0)) self._open_positions.append(pos) trade_cost = pos.entry_price * pos.shares_total if allow_parking_cash_release and self._cash < trade_cost and self._get_parking_value(date) > 0: @@ -6896,6 +7017,23 @@ def _last_market_closed_date() -> dt.date: return now_et.date() - dt.timedelta(days=1) +def _compute_max_effective_mhd(config: BacktestConfig) -> int: + """Return the widest max_holding_days value across all engines and event profiles.""" + mhd = config.execution.max_holding_days + if config.execution.dynamic_hold_enabled: + mhd = max(mhd, config.execution.dynamic_hold_extend_to) + for engine in config.get_strategy_engines(): + if engine.max_holding_days is not None: + engine_mhd = engine.max_holding_days + if engine.dynamic_hold_extend_to_override is not None: + engine_mhd = max(engine_mhd, engine.dynamic_hold_extend_to_override) + mhd = max(mhd, engine_mhd) + for profile in (config.event_type_profiles or {}).values(): + if profile.max_holding_days_override is not None: + mhd = max(mhd, profile.max_holding_days_override) + return mhd + + def _extend_store_to_requested_window( *, store: SnapshotStore, @@ -7473,7 +7611,15 @@ def main() -> None: raise RuntimeError("No trading days found in snapshot store.") start_d = dt.date.fromisoformat(args.start) if args.start else all_store_dates[0] end_d = dt.date.fromisoformat(args.end) if args.end else all_store_dates[-1] - store = store.slice_by_date_range(start_d, end_d) + if config.execution.lookback_entry_enabled and args.start: + # Extend slice start backward so pre-start events remain in the store + # for _collect_lookback_candidates; simulation dates are still gated by + # _requested_start_date set below, so those rows stay dormant otherwise. + max_mhd = _compute_max_effective_mhd(config) + lookback_start = start_d - dt.timedelta(days=max_mhd * 2) + store = store.slice_by_date_range(lookback_start, end_d) + else: + store = store.slice_by_date_range(start_d, end_d) store = _extend_store_to_requested_window( store=store, config=config, diff --git a/configs/experiments/return_max_long_v7.119.json b/configs/experiments/return_max_long_v7.119.json new file mode 100644 index 0000000..7e8f77e --- /dev/null +++ b/configs/experiments/return_max_long_v7.119.json @@ -0,0 +1,582 @@ +{ + "experiment_name": "return_max_long_v7.119", + "dataset_snapshot_id": "midlarge-liquid-long-v1_bucketfix_full_audit_canonical", + "description": "v7.118 + 6 per-engine exit/veto optimizations. 12개 엔진. Standalone SQS 92.4 (493% return).\nBEST conservative: tqqq_calm_v2 + sleeves = 3029.38% CW, SQS 89.2\nBEST aggressive: tqqq_active_v2 + sleeves = 3245.24% CW, SQS 89.2\nParking v3: gate_vol 0.35, TQQQ vol 0.22, temp 1.0, entropy 1.45 (1.15→1.45)\nLineage: v7.70→v7.110→v7.113→v7.114→v7.115→v7.116→v7.117→v7.118→v7.119", + "base_config": "configs/backtest/return_max_long_v1.json", + "overrides": { + "signal": { + "scoring_model": "return_max_long_v13e", + "score_threshold": 0.45, + "max_candidates_per_day": 18, + "a_tier_score_threshold": 0.58 + }, + "risk": { + "per_trade_risk_pct": 0.069, + "per_trade_risk_pct_a_tier": 0.0759, + "max_daily_new_risk_pct": 1.5, + "max_positions": 24, + "max_positions_per_sector": 5, + "max_position_value_pct": 1.5, + "max_adv_fraction": 0.2, + "macro_regime_neutral_size_scaler": 1.0, + "macro_regime_risk_off_size_scaler": 1.0, + "veto_unknown_direction": false, + "macro_regime_risk_off_a_tier_only": false, + "stop_atr_multiplier": 1.33, + "allow_budget_downsizing": true + }, + "execution": { + "a_tier_target_1_r": 3.5, + "a_tier_target_1_fraction": 0.0, + "non_a_tier_target_1_r": 2.25, + "non_a_tier_target_1_fraction": 0.2, + "trailing_warmup_days": 7, + "max_holding_days": 12, + "early_failure_no_progress_days": 1, + "early_failure_no_progress_r": 0.15, + "early_failure_no_progress_fraction": 1.0, + "lookback_entry_enabled": true + }, + "event_type_profiles": { + "material_contract": { + "enabled": true, + "direction_filter": "any", + "max_holding_days_override": 20 + }, + "other_material_event": { + "enabled": true, + "direction_filter": "any", + "max_holding_days_override": 20 + }, + "unknown": { + "enabled": true, + "direction_filter": "any", + "max_holding_days_override": 12 + } + } + }, + "strategy_engines": [ + { + "engine_id": "next_open_long_unknown_material_patient", + "event_types": [ + "material_contract" + ], + "event_directions": [ + "unknown" + ], + "guidance_statuses": [ + "not_provided" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "reaction_day_return_min": 0.0, + "reaction_day_return_max": 0.15, + "close_location_min": 0.5, + "close_location_max": 0.85, + "gap_size_min": 0.0, + "gap_size_max": 0.02, + "volume_ratio_min": 0.8, + "volume_ratio_max": 2.0, + "max_market_cap_proxy": 10000000000.0, + "document_quality_score_min": 0.5, + "parse_confidence_overall_min": 0.45, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "veto_parse_confidence_min_override": 0.45, + "next_open_gap_cap_pct": 0.02, + "early_failure_close_below_entry_and_reaction_close_override": true, + "early_failure_no_progress_days_override": 15, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 5.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 12, + "enabled": true, + "min_market_cap_proxy": 5000000000.0, + "macro_vix_max": 30.0 + }, + { + "engine_id": "reaction_close_long_core", + "event_types": [ + "earnings_release", + "guidance_update" + ], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 1.0, + "reaction_day_return_min": 0.0, + "reaction_day_return_max": 0.18, + "close_location_min": 0.45, + "volume_ratio_min": 1.0, + "gap_size_min": 0.0, + "weak_reaction_threshold": 0.03, + "weak_reaction_gap_max": 0.02, + "unknown_direction_reaction_min": 0.05, + "unknown_direction_close_location_min": 0.7, + "attention_max_wiki_spike_10d": 6.0, + "score_threshold_override": 0.42, + "enabled": true, + "residual_reserve_selected": true, + "mixed_inline_close_location_max": 0.88, + "mixed_inline_gap_size_max": 0.1, + "unknown_inline_exit_close_location_min": 0.9, + "unknown_inline_exit_gap_size_max": 0.04, + "unknown_inline_early_failure_no_progress_days_override": 2, + "unknown_inline_early_failure_no_progress_r_override": 0.1, + "unknown_inline_early_failure_no_progress_fraction_override": 1.0, + "per_trade_risk_pct_override": 0.045, + "trailing_warmup_days_override": 8, + "macro_vix_max": 30.0, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_r_override": 0.2 + }, + { + "engine_id": "reaction_close_long_residual_lowclose_gap_d3", + "event_types": [ + "earnings_release", + "guidance_update" + ], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.12, + "reaction_day_return_min": 0.03, + "reaction_day_return_max": 0.2, + "close_location_min": 0.35, + "close_location_max": 0.45, + "volume_ratio_min": 1.25, + "gap_size_min": 0.05, + "gap_size_max": 0.1, + "max_market_cap_proxy": 10000000000.0, + "score_threshold_override": 0.35, + "target_1_r_override": 3.0, + "target_1_fraction_override": 0.0, + "residual_reserve_selected": true, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 3, + "early_failure_no_progress_r_override": 0.25, + "early_failure_no_progress_fraction_override": 1.0, + "enabled": true, + "macro_vix_max": 30.0 + }, + { + "engine_id": "reaction_close_long_residual_smallcap_gap", + "event_types": [ + "earnings_release", + "guidance_update" + ], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.12, + "reaction_day_return_min": 0.03, + "reaction_day_return_max": 0.2, + "close_location_min": 0.45, + "volume_ratio_min": 1.25, + "gap_size_min": 0.05, + "gap_size_max": 0.1, + "max_market_cap_proxy": 10000000000.0, + "score_threshold_override": 0.35, + "target_1_r_override": 3.0, + "target_1_fraction_override": 0.0, + "enabled": true, + "macro_vix_max": 30.0 + }, + { + "engine_id": "reaction_close_long_extreme_orderly", + "event_types": [ + "earnings_release", + "guidance_update" + ], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.35, + "reaction_day_return_min": 0.2, + "reaction_day_return_max": 0.4, + "close_location_min": 0.83, + "volume_ratio_min": 6.0, + "gap_size_min": 0.0, + "gap_size_max": 0.15, + "attention_max_wiki_spike_10d": 6.0, + "score_threshold_override": 0.65, + "enabled": true, + "target_1_r_override": 99.0, + "target_1_fraction_override": 0.0, + "trailing_warmup_days_override": 8, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": null, + "early_failure_no_progress_r_override": null, + "early_failure_no_progress_fraction_override": null, + "macro_vix_max": 30.0 + }, + { + "engine_id": "next_open_long_unknown_inline_hivol", + "event_types": [ + "earnings_release" + ], + "event_directions": [ + "unknown" + ], + "guidance_statuses": [ + "inline_or_maintained" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.1, + "reaction_day_return_min": 0.0, + "reaction_day_return_max": 0.06, + "close_location_min": 0.74, + "close_location_max": 0.86, + "gap_size_min": 0.0, + "gap_size_max": 0.04, + "volume_ratio_min": 2.1, + "volume_ratio_max": 2.8, + "document_quality_score_min": 0.5, + "parse_confidence_overall_min": 0.5, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "veto_oneoff_penalty_override": 1.0, + "next_open_gap_cap_pct": 0.04, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 15, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 5.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 12, + "enabled": true, + "macro_vix_max": 30.0 + }, + { + "engine_id": "next_open_long_guidance_unknown_orderly", + "event_types": [ + "guidance_update" + ], + "event_directions": [ + "unknown" + ], + "guidance_statuses": [ + "not_provided" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.0675, + "reaction_day_return_min": -0.03, + "reaction_day_return_max": 0.05, + "close_location_min": 0.9, + "gap_size_min": -0.02, + "gap_size_max": 0.03, + "volume_ratio_min": 0.8, + "min_market_cap_proxy": 9000000000.0, + "document_quality_score_min": 0.52, + "parse_confidence_overall_min": 0.48, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "next_open_gap_cap_pct": 0.03, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 15, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 4.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 15, + "enabled": true, + "veto_parse_confidence_min_override": 0.48, + "per_trade_risk_pct_override": 0.04, + "stop_atr_multiplier_override": 2.0, + "use_reaction_day_low_stop_override": false, + "macro_vix_max": 30.0 + }, + { + "engine_id": "next_open_long_earnings_mixed_inline_orderly", + "event_types": [ + "earnings_release" + ], + "event_directions": [ + "mixed" + ], + "guidance_statuses": [ + "inline_or_maintained" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.03, + "reaction_day_return_min": -0.03, + "reaction_day_return_max": 0.06, + "close_location_min": 0.5, + "gap_size_min": -0.02, + "gap_size_max": 0.05, + "volume_ratio_min": 1.0, + "min_market_cap_proxy": 8000000000.0, + "document_quality_score_min": 0.5, + "parse_confidence_overall_min": 0.5, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "next_open_gap_cap_pct": 0.04, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 15, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 5.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 12, + "enabled": true, + "veto_parse_confidence_min_override": 0.5, + "per_trade_risk_pct_override": 0.04, + "stop_atr_multiplier_override": 3.0, + "use_reaction_day_low_stop_override": false, + "macro_vix_max": 30.0 + }, + { + "engine_id": "next_open_long_unknown_material_orderly", + "event_types": [ + "material_contract" + ], + "event_directions": [ + "unknown" + ], + "guidance_statuses": [ + "not_provided" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.02, + "reaction_day_return_min": -0.02, + "reaction_day_return_max": 0.06, + "close_location_min": 0.5, + "close_location_max": 0.8, + "gap_size_min": -0.02, + "gap_size_max": 0.02, + "volume_ratio_min": 0.9, + "volume_ratio_max": 1.4, + "min_market_cap_proxy": 10000000000.0, + "max_market_cap_proxy": 100000000000.0, + "document_quality_score_min": 0.5, + "parse_confidence_overall_min": 0.45, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "next_open_gap_cap_pct": 0.03, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 15, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 5.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 12, + "enabled": true, + "veto_parse_confidence_min_override": 0.45, + "per_trade_risk_pct_override": 0.015, + "stop_atr_multiplier_override": 3.0, + "use_reaction_day_low_stop_override": false, + "macro_vix_max": 30.0 + }, + { + "engine_id": "next_open_long_other_material_mixed_orderly", + "event_types": [ + "other_material_event" + ], + "event_directions": [ + "mixed" + ], + "guidance_statuses": [ + "not_provided" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.02, + "reaction_day_return_min": -0.04, + "reaction_day_return_max": 0.06, + "close_location_min": 0.45, + "gap_size_min": -0.03, + "gap_size_max": 0.05, + "volume_ratio_min": 1.0, + "min_market_cap_proxy": 6000000000.0, + "document_quality_score_min": 0.54, + "parse_confidence_overall_min": 0.5, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "next_open_gap_cap_pct": 0.04, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 1, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 5.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 12, + "enabled": true, + "veto_parse_confidence_min_override": 0.5, + "veto_oneoff_penalty_override": 0.0, + "per_trade_risk_pct_override": 0.015, + "stop_atr_multiplier_override": 3.0, + "use_reaction_day_low_stop_override": false, + "macro_vix_max": 30.0 + }, + { + "engine_id": "next_open_long_unknown_ome_orderly", + "event_types": [ + "other_material_event" + ], + "event_directions": [ + "unknown" + ], + "guidance_statuses": [ + "not_provided" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.04, + "reaction_day_return_min": 0.0, + "reaction_day_return_max": 0.12, + "close_location_min": 0.55, + "volume_ratio_min": 1.0, + "min_market_cap_proxy": 4000000000.0, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "next_open_gap_cap_pct": 0.04, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 1, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 5.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 12, + "enabled": true, + "veto_parse_confidence_min_override": 0.4, + "per_trade_risk_pct_override": 0.008, + "stop_atr_multiplier_override": 4.0, + "use_reaction_day_low_stop_override": false, + "macro_vix_max": 30.0 + }, + { + "engine_id": "next_open_long_bullish_raised_recovery_broad_oneoff", + "event_types": [ + "earnings_release" + ], + "event_directions": [ + "bullish" + ], + "guidance_statuses": [ + "raised" + ], + "filing_time_buckets": [ + "post_market" + ], + "timing_class": "after_close", + "direction": "long_only", + "entry_timing_policy": "next_open", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.03, + "reaction_day_return_min": 0.0, + "reaction_day_return_max": 0.1, + "close_location_min": 0.3, + "close_location_max": 0.75, + "gap_size_min": 0.01, + "gap_size_max": 0.12, + "volume_ratio_min": 1.3, + "volume_ratio_max": 5.0, + "min_market_cap_proxy": 8000000000.0, + "document_quality_score_min": 0.5, + "parse_confidence_overall_min": 0.5, + "score_threshold_override": 0.0, + "residual_reserve_selected": true, + "veto_parse_confidence_min_override": 0.5, + "veto_oneoff_penalty_override": 0.75, + "allow_oneoff_downsizing_override": true, + "oneoff_downsize_floor_override": 0.15, + "next_open_gap_cap_pct": 0.12, + "early_failure_close_below_entry_and_reaction_close_override": false, + "early_failure_no_progress_days_override": 12, + "early_failure_no_progress_r_override": 0.0, + "early_failure_no_progress_fraction_override": 1.0, + "target_1_r_override": 5.0, + "target_1_fraction_override": 0.1, + "trailing_warmup_days_override": 12, + "per_trade_risk_pct_override": 0.025, + "stop_atr_multiplier_override": 3.0, + "use_reaction_day_low_stop_override": false, + "enabled": true, + "macro_vix_max": 30.0 + } + ], + "splits": [ + { + "kind": "named_snapshot", + "params": { + "name": "train" + } + }, + { + "kind": "named_snapshot", + "params": { + "name": "valid" + } + }, + { + "kind": "named_snapshot", + "params": { + "name": "test" + } + } + ], + "tags": [ + "return-max", + "v6new", + "de-risk" + ], + "notes": "Add macro_vix_max to reaction_close_long_core on top of v6new.350 to keep current split/WFV while sharply improving matched stress OOT.", + "aliases": [ + "conviction", + "v7.119 — 12-engine composite champion (SQS 92.4, 493% standalone)" + ], + "version_family": "v7", + "created_at": "2026-04-07T03:50:56.416441+00:00", + "created_by": "ai_agent", + "status": "promoted", + "generation": 19, + "changelog": "6 per-engine exit/veto optimizations: guidance target 6→4 frac 0→0.1, patient close_below=true, residual frac→0, core EF r→0.20, material_mixed veto_oneoff→0. Combined +83pp conservative / +116pp aggressive CW", + "parent": "return_max_long_v7.118", + "id": 1138, + "performance_summary": null +} diff --git a/libs/backtest/domain.py b/libs/backtest/domain.py index 928b4ba..48ead79 100644 --- a/libs/backtest/domain.py +++ b/libs/backtest/domain.py @@ -211,6 +211,11 @@ class DailyPortfolioState(BaseModel): daily_new_risk_used: float peak_equity: float current_drawdown_pct: float + # Idle capital decomposition (optional; None when not instrumented) + raw_cash: float | None = None # self._cash (actual uninvested cash) + parking_value: float | None = None # market value of parked ETF positions + idle_alpha_exposure: float | None = None # notional in idle-alpha-sleeve positions + primary_exposure: float | None = None # notional in primary engine positions class MetricsBundle(BaseModel): @@ -257,9 +262,18 @@ class MetricsBundle(BaseModel): long_pnl_contribution_pct: float | None = None short_pnl_contribution_pct: float | None = None + # Simple (non-compounding) return + simple_return_pct: float | None = None # sum(net_pnl) / initial_equity * 100 + initial_equity: float | None = None # starting capital used for simple return calc + # Bootstrap confidence intervals (95%) bootstrap_cis: dict[str, tuple[float, float] | None] = Field(default_factory=dict) + # Idle capital decomposition (populated only when raw_cash/parking_value tracked) + avg_idle_fraction_pct: float | None = None # mean((raw_cash + parking_value) / equity) + avg_primary_utilization_pct: float | None = None # mean(primary_exposure / equity) + avg_ia_utilization_pct: float | None = None # mean(idle_alpha_exposure / equity) + # --------------------------------------------------------------------------- # Config models (mirror JSON Schema) @@ -556,6 +570,74 @@ PARKING_PRESETS: dict[str, dict] = { "cash_parking_topup_risk_score_max": 25.0, "cash_parking_reserve_pct": 0.0, }, + "qqqm_low_dd_risk25_gld": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.275, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_topup_risk_score_max": 25.0, + "cash_parking_reserve_pct": 0.0, + "cash_parking_defensive_symbol": "gld", + "cash_parking_defensive_relay_enabled": True, + "cash_parking_defensive_relay_trigger_mode": "always", + "cash_parking_defensive_relay_risk_score_max": 100.0, + "cash_parking_defensive_momentum_min": 0.05, + }, + "qqqm_low_dd_gld": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.275, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_defensive_symbol": "gld", + "cash_parking_defensive_relay_enabled": True, + "cash_parking_defensive_relay_trigger_mode": "always", + "cash_parking_defensive_relay_risk_score_max": 100.0, + "cash_parking_defensive_momentum_min": 0.05, + }, + "qqqm_low_dd_gld_dbc": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.275, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_defensive_symbol": "gld", + "cash_parking_defensive_alt_symbol": "dbc", + "cash_parking_defensive_relay_enabled": True, + "cash_parking_defensive_relay_trigger_mode": "always", + "cash_parking_defensive_relay_risk_score_max": 100.0, + "cash_parking_defensive_momentum_min": 0.05, + }, "qqqm_low_dd_tqqq_calm": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", @@ -577,6 +659,79 @@ PARKING_PRESETS: dict[str, dict] = { "cash_parking_low_vol_overlay_temperature_max": 0.92, "cash_parking_low_vol_overlay_entropy_max": 1.15, }, + "qqqm_low_dd_tqqq_calm_gld": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.275, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_low_vol_overlay_symbol": "tqqq", + "cash_parking_low_vol_overlay_vol_threshold": 0.17, + "cash_parking_low_vol_overlay_temperature_max": 0.92, + "cash_parking_low_vol_overlay_entropy_max": 1.15, + "cash_parking_defensive_symbol": "gld", + "cash_parking_defensive_relay_enabled": True, + "cash_parking_defensive_relay_trigger_mode": "always", + "cash_parking_defensive_relay_risk_score_max": 100.0, + "cash_parking_defensive_momentum_min": 0.05, + }, + "qqqm_low_dd_tqqq_calm_v2": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.35, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_low_vol_overlay_symbol": "tqqq", + "cash_parking_low_vol_overlay_vol_threshold": 0.22, + "cash_parking_low_vol_overlay_temperature_max": 1.0, + "cash_parking_low_vol_overlay_entropy_max": 1.45, + }, + "qqqm_low_dd_tqqq_calm_v2_gld": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.35, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_low_vol_overlay_symbol": "tqqq", + "cash_parking_low_vol_overlay_vol_threshold": 0.22, + "cash_parking_low_vol_overlay_temperature_max": 1.0, + "cash_parking_low_vol_overlay_entropy_max": 1.45, + "cash_parking_defensive_symbol": "gld", + "cash_parking_defensive_relay_enabled": True, + "cash_parking_defensive_relay_trigger_mode": "always", + "cash_parking_defensive_relay_risk_score_max": 100.0, + "cash_parking_defensive_momentum_min": 0.05, + }, "qqqm_low_dd_tqqq_active": { "cash_parking_enabled": True, "cash_parking_symbol": "qqqm", @@ -598,6 +753,79 @@ PARKING_PRESETS: dict[str, dict] = { "cash_parking_low_vol_overlay_temperature_max": 1.05, "cash_parking_low_vol_overlay_entropy_max": 1.30, }, + "qqqm_low_dd_tqqq_active_gld": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.275, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_low_vol_overlay_symbol": "tqqq", + "cash_parking_low_vol_overlay_vol_threshold": 0.22, + "cash_parking_low_vol_overlay_temperature_max": 1.05, + "cash_parking_low_vol_overlay_entropy_max": 1.30, + "cash_parking_defensive_symbol": "gld", + "cash_parking_defensive_relay_enabled": True, + "cash_parking_defensive_relay_trigger_mode": "always", + "cash_parking_defensive_relay_risk_score_max": 100.0, + "cash_parking_defensive_momentum_min": 0.05, + }, + "qqqm_low_dd_tqqq_active_v2": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.35, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_low_vol_overlay_symbol": "tqqq", + "cash_parking_low_vol_overlay_vol_threshold": 0.22, + "cash_parking_low_vol_overlay_temperature_max": 1.05, + "cash_parking_low_vol_overlay_entropy_max": 1.45, + }, + "qqqm_low_dd_tqqq_active_v2_gld": { + "cash_parking_enabled": True, + "cash_parking_symbol": "qqqm", + "cash_parking_gate_mode": "volatility", + "cash_parking_gate_vol_lookback": 20, + "cash_parking_gate_vol_threshold": 0.35, + "cash_parking_temperature_threshold": 1.2, + "cash_parking_entropy_lookback": 20, + "cash_parking_entropy_threshold": 1.45, + "cash_parking_require_trend": True, + "cash_parking_trend_mode": "momentum", + "cash_parking_trend_sma_period": 20, + "cash_parking_trend_reentry_pct": 0.001, + "cash_parking_autocorr_threshold": 0.0, + "cash_parking_topup_max_peak_drawdown_pct": 0.02, + "cash_parking_reserve_pct": 0.0, + "cash_parking_low_vol_overlay_symbol": "tqqq", + "cash_parking_low_vol_overlay_vol_threshold": 0.22, + "cash_parking_low_vol_overlay_temperature_max": 1.05, + "cash_parking_low_vol_overlay_entropy_max": 1.45, + "cash_parking_defensive_symbol": "gld", + "cash_parking_defensive_relay_enabled": True, + "cash_parking_defensive_relay_trigger_mode": "always", + "cash_parking_defensive_relay_risk_score_max": 100.0, + "cash_parking_defensive_momentum_min": 0.05, + }, # ── Bearish Parking (SH inverse ETF) ──────────────────────────────────── # Two-stage: normal→QQQ, mild stress→SGOV, deep stress→SH # Uses composite risk score: < exit_score=QQQ, exit_score→SGOV, > bearish_threshold→SH @@ -703,7 +931,7 @@ _MICRO_EVENT_ALPHA_ENGINES: list[dict[str, Any]] = [ "early_failure_no_progress_days_override": 12, "early_failure_no_progress_r_override": 0.0, "early_failure_no_progress_fraction_override": 1.0, - "target_1_r_override": 5.0, + "target_1_r_override": 3.0, "target_1_fraction_override": 0.1, "trailing_warmup_days_override": 12, "enabled": True, @@ -836,7 +1064,7 @@ _MICRO_EVENT_ALPHA_ENGINES: list[dict[str, Any]] = [ "trailing_warmup_days_override": 10, "enabled": True, "per_trade_risk_pct_override": 0.12375, - "stop_atr_multiplier_override": 3.0, + "stop_atr_multiplier_override": 3.9, "use_reaction_day_low_stop_override": False, "pre_event_entropy_60d_min": 1.97, }, @@ -943,6 +1171,42 @@ DIVIDEND_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { FORM4_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { "reserve_form4_cluster": { + "enabled": True, + "pit_events_path": "data/reference/form4_daily_events_pit.parquet", + "reserve_pct": 0.04, + "min_owner_count": 2, + "min_total_value": 5_000_000.0, + "min_purchase_pct": 0.0, + "max_lag_days": None, + "hold_days": 20, + "max_positions": 6, + "max_new_per_day": 2, + }, + "reserve_form4_cluster_plus": { + "enabled": True, + "pit_events_path": "data/reference/form4_daily_events_pit.parquet", + "reserve_pct": 0.06, + "min_owner_count": 2, + "min_total_value": 5_000_000.0, + "min_purchase_pct": 0.0, + "max_lag_days": None, + "hold_days": 20, + "max_positions": 6, + "max_new_per_day": 2, + }, + "reserve_form4_cluster_plus_high": { + "enabled": True, + "pit_events_path": "data/reference/form4_daily_events_pit.parquet", + "reserve_pct": 0.065, + "min_owner_count": 2, + "min_total_value": 5_000_000.0, + "min_purchase_pct": 0.0, + "max_lag_days": None, + "hold_days": 20, + "max_positions": 6, + "max_new_per_day": 2, + }, + "reserve_form4_cluster_plus_relaxed": { "enabled": True, "pit_events_path": "data/reference/form4_daily_events_pit.parquet", "reserve_pct": 0.06, @@ -950,6 +1214,71 @@ FORM4_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { "min_total_value": 5_000_000.0, "min_purchase_pct": 0.0, "max_lag_days": None, + "disable_day1_early_failure": True, + "hold_days": 20, + "max_positions": 6, + "max_new_per_day": 2, + }, + "reserve_form4_cluster_plus_patient2": { + "enabled": True, + "pit_events_path": "data/reference/form4_daily_events_pit.parquet", + "reserve_pct": 0.06, + "min_owner_count": 2, + "min_total_value": 5_000_000.0, + "min_purchase_pct": 0.0, + "max_lag_days": None, + "disable_day1_early_failure": True, + "no_progress_days_override": 2, + "no_progress_r_override": 0.15, + "no_progress_fraction_override": 1.0, + "hold_days": 20, + "max_positions": 6, + "max_new_per_day": 2, + }, + "reserve_form4_cluster_plus_patient3": { + "enabled": True, + "pit_events_path": "data/reference/form4_daily_events_pit.parquet", + "reserve_pct": 0.06, + "min_owner_count": 2, + "min_total_value": 5_000_000.0, + "min_purchase_pct": 0.0, + "max_lag_days": None, + "disable_day1_early_failure": True, + "no_progress_days_override": 3, + "no_progress_r_override": 0.15, + "no_progress_fraction_override": 1.0, + "hold_days": 20, + "max_positions": 6, + "max_new_per_day": 2, + }, + "reserve_form4_cluster_patient": { + "enabled": True, + "pit_events_path": "data/reference/form4_daily_events_pit.parquet", + "reserve_pct": 0.125, + "min_owner_count": 2, + "min_total_value": 5_000_000.0, + "min_purchase_pct": 0.0, + "max_lag_days": None, + "disable_day1_early_failure": True, + "no_progress_days_override": 3, + "no_progress_r_override": 0.15, + "no_progress_fraction_override": 1.0, + "hold_days": 20, + "max_positions": 6, + "max_new_per_day": 2, + }, + "reserve_form4_cluster_patient_high": { + "enabled": True, + "pit_events_path": "data/reference/form4_daily_events_pit.parquet", + "reserve_pct": 0.15, + "min_owner_count": 2, + "min_total_value": 5_000_000.0, + "min_purchase_pct": 0.0, + "max_lag_days": None, + "disable_day1_early_failure": True, + "no_progress_days_override": 3, + "no_progress_r_override": 0.15, + "no_progress_fraction_override": 1.0, "hold_days": 20, "max_positions": 6, "max_new_per_day": 2, @@ -957,6 +1286,505 @@ FORM4_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { } +OWNERSHIP_CAPTURE_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { + "ownership_13d_raise_reserve_plus_strict": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.15, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.50, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.50, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.50, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r70": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.70, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r80": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.80, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r90": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.90, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r95": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.95, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_r99": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.99, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_saturation4": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.50, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "symbol_max_entries_in_lookback": 4, + "symbol_entry_lookback_days": 540, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_saturation4_plus": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.515, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "symbol_max_entries_in_lookback": 4, + "symbol_entry_lookback_days": 540, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_balanced_purpose_plus_cooldown90_saturation4_fullidle": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.49, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "exclude_housekeeping_purpose": True, + "exclude_structural_exchange_purpose": True, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "symbol_cooldown_days_after_loss": 90, + "symbol_max_entries_in_lookback": 4, + "symbol_entry_lookback_days": 540, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + "extra_idle_deploy_pct_above_reserve": 1.0, + }, + "ownership_13d_raise_reserve_ultra_balanced": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.525, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_plus": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.55, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_60": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.60, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_65": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.65, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_70": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.70, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_75": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.75, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_80": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.80, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_85": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.85, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, + "ownership_13d_raise_reserve_ultra_90": { + "enabled": True, + "pit_events_path": "data/reference/ownership_13d13g_events_pit.parquet", + "reserve_pct": 0.90, + "form_groups": ["13D"], + "min_percent_owned": 5.0, + "min_percent_delta_points": 2.0, + "require_amendment": True, + "require_initial": False, + "require_activist": False, + "require_13g_to_13d_transition": False, + "hold_days": 30, + "min_avg_dollar_volume": 10_000_000.0, + "max_positions": 6, + "max_new_per_day": 2, + "min_cash_ratio_for_overlay": 0.25, + "max_idle_deploy_pct": 0.0, + }, +} + +RISK_OFF_ALPHA_SLEEVE_PRESETS: dict[str, dict[str, Any]] = { + "risk_off_alpha_gld_dbc": { + "enabled": True, + "reserve_pct": 0.20, + "symbols": ["gld", "dbc"], + "momentum_lookback_days": 20, + "min_symbol_momentum": 0.05, + "min_consecutive_sgov_days": 3, + "rotation_momentum_gap": 0.02, + "max_holding_days": 0, + "min_cash_ratio_for_overlay": 0.0, + "max_idle_deploy_pct": 0.0, + }, + "risk_off_alpha_gld_crisis60_balanced": { + "enabled": True, + "reserve_pct": 0.35, + "symbols": ["gld"], + "momentum_lookback_days": 20, + "min_symbol_momentum": 0.05, + "min_consecutive_sgov_days": 3, + "min_parking_risk_score": 60.0, + "rotation_momentum_gap": 0.02, + "max_holding_days": 0, + "min_cash_ratio_for_overlay": 0.0, + "max_idle_deploy_pct": 0.0, + }, + "risk_off_alpha_gld_on_gld_parking": { + "enabled": True, + "reserve_pct": 0.35, + "symbols": ["gld"], + "momentum_lookback_days": 20, + "min_symbol_momentum": 0.05, + "min_consecutive_sgov_days": 3, + "min_parking_risk_score": 60.0, + "rotation_momentum_gap": 0.02, + "max_holding_days": 0, + "min_cash_ratio_for_overlay": 0.0, + "max_idle_deploy_pct": 0.0, + }, + "risk_off_alpha_gld_crisis65_balanced_plus": { + "enabled": True, + "reserve_pct": 0.40, + "symbols": ["gld"], + "momentum_lookback_days": 20, + "min_symbol_momentum": 0.05, + "min_consecutive_sgov_days": 3, + "min_parking_risk_score": 65.0, + "rotation_momentum_gap": 0.02, + "max_holding_days": 0, + "min_cash_ratio_for_overlay": 0.0, + "max_idle_deploy_pct": 0.0, + }, + "risk_off_alpha_gld_crisis65_balanced_refined": { + "enabled": True, + "reserve_pct": 0.37, + "symbols": ["gld"], + "momentum_lookback_days": 20, + "min_symbol_momentum": 0.05, + "min_consecutive_sgov_days": 3, + "min_parking_risk_score": 65.0, + "rotation_momentum_gap": 0.02, + "max_holding_days": 0, + "min_cash_ratio_for_overlay": 0.0, + "max_idle_deploy_pct": 0.0, + }, + "risk_off_alpha_gld_crisis60": { + "enabled": True, + "reserve_pct": 0.60, + "symbols": ["gld"], + "momentum_lookback_days": 20, + "min_symbol_momentum": 0.05, + "min_consecutive_sgov_days": 3, + "min_parking_risk_score": 60.0, + "rotation_momentum_gap": 0.02, + "max_holding_days": 0, + "min_cash_ratio_for_overlay": 0.0, + "max_idle_deploy_pct": 0.0, + }, +} + + class RiskConfig(BaseModel): per_trade_risk_pct: float = 0.01 # 1% of equity per trade per_trade_risk_pct_a_tier: float | None = None @@ -1040,6 +1868,7 @@ class RiskConfig(BaseModel): cash_parking_account_type: str = "cash" # "cash" (no PDT, GFV only) or "margin" (PDT day trade rules) cash_parking_symbol: str = "spy" # "spy", "spym", "qual", "qqq", "qqqm", "dynamic", "sgov" cash_parking_defensive_symbol: str = "spy" # fallback defensive ETF for pair/regime parking ("spy", "spym", "qual") + cash_parking_defensive_alt_symbol: str | None = None # optional alternate defensive ETF; pick the stronger valid candidate when set cash_parking_defensive_relay_enabled: bool = False # when primary gate says SGOV, allow defensive ETF instead if it is still healthy cash_parking_defensive_relay_trigger_mode: str = "always" # "always", "turn_of_month", "recovery", "turn_or_recovery" cash_parking_defensive_relay_turn_strength_min: float = 0.0 # minimum month-turn strength to allow relay @@ -1047,6 +1876,9 @@ class RiskConfig(BaseModel): cash_parking_defensive_relay_recovery_momentum_min: float = 0.0 # minimum rebound return to allow relay cash_parking_defensive_relay_drawdown_accel_max: float = 999.0 # require drawdown damage to be stabilizing/improving cash_parking_defensive_relay_risk_score_max: float = 100.0 # hard cap for relay even if exit threshold is wider + cash_parking_defensive_momentum_min: float = -0.01 # require defensive ETF momentum above this to replace SGOV + cash_parking_defensive_vol_max: float = 0.0 # optional tighter vol cap for defensive ETF (0 = use parking vol threshold) + cash_parking_defensive_alloc_pct: float = 1.0 # if <1, allocate this fraction to defensive_symbol and keep the rest in SGOV cash_parking_reserve_pct: float = 0.02 # keep this % of equity as cash reserve cash_parking_trend_gate: bool = False # only park when price > SMA (skip downtrends) cash_parking_gate_sma_period: int = 20 # SMA period for trend gate (10, 20, 50, etc.) @@ -1128,6 +1960,12 @@ class RiskConfig(BaseModel): # None = disabled (default, existing behavior unchanged). cash_parking_bearish_symbol: str | None = None # "sh", "sds", etc. — inverse ETF for deep stress cash_parking_bearish_threshold: int = 60 # composite risk score >= this → use bearish_symbol + cash_parking_bearish_alloc_pct: float = 1.0 # if <1, allocate this fraction to bearish_symbol and keep the rest in SGOV + cash_parking_crisis_symbol: str | None = None # optional crisis safe-haven ETF for deep stress (e.g. "ief", "iei") + cash_parking_crisis_threshold: int = 75 # composite risk score >= this enables crisis_symbol evaluation + cash_parking_crisis_momentum_days: int = 20 # lookback for crisis symbol momentum confirmation + cash_parking_crisis_momentum_min: float = 0.0 # require crisis symbol momentum above this to replace SGOV + cash_parking_crisis_vol_max: float = 0.0 # optional max realized vol allowed for crisis symbol (0 = disabled) # Multi-tier regime parking: VIX-driven 3-symbol rotation (used with gate_mode="regime_tiered") cash_parking_regime_risk_on_symbol: str = "jepq" # symbol when VIX < vix_low_threshold cash_parking_regime_neutral_symbol: str = "qqqm" # symbol when VIX between thresholds @@ -1177,6 +2015,7 @@ class ExecutionConfig(BaseModel): trailing_model: str | None = None trailing_warmup_days: int = 0 # days after entry before trailing activates max_holding_days: int = 10 + lookback_entry_enabled: bool = False # enter positions for pre-start events still within holding window no_follow_through_exit: bool = False # exit at D+1 close if close < entry price early_failure_close_below_entry_and_reaction_close: bool = False early_failure_no_progress_days: int | None = None @@ -1500,11 +2339,59 @@ class Form4CaptureConfig(BaseModel): min_event_day_count: int = 1 max_lag_days: int | None = None require_officer_or_director: bool = False + disable_day1_early_failure: bool = False + no_progress_days_override: int | None = None + no_progress_r_override: float | None = None + no_progress_fraction_override: float | None = None hold_days: int = 20 max_positions: int = 6 max_new_per_day: int = 2 +class OwnershipCaptureConfig(BaseModel): + enabled: bool = False + pit_events_path: str | None = None + reserve_pct: float = 0.0 + form_groups: list[str] = Field(default_factory=lambda: ["13D"]) + min_percent_owned: float = 5.0 + min_percent_delta_points: float = 0.0 + exclude_housekeeping_purpose: bool = False + exclude_structural_exchange_purpose: bool = False + min_strength_score: int | None = None + require_amendment: bool = False + require_initial: bool = False + require_activist: bool = False + require_13g_to_13d_transition: bool = False + symbol_cooldown_days_after_loss: int = 0 + symbol_max_entries_in_lookback: int | None = None + symbol_entry_lookback_days: int = 365 + extra_idle_deploy_pct_above_reserve: float = 0.0 + disable_day1_early_failure: bool = False + no_progress_days_override: int | None = None + no_progress_r_override: float | None = None + no_progress_fraction_override: float | None = None + hold_days: int = 30 + min_avg_dollar_volume: float = 10_000_000.0 + max_positions: int = 6 + max_new_per_day: int = 2 + min_cash_ratio_for_overlay: float = 0.25 + max_idle_deploy_pct: float = 1.0 + + +class RiskOffAlphaConfig(BaseModel): + enabled: bool = False + reserve_pct: float = 0.0 + symbols: list[str] = Field(default_factory=lambda: ["gld", "dbc"]) + momentum_lookback_days: int = 20 + min_symbol_momentum: float = 0.05 + min_consecutive_sgov_days: int = 3 + min_parking_risk_score: float = 0.0 + rotation_momentum_gap: float = 0.02 + max_holding_days: int = 0 + min_cash_ratio_for_overlay: float = 0.0 + max_idle_deploy_pct: float = 0.0 + + class IdleAlphaConfig(BaseModel): dynamic_allocator_enabled: bool = False dynamic_allocator_cash_ratio_low: float = 0.04 @@ -1529,6 +2416,8 @@ class BacktestConfig(BaseModel): earnings_calendar_pit_path: str | None = None dividend_capture_sleeve_preset: str | None = None form4_capture_sleeve_preset: str | None = None + ownership_capture_sleeve_preset: str | None = None + risk_off_alpha_sleeve_preset: str | None = None universe: UniverseConfig = Field(default_factory=UniverseConfig) signal: SignalConfig = Field(default_factory=SignalConfig) risk: RiskConfig = Field(default_factory=RiskConfig) @@ -1536,9 +2425,12 @@ class BacktestConfig(BaseModel): reporting: ReportingConfig = Field(default_factory=ReportingConfig) dividend_capture: DividendCaptureConfig = Field(default_factory=DividendCaptureConfig) form4_capture: Form4CaptureConfig = Field(default_factory=Form4CaptureConfig) + ownership_capture: OwnershipCaptureConfig = Field(default_factory=OwnershipCaptureConfig) + risk_off_alpha: RiskOffAlphaConfig = Field(default_factory=RiskOffAlphaConfig) idle_alpha: IdleAlphaConfig = Field(default_factory=IdleAlphaConfig) event_type_profiles: dict[str, EventTypeProfile] = Field(default_factory=dict) idle_alpha_sleeve_preset: str | None = None + idle_alpha_dedup_mode: str = "skip" # "skip" (default, backward-compat) or "rename" (suffix __ia_sleeve on conflict) strategy_engines: list[StrategyEngineConfig] = Field(default_factory=list) strategy_engine_selection_mode: str = "interleave" # "interleave", "interleave_head_score", "global_score", "interleave_cap_efficiency_soft", "interleave_cap_efficiency_strict", or "interleave_cash_tiebreak" @@ -1546,6 +2438,8 @@ class BacktestConfig(BaseModel): self.risk.apply_parking_preset() self.apply_dividend_capture_sleeve_preset() self.apply_form4_capture_sleeve_preset() + self.apply_ownership_capture_sleeve_preset() + self.apply_risk_off_alpha_sleeve_preset() self.apply_idle_alpha_sleeve_preset() def apply_dividend_capture_sleeve_preset(self) -> None: @@ -1574,6 +2468,32 @@ class BacktestConfig(BaseModel): current.update(preset) self.form4_capture = Form4CaptureConfig.model_validate(current) + def apply_ownership_capture_sleeve_preset(self) -> None: + if not self.ownership_capture_sleeve_preset: + return + preset = OWNERSHIP_CAPTURE_SLEEVE_PRESETS.get(self.ownership_capture_sleeve_preset) + if preset is None: + raise ValueError( + "Unknown ownership capture sleeve preset: " + f"{self.ownership_capture_sleeve_preset}. Available: {list(OWNERSHIP_CAPTURE_SLEEVE_PRESETS.keys())}" + ) + current = self.ownership_capture.model_dump() + current.update(preset) + self.ownership_capture = OwnershipCaptureConfig.model_validate(current) + + def apply_risk_off_alpha_sleeve_preset(self) -> None: + if not self.risk_off_alpha_sleeve_preset: + return + preset = RISK_OFF_ALPHA_SLEEVE_PRESETS.get(self.risk_off_alpha_sleeve_preset) + if preset is None: + raise ValueError( + "Unknown risk-off alpha sleeve preset: " + f"{self.risk_off_alpha_sleeve_preset}. Available: {list(RISK_OFF_ALPHA_SLEEVE_PRESETS.keys())}" + ) + current = self.risk_off_alpha.model_dump() + current.update(preset) + self.risk_off_alpha = RiskOffAlphaConfig.model_validate(current) + def apply_idle_alpha_sleeve_preset(self) -> None: """Append named idle-alpha sleeve engines without touching risk config.""" if not self.idle_alpha_sleeve_preset: @@ -1589,7 +2509,10 @@ class BacktestConfig(BaseModel): for engine_payload in preset["strategy_engines"]: engine = StrategyEngineConfig.model_validate(engine_payload) if engine.engine_id in existing_engine_ids: - continue + if self.idle_alpha_dedup_mode == "rename": + engine = engine.model_copy(update={"engine_id": f"{engine.engine_id}__ia_sleeve"}) + else: + continue appended_engines.append(engine) existing_engine_ids.add(engine.engine_id) if appended_engines: @@ -1980,6 +2903,10 @@ class JournalEntry(BaseModel): reset_common_window_breakdown: dict[str, float] = Field(default_factory=dict) multi_capital_common_window_score: float | None = None multi_capital_common_window_breakdown: dict[str, float] = Field(default_factory=dict) + scenario_robustness_score: float | None = None + scenario_robustness_breakdown: dict[str, float] = Field(default_factory=dict) + overfit_check_score: float | None = None + overfit_check_breakdown: dict[str, float] = Field(default_factory=dict) verdict: str = "unknown" # better / worse / neutral / unknown verdict_reasoning: str = "" next_direction: str = "" @@ -2012,6 +2939,8 @@ class RegistryEntry(BaseModel): walk_forward_summary: WalkForwardSummary | None = None robustness_matrix_summary: RobustnessMatrixSummary | None = None out_of_time_robustness_summary: RobustnessMatrixSummary | None = None + scenario_robustness_score: float | None = None + overfit_check_score: float | None = None # train split metrics train_total_return_pct: float | None = None train_annualized_return_pct: float | None = None