refactor: GT 차트 폴더 구조 정리 및 2단계 산출물 추가
현물 GT 차트를 docs/02_ground_truth/gt로 통일하고, 선물 GT는 futures/gt로 이동하며 매매 기법 JSON을 추가한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
<button id="btn-zoom-in" title="확대">+ 확대</button>
|
||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||
@@ -80,13 +81,16 @@
|
||||
let showMarkers = true;
|
||||
let detailVisible = false;
|
||||
let lastDetailStart = 0;
|
||||
let lastDetailEnd = 0;
|
||||
let yAxisInverted = false;
|
||||
|
||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
||||
const ARROW_HALF = 6;
|
||||
const ARROW_HEIGHT = 8;
|
||||
const LABEL_OFFSET_X = 8;
|
||||
const LABEL_GAP = 12;
|
||||
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||
const ARROW_HALF = 12;
|
||||
const ARROW_HEIGHT = 16;
|
||||
const LABEL_OFFSET_X = 16;
|
||||
const LABEL_GAP = 24;
|
||||
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||
const SIM_START_COLOR = "#7b1fa2";
|
||||
let axisMeasureCtx = null;
|
||||
|
||||
@@ -161,7 +165,7 @@
|
||||
ctx.font = MARKER_FONT;
|
||||
const lx = x + LABEL_OFFSET_X;
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, lx, labelY);
|
||||
@@ -170,6 +174,10 @@
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
function drawTriangleOnLine(ctx, x, lineY, up, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
@@ -188,14 +196,17 @@
|
||||
|
||||
function drawOneMarker(u, m, color, up, label) {
|
||||
const ctx = u.ctx;
|
||||
const xOff = m.x_offset_px || 0;
|
||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
||||
const x = u.valToPos(m.time, "x", true);
|
||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
||||
const labelY = up
|
||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
||||
const arrowUp = visualUp(up);
|
||||
const stackIdx = m.stack_index || 0;
|
||||
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||
const anchorY = lineY + stackShift;
|
||||
drawTriangleOnLine(ctx, x, anchorY, arrowUp, color);
|
||||
const labelY = arrowUp
|
||||
? anchorY + ARROW_HEIGHT + LABEL_GAP
|
||||
: anchorY - ARROW_HEIGHT - LABEL_GAP;
|
||||
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||
}
|
||||
|
||||
@@ -219,13 +230,16 @@
|
||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||
const color = SIM_START_COLOR;
|
||||
drawTriangleOnLine(ctx, x, lineY, false, color);
|
||||
const arrowUp = visualUp(false);
|
||||
drawTriangleOnLine(ctx, x, lineY, arrowUp, color);
|
||||
const label = marker.label || "거래시작";
|
||||
ctx.font = MARKER_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||
const labelY = arrowUp
|
||||
? lineY + ARROW_HEIGHT + 12
|
||||
: lineY - ARROW_HEIGHT - 12;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, x, labelY);
|
||||
@@ -275,8 +289,11 @@
|
||||
const opts = {
|
||||
width: document.getElementById("overview").clientWidth,
|
||||
height: 480,
|
||||
padding: [28, 10, 28, 10],
|
||||
scales: { x: { time: true } },
|
||||
padding: [40, 10, 40, 10],
|
||||
scales: {
|
||||
x: { time: true },
|
||||
y: { dir: yAxisInverted ? -1 : 1 },
|
||||
},
|
||||
axes: [
|
||||
{ gap: 6 },
|
||||
{
|
||||
@@ -308,6 +325,7 @@
|
||||
function buildDetailCandles(startIdx, endIdx) {
|
||||
lastDetailStart = startIdx;
|
||||
const end = endIdx || DATA.times.length;
|
||||
lastDetailEnd = end;
|
||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||
const wrap = document.getElementById("detail");
|
||||
wrap.innerHTML = "";
|
||||
@@ -320,6 +338,7 @@
|
||||
borderVisible: true,
|
||||
minimumWidth: priceAxisW,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
invertScale: yAxisInverted,
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
width: wrap.clientWidth,
|
||||
@@ -344,28 +363,41 @@
|
||||
const t1 = DATA.times[end - 1];
|
||||
const markers = [];
|
||||
if (showMarkers) {
|
||||
const add = (list, kind, up) => {
|
||||
const colorMap = {
|
||||
longOpen: COLORS.longOpen,
|
||||
longClose: COLORS.longClose,
|
||||
shortOpen: COLORS.shortOpen,
|
||||
shortClose: COLORS.shortClose,
|
||||
};
|
||||
(list || []).forEach(m => {
|
||||
if (m.time >= t0 && m.time <= t1) markers.push({
|
||||
time: m.time,
|
||||
position: up ? "belowBar" : "aboveBar",
|
||||
color: colorMap[kind],
|
||||
shape: up ? "arrowUp" : "arrowDown",
|
||||
size: 5,
|
||||
text: markerLabel(kind, m),
|
||||
});
|
||||
});
|
||||
const colorMap = {
|
||||
longOpen: COLORS.longOpen,
|
||||
longClose: COLORS.longClose,
|
||||
shortOpen: COLORS.shortOpen,
|
||||
shortClose: COLORS.shortClose,
|
||||
};
|
||||
add(DATA.long_open_markers, "longOpen", true);
|
||||
add(DATA.long_close_markers, "longClose", false);
|
||||
add(DATA.short_open_markers, "shortOpen", false);
|
||||
add(DATA.short_close_markers, "shortClose", true);
|
||||
const kindUp = {
|
||||
longOpen: true,
|
||||
longClose: false,
|
||||
shortOpen: false,
|
||||
shortClose: true,
|
||||
};
|
||||
const pending = [];
|
||||
[
|
||||
[DATA.long_open_markers, "longOpen"],
|
||||
[DATA.long_close_markers, "longClose"],
|
||||
[DATA.short_open_markers, "shortOpen"],
|
||||
[DATA.short_close_markers, "shortClose"],
|
||||
].forEach(([list, kind]) => {
|
||||
(list || []).forEach(m => {
|
||||
if (m.time >= t0 && m.time <= t1) pending.push({ m, kind });
|
||||
});
|
||||
});
|
||||
pending.sort((a, b) => a.m.time - b.m.time || (a.m.stack_index || 0) - (b.m.stack_index || 0));
|
||||
pending.forEach(({ m, kind }) => {
|
||||
const arrowUp = visualUp(kindUp[kind]);
|
||||
markers.push({
|
||||
time: m.time,
|
||||
position: arrowUp ? "belowBar" : "aboveBar",
|
||||
color: colorMap[kind],
|
||||
shape: arrowUp ? "arrowUp" : "arrowDown",
|
||||
size: 10,
|
||||
text: markerLabel(kind, m),
|
||||
});
|
||||
});
|
||||
}
|
||||
markers.sort((a, b) => a.time - b.time);
|
||||
detailSeries.setMarkers(markers);
|
||||
@@ -468,6 +500,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleYFlip() {
|
||||
yAxisInverted = !yAxisInverted;
|
||||
const btn = document.getElementById("btn-flip-y");
|
||||
btn.classList.toggle("active", yAxisInverted);
|
||||
btn.textContent = yAxisInverted ? "↕ 복원" : "↕ 뒤집기";
|
||||
if (overviewPlot) buildOverview(true);
|
||||
if (detailChart) buildDetailCandles(lastDetailStart, lastDetailEnd || undefined);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function renderLegend() {
|
||||
@@ -528,6 +569,7 @@
|
||||
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-flip-y").onclick = toggleYFlip;
|
||||
document.getElementById("btn-markers").onclick = () => {
|
||||
showMarkers = !showMarkers;
|
||||
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -58,6 +58,7 @@
|
||||
<button id="btn-zoom-in" title="확대">+ 확대</button>
|
||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||
@@ -80,13 +81,16 @@
|
||||
let showMarkers = true;
|
||||
let detailVisible = false;
|
||||
let lastDetailStart = 0;
|
||||
let lastDetailEnd = 0;
|
||||
let yAxisInverted = false;
|
||||
|
||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
||||
const ARROW_HALF = 6;
|
||||
const ARROW_HEIGHT = 8;
|
||||
const LABEL_OFFSET_X = 8;
|
||||
const LABEL_GAP = 12;
|
||||
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||
const ARROW_HALF = 12;
|
||||
const ARROW_HEIGHT = 16;
|
||||
const LABEL_OFFSET_X = 16;
|
||||
const LABEL_GAP = 24;
|
||||
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||
const SIM_START_COLOR = "#7b1fa2";
|
||||
let axisMeasureCtx = null;
|
||||
|
||||
@@ -161,7 +165,7 @@
|
||||
ctx.font = MARKER_FONT;
|
||||
const lx = x + LABEL_OFFSET_X;
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, lx, labelY);
|
||||
@@ -170,6 +174,10 @@
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
function drawTriangleOnLine(ctx, x, lineY, up, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
@@ -188,14 +196,17 @@
|
||||
|
||||
function drawOneMarker(u, m, color, up, label) {
|
||||
const ctx = u.ctx;
|
||||
const xOff = m.x_offset_px || 0;
|
||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
||||
const x = u.valToPos(m.time, "x", true);
|
||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
||||
const labelY = up
|
||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
||||
const arrowUp = visualUp(up);
|
||||
const stackIdx = m.stack_index || 0;
|
||||
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||
const anchorY = lineY + stackShift;
|
||||
drawTriangleOnLine(ctx, x, anchorY, arrowUp, color);
|
||||
const labelY = arrowUp
|
||||
? anchorY + ARROW_HEIGHT + LABEL_GAP
|
||||
: anchorY - ARROW_HEIGHT - LABEL_GAP;
|
||||
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||
}
|
||||
|
||||
@@ -219,13 +230,16 @@
|
||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||
const color = SIM_START_COLOR;
|
||||
drawTriangleOnLine(ctx, x, lineY, false, color);
|
||||
const arrowUp = visualUp(false);
|
||||
drawTriangleOnLine(ctx, x, lineY, arrowUp, color);
|
||||
const label = marker.label || "거래시작";
|
||||
ctx.font = MARKER_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||
const labelY = arrowUp
|
||||
? lineY + ARROW_HEIGHT + 12
|
||||
: lineY - ARROW_HEIGHT - 12;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, x, labelY);
|
||||
@@ -275,8 +289,11 @@
|
||||
const opts = {
|
||||
width: document.getElementById("overview").clientWidth,
|
||||
height: 480,
|
||||
padding: [28, 10, 28, 10],
|
||||
scales: { x: { time: true } },
|
||||
padding: [40, 10, 40, 10],
|
||||
scales: {
|
||||
x: { time: true },
|
||||
y: { dir: yAxisInverted ? -1 : 1 },
|
||||
},
|
||||
axes: [
|
||||
{ gap: 6 },
|
||||
{
|
||||
@@ -308,6 +325,7 @@
|
||||
function buildDetailCandles(startIdx, endIdx) {
|
||||
lastDetailStart = startIdx;
|
||||
const end = endIdx || DATA.times.length;
|
||||
lastDetailEnd = end;
|
||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||
const wrap = document.getElementById("detail");
|
||||
wrap.innerHTML = "";
|
||||
@@ -320,6 +338,7 @@
|
||||
borderVisible: true,
|
||||
minimumWidth: priceAxisW,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
invertScale: yAxisInverted,
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
width: wrap.clientWidth,
|
||||
@@ -344,28 +363,41 @@
|
||||
const t1 = DATA.times[end - 1];
|
||||
const markers = [];
|
||||
if (showMarkers) {
|
||||
const add = (list, kind, up) => {
|
||||
const colorMap = {
|
||||
longOpen: COLORS.longOpen,
|
||||
longClose: COLORS.longClose,
|
||||
shortOpen: COLORS.shortOpen,
|
||||
shortClose: COLORS.shortClose,
|
||||
};
|
||||
(list || []).forEach(m => {
|
||||
if (m.time >= t0 && m.time <= t1) markers.push({
|
||||
time: m.time,
|
||||
position: up ? "belowBar" : "aboveBar",
|
||||
color: colorMap[kind],
|
||||
shape: up ? "arrowUp" : "arrowDown",
|
||||
size: 5,
|
||||
text: markerLabel(kind, m),
|
||||
});
|
||||
});
|
||||
const colorMap = {
|
||||
longOpen: COLORS.longOpen,
|
||||
longClose: COLORS.longClose,
|
||||
shortOpen: COLORS.shortOpen,
|
||||
shortClose: COLORS.shortClose,
|
||||
};
|
||||
add(DATA.long_open_markers, "longOpen", true);
|
||||
add(DATA.long_close_markers, "longClose", false);
|
||||
add(DATA.short_open_markers, "shortOpen", false);
|
||||
add(DATA.short_close_markers, "shortClose", true);
|
||||
const kindUp = {
|
||||
longOpen: true,
|
||||
longClose: false,
|
||||
shortOpen: false,
|
||||
shortClose: true,
|
||||
};
|
||||
const pending = [];
|
||||
[
|
||||
[DATA.long_open_markers, "longOpen"],
|
||||
[DATA.long_close_markers, "longClose"],
|
||||
[DATA.short_open_markers, "shortOpen"],
|
||||
[DATA.short_close_markers, "shortClose"],
|
||||
].forEach(([list, kind]) => {
|
||||
(list || []).forEach(m => {
|
||||
if (m.time >= t0 && m.time <= t1) pending.push({ m, kind });
|
||||
});
|
||||
});
|
||||
pending.sort((a, b) => a.m.time - b.m.time || (a.m.stack_index || 0) - (b.m.stack_index || 0));
|
||||
pending.forEach(({ m, kind }) => {
|
||||
const arrowUp = visualUp(kindUp[kind]);
|
||||
markers.push({
|
||||
time: m.time,
|
||||
position: arrowUp ? "belowBar" : "aboveBar",
|
||||
color: colorMap[kind],
|
||||
shape: arrowUp ? "arrowUp" : "arrowDown",
|
||||
size: 10,
|
||||
text: markerLabel(kind, m),
|
||||
});
|
||||
});
|
||||
}
|
||||
markers.sort((a, b) => a.time - b.time);
|
||||
detailSeries.setMarkers(markers);
|
||||
@@ -468,6 +500,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleYFlip() {
|
||||
yAxisInverted = !yAxisInverted;
|
||||
const btn = document.getElementById("btn-flip-y");
|
||||
btn.classList.toggle("active", yAxisInverted);
|
||||
btn.textContent = yAxisInverted ? "↕ 복원" : "↕ 뒤집기";
|
||||
if (overviewPlot) buildOverview(true);
|
||||
if (detailChart) buildDetailCandles(lastDetailStart, lastDetailEnd || undefined);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function renderLegend() {
|
||||
@@ -528,6 +569,7 @@
|
||||
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-flip-y").onclick = toggleYFlip;
|
||||
document.getElementById("btn-markers").onclick = () => {
|
||||
showMarkers = !showMarkers;
|
||||
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -58,6 +58,7 @@
|
||||
<button id="btn-zoom-in" title="확대">+ 확대</button>
|
||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||
@@ -80,13 +81,16 @@
|
||||
let showMarkers = true;
|
||||
let detailVisible = false;
|
||||
let lastDetailStart = 0;
|
||||
let lastDetailEnd = 0;
|
||||
let yAxisInverted = false;
|
||||
|
||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
||||
const ARROW_HALF = 6;
|
||||
const ARROW_HEIGHT = 8;
|
||||
const LABEL_OFFSET_X = 8;
|
||||
const LABEL_GAP = 12;
|
||||
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||
const ARROW_HALF = 12;
|
||||
const ARROW_HEIGHT = 16;
|
||||
const LABEL_OFFSET_X = 16;
|
||||
const LABEL_GAP = 24;
|
||||
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||
const SIM_START_COLOR = "#7b1fa2";
|
||||
let axisMeasureCtx = null;
|
||||
|
||||
@@ -161,7 +165,7 @@
|
||||
ctx.font = MARKER_FONT;
|
||||
const lx = x + LABEL_OFFSET_X;
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, lx, labelY);
|
||||
@@ -170,6 +174,10 @@
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
function drawTriangleOnLine(ctx, x, lineY, up, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
@@ -188,14 +196,17 @@
|
||||
|
||||
function drawOneMarker(u, m, color, up, label) {
|
||||
const ctx = u.ctx;
|
||||
const xOff = m.x_offset_px || 0;
|
||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
||||
const x = u.valToPos(m.time, "x", true);
|
||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
||||
const labelY = up
|
||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
||||
const arrowUp = visualUp(up);
|
||||
const stackIdx = m.stack_index || 0;
|
||||
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||
const anchorY = lineY + stackShift;
|
||||
drawTriangleOnLine(ctx, x, anchorY, arrowUp, color);
|
||||
const labelY = arrowUp
|
||||
? anchorY + ARROW_HEIGHT + LABEL_GAP
|
||||
: anchorY - ARROW_HEIGHT - LABEL_GAP;
|
||||
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||
}
|
||||
|
||||
@@ -219,13 +230,16 @@
|
||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||
const color = SIM_START_COLOR;
|
||||
drawTriangleOnLine(ctx, x, lineY, false, color);
|
||||
const arrowUp = visualUp(false);
|
||||
drawTriangleOnLine(ctx, x, lineY, arrowUp, color);
|
||||
const label = marker.label || "거래시작";
|
||||
ctx.font = MARKER_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||
const labelY = arrowUp
|
||||
? lineY + ARROW_HEIGHT + 12
|
||||
: lineY - ARROW_HEIGHT - 12;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, x, labelY);
|
||||
@@ -275,8 +289,11 @@
|
||||
const opts = {
|
||||
width: document.getElementById("overview").clientWidth,
|
||||
height: 480,
|
||||
padding: [28, 10, 28, 10],
|
||||
scales: { x: { time: true } },
|
||||
padding: [40, 10, 40, 10],
|
||||
scales: {
|
||||
x: { time: true },
|
||||
y: { dir: yAxisInverted ? -1 : 1 },
|
||||
},
|
||||
axes: [
|
||||
{ gap: 6 },
|
||||
{
|
||||
@@ -308,6 +325,7 @@
|
||||
function buildDetailCandles(startIdx, endIdx) {
|
||||
lastDetailStart = startIdx;
|
||||
const end = endIdx || DATA.times.length;
|
||||
lastDetailEnd = end;
|
||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||
const wrap = document.getElementById("detail");
|
||||
wrap.innerHTML = "";
|
||||
@@ -320,6 +338,7 @@
|
||||
borderVisible: true,
|
||||
minimumWidth: priceAxisW,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
invertScale: yAxisInverted,
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
width: wrap.clientWidth,
|
||||
@@ -344,28 +363,41 @@
|
||||
const t1 = DATA.times[end - 1];
|
||||
const markers = [];
|
||||
if (showMarkers) {
|
||||
const add = (list, kind, up) => {
|
||||
const colorMap = {
|
||||
longOpen: COLORS.longOpen,
|
||||
longClose: COLORS.longClose,
|
||||
shortOpen: COLORS.shortOpen,
|
||||
shortClose: COLORS.shortClose,
|
||||
};
|
||||
(list || []).forEach(m => {
|
||||
if (m.time >= t0 && m.time <= t1) markers.push({
|
||||
time: m.time,
|
||||
position: up ? "belowBar" : "aboveBar",
|
||||
color: colorMap[kind],
|
||||
shape: up ? "arrowUp" : "arrowDown",
|
||||
size: 5,
|
||||
text: markerLabel(kind, m),
|
||||
});
|
||||
});
|
||||
const colorMap = {
|
||||
longOpen: COLORS.longOpen,
|
||||
longClose: COLORS.longClose,
|
||||
shortOpen: COLORS.shortOpen,
|
||||
shortClose: COLORS.shortClose,
|
||||
};
|
||||
add(DATA.long_open_markers, "longOpen", true);
|
||||
add(DATA.long_close_markers, "longClose", false);
|
||||
add(DATA.short_open_markers, "shortOpen", false);
|
||||
add(DATA.short_close_markers, "shortClose", true);
|
||||
const kindUp = {
|
||||
longOpen: true,
|
||||
longClose: false,
|
||||
shortOpen: false,
|
||||
shortClose: true,
|
||||
};
|
||||
const pending = [];
|
||||
[
|
||||
[DATA.long_open_markers, "longOpen"],
|
||||
[DATA.long_close_markers, "longClose"],
|
||||
[DATA.short_open_markers, "shortOpen"],
|
||||
[DATA.short_close_markers, "shortClose"],
|
||||
].forEach(([list, kind]) => {
|
||||
(list || []).forEach(m => {
|
||||
if (m.time >= t0 && m.time <= t1) pending.push({ m, kind });
|
||||
});
|
||||
});
|
||||
pending.sort((a, b) => a.m.time - b.m.time || (a.m.stack_index || 0) - (b.m.stack_index || 0));
|
||||
pending.forEach(({ m, kind }) => {
|
||||
const arrowUp = visualUp(kindUp[kind]);
|
||||
markers.push({
|
||||
time: m.time,
|
||||
position: arrowUp ? "belowBar" : "aboveBar",
|
||||
color: colorMap[kind],
|
||||
shape: arrowUp ? "arrowUp" : "arrowDown",
|
||||
size: 10,
|
||||
text: markerLabel(kind, m),
|
||||
});
|
||||
});
|
||||
}
|
||||
markers.sort((a, b) => a.time - b.time);
|
||||
detailSeries.setMarkers(markers);
|
||||
@@ -468,6 +500,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleYFlip() {
|
||||
yAxisInverted = !yAxisInverted;
|
||||
const btn = document.getElementById("btn-flip-y");
|
||||
btn.classList.toggle("active", yAxisInverted);
|
||||
btn.textContent = yAxisInverted ? "↕ 복원" : "↕ 뒤집기";
|
||||
if (overviewPlot) buildOverview(true);
|
||||
if (detailChart) buildDetailCandles(lastDetailStart, lastDetailEnd || undefined);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function renderLegend() {
|
||||
@@ -528,6 +569,7 @@
|
||||
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-flip-y").onclick = toggleYFlip;
|
||||
document.getElementById("btn-markers").onclick = () => {
|
||||
showMarkers = !showMarkers;
|
||||
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user