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-in" title="확대">+ 확대</button>
|
||||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||||
|
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||||
@@ -118,13 +119,16 @@
|
|||||||
let showMarkers = true;
|
let showMarkers = true;
|
||||||
let detailVisible = false;
|
let detailVisible = false;
|
||||||
let lastDetailStart = 0;
|
let lastDetailStart = 0;
|
||||||
|
let lastDetailEnd = 0;
|
||||||
|
let yAxisInverted = false;
|
||||||
|
|
||||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||||
const SIM_START_COLOR = "#7b1fa2";
|
const SIM_START_COLOR = "#7b1fa2";
|
||||||
let axisMeasureCtx = null;
|
let axisMeasureCtx = null;
|
||||||
|
|
||||||
@@ -199,7 +203,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -224,16 +228,23 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visualUp(up) {
|
||||||
|
return yAxisInverted ? !up : up;
|
||||||
|
}
|
||||||
|
|
||||||
function drawOneMarker(u, m, color, up, label) {
|
function drawOneMarker(u, m, color, up, label) {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const xOff = m.x_offset_px || 0;
|
const x = u.valToPos(m.time, "x", true);
|
||||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
|
||||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
const arrowUp = visualUp(up);
|
||||||
const labelY = up
|
const stackIdx = m.stack_index || 0;
|
||||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
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);
|
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,13 +268,16 @@
|
|||||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
const color = SIM_START_COLOR;
|
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 || "거래시작";
|
const label = marker.label || "거래시작";
|
||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = arrowUp
|
||||||
ctx.lineWidth = 2;
|
? lineY + ARROW_HEIGHT + 12
|
||||||
|
: lineY - ARROW_HEIGHT - 12;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -313,8 +327,11 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: {
|
||||||
|
x: { time: true },
|
||||||
|
y: { dir: yAxisInverted ? -1 : 1 },
|
||||||
|
},
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
{
|
{
|
||||||
@@ -346,6 +363,7 @@
|
|||||||
function buildDetailCandles(startIdx, endIdx) {
|
function buildDetailCandles(startIdx, endIdx) {
|
||||||
lastDetailStart = startIdx;
|
lastDetailStart = startIdx;
|
||||||
const end = endIdx || DATA.times.length;
|
const end = endIdx || DATA.times.length;
|
||||||
|
lastDetailEnd = end;
|
||||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||||
const wrap = document.getElementById("detail");
|
const wrap = document.getElementById("detail");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
@@ -358,6 +376,7 @@
|
|||||||
borderVisible: true,
|
borderVisible: true,
|
||||||
minimumWidth: priceAxisW,
|
minimumWidth: priceAxisW,
|
||||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||||
|
invertScale: yAxisInverted,
|
||||||
},
|
},
|
||||||
timeScale: { timeVisible: true, secondsVisible: false },
|
timeScale: { timeVisible: true, secondsVisible: false },
|
||||||
width: wrap.clientWidth,
|
width: wrap.clientWidth,
|
||||||
@@ -382,28 +401,42 @@
|
|||||||
const t1 = DATA.times[end - 1];
|
const t1 = DATA.times[end - 1];
|
||||||
const markers = [];
|
const markers = [];
|
||||||
if (showMarkers) {
|
if (showMarkers) {
|
||||||
const add = (list, kind, up) => {
|
const colorMap = {
|
||||||
const colorMap = {
|
longOpen: COLORS.longOpen,
|
||||||
longOpen: COLORS.longOpen,
|
longClose: COLORS.longClose,
|
||||||
longClose: COLORS.longClose,
|
shortOpen: COLORS.shortOpen,
|
||||||
shortOpen: COLORS.shortOpen,
|
shortClose: COLORS.shortClose,
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
add(DATA.long_open_markers, "longOpen", true);
|
const kindUp = {
|
||||||
add(DATA.long_close_markers, "longClose", false);
|
longOpen: true,
|
||||||
add(DATA.short_open_markers, "shortOpen", false);
|
longClose: false,
|
||||||
add(DATA.short_close_markers, "shortClose", true);
|
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);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
detailSeries.setMarkers(markers);
|
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) {
|
function fmtMoney(v) {
|
||||||
return Math.round(v).toLocaleString("ko-KR") + "원";
|
return Math.round(v).toLocaleString("ko-KR") + "원";
|
||||||
@@ -622,6 +664,7 @@
|
|||||||
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
||||||
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
||||||
document.getElementById("btn-fit").onclick = applyFit;
|
document.getElementById("btn-fit").onclick = applyFit;
|
||||||
|
document.getElementById("btn-flip-y").onclick = toggleYFlip;
|
||||||
document.getElementById("btn-markers").onclick = () => {
|
document.getElementById("btn-markers").onclick = () => {
|
||||||
showMarkers = !showMarkers;
|
showMarkers = !showMarkers;
|
||||||
document.getElementById("btn-markers").textContent = 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-in" title="확대">+ 확대</button>
|
||||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||||
|
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||||
@@ -118,13 +119,16 @@
|
|||||||
let showMarkers = true;
|
let showMarkers = true;
|
||||||
let detailVisible = false;
|
let detailVisible = false;
|
||||||
let lastDetailStart = 0;
|
let lastDetailStart = 0;
|
||||||
|
let lastDetailEnd = 0;
|
||||||
|
let yAxisInverted = false;
|
||||||
|
|
||||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||||
const SIM_START_COLOR = "#7b1fa2";
|
const SIM_START_COLOR = "#7b1fa2";
|
||||||
let axisMeasureCtx = null;
|
let axisMeasureCtx = null;
|
||||||
|
|
||||||
@@ -199,7 +203,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -224,16 +228,23 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visualUp(up) {
|
||||||
|
return yAxisInverted ? !up : up;
|
||||||
|
}
|
||||||
|
|
||||||
function drawOneMarker(u, m, color, up, label) {
|
function drawOneMarker(u, m, color, up, label) {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const xOff = m.x_offset_px || 0;
|
const x = u.valToPos(m.time, "x", true);
|
||||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
|
||||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
const arrowUp = visualUp(up);
|
||||||
const labelY = up
|
const stackIdx = m.stack_index || 0;
|
||||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
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);
|
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,13 +268,16 @@
|
|||||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
const color = SIM_START_COLOR;
|
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 || "거래시작";
|
const label = marker.label || "거래시작";
|
||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = arrowUp
|
||||||
ctx.lineWidth = 2;
|
? lineY + ARROW_HEIGHT + 12
|
||||||
|
: lineY - ARROW_HEIGHT - 12;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -313,8 +327,11 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: {
|
||||||
|
x: { time: true },
|
||||||
|
y: { dir: yAxisInverted ? -1 : 1 },
|
||||||
|
},
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
{
|
{
|
||||||
@@ -346,6 +363,7 @@
|
|||||||
function buildDetailCandles(startIdx, endIdx) {
|
function buildDetailCandles(startIdx, endIdx) {
|
||||||
lastDetailStart = startIdx;
|
lastDetailStart = startIdx;
|
||||||
const end = endIdx || DATA.times.length;
|
const end = endIdx || DATA.times.length;
|
||||||
|
lastDetailEnd = end;
|
||||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||||
const wrap = document.getElementById("detail");
|
const wrap = document.getElementById("detail");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
@@ -358,6 +376,7 @@
|
|||||||
borderVisible: true,
|
borderVisible: true,
|
||||||
minimumWidth: priceAxisW,
|
minimumWidth: priceAxisW,
|
||||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||||
|
invertScale: yAxisInverted,
|
||||||
},
|
},
|
||||||
timeScale: { timeVisible: true, secondsVisible: false },
|
timeScale: { timeVisible: true, secondsVisible: false },
|
||||||
width: wrap.clientWidth,
|
width: wrap.clientWidth,
|
||||||
@@ -382,28 +401,42 @@
|
|||||||
const t1 = DATA.times[end - 1];
|
const t1 = DATA.times[end - 1];
|
||||||
const markers = [];
|
const markers = [];
|
||||||
if (showMarkers) {
|
if (showMarkers) {
|
||||||
const add = (list, kind, up) => {
|
const colorMap = {
|
||||||
const colorMap = {
|
longOpen: COLORS.longOpen,
|
||||||
longOpen: COLORS.longOpen,
|
longClose: COLORS.longClose,
|
||||||
longClose: COLORS.longClose,
|
shortOpen: COLORS.shortOpen,
|
||||||
shortOpen: COLORS.shortOpen,
|
shortClose: COLORS.shortClose,
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
add(DATA.long_open_markers, "longOpen", true);
|
const kindUp = {
|
||||||
add(DATA.long_close_markers, "longClose", false);
|
longOpen: true,
|
||||||
add(DATA.short_open_markers, "shortOpen", false);
|
longClose: false,
|
||||||
add(DATA.short_close_markers, "shortClose", true);
|
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);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
detailSeries.setMarkers(markers);
|
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) {
|
function fmtMoney(v) {
|
||||||
return Math.round(v).toLocaleString("ko-KR") + "원";
|
return Math.round(v).toLocaleString("ko-KR") + "원";
|
||||||
@@ -622,6 +664,7 @@
|
|||||||
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
||||||
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
||||||
document.getElementById("btn-fit").onclick = applyFit;
|
document.getElementById("btn-fit").onclick = applyFit;
|
||||||
|
document.getElementById("btn-flip-y").onclick = toggleYFlip;
|
||||||
document.getElementById("btn-markers").onclick = () => {
|
document.getElementById("btn-markers").onclick = () => {
|
||||||
showMarkers = !showMarkers;
|
showMarkers = !showMarkers;
|
||||||
document.getElementById("btn-markers").textContent = 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-in" title="확대">+ 확대</button>
|
||||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||||
|
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||||
@@ -118,13 +119,16 @@
|
|||||||
let showMarkers = true;
|
let showMarkers = true;
|
||||||
let detailVisible = false;
|
let detailVisible = false;
|
||||||
let lastDetailStart = 0;
|
let lastDetailStart = 0;
|
||||||
|
let lastDetailEnd = 0;
|
||||||
|
let yAxisInverted = false;
|
||||||
|
|
||||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||||
const SIM_START_COLOR = "#7b1fa2";
|
const SIM_START_COLOR = "#7b1fa2";
|
||||||
let axisMeasureCtx = null;
|
let axisMeasureCtx = null;
|
||||||
|
|
||||||
@@ -199,7 +203,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -224,16 +228,23 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visualUp(up) {
|
||||||
|
return yAxisInverted ? !up : up;
|
||||||
|
}
|
||||||
|
|
||||||
function drawOneMarker(u, m, color, up, label) {
|
function drawOneMarker(u, m, color, up, label) {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const xOff = m.x_offset_px || 0;
|
const x = u.valToPos(m.time, "x", true);
|
||||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
|
||||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
const arrowUp = visualUp(up);
|
||||||
const labelY = up
|
const stackIdx = m.stack_index || 0;
|
||||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
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);
|
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,13 +268,16 @@
|
|||||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
const color = SIM_START_COLOR;
|
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 || "거래시작";
|
const label = marker.label || "거래시작";
|
||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = arrowUp
|
||||||
ctx.lineWidth = 2;
|
? lineY + ARROW_HEIGHT + 12
|
||||||
|
: lineY - ARROW_HEIGHT - 12;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -313,8 +327,11 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: {
|
||||||
|
x: { time: true },
|
||||||
|
y: { dir: yAxisInverted ? -1 : 1 },
|
||||||
|
},
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
{
|
{
|
||||||
@@ -346,6 +363,7 @@
|
|||||||
function buildDetailCandles(startIdx, endIdx) {
|
function buildDetailCandles(startIdx, endIdx) {
|
||||||
lastDetailStart = startIdx;
|
lastDetailStart = startIdx;
|
||||||
const end = endIdx || DATA.times.length;
|
const end = endIdx || DATA.times.length;
|
||||||
|
lastDetailEnd = end;
|
||||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||||
const wrap = document.getElementById("detail");
|
const wrap = document.getElementById("detail");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
@@ -358,6 +376,7 @@
|
|||||||
borderVisible: true,
|
borderVisible: true,
|
||||||
minimumWidth: priceAxisW,
|
minimumWidth: priceAxisW,
|
||||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||||
|
invertScale: yAxisInverted,
|
||||||
},
|
},
|
||||||
timeScale: { timeVisible: true, secondsVisible: false },
|
timeScale: { timeVisible: true, secondsVisible: false },
|
||||||
width: wrap.clientWidth,
|
width: wrap.clientWidth,
|
||||||
@@ -382,28 +401,42 @@
|
|||||||
const t1 = DATA.times[end - 1];
|
const t1 = DATA.times[end - 1];
|
||||||
const markers = [];
|
const markers = [];
|
||||||
if (showMarkers) {
|
if (showMarkers) {
|
||||||
const add = (list, kind, up) => {
|
const colorMap = {
|
||||||
const colorMap = {
|
longOpen: COLORS.longOpen,
|
||||||
longOpen: COLORS.longOpen,
|
longClose: COLORS.longClose,
|
||||||
longClose: COLORS.longClose,
|
shortOpen: COLORS.shortOpen,
|
||||||
shortOpen: COLORS.shortOpen,
|
shortClose: COLORS.shortClose,
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
add(DATA.long_open_markers, "longOpen", true);
|
const kindUp = {
|
||||||
add(DATA.long_close_markers, "longClose", false);
|
longOpen: true,
|
||||||
add(DATA.short_open_markers, "shortOpen", false);
|
longClose: false,
|
||||||
add(DATA.short_close_markers, "shortClose", true);
|
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);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
detailSeries.setMarkers(markers);
|
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) {
|
function fmtMoney(v) {
|
||||||
return Math.round(v).toLocaleString("ko-KR") + "원";
|
return Math.round(v).toLocaleString("ko-KR") + "원";
|
||||||
@@ -622,6 +664,7 @@
|
|||||||
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
||||||
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
||||||
document.getElementById("btn-fit").onclick = applyFit;
|
document.getElementById("btn-fit").onclick = applyFit;
|
||||||
|
document.getElementById("btn-flip-y").onclick = toggleYFlip;
|
||||||
document.getElementById("btn-markers").onclick = () => {
|
document.getElementById("btn-markers").onclick = () => {
|
||||||
showMarkers = !showMarkers;
|
showMarkers = !showMarkers;
|
||||||
document.getElementById("btn-markers").textContent = 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-in" title="확대">+ 확대</button>
|
||||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||||
|
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||||
@@ -80,13 +81,16 @@
|
|||||||
let showMarkers = true;
|
let showMarkers = true;
|
||||||
let detailVisible = false;
|
let detailVisible = false;
|
||||||
let lastDetailStart = 0;
|
let lastDetailStart = 0;
|
||||||
|
let lastDetailEnd = 0;
|
||||||
|
let yAxisInverted = false;
|
||||||
|
|
||||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||||
const SIM_START_COLOR = "#7b1fa2";
|
const SIM_START_COLOR = "#7b1fa2";
|
||||||
let axisMeasureCtx = null;
|
let axisMeasureCtx = null;
|
||||||
|
|
||||||
@@ -161,7 +165,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -186,16 +190,23 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visualUp(up) {
|
||||||
|
return yAxisInverted ? !up : up;
|
||||||
|
}
|
||||||
|
|
||||||
function drawOneMarker(u, m, color, up, label) {
|
function drawOneMarker(u, m, color, up, label) {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const xOff = m.x_offset_px || 0;
|
const x = u.valToPos(m.time, "x", true);
|
||||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
|
||||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
const arrowUp = visualUp(up);
|
||||||
const labelY = up
|
const stackIdx = m.stack_index || 0;
|
||||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
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);
|
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +230,16 @@
|
|||||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
const color = SIM_START_COLOR;
|
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 || "거래시작";
|
const label = marker.label || "거래시작";
|
||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = arrowUp
|
||||||
ctx.lineWidth = 2;
|
? lineY + ARROW_HEIGHT + 12
|
||||||
|
: lineY - ARROW_HEIGHT - 12;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -275,8 +289,11 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: {
|
||||||
|
x: { time: true },
|
||||||
|
y: { dir: yAxisInverted ? -1 : 1 },
|
||||||
|
},
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
{
|
{
|
||||||
@@ -308,6 +325,7 @@
|
|||||||
function buildDetailCandles(startIdx, endIdx) {
|
function buildDetailCandles(startIdx, endIdx) {
|
||||||
lastDetailStart = startIdx;
|
lastDetailStart = startIdx;
|
||||||
const end = endIdx || DATA.times.length;
|
const end = endIdx || DATA.times.length;
|
||||||
|
lastDetailEnd = end;
|
||||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||||
const wrap = document.getElementById("detail");
|
const wrap = document.getElementById("detail");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
@@ -320,6 +338,7 @@
|
|||||||
borderVisible: true,
|
borderVisible: true,
|
||||||
minimumWidth: priceAxisW,
|
minimumWidth: priceAxisW,
|
||||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||||
|
invertScale: yAxisInverted,
|
||||||
},
|
},
|
||||||
timeScale: { timeVisible: true, secondsVisible: false },
|
timeScale: { timeVisible: true, secondsVisible: false },
|
||||||
width: wrap.clientWidth,
|
width: wrap.clientWidth,
|
||||||
@@ -344,28 +363,42 @@
|
|||||||
const t1 = DATA.times[end - 1];
|
const t1 = DATA.times[end - 1];
|
||||||
const markers = [];
|
const markers = [];
|
||||||
if (showMarkers) {
|
if (showMarkers) {
|
||||||
const add = (list, kind, up) => {
|
const colorMap = {
|
||||||
const colorMap = {
|
longOpen: COLORS.longOpen,
|
||||||
longOpen: COLORS.longOpen,
|
longClose: COLORS.longClose,
|
||||||
longClose: COLORS.longClose,
|
shortOpen: COLORS.shortOpen,
|
||||||
shortOpen: COLORS.shortOpen,
|
shortClose: COLORS.shortClose,
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
add(DATA.long_open_markers, "longOpen", true);
|
const kindUp = {
|
||||||
add(DATA.long_close_markers, "longClose", false);
|
longOpen: true,
|
||||||
add(DATA.short_open_markers, "shortOpen", false);
|
longClose: false,
|
||||||
add(DATA.short_close_markers, "shortClose", true);
|
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);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
detailSeries.setMarkers(markers);
|
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() {
|
function renderLegend() {
|
||||||
@@ -528,6 +570,7 @@
|
|||||||
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
||||||
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
||||||
document.getElementById("btn-fit").onclick = applyFit;
|
document.getElementById("btn-fit").onclick = applyFit;
|
||||||
|
document.getElementById("btn-flip-y").onclick = toggleYFlip;
|
||||||
document.getElementById("btn-markers").onclick = () => {
|
document.getElementById("btn-markers").onclick = () => {
|
||||||
showMarkers = !showMarkers;
|
showMarkers = !showMarkers;
|
||||||
document.getElementById("btn-markers").textContent = 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-in" title="확대">+ 확대</button>
|
||||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||||
|
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||||
@@ -80,13 +81,16 @@
|
|||||||
let showMarkers = true;
|
let showMarkers = true;
|
||||||
let detailVisible = false;
|
let detailVisible = false;
|
||||||
let lastDetailStart = 0;
|
let lastDetailStart = 0;
|
||||||
|
let lastDetailEnd = 0;
|
||||||
|
let yAxisInverted = false;
|
||||||
|
|
||||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||||
const SIM_START_COLOR = "#7b1fa2";
|
const SIM_START_COLOR = "#7b1fa2";
|
||||||
let axisMeasureCtx = null;
|
let axisMeasureCtx = null;
|
||||||
|
|
||||||
@@ -161,7 +165,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -186,16 +190,23 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visualUp(up) {
|
||||||
|
return yAxisInverted ? !up : up;
|
||||||
|
}
|
||||||
|
|
||||||
function drawOneMarker(u, m, color, up, label) {
|
function drawOneMarker(u, m, color, up, label) {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const xOff = m.x_offset_px || 0;
|
const x = u.valToPos(m.time, "x", true);
|
||||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
|
||||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
const arrowUp = visualUp(up);
|
||||||
const labelY = up
|
const stackIdx = m.stack_index || 0;
|
||||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
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);
|
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +230,16 @@
|
|||||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
const color = SIM_START_COLOR;
|
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 || "거래시작";
|
const label = marker.label || "거래시작";
|
||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = arrowUp
|
||||||
ctx.lineWidth = 2;
|
? lineY + ARROW_HEIGHT + 12
|
||||||
|
: lineY - ARROW_HEIGHT - 12;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -275,8 +289,11 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: {
|
||||||
|
x: { time: true },
|
||||||
|
y: { dir: yAxisInverted ? -1 : 1 },
|
||||||
|
},
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
{
|
{
|
||||||
@@ -308,6 +325,7 @@
|
|||||||
function buildDetailCandles(startIdx, endIdx) {
|
function buildDetailCandles(startIdx, endIdx) {
|
||||||
lastDetailStart = startIdx;
|
lastDetailStart = startIdx;
|
||||||
const end = endIdx || DATA.times.length;
|
const end = endIdx || DATA.times.length;
|
||||||
|
lastDetailEnd = end;
|
||||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||||
const wrap = document.getElementById("detail");
|
const wrap = document.getElementById("detail");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
@@ -320,6 +338,7 @@
|
|||||||
borderVisible: true,
|
borderVisible: true,
|
||||||
minimumWidth: priceAxisW,
|
minimumWidth: priceAxisW,
|
||||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||||
|
invertScale: yAxisInverted,
|
||||||
},
|
},
|
||||||
timeScale: { timeVisible: true, secondsVisible: false },
|
timeScale: { timeVisible: true, secondsVisible: false },
|
||||||
width: wrap.clientWidth,
|
width: wrap.clientWidth,
|
||||||
@@ -344,28 +363,42 @@
|
|||||||
const t1 = DATA.times[end - 1];
|
const t1 = DATA.times[end - 1];
|
||||||
const markers = [];
|
const markers = [];
|
||||||
if (showMarkers) {
|
if (showMarkers) {
|
||||||
const add = (list, kind, up) => {
|
const colorMap = {
|
||||||
const colorMap = {
|
longOpen: COLORS.longOpen,
|
||||||
longOpen: COLORS.longOpen,
|
longClose: COLORS.longClose,
|
||||||
longClose: COLORS.longClose,
|
shortOpen: COLORS.shortOpen,
|
||||||
shortOpen: COLORS.shortOpen,
|
shortClose: COLORS.shortClose,
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
add(DATA.long_open_markers, "longOpen", true);
|
const kindUp = {
|
||||||
add(DATA.long_close_markers, "longClose", false);
|
longOpen: true,
|
||||||
add(DATA.short_open_markers, "shortOpen", false);
|
longClose: false,
|
||||||
add(DATA.short_close_markers, "shortClose", true);
|
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);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
detailSeries.setMarkers(markers);
|
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() {
|
function renderLegend() {
|
||||||
@@ -528,6 +570,7 @@
|
|||||||
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
||||||
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
||||||
document.getElementById("btn-fit").onclick = applyFit;
|
document.getElementById("btn-fit").onclick = applyFit;
|
||||||
|
document.getElementById("btn-flip-y").onclick = toggleYFlip;
|
||||||
document.getElementById("btn-markers").onclick = () => {
|
document.getElementById("btn-markers").onclick = () => {
|
||||||
showMarkers = !showMarkers;
|
showMarkers = !showMarkers;
|
||||||
document.getElementById("btn-markers").textContent = 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-in" title="확대">+ 확대</button>
|
||||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||||
|
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||||
@@ -80,13 +81,16 @@
|
|||||||
let showMarkers = true;
|
let showMarkers = true;
|
||||||
let detailVisible = false;
|
let detailVisible = false;
|
||||||
let lastDetailStart = 0;
|
let lastDetailStart = 0;
|
||||||
|
let lastDetailEnd = 0;
|
||||||
|
let yAxisInverted = false;
|
||||||
|
|
||||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||||
const SIM_START_COLOR = "#7b1fa2";
|
const SIM_START_COLOR = "#7b1fa2";
|
||||||
let axisMeasureCtx = null;
|
let axisMeasureCtx = null;
|
||||||
|
|
||||||
@@ -161,7 +165,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -186,16 +190,23 @@
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visualUp(up) {
|
||||||
|
return yAxisInverted ? !up : up;
|
||||||
|
}
|
||||||
|
|
||||||
function drawOneMarker(u, m, color, up, label) {
|
function drawOneMarker(u, m, color, up, label) {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const xOff = m.x_offset_px || 0;
|
const x = u.valToPos(m.time, "x", true);
|
||||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
|
||||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
const arrowUp = visualUp(up);
|
||||||
const labelY = up
|
const stackIdx = m.stack_index || 0;
|
||||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
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);
|
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +230,16 @@
|
|||||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
const color = SIM_START_COLOR;
|
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 || "거래시작";
|
const label = marker.label || "거래시작";
|
||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = arrowUp
|
||||||
ctx.lineWidth = 2;
|
? lineY + ARROW_HEIGHT + 12
|
||||||
|
: lineY - ARROW_HEIGHT - 12;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -275,8 +289,11 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: {
|
||||||
|
x: { time: true },
|
||||||
|
y: { dir: yAxisInverted ? -1 : 1 },
|
||||||
|
},
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
{
|
{
|
||||||
@@ -308,6 +325,7 @@
|
|||||||
function buildDetailCandles(startIdx, endIdx) {
|
function buildDetailCandles(startIdx, endIdx) {
|
||||||
lastDetailStart = startIdx;
|
lastDetailStart = startIdx;
|
||||||
const end = endIdx || DATA.times.length;
|
const end = endIdx || DATA.times.length;
|
||||||
|
lastDetailEnd = end;
|
||||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||||
const wrap = document.getElementById("detail");
|
const wrap = document.getElementById("detail");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
@@ -320,6 +338,7 @@
|
|||||||
borderVisible: true,
|
borderVisible: true,
|
||||||
minimumWidth: priceAxisW,
|
minimumWidth: priceAxisW,
|
||||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||||
|
invertScale: yAxisInverted,
|
||||||
},
|
},
|
||||||
timeScale: { timeVisible: true, secondsVisible: false },
|
timeScale: { timeVisible: true, secondsVisible: false },
|
||||||
width: wrap.clientWidth,
|
width: wrap.clientWidth,
|
||||||
@@ -344,28 +363,42 @@
|
|||||||
const t1 = DATA.times[end - 1];
|
const t1 = DATA.times[end - 1];
|
||||||
const markers = [];
|
const markers = [];
|
||||||
if (showMarkers) {
|
if (showMarkers) {
|
||||||
const add = (list, kind, up) => {
|
const colorMap = {
|
||||||
const colorMap = {
|
longOpen: COLORS.longOpen,
|
||||||
longOpen: COLORS.longOpen,
|
longClose: COLORS.longClose,
|
||||||
longClose: COLORS.longClose,
|
shortOpen: COLORS.shortOpen,
|
||||||
shortOpen: COLORS.shortOpen,
|
shortClose: COLORS.shortClose,
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
add(DATA.long_open_markers, "longOpen", true);
|
const kindUp = {
|
||||||
add(DATA.long_close_markers, "longClose", false);
|
longOpen: true,
|
||||||
add(DATA.short_open_markers, "shortOpen", false);
|
longClose: false,
|
||||||
add(DATA.short_close_markers, "shortClose", true);
|
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);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
detailSeries.setMarkers(markers);
|
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() {
|
function renderLegend() {
|
||||||
@@ -528,6 +570,7 @@
|
|||||||
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
||||||
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
||||||
document.getElementById("btn-fit").onclick = applyFit;
|
document.getElementById("btn-fit").onclick = applyFit;
|
||||||
|
document.getElementById("btn-flip-y").onclick = toggleYFlip;
|
||||||
document.getElementById("btn-markers").onclick = () => {
|
document.getElementById("btn-markers").onclick = () => {
|
||||||
showMarkers = !showMarkers;
|
showMarkers = !showMarkers;
|
||||||
document.getElementById("btn-markers").textContent = 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}`;
|
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 SIM_START_COLOR = "#7b1fa2";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
|
|
||||||
function markerSuffix(signalType) {
|
function markerSuffix(signalType) {
|
||||||
if (signalType === "pullback") return "*";
|
if (signalType === "pullback") return "*";
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -223,8 +223,8 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
ctx.textBaseline = "alphabetic";
|
ctx.textBaseline = "alphabetic";
|
||||||
}
|
}
|
||||||
|
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
|
||||||
function drawMarkers(u, buys, sells) {
|
function drawMarkers(u, buys, sells) {
|
||||||
if (!showMarkers) return;
|
if (!showMarkers) return;
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: { x: { time: true } },
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
const sm = DATA.sim_start_marker;
|
const sm = DATA.sim_start_marker;
|
||||||
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
||||||
time: sm.time, position: "aboveBar",
|
time: sm.time, position: "aboveBar",
|
||||||
color: SIM_START_COLOR, shape: "arrowDown", size: 3,
|
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||||
text: sm.label || "거래시작",
|
text: sm.label || "거래시작",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
time: m.time, position: "belowBar",
|
time: m.time, position: "belowBar",
|
||||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||||
shape: "arrowUp", size: 5,
|
shape: "arrowUp", size: 10,
|
||||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
if (m.time >= t0 && m.time <= t1) markers.push({
|
if (m.time >= t0 && m.time <= t1) markers.push({
|
||||||
time: m.time, position: "aboveBar",
|
time: m.time, position: "aboveBar",
|
||||||
color: m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828",
|
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),
|
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}`;
|
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 SIM_START_COLOR = "#7b1fa2";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
|
|
||||||
function markerSuffix(signalType) {
|
function markerSuffix(signalType) {
|
||||||
if (signalType === "pullback") return "*";
|
if (signalType === "pullback") return "*";
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -223,8 +223,8 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
ctx.textBaseline = "alphabetic";
|
ctx.textBaseline = "alphabetic";
|
||||||
}
|
}
|
||||||
|
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
|
||||||
function drawMarkers(u, buys, sells) {
|
function drawMarkers(u, buys, sells) {
|
||||||
if (!showMarkers) return;
|
if (!showMarkers) return;
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: { x: { time: true } },
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
const sm = DATA.sim_start_marker;
|
const sm = DATA.sim_start_marker;
|
||||||
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
||||||
time: sm.time, position: "aboveBar",
|
time: sm.time, position: "aboveBar",
|
||||||
color: SIM_START_COLOR, shape: "arrowDown", size: 3,
|
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||||
text: sm.label || "거래시작",
|
text: sm.label || "거래시작",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
time: m.time, position: "belowBar",
|
time: m.time, position: "belowBar",
|
||||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||||
shape: "arrowUp", size: 5,
|
shape: "arrowUp", size: 10,
|
||||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
if (m.time >= t0 && m.time <= t1) markers.push({
|
if (m.time >= t0 && m.time <= t1) markers.push({
|
||||||
time: m.time, position: "aboveBar",
|
time: m.time, position: "aboveBar",
|
||||||
color: m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828",
|
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),
|
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}`;
|
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 SIM_START_COLOR = "#7b1fa2";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
|
|
||||||
function markerSuffix(signalType) {
|
function markerSuffix(signalType) {
|
||||||
if (signalType === "pullback") return "*";
|
if (signalType === "pullback") return "*";
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -223,8 +223,8 @@
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
ctx.textBaseline = "alphabetic";
|
ctx.textBaseline = "alphabetic";
|
||||||
}
|
}
|
||||||
|
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
|
||||||
function drawMarkers(u, buys, sells) {
|
function drawMarkers(u, buys, sells) {
|
||||||
if (!showMarkers) return;
|
if (!showMarkers) return;
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: { x: { time: true } },
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
const sm = DATA.sim_start_marker;
|
const sm = DATA.sim_start_marker;
|
||||||
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
||||||
time: sm.time, position: "aboveBar",
|
time: sm.time, position: "aboveBar",
|
||||||
color: SIM_START_COLOR, shape: "arrowDown", size: 3,
|
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||||
text: sm.label || "거래시작",
|
text: sm.label || "거래시작",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
time: m.time, position: "belowBar",
|
time: m.time, position: "belowBar",
|
||||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||||
shape: "arrowUp", size: 5,
|
shape: "arrowUp", size: 10,
|
||||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
if (m.time >= t0 && m.time <= t1) markers.push({
|
if (m.time >= t0 && m.time <= t1) markers.push({
|
||||||
time: m.time, position: "aboveBar",
|
time: m.time, position: "aboveBar",
|
||||||
color: m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828",
|
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),
|
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}`;
|
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 SIM_START_COLOR = "#7b1fa2";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
|
|
||||||
function markerSuffix(signalType) {
|
function markerSuffix(signalType) {
|
||||||
if (signalType === "pullback") return "*";
|
if (signalType === "pullback") return "*";
|
||||||
@@ -400,7 +400,7 @@ __EXTRA_BODY__
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -437,8 +437,8 @@ __EXTRA_BODY__
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = lineY - ARROW_HEIGHT - 12;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -448,7 +448,7 @@ __EXTRA_BODY__
|
|||||||
ctx.textBaseline = "alphabetic";
|
ctx.textBaseline = "alphabetic";
|
||||||
}
|
}
|
||||||
|
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
|
||||||
function drawMarkers(u, buys, sells) {
|
function drawMarkers(u, buys, sells) {
|
||||||
if (!showMarkers) return;
|
if (!showMarkers) return;
|
||||||
@@ -509,7 +509,7 @@ __EXTRA_BODY__
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: { x: { time: true } },
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
@@ -589,7 +589,7 @@ __EXTRA_BODY__
|
|||||||
const sm = DATA.sim_start_marker;
|
const sm = DATA.sim_start_marker;
|
||||||
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
if (sm.time >= t0 && sm.time <= t1) markers.push({
|
||||||
time: sm.time, position: "aboveBar",
|
time: sm.time, position: "aboveBar",
|
||||||
color: SIM_START_COLOR, shape: "arrowDown", size: 3,
|
color: SIM_START_COLOR, shape: "arrowDown", size: 6,
|
||||||
text: sm.label || "거래시작",
|
text: sm.label || "거래시작",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -599,7 +599,7 @@ __EXTRA_BODY__
|
|||||||
time: m.time, position: "belowBar",
|
time: m.time, position: "belowBar",
|
||||||
color: m.signal_type === "breakout" ? "#ef6c00"
|
color: m.signal_type === "breakout" ? "#ef6c00"
|
||||||
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
: m.signal_type === "div_bull" ? "#7b1fa2" : "#2e7d32",
|
||||||
shape: "arrowUp", size: 5,
|
shape: "arrowUp", size: 10,
|
||||||
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
text: "B" + m.marker_id + markerSuffix(m.signal_type),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -607,7 +607,7 @@ __EXTRA_BODY__
|
|||||||
if (m.time >= t0 && m.time <= t1) markers.push({
|
if (m.time >= t0 && m.time <= t1) markers.push({
|
||||||
time: m.time, position: "aboveBar",
|
time: m.time, position: "aboveBar",
|
||||||
color: m.signal_type === "div_bear" ? "#7b1fa2" : "#c62828",
|
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),
|
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_open: list[dict[str, Any]],
|
||||||
long_close: list[dict[str, Any]],
|
long_close: list[dict[str, Any]],
|
||||||
short_open: list[dict[str, Any]],
|
short_open: list[dict[str, Any]],
|
||||||
short_close: list[dict[str, Any]],
|
short_close: list[dict[str, Any]],
|
||||||
stagger_px: int = 18,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""동일 시각에 겹치는 마커에 수평 오프셋을 부여한다.
|
"""동일 시각에 겹치는 마커에 세로 스택 인덱스를 부여한다.
|
||||||
|
|
||||||
GT 매도 봉에서 L↓(롱 청산)과 S↓(숏 진입)이 같은 좌표에 겹치는 문제를 방지한다.
|
같은 봉의 L↓·S↓ 등이 좌우로 벌어지지 않고 동일 X에서 위아래로 쌓이도록 한다.
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@@ -47,13 +46,15 @@ def _stagger_marker_positions(
|
|||||||
|
|
||||||
for markers_at_time in groups.values():
|
for markers_at_time in groups.values():
|
||||||
if len(markers_at_time) <= 1:
|
if len(markers_at_time) <= 1:
|
||||||
|
for marker in markers_at_time:
|
||||||
|
marker["stack_index"] = 0
|
||||||
continue
|
continue
|
||||||
markers_at_time.sort(
|
markers_at_time.sort(
|
||||||
key=lambda m: kind_rank.get(f"{m.get('position')}_{m.get('action')}", 9)
|
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):
|
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>
|
_FUTURES_HTML_TEMPLATE = """<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
@@ -115,6 +116,7 @@ __EXTRA_BODY__
|
|||||||
<button id="btn-zoom-in" title="확대">+ 확대</button>
|
<button id="btn-zoom-in" title="확대">+ 확대</button>
|
||||||
<button id="btn-zoom-out" title="축소">− 축소</button>
|
<button id="btn-zoom-out" title="축소">− 축소</button>
|
||||||
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
<button id="btn-fit" title="현재 뷰 맞춤">맞춤</button>
|
||||||
|
<button id="btn-flip-y" title="가격 축 위·아래 반전">↕ 뒤집기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
<button id="btn-markers" title="마커 표시/숨김">마커 숨김</button>
|
||||||
@@ -137,13 +139,16 @@ __EXTRA_BODY__
|
|||||||
let showMarkers = true;
|
let showMarkers = true;
|
||||||
let detailVisible = false;
|
let detailVisible = false;
|
||||||
let lastDetailStart = 0;
|
let lastDetailStart = 0;
|
||||||
|
let lastDetailEnd = 0;
|
||||||
|
let yAxisInverted = false;
|
||||||
|
|
||||||
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
const AXIS_FONT = "12px Malgun Gothic, Arial, sans-serif";
|
||||||
const MARKER_FONT = "bold 12px Malgun Gothic, Arial, sans-serif";
|
const MARKER_FONT = "bold 24px Malgun Gothic, Arial, sans-serif";
|
||||||
const ARROW_HALF = 6;
|
const ARROW_HALF = 12;
|
||||||
const ARROW_HEIGHT = 8;
|
const ARROW_HEIGHT = 16;
|
||||||
const LABEL_OFFSET_X = 8;
|
const LABEL_OFFSET_X = 16;
|
||||||
const LABEL_GAP = 12;
|
const LABEL_GAP = 24;
|
||||||
|
const STACK_STEP = ARROW_HEIGHT + LABEL_GAP + 28;
|
||||||
const SIM_START_COLOR = "#7b1fa2";
|
const SIM_START_COLOR = "#7b1fa2";
|
||||||
let axisMeasureCtx = null;
|
let axisMeasureCtx = null;
|
||||||
|
|
||||||
@@ -218,7 +223,7 @@ __EXTRA_BODY__
|
|||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
const lx = x + LABEL_OFFSET_X;
|
const lx = x + LABEL_OFFSET_X;
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, lx, labelY);
|
ctx.strokeText(label, lx, labelY);
|
||||||
@@ -227,6 +232,10 @@ __EXTRA_BODY__
|
|||||||
ctx.textBaseline = "alphabetic";
|
ctx.textBaseline = "alphabetic";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visualUp(up) {
|
||||||
|
return yAxisInverted ? !up : up;
|
||||||
|
}
|
||||||
|
|
||||||
function drawTriangleOnLine(ctx, x, lineY, up, color) {
|
function drawTriangleOnLine(ctx, x, lineY, up, color) {
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -245,14 +254,17 @@ __EXTRA_BODY__
|
|||||||
|
|
||||||
function drawOneMarker(u, m, color, up, label) {
|
function drawOneMarker(u, m, color, up, label) {
|
||||||
const ctx = u.ctx;
|
const ctx = u.ctx;
|
||||||
const xOff = m.x_offset_px || 0;
|
const x = u.valToPos(m.time, "x", true);
|
||||||
const x = u.valToPos(m.time, "x", true) + xOff;
|
|
||||||
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
const lineY = u.valToPos(markerChartPrice(m), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
drawTriangleOnLine(ctx, x, lineY, up, color);
|
const arrowUp = visualUp(up);
|
||||||
const labelY = up
|
const stackIdx = m.stack_index || 0;
|
||||||
? lineY + ARROW_HEIGHT + LABEL_GAP
|
const stackShift = stackIdx * STACK_STEP * (arrowUp ? 1 : -1);
|
||||||
: lineY - ARROW_HEIGHT - LABEL_GAP;
|
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);
|
drawMarkerLabel(ctx, label, x, labelY, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,13 +288,16 @@ __EXTRA_BODY__
|
|||||||
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
const lineY = u.valToPos(markerChartPrice(marker), "y", true);
|
||||||
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
|
||||||
const color = SIM_START_COLOR;
|
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 || "거래시작";
|
const label = marker.label || "거래시작";
|
||||||
ctx.font = MARKER_FONT;
|
ctx.font = MARKER_FONT;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = arrowUp ? "top" : "bottom";
|
||||||
const labelY = lineY - ARROW_HEIGHT - 6;
|
const labelY = arrowUp
|
||||||
ctx.lineWidth = 2;
|
? lineY + ARROW_HEIGHT + 12
|
||||||
|
: lineY - ARROW_HEIGHT - 12;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
ctx.lineJoin = "round";
|
ctx.lineJoin = "round";
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||||
ctx.strokeText(label, x, labelY);
|
ctx.strokeText(label, x, labelY);
|
||||||
@@ -332,8 +347,11 @@ __EXTRA_BODY__
|
|||||||
const opts = {
|
const opts = {
|
||||||
width: document.getElementById("overview").clientWidth,
|
width: document.getElementById("overview").clientWidth,
|
||||||
height: 480,
|
height: 480,
|
||||||
padding: [28, 10, 28, 10],
|
padding: [40, 10, 40, 10],
|
||||||
scales: { x: { time: true } },
|
scales: {
|
||||||
|
x: { time: true },
|
||||||
|
y: { dir: yAxisInverted ? -1 : 1 },
|
||||||
|
},
|
||||||
axes: [
|
axes: [
|
||||||
{ gap: 6 },
|
{ gap: 6 },
|
||||||
{
|
{
|
||||||
@@ -365,6 +383,7 @@ __EXTRA_BODY__
|
|||||||
function buildDetailCandles(startIdx, endIdx) {
|
function buildDetailCandles(startIdx, endIdx) {
|
||||||
lastDetailStart = startIdx;
|
lastDetailStart = startIdx;
|
||||||
const end = endIdx || DATA.times.length;
|
const end = endIdx || DATA.times.length;
|
||||||
|
lastDetailEnd = end;
|
||||||
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
document.getElementById("detail-wrap").style.display = detailVisible ? "block" : "none";
|
||||||
const wrap = document.getElementById("detail");
|
const wrap = document.getElementById("detail");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
@@ -377,6 +396,7 @@ __EXTRA_BODY__
|
|||||||
borderVisible: true,
|
borderVisible: true,
|
||||||
minimumWidth: priceAxisW,
|
minimumWidth: priceAxisW,
|
||||||
scaleMargins: { top: 0.08, bottom: 0.08 },
|
scaleMargins: { top: 0.08, bottom: 0.08 },
|
||||||
|
invertScale: yAxisInverted,
|
||||||
},
|
},
|
||||||
timeScale: { timeVisible: true, secondsVisible: false },
|
timeScale: { timeVisible: true, secondsVisible: false },
|
||||||
width: wrap.clientWidth,
|
width: wrap.clientWidth,
|
||||||
@@ -401,28 +421,41 @@ __EXTRA_BODY__
|
|||||||
const t1 = DATA.times[end - 1];
|
const t1 = DATA.times[end - 1];
|
||||||
const markers = [];
|
const markers = [];
|
||||||
if (showMarkers) {
|
if (showMarkers) {
|
||||||
const add = (list, kind, up) => {
|
const colorMap = {
|
||||||
const colorMap = {
|
longOpen: COLORS.longOpen,
|
||||||
longOpen: COLORS.longOpen,
|
longClose: COLORS.longClose,
|
||||||
longClose: COLORS.longClose,
|
shortOpen: COLORS.shortOpen,
|
||||||
shortOpen: COLORS.shortOpen,
|
shortClose: COLORS.shortClose,
|
||||||
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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
add(DATA.long_open_markers, "longOpen", true);
|
const kindUp = {
|
||||||
add(DATA.long_close_markers, "longClose", false);
|
longOpen: true,
|
||||||
add(DATA.short_open_markers, "shortOpen", false);
|
longClose: false,
|
||||||
add(DATA.short_close_markers, "shortClose", true);
|
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);
|
markers.sort((a, b) => a.time - b.time);
|
||||||
detailSeries.setMarkers(markers);
|
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__
|
__EXTRA_SCRIPT__
|
||||||
|
|
||||||
function renderLegend() {
|
function renderLegend() {
|
||||||
@@ -585,6 +627,7 @@ __EXTRA_SCRIPT__
|
|||||||
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
document.getElementById("btn-zoom-in").onclick = () => applyZoom(0.6);
|
||||||
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
document.getElementById("btn-zoom-out").onclick = () => applyZoom(1.4);
|
||||||
document.getElementById("btn-fit").onclick = applyFit;
|
document.getElementById("btn-fit").onclick = applyFit;
|
||||||
|
document.getElementById("btn-flip-y").onclick = toggleYFlip;
|
||||||
document.getElementById("btn-markers").onclick = () => {
|
document.getElementById("btn-markers").onclick = () => {
|
||||||
showMarkers = !showMarkers;
|
showMarkers = !showMarkers;
|
||||||
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";
|
document.getElementById("btn-markers").textContent = showMarkers ? "마커 숨김" : "마커 표시";
|
||||||
@@ -646,7 +689,7 @@ def _build_futures_chart_payload(
|
|||||||
"meta": chart_meta,
|
"meta": chart_meta,
|
||||||
"bar_count": len(df),
|
"bar_count": len(df),
|
||||||
}
|
}
|
||||||
_stagger_marker_positions(
|
_stack_marker_positions(
|
||||||
payload["long_open_markers"],
|
payload["long_open_markers"],
|
||||||
payload["long_close_markers"],
|
payload["long_close_markers"],
|
||||||
payload["short_open_markers"],
|
payload["short_open_markers"],
|
||||||
@@ -857,7 +900,7 @@ def _build_futures_sim_chart_payload(
|
|||||||
start_marker = _sim_start_marker(df, sim_pnl)
|
start_marker = _sim_start_marker(df, sim_pnl)
|
||||||
if start_marker is not None:
|
if start_marker is not None:
|
||||||
payload["sim_start_marker"] = start_marker
|
payload["sim_start_marker"] = start_marker
|
||||||
_stagger_marker_positions(
|
_stack_marker_positions(
|
||||||
payload["long_open_markers"],
|
payload["long_open_markers"],
|
||||||
payload["long_close_markers"],
|
payload["long_close_markers"],
|
||||||
payload["short_open_markers"],
|
payload["short_open_markers"],
|
||||||
|
|||||||
Reference in New Issue
Block a user