feat(spot): 2단계 인과 기법 분석 파이프라인 마무리
common/spot/futures 경로 정비, 캔들 데이터 모듈 복원, MTF 규칙 자동 저장 및 2단계 설계·최종 정리 문서를 반영해 3단계 착수 기반을 확정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user