Files
Bithumb/src/deepcoin/techniques/runner.py
dsyoon 741c949470 refactor: Git에서 데이터 제거, 설정·코드만 유지
파이프라인 산출물(data/, docs/)을 Git 추적에서 제외하고
히스토리를 단일 커밋으로 재구성해 저장소 용량을 경량화한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 10:01:43 +09:00

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)