feat: 선물 차트 마커 UI 개선 및 동일 타점 세로 스택
마커 라벨 2배 확대, Y축 뒤집기 버튼 추가, 같은 시각 마커를 좌우 대신 위아래로 표시한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,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>
|
||||
@@ -118,13 +119,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;
|
||||
|
||||
@@ -199,7 +203,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);
|
||||
@@ -224,16 +228,23 @@
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -257,13 +268,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);
|
||||
@@ -313,8 +327,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 },
|
||||
{
|
||||
@@ -346,6 +363,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 = "";
|
||||
@@ -358,6 +376,7 @@
|
||||
borderVisible: true,
|
||||
minimumWidth: priceAxisW,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
invertScale: yAxisInverted,
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
width: wrap.clientWidth,
|
||||
@@ -382,28 +401,42 @@
|
||||
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 up = kindUp[kind];
|
||||
const arrowUp = visualUp(up);
|
||||
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);
|
||||
@@ -506,6 +539,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 fmtMoney(v) {
|
||||
return Math.round(v).toLocaleString("ko-KR") + "원";
|
||||
@@ -622,6 +664,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
@@ -96,6 +96,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>
|
||||
@@ -118,13 +119,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;
|
||||
|
||||
@@ -199,7 +203,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);
|
||||
@@ -224,16 +228,23 @@
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -257,13 +268,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);
|
||||
@@ -313,8 +327,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 },
|
||||
{
|
||||
@@ -346,6 +363,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 = "";
|
||||
@@ -358,6 +376,7 @@
|
||||
borderVisible: true,
|
||||
minimumWidth: priceAxisW,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
invertScale: yAxisInverted,
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
width: wrap.clientWidth,
|
||||
@@ -382,28 +401,42 @@
|
||||
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 up = kindUp[kind];
|
||||
const arrowUp = visualUp(up);
|
||||
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);
|
||||
@@ -506,6 +539,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 fmtMoney(v) {
|
||||
return Math.round(v).toLocaleString("ko-KR") + "원";
|
||||
@@ -622,6 +664,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
@@ -96,6 +96,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>
|
||||
@@ -118,13 +119,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;
|
||||
|
||||
@@ -199,7 +203,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);
|
||||
@@ -224,16 +228,23 @@
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -257,13 +268,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);
|
||||
@@ -313,8 +327,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 },
|
||||
{
|
||||
@@ -346,6 +363,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 = "";
|
||||
@@ -358,6 +376,7 @@
|
||||
borderVisible: true,
|
||||
minimumWidth: priceAxisW,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
invertScale: yAxisInverted,
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
width: wrap.clientWidth,
|
||||
@@ -382,28 +401,42 @@
|
||||
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 up = kindUp[kind];
|
||||
const arrowUp = visualUp(up);
|
||||
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);
|
||||
@@ -506,6 +539,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 fmtMoney(v) {
|
||||
return Math.round(v).toLocaleString("ko-KR") + "원";
|
||||
@@ -622,6 +664,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);
|
||||
@@ -186,16 +190,23 @@
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
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,42 @@
|
||||
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 up = kindUp[kind];
|
||||
const arrowUp = visualUp(up);
|
||||
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 +501,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 +570,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);
|
||||
@@ -186,16 +190,23 @@
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
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,42 @@
|
||||
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 up = kindUp[kind];
|
||||
const arrowUp = visualUp(up);
|
||||
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 +501,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 +570,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);
|
||||
@@ -186,16 +190,23 @@
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
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,42 @@
|
||||
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 up = kindUp[kind];
|
||||
const arrowUp = visualUp(up);
|
||||
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 +501,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 +570,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
@@ -157,11 +157,11 @@
|
||||
el.textContent = `타점 ${currentLegIdx + 1} / ${total}`;
|
||||
}
|
||||
|
||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
||||
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||
const SIM_START_COLOR = "#7b1fa2";
|
||||
const ARROW_HALF = 6;
|
||||
const ARROW_HEIGHT = 8;
|
||||
const LABEL_OFFSET_X = 8;
|
||||
const ARROW_HALF = 12;
|
||||
const ARROW_HEIGHT = 16;
|
||||
const LABEL_OFFSET_X = 16;
|
||||
|
||||
function markerSuffix(signalType) {
|
||||
if (signalType === "pullback") return "*";
|
||||
@@ -186,7 +186,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);
|
||||
@@ -223,8 +223,8 @@
|
||||
ctx.font = MARKER_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
||||
ctx.lineWidth = 2;
|
||||
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, x, labelY);
|
||||
@@ -234,7 +234,7 @@
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
const LABEL_GAP = 12;
|
||||
const LABEL_GAP = 24;
|
||||
|
||||
function drawMarkers(u, buys, sells) {
|
||||
if (!showMarkers) return;
|
||||
@@ -295,7 +295,7 @@
|
||||
const opts = {
|
||||
width: document.getElementById("overview").clientWidth,
|
||||
height: 480,
|
||||
padding: [28, 10, 28, 10],
|
||||
padding: [40, 10, 40, 10],
|
||||
scales: { x: { time: true } },
|
||||
axes: [
|
||||
{ gap: 6 },
|
||||
@@ -375,7 +375,7 @@
|
||||
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,
|
||||
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||
text: sm.label || "거래시작",
|
||||
});
|
||||
}
|
||||
@@ -385,7 +385,7 @@
|
||||
time: m.time, position: "belowBar",
|
||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||
shape: "arrowUp", size: 5,
|
||||
shape: "arrowUp", size: 10,
|
||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
@@ -393,7 +393,7 @@
|
||||
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: 5,
|
||||
shape: "arrowDown", size: 10,
|
||||
text: "S" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -157,11 +157,11 @@
|
||||
el.textContent = `타점 ${currentLegIdx + 1} / ${total}`;
|
||||
}
|
||||
|
||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
||||
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||
const SIM_START_COLOR = "#7b1fa2";
|
||||
const ARROW_HALF = 6;
|
||||
const ARROW_HEIGHT = 8;
|
||||
const LABEL_OFFSET_X = 8;
|
||||
const ARROW_HALF = 12;
|
||||
const ARROW_HEIGHT = 16;
|
||||
const LABEL_OFFSET_X = 16;
|
||||
|
||||
function markerSuffix(signalType) {
|
||||
if (signalType === "pullback") return "*";
|
||||
@@ -186,7 +186,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);
|
||||
@@ -223,8 +223,8 @@
|
||||
ctx.font = MARKER_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
||||
ctx.lineWidth = 2;
|
||||
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, x, labelY);
|
||||
@@ -234,7 +234,7 @@
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
const LABEL_GAP = 12;
|
||||
const LABEL_GAP = 24;
|
||||
|
||||
function drawMarkers(u, buys, sells) {
|
||||
if (!showMarkers) return;
|
||||
@@ -295,7 +295,7 @@
|
||||
const opts = {
|
||||
width: document.getElementById("overview").clientWidth,
|
||||
height: 480,
|
||||
padding: [28, 10, 28, 10],
|
||||
padding: [40, 10, 40, 10],
|
||||
scales: { x: { time: true } },
|
||||
axes: [
|
||||
{ gap: 6 },
|
||||
@@ -375,7 +375,7 @@
|
||||
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,
|
||||
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||
text: sm.label || "거래시작",
|
||||
});
|
||||
}
|
||||
@@ -385,7 +385,7 @@
|
||||
time: m.time, position: "belowBar",
|
||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||
shape: "arrowUp", size: 5,
|
||||
shape: "arrowUp", size: 10,
|
||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
@@ -393,7 +393,7 @@
|
||||
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: 5,
|
||||
shape: "arrowDown", size: 10,
|
||||
text: "S" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -157,11 +157,11 @@
|
||||
el.textContent = `타점 ${currentLegIdx + 1} / ${total}`;
|
||||
}
|
||||
|
||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
||||
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||
const SIM_START_COLOR = "#7b1fa2";
|
||||
const ARROW_HALF = 6;
|
||||
const ARROW_HEIGHT = 8;
|
||||
const LABEL_OFFSET_X = 8;
|
||||
const ARROW_HALF = 12;
|
||||
const ARROW_HEIGHT = 16;
|
||||
const LABEL_OFFSET_X = 16;
|
||||
|
||||
function markerSuffix(signalType) {
|
||||
if (signalType === "pullback") return "*";
|
||||
@@ -186,7 +186,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);
|
||||
@@ -223,8 +223,8 @@
|
||||
ctx.font = MARKER_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
||||
ctx.lineWidth = 2;
|
||||
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, x, labelY);
|
||||
@@ -234,7 +234,7 @@
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
const LABEL_GAP = 12;
|
||||
const LABEL_GAP = 24;
|
||||
|
||||
function drawMarkers(u, buys, sells) {
|
||||
if (!showMarkers) return;
|
||||
@@ -295,7 +295,7 @@
|
||||
const opts = {
|
||||
width: document.getElementById("overview").clientWidth,
|
||||
height: 480,
|
||||
padding: [28, 10, 28, 10],
|
||||
padding: [40, 10, 40, 10],
|
||||
scales: { x: { time: true } },
|
||||
axes: [
|
||||
{ gap: 6 },
|
||||
@@ -375,7 +375,7 @@
|
||||
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,
|
||||
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||
text: sm.label || "거래시작",
|
||||
});
|
||||
}
|
||||
@@ -385,7 +385,7 @@
|
||||
time: m.time, position: "belowBar",
|
||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||
shape: "arrowUp", size: 5,
|
||||
shape: "arrowUp", size: 10,
|
||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
@@ -393,7 +393,7 @@
|
||||
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: 5,
|
||||
shape: "arrowDown", size: 10,
|
||||
text: "S" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -371,11 +371,11 @@ __EXTRA_BODY__
|
||||
el.textContent = `타점 ${currentLegIdx + 1} / ${total}`;
|
||||
}
|
||||
|
||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
||||
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||
const SIM_START_COLOR = "#7b1fa2";
|
||||
const ARROW_HALF = 6;
|
||||
const ARROW_HEIGHT = 8;
|
||||
const LABEL_OFFSET_X = 8;
|
||||
const ARROW_HALF = 12;
|
||||
const ARROW_HEIGHT = 16;
|
||||
const LABEL_OFFSET_X = 16;
|
||||
|
||||
function markerSuffix(signalType) {
|
||||
if (signalType === "pullback") return "*";
|
||||
@@ -400,7 +400,7 @@ __EXTRA_BODY__
|
||||
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);
|
||||
@@ -437,8 +437,8 @@ __EXTRA_BODY__
|
||||
ctx.font = MARKER_FONT;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
||||
ctx.lineWidth = 2;
|
||||
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.strokeText(label, x, labelY);
|
||||
@@ -448,7 +448,7 @@ __EXTRA_BODY__
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
const LABEL_GAP = 12;
|
||||
const LABEL_GAP = 24;
|
||||
|
||||
function drawMarkers(u, buys, sells) {
|
||||
if (!showMarkers) return;
|
||||
@@ -509,7 +509,7 @@ __EXTRA_BODY__
|
||||
const opts = {
|
||||
width: document.getElementById("overview").clientWidth,
|
||||
height: 480,
|
||||
padding: [28, 10, 28, 10],
|
||||
padding: [40, 10, 40, 10],
|
||||
scales: { x: { time: true } },
|
||||
axes: [
|
||||
{ gap: 6 },
|
||||
@@ -589,7 +589,7 @@ __EXTRA_BODY__
|
||||
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,
|
||||
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||
text: sm.label || "거래시작",
|
||||
});
|
||||
}
|
||||
@@ -599,7 +599,7 @@ __EXTRA_BODY__
|
||||
time: m.time, position: "belowBar",
|
||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||
shape: "arrowUp", size: 5,
|
||||
shape: "arrowUp", size: 10,
|
||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
@@ -607,7 +607,7 @@ __EXTRA_BODY__
|
||||
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: 5,
|
||||
shape: "arrowDown", size: 10,
|
||||
text: "S" + m.marker_id + markerSuffix(m.signal_type),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,16 +20,15 @@ from deepcoin.ground_truth.futures import (
|
||||
)
|
||||
|
||||
|
||||
def _stagger_marker_positions(
|
||||
def _stack_marker_positions(
|
||||
long_open: list[dict[str, Any]],
|
||||
long_close: list[dict[str, Any]],
|
||||
short_open: list[dict[str, Any]],
|
||||
short_close: list[dict[str, Any]],
|
||||
stagger_px: int = 18,
|
||||
) -> None:
|
||||
"""동일 시각에 겹치는 마커에 수평 오프셋을 부여한다.
|
||||
"""동일 시각에 겹치는 마커에 세로 스택 인덱스를 부여한다.
|
||||
|
||||
GT 매도 봉에서 L↓(롱 청산)과 S↓(숏 진입)이 같은 좌표에 겹치는 문제를 방지한다.
|
||||
같은 봉의 L↓·S↓ 등이 좌우로 벌어지지 않고 동일 X에서 위아래로 쌓이도록 한다.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -47,13 +46,15 @@ def _stagger_marker_positions(
|
||||
|
||||
for markers_at_time in groups.values():
|
||||
if len(markers_at_time) <= 1:
|
||||
for marker in markers_at_time:
|
||||
marker["stack_index"] = 0
|
||||
continue
|
||||
markers_at_time.sort(
|
||||
key=lambda m: kind_rank.get(f"{m.get('position')}_{m.get('action')}", 9)
|
||||
)
|
||||
count = len(markers_at_time)
|
||||
for index, marker in enumerate(markers_at_time):
|
||||
marker["x_offset_px"] = int((index - (count - 1) / 2) * stagger_px)
|
||||
marker["stack_index"] = index
|
||||
marker.pop("x_offset_px", None)
|
||||
|
||||
_FUTURES_HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
@@ -115,6 +116,7 @@ __EXTRA_BODY__
|
||||
<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>
|
||||
@@ -137,13 +139,16 @@ __EXTRA_BODY__
|
||||
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;
|
||||
|
||||
@@ -218,7 +223,7 @@ __EXTRA_BODY__
|
||||
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);
|
||||
@@ -227,6 +232,10 @@ __EXTRA_BODY__
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
function visualUp(up) {
|
||||
return yAxisInverted ? !up : up;
|
||||
}
|
||||
|
||||
function drawTriangleOnLine(ctx, x, lineY, up, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
@@ -245,14 +254,17 @@ __EXTRA_BODY__
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -276,13 +288,16 @@ __EXTRA_BODY__
|
||||
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);
|
||||
@@ -332,8 +347,11 @@ __EXTRA_BODY__
|
||||
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 },
|
||||
{
|
||||
@@ -365,6 +383,7 @@ __EXTRA_BODY__
|
||||
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 = "";
|
||||
@@ -377,6 +396,7 @@ __EXTRA_BODY__
|
||||
borderVisible: true,
|
||||
minimumWidth: priceAxisW,
|
||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||
invertScale: yAxisInverted,
|
||||
},
|
||||
timeScale: { timeVisible: true, secondsVisible: false },
|
||||
width: wrap.clientWidth,
|
||||
@@ -401,28 +421,41 @@ __EXTRA_BODY__
|
||||
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);
|
||||
@@ -525,6 +558,15 @@ __EXTRA_BODY__
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
__EXTRA_SCRIPT__
|
||||
|
||||
function renderLegend() {
|
||||
@@ -585,6 +627,7 @@ __EXTRA_SCRIPT__
|
||||
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 ? "마커 숨김" : "마커 표시";
|
||||
@@ -646,7 +689,7 @@ def _build_futures_chart_payload(
|
||||
"meta": chart_meta,
|
||||
"bar_count": len(df),
|
||||
}
|
||||
_stagger_marker_positions(
|
||||
_stack_marker_positions(
|
||||
payload["long_open_markers"],
|
||||
payload["long_close_markers"],
|
||||
payload["short_open_markers"],
|
||||
@@ -857,7 +900,7 @@ def _build_futures_sim_chart_payload(
|
||||
start_marker = _sim_start_marker(df, sim_pnl)
|
||||
if start_marker is not None:
|
||||
payload["sim_start_marker"] = start_marker
|
||||
_stagger_marker_positions(
|
||||
_stack_marker_positions(
|
||||
payload["long_open_markers"],
|
||||
payload["long_close_markers"],
|
||||
payload["short_open_markers"],
|
||||
|
||||
Reference in New Issue
Block a user