feat(weekly-report): add ML trigger check and degradation sweep
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -157,3 +157,59 @@ def load_trend(report_dir: str, weeks: int = 4) -> dict:
|
|||||||
"mdd": mdd_list,
|
"mdd": mdd_list,
|
||||||
"pf_declining_3w": declining,
|
"pf_declining_3w": declining,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── ML 재학습 트리거 & 성능 저하 스윕 ─────────────────────────────
|
||||||
|
from scripts.strategy_sweep import (
|
||||||
|
run_single_backtest,
|
||||||
|
generate_combinations,
|
||||||
|
PARAM_GRID,
|
||||||
|
)
|
||||||
|
|
||||||
|
ML_TRADE_THRESHOLD = 150
|
||||||
|
|
||||||
|
|
||||||
|
def check_ml_trigger(
|
||||||
|
cumulative_trades: int,
|
||||||
|
current_pf: float,
|
||||||
|
pf_declining_3w: bool,
|
||||||
|
) -> dict:
|
||||||
|
"""ML 재학습 조건 체크. 3개 중 2개 이상 충족 시 권장."""
|
||||||
|
conditions = {
|
||||||
|
"cumulative_trades_enough": cumulative_trades >= ML_TRADE_THRESHOLD,
|
||||||
|
"pf_below_1": current_pf < 1.0,
|
||||||
|
"pf_declining_3w": pf_declining_3w,
|
||||||
|
}
|
||||||
|
met = sum(conditions.values())
|
||||||
|
return {
|
||||||
|
"conditions": conditions,
|
||||||
|
"met_count": met,
|
||||||
|
"recommend": met >= 2,
|
||||||
|
"cumulative_trades": cumulative_trades,
|
||||||
|
"threshold": ML_TRADE_THRESHOLD,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_degradation_sweep(
|
||||||
|
symbols: list[str],
|
||||||
|
train_months: int,
|
||||||
|
test_months: int,
|
||||||
|
top_n: int = 3,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""전체 파라미터 스윕을 실행하고 PF 상위 N개 대안을 반환한다."""
|
||||||
|
combos = generate_combinations(PARAM_GRID)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for params in combos:
|
||||||
|
try:
|
||||||
|
summary = run_single_backtest(symbols, params, train_months, test_months)
|
||||||
|
results.append({"params": params, "summary": summary})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"스윕 실패: {e}")
|
||||||
|
|
||||||
|
results.sort(
|
||||||
|
key=lambda r: r["summary"]["profit_factor"]
|
||||||
|
if r["summary"]["profit_factor"] != float("inf") else 999,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return results[:top_n]
|
||||||
|
|||||||
@@ -110,3 +110,56 @@ def test_load_trend_empty_dir(tmp_path):
|
|||||||
trend = load_trend(str(tmp_path), weeks=4)
|
trend = load_trend(str(tmp_path), weeks=4)
|
||||||
assert trend["pf"] == []
|
assert trend["pf"] == []
|
||||||
assert trend["pf_declining_3w"] is False
|
assert trend["pf_declining_3w"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_ml_trigger_all_met():
|
||||||
|
"""3개 조건 모두 충족 시 recommend=True."""
|
||||||
|
from scripts.weekly_report import check_ml_trigger
|
||||||
|
|
||||||
|
result = check_ml_trigger(
|
||||||
|
cumulative_trades=200, current_pf=0.85, pf_declining_3w=True,
|
||||||
|
)
|
||||||
|
assert result["recommend"] is True
|
||||||
|
assert result["met_count"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_ml_trigger_none_met():
|
||||||
|
"""조건 미충족 시 recommend=False."""
|
||||||
|
from scripts.weekly_report import check_ml_trigger
|
||||||
|
|
||||||
|
result = check_ml_trigger(
|
||||||
|
cumulative_trades=50, current_pf=1.5, pf_declining_3w=False,
|
||||||
|
)
|
||||||
|
assert result["recommend"] is False
|
||||||
|
assert result["met_count"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_degradation_sweep_returns_top_n():
|
||||||
|
"""스윕을 실행하고 PF 상위 N개 대안을 반환."""
|
||||||
|
from scripts.weekly_report import run_degradation_sweep
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
fake_summaries = [
|
||||||
|
{"profit_factor": 1.15, "total_trades": 30, "total_pnl": 50, "return_pct": 5,
|
||||||
|
"win_rate": 55, "avg_win": 10, "avg_loss": -8, "max_drawdown_pct": 10,
|
||||||
|
"sharpe_ratio": 2.0, "total_fees": 1, "close_reasons": {}},
|
||||||
|
{"profit_factor": 1.08, "total_trades": 25, "total_pnl": 30, "return_pct": 3,
|
||||||
|
"win_rate": 50, "avg_win": 8, "avg_loss": -7, "max_drawdown_pct": 12,
|
||||||
|
"sharpe_ratio": 1.5, "total_fees": 1, "close_reasons": {}},
|
||||||
|
{"profit_factor": 0.95, "total_trades": 20, "total_pnl": -10, "return_pct": -1,
|
||||||
|
"win_rate": 40, "avg_win": 6, "avg_loss": -9, "max_drawdown_pct": 15,
|
||||||
|
"sharpe_ratio": 0.5, "total_fees": 1, "close_reasons": {}},
|
||||||
|
]
|
||||||
|
fake_combos = [
|
||||||
|
{"atr_sl_mult": 1.5}, {"atr_sl_mult": 1.0}, {"atr_sl_mult": 2.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch("scripts.weekly_report.run_single_backtest") as mock_bt:
|
||||||
|
mock_bt.side_effect = fake_summaries
|
||||||
|
with patch("scripts.weekly_report.generate_combinations", return_value=fake_combos):
|
||||||
|
alternatives = run_degradation_sweep(
|
||||||
|
symbols=["XRPUSDT"], train_months=3, test_months=1, top_n=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(alternatives) <= 3
|
||||||
|
assert alternatives[0]["summary"]["profit_factor"] >= alternatives[1]["summary"]["profit_factor"]
|
||||||
|
|||||||
Reference in New Issue
Block a user