알림목록 캔들차트 매매 시그널 표시

This commit is contained in:
Macbook
2026-06-09 00:55:37 +09:00
parent c9b0441500
commit 8a2d8054ad
5 changed files with 125 additions and 8 deletions
@@ -120,6 +120,11 @@ const FullscreenChartModal: React.FC<Props> = ({ item, theme, onClose }) => {
indicators={allIndicatorConfigs} indicators={allIndicatorConfigs}
enabled enabled
fillHeight fillHeight
signalMarker={{
candleTime: item.candleTime,
signalType: item.signalType,
price: item.price,
}}
/> />
</div> </div>
</div> </div>
@@ -232,6 +232,15 @@ const TradeNotificationListRow: React.FC<Props> = ({
const indicatorScrollNeeded = indicatorChartCount >= 2; const indicatorScrollNeeded = indicatorChartCount >= 2;
const indicatorPlaceholder = item.strategyId != null && indicatorChartCount === 0 && chartActive; const indicatorPlaceholder = item.strategyId != null && indicatorChartCount === 0 && chartActive;
const signalMarker = useMemo(
() => ({
candleTime: item.candleTime,
signalType: item.signalType,
price: item.price,
}),
[item.candleTime, item.signalType, item.price],
);
const handleRowClick = useCallback((e: React.MouseEvent) => { const handleRowClick = useCallback((e: React.MouseEvent) => {
const t = e.target as HTMLElement; const t = e.target as HTMLElement;
if (t.closest( if (t.closest(
@@ -432,6 +441,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
onExpand={() => setExpandedChartKey('candle')} onExpand={() => setExpandedChartKey('candle')}
onRestore={() => setExpandedChartKey(null)} onRestore={() => setExpandedChartKey(null)}
onRealtimeActivity={chartActive ? bumpChartActivity : undefined} onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
signalMarker={signalMarker}
/> />
</div> </div>
) : ( ) : (
@@ -476,6 +486,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
onExpand={() => setExpandedChartKey('candle')} onExpand={() => setExpandedChartKey('candle')}
onRestore={() => setExpandedChartKey(null)} onRestore={() => setExpandedChartKey(null)}
onRealtimeActivity={chartActive ? bumpChartActivity : undefined} onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
signalMarker={signalMarker}
/> />
{indicatorPlaceholder && ( {indicatorPlaceholder && (
<div className="tnl-combined-chart-placeholder"> </div> <div className="tnl-combined-chart-placeholder"> </div>
@@ -5,6 +5,7 @@ import React from 'react';
import type { IndicatorConfig } from '../../types'; import type { IndicatorConfig } from '../../types';
import type { Theme, Timeframe } from '../../types'; import type { Theme, Timeframe } from '../../types';
import TradeSignalMiniChart from './TradeSignalMiniChart'; import TradeSignalMiniChart from './TradeSignalMiniChart';
import type { TradeSignalChartMarker } from '../../utils/tradeSignalChartMarkers';
const IcExpand = () => ( const IcExpand = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
@@ -36,6 +37,7 @@ export interface TradeSignalChartCardProps {
onExpand: () => void; onExpand: () => void;
onRestore: () => void; onRestore: () => void;
onRealtimeActivity?: () => void; onRealtimeActivity?: () => void;
signalMarker?: TradeSignalChartMarker;
} }
const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({ const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
@@ -50,6 +52,7 @@ const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
onExpand, onExpand,
onRestore, onRestore,
onRealtimeActivity, onRealtimeActivity,
signalMarker,
}) => ( }) => (
<section <section
className={[ className={[
@@ -70,6 +73,7 @@ const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
enabled={enabled} enabled={enabled}
fillHeight={expanded} fillHeight={expanded}
onRealtimeActivity={onRealtimeActivity} onRealtimeActivity={onRealtimeActivity}
signalMarker={signalMarker}
/> />
{expanded ? ( {expanded ? (
<button <button
@@ -19,6 +19,10 @@ import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings'; import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
import { useSessionCandleOverlayControls } from '../../hooks/useSessionCandleOverlayControls'; import { useSessionCandleOverlayControls } from '../../hooks/useSessionCandleOverlayControls';
import { ensurePaperChartOverlays } from '../../utils/strategyToChartIndicators'; import { ensurePaperChartOverlays } from '../../utils/strategyToChartIndicators';
import {
applyNotificationSignalMarkers,
type TradeSignalChartMarker,
} from '../../utils/tradeSignalChartMarkers';
interface Props { interface Props {
market: string; market: string;
@@ -30,6 +34,8 @@ interface Props {
fillHeight?: boolean; fillHeight?: boolean;
/** STOMP/WS 실시간 봉 수신 시 (목록 행 형광 플래시 등) */ /** STOMP/WS 실시간 봉 수신 시 (목록 행 형광 플래시 등) */
onRealtimeActivity?: () => void; onRealtimeActivity?: () => void;
/** 알림 발생 시점 매매 시그널 (캔들 마커) */
signalMarker?: TradeSignalChartMarker;
} }
const noop = () => {}; const noop = () => {};
@@ -42,6 +48,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
enabled = true, enabled = true,
fillHeight = false, fillHeight = false,
onRealtimeActivity, onRealtimeActivity,
signalMarker,
}) => { }) => {
const { getParams, getVisualConfig } = useIndicatorSettings(); const { getParams, getVisualConfig } = useIndicatorSettings();
const managerRef = useRef<ChartManager | null>(null); const managerRef = useRef<ChartManager | null>(null);
@@ -53,6 +60,8 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const chartLiveReadyRef = useRef(false); const chartLiveReadyRef = useRef(false);
const marketRef = useRef(market); const marketRef = useRef(market);
marketRef.current = market; marketRef.current = market;
const signalMarkerRef = useRef(signalMarker);
signalMarkerRef.current = signalMarker;
const baseIndicators = useMemo( const baseIndicators = useMemo(
() => ensurePaperChartOverlays(indicators, getParams, getVisualConfig), () => ensurePaperChartOverlays(indicators, getParams, getVisualConfig),
@@ -66,6 +75,15 @@ const TradeSignalMiniChart: React.FC<Props> = ({
syncToManager, syncToManager,
} = useSessionCandleOverlayControls(managerRef, baseIndicators); } = useSessionCandleOverlayControls(managerRef, baseIndicators);
const applySignalMarkers = useCallback((attempt = 0) => {
const mgr = managerRef.current;
if (!mgr?.hasMainSeries()) {
if (attempt < 80) requestAnimationFrame(() => applySignalMarkers(attempt + 1));
return;
}
applyNotificationSignalMarkers(mgr, signalMarkerRef.current, false);
}, []);
const syncChartLayout = useCallback(() => { const syncChartLayout = useCallback(() => {
const wrap = canvasWrapRef.current; const wrap = canvasWrapRef.current;
const mgr = managerRef.current; const mgr = managerRef.current;
@@ -81,11 +99,19 @@ const TradeSignalMiniChart: React.FC<Props> = ({
chartLiveReadyRef.current = true; chartLiveReadyRef.current = true;
const pending = pendingBarRef.current; const pending = pendingBarRef.current;
const mgr = managerRef.current; const mgr = managerRef.current;
if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return; if (pending && mgr && barsMarketRef.current === marketRef.current) {
pendingBarRef.current = null; pendingBarRef.current = null;
mgr.updateBar(pending); mgr.updateBar(pending);
}
syncChartLayout(); syncChartLayout();
}, [syncChartLayout]); applySignalMarkers();
}, [syncChartLayout, applySignalMarkers]);
const handleManagerReady = useCallback((mgr: ChartManager) => {
managerRef.current = mgr;
syncToManager(mgr);
applySignalMarkers();
}, [syncToManager, applySignalMarkers]);
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => { const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
if (barsMarketRef.current !== marketRef.current) return; if (barsMarketRef.current !== marketRef.current) return;
@@ -134,8 +160,17 @@ const TradeSignalMiniChart: React.FC<Props> = ({
chartLiveReadyRef.current = false; chartLiveReadyRef.current = false;
pendingBarRef.current = null; pendingBarRef.current = null;
chartReloadTriggeredRef.current = false; chartReloadTriggeredRef.current = false;
managerRef.current?.clearBacktestMarkers();
}, [market, timeframe, enabled]); }, [market, timeframe, enabled]);
useEffect(() => {
if (!enabled) {
managerRef.current?.clearBacktestMarkers();
return;
}
applySignalMarkers();
}, [enabled, signalMarker, applySignalMarkers, barsReady]);
useEffect(() => { useEffect(() => {
if (!enabled) return; if (!enabled) return;
const wrap = canvasWrapRef.current; const wrap = canvasWrapRef.current;
@@ -223,10 +258,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
drawingsLocked drawingsLocked
drawingsVisible={false} drawingsVisible={false}
onCrosshair={noop as (d: LegendData | null) => void} onCrosshair={noop as (d: LegendData | null) => void}
onManagerReady={mgr => { onManagerReady={handleManagerReady}
managerRef.current = mgr;
syncToManager(mgr);
}}
onAddDrawing={noop as (d: Drawing) => void} onAddDrawing={noop as (d: Drawing) => void}
onCandlesReady={handleCandlesReady} onCandlesReady={handleCandlesReady}
magnifierEnabled={false} magnifierEnabled={false}
@@ -0,0 +1,65 @@
/**
* 알림 목록 미니 차트 — 매매 시그널 마커
*/
import type { ChartManager } from './ChartManager';
export interface TradeSignalChartMarker {
candleTime: number;
signalType: 'BUY' | 'SELL';
price: number;
}
/** 알림 candleTime → 차트 bar time (정확 일치 또는 최근접 bar) */
export function resolveSignalMarkerBarTime(
mgr: ChartManager,
candleTimeSec: number,
): number | null {
if (!Number.isFinite(candleTimeSec) || candleTimeSec <= 0) return null;
if (mgr.getBarAtTime(candleTimeSec)) return candleTimeSec;
const bars = mgr.getRawBars();
if (bars.length === 0) return null;
let bestTime = bars[0].time;
let bestDist = Math.abs(bars[0].time - candleTimeSec);
for (let i = 1; i < bars.length; i++) {
const t = bars[i].time;
const dist = Math.abs(t - candleTimeSec);
if (dist < bestDist) {
bestDist = dist;
bestTime = t;
}
}
const maxSnapSec = Math.max(86400, estimateBarIntervalSec(bars) * 2);
return bestDist <= maxSnapSec ? bestTime : null;
}
function estimateBarIntervalSec(bars: { time: number }[]): number {
if (bars.length < 2) return 60;
const last = bars[bars.length - 1].time - bars[bars.length - 2].time;
return last > 0 ? last : 60;
}
export function buildNotificationSignalMarkers(
mgr: ChartManager,
marker: TradeSignalChartMarker | undefined,
): Array<{ time: number; type: string; price: number }> {
if (!marker || !Number.isFinite(marker.price) || marker.price <= 0) return [];
const time = resolveSignalMarkerBarTime(mgr, marker.candleTime);
if (time == null) return [];
return [{ time, type: marker.signalType, price: marker.price }];
}
export function applyNotificationSignalMarkers(
mgr: ChartManager,
marker: TradeSignalChartMarker | undefined,
showPrice = false,
): void {
const signals = buildNotificationSignalMarkers(mgr, marker);
if (signals.length === 0) {
mgr.clearBacktestMarkers();
return;
}
mgr.setBacktestMarkers(signals, showPrice);
}