Files
Bithumb/docs/02_ground_truth/ground_truth_chart_sim_v2.html
dsyoon 75399ce79c feat: Ground Truth 2단계 1년 수익 시뮬 및 sim 차트 추가
분할 매수/매도 PnL 시뮬, 체결 타점·거래시작 마커, x축 unix 변환 수정.

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

609 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Ground Truth 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>
<script src="ground_truth_chart_sim_v2_data.js"></script>
<style>
body { font-family: "Malgun Gothic", Arial, sans-serif; margin: 0; background: #f5f5f5; color: #333; }
header { padding: 16px 24px; background: #fff; border-bottom: 1px solid #ddd; }
h1 { margin: 0 0 6px; font-size: 20px; }
.meta { font-size: 13px; color: #666; }
.toolbar { padding: 10px 24px; background: #fff; border-bottom: 1px solid #eee; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.toolbar-group { display: flex; gap: 6px; align-items: center; padding-right: 12px; border-right: 1px solid #e0e0e0; }
.toolbar-group:last-of-type { border-right: none; }
.toolbar button { padding: 6px 12px; border: 1px solid #bbb; background: #fff; cursor: pointer; border-radius: 4px; font-size: 13px; white-space: nowrap; }
.toolbar button:hover { background: #f0f4f8; }
.toolbar button.active { background: #1565c0; color: #fff; border-color: #1565c0; }
.toolbar button.home { background: #2e7d32; color: #fff; border-color: #2e7d32; font-weight: bold; }
.toolbar button.home:hover { background: #1b5e20; }
.toolbar .leg-info { font-size: 12px; color: #555; min-width: 90px; }
#status { font-size: 12px; color: #888; margin-left: auto; }
#overview { height: 480px; margin: 12px 24px; background: #fff; border: 1px solid #ddd; overflow: visible; }
#overview .u-wrap, #overview .uplot { overflow: visible !important; }
#detail-wrap { margin: 0 24px 12px; display: none; }
#detail-wrap h2 { font-size: 15px; margin: 0 0 8px; }
#detail { height: 360px; background: #fff; border: 1px solid #ddd; overflow: visible; }
.sim-panel { margin: 12px 24px 0; padding: 16px 20px; background: #fff; border: 1px solid #ddd; border-radius: 4px; }
.sim-panel h2 { margin: 0 0 12px; font-size: 16px; }
.sim-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.sim-card { padding: 10px 12px; background: #fafafa; border: 1px solid #eee; border-radius: 4px; }
.sim-card .label { font-size: 12px; color: #777; margin-bottom: 4px; }
.sim-card .value { font-size: 18px; font-weight: bold; }
.sim-card .value.positive { color: #2e7d32; }
.sim-card .value.negative { color: #c62828; }
.sim-note { margin-top: 10px; font-size: 12px; color: #666; line-height: 1.5; }
#trade-table-wrap { margin: 12px 24px 0; background: #fff; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; }
#trade-table-wrap summary { padding: 10px 16px; cursor: pointer; font-size: 14px; background: #fafafa; border-bottom: 1px solid #eee; }
.trade-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.trade-table th, .trade-table td { padding: 6px 10px; border-bottom: 1px solid #eee; text-align: right; }
.trade-table th:first-child, .trade-table td:first-child { text-align: left; }
.trade-table th { background: #f5f5f5; position: sticky; top: 0; }
.trade-table tr.skipped td { color: #999; }
.trade-scroll { max-height: 240px; overflow: auto; }
</style>
</head>
<body>
<header>
<h1 id="title">Ground Truth Chart</h1>
<div class="meta" id="meta"></div>
</header>
<section class="sim-panel" id="sim-panel">
<h2>2단계 수익 시뮬레이션 (최근 1년 · 초기 40만원)</h2>
<div class="sim-grid" id="sim-grid"></div>
<div class="sim-note" id="sim-note"></div>
</section>
<details id="trade-table-wrap">
<summary>체결 내역 (<span id="trade-count">0</span>건)</summary>
<div class="trade-scroll">
<table class="trade-table">
<thead>
<tr>
<th>시각</th><th>구분</th><th>유형</th><th>가격</th><th>주문금액</th>
<th>수수료</th><th>현금</th><th>코인</th><th>비고</th>
</tr>
</thead>
<tbody id="trade-body"></tbody>
</table>
</div>
</details>
<div class="toolbar">
<div class="toolbar-group">
<button id="btn-home" class="home" title="전체 2년 화면으로 복귀"></button>
<button id="btn-prev-leg" title="이전 매수·매도 타점">◀ 이전</button>
<button id="btn-next-leg" title="다음 매수·매도 타점">다음 ▶</button>
<span class="leg-info" id="leg-info">타점 - / -</span>
</div>
<div class="toolbar-group">
<button id="btn-all" class="btn-period active">전체</button>
<button id="btn-365d" class="btn-period">1년</button>
<button id="btn-30d" class="btn-period">30일</button>
<button id="btn-7d" class="btn-period">7일</button>
<button id="btn-3d" class="btn-period">3일</button>
</div>
<div class="toolbar-group">
<button id="btn-zoom-in" title="확대">+ 확대</button>
<button id="btn-zoom-out" title="축소"> 축소</button>
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
</div>
<div class="toolbar-group">
<button id="btn-markers" title="매수·매도 마커 표시/숨김">마커 숨김</button>
<button id="btn-toggle-detail" title="상세 캔들 패널 표시/숨김">상세 패널</button>
</div>
<span id="status">데이터 로딩 중…</span>
</div>
<div id="overview"></div>
<div id="detail-wrap">
<h2 id="detail-title">상세 캔들</h2>
<div id="detail"></div>
</div>
<script>
let DATA = null;
let overviewPlot = null;
let detailChart = null;
let detailSeries = null;
let currentMode = "overview";
let currentLegIdx = 0;
let showMarkers = true;
let detailVisible = false;
let lastDetailStart = 0;
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
let axisMeasureCtx = null;
function fmtPrice(v) {
return Math.round(v).toLocaleString("ko-KR");
}
function measureTextWidth(text, font) {
if (!axisMeasureCtx) {
const c = document.createElement("canvas");
axisMeasureCtx = c.getContext("2d");
}
axisMeasureCtx.font = font;
return axisMeasureCtx.measureText(text).width;
}
function yAxisLabelWidth() {
const vals = DATA.close;
if (!vals || !vals.length) return 88;
const samples = new Set([vals[0], vals[vals.length - 1]]);
let lo = vals[0], hi = vals[0];
for (let i = 1; i < vals.length; i++) {
if (vals[i] < lo) lo = vals[i];
if (vals[i] > hi) hi = vals[i];
}
samples.add(lo);
samples.add(hi);
samples.add((lo + hi) / 2);
let maxW = 0;
samples.forEach(v => {
maxW = Math.max(maxW, measureTextWidth(fmtPrice(v), AXIS_FONT));
});
return Math.ceil(maxW) + 20;
}
function updateLegInfo() {
const total = DATA.buy_markers.length;
const el = document.getElementById("leg-info");
if (!total) { el.textContent = "타점 없음"; return; }
el.textContent = `타점 ${currentLegIdx + 1} / ${total}`;
}
const MARKER_FONT = "bold 18px Malgun Gothic, Arial, sans-serif";
const SIM_START_COLOR = "#7b1fa2";
function markerSuffix(signalType) {
if (signalType === "pullback") return "*";
if (signalType === "breakout") return "^";
if (signalType === "div_bull" || signalType === "div_bear") return "d";
return "";
}
function drawMarkerLabel(ctx, label, x, y, color, up) {
ctx.font = MARKER_FONT;
const lx = x + 10;
const ly = y + (up ? 28 : -20);
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.85)";
ctx.strokeText(label, lx, ly);
ctx.fillStyle = color;
ctx.fillText(label, lx, ly);
}
function drawSimStartMarker(u, marker) {
if (!marker) return;
const ctx = u.ctx;
const x = u.valToPos(marker.time, "x", true);
const y = u.valToPos(marker.price, "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
const color = SIM_START_COLOR;
const s = 10;
const gap = 14;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x, y - gap);
ctx.lineTo(x - s, y - gap - 18);
ctx.lineTo(x + s, y - gap - 18);
ctx.closePath();
ctx.fill();
const label = marker.label || "거래시작";
ctx.font = MARKER_FONT;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
const labelY = y - gap - 18 - 6;
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(255,255,255,0.85)";
ctx.strokeText(label, x, labelY);
ctx.fillStyle = color;
ctx.fillText(label, x, labelY);
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
}
function drawMarkers(u, buys, sells) {
if (!showMarkers) return;
const ctx = u.ctx;
const drawOne = (m, color, up) => {
const x = u.valToPos(m.time, "x", true);
const y = u.valToPos(m.price, "y", true);
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
ctx.fillStyle = color;
ctx.beginPath();
const s = 8;
const gap = 12;
if (up) {
ctx.moveTo(x, y + gap); ctx.lineTo(x - s, y + gap + 16); ctx.lineTo(x + s, y + gap + 16);
} else {
ctx.moveTo(x, y - gap); ctx.lineTo(x - s, y - gap - 16); ctx.lineTo(x + s, y - gap - 16);
}
ctx.closePath(); ctx.fill();
const label = (up ? "B" : "S") + m.marker_id + markerSuffix(m.signal_type);
drawMarkerLabel(ctx, label, x, y, color, up);
};
buys.forEach(m => {
let color = "#2e7d32";
if (m.signal_type === "breakout") color = "#ef6c00";
else if (m.signal_type === "div_bull") color = "#7b1fa2";
drawOne(m, color, true);
});
sells.forEach(m => {
const color = m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828";
drawOne(m, color, false);
});
}
function overviewXRange() {
if (!overviewPlot) return { min: DATA.times[0], max: DATA.times[DATA.times.length - 1] };
const s = overviewPlot.scales.x;
return { min: s.min, max: s.max };
}
function setOverviewXRange(min, max) {
const t0 = DATA.times[0];
const t1 = DATA.times[DATA.times.length - 1];
overviewPlot.setScale("x", {
min: Math.max(t0, min),
max: Math.min(t1, max),
});
}
function fitOverview() {
setOverviewXRange(DATA.times[0], DATA.times[DATA.times.length - 1]);
}
function zoomOverview(factor) {
const { min, max } = overviewXRange();
const mid = (min + max) / 2;
const half = Math.max((max - min) * factor / 2, 3600);
setOverviewXRange(mid - half, mid + half);
}
function buildOverview(keepRange) {
const prev = keepRange ? overviewXRange() : null;
if (overviewPlot) { overviewPlot.destroy(); overviewPlot = null; }
const yAxisW = yAxisLabelWidth();
const opts = {
width: document.getElementById("overview").clientWidth,
height: 480,
padding: [14, 10, 14, 10],
scales: { x: { time: true } },
axes: [
{ gap: 6 },
{
side: 3,
size: yAxisW,
gap: 10,
font: AXIS_FONT,
values: (u, vals) => vals.map(v => fmtPrice(v)),
},
],
series: [
{},
{ label: "종가", stroke: "#1565c0", width: 1 },
],
cursor: { drag: { x: true, y: false, setScale: true } },
hooks: {
draw: [(u) => {
drawSimStartMarker(u, DATA.sim_start_marker);
drawMarkers(u, DATA.buy_markers, DATA.sell_markers);
}],
},
};
overviewPlot = new uPlot(opts, [DATA.times, DATA.close], document.getElementById("overview"));
if (prev && keepRange) setOverviewXRange(prev.min, prev.max);
else fitOverview();
}
function sliceLastDays(days) {
const cutoff = DATA.times[DATA.times.length - 1] - days * 86400;
let start = 0;
for (let i = DATA.times.length - 1; i >= 0; i--) {
if (DATA.times[i] < cutoff) { start = i + 1; break; }
}
return { start, end: DATA.times.length };
}
function buildDetailCandles(startIdx, endIdx) {
lastDetailStart = startIdx;
const end = endIdx || DATA.times.length;
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
const wrap = document.getElementById("detail");
wrap.innerHTML = "";
const priceAxisW = yAxisLabelWidth();
detailChart = LightweightCharts.createChart(wrap, {
layout: { background: { color: "#fff" }, textColor: "#333", fontSize: 14 },
grid: { vertLines: { color: "#eee" }, horzLines: { color: "#eee" } },
rightPriceScale: { visible: false },
leftPriceScale: {
borderVisible: true,
minimumWidth: priceAxisW,
scaleMargins: { top: 0.08, bottom: 0.08 },
},
timeScale: { timeVisible: true, secondsVisible: false },
width: wrap.clientWidth,
height: 360,
});
detailSeries = detailChart.addCandlestickSeries({
priceScaleId: "left",
upColor: "#c62828", downColor: "#1565c0",
borderUpColor: "#c62828", borderDownColor: "#1565c0",
wickUpColor: "#c62828", wickDownColor: "#1565c0",
});
const candles = [];
for (let i = startIdx; i < end; i++) {
candles.push({
time: DATA.times[i],
open: DATA.open[i], high: DATA.high[i],
low: DATA.low[i], close: DATA.close[i],
});
}
detailSeries.setData(candles);
const t0 = DATA.times[startIdx];
const t1 = DATA.times[end - 1];
const markers = [];
if (DATA.sim_start_marker) {
const sm = DATA.sim_start_marker;
if (sm.time >= t0 && sm.time <= t1) markers.push({
time: sm.time, position: "aboveBar",
color: SIM_START_COLOR, shape: "arrowDown", size: 3,
text: sm.label || "거래시작",
});
}
if (showMarkers) {
DATA.buy_markers.forEach(m => {
if (m.time >= t0 && m.time <= t1) markers.push({
time: m.time, position: "belowBar",
color: m.signal_type === "breakout" ? "#ef6c00"
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
shape: "arrowUp", size: 3,
text: "B" + m.marker_id + markerSuffix(m.signal_type),
});
});
DATA.sell_markers.forEach(m => {
if (m.time >= t0 && m.time <= t1) markers.push({
time: m.time, position: "aboveBar",
color: m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828",
shape: "arrowDown", size: 3,
text: "S" + m.marker_id + markerSuffix(m.signal_type),
});
});
}
markers.sort((a, b) => a.time - b.time);
detailSeries.setMarkers(markers);
detailChart.timeScale().fitContent();
}
function setActive(btnId) {
document.querySelectorAll(".btn-period").forEach(b => b.classList.remove("active"));
const el = document.getElementById(btnId);
if (el) el.classList.add("active");
}
function goHome() {
currentMode = "overview";
setActive("btn-all");
document.getElementById("overview").style.display = "block";
if (!overviewPlot) buildOverview(false);
else fitOverview();
document.getElementById("status").textContent =
`전체 ${DATA.bar_count.toLocaleString()}봉 | 드래그=줌, 더블클릭=리셋`;
}
function nearestSellAfter(buyTime) {
let best = null;
for (const s of DATA.sell_markers) {
if (s.time >= buyTime && (!best || s.time < best.time)) best = s;
}
return best || DATA.sell_markers[DATA.sell_markers.length - 1];
}
function jumpToLeg(idx) {
const total = DATA.buy_markers.length;
if (!total) return;
currentLegIdx = Math.max(0, Math.min(idx, total - 1));
updateLegInfo();
const buy = DATA.buy_markers[currentLegIdx];
const sell = nearestSellAfter(buy.time);
const span = sell ? Math.max(sell.time - buy.time, 86400) : 86400 * 3;
const pad = span * 0.4;
const vmin = buy.time - pad;
const vmax = (sell ? sell.time : buy.time) + pad;
currentMode = "overview";
setActive("btn-all");
document.getElementById("overview").style.display = "block";
if (!overviewPlot) buildOverview(false);
setOverviewXRange(vmin, vmax);
let start = 0;
for (let i = 0; i < DATA.times.length; i++) {
if (DATA.times[i] >= vmin) { start = i; break; }
}
let end = DATA.times.length;
for (let i = DATA.times.length - 1; i >= 0; i--) {
if (DATA.times[i] <= vmax) { end = i + 1; break; }
}
const buyLabel = buy.signal_type === "pullback" ? "눌림목 매수"
: buy.signal_type === "breakout" ? "돌파 매수"
: buy.signal_type === "div_bull" ? "다이버전스 매수" : "스윙 매수";
document.getElementById("detail-title").textContent =
`B${buy.marker_id} ${buyLabel}${new Date(buy.time * 1000).toLocaleString("ko-KR")}`;
buildDetailCandles(start, end);
const sellText = sell ? ` → 매도 ${fmtPrice(sell.price)}` : "";
document.getElementById("status").textContent =
`B${buy.marker_id} ${buyLabel} ${fmtPrice(buy.price)}${sellText}`;
}
function showPeriod(days, btnId, label) {
currentMode = "detail";
setActive(btnId);
detailVisible = true;
document.getElementById("btn-toggle-detail").textContent = "상세 숨김";
const { start } = sliceLastDays(days);
document.getElementById("detail-title").textContent =
`${label} 3분봉 캔들 (${(DATA.times.length - start).toLocaleString()}봉)`;
buildDetailCandles(start);
document.getElementById("overview").style.display = "block";
if (!overviewPlot) buildOverview(false);
const t0 = DATA.times[start];
setOverviewXRange(t0, DATA.times[DATA.times.length - 1]);
document.getElementById("status").textContent = `${label} 구간 표시`;
}
function applyZoom(factor) {
if (currentMode === "detail" && detailChart) {
const ts = detailChart.timeScale();
const r = ts.getVisibleLogicalRange();
if (!r) return;
const mid = (r.from + r.to) / 2;
const half = Math.max((r.to - r.from) * factor / 2, 10);
ts.setVisibleLogicalRange({ from: mid - half, to: mid + half });
} else if (overviewPlot) {
zoomOverview(factor);
}
}
function applyFit() {
if (currentMode === "detail" && detailChart) {
detailChart.timeScale().fitContent();
} else if (overviewPlot) {
fitOverview();
}
}
function fmtMoney(v) {
return Math.round(v).toLocaleString("ko-KR") + "원";
}
function fmtPct(v) {
const sign = v > 0 ? "+" : "";
return sign + v.toFixed(2) + "%";
}
function renderSimPanel() {
const p = DATA.sim_pnl;
const retClass = p.total_return_pct >= 0 ? "positive" : "negative";
document.getElementById("sim-grid").innerHTML = [
["초기 자본", fmtMoney(p.initial_cash_krw), ""],
["최종 평가액", fmtMoney(p.final_equity_krw), retClass],
["손익", fmtMoney(p.total_pnl_krw), retClass],
["수익률", fmtPct(p.total_return_pct), retClass],
["현금 잔고", fmtMoney(p.final_cash_krw), ""],
["보유 코인", p.final_coin_qty.toFixed(8), ""],
["코인 평가", fmtMoney(p.final_coin_value_krw), ""],
["매수/매도", `${p.buys_executed}/${p.sells_executed}`, ""],
].map(([label, value, cls]) =>
`<div class="sim-card"><div class="label">${label}</div><div class="value ${cls}">${value}</div></div>`
).join("");
document.getElementById("sim-note").textContent =
`시뮬 기간: ${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)}%`;
const tbody = document.getElementById("trade-body");
tbody.innerHTML = "";
(p.trades || []).forEach(t => {
const tr = document.createElement("tr");
if (t.skipped) tr.className = "skipped";
tr.innerHTML = `
<td>${t.datetime}</td>
<td>${t.side === "buy" ? "매수" : "매도"}</td>
<td>${t.signal_type}</td>
<td>${fmtPrice(t.price)}</td>
<td>${t.order_krw ? fmtMoney(t.order_krw) : "-"}</td>
<td>${t.fee_krw ? fmtMoney(t.fee_krw) : "-"}</td>
<td>${fmtMoney(t.cash_after)}</td>
<td>${t.coin_after.toFixed(8)}</td>
<td>${t.skipped ? (t.skip_reason || "스킵") : "분할 " + t.cluster_size}</td>`;
tbody.appendChild(tr);
});
document.getElementById("trade-count").textContent = String((p.trades || []).length);
}
function init() {
DATA = window.CHART_DATA;
if (!DATA) throw new Error("차트 데이터 JS 없음");
const m = DATA.meta;
const simMode = !!DATA.sim_pnl;
const chartDays = m.chart_lookback_days || m.lookback_days;
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 ? " · 2단계 시뮬" : "";
document.getElementById("title").textContent =
`${m.symbol} Ground Truth${tier} (${m.interval_label}) — 차트 ${chartLabel} / GT ${gtLabel}${simSuffix}`;
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}`;
if (simMode) renderSimPanel();
updateLegInfo();
document.getElementById("status").textContent =
`전체 ${DATA.bar_count.toLocaleString()}봉 | 드래그=줌, 더블클릭=리셋`;
buildOverview(false);
document.getElementById("btn-home").onclick = goHome;
document.getElementById("btn-prev-leg").onclick = () => jumpToLeg(currentLegIdx - 1);
document.getElementById("btn-next-leg").onclick = () => jumpToLeg(currentLegIdx + 1);
document.getElementById("btn-all").onclick = goHome;
document.getElementById("btn-365d").onclick = () => showPeriod(365, "btn-365d", "최근 1년");
document.getElementById("btn-30d").onclick = () => showPeriod(30, "btn-30d", "최근 30일");
document.getElementById("btn-7d").onclick = () => showPeriod(7, "btn-7d", "최근 7일");
document.getElementById("btn-3d").onclick = () => showPeriod(3, "btn-3d", "최근 3일");
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
document.getElementById("btn-fit").onclick = applyFit;
document.getElementById("btn-markers").onclick = () => {
showMarkers = !showMarkers;
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";
if (overviewPlot) buildOverview(true);
if (detailChart) buildDetailCandles(lastDetailStart);
};
document.getElementById("btn-toggle-detail").onclick = () => {
detailVisible = !detailVisible;
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
document.getElementById("btn-toggle-detail").textContent = detailVisible ? "상세 숨김" : "상세 패널";
if (detailVisible && !detailChart) {
const { start } = sliceLastDays(7);
buildDetailCandles(start);
}
};
document.getElementById("overview").addEventListener("dblclick", () => {
if (currentMode === "overview") fitOverview();
});
window.addEventListener("resize", () => {
if (overviewPlot) buildOverview(true);
});
}
try { init(); } catch (err) {
document.getElementById("status").textContent = "데이터 로드 실패: " + err;
}
</script>
</body>
</html>