백테스팅 실시간 매매 수정
This commit is contained in:
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import type { ChartManager } from '../utils/ChartManager';
|
||||
import {
|
||||
layoutStackedSignalLabels,
|
||||
resolveSignalBarTime,
|
||||
resolveSignalLabelAnchors,
|
||||
type DrawableSignalLabel,
|
||||
type TradeSignalLabelItem,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
|
||||
interface Props {
|
||||
manager: ChartManager;
|
||||
/** tv-chart-wrap 등 상위 — DrawingCanvas(z-index 10) 위에 표시 */
|
||||
getMountTarget?: () => HTMLElement | null;
|
||||
}
|
||||
|
||||
const ARROW_OFFSET = 16;
|
||||
@@ -18,9 +21,10 @@ const FONT = '600 11px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
|
||||
function resolveArrowY(
|
||||
mgr: ChartManager,
|
||||
item: TradeSignalLabelItem,
|
||||
barTime: number,
|
||||
mainTopY: number,
|
||||
): number | null {
|
||||
const bar = mgr.getBarAtTime(item.time);
|
||||
const bar = mgr.getBarAtTime(barTime);
|
||||
let paneY: number | null;
|
||||
if (bar) {
|
||||
paneY = item.isBuy ? mgr.priceToY(bar.low) : mgr.priceToY(bar.high);
|
||||
@@ -60,9 +64,10 @@ function renderTradeSignalLabels(
|
||||
|
||||
const drawable: DrawableSignalLabel[] = [];
|
||||
for (const item of labels) {
|
||||
const x = mgr.timeToX(item.time);
|
||||
const barTime = resolveSignalBarTime(mgr, item.time);
|
||||
const x = mgr.timeToX(barTime);
|
||||
if (x == null || x < -40 || x > cssW + 40) continue;
|
||||
const arrowY = resolveArrowY(mgr, item, main.topY);
|
||||
const arrowY = resolveArrowY(mgr, item, barTime, main.topY);
|
||||
if (arrowY == null) continue;
|
||||
drawable.push({ item, x, arrowY });
|
||||
}
|
||||
@@ -94,35 +99,45 @@ function renderTradeSignalLabels(
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
const TradeSignalLabelOverlay: React.FC<Props> = ({ manager, getMountTarget }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
|
||||
const redrawRafRef = useRef(0);
|
||||
|
||||
const resolveMountTarget = useCallback((): HTMLElement | null => {
|
||||
return getMountTarget?.() ?? manager.getContainer();
|
||||
}, [getMountTarget, manager]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = manager.getContainer();
|
||||
const container = resolveMountTarget();
|
||||
if (container) {
|
||||
setPortalTarget(container);
|
||||
return;
|
||||
}
|
||||
const tid = window.setTimeout(() => setPortalTarget(manager.getContainer()), 100);
|
||||
const tid = window.setTimeout(() => setPortalTarget(resolveMountTarget()), 100);
|
||||
return () => clearTimeout(tid);
|
||||
}, [manager]);
|
||||
}, [resolveMountTarget]);
|
||||
|
||||
const redraw = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const container = manager.getContainer();
|
||||
if (!canvas || !container) return;
|
||||
const chartContainer = manager.getContainer();
|
||||
const mountEl = resolveMountTarget();
|
||||
if (!canvas || !chartContainer || !mountEl) return;
|
||||
|
||||
const cssW = container.clientWidth;
|
||||
const cssH = container.clientHeight;
|
||||
const cssW = chartContainer.clientWidth;
|
||||
const cssH = chartContainer.clientHeight;
|
||||
if (cssW <= 0 || cssH <= 0) return;
|
||||
|
||||
const chartRect = chartContainer.getBoundingClientRect();
|
||||
const mountRect = mountEl.getBoundingClientRect();
|
||||
canvas.style.left = `${chartRect.left - mountRect.left}px`;
|
||||
canvas.style.top = `${chartRect.top - mountRect.top}px`;
|
||||
canvas.style.width = `${cssW}px`;
|
||||
canvas.style.height = `${cssH}px`;
|
||||
|
||||
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;
|
||||
@@ -130,7 +145,7 @@ const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
ctx.clearRect(0, 0, cssW, cssH);
|
||||
|
||||
renderTradeSignalLabels(ctx, manager, cssW, manager.getTradeSignalLabels());
|
||||
}, [manager]);
|
||||
}, [manager, resolveMountTarget]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!portalTarget) return;
|
||||
@@ -153,17 +168,20 @@ const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
manager.subscribeTradeSignalLabels(scheduleRedraw),
|
||||
];
|
||||
|
||||
const container = manager.getContainer();
|
||||
const chartContainer = manager.getContainer();
|
||||
const ro = new ResizeObserver(scheduleRedraw);
|
||||
if (container) ro.observe(container);
|
||||
if (chartContainer) ro.observe(chartContainer);
|
||||
if (portalTarget !== chartContainer) ro.observe(portalTarget);
|
||||
|
||||
const scrollWrap = container?.closest('.tv-chart-wrap') as HTMLElement | null;
|
||||
const scrollWrap = portalTarget.closest('.tv-chart-wrap') as HTMLElement | null;
|
||||
scrollWrap?.addEventListener('scroll', scheduleRedraw, { passive: true });
|
||||
window.addEventListener('scroll', scheduleRedraw, { passive: true, capture: true });
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(u => u());
|
||||
ro.disconnect();
|
||||
scrollWrap?.removeEventListener('scroll', scheduleRedraw);
|
||||
window.removeEventListener('scroll', scheduleRedraw, true);
|
||||
if (redrawRafRef.current) {
|
||||
cancelAnimationFrame(redrawRafRef.current);
|
||||
redrawRafRef.current = 0;
|
||||
@@ -180,9 +198,8 @@ const TradeSignalLabelOverlay: React.FC<Props> = ({ manager }) => {
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 8,
|
||||
zIndex: 15,
|
||||
}}
|
||||
/>,
|
||||
portalTarget,
|
||||
|
||||
@@ -1885,9 +1885,6 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
options={paneSeparatorOptions}
|
||||
/>
|
||||
)}
|
||||
{chartMgr && chartPaintReady && (
|
||||
<TradeSignalLabelOverlay manager={chartMgr} />
|
||||
)}
|
||||
{chartMgr && (
|
||||
<DrawingCanvas
|
||||
manager={chartMgr}
|
||||
@@ -1902,6 +1899,12 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
selectedDrawingId={selectedDrawingId}
|
||||
/>
|
||||
)}
|
||||
{chartMgr && chartPaintReady && (
|
||||
<TradeSignalLabelOverlay
|
||||
manager={chartMgr}
|
||||
getMountTarget={() => wrapperRef.current}
|
||||
/>
|
||||
)}
|
||||
{/* PaneLegend: containerRef 를 상태에 저장해 React 렌더와 동기화 */}
|
||||
{chartMgr && showCandlePaneControls && !candleOnlyLocked && (
|
||||
<CandlePaneControlsPortal
|
||||
|
||||
@@ -30,6 +30,7 @@ import { formatUpbitKrwPrice } from './safeFormat';
|
||||
import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from './tradeSignalColors';
|
||||
import {
|
||||
toTradeSignalLabelItem,
|
||||
resolveSignalBarTime,
|
||||
type TradeSignalLabelItem,
|
||||
type TradeSignalMarkerWithPrice,
|
||||
} from './tradeSignalLabels';
|
||||
@@ -1832,7 +1833,7 @@ export class ChartManager {
|
||||
? `${label} : ${formatUpbitKrwPrice(s.price)}`
|
||||
: label;
|
||||
return {
|
||||
time: s.time,
|
||||
time: resolveSignalBarTime(this, s.time),
|
||||
position: buy ? ('belowBar' as const) : ('aboveBar' as const),
|
||||
shape: buy ? ('arrowUp' as const) : ('arrowDown' as const),
|
||||
color: s.type === 'PARTIAL_SELL' ? '#FF9800' : (buy ? TRADE_BUY_COLOR : TRADE_SELL_COLOR),
|
||||
@@ -1868,7 +1869,7 @@ export class ChartManager {
|
||||
? `${label} : ${formatUpbitKrwPrice(m.price)}`
|
||||
: label;
|
||||
return {
|
||||
time: m.time,
|
||||
time: resolveSignalBarTime(this, m.time),
|
||||
position: buy ? ('belowBar' as const) : ('aboveBar' as const),
|
||||
shape: buy ? ('arrowUp' as const) : ('arrowDown' as const),
|
||||
color: buy ? TRADE_BUY_COLOR : TRADE_SELL_COLOR,
|
||||
|
||||
Reference in New Issue
Block a user