매수매도 시그널 표시 수정
This commit is contained in:
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user