파이프라인 산출물(data/, docs/)을 Git 추적에서 제외하고 히스토리를 단일 커밋으로 재구성해 저장소 용량을 경량화한다. Co-authored-by: Cursor <cursoragent@cursor.com>
207 lines
6.6 KiB
Python
207 lines
6.6 KiB
Python
"""매매 기법 실행 및 결과 저장."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from collections.abc import Callable
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pandas as pd
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from deepcoin.data.candle_loader import load_candles
|
|
from deepcoin.data.intervals import interval_label
|
|
from deepcoin.evaluation.gt_align import align_with_ground_truth
|
|
from deepcoin.ground_truth.pnl import simulate_gt_pnl
|
|
from deepcoin.techniques.base import BaseTechnique, TechniqueParams, TechniqueResult
|
|
from deepcoin.techniques.legs import legs_to_signal_dicts, signals_to_legs, summarize_legs
|
|
from deepcoin.techniques.registry import get_all_techniques, get_technique
|
|
|
|
|
|
def run_technique(
|
|
technique: BaseTechnique,
|
|
df: pd.DataFrame,
|
|
params: TechniqueParams,
|
|
gt_result: dict[str, Any] | None = None,
|
|
tolerance_bars: int = 480,
|
|
) -> TechniqueResult:
|
|
"""단일 기법을 실행하고 GT 정합을 계산한다.
|
|
|
|
Args:
|
|
technique: 실행할 기법.
|
|
df: 캔들 DataFrame.
|
|
params: 실행 파라미터.
|
|
gt_result: Ground Truth JSON dict (정합 평가용).
|
|
tolerance_bars: GT 신호 매칭 허용 봉 수.
|
|
|
|
Returns:
|
|
TechniqueResult.
|
|
"""
|
|
merged_extra = {**technique.default_extra_params(), **params.extra}
|
|
run_params = TechniqueParams(
|
|
interval_min=params.interval_min,
|
|
lookback_days=params.lookback_days,
|
|
min_leg_pct=params.min_leg_pct,
|
|
initial_cash_krw=params.initial_cash_krw,
|
|
fee_rate=params.fee_rate,
|
|
extra=merged_extra,
|
|
)
|
|
|
|
raw_signals = technique.generate_signals(df, run_params)
|
|
raw_signals = [s for s in raw_signals if s.price > 0]
|
|
legs = signals_to_legs(raw_signals, min_leg_pct=run_params.min_leg_pct)
|
|
summary = summarize_legs(legs)
|
|
pnl = simulate_gt_pnl(
|
|
legs,
|
|
initial_cash_krw=run_params.initial_cash_krw,
|
|
fee_rate=run_params.fee_rate,
|
|
)
|
|
|
|
alignment = None
|
|
if gt_result is not None:
|
|
alignment = align_with_ground_truth(
|
|
gt_result=gt_result,
|
|
technique_signals=[s.to_dict() for s in raw_signals],
|
|
technique_legs=legs,
|
|
tolerance_bars=tolerance_bars,
|
|
)
|
|
|
|
return TechniqueResult(
|
|
technique_id=technique.technique_id,
|
|
technique_name=technique.technique_name,
|
|
category=technique.category,
|
|
causal=technique.causal,
|
|
description=technique.description,
|
|
params={
|
|
"interval_min": run_params.interval_min,
|
|
"lookback_days": run_params.lookback_days,
|
|
"min_leg_pct": run_params.min_leg_pct,
|
|
"initial_cash_krw": run_params.initial_cash_krw,
|
|
"fee_rate": run_params.fee_rate,
|
|
**merged_extra,
|
|
},
|
|
signals=[s.to_dict() for s in raw_signals],
|
|
legs=legs,
|
|
summary=summary,
|
|
pnl=pnl,
|
|
alignment=alignment,
|
|
)
|
|
|
|
|
|
def run_all_techniques(
|
|
db_path: Path,
|
|
symbol: str,
|
|
params: TechniqueParams,
|
|
gt_result: dict[str, Any] | None = None,
|
|
tolerance_bars: int = 480,
|
|
technique_ids: list[str] | None = None,
|
|
on_result: Callable[[TechniqueResult], None] | None = None,
|
|
skip_errors: bool = True,
|
|
) -> list[TechniqueResult]:
|
|
"""등록된 기법을 일괄 실행한다.
|
|
|
|
Args:
|
|
on_result: 기법 1건 완료 시 호출 (즉시 저장 등).
|
|
skip_errors: True면 실패 기법은 건너뛰고 계속 실행.
|
|
"""
|
|
df = load_candles(
|
|
db_path=db_path,
|
|
symbol=symbol,
|
|
interval_min=params.interval_min,
|
|
lookback_days=params.lookback_days,
|
|
)
|
|
|
|
techniques = get_all_techniques()
|
|
if technique_ids:
|
|
techniques = [t for t in techniques if t.technique_id in technique_ids]
|
|
|
|
results: list[TechniqueResult] = []
|
|
total = len(techniques)
|
|
for idx, technique in enumerate(techniques, start=1):
|
|
try:
|
|
result = run_technique(
|
|
technique=technique,
|
|
df=df,
|
|
params=params,
|
|
gt_result=gt_result,
|
|
tolerance_bars=tolerance_bars,
|
|
)
|
|
except Exception:
|
|
logger.exception("기법 실행 실패: %s", technique.technique_id)
|
|
if not skip_errors:
|
|
raise
|
|
continue
|
|
results.append(result)
|
|
if on_result is not None:
|
|
on_result(result)
|
|
pct = idx / total * 100.0 if total else 100.0
|
|
logger.info(
|
|
"기법 진행 %d/%d (%.1f%%) — %s",
|
|
idx,
|
|
total,
|
|
pct,
|
|
technique.technique_id,
|
|
)
|
|
return results
|
|
|
|
|
|
def load_technique_result(path: Path) -> TechniqueResult:
|
|
"""저장된 기법 JSON을 TechniqueResult로 로드한다."""
|
|
with path.open(encoding="utf-8") as fp:
|
|
payload = json.load(fp)
|
|
return TechniqueResult(
|
|
technique_id=payload["technique_id"],
|
|
technique_name=payload["technique_name"],
|
|
category=payload["category"],
|
|
causal=payload["causal"],
|
|
description=payload.get("description", ""),
|
|
params=payload.get("params", {}),
|
|
signals=payload.get("signals", []),
|
|
legs=payload.get("legs", []),
|
|
summary=payload.get("summary", {}),
|
|
pnl=payload.get("pnl", {}),
|
|
alignment=payload.get("alignment"),
|
|
)
|
|
|
|
|
|
def load_technique_results(
|
|
output_dir: Path,
|
|
technique_ids: list[str] | None = None,
|
|
) -> list[TechniqueResult]:
|
|
"""저장된 기법 JSON 목록을 로드한다."""
|
|
if technique_ids:
|
|
paths = [output_dir / f"{tid}.json" for tid in technique_ids]
|
|
else:
|
|
paths = sorted(output_dir.glob("*.json"))
|
|
|
|
results: list[TechniqueResult] = []
|
|
for path in paths:
|
|
if not path.exists():
|
|
continue
|
|
results.append(load_technique_result(path))
|
|
return results
|
|
|
|
|
|
def save_technique_result(result: TechniqueResult, output_dir: Path) -> Path:
|
|
"""기법 결과를 JSON으로 저장한다."""
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
out_path = output_dir / f"{result.technique_id}.json"
|
|
payload = result.to_dict()
|
|
payload["meta"] = {
|
|
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
"interval_label": interval_label(int(result.params["interval_min"])),
|
|
}
|
|
with out_path.open("w", encoding="utf-8") as fp:
|
|
json.dump(payload, fp, ensure_ascii=False, indent=2)
|
|
return out_path
|
|
|
|
|
|
def load_ground_truth(gt_path: Path) -> dict[str, Any]:
|
|
"""Ground Truth JSON을 로드한다."""
|
|
with gt_path.open(encoding="utf-8") as fp:
|
|
return json.load(fp)
|