백테스팅 실시간 매매 수정

This commit is contained in:
Macbook
2026-06-13 02:33:17 +09:00
parent f0c7fbd5e9
commit 95595f7e9e
3 changed files with 45 additions and 24 deletions
@@ -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,
+6 -3
View File
@@ -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
+3 -2
View File
@@ -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,