refactor: Git에서 데이터 제거, 설정·코드만 유지
파이프라인 산출물(data/, docs/)을 Git 추적에서 제외하고 히스토리를 단일 커밋으로 재구성해 저장소 용량을 경량화한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
206
src/deepcoin/techniques/runner.py
Normal file
206
src/deepcoin/techniques/runner.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""매매 기법 실행 및 결과 저장."""
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user