feat(spot): fractal_swing live 운영 — 슬리피지·증분 sync·신호 tail 갱신
운영 백테스트(+1,873,140%)과 live/paper 체결 규칙을 맞추고, 캔들 증분 sync· tail 신호 갱신·일일 체결 상한·슬리피지를 반영한다. docs/live 차트 생성 스크립트와 .env.example·README를 갱신한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
135
scripts/3_render_live_chart.py
Normal file
135
scripts/3_render_live_chart.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""3단계: 운영 백테스트(+1,873,140%) 매수·매도 타점 HTML 차트 생성."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.operations.chart import render_ops_live_chart
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
level = logging.DEBUG if verbose else logging.INFO
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def _write_index_html(
|
||||
index_path: Path,
|
||||
chart_name: str,
|
||||
report: dict,
|
||||
) -> Path:
|
||||
"""docs/live 인덱스 HTML을 생성한다."""
|
||||
sim = report["filtered_sim"]
|
||||
ret = sim.get("total_return_pct", 0)
|
||||
index_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DeepCoin Live — 운영 백테스트 차트</title>
|
||||
<style>
|
||||
body {{ font-family: "Malgun Gothic", Arial, sans-serif; margin: 32px; color: #333; background: #f5f5f5; }}
|
||||
h1 {{ font-size: 22px; margin-bottom: 8px; }}
|
||||
.meta {{ color: #666; font-size: 14px; margin-bottom: 20px; line-height: 1.6; }}
|
||||
.card {{ background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 20px 24px; max-width: 720px; }}
|
||||
.stat {{ font-size: 28px; font-weight: bold; color: #2e7d32; margin: 8px 0 16px; }}
|
||||
a {{ color: #1565c0; text-decoration: none; font-size: 16px; }}
|
||||
a:hover {{ text-decoration: underline; }}
|
||||
ul {{ margin: 12px 0 0; padding-left: 20px; font-size: 14px; color: #555; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DeepCoin Live — 운영 백테스트</h1>
|
||||
<p class="meta">
|
||||
{report.get("symbol", "BTC")} · {report.get("technique_name", "")} ({report.get("technique_id", "")})<br>
|
||||
sim 기간: 최근 {report.get("sim_lookback_days", 1095)}일 ·
|
||||
슬리피지 {report.get("slippage_rate", 0) * 100:.2f}% ·
|
||||
일 체결 상한 {report.get("daily_max_trades", "-")} ·
|
||||
MTF {"on" if report.get("mtf_enabled") else "off"}
|
||||
</p>
|
||||
<div class="card">
|
||||
<div>3년 수익률 (운영 규칙 sim)</div>
|
||||
<div class="stat">{ret:+.2f}%</div>
|
||||
<p>
|
||||
<a href="{chart_name}">매수·매도 타점 차트 열기</a>
|
||||
</p>
|
||||
<ul>
|
||||
<li>매수 {sim.get("buys_executed", 0):,} / 매도 {sim.get("sells_executed", 0):,} 체결</li>
|
||||
<li>초기 {sim.get("initial_cash_krw", 0):,.0f}원 → 최종 {sim.get("final_equity_krw", 0):,.0f}원</li>
|
||||
<li>차트: B=매수 S=매도 마커, 이전/다음 타점 탐색, 기간 줌</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
index_path.write_text(html, encoding="utf-8")
|
||||
return index_path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""CLI 진입점."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="운영 백테스트 매수·매도 타점 HTML 차트 (docs/live)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=str,
|
||||
default=None,
|
||||
help="HTML 출력 경로 (기본: docs/live/{technique}_ops_chart.html)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--chart-days",
|
||||
type=int,
|
||||
default=None,
|
||||
help="차트 캔들 표시 일수 (기본: GT_SIM_LOOKBACK_DAYS)",
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
args = parser.parse_args()
|
||||
_configure_logging(args.verbose)
|
||||
|
||||
settings = load_settings()
|
||||
live_dir = ROOT / "docs" / "live"
|
||||
default_name = f"{settings.ops_technique_id}_ops_chart.html"
|
||||
output_path = Path(args.output) if args.output else live_dir / default_name
|
||||
if not output_path.is_absolute():
|
||||
output_path = ROOT / output_path
|
||||
|
||||
print("\n=== 운영 백테스트 차트 생성 ===", flush=True)
|
||||
print(
|
||||
f"기법: {settings.ops_technique_id} · 슬리피지 {settings.ops_slippage_rate} · "
|
||||
f"일 상한 {settings.ops_daily_max_trades}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
chart_path, report = render_ops_live_chart(
|
||||
settings,
|
||||
output_path,
|
||||
chart_lookback_days=args.chart_days,
|
||||
)
|
||||
data_js = chart_path.with_name(f"{chart_path.stem}_data.js")
|
||||
index_path = _write_index_html(live_dir / "index.html", chart_path.name, report)
|
||||
|
||||
sim = report["filtered_sim"]
|
||||
print(f"\n3년 수익률: {sim.get('total_return_pct'):+.2f}%")
|
||||
print(f"매수/매도: {sim.get('buys_executed')}/{sim.get('sells_executed')}")
|
||||
print(f"HTML: {chart_path}")
|
||||
print(f"데이터: {data_js} ({data_js.stat().st_size / 1e6:.1f} MB)")
|
||||
print(f"인덱스: {index_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -43,6 +43,7 @@ def main() -> int:
|
||||
raw = report["raw_sim"]
|
||||
print("\n=== 3단계 MTF 필터 백테스트 ===")
|
||||
print(f"기법: {report['technique_id']}")
|
||||
print(f"슬리피지: {report.get('slippage_rate')} · 일일체결상한: {report.get('daily_max_trades')}")
|
||||
print(f"원시 신호: {report['raw_signal_count']} → 필터 통과: {report['filtered_signal_count']}")
|
||||
print(f"원시 3년 sim: {raw.get('total_return_pct')}%")
|
||||
print(f"필터 3년 sim: {filt.get('total_return_pct')}%")
|
||||
|
||||
12
scripts/3_run_fractal_ops.sh
Executable file
12
scripts/3_run_fractal_ops.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# fractal_swing paper 운영 — 3분봉 tick (180초 간격)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
export PYTHONPATH=src
|
||||
|
||||
echo "=== fractal 현실적 백테스트 ==="
|
||||
python scripts/3_run_fractal_realistic_backtest.py
|
||||
|
||||
echo ""
|
||||
echo "=== paper 운영 (180초 loop) — Ctrl+C 종료 ==="
|
||||
python scripts/3_run_operations.py --loop 180
|
||||
106
scripts/3_run_fractal_realistic_backtest.py
Normal file
106
scripts/3_run_fractal_realistic_backtest.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""fractal_swing 현실적 백테스트 — 슬리피지·일일체결 상한 시나리오."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / "src"
|
||||
if str(SRC) not in sys.path:
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
from deepcoin.config import load_settings
|
||||
from deepcoin.evaluation.causal_sim import normalize_signals_for_sim
|
||||
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl
|
||||
from deepcoin.operations.signal_pipeline import (
|
||||
_signals_in_lookback,
|
||||
generate_raw_signals,
|
||||
load_ops_candles,
|
||||
)
|
||||
|
||||
|
||||
def _configure_logging(verbose: bool) -> None:
|
||||
level = logging.DEBUG if verbose else logging.INFO
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""fractal_swing 슬리피지 시나리오 백테스트."""
|
||||
parser = argparse.ArgumentParser(description="fractal_swing 현실적 3년 백테스트")
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
args = parser.parse_args()
|
||||
_configure_logging(args.verbose)
|
||||
|
||||
settings = load_settings()
|
||||
df = load_ops_candles(settings)
|
||||
gen = generate_raw_signals(settings, df=df, use_cache=True)
|
||||
end = gen["data_end"]
|
||||
price = gen["last_price"]
|
||||
scoped = _signals_in_lookback(gen["raw_signals"], end, settings.gt_sim_lookback_days)
|
||||
normalized = normalize_signals_for_sim(scoped)
|
||||
|
||||
scenarios = [
|
||||
{"label": "stage2_ideal", "slippage_rate": 0.0, "daily_max_trades": None},
|
||||
{"label": "ops_default", "slippage_rate": settings.ops_slippage_rate, "daily_max_trades": settings.ops_daily_max_trades},
|
||||
{"label": "slippage_0.1pct", "slippage_rate": 0.001, "daily_max_trades": settings.ops_daily_max_trades},
|
||||
{"label": "slippage_0.1pct_no_cap", "slippage_rate": 0.001, "daily_max_trades": None},
|
||||
]
|
||||
|
||||
rows = []
|
||||
for sc in scenarios:
|
||||
sim = simulate_gt_signals_pnl(
|
||||
signals=normalized,
|
||||
initial_cash_krw=settings.gt_initial_cash_krw,
|
||||
fee_rate=settings.gt_trading_fee_rate,
|
||||
min_order_krw=settings.ops_min_order_krw,
|
||||
slippage_rate=sc["slippage_rate"],
|
||||
daily_max_trades=sc["daily_max_trades"],
|
||||
sim_lookback_days=settings.gt_sim_lookback_days,
|
||||
data_end=end,
|
||||
last_mark_price=price,
|
||||
)
|
||||
buys = sim["buys_executed"]
|
||||
rows.append({
|
||||
**sc,
|
||||
"return_pct": sim["total_return_pct"],
|
||||
"final_equity_krw": sim["final_equity_krw"],
|
||||
"buys_executed": buys,
|
||||
"sells_executed": sim["sells_executed"],
|
||||
"buys_skipped": sim["buys_skipped"],
|
||||
"daily_buys_avg": round(buys / settings.gt_sim_lookback_days, 2),
|
||||
})
|
||||
|
||||
report = {
|
||||
"technique_id": settings.ops_technique_id,
|
||||
"symbol": settings.symbol,
|
||||
"sim_lookback_days": settings.gt_sim_lookback_days,
|
||||
"initial_cash_krw": settings.gt_initial_cash_krw,
|
||||
"signals_in_period": len(scoped),
|
||||
"scenarios": rows,
|
||||
}
|
||||
out = Path("docs/spot/3_operations/fractal_realistic_backtest.json")
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
print("\n=== fractal_swing 현실적 백테스트 (3년) ===")
|
||||
for row in rows:
|
||||
print(
|
||||
f"{row['label']}: {row['return_pct']}% "
|
||||
f"(슬리피지 {row['slippage_rate']}, 일상한 {row['daily_max_trades']}) "
|
||||
f"매수 {row['buys_executed']} (일 {row['daily_buys_avg']})"
|
||||
)
|
||||
print(f"\nJSON: {out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user