매수매도 시그널 표시 수정

This commit is contained in:
Macbook
2026-06-12 00:32:03 +09:00
parent 578cba3eb8
commit 423c60183e
4 changed files with 454 additions and 4 deletions
@@ -0,0 +1,167 @@
import React, { useCallback, useEffect, useRef } from 'react';
import type { ChartManager } from '../utils/ChartManager';
import {
layoutStackedSignalLabels,
resolveSignalLabelAnchors,
type DrawableSignalLabel,
type TradeSignalLabelItem,
} from '../utils/tradeSignalLabels';
interface Props {
manager: ChartManager;
}
const ARROW_OFFSET = 16;
const FONT = '600 11px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
function resolveArrowY(
mgr: ChartManager,
item: TradeSignalLabelItem,
mainTopY: number,
): number | null {
const bar = mgr.getBarAtTime(item.time);
let paneY: number | null;
if (bar) {
paneY = item.isBuy ? mgr.priceToY(bar.low) : mgr.priceToY(bar.high);
} else {
paneY = mgr.priceToY(item.price);
}
if (paneY == null) return null;
const chartY = paneY + mainTopY;
return item.isBuy ? chartY + ARROW_OFFSET : chartY - ARROW_OFFSET;
}
function drawLabel(
ctx: CanvasRenderingContext2D,
x: number,
textY: number,
text: string,
color: string,
baseline: 'top' | 'bottom',
): void {
ctx.font = FONT;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = baseline;
ctx.fillText(text, x, textY);
}
function renderTradeSignalLabels(
ctx: CanvasRenderingContext2D,
mgr: ChartManager,
cssW: number,
labels: TradeSignalLabelItem[],
): void {
const main = mgr.getPaneLayouts().find(l => l.paneIndex === 0);
if (!main || labels.length === 0) return;
const { buyBaseBottomY, sellBaseTopY } = resolveSignalLabelAnchors(mgr, main);
const drawable: DrawableSignalLabel[] = [];
for (const item of labels) {
const x = mgr.timeToX(item.time);
if (x == null || x < -40 || x > cssW + 40) continue;
const arrowY = resolveArrowY(mgr, item, main.topY);
if (arrowY == null) continue;
drawable.push({ item, x, arrowY });
}
if (drawable.length === 0) return;
const buys = drawable.filter(d => d.item.isBuy);
const sells = drawable.filter(d => !d.item.isBuy);
const buyPlaced = layoutStackedSignalLabels(
ctx, buys, mgr, buyBaseBottomY, 'bottom', FONT,
);
const sellPlaced = layoutStackedSignalLabels(
ctx, sells, mgr, sellBaseTopY, 'top', FONT,
);
ctx.setLineDash([3, 4]);
ctx.lineWidth = 1;
for (const p of [...buyPlaced, ...sellPlaced]) {
ctx.strokeStyle = p.item.color;
ctx.beginPath();
ctx.moveTo(p.x, p.arrowY);
ctx.lineTo(p.x, p.textY);
ctx.stroke();
drawLabel(ctx, p.x, p.textY, p.item.text, p.item.color, p.baseline);
}
ctx.setLineDash([]);
}
const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const redraw = useCallback(() => {
const canvas = canvasRef.current;
const container = manager.getContainer();
if (!canvas || !container) return;
const cssW = container.clientWidth;
const cssH = container.clientHeight;
if (cssW <= 0 || cssH <= 0) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
canvas.style.width = `${cssW}px`;
canvas.style.height = `${cssH}px`;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, cssW, cssH);
renderTradeSignalLabels(ctx, manager, cssW, manager.getTradeSignalLabels());
}, [manager]);
useEffect(() => {
redraw();
let layoutRaf = 0;
const scheduleRedraw = () => {
cancelAnimationFrame(layoutRaf);
layoutRaf = requestAnimationFrame(redraw);
};
const unsubs = [
manager.subscribePaneLayout(scheduleRedraw),
manager.subscribeViewport(scheduleRedraw),
manager.subscribeTradeSignalLabels(scheduleRedraw),
];
const container = manager.getContainer();
const ro = new ResizeObserver(scheduleRedraw);
if (container) ro.observe(container);
const raf1 = requestAnimationFrame(redraw);
const raf2 = requestAnimationFrame(() => requestAnimationFrame(redraw));
return () => {
unsubs.forEach(u => u());
ro.disconnect();
cancelAnimationFrame(layoutRaf);
cancelAnimationFrame(raf1);
cancelAnimationFrame(raf2);
};
}, [manager, redraw]);
return (
<canvas
ref={canvasRef}
className="trade-signal-label-overlay"
aria-hidden
style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
zIndex: 8,
}}
/>
);
};
export default TradeSignalLabelOverlay;
+4
View File
@@ -64,6 +64,7 @@ import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKe
import { sortIndicatorsForPaneLoad } from '../utils/indicatorPaneMerge';
import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator';
import PaneSeparatorOverlay from './PaneSeparatorOverlay';
import TradeSignalLabelOverlay from './TradeSignalLabelOverlay';
import { centerChartOnSignalBar } from '../utils/tradeSignalChartMarkers';
interface TradingChartProps {
@@ -1861,6 +1862,9 @@ const TradingChart: React.FC<TradingChartProps> = ({
options={paneSeparatorOptions}
/>
)}
{chartMgr && chartPaintReady && (
<TradeSignalLabelOverlay manager={chartMgr} />
)}
{chartMgr && (
<DrawingCanvas
manager={chartMgr}