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