from __future__ import annotations import itertools import json import sys from dataclasses import asdict from pathlib import Path import pandas as pd PACKAGE_PARENT = Path(__file__).resolve().parents[2] if str(PACKAGE_PARENT) not in sys.path: sys.path.insert(0, str(PACKAGE_PARENT)) from strategy29.backtest.window_analysis import slice_bundle from strategy29.backtest.metrics import max_drawdown, sharpe_ratio from strategy32.backtest.simulator import Strategy32Backtester from strategy32.config import PROFILE_V7_DEFAULT, build_strategy32_config from strategy32.research.adverse_regime import AdverseRegimeResearchHarness from strategy32.research.hybrid_regime import _curve_returns, _run_adverse_component_curve from strategy32.research.soft_router import ( WINDOWS, YEAR_PERIODS, YTD_START, SoftRouterCandidate, build_regime_score_frame, compose_soft_router_curve, evaluate_candidate_exact, load_component_bundle, score_candidate, segment_metrics, ) from strategy32.research.hybrid_regime import STATIC_FILTERS OUT_JSON = Path("/tmp/strategy32_soft_router_search.json") OUT_MD = Path("/Volumes/SSD/data/nextcloud/data/tara/files/๐Ÿ“‚HeadOffice/money-bot/strategy32/015_soft_router_ํƒ์ƒ‰๊ฒฐ๊ณผ.md") PROFILES = ("loose_positive",) CORE_FILTERS = ("overheat_tolerant", "prev_balanced") CAP_ENGINES = ("cap_btc_rebound",) CHOP_ENGINES = ("chop_inverse_carry", "chop_inverse_carry_strict") DIST_ENGINES = ("dist_inverse_carry_strict",) CORE_FLOORS = (0.00, 0.10, 0.20) CAP_MAX_WEIGHTS = (0.20, 0.35, 0.50) CHOP_MAX_WEIGHTS = (0.10, 0.20, 0.35) DIST_MAX_WEIGHTS = (0.10, 0.20, 0.35) CHOP_BLEND_FLOORS = (0.00, 0.10, 0.20) def _evaluate_from_curve(curve: pd.Series, latest_bar: pd.Timestamp) -> tuple[dict[str, dict[str, float]], dict[str, dict[str, float]], float, int, int]: window_results = { label: segment_metrics(curve, latest_bar - pd.Timedelta(days=days), latest_bar) for days, label in WINDOWS } year_results = { label: segment_metrics(curve, start, min(latest_bar, end_exclusive - pd.Timedelta(seconds=1))) for label, start, end_exclusive in YEAR_PERIODS } year_results["2026_YTD"] = segment_metrics(curve, YTD_START, latest_bar) score, negative_years, mdd_violations = score_candidate( window_results, {k: v for k, v in year_results.items() if k != "2026_YTD"}, ) return window_results, year_results, score, negative_years, mdd_violations def _exact_static_variant(bundle, latest_bar: pd.Timestamp, filter_name: str) -> dict[str, object]: window_results: dict[str, dict[str, float]] = {} year_results: dict[str, dict[str, float]] = {} for days, label in WINDOWS: eval_start = latest_bar - pd.Timedelta(days=days) raw_start = eval_start - pd.Timedelta(days=90) from strategy29.backtest.window_analysis import slice_bundle sliced = slice_bundle(bundle, raw_start, latest_bar) cfg = build_strategy32_config(PROFILE_V7_DEFAULT, **STATIC_FILTERS[filter_name]) backtester = Strategy32Backtester(cfg, sliced, trade_start=eval_start) backtester.engine_config.initial_capital = 1000.0 curve = backtester.run().equity_curve.loc[lambda s: s.index >= eval_start] window_results[label] = segment_metrics(curve, eval_start, latest_bar) for label, start, end_exclusive in YEAR_PERIODS: eval_end = min(latest_bar, end_exclusive - pd.Timedelta(seconds=1)) raw_start = start - pd.Timedelta(days=90) from strategy29.backtest.window_analysis import slice_bundle sliced = slice_bundle(bundle, raw_start, eval_end) cfg = build_strategy32_config(PROFILE_V7_DEFAULT, **STATIC_FILTERS[filter_name]) backtester = Strategy32Backtester(cfg, sliced, trade_start=start) backtester.engine_config.initial_capital = 1000.0 curve = backtester.run().equity_curve.loc[lambda s: s.index >= start] year_results[label] = segment_metrics(curve, start, eval_end) raw_start = YTD_START - pd.Timedelta(days=90) from strategy29.backtest.window_analysis import slice_bundle sliced = slice_bundle(bundle, raw_start, latest_bar) cfg = build_strategy32_config(PROFILE_V7_DEFAULT, **STATIC_FILTERS[filter_name]) backtester = Strategy32Backtester(cfg, sliced, trade_start=YTD_START) backtester.engine_config.initial_capital = 1000.0 curve = backtester.run().equity_curve.loc[lambda s: s.index >= YTD_START] year_results["2026_YTD"] = segment_metrics(curve, YTD_START, latest_bar) score, negative_years, mdd_violations = score_candidate( window_results, {k: v for k, v in year_results.items() if k != "2026_YTD"}, ) return { "name": filter_name, "windows": window_results, "years": year_results, "score": score, "negative_years": negative_years, "mdd_violations": mdd_violations, "validation": "exact_static_variant", } def main() -> None: bundle, latest_bar = load_component_bundle() eval_start = latest_bar - pd.Timedelta(days=1825) raw_start = eval_start - pd.Timedelta(days=90) sliced = slice_bundle(bundle, raw_start, latest_bar) precomputed: dict[str, object] = {"profiles": {}} for profile_name in PROFILES: score_frame = build_regime_score_frame(sliced, eval_start, latest_bar, profile_name=profile_name) harness = AdverseRegimeResearchHarness(sliced, latest_bar) timestamps = sorted(sliced.prices["BTC"]["timestamp"].loc[sliced.prices["BTC"]["timestamp"] >= eval_start].tolist()) core_returns: dict[str, pd.Series] = {} adverse_returns: dict[str, pd.Series] = {} for core_filter in CORE_FILTERS: cfg = build_strategy32_config(PROFILE_V7_DEFAULT, **STATIC_FILTERS[core_filter]) backtester = Strategy32Backtester(cfg, sliced, trade_start=eval_start) backtester.engine_config.initial_capital = 1000.0 core_curve = backtester.run().equity_curve.loc[lambda s: s.index >= eval_start] core_returns[core_filter] = _curve_returns(core_curve) print(f"[cache core] {profile_name}|{core_filter}", flush=True) for engine_name in sorted(set(CAP_ENGINES) | set(CHOP_ENGINES) | set(DIST_ENGINES)): adverse_curve = _run_adverse_component_curve( eval_start=eval_start, engine_name=engine_name, harness=harness, regime_frame=score_frame, ) adverse_returns[engine_name] = _curve_returns(adverse_curve) print(f"[cache adverse] {profile_name}|{engine_name}", flush=True) precomputed["profiles"][profile_name] = { "score_frame": score_frame, "timestamps": timestamps, "core_returns": core_returns, "adverse_returns": adverse_returns, } candidates = [ SoftRouterCandidate(*combo) for combo in itertools.product( PROFILES, CORE_FILTERS, CAP_ENGINES, CHOP_ENGINES, DIST_ENGINES, CORE_FLOORS, CAP_MAX_WEIGHTS, CHOP_MAX_WEIGHTS, DIST_MAX_WEIGHTS, CHOP_BLEND_FLOORS, ) ] approx_rows: list[dict[str, object]] = [] for idx, candidate in enumerate(candidates, start=1): profile_cache = precomputed["profiles"][candidate.regime_profile] components = { "timestamps": profile_cache["timestamps"], "score_frame": profile_cache["score_frame"], "core_returns": profile_cache["core_returns"][candidate.core_filter], "cap_returns": profile_cache["adverse_returns"][candidate.cap_engine], "chop_returns": profile_cache["adverse_returns"][candidate.chop_engine], "dist_returns": profile_cache["adverse_returns"][candidate.dist_engine], } curve, weights = compose_soft_router_curve(candidate=candidate, **components) window_results, year_results, score, negative_years, mdd_violations = _evaluate_from_curve(curve, latest_bar) approx_rows.append( { "candidate": asdict(candidate), "name": candidate.name, "score": score, "negative_years": negative_years, "mdd_violations": mdd_violations, "windows": window_results, "years": year_results, "avg_weights": { "core": float(weights["core_weight"].mean()), "cap": float(weights["cap_weight"].mean()), "chop": float(weights["chop_weight"].mean()), "dist": float(weights["dist_weight"].mean()), "cash": float(weights["cash_weight"].mean()), }, "validation": "approx_full_curve_slice", } ) if idx % 100 == 0 or idx == len(candidates): print( f"[approx {idx:04d}/{len(candidates)}] top={approx_rows[-1]['name']} " f"1y={window_results['1y']['total_return'] * 100:.2f}% " f"5y_ann={window_results['5y']['annualized_return'] * 100:.2f}%", flush=True, ) approx_rows.sort(key=lambda row: (int(row["negative_years"]), int(row["mdd_violations"]), -float(row["score"]))) exact_top = [] for row in approx_rows[:3]: candidate = SoftRouterCandidate(**row["candidate"]) print(f"[exact-start] {candidate.name}", flush=True) result = evaluate_candidate_exact(bundle=bundle, latest_bar=latest_bar, candidate=candidate) exact_top.append(result) exact_top.sort(key=lambda item: (int(item["negative_years"]), int(item["mdd_violations"]), -float(item["score"]))) print( f"[exact] {candidate.name} 1y={result['windows']['1y']['total_return'] * 100:.2f}% " f"5y_ann={result['windows']['5y']['annualized_return'] * 100:.2f}% " f"neg={result['negative_years']} mdd_viol={result['mdd_violations']}", flush=True, ) static_exact = [_exact_static_variant(bundle, latest_bar, filter_name) for filter_name in CORE_FILTERS] payload = { "analysis": "strategy32_soft_router_search", "latest_completed_bar": str(latest_bar), "candidate_count": len(candidates), "component_cache_count": sum( len(profile_cache["core_returns"]) + len(profile_cache["adverse_returns"]) for profile_cache in precomputed["profiles"].values() ), "summary": approx_rows[:20], "exact_top": exact_top, "exact_static": static_exact, } OUT_JSON.write_text(json.dumps(payload, indent=2), encoding="utf-8") lines = [ "# Strategy32 Soft Router ํƒ์ƒ‰๊ฒฐ๊ณผ", "", "## 1. ๋ชฉ์ ", "", "`5๊ฐœ ํ•˜๋“œ ๋ ˆ์ง -> 1์—”์ง„ ์„ ํƒ` ๊ตฌ์กฐ๋ฅผ ๋ฒ„๋ฆฌ๊ณ , `์ •์  ์ฝ”์–ด ์—”์ง„ + adverse overlay` ๊ตฌ์กฐ๋ฅผ ์—ฐ์†ํ˜• ์ ์ˆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ํƒ์ƒ‰ํ•œ๋‹ค.", "", "## 2. ํƒ์ƒ‰ ๋ฒ”์œ„", "", f"- profiles: `{', '.join(PROFILES)}`", f"- core filters: `{', '.join(CORE_FILTERS)}`", f"- cap engines: `{', '.join(CAP_ENGINES)}`", f"- chop engines: `{', '.join(CHOP_ENGINES)}`", f"- dist engines: `{', '.join(DIST_ENGINES)}`", f"- total candidates: `{len(candidates)}`", "", "## 3. exact ์ƒ์œ„ ํ›„๋ณด", "", "| rank | candidate | 1y | 2y ann | 3y ann | 4y ann | 5y ann | 5y MDD | 2025 | 2024 |", "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|", ] for idx, row in enumerate(exact_top, start=1): lines.append( f"| `{idx}` | `{row['name']}` | `{row['windows']['1y']['total_return'] * 100:.2f}%` | " f"`{row['windows']['2y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['3y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['4y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['5y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['5y']['max_drawdown'] * 100:.2f}%` | " f"`{row['years']['2025']['total_return'] * 100:.2f}%` | " f"`{row['years']['2024']['total_return'] * 100:.2f}%` |" ) lines.extend( [ "", "## 4. ์ •์  ์ฝ”์–ด exact ๋น„๊ต", "", "| core filter | 1y | 2y ann | 3y ann | 4y ann | 5y ann | 5y MDD | 2025 | 2024 |", "|---|---:|---:|---:|---:|---:|---:|---:|---:|", ] ) for row in static_exact: lines.append( f"| `{row['name']}` | `{row['windows']['1y']['total_return'] * 100:.2f}%` | " f"`{row['windows']['2y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['3y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['4y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['5y']['annualized_return'] * 100:.2f}%` | " f"`{row['windows']['5y']['max_drawdown'] * 100:.2f}%` | " f"`{row['years']['2025']['total_return'] * 100:.2f}%` | " f"`{row['years']['2024']['total_return'] * 100:.2f}%` |" ) lines.extend( [ "", "## 5. ํ•ด์„", "", "- soft router๊ฐ€ ์ •์  ์ฝ”์–ด๋ณด๋‹ค ์ข‹์•„์ง€๋ ค๋ฉด, adverse overlay๊ฐ€ `2024/2025 ๋ฐฉ์–ด`๋ฅผ ๋งŒ๋“ค์–ด๋‚ด๋ฉด์„œ `5y CAGR`์„ ํฌ๊ฒŒ ํ›ผ์†ํ•˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค.", "- exact ๊ฒฐ๊ณผ๊ฐ€ ์ •์  ์ฝ”์–ด๋ณด๋‹ค ์•ฝํ•˜๋ฉด, ํ˜„์žฌ adverse overlay ์‹ ํ˜ธ ํ’ˆ์งˆ ๋˜๋Š” overlay weight ๊ณต์‹์ด ์•„์ง ์ตœ์ ์ด ์•„๋‹ˆ๋ผ๋Š” ๋œป์ด๋‹ค.", "", "## 6. ์›๋ณธ ๊ฒฐ๊ณผ", "", f"- JSON: [{OUT_JSON}]({OUT_JSON})", ] ) OUT_MD.write_text("\n".join(lines) + "\n", encoding="utf-8") if __name__ == "__main__": main()