feat(spot): 2단계 인과 기법 분석 파이프라인 마무리

common/spot/futures 경로 정비, 캔들 데이터 모듈 복원, MTF 규칙 자동 저장 및 2단계 설계·최종 정리 문서를 반영해 3단계 착수 기반을 확정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
xavis
2026-06-12 16:09:32 +09:00
parent 741c949470
commit 2d515dd669
18 changed files with 2073 additions and 335 deletions

View File

@@ -254,7 +254,7 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Ground Truth Chart</title>
<title>DeepCoin Chart</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css" />
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"></script>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
@@ -284,7 +284,7 @@ __EXTRA_STYLES__
</head>
<body>
<header>
<h1 id="title">Ground Truth Chart</h1>
<h1 id="title">DeepCoin Chart</h1>
<div class="meta" id="meta"></div>
</header>
__EXTRA_BODY__
@@ -717,6 +717,71 @@ __EXTRA_BODY__
__EXTRA_SCRIPT__
function resolveChartPresentation(m, simMode, chartLabel, gtLabel, simPnl) {
const stage = m.stage || "";
const techniqueName = m.technique_name || "";
const techniqueId = m.technique_id || "";
const simDays = (simPnl && simPnl.sim_lookback_days) || 1095;
const simLabel = simDays >= 365
? `최근 ${Math.round(simDays / 365)}년`
: `최근 ${simDays}일`;
const initCash = (simPnl && simPnl.initial_cash_krw) || 0;
const initLabel = initCash ? ` · 초기 ${Math.round(initCash).toLocaleString()}원` : "";
if (stage === "spot_2_causal_sim_best") {
const bench = m.stage1_benchmark_return_pct;
const benchNote = Number.isFinite(bench)
? ` | 1단계 v3 GT sim ${bench >= 0 ? "+" : ""}${bench.toFixed(2)}%`
: "";
return {
pageTitle: `${m.symbol} 인과 sim — ${techniqueName}`,
title: `${m.symbol} 2단계 인과 sim · 최고 수익 (${techniqueName}, ${m.interval_label}) — 차트 ${chartLabel}`,
panelTitle: `2단계 인과 sim · 최고 수익 기법 (${simLabel}${initLabel})`,
legend: "B=매수 S=매도 | 과거 데이터만 · 인과 기법 신호",
simNoteExtra: `기법 ${techniqueId} | 미래 데이터 미사용${benchNote}`,
};
}
if (stage === "spot_2_causal_sim" || techniqueId) {
return {
pageTitle: `${m.symbol} 인과 sim — ${techniqueName}`,
title: `${m.symbol} 2단계 인과 sim (${techniqueName}, ${m.interval_label}) — 차트 ${chartLabel}`,
panelTitle: `2단계 인과 sim (${simLabel}${initLabel})`,
legend: "B=매수 S=매도 | 과거 데이터만 · 인과 기법 신호",
simNoteExtra: `기법 ${techniqueId} | 미래 데이터 미사용`,
};
}
if (simMode) {
const tier = m.chart_tier ? ` ${String(m.chart_tier).toUpperCase()}` : "";
const tierKey = (m.chart_tier || "v3").toLowerCase();
const legend = tierKey === "v1"
? "B=스윙매수 S=스윙매도"
: tierKey === "v2"
? "B/S=스윙 B*=눌림목"
: "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스";
return {
pageTitle: `${m.symbol} GT sim${tier}`,
title: `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel} · 1단계 sim`,
panelTitle: `1단계 GT sim (${simLabel}${initLabel}) · 사후 최적 타점`,
legend: legend,
simNoteExtra: "사후 GT 타점 · 미래 데이터 사용 (벤치마크)",
};
}
const tier = m.chart_tier ? ` ${String(m.chart_tier).toUpperCase()}` : "";
const tierKey = (m.chart_tier || "v3").toLowerCase();
const legend = tierKey === "v1"
? "B=스윙매수 S=스윙매도"
: tierKey === "v2"
? "B/S=스윙 B*=눌림목"
: "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스";
return {
pageTitle: `${m.symbol} Ground Truth${tier}`,
title: `${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}`,
panelTitle: "",
legend: legend,
simNoteExtra: "",
};
}
function init() {
DATA = window.CHART_DATA;
if (!DATA) throw new Error("차트 데이터 JS 없음");
@@ -726,35 +791,27 @@ __EXTRA_SCRIPT__
const gtDays = m.gt_lookback_days || m.lookback_days;
const chartLabel = chartDays >= 365 ? `${Math.round(chartDays / 365)}년` : `${chartDays}일`;
const gtLabel = gtDays >= 365 ? `${Math.round(gtDays / 365)}년` : `${gtDays}일`;
const tier = m.chart_tier ? ` ${m.chart_tier.toUpperCase()}` : "";
const simSuffix = simMode ? " · 1단계 sim" : "";
document.getElementById("title").textContent =
`${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}${simSuffix}`;
const pres = resolveChartPresentation(m, simMode, chartLabel, gtLabel, DATA.sim_pnl);
document.title = pres.pageTitle;
document.getElementById("title").textContent = pres.title;
document.getElementById("btn-all").textContent = `전체 ${chartLabel}`;
const chartFrom = m.chart_data_from || m.data_from;
const chartTo = m.chart_data_to || m.data_to;
const tierKey = (m.chart_tier || "v3").toLowerCase();
const legend = tierKey === "v1"
? "B=스윙매수 S=스윙매도"
: tierKey === "v2"
? "B/S=스윙 B*=눌림목"
: "B/S=스윙 B*=눌림 B^=돌파 Bd/Sd=다이버전스";
const markerRange = simMode && m.sim_period_from
? `체결 ${DATA.buy_markers.length}/${DATA.sell_markers.length} · ${m.sim_period_from.slice(0, 16)} ~ ${(m.sim_period_to || chartTo).slice(0, 16)}`
: gtLabel;
const legendExtra = simMode ? " | ▼보라=거래시작" : "";
document.getElementById("meta").textContent =
`차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${markerRange}) | ${legend}${legendExtra}`;
let metaLine =
`차트 ${chartFrom} ~ ${chartTo} (${DATA.bar_count.toLocaleString()}봉) | 매수 ${DATA.buy_markers.length} / 매도 ${DATA.sell_markers.length} (${markerRange}) | ${pres.legend}${legendExtra}`;
if (m.comparison_label) {
metaLine += ` | 대조: ${m.comparison_label}`;
}
document.getElementById("meta").textContent = metaLine;
window.__SIM_NOTE_EXTRA__ = pres.simNoteExtra || "";
if (simMode) {
const simDays = DATA.sim_pnl.sim_lookback_days || 1095;
const simLabel = simDays >= 365
? `최근 ${Math.round(simDays / 365)}년`
: `최근 ${simDays}일`;
const panelTitle = document.getElementById("sim-panel-title");
if (panelTitle) {
const initCash = DATA.sim_pnl?.initial_cash_krw || 0;
const initLabel = initCash ? `${Math.round(initCash).toLocaleString()}원` : "";
panelTitle.textContent = `1단계 수익 sim (${simLabel}${initLabel ? ` · 초기 ${initLabel}` : ""})`;
if (panelTitle && pres.panelTitle) {
panelTitle.textContent = pres.panelTitle;
}
renderSimPanel();
}
@@ -834,7 +891,7 @@ _SIM_EXTRA_STYLES = """
_SIM_EXTRA_BODY = """
<section class="sim-panel" id="sim-panel">
<h2 id="sim-panel-title">1단계 수익 sim</h2>
<h2 id="sim-panel-title">수익 sim</h2>
<div class="sim-grid" id="sim-grid"></div>
<div class="sim-note" id="sim-note"></div>
</section>
@@ -910,7 +967,8 @@ _SIM_EXTRA_SCRIPT = """
`시뮬 기간: ${p.period_from} ~ ${p.period_to} (${p.sim_lookback_days}일) | ` +
`신호 ${p.signals_in_period}건 | 분할매수/매도 클러스터 적용 | ` +
`스킵 매수 ${p.buys_skipped} / 매도 ${p.sells_skipped} | 수수료 ${(p.fee_rate * 100).toFixed(2)}%` +
(p.buy_sizing_rule ? ` | 매수 ${p.buy_sizing_rule}` : "");
(p.buy_sizing_rule ? ` | 매수 ${p.buy_sizing_rule}` : "") +
(window.__SIM_NOTE_EXTRA__ ? ` | ${window.__SIM_NOTE_EXTRA__}` : "");
const tbody = document.getElementById("trade-body");
tbody.innerHTML = "";
(p.trades || []).forEach(t => {