diff --git a/scripts/weekly_report.py b/scripts/weekly_report.py new file mode 100644 index 0000000..b05e0ba --- /dev/null +++ b/scripts/weekly_report.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +주간 전략 리포트: 데이터 수집 → WF 백테스트 → 실전 로그 → 추이 → Discord 알림. + +사용법: + python scripts/weekly_report.py + python scripts/weekly_report.py --skip-fetch + python scripts/weekly_report.py --date 2026-03-07 +""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import subprocess +from datetime import date, timedelta + +from loguru import logger + +from src.backtester import WalkForwardBacktester, WalkForwardConfig + + +# ── 프로덕션 파라미터 ────────────────────────────────────────────── +SYMBOLS = ["XRPUSDT", "TRXUSDT", "DOGEUSDT"] +PROD_PARAMS = { + "atr_sl_mult": 2.0, + "atr_tp_mult": 2.0, + "signal_threshold": 3, + "adx_threshold": 25, + "volume_multiplier": 2.5, +} +TRAIN_MONTHS = 3 +TEST_MONTHS = 1 +FETCH_DAYS = 35 + + +def fetch_latest_data(symbols: list[str], days: int = FETCH_DAYS) -> None: + """심볼별로 fetch_history.py를 subprocess로 호출하여 최신 데이터를 수집한다.""" + script = str(Path(__file__).parent / "fetch_history.py") + for sym in symbols: + cmd = [ + sys.executable, script, + "--symbol", sym, + "--interval", "15m", + "--days", str(days), + ] + logger.info(f"데이터 수집: {sym} (최근 {days}일)") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + logger.warning(f" {sym} 수집 실패: {result.stderr[:200]}") + else: + logger.info(f" {sym} 수집 완료") + + +def run_backtest( + symbols: list[str], + train_months: int, + test_months: int, + params: dict, +) -> dict: + """현재 파라미터로 Walk-Forward 백테스트를 실행하고 결과를 반환한다.""" + cfg = WalkForwardConfig( + symbols=symbols, + use_ml=False, + train_months=train_months, + test_months=test_months, + **params, + ) + wf = WalkForwardBacktester(cfg) + return wf.run() diff --git a/tests/test_weekly_report.py b/tests/test_weekly_report.py new file mode 100644 index 0000000..bb4693c --- /dev/null +++ b/tests/test_weekly_report.py @@ -0,0 +1,48 @@ +import pytest +from unittest.mock import patch, MagicMock +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def test_fetch_latest_data_calls_subprocess(): + """fetch_latest_data가 심볼별로 fetch_history.py를 호출하는지 확인.""" + from scripts.weekly_report import fetch_latest_data + + with patch("subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0) + fetch_latest_data(["XRPUSDT", "TRXUSDT"], days=35) + + assert mock_run.call_count == 2 + args_0 = mock_run.call_args_list[0][0][0] + assert "--symbol" in args_0 + assert "XRPUSDT" in args_0 + assert "--days" in args_0 + assert "35" in args_0 + + +def test_run_backtest_returns_summary(): + """run_backtest가 WF 백테스트를 실행하고 결과를 반환하는지 확인.""" + from scripts.weekly_report import run_backtest + + mock_result = { + "summary": { + "total_trades": 27, "total_pnl": 217.0, "return_pct": 21.7, + "win_rate": 66.7, "profit_factor": 1.57, "max_drawdown_pct": 12.0, + "sharpe_ratio": 33.3, "avg_win": 20.0, "avg_loss": -10.0, + "total_fees": 5.0, "close_reasons": {}, + }, + "folds": [], "trades": [], + } + + with patch("scripts.weekly_report.WalkForwardBacktester") as MockWF: + MockWF.return_value.run.return_value = mock_result + result = run_backtest( + symbols=["XRPUSDT"], train_months=3, test_months=1, + params={"atr_sl_mult": 2.0, "atr_tp_mult": 2.0, + "signal_threshold": 3, "adx_threshold": 25, + "volume_multiplier": 2.5}, + ) + + assert result["summary"]["profit_factor"] == 1.57 + assert result["summary"]["total_trades"] == 27