From b507fbf499b521598d6484116ba18955dda7da25 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Tue, 17 Mar 2026 16:58:04 -0700 Subject: [PATCH] Add attention client and continue PEAD research --- apps/backtester/run.py | 31 +- apps/tools/free_attention_probe.py | 406 ++++++++++++++++++ apps/tracker/cli.py | 62 +-- ...acro_block_crashcap_gap10_longtrend12.json | 81 ++++ ...k_crashcap_gap10_longtrend12_sdlong25.json | 81 ++++ ...crashcap_gap10_interleave_longtrend25.json | 81 ++++ ...shcap_gap10_longtrend12_sdlong25_max4.json | 81 ++++ ...ck_crashcap_gap7_longtrend12_sdlong25.json | 81 ++++ ...step59_same_day_only_max4_longtrend25.json | 81 ++++ ..._day_only_interleave_max4_longtrend25.json | 81 ++++ ...step61_same_day_only_max5_longtrend25.json | 81 ++++ ...10_longtrend12_sdlong25_max4_acsgap10.json | 82 ++++ ...10_longtrend12_sdlong25_max4_acsgap12.json | 82 ++++ ...ashcap_gap10_interleave_max4_acsgap10.json | 82 ++++ ...ashcap_gap10_interleave_max4_acsgap12.json | 82 ++++ ...rend12_sdlong25_max4_acsgap10_react12.json | 83 ++++ ...rend12_sdlong25_max4_acsgap10_react14.json | 83 ++++ journal/LEADERBOARD.md | 160 +++---- journal/attention_probe_20260317.md | 54 +++ journal/experiment_registry.json | 386 ++++++++++++++++- journal/improvement_journal.jsonl | 20 +- libs/backtest/allocator.py | 18 +- libs/backtest/domain.py | 8 + libs/backtest/selector.py | 20 + libs/backtest/tracker.py | 17 +- libs/oracle_client/__init__.py | 2 + libs/oracle_client/attention.py | 64 +++ libs/oracle_client/client.py | 9 +- libs/oracle_client/models.py | 54 +++ tests/fixtures/attention_collect.json | 9 + tests/fixtures/attention_entity.json | 14 + tests/fixtures/attention_event.json | 30 ++ .../integration/backtest/test_backtest_run.py | 66 +++ tests/unit/backtest/test_selector.py | 32 ++ tests/unit/backtest/test_tracker.py | 39 ++ tests/unit/test_oracle_client.py | 109 +++++ 36 files changed, 2626 insertions(+), 126 deletions(-) create mode 100644 apps/tools/free_attention_probe.py create mode 100644 configs/experiments/pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12.json create mode 100644 configs/experiments/pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25.json create mode 100644 configs/experiments/pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25.json create mode 100644 configs/experiments/pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4.json create mode 100644 configs/experiments/pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25.json create mode 100644 configs/experiments/pead_midcap_step59_same_day_only_max4_longtrend25.json create mode 100644 configs/experiments/pead_midcap_step60_same_day_only_interleave_max4_longtrend25.json create mode 100644 configs/experiments/pead_midcap_step61_same_day_only_max5_longtrend25.json create mode 100644 configs/experiments/pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10.json create mode 100644 configs/experiments/pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12.json create mode 100644 configs/experiments/pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10.json create mode 100644 configs/experiments/pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12.json create mode 100644 configs/experiments/pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12.json create mode 100644 configs/experiments/pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14.json create mode 100644 journal/attention_probe_20260317.md create mode 100644 libs/oracle_client/attention.py create mode 100644 tests/fixtures/attention_collect.json create mode 100644 tests/fixtures/attention_entity.json create mode 100644 tests/fixtures/attention_event.json diff --git a/apps/backtester/run.py b/apps/backtester/run.py index d0f8555..43d99c9 100644 --- a/apps/backtester/run.py +++ b/apps/backtester/run.py @@ -16,6 +16,7 @@ from libs.backtest.domain import ( BacktestConfig, Candidate, DailyPortfolioState, + ExecutionConfig, ExperimentManifest, ExperimentResult, FilledTrade, @@ -336,6 +337,7 @@ class BacktestRunner: portfolio_state=portfolio_state, open_positions=self._open_positions, config=self.config, + execution_config=self._build_effective_execution_config(candidate), cooldown_remaining=self._cooldown_remaining, macro_data=macro_data, engine_daily_new_risk_used=self._engine_daily_new_risk_used[candidate.engine_id], @@ -477,19 +479,30 @@ class BacktestRunner: 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.""" + def _build_effective_execution_config(self, candidate: Candidate) -> ExecutionConfig: + """Resolve per-engine and per-event execution overrides.""" + execution_updates: dict[str, Any] = {} + 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: + if max_holding_days is not None: + execution_updates["max_holding_days"] = max_holding_days + + if candidate.engine_target_atr_multiplier is not None: + execution_updates["target_atr_multiplier"] = candidate.engine_target_atr_multiplier + if candidate.engine_target_1_fraction is not None: + execution_updates["target_1_fraction"] = candidate.engine_target_1_fraction + if candidate.engine_trailing_model is not None: + execution_updates["trailing_model"] = candidate.engine_trailing_model + if candidate.engine_trailing_warmup_days is not None: + execution_updates["trailing_warmup_days"] = candidate.engine_trailing_warmup_days + + if not execution_updates: return self.config.execution - return self.config.execution.model_copy( - update={"max_holding_days": max_holding_days} - ) + return self.config.execution.model_copy(update=execution_updates) def _build_per_engine_metrics(self) -> dict[str, dict[str, Any]]: """Run each engine in isolation for standalone metrics and shadow summaries.""" @@ -531,6 +544,10 @@ class BacktestRunner: "entry_timing_policy": engine.entry_timing_policy, "max_holding_days": engine.max_holding_days, "engine_risk_budget_pct": engine.engine_risk_budget_pct, + "target_atr_multiplier_override": engine.target_atr_multiplier_override, + "target_1_fraction_override": engine.target_1_fraction_override, + "trailing_model_override": engine.trailing_model_override, + "trailing_warmup_days_override": engine.trailing_warmup_days_override, "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), diff --git a/apps/tools/free_attention_probe.py b/apps/tools/free_attention_probe.py new file mode 100644 index 0000000..8ec4f37 --- /dev/null +++ b/apps/tools/free_attention_probe.py @@ -0,0 +1,406 @@ +"""Quick probe for free attention data on snapshot events. + +This is a research helper, not a production pipeline. + +Current sources: +- Wikimedia pageviews: scalable attention proxy +- GDELT Doc API: optional spot-check article count for a few names +""" + +from __future__ import annotations + +import argparse +import asyncio +import datetime as dt +import re +import time +from dataclasses import dataclass +from pathlib import Path + +import pandas as pd +import requests +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine + +from libs.common.config import get_settings + +USER_AGENT = "codex-fithia2-free-attention-probe/1.0" +WIKI_SEARCH_URL = "https://en.wikipedia.org/w/api.php" +WIKI_PAGEVIEWS_URL = ( + "https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/" + "en.wikipedia.org/all-access/all-agents/{article}/daily/{start}/{end}" +) +GDELT_DOC_URL = "https://api.gdeltproject.org/api/v2/doc/doc" + +STOPWORDS = { + "inc", + "incorporated", + "corp", + "corporation", + "ltd", + "holdings", + "group", + "co", + "company", + "plc", + "nv", +} + +MANUAL_WIKI_TITLE = { + "AMERICAN AIRLINES GROUP INC.": "American Airlines Group", + "ASTRONICS CORPORATION": "Astronics", + "CENTURY ALUMINUM COMPANY": "Century Aluminum", + "CLEANSPARK, INC.": "CleanSpark", + "CCC INTELLIGENT SOLUTIONS HOLDINGS INC.": "CCC Intelligent Solutions", + "FLUENCE ENERGY, INC.": "Fluence Energy", + "IMMUNITYBIO, INC.": "ImmunityBio", + "IMMUNITYBIO,\xa0INC.": "ImmunityBio", + "LYFT, INC.": "Lyft", + "MIRION TECHNOLOGIES, INC.": "Mirion Technologies", + "MOSAIC CO": "The Mosaic Company", + "NORWEGIAN CRUISE LINE HOLDINGS LTD.": "Norwegian Cruise Line Holdings", + "PAR PACIFIC HOLDINGS, INC.": "Par Pacific Holdings", + "PATTERSON-UTI ENERGY, INC.": "Patterson-UTI Energy", + "RITHM CAPITAL CORP.": "Rithm Capital", + "SOUNDHOUND AI, INC.": "SoundHound AI", + "TANGO THERAPEUTICS, INC.": "Tango Therapeutics", + "TERNS PHARMACEUTICALS, INC.": "Terns Pharmaceuticals", + "UNITY SOFTWARE INC.": "Unity Technologies", +} + + +@dataclass(frozen=True) +class ProbeRow: + ticker: str + issuer_name: str + event_date: dt.date + reaction_day_return: float + fwd_return_3d: float + fwd_return_5d: float + + +def _session() -> requests.Session: + s = requests.Session() + s.headers.update({"User-Agent": USER_AGENT}) + return s + + +def _clean_tokens(text_value: str) -> list[str]: + tokens = re.findall(r"[A-Za-z0-9]+", text_value.lower().replace("\xa0", " ")) + return [token for token in tokens if token not in STOPWORDS] + + +def _candidate_names(name: str) -> list[str]: + clean = name.replace("\xa0", " ").strip() + manual = MANUAL_WIKI_TITLE.get(clean.upper()) + candidates = [candidate for candidate in [manual, clean] if candidate] + token_name = " ".join(_clean_tokens(clean)) + if token_name: + candidates.append(token_name) + + seen: set[str] = set() + result: list[str] = [] + for candidate in candidates: + stripped = candidate.strip(" ,.") + if stripped and stripped not in seen: + result.append(stripped) + seen.add(stripped) + return result + + +def _title_match_score(name: str, title: str) -> float: + name_tokens = set(_clean_tokens(name)) + title_tokens = set(_clean_tokens(title)) + if not name_tokens or not title_tokens: + return 0.0 + overlap = len(name_tokens & title_tokens) + score = overlap / max(1, len(name_tokens)) + first_word = name.split()[0].lower() if name.split() else "" + if first_word and title.lower().startswith(first_word): + score += 0.1 + return score + + +def resolve_wikipedia_title(session: requests.Session, issuer_name: str) -> tuple[str | None, float]: + best_score = 0.0 + best_title: str | None = None + for candidate in _candidate_names(issuer_name): + response = session.get( + WIKI_SEARCH_URL, + params={ + "action": "query", + "list": "search", + "srsearch": candidate, + "format": "json", + "srlimit": 5, + }, + timeout=20, + ) + response.raise_for_status() + hits = response.json().get("query", {}).get("search", []) + for hit in hits: + title = hit["title"] + score = _title_match_score(candidate, title) + if score > best_score: + best_score = score + best_title = title + if best_score >= 0.55: + return best_title, best_score + return None, best_score + + +def fetch_pageview_spike( + session: requests.Session, + article_title: str, + event_date: dt.date, +) -> dict[str, float] | None: + start = (event_date - dt.timedelta(days=20)).strftime("%Y%m%d") + end = (event_date + dt.timedelta(days=2)).strftime("%Y%m%d") + response = session.get( + WIKI_PAGEVIEWS_URL.format( + article=article_title.replace(" ", "_"), + start=start, + end=end, + ), + timeout=20, + ) + if response.status_code != 200: + return None + + items = response.json().get("items", []) + if len(items) < 8: + return None + + views = pd.DataFrame( + [(pd.to_datetime(item["timestamp"][:8]), item["views"]) for item in items], + columns=["date", "views"], + ).sort_values("date") + event_ts = pd.Timestamp(event_date) + pre_event = views.loc[views["date"] < event_ts, "views"] + event_views = views.loc[views["date"] == event_ts, "views"] + if pre_event.empty or event_views.empty: + return None + + baseline = float(pre_event.tail(10).median()) + if baseline <= 0: + return None + + event_value = float(event_views.iloc[0]) + return { + "event_views": event_value, + "baseline_views": baseline, + "pageview_spike": event_value / baseline, + } + + +def fetch_gdelt_article_count( + session: requests.Session, + issuer_name: str, + event_date: dt.date, +) -> int | None: + exact_name = MANUAL_WIKI_TITLE.get(issuer_name.upper(), issuer_name) + phrase = exact_name.replace("\xa0", " ").replace('"', "") + if len(phrase) < 6: + return None + + time.sleep(6.0) + start = (event_date - dt.timedelta(days=1)).strftime("%Y%m%d") + "000000" + end = (event_date + dt.timedelta(days=1)).strftime("%Y%m%d") + "235959" + response = session.get( + GDELT_DOC_URL, + params={ + "query": f'"{phrase}"', + "mode": "ArtList", + "maxrecords": 50, + "format": "json", + "startdatetime": start, + "enddatetime": end, + }, + timeout=30, + ) + if response.status_code != 200: + return None + + payload = response.json() + articles = payload.get("articles", []) + return len(articles) + + +async def load_probe_rows(snapshot_path: Path, split_name: str, limit: int) -> list[ProbeRow]: + snapshot = pd.read_parquet(snapshot_path) + snapshot = snapshot[ + ["event_id", "event_date", "reaction_day_return", "fwd_return_3d", "fwd_return_5d"] + ].copy() + snapshot["event_date"] = pd.to_datetime(snapshot["event_date"]).dt.date + + engine = create_async_engine(get_settings().postgres_dsn) + try: + async with engine.connect() as conn: + result = await conn.execute( + text( + """ + select e.event_id, e.event_type, sm.ticker, i.issuer_name + from events e + left join issuer_master i on i.issuer_id = e.issuer_id + left join symbol_master sm on sm.symbol_id = e.symbol_id + where e.event_id = any(:ids) + """ + ), + {"ids": snapshot["event_id"].tolist()}, + ) + meta = pd.DataFrame(result.fetchall(), columns=result.keys()) + finally: + await engine.dispose() + + merged = snapshot.merge(meta, on="event_id", how="left") + merged = merged[merged["event_type"] == "earnings_release"].copy() + generic = ( + (merged["issuer_name"].fillna("") == merged["ticker"].fillna("") + " Corporation") + | (merged["issuer_name"].fillna("") == merged["ticker"].fillna("") + " Inc.") + | (merged["issuer_name"].fillna("") == merged["ticker"].fillna("") + " Ltd.") + ) + filtered = merged.loc[~generic & merged["issuer_name"].notna()].sort_values("event_date") + if limit > 0: + filtered = filtered.head(limit) + + return [ + ProbeRow( + ticker=str(row["ticker"]), + issuer_name=str(row["issuer_name"]), + event_date=row["event_date"], + reaction_day_return=float(row["reaction_day_return"]), + fwd_return_3d=float(row["fwd_return_3d"]), + fwd_return_5d=float(row["fwd_return_5d"]), + ) + for _, row in filtered.iterrows() + ] + + +def run_probe( + rows: list[ProbeRow], + output_csv: Path | None, + gdelt_limit: int, +) -> pd.DataFrame: + session = _session() + resolved_rows: list[dict[str, object]] = [] + + for idx, row in enumerate(rows): + title, match_score = resolve_wikipedia_title(session, row.issuer_name) + if not title: + continue + pageviews = fetch_pageview_spike(session, title, row.event_date) + if not pageviews: + continue + + gdelt_count = None + if idx < gdelt_limit: + gdelt_count = fetch_gdelt_article_count(session, row.issuer_name, row.event_date) + + signed_cont_3d = (1.0 if row.reaction_day_return >= 0 else -1.0) * row.fwd_return_3d + signed_cont_5d = (1.0 if row.reaction_day_return >= 0 else -1.0) * row.fwd_return_5d + resolved_rows.append( + { + "ticker": row.ticker, + "issuer_name": row.issuer_name, + "article_title": title, + "match_score": round(match_score, 3), + "event_date": row.event_date.isoformat(), + "reaction_day_return": row.reaction_day_return, + "fwd_return_3d": row.fwd_return_3d, + "fwd_return_5d": row.fwd_return_5d, + "signed_cont_3d": signed_cont_3d, + "signed_cont_5d": signed_cont_5d, + **pageviews, + "gdelt_article_count_3d": gdelt_count, + } + ) + + df = pd.DataFrame(resolved_rows) + if output_csv and not df.empty: + output_csv.parent.mkdir(parents=True, exist_ok=True) + df.to_csv(output_csv, index=False) + return df + + +def print_summary(df: pd.DataFrame, sampled_rows: int) -> None: + print(f"resolved_rows={len(df)} sampled_rows={sampled_rows}") + if df.empty: + return + + preview_cols = [ + "ticker", + "issuer_name", + "article_title", + "match_score", + "pageview_spike", + "signed_cont_3d", + "signed_cont_5d", + "gdelt_article_count_3d", + ] + print(df[preview_cols].to_string(index=False)) + + median_spike = float(df["pageview_spike"].median()) + high = df[df["pageview_spike"] >= median_spike] + low = df[df["pageview_spike"] < median_spike] + print("") + print(f"median_pageview_spike={median_spike:.3f}") + print(f"high_group_n={len(high)} low_group_n={len(low)}") + print(f"high_signed_cont_3d_mean={high['signed_cont_3d'].mean():.4f}") + print(f"low_signed_cont_3d_mean={low['signed_cont_3d'].mean():.4f}") + print(f"high_signed_cont_5d_mean={high['signed_cont_5d'].mean():.4f}") + print(f"low_signed_cont_5d_mean={low['signed_cont_5d'].mean():.4f}") + print(f"corr(pageview_spike,signed_cont_3d)={df['pageview_spike'].corr(df['signed_cont_3d']):.4f}") + print(f"corr(pageview_spike,signed_cont_5d)={df['pageview_spike'].corr(df['signed_cont_5d']):.4f}") + + gdelt = df["gdelt_article_count_3d"].dropna() + if not gdelt.empty: + print( + "gdelt_counts_sample=" + + ", ".join(str(int(value)) for value in gdelt.tolist()) + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Probe free attention data against snapshot outcomes.") + parser.add_argument( + "--snapshot", + default="data/datasets/snapshots/midcap-filtered/test.parquet", + help="Snapshot parquet path", + ) + parser.add_argument( + "--split", + default="test", + help="Label only for reporting; snapshot path determines actual data", + ) + parser.add_argument( + "--limit", + type=int, + default=30, + help="Number of filtered earnings events to sample", + ) + parser.add_argument( + "--gdelt-limit", + type=int, + default=5, + help="How many resolved rows to spot-check with GDELT article counts", + ) + parser.add_argument( + "--output-csv", + default="data/research/free_attention_probe_test_sample.csv", + help="Output CSV path", + ) + return parser.parse_args() + + +async def main() -> None: + args = parse_args() + snapshot_path = Path(args.snapshot) + rows = await load_probe_rows(snapshot_path, args.split, args.limit) + df = run_probe(rows, Path(args.output_csv), args.gdelt_limit) + print_summary(df, sampled_rows=len(rows)) + if not df.empty: + print(f"saved_csv={args.output_csv}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/apps/tracker/cli.py b/apps/tracker/cli.py index 3b404c3..8a55fae 100644 --- a/apps/tracker/cli.py +++ b/apps/tracker/cli.py @@ -29,6 +29,7 @@ from libs.backtest.tracker import ( compute_sqs_v2, compute_unified_score, get_next_entry_id, + journal_lock, load_journal, rebuild_registry, scan_runs_for_experiment, @@ -43,13 +44,6 @@ def cmd_record(args: argparse.Namespace) -> None: registry_path = journal_dir / "experiment_registry.json" leaderboard_path = journal_dir / "LEADERBOARD.md" - # Check duplicate - dupes = check_duplicate(journal_path, args.experiment) - if dupes and not args.force: - print(f"WARNING: experiment '{args.experiment}' already in journal ({len(dupes)} entries).") - print("Use --force to add anyway.") - sys.exit(1) - # Scan runs runs_dir = Path(args.runs_dir) if not runs_dir.exists(): @@ -110,29 +104,37 @@ def cmd_record(args: argparse.Namespace) -> None: # Build tags from experiment name tags = [t for t in args.experiment.replace("-", "_").split("_") if t] - entry_id = get_next_entry_id(journal_path) - entry = JournalEntry( - entry_id=entry_id, - timestamp=utc_now().isoformat(), - experiment_name=args.experiment, - hypothesis=args.hypothesis or "", - config_delta=config_delta, - results=results, - sqs_score=sqs_score, - sqs_breakdown=sqs_breakdown, - sqs_v2_score=sqs_v2_score, - sqs_v2_breakdown=sqs_v2_breakdown, - promotion_score=promotion_score, - promotion_breakdown=promotion_breakdown, - unified_score=unified_score, - unified_breakdown=unified_breakdown, - verdict=args.verdict or "unknown", - verdict_reasoning=args.reasoning or "", - next_direction=args.next or "", - tags=tags, - ) + with journal_lock(journal_path): + dupes = check_duplicate(journal_path, args.experiment) + if dupes and not args.force: + print(f"WARNING: experiment '{args.experiment}' already in journal ({len(dupes)} entries).") + print("Use --force to add anyway.") + sys.exit(1) + + entry_id = get_next_entry_id(journal_path) + entry = JournalEntry( + entry_id=entry_id, + timestamp=utc_now().isoformat(), + experiment_name=args.experiment, + hypothesis=args.hypothesis or "", + config_delta=config_delta, + results=results, + sqs_score=sqs_score, + sqs_breakdown=sqs_breakdown, + sqs_v2_score=sqs_v2_score, + sqs_v2_breakdown=sqs_v2_breakdown, + promotion_score=promotion_score, + promotion_breakdown=promotion_breakdown, + unified_score=unified_score, + unified_breakdown=unified_breakdown, + verdict=args.verdict or "unknown", + verdict_reasoning=args.reasoning or "", + next_direction=args.next or "", + tags=tags, + ) - append_journal_entry(journal_path, entry) + append_journal_entry(journal_path, entry) + rebuild_registry(journal_path, registry_path, leaderboard_path) source_label = f", source={public_source}" if public_source else "" print(f"Recorded {entry_id}: {args.experiment} (SQS={sqs_score}{source_label})") @@ -142,8 +144,6 @@ def cmd_record(args: argparse.Namespace) -> None: ret = f"Ret={sr.total_return_pct:+.2f}%" if sr.total_return_pct is not None else "Ret=-" print(f" {split_name}: {sr.trade_count} trades, {pf}, {ret}") - # Rebuild leaderboard - rebuild_registry(journal_path, registry_path, leaderboard_path) print(f"Leaderboard updated: {leaderboard_path}") diff --git a/configs/experiments/pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12.json b/configs/experiments/pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12.json new file mode 100644 index 0000000..f4d68bc --- /dev/null +++ b/configs/experiments/pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 54: Keep the step52 short-core structure, but let the same-day long overlay run with a wider target, partial exit, wider trailing stop, and a 12-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, + "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_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.125, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step54", "short_core", "macro_block", "crashcap", "gap10", "longtrend12"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25.json b/configs/experiments/pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25.json new file mode 100644 index 0000000..4880723 --- /dev/null +++ b/configs/experiments/pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 55: Step54 plus a larger same-day long overlay budget, increasing long participation while keeping the short-core sleeves intact.", + "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_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step55", "short_core", "macro_block", "crashcap", "gap10", "longtrend12", "sdlong25"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25.json b/configs/experiments/pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25.json new file mode 100644 index 0000000..a295af8 --- /dev/null +++ b/configs/experiments/pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 56: Step55 plus interleaved engine selection so the strengthened same-day long overlay gets consistent portfolio slots instead of competing purely on raw PEAD score.", + "base_config": "configs/backtest/defaults.json", + "overrides": { + "strategy_engine_selection_mode": "interleave", + "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_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "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.125, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step56", "short_core", "macro_block", "crashcap", "gap10", "interleave", "longtrend25"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4.json b/configs/experiments/pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4.json new file mode 100644 index 0000000..eae590c --- /dev/null +++ b/configs/experiments/pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 57: Step55 plus four daily candidate slots so the added long trend sleeve does not crowd out positive short-core entries.", + "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": 4 + }, + "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_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step57", "short_core", "macro_block", "crashcap", "gap10", "longtrend12", "sdlong25", "max4"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25.json b/configs/experiments/pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25.json new file mode 100644 index 0000000..7bbee05 --- /dev/null +++ b/configs/experiments/pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 58: Step55 with a looser same-day long gap filter, broadening the trend-hold overlay beyond only the most extreme 10% gap moves.", + "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_close12_gap7_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.07, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step58", "short_core", "macro_block", "crashcap", "gap7", "longtrend12", "sdlong25"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step59_same_day_only_max4_longtrend25.json b/configs/experiments/pead_midcap_step59_same_day_only_max4_longtrend25.json new file mode 100644 index 0000000..4a418dd --- /dev/null +++ b/configs/experiments/pead_midcap_step59_same_day_only_max4_longtrend25.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step59_same_day_only_max4_longtrend25", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 59: Remove after-close short from the active book and keep only the same-day short core plus same-day long trend sleeve under four daily slots.", + "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": 4 + }, + "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_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + }, + { + "engine_id": "earnings_after_close_short_core_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": 0.25, + "shadow_only": true + } + ], + "splits": [], + "tags": ["pead", "midcap", "step59", "same_day_only", "max4", "longtrend25"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step60_same_day_only_interleave_max4_longtrend25.json b/configs/experiments/pead_midcap_step60_same_day_only_interleave_max4_longtrend25.json new file mode 100644 index 0000000..e149b2c --- /dev/null +++ b/configs/experiments/pead_midcap_step60_same_day_only_interleave_max4_longtrend25.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step60_same_day_only_interleave_max4_longtrend25", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 60: Same-day only book with interleaved sleeve selection so the long trend overlay always gets candidate representation alongside the short core.", + "base_config": "configs/backtest/defaults.json", + "overrides": { + "strategy_engine_selection_mode": "interleave", + "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": 4 + }, + "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_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + }, + { + "engine_id": "earnings_after_close_short_core_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": 0.25, + "shadow_only": true + } + ], + "splits": [], + "tags": ["pead", "midcap", "step60", "same_day_only", "interleave", "max4", "longtrend25"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step61_same_day_only_max5_longtrend25.json b/configs/experiments/pead_midcap_step61_same_day_only_max5_longtrend25.json new file mode 100644 index 0000000..deb18c8 --- /dev/null +++ b/configs/experiments/pead_midcap_step61_same_day_only_max5_longtrend25.json @@ -0,0 +1,81 @@ +{ + "experiment_name": "pead_midcap_step61_same_day_only_max5_longtrend25", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 61: Same-day only book with five daily slots, testing whether the short core plus long trend overlay can lift train return when after-close short is removed.", + "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": 5 + }, + "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_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + }, + { + "engine_id": "earnings_after_close_short_core_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": 0.25, + "shadow_only": true + } + ], + "splits": [], + "tags": ["pead", "midcap", "step61", "same_day_only", "max5", "longtrend25"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10.json b/configs/experiments/pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10.json new file mode 100644 index 0000000..76a0dab --- /dev/null +++ b/configs/experiments/pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10.json @@ -0,0 +1,82 @@ +{ + "experiment_name": "pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 62: Keep the max4 mixed-sleeve portfolio, but require after-close short setups to have at least a 10% negative gap so weak downside follow-through does not crowd higher-conviction 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": 4 + }, + "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_gap10", + "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, + "gap_size_max": -0.10, + "shadow_only": false + }, + { + "engine_id": "earnings_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step62", "short_core", "macro_block", "crashcap", "gap10", "longtrend12", "sdlong25", "max4", "acsgap10"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12.json b/configs/experiments/pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12.json new file mode 100644 index 0000000..a7a3a33 --- /dev/null +++ b/configs/experiments/pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12.json @@ -0,0 +1,82 @@ +{ + "experiment_name": "pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 63: Same as step62, but require an even deeper 12% negative gap for after-close short entries to concentrate the sleeve into only the strongest downside reactions.", + "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": 4 + }, + "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_gap12", + "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, + "gap_size_max": -0.12, + "shadow_only": false + }, + { + "engine_id": "earnings_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step63", "short_core", "macro_block", "crashcap", "gap10", "longtrend12", "sdlong25", "max4", "acsgap12"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10.json b/configs/experiments/pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10.json new file mode 100644 index 0000000..e1a57be --- /dev/null +++ b/configs/experiments/pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10.json @@ -0,0 +1,82 @@ +{ + "experiment_name": "pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 64: Interleave the max4 mixed-sleeve portfolio while applying a 10% negative-gap gate to after-close short setups.", + "base_config": "configs/backtest/defaults.json", + "overrides": { + "strategy_engine_selection_mode": "interleave", + "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": 4 + }, + "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_gap10", + "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, + "gap_size_max": -0.10, + "shadow_only": false + }, + { + "engine_id": "earnings_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step64", "short_core", "macro_block", "crashcap", "gap10", "interleave", "max4", "acsgap10"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12.json b/configs/experiments/pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12.json new file mode 100644 index 0000000..fed4763 --- /dev/null +++ b/configs/experiments/pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12.json @@ -0,0 +1,82 @@ +{ + "experiment_name": "pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 65: Interleaved max4 mixed-sleeve portfolio with a stricter 12% negative-gap gate on after-close shorts.", + "base_config": "configs/backtest/defaults.json", + "overrides": { + "strategy_engine_selection_mode": "interleave", + "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": 4 + }, + "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_gap12", + "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, + "gap_size_max": -0.12, + "shadow_only": false + }, + { + "engine_id": "earnings_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step65", "short_core", "macro_block", "crashcap", "gap10", "interleave", "max4", "acsgap12"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12.json b/configs/experiments/pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12.json new file mode 100644 index 0000000..83a1c00 --- /dev/null +++ b/configs/experiments/pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12.json @@ -0,0 +1,83 @@ +{ + "experiment_name": "pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 66: Step62 plus a minimum after-close downside reaction of 12%, keeping only deeper negative gap and reaction combinations in the filtered short 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": 4 + }, + "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_gap10_react12", + "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, + "reaction_day_return_max": -0.12, + "gap_size_max": -0.10, + "shadow_only": false + }, + { + "engine_id": "earnings_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step66", "short_core", "macro_block", "crashcap", "gap10", "longtrend12", "sdlong25", "max4", "acsgap10", "react12"], + "notes": null +} diff --git a/configs/experiments/pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14.json b/configs/experiments/pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14.json new file mode 100644 index 0000000..f839d42 --- /dev/null +++ b/configs/experiments/pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14.json @@ -0,0 +1,83 @@ +{ + "experiment_name": "pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14", + "dataset_snapshot_id": "midcap-filtered", + "description": "Step 67: Step62 plus a stricter 14% downside reaction requirement for after-close shorts, testing whether only the sharpest downside follow-through setups should remain.", + "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": 4 + }, + "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_gap10_react14", + "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, + "reaction_day_return_max": -0.14, + "gap_size_max": -0.10, + "shadow_only": false + }, + { + "engine_id": "earnings_same_day_long_close12_gap10_trend", + "event_types": ["earnings_release"], + "timing_class": "same_day", + "direction": "long_only", + "entry_timing_policy": "reaction_close", + "max_holding_days": 12, + "engine_risk_budget_pct": 0.25, + "gap_size_min": 0.10, + "target_atr_multiplier_override": 2.5, + "target_1_fraction_override": 0.33, + "trailing_model_override": "pct_10", + "trailing_warmup_days_override": 2, + "shadow_only": false + } + ], + "splits": [], + "tags": ["pead", "midcap", "step67", "short_core", "macro_block", "crashcap", "gap10", "longtrend12", "sdlong25", "max4", "acsgap10", "react14"], + "notes": null +} diff --git a/journal/LEADERBOARD.md b/journal/LEADERBOARD.md index f98d375..2a8dcf2 100644 --- a/journal/LEADERBOARD.md +++ b/journal/LEADERBOARD.md @@ -1,84 +1,98 @@ # Strategy Improvement Leaderboard -_Updated: 2026-03-17T09:17:27.726326+00:00_ +_Updated: 2026-03-17T10:37:12.332837+00:00_ | # | Experiment | SQS | [T]PF | [T]Ret% | [T]WR | [T]Sharpe | [T]DD% | [T]N | [T]Gross% | [T]Net% | [T]DIM% | [V]PF | [V]Ret% | [V]WR | [V]Sharpe | [V]DD% | [V]N | [V]Gross% | [V]Net% | [V]DIM% | Date | |---|-----------|-----|-------|---------|-------|-----------|--------|------|-----------|---------|---------|-------|---------|-------|-----------|--------|------|-----------|---------|---------|------| -| 1 | pead_midcap_step52_short_core_macro_block_crashcap_gap10 | 49.8 | 3.35 | +1.0 | 77% | 3.0 | 0.2 | 22 | 2.9 | -2.2 | 53.2 | 5.66 | +1.8 | 72% | 4.8 | 0.2 | 25 | 3.6 | -2.6 | 56.1 | 2026-03-17 | -| 2 | pead_midcap_step48_short_core_macro_block_nolong | 46.9 | 3.40 | +0.8 | 80% | 2.4 | 0.4 | 20 | 2.6 | +2.6 | 52.2 | 5.73 | +1.3 | 70% | 3.4 | 0.3 | 20 | 3.3 | +3.3 | 51.8 | 2026-03-17 | -| 3 | pead_midcap_step46_short_core_macro_block_acshort12 | 45.8 | 4.52 | +1.3 | 71% | 3.6 | 0.3 | 21 | 2.1 | +2.1 | 40.4 | 2.44 | +1.3 | 69% | 3.2 | 0.4 | 26 | 4.0 | +4.0 | 57.9 | 2026-03-17 | -| 4 | pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12 | 44.5 | 2.06 | +2.1 | 61% | 4.5 | 0.4 | 54 | 6.0 | +6.0 | 76.6 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | -| 5 | pead_midcap_step51_short_core_macro_block_crashcap | 44.4 | 4.19 | +1.2 | 73% | 3.6 | 0.2 | 22 | 2.3 | -0.8 | 42.6 | 2.31 | +1.3 | 68% | 3.0 | 0.4 | 28 | 4.1 | -1.9 | 57.9 | 2026-03-17 | -| 6 | pead_midcap_step45_short_core_macro_block | 44.3 | 3.78 | +1.2 | 70% | 3.4 | 0.3 | 23 | 2.4 | +2.4 | 42.6 | 2.31 | +1.3 | 68% | 3.0 | 0.4 | 28 | 4.2 | +4.2 | 57.9 | 2026-03-17 | -| 7 | pead_midcap_step53_short_core_macro_block_crashcap_gap14 | 39.5 | 4.60 | +1.1 | 84% | 3.5 | 0.2 | 19 | 2.3 | +2.3 | 53.2 | 8.21 | +2.0 | 76% | 5.3 | 0.2 | 25 | 3.6 | +3.6 | 56.1 | 2026-03-17 | -| 8 | pead_midcap_step50_same_day_short_long_macro_block | 36.2 | 3.21 | +0.8 | 60% | 2.7 | 0.4 | 15 | 1.6 | +1.6 | 38.3 | 2.21 | +1.0 | 65% | 2.7 | 0.3 | 20 | 3.4 | +3.4 | 43.9 | 2026-03-17 | -| 9 | pead_midcap_step30_balanced_sleeves_nofrac | 35.4 | 1.41 | +1.4 | 54% | 2.8 | 0.8 | 67 | 8.0 | +8.0 | 76.6 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | -| 10 | pead_midcap_step47_short_core_macro_block_sdlong25 | 35.2 | 3.43 | +1.3 | 68% | 3.3 | 0.3 | 25 | 2.9 | +2.9 | 48.9 | 1.77 | +1.0 | 65% | 2.2 | 0.4 | 31 | 4.6 | +4.6 | 57.9 | 2026-03-17 | -| 11 | pead_midcap_step44_short_core_macro50 | 35.0 | 2.01 | +1.3 | 57% | 2.6 | 0.7 | 58 | 4.5 | -1.6 | 72.3 | 1.78 | +1.1 | 55% | 2.5 | 0.4 | 40 | 4.8 | -2.0 | 73.7 | 2026-03-17 | -| 12 | pead_midcap_step33_balanced_sleeves_nofrac_acshort6 | 35.0 | 1.47 | +1.2 | 52% | 2.3 | 0.7 | 48 | 6.2 | +6.2 | 70.2 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | -| 13 | pead_midcap_step43_short_core_sdlong25 | 31.7 | 1.66 | +1.5 | 57% | 2.0 | 1.3 | 58 | 7.1 | +7.1 | 72.3 | 1.46 | +1.0 | 56% | 1.9 | 0.6 | 45 | 6.2 | +6.2 | 73.7 | 2026-03-17 | -| 14 | pead_midcap_step41_short_core_sdlong12_acshort50 | 30.7 | 1.53 | +1.2 | 56% | 1.6 | 1.3 | 59 | 6.8 | +6.8 | 72.3 | 1.48 | +0.9 | 55% | 1.8 | 0.6 | 40 | 5.6 | +5.6 | 73.7 | 2026-03-17 | -| 15 | pead_midcap_step37_balanced_sleeves_aclong_vol3 | 30.6 | 1.38 | +1.2 | 54% | 2.5 | 0.8 | 63 | 7.5 | +7.5 | 76.6 | 1.38 | +1.1 | 51% | 1.8 | 0.9 | 57 | 7.9 | +7.9 | 77.2 | 2026-03-17 | -| 16 | pead_midcap_step42_short_core_only | 30.5 | 1.44 | +0.8 | 63% | 1.2 | 1.2 | 46 | 5.5 | +5.5 | 71.7 | 2.55 | +1.2 | 58% | 2.8 | 0.5 | 31 | 4.5 | +4.5 | 71.4 | 2026-03-17 | -| 17 | pead_midcap_step14_score65 | 29.9 | 1.22 | +0.7 | 57% | 1.3 | 0.9 | 72 | - | - | - | 1.40 | +1.0 | 56% | 1.6 | 0.7 | 66 | - | - | - | 2026-03-17 | -| 18 | pead_midcap_step18_nofrac | 29.4 | 1.23 | +0.8 | 52% | 1.4 | 0.9 | 64 | - | - | - | 1.46 | +1.2 | 47% | 1.9 | 0.6 | 55 | - | - | - | 2026-03-17 | -| 19 | pead_midcap_step31_balanced_sleeves_nofrac_acshort12 | 29.3 | 1.49 | +1.6 | 55% | 3.2 | 0.7 | 64 | 7.6 | +7.6 | 76.6 | 1.38 | +1.1 | 52% | 1.8 | 0.9 | 58 | 8.0 | +8.0 | 77.2 | 2026-03-17 | -| 20 | pead_midcap_step19_hold5 | 29.2 | 1.20 | +0.7 | 57% | 1.2 | 0.9 | 72 | - | - | - | 1.55 | +1.4 | 57% | 2.2 | 0.7 | 68 | - | - | - | 2026-03-17 | -| 21 | pead_midcap_step49_same_day_short_macro_block | 29.1 | 2.40 | +0.4 | 70% | 1.6 | 0.4 | 10 | 1.0 | +1.0 | 26.1 | 25.09 | +1.2 | 75% | 3.2 | 0.3 | 12 | 2.6 | +2.6 | 32.1 | 2026-03-17 | -| 22 | pead_midcap_step20_best3 | 28.7 | 1.22 | +0.7 | 52% | 1.3 | 0.9 | 64 | - | - | - | 1.61 | +1.6 | 48% | 2.5 | 0.6 | 56 | - | - | - | 2026-03-17 | -| 23 | pead_midcap_step40_short_core_sdlong12 | 28.6 | 1.77 | +1.5 | 58% | 2.2 | 1.1 | 55 | 6.4 | +6.4 | 72.3 | 1.48 | +0.9 | 55% | 1.8 | 0.6 | 40 | 5.6 | +5.6 | 73.7 | 2026-03-17 | -| 24 | pead_midcap_step17_target2 | 27.3 | 1.18 | +0.6 | 53% | 1.1 | 0.8 | 66 | - | - | - | 1.38 | +1.0 | 51% | 1.5 | 0.7 | 59 | - | - | - | 2026-03-17 | -| 25 | pead_midcap_step39_balanced_sleeves_sdlong12 | 27.0 | 1.50 | +1.5 | 55% | 3.4 | 0.5 | 62 | 7.4 | +7.4 | 76.6 | 1.38 | +1.0 | 51% | 1.7 | 0.9 | 53 | 7.5 | +7.5 | 77.2 | 2026-03-17 | -| 26 | pead_midcap_step27_sdlong_close7_budget25 | 26.4 | 1.17 | +0.6 | 54% | 0.9 | 1.3 | 68 | 8.5 | +8.5 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | -| 27 | pead_midcap_step13_best | 26.3 | 1.12 | +0.4 | 55% | 0.8 | 0.9 | 75 | - | - | - | 1.61 | +1.6 | 59% | 2.2 | 0.7 | 70 | - | - | - | 2026-03-16 | -| 28 | pead_midcap_step23_sdlong_close7 | 24.9 | 1.13 | +0.5 | 54% | 0.7 | 1.4 | 69 | 8.6 | +8.6 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | -| 29 | pead_midcap_step34_balanced_sleeves_nofrac_aclong25 | 23.9 | 1.62 | +1.8 | 56% | 3.8 | 0.6 | 62 | 7.4 | +7.4 | 76.6 | 1.30 | +0.9 | 51% | 1.4 | 0.9 | 57 | 7.9 | +7.9 | 77.2 | 2026-03-17 | -| 30 | pead_midcap_step16_react7_score65 | 23.8 | 0.97 | -0.1 | 56% | -0.2 | 1.6 | 89 | - | - | - | 2.01 | +2.8 | 63% | 3.7 | 0.8 | 83 | - | - | - | 2026-03-17 | -| 31 | pead_midcap_step5_maxcand3 | 23.4 | 0.95 | -0.2 | 54% | -0.4 | 1.4 | 96 | - | - | - | 2.08 | +3.3 | 65% | 4.0 | 1.1 | 89 | - | - | - | 2026-03-16 | -| 32 | pead_midcap_step38_balanced_sleeves_aclong_vol4 | 23.3 | 1.12 | +0.4 | 51% | 0.8 | 0.9 | 61 | 7.4 | +7.4 | 76.6 | 1.29 | +0.8 | 53% | 1.4 | 0.9 | 53 | 7.6 | +7.6 | 77.2 | 2026-03-17 | -| 33 | pead_midcap_step15_react7 | 23.0 | 0.94 | -0.3 | 55% | -0.5 | 1.6 | 91 | - | - | - | 1.89 | +2.6 | 62% | 3.5 | 0.9 | 84 | - | - | - | 2026-03-17 | -| 34 | pead_midcap_portfolio_v2 | 23.0 | 1.08 | +0.3 | 51% | 0.5 | 1.8 | 70 | 7.9 | +7.9 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | -| 35 | pead_midcap_step11_score60 | 22.1 | 1.02 | +0.1 | 52% | 0.2 | 0.9 | 77 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | -| 36 | pead_midcap_step12_vol2x | 22.1 | 1.02 | +0.1 | 52% | 0.1 | 0.9 | 77 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | -| 37 | pead_midcap_step3_10pct | 21.9 | 1.00 | +0.0 | 50% | 0.0 | 1.4 | 98 | - | - | - | 1.66 | +2.0 | 60% | 2.9 | 0.7 | 78 | - | - | - | 2026-03-16 | -| 38 | pead_midcap_step10_short | 21.5 | 1.00 | -0.0 | 52% | -0.0 | 0.9 | 79 | - | - | - | 1.61 | +1.6 | 59% | 2.2 | 0.7 | 70 | - | - | - | 2026-03-16 | -| 39 | pead_midcap_step35_balanced_sleeves_nofrac_aclong12 | 21.2 | 2.01 | +2.2 | 61% | 4.2 | 0.4 | 56 | 6.2 | +1.1 | 76.6 | 1.24 | +0.7 | 51% | 1.2 | 0.9 | 53 | 7.5 | +1.6 | 77.2 | 2026-03-17 | -| 40 | pead_midcap_step2_notrail | 17.2 | 0.93 | -0.3 | 67% | -0.4 | 1.7 | 54 | - | - | - | 1.07 | +0.4 | 73% | 0.4 | 1.7 | 62 | - | - | - | 2026-03-16 | -| 41 | pead_midcap_step1_fixedr | 16.2 | 0.91 | -0.5 | 47% | -0.7 | 1.5 | 95 | - | - | - | 1.77 | +2.8 | 49% | 3.4 | 1.6 | 69 | - | - | - | 2026-03-16 | -| 42 | pead_midcap_combo_10pct_maxcand3 | 15.9 | 0.97 | -0.1 | 51% | -0.2 | 0.9 | 79 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | -| 43 | pead_midcap_step6_drift | 14.7 | 0.86 | -0.9 | 43% | -1.6 | 1.7 | 100 | - | - | - | 1.70 | +2.7 | 51% | 3.7 | 1.0 | 75 | - | - | - | 2026-03-16 | -| 44 | pead_midcap_step7_fixedr | 13.8 | 0.94 | -0.2 | 45% | -0.4 | 1.0 | 71 | - | - | - | 2.00 | +2.4 | 53% | 3.1 | 0.7 | 53 | - | - | - | 2026-03-16 | -| 45 | pead_midcap_step4_longonly | 12.2 | 0.84 | -0.8 | 45% | -1.2 | 1.4 | 78 | - | - | - | 1.43 | +1.5 | 61% | 1.7 | 1.6 | 76 | - | - | - | 2026-03-16 | -| 46 | pead_midcap_step8_nft | 11.5 | 0.65 | -1.8 | 41% | -3.0 | 2.2 | 71 | - | - | - | 1.55 | +1.8 | 46% | 2.4 | 1.0 | 57 | - | - | - | 2026-03-16 | -| 47 | pead_midcap_step9_stop2 | 11.4 | 0.77 | -1.7 | 45% | -1.8 | 2.5 | 71 | - | - | - | 1.81 | +3.2 | 53% | 2.8 | 1.2 | 53 | - | - | - | 2026-03-16 | +| 1 | pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25 | 51.3 | 4.03 | +1.1 | 80% | 3.4 | 0.2 | 20 | 2.5 | -1.5 | 51.1 | 4.82 | +1.9 | 68% | 4.0 | 0.6 | 28 | 4.2 | -2.7 | 56.1 | 2026-03-17 | +| 2 | pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10 | 50.9 | 3.69 | +1.0 | 80% | 3.4 | 0.2 | 20 | 2.5 | -1.5 | 48.9 | 4.64 | +1.8 | 69% | 4.5 | 0.4 | 26 | 3.5 | -2.0 | 49.1 | 2026-03-17 | +| 3 | pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12 | 50.9 | 3.69 | +1.0 | 80% | 3.4 | 0.2 | 20 | 2.5 | -1.5 | 48.9 | 4.64 | +1.8 | 69% | 4.5 | 0.4 | 26 | 3.5 | -2.0 | 49.1 | 2026-03-17 | +| 4 | pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10 | 50.6 | 3.69 | +1.0 | 80% | 3.4 | 0.2 | 20 | 2.5 | -1.5 | 48.9 | 4.33 | +1.7 | 65% | 3.7 | 0.6 | 26 | 3.8 | -2.3 | 49.1 | 2026-03-17 | +| 5 | pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25 | 49.8 | 3.40 | +1.0 | 78% | 3.1 | 0.2 | 23 | 3.0 | -2.0 | 53.2 | 4.62 | +1.9 | 71% | 4.5 | 0.3 | 28 | 3.8 | -2.4 | 56.1 | 2026-03-17 | +| 6 | pead_midcap_step52_short_core_macro_block_crashcap_gap10 | 49.8 | 3.35 | +1.0 | 77% | 3.0 | 0.2 | 22 | 2.9 | -2.2 | 53.2 | 5.66 | +1.8 | 72% | 4.8 | 0.2 | 25 | 3.6 | -2.6 | 56.1 | 2026-03-17 | +| 7 | pead_midcap_step48_short_core_macro_block_nolong | 46.9 | 3.40 | +0.8 | 80% | 2.4 | 0.4 | 20 | 2.6 | +2.6 | 52.2 | 5.73 | +1.3 | 70% | 3.4 | 0.3 | 20 | 3.3 | +3.3 | 51.8 | 2026-03-17 | +| 8 | pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25 | 46.6 | 2.76 | +0.9 | 74% | 2.8 | 0.2 | 23 | 3.2 | -1.3 | 53.2 | 2.89 | +1.6 | 69% | 3.6 | 0.5 | 29 | 4.0 | -2.0 | 56.1 | 2026-03-17 | +| 9 | pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12 | 46.5 | 2.99 | +0.9 | 77% | 2.5 | 0.3 | 22 | 2.9 | -2.2 | 53.2 | 6.10 | +2.0 | 73% | 4.7 | 0.2 | 26 | 3.6 | -2.6 | 56.1 | 2026-03-17 | +| 10 | pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4 | 46.5 | 2.66 | +0.9 | 75% | 2.9 | 0.2 | 24 | 3.2 | -2.2 | 53.2 | 4.44 | +1.9 | 69% | 4.5 | 0.3 | 29 | 3.9 | -2.5 | 56.1 | 2026-03-17 | +| 11 | pead_midcap_step46_short_core_macro_block_acshort12 | 45.8 | 4.52 | +1.3 | 71% | 3.6 | 0.3 | 21 | 2.1 | +2.1 | 40.4 | 2.44 | +1.3 | 69% | 3.2 | 0.4 | 26 | 4.0 | +4.0 | 57.9 | 2026-03-17 | +| 12 | pead_midcap_step36_balanced_sleeves_nofrac_aclong12_sdlong12 | 44.5 | 2.06 | +2.1 | 61% | 4.5 | 0.4 | 54 | 6.0 | +6.0 | 76.6 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 13 | pead_midcap_step51_short_core_macro_block_crashcap | 44.4 | 4.19 | +1.2 | 73% | 3.6 | 0.2 | 22 | 2.3 | -0.8 | 42.6 | 2.31 | +1.3 | 68% | 3.0 | 0.4 | 28 | 4.1 | -1.9 | 57.9 | 2026-03-17 | +| 14 | pead_midcap_step45_short_core_macro_block | 44.3 | 3.78 | +1.2 | 70% | 3.4 | 0.3 | 23 | 2.4 | +2.4 | 42.6 | 2.31 | +1.3 | 68% | 3.0 | 0.4 | 28 | 4.2 | +4.2 | 57.9 | 2026-03-17 | +| 15 | pead_midcap_step53_short_core_macro_block_crashcap_gap14 | 39.5 | 4.60 | +1.1 | 84% | 3.5 | 0.2 | 19 | 2.3 | +2.3 | 53.2 | 8.21 | +2.0 | 76% | 5.3 | 0.2 | 25 | 3.6 | +3.6 | 56.1 | 2026-03-17 | +| 16 | pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12 | 38.3 | 3.60 | +0.9 | 79% | 3.2 | 0.2 | 19 | 1.9 | -0.9 | 38.3 | 4.56 | +1.5 | 67% | 4.0 | 0.5 | 24 | 3.4 | -1.9 | 47.4 | 2026-03-17 | +| 17 | pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12 | 38.2 | 3.60 | +0.9 | 79% | 3.2 | 0.2 | 19 | 1.9 | -0.9 | 38.3 | 5.27 | +1.8 | 71% | 4.9 | 0.3 | 24 | 3.1 | -1.6 | 45.6 | 2026-03-17 | +| 18 | pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14 | 38.2 | 3.60 | +0.9 | 79% | 3.2 | 0.2 | 19 | 1.9 | -0.9 | 38.3 | 5.33 | +1.8 | 72% | 4.4 | 0.5 | 25 | 3.2 | -1.7 | 47.4 | 2026-03-17 | +| 19 | pead_midcap_step50_same_day_short_long_macro_block | 36.2 | 3.21 | +0.8 | 60% | 2.7 | 0.4 | 15 | 1.6 | +1.6 | 38.3 | 2.21 | +1.0 | 65% | 2.7 | 0.3 | 20 | 3.4 | +3.4 | 43.9 | 2026-03-17 | +| 20 | pead_midcap_step30_balanced_sleeves_nofrac | 35.4 | 1.41 | +1.4 | 54% | 2.8 | 0.8 | 67 | 8.0 | +8.0 | 76.6 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 21 | pead_midcap_step47_short_core_macro_block_sdlong25 | 35.2 | 3.43 | +1.3 | 68% | 3.3 | 0.3 | 25 | 2.9 | +2.9 | 48.9 | 1.77 | +1.0 | 65% | 2.2 | 0.4 | 31 | 4.6 | +4.6 | 57.9 | 2026-03-17 | +| 22 | pead_midcap_step44_short_core_macro50 | 35.0 | 2.01 | +1.3 | 57% | 2.6 | 0.7 | 58 | 4.5 | -1.6 | 72.3 | 1.78 | +1.1 | 55% | 2.5 | 0.4 | 40 | 4.8 | -2.0 | 73.7 | 2026-03-17 | +| 23 | pead_midcap_step33_balanced_sleeves_nofrac_acshort6 | 35.0 | 1.47 | +1.2 | 52% | 2.3 | 0.7 | 48 | 6.2 | +6.2 | 70.2 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 24 | pead_midcap_step59_same_day_only_max4_longtrend25 | 33.2 | 2.61 | +0.6 | 69% | 2.3 | 0.3 | 13 | 1.4 | -0.5 | 31.9 | 6.63 | +1.9 | 73% | 4.5 | 0.4 | 22 | 3.9 | -2.4 | 42.1 | 2026-03-17 | +| 25 | pead_midcap_step60_same_day_only_interleave_max4_longtrend25 | 33.2 | 2.61 | +0.6 | 69% | 2.3 | 0.3 | 13 | 1.4 | -0.5 | 31.9 | 6.63 | +1.9 | 73% | 4.5 | 0.4 | 22 | 3.9 | -2.4 | 42.1 | 2026-03-17 | +| 26 | pead_midcap_step61_same_day_only_max5_longtrend25 | 33.2 | 2.61 | +0.6 | 69% | 2.3 | 0.3 | 13 | 1.4 | -0.5 | 31.9 | 6.63 | +1.9 | 73% | 4.5 | 0.4 | 22 | 3.9 | -2.4 | 42.1 | 2026-03-17 | +| 27 | pead_midcap_step43_short_core_sdlong25 | 31.7 | 1.66 | +1.5 | 57% | 2.0 | 1.3 | 58 | 7.1 | +7.1 | 72.3 | 1.46 | +1.0 | 56% | 1.9 | 0.6 | 45 | 6.2 | +6.2 | 73.7 | 2026-03-17 | +| 28 | pead_midcap_step41_short_core_sdlong12_acshort50 | 30.7 | 1.53 | +1.2 | 56% | 1.6 | 1.3 | 59 | 6.8 | +6.8 | 72.3 | 1.48 | +0.9 | 55% | 1.8 | 0.6 | 40 | 5.6 | +5.6 | 73.7 | 2026-03-17 | +| 29 | pead_midcap_step37_balanced_sleeves_aclong_vol3 | 30.6 | 1.38 | +1.2 | 54% | 2.5 | 0.8 | 63 | 7.5 | +7.5 | 76.6 | 1.38 | +1.1 | 51% | 1.8 | 0.9 | 57 | 7.9 | +7.9 | 77.2 | 2026-03-17 | +| 30 | pead_midcap_step42_short_core_only | 30.5 | 1.44 | +0.8 | 63% | 1.2 | 1.2 | 46 | 5.5 | +5.5 | 71.7 | 2.55 | +1.2 | 58% | 2.8 | 0.5 | 31 | 4.5 | +4.5 | 71.4 | 2026-03-17 | +| 31 | pead_midcap_step14_score65 | 29.9 | 1.22 | +0.7 | 57% | 1.3 | 0.9 | 72 | - | - | - | 1.40 | +1.0 | 56% | 1.6 | 0.7 | 66 | - | - | - | 2026-03-17 | +| 32 | pead_midcap_step18_nofrac | 29.4 | 1.23 | +0.8 | 52% | 1.4 | 0.9 | 64 | - | - | - | 1.46 | +1.2 | 47% | 1.9 | 0.6 | 55 | - | - | - | 2026-03-17 | +| 33 | pead_midcap_step31_balanced_sleeves_nofrac_acshort12 | 29.3 | 1.49 | +1.6 | 55% | 3.2 | 0.7 | 64 | 7.6 | +7.6 | 76.6 | 1.38 | +1.1 | 52% | 1.8 | 0.9 | 58 | 8.0 | +8.0 | 77.2 | 2026-03-17 | +| 34 | pead_midcap_step19_hold5 | 29.2 | 1.20 | +0.7 | 57% | 1.2 | 0.9 | 72 | - | - | - | 1.55 | +1.4 | 57% | 2.2 | 0.7 | 68 | - | - | - | 2026-03-17 | +| 35 | pead_midcap_step49_same_day_short_macro_block | 29.1 | 2.40 | +0.4 | 70% | 1.6 | 0.4 | 10 | 1.0 | +1.0 | 26.1 | 25.09 | +1.2 | 75% | 3.2 | 0.3 | 12 | 2.6 | +2.6 | 32.1 | 2026-03-17 | +| 36 | pead_midcap_step20_best3 | 28.7 | 1.22 | +0.7 | 52% | 1.3 | 0.9 | 64 | - | - | - | 1.61 | +1.6 | 48% | 2.5 | 0.6 | 56 | - | - | - | 2026-03-17 | +| 37 | pead_midcap_step40_short_core_sdlong12 | 28.6 | 1.77 | +1.5 | 58% | 2.2 | 1.1 | 55 | 6.4 | +6.4 | 72.3 | 1.48 | +0.9 | 55% | 1.8 | 0.6 | 40 | 5.6 | +5.6 | 73.7 | 2026-03-17 | +| 38 | pead_midcap_step17_target2 | 27.3 | 1.18 | +0.6 | 53% | 1.1 | 0.8 | 66 | - | - | - | 1.38 | +1.0 | 51% | 1.5 | 0.7 | 59 | - | - | - | 2026-03-17 | +| 39 | pead_midcap_step39_balanced_sleeves_sdlong12 | 27.0 | 1.50 | +1.5 | 55% | 3.4 | 0.5 | 62 | 7.4 | +7.4 | 76.6 | 1.38 | +1.0 | 51% | 1.7 | 0.9 | 53 | 7.5 | +7.5 | 77.2 | 2026-03-17 | +| 40 | pead_midcap_step27_sdlong_close7_budget25 | 26.4 | 1.17 | +0.6 | 54% | 0.9 | 1.3 | 68 | 8.5 | +8.5 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 41 | pead_midcap_step13_best | 26.3 | 1.12 | +0.4 | 55% | 0.8 | 0.9 | 75 | - | - | - | 1.61 | +1.6 | 59% | 2.2 | 0.7 | 70 | - | - | - | 2026-03-16 | +| 42 | pead_midcap_step23_sdlong_close7 | 24.9 | 1.13 | +0.5 | 54% | 0.7 | 1.4 | 69 | 8.6 | +8.6 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 43 | pead_midcap_step34_balanced_sleeves_nofrac_aclong25 | 23.9 | 1.62 | +1.8 | 56% | 3.8 | 0.6 | 62 | 7.4 | +7.4 | 76.6 | 1.30 | +0.9 | 51% | 1.4 | 0.9 | 57 | 7.9 | +7.9 | 77.2 | 2026-03-17 | +| 44 | pead_midcap_step16_react7_score65 | 23.8 | 0.97 | -0.1 | 56% | -0.2 | 1.6 | 89 | - | - | - | 2.01 | +2.8 | 63% | 3.7 | 0.8 | 83 | - | - | - | 2026-03-17 | +| 45 | pead_midcap_step5_maxcand3 | 23.4 | 0.95 | -0.2 | 54% | -0.4 | 1.4 | 96 | - | - | - | 2.08 | +3.3 | 65% | 4.0 | 1.1 | 89 | - | - | - | 2026-03-16 | +| 46 | pead_midcap_step38_balanced_sleeves_aclong_vol4 | 23.3 | 1.12 | +0.4 | 51% | 0.8 | 0.9 | 61 | 7.4 | +7.4 | 76.6 | 1.29 | +0.8 | 53% | 1.4 | 0.9 | 53 | 7.6 | +7.6 | 77.2 | 2026-03-17 | +| 47 | pead_midcap_step15_react7 | 23.0 | 0.94 | -0.3 | 55% | -0.5 | 1.6 | 91 | - | - | - | 1.89 | +2.6 | 62% | 3.5 | 0.9 | 84 | - | - | - | 2026-03-17 | +| 48 | pead_midcap_portfolio_v2 | 23.0 | 1.08 | +0.3 | 51% | 0.5 | 1.8 | 70 | 7.9 | +7.9 | 72.3 | - | - | - | - | - | 0 | - | - | - | 2026-03-17 | +| 49 | pead_midcap_step11_score60 | 22.1 | 1.02 | +0.1 | 52% | 0.2 | 0.9 | 77 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | +| 50 | pead_midcap_step12_vol2x | 22.1 | 1.02 | +0.1 | 52% | 0.1 | 0.9 | 77 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | +| 51 | pead_midcap_step3_10pct | 21.9 | 1.00 | +0.0 | 50% | 0.0 | 1.4 | 98 | - | - | - | 1.66 | +2.0 | 60% | 2.9 | 0.7 | 78 | - | - | - | 2026-03-16 | +| 52 | pead_midcap_step10_short | 21.5 | 1.00 | -0.0 | 52% | -0.0 | 0.9 | 79 | - | - | - | 1.61 | +1.6 | 59% | 2.2 | 0.7 | 70 | - | - | - | 2026-03-16 | +| 53 | pead_midcap_step35_balanced_sleeves_nofrac_aclong12 | 21.2 | 2.01 | +2.2 | 61% | 4.2 | 0.4 | 56 | 6.2 | +1.1 | 76.6 | 1.24 | +0.7 | 51% | 1.2 | 0.9 | 53 | 7.5 | +1.6 | 77.2 | 2026-03-17 | +| 54 | pead_midcap_step2_notrail | 17.2 | 0.93 | -0.3 | 67% | -0.4 | 1.7 | 54 | - | - | - | 1.07 | +0.4 | 73% | 0.4 | 1.7 | 62 | - | - | - | 2026-03-16 | +| 55 | pead_midcap_step1_fixedr | 16.2 | 0.91 | -0.5 | 47% | -0.7 | 1.5 | 95 | - | - | - | 1.77 | +2.8 | 49% | 3.4 | 1.6 | 69 | - | - | - | 2026-03-16 | +| 56 | pead_midcap_combo_10pct_maxcand3 | 15.9 | 0.97 | -0.1 | 51% | -0.2 | 0.9 | 79 | - | - | - | 1.91 | +2.3 | 62% | 3.0 | 0.7 | 72 | - | - | - | 2026-03-16 | +| 57 | pead_midcap_step6_drift | 14.7 | 0.86 | -0.9 | 43% | -1.6 | 1.7 | 100 | - | - | - | 1.70 | +2.7 | 51% | 3.7 | 1.0 | 75 | - | - | - | 2026-03-16 | +| 58 | pead_midcap_step7_fixedr | 13.8 | 0.94 | -0.2 | 45% | -0.4 | 1.0 | 71 | - | - | - | 2.00 | +2.4 | 53% | 3.1 | 0.7 | 53 | - | - | - | 2026-03-16 | +| 59 | pead_midcap_step4_longonly | 12.2 | 0.84 | -0.8 | 45% | -1.2 | 1.4 | 78 | - | - | - | 1.43 | +1.5 | 61% | 1.7 | 1.6 | 76 | - | - | - | 2026-03-16 | +| 60 | pead_midcap_step8_nft | 11.5 | 0.65 | -1.8 | 41% | -3.0 | 2.2 | 71 | - | - | - | 1.55 | +1.8 | 46% | 2.4 | 1.0 | 57 | - | - | - | 2026-03-16 | +| 61 | pead_midcap_step9_stop2 | 11.4 | 0.77 | -1.7 | 45% | -1.8 | 2.5 | 71 | - | - | - | 1.81 | +3.2 | 53% | 2.8 | 1.2 | 53 | - | - | - | 2026-03-16 | ## Recent Entries -### 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 39.5) -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-0061 (2026-03-17) — pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14 +Hypothesis: A stricter 14% downside reaction requirement may further improve the filtered after-close short sleeve by keeping only the sharpest downside continuation setups. +Verdict: **NEUTRAL** (SQS 38.2) +Reasoning: This pushed train to +5.32% and lifted valid slightly, but test slipped back to +0.95%. It is a stronger train-focused branch, not a clear overall winner versus step66. +Next: Favor step66 for balance; step67 is only useful if we optimize explicitly for train-heavy return. -### 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 49.8) -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-0060 (2026-03-17) — pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12 +Hypothesis: Adding a 12% downside reaction requirement on top of the 10% after-close gap filter may remove the weakest residual after-close shorts without sacrificing the recent OOS edge. +Verdict: **BETTER** (SQS 50.9) +Reasoning: This matched step62 on valid/test while lifting train from +5.01% to +5.09%. It is a cleaner version of the filtered short-sleeve branch with no observable downside so far. +Next: Use step66 as the balanced return-first branch; only test further changes if they can raise test above +1.0% without giving back the train lift. -### 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 44.4) -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-0059 (2026-03-17) — pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12 +Hypothesis: A stricter after-close gap gate plus interleaving may produce the strongest hybrid of train lift and balanced sleeve participation. +Verdict: **WORSE** (SQS 38.3) +Reasoning: Train ticked up slightly, but valid deteriorated meaningfully and test did not improve. Interleaving is not helping this filtered branch. +Next: Stay with the non-interleaved filtered short sleeve; the next branch should tune the filtered after-close short only if we need more test return. -### 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 36.2) -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-0058 (2026-03-17) — pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10 +Hypothesis: Interleaving the filtered mixed-sleeve portfolio might recover some of the earlier test strength without sacrificing the new train lift from the after-close gap gate. +Verdict: **WORSE** (SQS 50.6) +Reasoning: Test held steady, but valid return and drawdown got materially worse while train did not improve. The gap filter works better with raw global-score ranking than with interleaving. +Next: Keep global-score selection and treat step62/63 as the active return-first branch. -### 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 29.1) -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. +### IMP-0057 (2026-03-17) — pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12 +Hypothesis: A stricter 12% negative gap gate on after-close shorts may further improve the mixed-sleeve portfolio by concentrating the short sleeve into only the sharpest downside reactions. +Verdict: **BETTER** (SQS 38.2) +Reasoning: The 12% gate slightly improved train and valid versus step62 while keeping test near 0.95% with lower drawdown than the old mixed-sleeve base. This is the strongest return-first variant so far. +Next: Combine the after-close gap gate with interleaved max4 sleeve selection to test whether test return can recover toward the 1.0%+ level. diff --git a/journal/attention_probe_20260317.md b/journal/attention_probe_20260317.md new file mode 100644 index 0000000..5b96cff --- /dev/null +++ b/journal/attention_probe_20260317.md @@ -0,0 +1,54 @@ +# Free Attention Probe + +Date: 2026-03-17 + +Goal: +- Verify that free historical attention/news proxies can be fetched for old events. +- Run a short-sample sanity check before building anything into Stock Oracle. + +Sources tested: +- `Wikimedia pageviews` for historical attention spikes +- `GDELT Doc API` for spot-check historical news article counts + +Method: +- Start from `data/datasets/snapshots/midcap-filtered/test.parquet` +- Restrict to `earnings_release` +- Join `ticker` / `issuer_name` from local Postgres +- Keep only names that are not obvious `{TICKER} Corporation` placeholders +- Resolve a Wikipedia article title from issuer name +- Compute `pageview_spike = event_day_views / median(last_10_pre_event_views)` +- Compare against signed continuation: + - `signed_cont_3d = sign(reaction_day_return) * fwd_return_3d` + - `signed_cont_5d = sign(reaction_day_return) * fwd_return_5d` + +Probe run: +- command: + - `python -m apps.tools.free_attention_probe --limit 30 --gdelt-limit 5` +- output csv: + - `data/research/free_attention_probe_test_sample.csv` + +Results: +- Sampled 30 non-generic test-split earnings events +- Resolved 22 rows with usable Wikipedia pageviews +- Raw sample: + - median pageview spike `1.226x` + - high-spike group signed 3D continuation mean `+0.0672` + - low-spike group signed 3D continuation mean `+0.0186` + - high-spike group signed 5D continuation mean `+0.0693` + - low-spike group signed 5D continuation mean `+0.0458` + - corr(pageview_spike, signed_cont_3d) `+0.2646` + - corr(pageview_spike, signed_cont_5d) `-0.1075` +- After filtering to higher-confidence mappings and excluding obviously bad article matches, the broad signal became inconclusive. +- Negative-reaction subset looked more promising than the full sample on 5D continuation, but sample size was too small to trust. + +GDELT spot-check: +- Historical fetch works. +- Exact-phrase matching is fragile without a better company-name resolver. +- In the small spot-check, valid 3-day article counts were observed for some names, but coverage was too patchy for immediate use as-is. + +Conclusion: +- Free historical attention/news data is usable for short-window research. +- `Wikimedia pageviews` is immediately practical. +- `GDELT` is viable, but only after better issuer-name normalization and article/entity resolution. +- Current evidence does not justify adding raw pageview spike directly to strategy scoring yet. +- The most promising next test is a conditional filter on downside earnings reactions, not a global attention overlay. diff --git a/journal/experiment_registry.json b/journal/experiment_registry.json index 422a617..23903cf 100644 --- a/journal/experiment_registry.json +++ b/journal/experiment_registry.json @@ -1,5 +1,140 @@ { "entries": [ + { + "entry_id": "IMP-0048", + "experiment_name": "pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25", + "sqs_score": 51.3, + "sqs_v2_score": 88.3, + "promotion_score": 89.1, + "unified_score": 51.3, + "profit_factor": 4.034651941086868, + "total_return_pct": 1.0723281131463445, + "win_rate": 0.8, + "sharpe_ratio": 3.399127342667149, + "max_drawdown_pct": 0.24183984749880666, + "trade_count": 20, + "avg_gross_exposure_pct": 2.4654753372509486, + "avg_net_exposure_pct": -1.477511185545206, + "days_in_market_pct": 51.06382978723404, + "valid_profit_factor": 4.8198685654623254, + "valid_total_return_pct": 1.9154249967419892, + "valid_win_rate": 0.6785714285714286, + "valid_sharpe_ratio": 3.973180662878923, + "valid_max_drawdown_pct": 0.6079718599673478, + "valid_trade_count": 28, + "valid_avg_gross_exposure_pct": 4.161026862046388, + "valid_avg_net_exposure_pct": -2.684188461871084, + "valid_days_in_market_pct": 56.14035087719298, + "timestamp": "2026-03-17T10:10:07.544224+00:00" + }, + { + "entry_id": "IMP-0056", + "experiment_name": "pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10", + "sqs_score": 50.9, + "sqs_v2_score": 87.7, + "promotion_score": 88.9, + "unified_score": 50.9, + "profit_factor": 3.693559917731055, + "total_return_pct": 0.9821623910714115, + "win_rate": 0.8, + "sharpe_ratio": 3.3957804338520745, + "max_drawdown_pct": 0.23230116536263104, + "trade_count": 20, + "avg_gross_exposure_pct": 2.4512706589430846, + "avg_net_exposure_pct": -1.4631851279456756, + "days_in_market_pct": 48.93617021276596, + "valid_profit_factor": 4.640144626555104, + "valid_total_return_pct": 1.7785589226570302, + "valid_win_rate": 0.6923076923076923, + "valid_sharpe_ratio": 4.453025001679815, + "valid_max_drawdown_pct": 0.394474954823559, + "valid_trade_count": 26, + "valid_avg_gross_exposure_pct": 3.5028084000751822, + "valid_avg_net_exposure_pct": -2.028975630946834, + "valid_days_in_market_pct": 49.122807017543856, + "timestamp": "2026-03-17T10:20:47.944163+00:00" + }, + { + "entry_id": "IMP-0060", + "experiment_name": "pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12", + "sqs_score": 50.9, + "sqs_v2_score": 87.7, + "promotion_score": 88.9, + "unified_score": 50.9, + "profit_factor": 3.693559917731055, + "total_return_pct": 0.9821623910714115, + "win_rate": 0.8, + "sharpe_ratio": 3.3957804338520745, + "max_drawdown_pct": 0.23230116536263104, + "trade_count": 20, + "avg_gross_exposure_pct": 2.4512706589430846, + "avg_net_exposure_pct": -1.4631851279456756, + "days_in_market_pct": 48.93617021276596, + "valid_profit_factor": 4.640144626555104, + "valid_total_return_pct": 1.7785589226570302, + "valid_win_rate": 0.6923076923076923, + "valid_sharpe_ratio": 4.453025001679815, + "valid_max_drawdown_pct": 0.394474954823559, + "valid_trade_count": 26, + "valid_avg_gross_exposure_pct": 3.5028084000751822, + "valid_avg_net_exposure_pct": -2.028975630946834, + "valid_days_in_market_pct": 49.122807017543856, + "timestamp": "2026-03-17T10:26:25.356022+00:00" + }, + { + "entry_id": "IMP-0058", + "experiment_name": "pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10", + "sqs_score": 50.6, + "sqs_v2_score": 87.7, + "promotion_score": 88.2, + "unified_score": 50.6, + "profit_factor": 3.693559917731055, + "total_return_pct": 0.9821623910714115, + "win_rate": 0.8, + "sharpe_ratio": 3.3957804338520745, + "max_drawdown_pct": 0.23230116536263104, + "trade_count": 20, + "avg_gross_exposure_pct": 2.4512706589430846, + "avg_net_exposure_pct": -1.4631851279456756, + "days_in_market_pct": 48.93617021276596, + "valid_profit_factor": 4.330878953751208, + "valid_total_return_pct": 1.675907371790352, + "valid_win_rate": 0.6538461538461539, + "valid_sharpe_ratio": 3.7033259316462277, + "valid_max_drawdown_pct": 0.608675665764912, + "valid_trade_count": 26, + "valid_avg_gross_exposure_pct": 3.7950928397401578, + "valid_avg_net_exposure_pct": -2.3215832255311892, + "valid_days_in_market_pct": 49.122807017543856, + "timestamp": "2026-03-17T10:22:41.050471+00:00" + }, + { + "entry_id": "IMP-0049", + "experiment_name": "pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25", + "sqs_score": 49.8, + "sqs_v2_score": 87.1, + "promotion_score": 89.1, + "unified_score": 49.8, + "profit_factor": 3.4044506466943907, + "total_return_pct": 1.0338260227821447, + "win_rate": 0.782608695652174, + "sharpe_ratio": 3.1121570919759436, + "max_drawdown_pct": 0.24257912061402945, + "trade_count": 23, + "avg_gross_exposure_pct": 3.034569947217148, + "avg_net_exposure_pct": -2.0461795297656566, + "days_in_market_pct": 53.191489361702125, + "valid_profit_factor": 4.621364494548783, + "valid_total_return_pct": 1.9321698387879296, + "valid_win_rate": 0.7142857142857143, + "valid_sharpe_ratio": 4.486982260533271, + "valid_max_drawdown_pct": 0.29678396454155964, + "valid_trade_count": 28, + "valid_avg_gross_exposure_pct": 3.8320562275604266, + "valid_avg_net_exposure_pct": -2.3571340234511924, + "valid_days_in_market_pct": 56.14035087719298, + "timestamp": "2026-03-17T10:10:07.544223+00:00" + }, { "entry_id": "IMP-0046", "experiment_name": "pead_midcap_step52_short_core_macro_block_crashcap_gap10", @@ -54,6 +189,87 @@ "valid_days_in_market_pct": 51.78571428571429, "timestamp": "2026-03-17T07:54:26.967747+00:00" }, + { + "entry_id": "IMP-0051", + "experiment_name": "pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25", + "sqs_score": 46.6, + "sqs_v2_score": 86.3, + "promotion_score": 87.7, + "unified_score": 46.6, + "profit_factor": 2.7627983723004212, + "total_return_pct": 0.9267893134084734, + "win_rate": 0.7391304347826086, + "sharpe_ratio": 2.781969702284716, + "max_drawdown_pct": 0.24257912061402945, + "trade_count": 23, + "avg_gross_exposure_pct": 3.172184023694837, + "avg_net_exposure_pct": -1.2529216711302178, + "days_in_market_pct": 53.191489361702125, + "valid_profit_factor": 2.885213921540522, + "valid_total_return_pct": 1.6066212170483485, + "valid_win_rate": 0.6896551724137931, + "valid_sharpe_ratio": 3.6006506236711573, + "valid_max_drawdown_pct": 0.4958133428812383, + "valid_trade_count": 29, + "valid_avg_gross_exposure_pct": 4.030999519248784, + "valid_avg_net_exposure_pct": -1.9829383669087164, + "valid_days_in_market_pct": 56.14035087719298, + "timestamp": "2026-03-17T10:13:47.820620+00:00" + }, + { + "entry_id": "IMP-0050", + "experiment_name": "pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12", + "sqs_score": 46.5, + "sqs_v2_score": 86.1, + "promotion_score": 88.6, + "unified_score": 46.5, + "profit_factor": 2.991176064691692, + "total_return_pct": 0.856133035813924, + "win_rate": 0.7727272727272727, + "sharpe_ratio": 2.4909364067019513, + "max_drawdown_pct": 0.3359030771970047, + "trade_count": 22, + "avg_gross_exposure_pct": 2.921981872114122, + "avg_net_exposure_pct": -2.1517863691688386, + "days_in_market_pct": 53.191489361702125, + "valid_profit_factor": 6.099913503899416, + "valid_total_return_pct": 1.9918550334882748, + "valid_win_rate": 0.7307692307692307, + "valid_sharpe_ratio": 4.666899835993297, + "valid_max_drawdown_pct": 0.21797225956805316, + "valid_trade_count": 26, + "valid_avg_gross_exposure_pct": 3.637408212559115, + "valid_avg_net_exposure_pct": -2.5520608569407015, + "valid_days_in_market_pct": 56.14035087719298, + "timestamp": "2026-03-17T10:10:07.544222+00:00" + }, + { + "entry_id": "IMP-0052", + "experiment_name": "pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4", + "sqs_score": 46.5, + "sqs_v2_score": 86.3, + "promotion_score": 88.6, + "unified_score": 46.5, + "profit_factor": 2.664801821772631, + "total_return_pct": 0.91437274196824, + "win_rate": 0.75, + "sharpe_ratio": 2.893891891447354, + "max_drawdown_pct": 0.24257912061402945, + "trade_count": 24, + "avg_gross_exposure_pct": 3.169455379761105, + "avg_net_exposure_pct": -2.1804906875349683, + "days_in_market_pct": 53.191489361702125, + "valid_profit_factor": 4.4424890742925465, + "valid_total_return_pct": 1.9106867186089511, + "valid_win_rate": 0.6896551724137931, + "valid_sharpe_ratio": 4.511177385736093, + "valid_max_drawdown_pct": 0.3179173247540872, + "valid_trade_count": 29, + "valid_avg_gross_exposure_pct": 3.944739633758862, + "valid_avg_net_exposure_pct": -2.4700155661285508, + "valid_days_in_market_pct": 56.14035087719298, + "timestamp": "2026-03-17T10:13:47.821673+00:00" + }, { "entry_id": "IMP-0040", "experiment_name": "pead_midcap_step46_short_core_macro_block_acshort12", @@ -189,6 +405,87 @@ "valid_days_in_market_pct": 56.14035087719298, "timestamp": "2026-03-17T07:54:28.762757+00:00" }, + { + "entry_id": "IMP-0059", + "experiment_name": "pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12", + "sqs_score": 38.3, + "sqs_v2_score": 44.4, + "promotion_score": 69.0, + "unified_score": 38.3, + "profit_factor": 3.6038727896341176, + "total_return_pct": 0.9494594526291912, + "win_rate": 0.7894736842105263, + "sharpe_ratio": 3.2438715317612528, + "max_drawdown_pct": 0.23230116536263104, + "trade_count": 19, + "avg_gross_exposure_pct": 1.9049388001836767, + "avg_net_exposure_pct": -0.9171395725318859, + "days_in_market_pct": 38.297872340425535, + "valid_profit_factor": 4.557936520788986, + "valid_total_return_pct": 1.5351686099939834, + "valid_win_rate": 0.6666666666666666, + "valid_sharpe_ratio": 3.9755987852769445, + "valid_max_drawdown_pct": 0.5303323192221483, + "valid_trade_count": 24, + "valid_avg_gross_exposure_pct": 3.4173394039982, + "valid_avg_net_exposure_pct": -1.9446456373142142, + "valid_days_in_market_pct": 47.368421052631575, + "timestamp": "2026-03-17T10:22:41.560235+00:00" + }, + { + "entry_id": "IMP-0057", + "experiment_name": "pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12", + "sqs_score": 38.2, + "sqs_v2_score": 44.4, + "promotion_score": 69.6, + "unified_score": 38.2, + "profit_factor": 3.6038727896341176, + "total_return_pct": 0.9494594526291912, + "win_rate": 0.7894736842105263, + "sharpe_ratio": 3.2438715317612528, + "max_drawdown_pct": 0.23230116536263104, + "trade_count": 19, + "avg_gross_exposure_pct": 1.9049388001836767, + "avg_net_exposure_pct": -0.9171395725318859, + "days_in_market_pct": 38.297872340425535, + "valid_profit_factor": 5.270593419113103, + "valid_total_return_pct": 1.7898083823081543, + "valid_win_rate": 0.7083333333333334, + "valid_sharpe_ratio": 4.878106921358311, + "valid_max_drawdown_pct": 0.31708063303845857, + "valid_trade_count": 24, + "valid_avg_gross_exposure_pct": 3.0974719922506315, + "valid_avg_net_exposure_pct": -1.626276620699805, + "valid_days_in_market_pct": 45.614035087719294, + "timestamp": "2026-03-17T10:20:48.356015+00:00" + }, + { + "entry_id": "IMP-0061", + "experiment_name": "pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14", + "sqs_score": 38.2, + "sqs_v2_score": 44.4, + "promotion_score": 69.5, + "unified_score": 38.2, + "profit_factor": 3.6038727896341176, + "total_return_pct": 0.9494594526291912, + "win_rate": 0.7894736842105263, + "sharpe_ratio": 3.2438715317612528, + "max_drawdown_pct": 0.23230116536263104, + "trade_count": 19, + "avg_gross_exposure_pct": 1.9049388001836767, + "avg_net_exposure_pct": -0.9171395725318859, + "days_in_market_pct": 38.297872340425535, + "valid_profit_factor": 5.331499198553463, + "valid_total_return_pct": 1.8247351197415846, + "valid_win_rate": 0.72, + "valid_sharpe_ratio": 4.44451943654, + "valid_max_drawdown_pct": 0.480176430930607, + "valid_trade_count": 25, + "valid_avg_gross_exposure_pct": 3.1678311806010018, + "valid_avg_net_exposure_pct": -1.6948561495104018, + "valid_days_in_market_pct": 47.368421052631575, + "timestamp": "2026-03-17T10:26:25.840092+00:00" + }, { "entry_id": "IMP-0044", "experiment_name": "pead_midcap_step50_same_day_short_long_macro_block", @@ -325,7 +622,88 @@ "timestamp": "2026-03-17T06:59:09.622681+00:00" }, { - "entry_id": "IMP-0037", + "entry_id": "IMP-0053", + "experiment_name": "pead_midcap_step59_same_day_only_max4_longtrend25", + "sqs_score": 33.2, + "sqs_v2_score": 42.7, + "promotion_score": 68.6, + "unified_score": 33.2, + "profit_factor": 2.6072978977047137, + "total_return_pct": 0.5860747837766976, + "win_rate": 0.6923076923076923, + "sharpe_ratio": 2.308425640364089, + "max_drawdown_pct": 0.31275465094584215, + "trade_count": 13, + "avg_gross_exposure_pct": 1.4491267702359076, + "avg_net_exposure_pct": -0.4649535073354661, + "days_in_market_pct": 31.914893617021278, + "valid_profit_factor": 6.627861685804674, + "valid_total_return_pct": 1.9039976324784367, + "valid_win_rate": 0.7272727272727273, + "valid_sharpe_ratio": 4.451621602089021, + "valid_max_drawdown_pct": 0.44666468803527526, + "valid_trade_count": 22, + "valid_avg_gross_exposure_pct": 3.8681902002325703, + "valid_avg_net_exposure_pct": -2.3913045273063496, + "valid_days_in_market_pct": 42.10526315789473, + "timestamp": "2026-03-17T10:18:37.790830+00:00" + }, + { + "entry_id": "IMP-0054", + "experiment_name": "pead_midcap_step60_same_day_only_interleave_max4_longtrend25", + "sqs_score": 33.2, + "sqs_v2_score": 42.7, + "promotion_score": 68.6, + "unified_score": 33.2, + "profit_factor": 2.6072978977047137, + "total_return_pct": 0.5860747837766976, + "win_rate": 0.6923076923076923, + "sharpe_ratio": 2.308425640364089, + "max_drawdown_pct": 0.31275465094584215, + "trade_count": 13, + "avg_gross_exposure_pct": 1.4491267702359076, + "avg_net_exposure_pct": -0.4649535073354661, + "days_in_market_pct": 31.914893617021278, + "valid_profit_factor": 6.627861685804674, + "valid_total_return_pct": 1.9039976324784367, + "valid_win_rate": 0.7272727272727273, + "valid_sharpe_ratio": 4.451621602089021, + "valid_max_drawdown_pct": 0.44666468803527526, + "valid_trade_count": 22, + "valid_avg_gross_exposure_pct": 3.8681902002325703, + "valid_avg_net_exposure_pct": -2.3913045273063496, + "valid_days_in_market_pct": 42.10526315789473, + "timestamp": "2026-03-17T10:18:38.178487+00:00" + }, + { + "entry_id": "IMP-0055", + "experiment_name": "pead_midcap_step61_same_day_only_max5_longtrend25", + "sqs_score": 33.2, + "sqs_v2_score": 42.7, + "promotion_score": 68.6, + "unified_score": 33.2, + "profit_factor": 2.6072978977047137, + "total_return_pct": 0.5860747837766976, + "win_rate": 0.6923076923076923, + "sharpe_ratio": 2.308425640364089, + "max_drawdown_pct": 0.31275465094584215, + "trade_count": 13, + "avg_gross_exposure_pct": 1.4491267702359076, + "avg_net_exposure_pct": -0.4649535073354661, + "days_in_market_pct": 31.914893617021278, + "valid_profit_factor": 6.627861685804674, + "valid_total_return_pct": 1.9039976324784367, + "valid_win_rate": 0.7272727272727273, + "valid_sharpe_ratio": 4.451621602089021, + "valid_max_drawdown_pct": 0.44666468803527526, + "valid_trade_count": 22, + "valid_avg_gross_exposure_pct": 3.8681902002325703, + "valid_avg_net_exposure_pct": -2.3913045273063496, + "valid_days_in_market_pct": 42.10526315789473, + "timestamp": "2026-03-17T10:18:38.591808+00:00" + }, + { + "entry_id": "IMP-0036", "experiment_name": "pead_midcap_step43_short_core_sdlong25", "sqs_score": 31.7, "sqs_v2_score": 78.2, @@ -406,7 +784,7 @@ "timestamp": "2026-03-17T07:11:20.119143+00:00" }, { - "entry_id": "IMP-0036", + "entry_id": "IMP-0034", "experiment_name": "pead_midcap_step42_short_core_only", "sqs_score": 30.5, "sqs_v2_score": 67.1, @@ -595,7 +973,7 @@ "timestamp": "2026-03-17T02:17:02.185037+00:00" }, { - "entry_id": "IMP-0034", + "entry_id": "IMP-0037", "experiment_name": "pead_midcap_step40_short_core_sdlong12", "sqs_score": 28.6, "sqs_v2_score": 81.4, @@ -1270,5 +1648,5 @@ "timestamp": "2026-03-16T23:01:55.163461+00:00" } ], - "updated_at": "2026-03-17T09:17:27.726326+00:00" + "updated_at": "2026-03-17T10:37:12.332837+00:00" } \ No newline at end of file diff --git a/journal/improvement_journal.jsonl b/journal/improvement_journal.jsonl index 8c0efc2..b6a48a4 100644 --- a/journal/improvement_journal.jsonl +++ b/journal/improvement_journal.jsonl @@ -31,10 +31,10 @@ {"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-0034","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-0036","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-0037","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_20260317081817694940_42f8ea0e","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,"avg_gross_exposure_pct":4.823763728554508,"avg_net_exposure_pct":-1.952624774981557,"days_in_market_pct":73.68421052631578},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081807057141_42f8ea0e","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,"avg_gross_exposure_pct":4.453478837236966,"avg_net_exposure_pct":-1.5519032472252936,"days_in_market_pct":72.3404255319149},"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"],"sqs_v2_score":86.9,"sqs_v2_breakdown":{"profitability":85.2,"risk":100.0,"consistency":86.5,"robustness":76.6,"capital_efficiency":70.8}} {"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"]} @@ -45,3 +45,17 @@ {"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_20260317081833666987_5039805d","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,"avg_gross_exposure_pct":4.09489843643577,"avg_net_exposure_pct":-1.9273432556417505,"days_in_market_pct":57.89473684210527},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081834927778_5039805d","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,"avg_gross_exposure_pct":2.2928950819181733,"avg_net_exposure_pct":-0.8189811228558067,"days_in_market_pct":42.5531914893617},"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"],"sqs_v2_score":89.6,"sqs_v2_breakdown":{"profitability":85.0,"risk":100.0,"consistency":95.8,"robustness":56.7,"capital_efficiency":100.0}} {"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_20260317081834927839_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,"avg_gross_exposure_pct":3.5838805580978437,"avg_net_exposure_pct":-2.593156236925373,"days_in_market_pct":56.14035087719298},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317081833658080_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,"avg_gross_exposure_pct":2.858350680305373,"avg_net_exposure_pct":-2.222595187645864,"days_in_market_pct":53.191489361702125},"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"],"sqs_v2_score":87.2,"sqs_v2_breakdown":{"profitability":84.0,"risk":100.0,"consistency":95.8,"robustness":56.7,"capital_efficiency":79.5}} {"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"]} +{"entry_id":"IMP-0048","timestamp":"2026-03-17T10:10:07.544224+00:00","experiment_name":"pead_midcap_step56_short_core_macro_block_crashcap_gap10_interleave_longtrend25","hypothesis":"Interleaving sleeves instead of raw global-score sorting should help the long trend overlay get allocated earlier and improve realized returns.","config_delta":{"base_experiment":"pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100427495678_c4cb6228","trade_count":191,"profit_factor":1.31467289884978,"total_return_pct":3.000532477442888,"win_rate":0.5078534031413613,"max_drawdown_pct":1.1246540668445304,"sharpe_ratio":0.682160145042935,"monthly_win_rate":0.6538461538461539,"equity_curve_r_squared":0.7049623155490459,"avg_gross_exposure_pct":2.1440841165534352,"avg_net_exposure_pct":-0.741975012003786,"days_in_market_pct":28.169014084507044},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100431529104_c4cb6228","trade_count":28,"profit_factor":4.8198685654623254,"total_return_pct":1.9154249967419892,"win_rate":0.6785714285714286,"max_drawdown_pct":0.6079718599673478,"sharpe_ratio":3.973180662878923,"monthly_win_rate":1.0,"equity_curve_r_squared":0.5809751515505278,"avg_gross_exposure_pct":4.161026862046388,"avg_net_exposure_pct":-2.684188461871084,"days_in_market_pct":56.14035087719298},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100434744381_c4cb6228","trade_count":20,"profit_factor":4.034651941086868,"total_return_pct":1.0723281131463445,"win_rate":0.8,"max_drawdown_pct":0.24183984749880666,"sharpe_ratio":3.399127342667149,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8859682722741977,"avg_gross_exposure_pct":2.4654753372509486,"avg_net_exposure_pct":-1.477511185545206,"days_in_market_pct":51.06382978723404}},"sqs_score":51.3,"sqs_breakdown":{"valid_quality":82.6,"test_quality":79.4,"floor_quality":79.4,"gap_quality":100.0},"sqs_v2_score":88.3,"sqs_v2_breakdown":{"profitability":84.3,"risk":100.0,"consistency":95.8,"robustness":55.6,"capital_efficiency":90.9},"promotion_score":89.1,"promotion_breakdown":{"valid_quality":89.8,"test_quality":88.3,"floor_quality":88.3},"unified_score":51.3,"unified_breakdown":{"valid_quality":82.6,"test_quality":79.4,"floor_quality":79.4,"gap_quality":100.0},"verdict":"neutral","verdict_reasoning":"Interleaving lifted test PF and return slightly, but train remained below step52 and valid drawdown worsened, so the gain was too narrow.","next_direction":"Keep the stronger same-day long sleeve, but try opening one more candidate slot per day.","tags":["pead","midcap","step56","short","core","macro","block","crashcap","gap10","interleave","longtrend25"]} +{"entry_id":"IMP-0049","timestamp":"2026-03-17T10:10:07.544223+00:00","experiment_name":"pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25","hypothesis":"If the same-day long trend sleeve is real alpha, raising its budget from 12.5% to 25% should lift valid/test returns while keeping drawdown contained.","config_delta":{"base_experiment":"pead_midcap_step52_short_core_macro_block_crashcap_gap10","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100414827938_b2c1159a","trade_count":208,"profit_factor":1.3026092902212059,"total_return_pct":3.197582862252573,"win_rate":0.5048076923076923,"max_drawdown_pct":1.3030593106539847,"sharpe_ratio":0.6650887543632845,"monthly_win_rate":0.6538461538461539,"equity_curve_r_squared":0.6942797350314213,"avg_gross_exposure_pct":2.2965079311515697,"avg_net_exposure_pct":-1.068925327205924,"days_in_market_pct":28.38569880823402},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100418563512_b2c1159a","trade_count":28,"profit_factor":4.621364494548783,"total_return_pct":1.9321698387879296,"win_rate":0.7142857142857143,"max_drawdown_pct":0.29678396454155964,"sharpe_ratio":4.486982260533271,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6351890668895267,"avg_gross_exposure_pct":3.8320562275604266,"avg_net_exposure_pct":-2.3571340234511924,"days_in_market_pct":56.14035087719298},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100421770808_b2c1159a","trade_count":23,"profit_factor":3.4044506466943907,"total_return_pct":1.0338260227821447,"win_rate":0.782608695652174,"max_drawdown_pct":0.24257912061402945,"sharpe_ratio":3.1121570919759436,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8494393946803189,"avg_gross_exposure_pct":3.034569947217148,"avg_net_exposure_pct":-2.0461795297656566,"days_in_market_pct":53.191489361702125}},"sqs_score":49.8,"sqs_breakdown":{"valid_quality":83.6,"test_quality":76.1,"floor_quality":76.1,"gap_quality":91.7},"sqs_v2_score":87.1,"sqs_v2_breakdown":{"profitability":84.1,"risk":100.0,"consistency":95.8,"robustness":57.2,"capital_efficiency":77.7},"promotion_score":89.1,"promotion_breakdown":{"valid_quality":90.7,"test_quality":87.1,"floor_quality":87.1},"unified_score":49.8,"unified_breakdown":{"valid_quality":83.6,"test_quality":76.1,"floor_quality":76.1,"gap_quality":91.7},"verdict":"neutral","verdict_reasoning":"This was the best balanced trend-hold variant: valid and test returns edged above step52, but train return stayed below baseline, so it did not clear a full all-split improvement bar.","next_direction":"Test whether selection crowding is the bottleneck by interleaving sleeves or allowing one extra daily slot.","tags":["pead","midcap","step55","short","core","macro","block","crashcap","gap10","longtrend12","sdlong25"]} +{"entry_id":"IMP-0050","timestamp":"2026-03-17T10:10:07.544222+00:00","experiment_name":"pead_midcap_step54_short_core_macro_block_crashcap_gap10_longtrend12","hypothesis":"Same-day long close-entry sleeve with 12-day trend hold should improve valid/test without materially hurting the short-core base.","config_delta":{"base_experiment":"pead_midcap_step52_short_core_macro_block_crashcap_gap10","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100332469191_e1ab5792","trade_count":201,"profit_factor":1.27971446872751,"total_return_pct":2.88150490621911,"win_rate":0.5024875621890548,"max_drawdown_pct":1.4049225155953227,"sharpe_ratio":0.6136649773412598,"monthly_win_rate":0.6923076923076923,"equity_curve_r_squared":0.7371903755424146,"avg_gross_exposure_pct":2.1995436175888323,"avg_net_exposure_pct":-1.2043129551276188,"days_in_market_pct":27.627302275189596},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100351662584_e1ab5792","trade_count":26,"profit_factor":6.099913503899416,"total_return_pct":1.9918550334882748,"win_rate":0.7307692307692307,"max_drawdown_pct":0.21797225956805316,"sharpe_ratio":4.666899835993297,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6469910274326361,"avg_gross_exposure_pct":3.637408212559115,"avg_net_exposure_pct":-2.5520608569407015,"days_in_market_pct":56.14035087719298},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100408811304_e1ab5792","trade_count":22,"profit_factor":2.991176064691692,"total_return_pct":0.856133035813924,"win_rate":0.7727272727272727,"max_drawdown_pct":0.3359030771970047,"sharpe_ratio":2.4909364067019513,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8042967657946622,"avg_gross_exposure_pct":2.921981872114122,"avg_net_exposure_pct":-2.1517863691688386,"days_in_market_pct":53.191489361702125}},"sqs_score":46.5,"sqs_breakdown":{"valid_quality":83.6,"test_quality":70.9,"floor_quality":70.9,"gap_quality":74.3},"sqs_v2_score":86.1,"sqs_v2_breakdown":{"profitability":83.4,"risk":100.0,"consistency":95.8,"robustness":56.7,"capital_efficiency":71.0},"promotion_score":88.6,"promotion_breakdown":{"valid_quality":90.7,"test_quality":86.1,"floor_quality":86.1},"unified_score":46.5,"unified_breakdown":{"valid_quality":83.6,"test_quality":70.9,"floor_quality":70.9,"gap_quality":74.3},"verdict":"neutral","verdict_reasoning":"Long trend sleeve improved valid return and PF, but train and test returns fell versus step52. Improvement was not broad enough across all three splits.","next_direction":"Increase same-day long sleeve budget and test whether the trend hold needs more capital to matter.","tags":["pead","midcap","step54","short","core","macro","block","crashcap","gap10","longtrend12"]} +{"entry_id":"IMP-0051","timestamp":"2026-03-17T10:13:47.820620+00:00","experiment_name":"pead_midcap_step58_short_core_macro_block_crashcap_gap7_longtrend12_sdlong25","hypothesis":"Loosening the same-day long gap filter from 10% to 7% may broaden the trend-hold sleeve enough to lift absolute return across splits.","config_delta":{"base_experiment":"pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100701045116_89da3e85","trade_count":217,"profit_factor":1.1836228679446417,"total_return_pct":2.1640217345169948,"win_rate":0.511520737327189,"max_drawdown_pct":1.3943515116639358,"sharpe_ratio":0.41689043269693754,"monthly_win_rate":0.6071428571428571,"equity_curve_r_squared":0.24452603324168182,"avg_gross_exposure_pct":2.48302270403254,"avg_net_exposure_pct":-0.9240632756453898,"days_in_market_pct":29.577464788732392},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100705201243_89da3e85","trade_count":29,"profit_factor":2.885213921540522,"total_return_pct":1.6066212170483485,"win_rate":0.6896551724137931,"max_drawdown_pct":0.4958133428812383,"sharpe_ratio":3.6006506236711573,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6323181349051988,"avg_gross_exposure_pct":4.030999519248784,"avg_net_exposure_pct":-1.9829383669087164,"days_in_market_pct":56.14035087719298},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100708488141_89da3e85","trade_count":23,"profit_factor":2.7627983723004212,"total_return_pct":0.9267893134084734,"win_rate":0.7391304347826086,"max_drawdown_pct":0.24257912061402945,"sharpe_ratio":2.781969702284716,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8290340590935003,"avg_gross_exposure_pct":3.172184023694837,"avg_net_exposure_pct":-1.2529216711302178,"days_in_market_pct":53.191489361702125}},"sqs_score":46.6,"sqs_breakdown":{"valid_quality":81.6,"test_quality":71.0,"floor_quality":71.0,"gap_quality":81.3},"sqs_v2_score":86.3,"sqs_v2_breakdown":{"profitability":83.7,"risk":100.0,"consistency":95.8,"robustness":57.2,"capital_efficiency":70.9},"promotion_score":87.7,"promotion_breakdown":{"valid_quality":88.8,"test_quality":86.3,"floor_quality":86.3},"unified_score":46.6,"unified_breakdown":{"valid_quality":81.6,"test_quality":71.0,"floor_quality":71.0,"gap_quality":81.3},"verdict":"worse","verdict_reasoning":"Broadening the long sleeve diluted quality. Train, valid, and test all weakened versus the 10% gap version, so the extra breadth was not productive.","next_direction":"Stay with the tighter 10% gap gate and test whether after-close short should be removed from the max4 portfolio.","tags":["pead","midcap","step58","short","core","macro","block","crashcap","gap7","longtrend12","sdlong25"]} +{"entry_id":"IMP-0052","timestamp":"2026-03-17T10:13:47.821673+00:00","experiment_name":"pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4","hypothesis":"Adding a fourth daily slot should let the positive same-day long trend sleeve coexist with the short-core sleeves and lift total return, especially in train.","config_delta":{"base_experiment":"pead_midcap_step55_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100647683228_d591afad","trade_count":214,"profit_factor":1.3583319773998321,"total_return_pct":3.8172175024118773,"win_rate":0.5186915887850467,"max_drawdown_pct":1.3295910930658135,"sharpe_ratio":0.7580236981795051,"monthly_win_rate":0.6538461538461539,"equity_curve_r_squared":0.685718632923443,"avg_gross_exposure_pct":2.3612289576418535,"avg_net_exposure_pct":-1.001836917901861,"days_in_market_pct":28.38569880823402},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100651417016_d591afad","trade_count":29,"profit_factor":4.4424890742925465,"total_return_pct":1.9106867186089511,"win_rate":0.6896551724137931,"max_drawdown_pct":0.3179173247540872,"sharpe_ratio":4.511177385736093,"monthly_win_rate":1.0,"equity_curve_r_squared":0.625777909809139,"avg_gross_exposure_pct":3.944739633758862,"avg_net_exposure_pct":-2.4700155661285508,"days_in_market_pct":56.14035087719298},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317100654743633_d591afad","trade_count":24,"profit_factor":2.664801821772631,"total_return_pct":0.91437274196824,"win_rate":0.75,"max_drawdown_pct":0.24257912061402945,"sharpe_ratio":2.893891891447354,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8412595666380652,"avg_gross_exposure_pct":3.169455379761105,"avg_net_exposure_pct":-2.1804906875349683,"days_in_market_pct":53.191489361702125}},"sqs_score":46.5,"sqs_breakdown":{"valid_quality":83.5,"test_quality":70.9,"floor_quality":70.9,"gap_quality":74.7},"sqs_v2_score":86.3,"sqs_v2_breakdown":{"profitability":83.7,"risk":100.0,"consistency":95.8,"robustness":57.8,"capital_efficiency":70.4},"promotion_score":88.6,"promotion_breakdown":{"valid_quality":90.4,"test_quality":86.3,"floor_quality":86.3},"unified_score":46.5,"unified_breakdown":{"valid_quality":83.5,"test_quality":70.9,"floor_quality":70.9,"gap_quality":74.7},"verdict":"neutral","verdict_reasoning":"This was the first long-trend variant to beat step52 on train return, but valid and test softened versus the best balanced variants, so it improved absolute return without clearing a robust all-split bar.","next_direction":"Keep max4, then remove or sharply reduce after-close short to see whether it is diluting the higher-conviction sleeves.","tags":["pead","midcap","step57","short","core","macro","block","crashcap","gap10","longtrend12","sdlong25","max4"]} +{"entry_id":"IMP-0053","timestamp":"2026-03-17T10:18:37.790830+00:00","experiment_name":"pead_midcap_step59_same_day_only_max4_longtrend25","hypothesis":"If after-close short is diluting the portfolio, a same-day-only book should lift train return while keeping the positive long trend overlay intact.","config_delta":{"base_experiment":"pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101616130067_4ce1573f","trade_count":124,"profit_factor":1.6549151417942642,"total_return_pct":3.4598346586282274,"win_rate":0.5403225806451613,"max_drawdown_pct":1.3037083252920614,"sharpe_ratio":0.8632124891148151,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.6337366863338719,"avg_gross_exposure_pct":1.4618927749473103,"avg_net_exposure_pct":0.040921478947221035,"days_in_market_pct":19.176598049837487},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101626096198_4ce1573f","trade_count":22,"profit_factor":6.627861685804674,"total_return_pct":1.9039976324784367,"win_rate":0.7272727272727273,"max_drawdown_pct":0.44666468803527526,"sharpe_ratio":4.451621602089021,"monthly_win_rate":1.0,"equity_curve_r_squared":0.5794666301732988,"avg_gross_exposure_pct":3.8681902002325703,"avg_net_exposure_pct":-2.3913045273063496,"days_in_market_pct":42.10526315789473},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101634807938_4ce1573f","trade_count":13,"profit_factor":2.6072978977047137,"total_return_pct":0.5860747837766976,"win_rate":0.6923076923076923,"max_drawdown_pct":0.31275465094584215,"sharpe_ratio":2.308425640364089,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.6952685722248394,"avg_gross_exposure_pct":1.4491267702359076,"avg_net_exposure_pct":-0.4649535073354661,"days_in_market_pct":31.914893617021278}},"sqs_score":33.2,"sqs_breakdown":{"valid_quality":81.1,"test_quality":49.7,"floor_quality":49.7,"gap_quality":12.0},"sqs_v2_score":42.7,"sqs_v2_breakdown":{"profitability":82.3,"risk":100.0,"consistency":95.8,"robustness":45.1,"capital_efficiency":78.5},"promotion_score":68.6,"promotion_breakdown":{"valid_quality":89.8,"test_quality":42.7,"floor_quality":42.7},"unified_score":33.2,"unified_breakdown":{"valid_quality":81.1,"test_quality":49.7,"floor_quality":49.7,"gap_quality":12.0},"verdict":"worse","verdict_reasoning":"Removing after-close short improved train and valid quality, but test return collapsed from +0.91% to +0.59%. The sleeve is still needed for recent OOS behavior.","next_direction":"Keep after-close short active, but filter it harder so only the deeper negative gaps remain.","tags":["pead","midcap","step59","same","day","only","max4","longtrend25"]} +{"entry_id":"IMP-0054","timestamp":"2026-03-17T10:18:38.178487+00:00","experiment_name":"pead_midcap_step60_same_day_only_interleave_max4_longtrend25","hypothesis":"Interleaving the same-day-only sleeves may preserve the train lift while forcing better balance between short core and long trend entries.","config_delta":{"base_experiment":"pead_midcap_step59_same_day_only_max4_longtrend25","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101616097173_ce78581d","trade_count":124,"profit_factor":1.7147359082965992,"total_return_pct":3.794868328446057,"win_rate":0.5403225806451613,"max_drawdown_pct":1.3030513376085362,"sharpe_ratio":0.9460927004826782,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.645777565512627,"avg_gross_exposure_pct":1.498662692128791,"avg_net_exposure_pct":0.0720408710547762,"days_in_market_pct":19.284940411700973},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101626066831_ce78581d","trade_count":22,"profit_factor":6.627861685804674,"total_return_pct":1.9039976324784367,"win_rate":0.7272727272727273,"max_drawdown_pct":0.44666468803527526,"sharpe_ratio":4.451621602089021,"monthly_win_rate":1.0,"equity_curve_r_squared":0.5794666301732988,"avg_gross_exposure_pct":3.8681902002325703,"avg_net_exposure_pct":-2.3913045273063496,"days_in_market_pct":42.10526315789473},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101634790427_ce78581d","trade_count":13,"profit_factor":2.6072978977047137,"total_return_pct":0.5860747837766976,"win_rate":0.6923076923076923,"max_drawdown_pct":0.31275465094584215,"sharpe_ratio":2.308425640364089,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.6952685722248394,"avg_gross_exposure_pct":1.4491267702359076,"avg_net_exposure_pct":-0.4649535073354661,"days_in_market_pct":31.914893617021278}},"sqs_score":33.2,"sqs_breakdown":{"valid_quality":81.1,"test_quality":49.7,"floor_quality":49.7,"gap_quality":12.0},"sqs_v2_score":42.7,"sqs_v2_breakdown":{"profitability":82.3,"risk":100.0,"consistency":95.8,"robustness":45.1,"capital_efficiency":78.5},"promotion_score":68.6,"promotion_breakdown":{"valid_quality":89.8,"test_quality":42.7,"floor_quality":42.7},"unified_score":33.2,"unified_breakdown":{"valid_quality":81.1,"test_quality":49.7,"floor_quality":49.7,"gap_quality":12.0},"verdict":"neutral","verdict_reasoning":"Interleaving improved train return to the highest seen in this branch, but valid stayed flat and test remained stuck at +0.59%, so it is a useful clue rather than a promotion candidate.","next_direction":"Apply a stricter filter to after-close short instead of removing it entirely.","tags":["pead","midcap","step60","same","day","only","interleave","max4","longtrend25"]} +{"entry_id":"IMP-0055","timestamp":"2026-03-17T10:18:38.591808+00:00","experiment_name":"pead_midcap_step61_same_day_only_max5_longtrend25","hypothesis":"A fifth daily slot may restore some lost test opportunity while keeping the same-day-only train lift.","config_delta":{"base_experiment":"pead_midcap_step59_same_day_only_max4_longtrend25","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101616141383_4177a33d","trade_count":125,"profit_factor":1.6152818286538184,"total_return_pct":3.3281551222560664,"win_rate":0.528,"max_drawdown_pct":1.304467392506448,"sharpe_ratio":0.8410110414784137,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.6390485285220323,"avg_gross_exposure_pct":1.4445779873336309,"avg_net_exposure_pct":-0.001513116209876324,"days_in_market_pct":19.068255687974},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101626106651_4177a33d","trade_count":22,"profit_factor":6.627861685804674,"total_return_pct":1.9039976324784367,"win_rate":0.7272727272727273,"max_drawdown_pct":0.44666468803527526,"sharpe_ratio":4.451621602089021,"monthly_win_rate":1.0,"equity_curve_r_squared":0.5794666301732988,"avg_gross_exposure_pct":3.8681902002325703,"avg_net_exposure_pct":-2.3913045273063496,"days_in_market_pct":42.10526315789473},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101634804824_4177a33d","trade_count":13,"profit_factor":2.6072978977047137,"total_return_pct":0.5860747837766976,"win_rate":0.6923076923076923,"max_drawdown_pct":0.31275465094584215,"sharpe_ratio":2.308425640364089,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.6952685722248394,"avg_gross_exposure_pct":1.4491267702359076,"avg_net_exposure_pct":-0.4649535073354661,"days_in_market_pct":31.914893617021278}},"sqs_score":33.2,"sqs_breakdown":{"valid_quality":81.1,"test_quality":49.7,"floor_quality":49.7,"gap_quality":12.0},"sqs_v2_score":42.7,"sqs_v2_breakdown":{"profitability":82.3,"risk":100.0,"consistency":95.8,"robustness":45.1,"capital_efficiency":78.5},"promotion_score":68.6,"promotion_breakdown":{"valid_quality":89.8,"test_quality":42.7,"floor_quality":42.7},"unified_score":33.2,"unified_breakdown":{"valid_quality":81.1,"test_quality":49.7,"floor_quality":49.7,"gap_quality":12.0},"verdict":"worse","verdict_reasoning":"The extra slot did not recover test. It raised trade count slightly but underperformed the interleaved same-day-only variant and still lagged the mixed-sleeve portfolio.","next_direction":"Return to the mixed-sleeve structure and tighten after-close short with a negative gap filter.","tags":["pead","midcap","step61","same","day","only","max5","longtrend25"]} +{"entry_id":"IMP-0056","timestamp":"2026-03-17T10:20:47.944163+00:00","experiment_name":"pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10","hypothesis":"Weak after-close shorts are diluting the max4 mixed-sleeve portfolio; requiring at least a 10% negative gap should keep the sleeve productive while freeing slots for stronger same-day setups.","config_delta":{"base_experiment":"pead_midcap_step57_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101945843553_a9942261","trade_count":190,"profit_factor":1.5870238518554496,"total_return_pct":5.006752568666314,"win_rate":0.5473684210526316,"max_drawdown_pct":1.2008626598937386,"sharpe_ratio":1.1008608430630382,"monthly_win_rate":0.64,"equity_curve_r_squared":0.7668760857061504,"avg_gross_exposure_pct":2.1363506899122533,"avg_net_exposure_pct":-0.7060295226606695,"days_in_market_pct":25.785482123510295},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101951495313_a9942261","trade_count":26,"profit_factor":4.640144626555104,"total_return_pct":1.7785589226570302,"win_rate":0.6923076923076923,"max_drawdown_pct":0.394474954823559,"sharpe_ratio":4.453025001679815,"monthly_win_rate":1.0,"equity_curve_r_squared":0.5596638198344033,"avg_gross_exposure_pct":3.5028084000751822,"avg_net_exposure_pct":-2.028975630946834,"days_in_market_pct":49.122807017543856},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101956624074_a9942261","trade_count":20,"profit_factor":3.693559917731055,"total_return_pct":0.9821623910714115,"win_rate":0.8,"max_drawdown_pct":0.23230116536263104,"sharpe_ratio":3.3957804338520745,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.9033229833419024,"avg_gross_exposure_pct":2.4512706589430846,"avg_net_exposure_pct":-1.4631851279456756,"days_in_market_pct":48.93617021276596}},"sqs_score":50.9,"sqs_breakdown":{"valid_quality":81.7,"test_quality":79.2,"floor_quality":79.2,"gap_quality":100.0},"sqs_v2_score":87.7,"sqs_v2_breakdown":{"profitability":83.9,"risk":100.0,"consistency":95.8,"robustness":55.6,"capital_efficiency":86.1},"promotion_score":88.9,"promotion_breakdown":{"valid_quality":89.9,"test_quality":87.7,"floor_quality":87.7},"unified_score":50.9,"unified_breakdown":{"valid_quality":81.7,"test_quality":79.2,"floor_quality":79.2,"gap_quality":100.0},"verdict":"better","verdict_reasoning":"This filter was a clear improvement over step57: train return jumped from +3.82% to +5.01%, test improved from +0.91% to +0.98%, and drawdown fell. Valid softened slightly but remained strong.","next_direction":"Try the same negative-gap filter with interleaved sleeve selection to see if test can move back above 1.0% without giving up the train lift.","tags":["pead","midcap","step62","short","core","macro","block","crashcap","gap10","longtrend12","sdlong25","max4","acsgap10"]} +{"entry_id":"IMP-0057","timestamp":"2026-03-17T10:20:48.356015+00:00","experiment_name":"pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12","hypothesis":"A stricter 12% negative gap gate on after-close shorts may further improve the mixed-sleeve portfolio by concentrating the short sleeve into only the sharpest downside reactions.","config_delta":{"base_experiment":"pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101945804213_83edae67","trade_count":182,"profit_factor":1.6277512600955655,"total_return_pct":5.049056193144744,"win_rate":0.5494505494505495,"max_drawdown_pct":1.2723459513687636,"sharpe_ratio":1.1295117660384937,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.7296785391968574,"avg_gross_exposure_pct":2.075630672791522,"avg_net_exposure_pct":-0.6452651158022451,"days_in_market_pct":25.243770314192847},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101951483361_83edae67","trade_count":24,"profit_factor":5.270593419113103,"total_return_pct":1.7898083823081543,"win_rate":0.7083333333333334,"max_drawdown_pct":0.31708063303845857,"sharpe_ratio":4.878106921358311,"monthly_win_rate":1.0,"equity_curve_r_squared":0.637601759273469,"avg_gross_exposure_pct":3.0974719922506315,"avg_net_exposure_pct":-1.626276620699805,"days_in_market_pct":45.614035087719294},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317101956614713_83edae67","trade_count":19,"profit_factor":3.6038727896341176,"total_return_pct":0.9494594526291912,"win_rate":0.7894736842105263,"max_drawdown_pct":0.23230116536263104,"sharpe_ratio":3.2438715317612528,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8958519994688269,"avg_gross_exposure_pct":1.9049388001836767,"avg_net_exposure_pct":-0.9171395725318859,"days_in_market_pct":38.297872340425535}},"sqs_score":38.2,"sqs_breakdown":{"valid_quality":82.3,"test_quality":57.7,"floor_quality":57.7,"gap_quality":34.7},"sqs_v2_score":44.4,"sqs_v2_breakdown":{"profitability":83.8,"risk":100.0,"consistency":95.8,"robustness":55.0,"capital_efficiency":98.1},"promotion_score":69.6,"promotion_breakdown":{"valid_quality":90.3,"test_quality":44.4,"floor_quality":44.4},"unified_score":38.2,"unified_breakdown":{"valid_quality":82.3,"test_quality":57.7,"floor_quality":57.7,"gap_quality":34.7},"verdict":"better","verdict_reasoning":"The 12% gate slightly improved train and valid versus step62 while keeping test near 0.95% with lower drawdown than the old mixed-sleeve base. This is the strongest return-first variant so far.","next_direction":"Combine the after-close gap gate with interleaved max4 sleeve selection to test whether test return can recover toward the 1.0%+ level.","tags":["pead","midcap","step63","short","core","macro","block","crashcap","gap10","longtrend12","sdlong25","max4","acsgap12"]} +{"entry_id":"IMP-0058","timestamp":"2026-03-17T10:22:41.050471+00:00","experiment_name":"pead_midcap_step64_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap10","hypothesis":"Interleaving the filtered mixed-sleeve portfolio might recover some of the earlier test strength without sacrificing the new train lift from the after-close gap gate.","config_delta":{"base_experiment":"pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102155738633_2e938802","trade_count":191,"profit_factor":1.5695913044241423,"total_return_pct":5.006400905086412,"win_rate":0.5392670157068062,"max_drawdown_pct":1.5104386424090617,"sharpe_ratio":1.1022536648052863,"monthly_win_rate":0.64,"equity_curve_r_squared":0.7673617882130458,"avg_gross_exposure_pct":2.194526583220677,"avg_net_exposure_pct":-0.6654873263018454,"days_in_market_pct":26.00216684723727},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102201332816_2e938802","trade_count":26,"profit_factor":4.330878953751208,"total_return_pct":1.675907371790352,"win_rate":0.6538461538461539,"max_drawdown_pct":0.608675665764912,"sharpe_ratio":3.7033259316462277,"monthly_win_rate":1.0,"equity_curve_r_squared":0.527209136324793,"avg_gross_exposure_pct":3.7950928397401578,"avg_net_exposure_pct":-2.3215832255311892,"days_in_market_pct":49.122807017543856},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102206621120_2e938802","trade_count":20,"profit_factor":3.693559917731055,"total_return_pct":0.9821623910714115,"win_rate":0.8,"max_drawdown_pct":0.23230116536263104,"sharpe_ratio":3.3957804338520745,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.9033229833419024,"avg_gross_exposure_pct":2.4512706589430846,"avg_net_exposure_pct":-1.4631851279456756,"days_in_market_pct":48.93617021276596}},"sqs_score":50.6,"sqs_breakdown":{"valid_quality":80.6,"test_quality":79.2,"floor_quality":79.2,"gap_quality":100.0},"sqs_v2_score":87.7,"sqs_v2_breakdown":{"profitability":83.9,"risk":100.0,"consistency":95.8,"robustness":55.6,"capital_efficiency":86.1},"promotion_score":88.2,"promotion_breakdown":{"valid_quality":88.7,"test_quality":87.7,"floor_quality":87.7},"unified_score":50.6,"unified_breakdown":{"valid_quality":80.6,"test_quality":79.2,"floor_quality":79.2,"gap_quality":100.0},"verdict":"worse","verdict_reasoning":"Test held steady, but valid return and drawdown got materially worse while train did not improve. The gap filter works better with raw global-score ranking than with interleaving.","next_direction":"Keep global-score selection and treat step62/63 as the active return-first branch.","tags":["pead","midcap","step64","short","core","macro","block","crashcap","gap10","interleave","max4","acsgap10"]} +{"entry_id":"IMP-0059","timestamp":"2026-03-17T10:22:41.560235+00:00","experiment_name":"pead_midcap_step65_short_core_macro_block_crashcap_gap10_interleave_max4_acsgap12","hypothesis":"A stricter after-close gap gate plus interleaving may produce the strongest hybrid of train lift and balanced sleeve participation.","config_delta":{"base_experiment":"pead_midcap_step63_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap12","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102155749766_057bd373","trade_count":182,"profit_factor":1.6174212176205378,"total_return_pct":5.077465154216028,"win_rate":0.5439560439560439,"max_drawdown_pct":1.5094301004863888,"sharpe_ratio":1.1510607734250322,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.7322022304994754,"avg_gross_exposure_pct":2.127978254795758,"avg_net_exposure_pct":-0.5837963895741501,"days_in_market_pct":25.46045503791983},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102201332544_057bd373","trade_count":24,"profit_factor":4.557936520788986,"total_return_pct":1.5351686099939834,"win_rate":0.6666666666666666,"max_drawdown_pct":0.5303323192221483,"sharpe_ratio":3.9755987852769445,"monthly_win_rate":1.0,"equity_curve_r_squared":0.6279656300903311,"avg_gross_exposure_pct":3.4173394039982,"avg_net_exposure_pct":-1.9446456373142142,"days_in_market_pct":47.368421052631575},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102206620963_057bd373","trade_count":19,"profit_factor":3.6038727896341176,"total_return_pct":0.9494594526291912,"win_rate":0.7894736842105263,"max_drawdown_pct":0.23230116536263104,"sharpe_ratio":3.2438715317612528,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8958519994688269,"avg_gross_exposure_pct":1.9049388001836767,"avg_net_exposure_pct":-0.9171395725318859,"days_in_market_pct":38.297872340425535}},"sqs_score":38.3,"sqs_breakdown":{"valid_quality":81.1,"test_quality":57.7,"floor_quality":57.7,"gap_quality":38.7},"sqs_v2_score":44.4,"sqs_v2_breakdown":{"profitability":83.8,"risk":100.0,"consistency":95.8,"robustness":55.0,"capital_efficiency":98.1},"promotion_score":69.0,"promotion_breakdown":{"valid_quality":89.1,"test_quality":44.4,"floor_quality":44.4},"unified_score":38.3,"unified_breakdown":{"valid_quality":81.1,"test_quality":57.7,"floor_quality":57.7,"gap_quality":38.7},"verdict":"worse","verdict_reasoning":"Train ticked up slightly, but valid deteriorated meaningfully and test did not improve. Interleaving is not helping this filtered branch.","next_direction":"Stay with the non-interleaved filtered short sleeve; the next branch should tune the filtered after-close short only if we need more test return.","tags":["pead","midcap","step65","short","core","macro","block","crashcap","gap10","interleave","max4","acsgap12"]} +{"entry_id":"IMP-0060","timestamp":"2026-03-17T10:26:25.356022+00:00","experiment_name":"pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12","hypothesis":"Adding a 12% downside reaction requirement on top of the 10% after-close gap filter may remove the weakest residual after-close shorts without sacrificing the recent OOS edge.","config_delta":{"base_experiment":"pead_midcap_step62_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102528258441_f85583ef","trade_count":187,"profit_factor":1.6195866594933024,"total_return_pct":5.086267802803064,"win_rate":0.5508021390374331,"max_drawdown_pct":1.2717965470897303,"sharpe_ratio":1.1327429527860058,"monthly_win_rate":0.64,"equity_curve_r_squared":0.7645174872600673,"avg_gross_exposure_pct":2.1004912456069564,"avg_net_exposure_pct":-0.671173830932749,"days_in_market_pct":25.46045503791983},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102534670418_f85583ef","trade_count":26,"profit_factor":4.640144626555104,"total_return_pct":1.7785589226570302,"win_rate":0.6923076923076923,"max_drawdown_pct":0.394474954823559,"sharpe_ratio":4.453025001679815,"monthly_win_rate":1.0,"equity_curve_r_squared":0.5596638198344033,"avg_gross_exposure_pct":3.5028084000751822,"avg_net_exposure_pct":-2.028975630946834,"days_in_market_pct":49.122807017543856},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102539620352_f85583ef","trade_count":20,"profit_factor":3.693559917731055,"total_return_pct":0.9821623910714115,"win_rate":0.8,"max_drawdown_pct":0.23230116536263104,"sharpe_ratio":3.3957804338520745,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.9033229833419024,"avg_gross_exposure_pct":2.4512706589430846,"avg_net_exposure_pct":-1.4631851279456756,"days_in_market_pct":48.93617021276596}},"sqs_score":50.9,"sqs_breakdown":{"valid_quality":81.7,"test_quality":79.2,"floor_quality":79.2,"gap_quality":100.0},"sqs_v2_score":87.7,"sqs_v2_breakdown":{"profitability":83.9,"risk":100.0,"consistency":95.8,"robustness":55.6,"capital_efficiency":86.1},"promotion_score":88.9,"promotion_breakdown":{"valid_quality":89.9,"test_quality":87.7,"floor_quality":87.7},"unified_score":50.9,"unified_breakdown":{"valid_quality":81.7,"test_quality":79.2,"floor_quality":79.2,"gap_quality":100.0},"verdict":"better","verdict_reasoning":"This matched step62 on valid/test while lifting train from +5.01% to +5.09%. It is a cleaner version of the filtered short-sleeve branch with no observable downside so far.","next_direction":"Use step66 as the balanced return-first branch; only test further changes if they can raise test above +1.0% without giving back the train lift.","tags":["pead","midcap","step66","short","core","macro","block","crashcap","gap10","longtrend12","sdlong25","max4","acsgap10","react12"]} +{"entry_id":"IMP-0061","timestamp":"2026-03-17T10:26:25.840092+00:00","experiment_name":"pead_midcap_step67_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react14","hypothesis":"A stricter 14% downside reaction requirement may further improve the filtered after-close short sleeve by keeping only the sharpest downside continuation setups.","config_delta":{"base_experiment":"pead_midcap_step66_short_core_macro_block_crashcap_gap10_longtrend12_sdlong25_max4_acsgap10_react12","changes":{}},"results":{"train":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102528258779_fbf8df06","trade_count":176,"profit_factor":1.72971812082782,"total_return_pct":5.315512807515508,"win_rate":0.5625,"max_drawdown_pct":1.1823507388022194,"sharpe_ratio":1.1862508329604826,"monthly_win_rate":0.625,"equity_curve_r_squared":0.7409760880151399,"avg_gross_exposure_pct":1.9641530768509556,"avg_net_exposure_pct":-0.5086575725921745,"days_in_market_pct":24.918743228602384},"valid":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102534670454_fbf8df06","trade_count":25,"profit_factor":5.331499198553463,"total_return_pct":1.8247351197415846,"win_rate":0.72,"max_drawdown_pct":0.480176430930607,"sharpe_ratio":4.44451943654,"monthly_win_rate":1.0,"equity_curve_r_squared":0.5922580595870112,"avg_gross_exposure_pct":3.1678311806010018,"avg_net_exposure_pct":-1.6948561495104018,"days_in_market_pct":47.368421052631575},"test":{"run_id":"bt_baseline_swing_v1_midcap-filte_20260317102539620462_fbf8df06","trade_count":19,"profit_factor":3.6038727896341176,"total_return_pct":0.9494594526291912,"win_rate":0.7894736842105263,"max_drawdown_pct":0.23230116536263104,"sharpe_ratio":3.2438715317612528,"monthly_win_rate":0.6666666666666666,"equity_curve_r_squared":0.8958519994688269,"avg_gross_exposure_pct":1.9049388001836767,"avg_net_exposure_pct":-0.9171395725318859,"days_in_market_pct":38.297872340425535}},"sqs_score":38.2,"sqs_breakdown":{"valid_quality":82.1,"test_quality":57.7,"floor_quality":57.7,"gap_quality":35.3},"sqs_v2_score":44.4,"sqs_v2_breakdown":{"profitability":83.8,"risk":100.0,"consistency":95.8,"robustness":55.0,"capital_efficiency":98.1},"promotion_score":69.5,"promotion_breakdown":{"valid_quality":90.1,"test_quality":44.4,"floor_quality":44.4},"unified_score":38.2,"unified_breakdown":{"valid_quality":82.1,"test_quality":57.7,"floor_quality":57.7,"gap_quality":35.3},"verdict":"neutral","verdict_reasoning":"This pushed train to +5.32% and lifted valid slightly, but test slipped back to +0.95%. It is a stronger train-focused branch, not a clear overall winner versus step66.","next_direction":"Favor step66 for balance; step67 is only useful if we optimize explicitly for train-heavy return.","tags":["pead","midcap","step67","short","core","macro","block","crashcap","gap10","longtrend12","sdlong25","max4","acsgap10","react14"]} diff --git a/libs/backtest/allocator.py b/libs/backtest/allocator.py index 6c40473..72031ef 100644 --- a/libs/backtest/allocator.py +++ b/libs/backtest/allocator.py @@ -8,6 +8,7 @@ from libs.backtest.domain import ( BacktestConfig, Candidate, DailyPortfolioState, + ExecutionConfig, EventTypeProfile, OpenPosition, PlannedOrder, @@ -212,6 +213,7 @@ def build_planned_order( portfolio_state: DailyPortfolioState, open_positions: list[OpenPosition], config: BacktestConfig, + execution_config: ExecutionConfig | None = None, cooldown_remaining: int = 0, macro_data: dict[str, Any] | None = None, engine_daily_new_risk_used: float = 0.0, @@ -223,6 +225,8 @@ def build_planned_order( engine_daily_new_risk_used=engine_daily_new_risk_used, ) + exec_cfg = execution_config or config.execution + # Apply event-type-specific overrides for stop/target ATR multipliers profile = config.get_event_profile(candidate.event_type) stop_atr_mult = ( @@ -231,20 +235,24 @@ def build_planned_order( else config.risk.stop_atr_multiplier ) target_atr_mult = ( - profile.target_atr_multiplier_override - if profile and profile.target_atr_multiplier_override is not None - else config.execution.target_atr_multiplier + candidate.engine_target_atr_multiplier + if candidate.engine_target_atr_multiplier is not None + else ( + profile.target_atr_multiplier_override + if profile and profile.target_atr_multiplier_override is not None + else exec_cfg.target_atr_multiplier + ) ) stop_price = compute_stop_price( candidate, RiskConfig(**{**config.risk.model_dump(), "stop_atr_multiplier": stop_atr_mult}) ) - target_r = config.execution.target_1_r or 2.0 + target_r = exec_cfg.target_1_r or 2.0 target_price = compute_target_price( candidate.entry_price_est, stop_price, target_r, - target_model=config.execution.target_model, + target_model=exec_cfg.target_model, target_atr_multiplier=target_atr_mult, atr_14=candidate.atr_14, trade_direction=candidate.trade_direction, diff --git a/libs/backtest/domain.py b/libs/backtest/domain.py index 673aecd..4983f67 100644 --- a/libs/backtest/domain.py +++ b/libs/backtest/domain.py @@ -58,6 +58,10 @@ class Candidate(BaseModel): shadow_only: bool = False engine_max_holding_days: int | None = None engine_risk_budget_pct: float = 1.0 + engine_target_atr_multiplier: float | None = None + engine_target_1_fraction: float | None = None + engine_trailing_model: str | None = None + engine_trailing_warmup_days: int | None = None trade_direction: str = "long" # "long" or "short" features: dict[str, Any] = Field(default_factory=dict) @@ -257,6 +261,10 @@ class StrategyEngineConfig(BaseModel): entry_timing_policy: str = "next_open" # "next_open", "reaction_close" max_holding_days: int | None = None engine_risk_budget_pct: float = 1.0 + target_atr_multiplier_override: float | None = None + target_1_fraction_override: float | None = None + trailing_model_override: str | None = None + trailing_warmup_days_override: int | None = None score_threshold_override: float | None = None pead_reaction_threshold_override: float | None = None pead_volume_threshold_override: float | None = None diff --git a/libs/backtest/selector.py b/libs/backtest/selector.py index 474140e..1a78d51 100644 --- a/libs/backtest/selector.py +++ b/libs/backtest/selector.py @@ -146,6 +146,26 @@ def build_candidate( if strategy_engine else 1.0 ), + engine_target_atr_multiplier=( + strategy_engine.target_atr_multiplier_override + if strategy_engine + else None + ), + engine_target_1_fraction=( + strategy_engine.target_1_fraction_override + if strategy_engine + else None + ), + engine_trailing_model=( + strategy_engine.trailing_model_override + if strategy_engine + else None + ), + engine_trailing_warmup_days=( + strategy_engine.trailing_warmup_days_override + if strategy_engine + else None + ), trade_direction=trade_direction, features={k: v for k, v in row.items() if k not in _RESERVED_KEYS}, ) diff --git a/libs/backtest/tracker.py b/libs/backtest/tracker.py index 767d8af..e06ee23 100644 --- a/libs/backtest/tracker.py +++ b/libs/backtest/tracker.py @@ -1,10 +1,12 @@ """Strategy improvement tracker: SQS computation, journal I/O, leaderboard.""" from __future__ import annotations +import contextlib import functools +import fcntl import json from pathlib import Path -from typing import Any +from typing import Any, Iterator from libs.backtest.domain import ( ConfigDelta, @@ -500,6 +502,19 @@ def get_next_entry_id(journal_path: Path) -> str: return f"IMP-{len(entries) + 1:04d}" +@contextlib.contextmanager +def journal_lock(journal_path: Path) -> Iterator[None]: + """Serialize journal mutations across concurrent record commands.""" + journal_path.parent.mkdir(parents=True, exist_ok=True) + lock_path = journal_path.with_suffix(f"{journal_path.suffix}.lock") + with lock_path.open("a+") as lock_file: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) + + def append_journal_entry(journal_path: Path, entry: JournalEntry) -> None: """Append a single JournalEntry as one JSON line.""" journal_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/libs/oracle_client/__init__.py b/libs/oracle_client/__init__.py index 0e478aa..2ecb2b9 100644 --- a/libs/oracle_client/__init__.py +++ b/libs/oracle_client/__init__.py @@ -1,5 +1,6 @@ """Stock Oracle typed client library.""" +from libs.oracle_client.attention import AttentionService from libs.oracle_client.client import OracleClient, make_oracle_client from libs.oracle_client.company import CompanyService from libs.oracle_client.filings import FilingsService @@ -10,6 +11,7 @@ from libs.oracle_client.price import PriceService from libs.oracle_client.screener import ScreenerService __all__ = [ + "AttentionService", "OracleClient", "make_oracle_client", "CompanyService", diff --git a/libs/oracle_client/attention.py b/libs/oracle_client/attention.py new file mode 100644 index 0000000..b818103 --- /dev/null +++ b/libs/oracle_client/attention.py @@ -0,0 +1,64 @@ +"""Attention-related Oracle service methods.""" + +from __future__ import annotations + +import datetime as dt + +from libs.oracle_client.client import OracleClient +from libs.oracle_client.models import ( + CollectionStatusResponse, + EntityResolveResponse, + EventAttentionResponse, +) + + +def _date_to_iso(event_date: str | dt.date) -> str: + if isinstance(event_date, dt.date): + return event_date.isoformat() + return event_date + + +class AttentionService: + def __init__(self, client: OracleClient) -> None: + self._client = client + + async def get_entity(self, ticker: str) -> EntityResolveResponse: + data = await self._client.get(f"/api/v1/attention/entity/{ticker}") + return EntityResolveResponse.model_validate(data) + + async def resolve_entity(self, ticker: str) -> EntityResolveResponse: + data = await self._client.post(f"/api/v1/attention/admin/resolve/{ticker}") + return EntityResolveResponse.model_validate(data) + + async def get_event_attention( + self, + ticker: str, + event_date: str | dt.date, + ) -> EventAttentionResponse: + data = await self._client.get( + f"/api/v1/attention/event/{ticker}", + params={"event_date": _date_to_iso(event_date)}, + ) + return EventAttentionResponse.model_validate(data) + + async def collect_wiki( + self, + ticker: str, + event_date: str | dt.date, + ) -> CollectionStatusResponse: + data = await self._client.post( + f"/api/v1/attention/admin/collect/wiki/{ticker}", + params={"event_date": _date_to_iso(event_date)}, + ) + return CollectionStatusResponse.model_validate(data) + + async def collect_gdelt( + self, + ticker: str, + event_date: str | dt.date, + ) -> CollectionStatusResponse: + data = await self._client.post( + f"/api/v1/attention/admin/collect/gdelt/{ticker}", + params={"event_date": _date_to_iso(event_date)}, + ) + return CollectionStatusResponse.model_validate(data) diff --git a/libs/oracle_client/client.py b/libs/oracle_client/client.py index f7c435c..443e437 100644 --- a/libs/oracle_client/client.py +++ b/libs/oracle_client/client.py @@ -54,10 +54,15 @@ class OracleClient: return self._handle_response(response, path) @with_retry(max_attempts=3, min_wait=0.1, max_wait=5.0, multiplier=0.1) - async def post(self, path: str, json: dict[str, Any] | None = None) -> Any: + async def post( + self, + path: str, + json: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> Any: client = self._ensure_client() try: - response = await client.post(path, json=json) + response = await client.post(path, json=json, params=params) except httpx.ConnectError as exc: raise OracleConnectionError(str(exc), source="oracle", entity=path) from exc except httpx.TimeoutException as exc: diff --git a/libs/oracle_client/models.py b/libs/oracle_client/models.py index 457514a..36828e4 100644 --- a/libs/oracle_client/models.py +++ b/libs/oracle_client/models.py @@ -200,3 +200,57 @@ class ScreenerResponse(BaseModel): total: int = 0 page: int = 1 page_size: int = 250 + + +# --------------------------------------------------------------------------- +# Attention +# --------------------------------------------------------------------------- + + +class EntityInfo(BaseModel): + ticker: str + canonical_name: str + wiki_title: str | None = None + gdelt_query: str | None = None + aliases: list[str] = Field(default_factory=list) + resolver_confidence: float = 0.0 + is_manual_override: bool = False + + +class EntityResolveResponse(BaseModel): + ticker: str + entity: EntityInfo + status: str + message: str + + +class WikiFeatures(BaseModel): + views: int | None = None + baseline_10d: float | None = None + spike_10d: float | None = None + zscore_20d: float | None = None + + +class NewsFeatures(BaseModel): + article_count_1d: int = 0 + article_count_3d: int = 0 + unique_domains_3d: int = 0 + us_article_count_3d: int = 0 + gdelt_status: str = "not_collected" + + +class EventAttentionResponse(BaseModel): + ticker: str + event_date: str + entity: EntityInfo + wiki: WikiFeatures + news: NewsFeatures + metadata: dict[str, Any] = Field(default_factory=dict) + + +class CollectionStatusResponse(BaseModel): + ticker: str + source: str + records_collected: int + date_range: dict[str, Any] = Field(default_factory=dict) + status: str diff --git a/tests/fixtures/attention_collect.json b/tests/fixtures/attention_collect.json new file mode 100644 index 0000000..a92d000 --- /dev/null +++ b/tests/fixtures/attention_collect.json @@ -0,0 +1,9 @@ +{ + "ticker": "AAPL", + "source": "wiki", + "records_collected": 0, + "date_range": { + "event_date": "2024-02-01" + }, + "status": "success" +} diff --git a/tests/fixtures/attention_entity.json b/tests/fixtures/attention_entity.json new file mode 100644 index 0000000..c30491e --- /dev/null +++ b/tests/fixtures/attention_entity.json @@ -0,0 +1,14 @@ +{ + "ticker": "AAPL", + "entity": { + "ticker": "AAPL", + "canonical_name": "Apple", + "wiki_title": "Apple Inc.", + "gdelt_query": "\"Apple\" OR \"Apple Inc.\"", + "aliases": ["Apple Inc."], + "resolver_confidence": 0.95, + "is_manual_override": false + }, + "status": "exists", + "message": "Entity mapping retrieved from database." +} diff --git a/tests/fixtures/attention_event.json b/tests/fixtures/attention_event.json new file mode 100644 index 0000000..87d9048 --- /dev/null +++ b/tests/fixtures/attention_event.json @@ -0,0 +1,30 @@ +{ + "ticker": "AAPL", + "event_date": "2024-02-01", + "entity": { + "ticker": "AAPL", + "canonical_name": "Apple", + "wiki_title": "Apple Inc.", + "gdelt_query": "\"Apple\" OR \"Apple Inc.\"", + "aliases": ["Apple Inc."], + "resolver_confidence": 0.95, + "is_manual_override": false + }, + "wiki": { + "views": 28837, + "baseline_10d": 50133.5, + "spike_10d": 0.5752, + "zscore_20d": -4.7184 + }, + "news": { + "article_count_1d": 0, + "article_count_3d": 0, + "unique_domains_3d": 0, + "us_article_count_3d": 0, + "gdelt_status": "not_collected" + }, + "metadata": { + "wiki_title": "Apple Inc.", + "resolver_confidence": 0.95 + } +} diff --git a/tests/integration/backtest/test_backtest_run.py b/tests/integration/backtest/test_backtest_run.py index fcd0b7c..0653f46 100644 --- a/tests/integration/backtest/test_backtest_run.py +++ b/tests/integration/backtest/test_backtest_run.py @@ -391,3 +391,69 @@ class TestBacktestRunIntegration: assert "avg_gross_exposure_pct" in metrics_summary assert "avg_net_exposure_pct" in metrics_summary assert "days_in_market_pct" in metrics_summary + + def test_engine_execution_overrides_flow_into_effective_execution_config(self): + from apps.backtester.run import BacktestRunner + from libs.backtest.domain import Candidate, ExperimentManifest, StrategyEngineConfig + + store = _build_multi_engine_store() + manifest = ExperimentManifest( + experiment_name="portfolio_exec_overrides", + dataset_snapshot_id="test_snapshot", + base_config="configs/backtest/defaults.json", + overrides={}, + strategy_engines=[ + StrategyEngineConfig( + engine_id="earnings_same_day_long_trend_v1", + event_types=["earnings_release"], + timing_class="same_day", + direction="long_only", + entry_timing_policy="reaction_close", + max_holding_days=12, + engine_risk_budget_pct=0.25, + target_atr_multiplier_override=2.5, + target_1_fraction_override=0.33, + trailing_model_override="pct_10", + trailing_warmup_days_override=2, + ), + ], + ) + config = _make_config(strategy_engines=manifest.strategy_engines) + runner = BacktestRunner(manifest=manifest, config=config, store=store, initial_equity=100_000.0) + + candidate = Candidate( + event_id="EVT::SD::LONG", + symbol="AMD", + issuer_id="ISSUER::AMD", + score=0.92, + sector="Technology", + event_type="earnings_release", + event_timestamp=dt.datetime(2026, 1, 6, 21, 0, tzinfo=_UTC), + event_date=dt.date(2026, 1, 6), + filing_time_bucket="post_market", + timing_class="same_day", + reaction_date=dt.date(2026, 1, 6), + execution_date=dt.date(2026, 1, 6), + entry_price_est=122.0, + avg_dollar_volume=9_000_000.0, + atr_14=3.0, + score_bucket="high", + engine_id="earnings_same_day_long_trend_v1", + entry_timing_policy="reaction_close", + shadow_only=False, + engine_max_holding_days=12, + engine_risk_budget_pct=0.25, + engine_target_atr_multiplier=2.5, + engine_target_1_fraction=0.33, + engine_trailing_model="pct_10", + engine_trailing_warmup_days=2, + trade_direction="long", + ) + effective_exec = runner._build_effective_execution_config(candidate) + + assert candidate.engine_id == "earnings_same_day_long_trend_v1" + assert effective_exec.max_holding_days == 12 + assert effective_exec.target_atr_multiplier == pytest.approx(2.5) + assert effective_exec.target_1_fraction == pytest.approx(0.33) + assert effective_exec.trailing_model == "pct_10" + assert effective_exec.trailing_warmup_days == 2 diff --git a/tests/unit/backtest/test_selector.py b/tests/unit/backtest/test_selector.py index 121752b..03b2443 100644 --- a/tests/unit/backtest/test_selector.py +++ b/tests/unit/backtest/test_selector.py @@ -143,6 +143,38 @@ class TestBuildCandidate: assert c.engine_id == "earnings_same_day_long_close_v1" assert c.entry_timing_policy == "reaction_close" + def test_engine_execution_overrides_are_copied_to_candidate(self): + from libs.backtest.selector import build_candidate + + engine = StrategyEngineConfig( + engine_id="earnings_same_day_long_trend_v1", + event_types=["earnings"], + timing_class="same_day", + direction="long_only", + entry_timing_policy="reaction_close", + max_holding_days=12, + engine_risk_budget_pct=0.25, + target_atr_multiplier_override=2.5, + target_1_fraction_override=0.33, + trailing_model_override="pct_10", + trailing_warmup_days_override=2, + ) + 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.engine_max_holding_days == 12 + assert c.engine_risk_budget_pct == pytest.approx(0.25) + assert c.engine_target_atr_multiplier == pytest.approx(2.5) + assert c.engine_target_1_fraction == pytest.approx(0.33) + assert c.engine_trailing_model == "pct_10" + assert c.engine_trailing_warmup_days == 2 + def test_engine_route_skips_non_matching_direction(self): from libs.backtest.selector import build_candidate diff --git a/tests/unit/backtest/test_tracker.py b/tests/unit/backtest/test_tracker.py index 3e743ab..4eb2e84 100644 --- a/tests/unit/backtest/test_tracker.py +++ b/tests/unit/backtest/test_tracker.py @@ -2,6 +2,8 @@ from __future__ import annotations import json +import multiprocessing +import time from pathlib import Path import pytest @@ -26,11 +28,30 @@ from libs.backtest.tracker import ( compute_unified_score, compute_unified_split_quality, get_next_entry_id, + journal_lock, load_journal, rebuild_registry, ) +def _write_locked_journal_entry(payload: tuple[str, str]) -> str: + journal_path_str, experiment_name = payload + journal_path = Path(journal_path_str) + with journal_lock(journal_path): + entry_id = get_next_entry_id(journal_path) + time.sleep(0.05) + append_journal_entry( + journal_path, + JournalEntry( + entry_id=entry_id, + timestamp="2026-03-17T10:10:07+00:00", + experiment_name=experiment_name, + hypothesis="h", + ), + ) + return entry_id + + # --------------------------------------------------------------------------- # _normalize / _normalize_inverse # --------------------------------------------------------------------------- @@ -514,6 +535,24 @@ class TestJournalIO: append_journal_entry(journal_path, entry) assert get_next_entry_id(journal_path) == "IMP-0002" + def test_journal_lock_serializes_concurrent_writers(self, tmp_path): + journal_path = tmp_path / "journal.jsonl" + ctx = multiprocessing.get_context("spawn") + payloads = [ + (str(journal_path), "exp_a"), + (str(journal_path), "exp_b"), + (str(journal_path), "exp_c"), + ] + with ctx.Pool(processes=3) as pool: + ids = pool.map(_write_locked_journal_entry, payloads) + + assert sorted(ids) == ["IMP-0001", "IMP-0002", "IMP-0003"] + assert [entry.entry_id for entry in load_journal(journal_path)] == [ + "IMP-0001", + "IMP-0002", + "IMP-0003", + ] + def test_load_empty(self, tmp_path): journal_path = tmp_path / "nonexistent.jsonl" entries = load_journal(journal_path) diff --git a/tests/unit/test_oracle_client.py b/tests/unit/test_oracle_client.py index 52eb4ca..ac286c1 100644 --- a/tests/unit/test_oracle_client.py +++ b/tests/unit/test_oracle_client.py @@ -207,3 +207,112 @@ async def test_client_without_context_manager_raises(): client = OracleClient("http://oracle:18001") with pytest.raises(RuntimeError, match="async context manager"): await client.get("/health") + + +@pytest.mark.asyncio +async def test_get_attention_entity(httpx_mock: HTTPXMock): + from libs.oracle_client.attention import AttentionService + from libs.oracle_client.client import OracleClient + + data = load_fixture("attention_entity.json") + httpx_mock.add_response( + json=data, + url="http://oracle:18001/api/v1/attention/entity/AAPL", + ) + + async with OracleClient("http://oracle:18001") as client: + svc = AttentionService(client) + result = await svc.get_entity("AAPL") + + assert result.ticker == "AAPL" + assert result.entity.canonical_name == "Apple" + assert result.entity.wiki_title == "Apple Inc." + assert result.status == "exists" + + +@pytest.mark.asyncio +async def test_get_event_attention(httpx_mock: HTTPXMock): + import datetime as dt + + from libs.oracle_client.attention import AttentionService + from libs.oracle_client.client import OracleClient + + data = load_fixture("attention_event.json") + httpx_mock.add_response( + json=data, + url="http://oracle:18001/api/v1/attention/event/AAPL?event_date=2024-02-01", + ) + + async with OracleClient("http://oracle:18001") as client: + svc = AttentionService(client) + result = await svc.get_event_attention("AAPL", dt.date(2024, 2, 1)) + + assert result.ticker == "AAPL" + assert result.event_date == "2024-02-01" + assert result.wiki.views == 28837 + assert result.news.gdelt_status == "not_collected" + + +@pytest.mark.asyncio +async def test_resolve_attention_entity(httpx_mock: HTTPXMock): + from libs.oracle_client.attention import AttentionService + from libs.oracle_client.client import OracleClient + + data = load_fixture("attention_entity.json") + data["status"] = "resolved" + data["message"] = "Entity resolved: wiki_title='Apple Inc.' confidence=0.95" + httpx_mock.add_response( + json=data, + method="POST", + url="http://oracle:18001/api/v1/attention/admin/resolve/AAPL", + ) + + async with OracleClient("http://oracle:18001") as client: + svc = AttentionService(client) + result = await svc.resolve_entity("AAPL") + + assert result.status == "resolved" + assert result.entity.resolver_confidence == 0.95 + + +@pytest.mark.asyncio +async def test_collect_attention_wiki(httpx_mock: HTTPXMock): + from libs.oracle_client.attention import AttentionService + from libs.oracle_client.client import OracleClient + + data = load_fixture("attention_collect.json") + httpx_mock.add_response( + json=data, + method="POST", + url="http://oracle:18001/api/v1/attention/admin/collect/wiki/AAPL?event_date=2024-02-01", + ) + + async with OracleClient("http://oracle:18001") as client: + svc = AttentionService(client) + result = await svc.collect_wiki("AAPL", "2024-02-01") + + assert result.ticker == "AAPL" + assert result.source == "wiki" + assert result.records_collected == 0 + + +@pytest.mark.asyncio +async def test_collect_attention_gdelt(httpx_mock: HTTPXMock): + from libs.oracle_client.attention import AttentionService + from libs.oracle_client.client import OracleClient + + data = load_fixture("attention_collect.json") + data["source"] = "gdelt" + data["records_collected"] = 12 + httpx_mock.add_response( + json=data, + method="POST", + url="http://oracle:18001/api/v1/attention/admin/collect/gdelt/AAPL?event_date=2024-02-01", + ) + + async with OracleClient("http://oracle:18001") as client: + svc = AttentionService(client) + result = await svc.collect_gdelt("AAPL", "2024-02-01") + + assert result.source == "gdelt" + assert result.records_collected == 12