알림목록 캔들차트 매매 시그널 표시
This commit is contained in:
@@ -120,6 +120,11 @@ const FullscreenChartModal: React.FC<Props> = ({ item, theme, onClose }) => {
|
||||
indicators={allIndicatorConfigs}
|
||||
enabled
|
||||
fillHeight
|
||||
signalMarker={{
|
||||
candleTime: item.candleTime,
|
||||
signalType: item.signalType,
|
||||
price: item.price,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,6 +232,15 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
const indicatorScrollNeeded = indicatorChartCount >= 2;
|
||||
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 t = e.target as HTMLElement;
|
||||
if (t.closest(
|
||||
@@ -432,6 +441,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
onExpand={() => setExpandedChartKey('candle')}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
|
||||
signalMarker={signalMarker}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -476,6 +486,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
onExpand={() => setExpandedChartKey('candle')}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
|
||||
signalMarker={signalMarker}
|
||||
/>
|
||||
{indicatorPlaceholder && (
|
||||
<div className="tnl-combined-chart-placeholder">전략 지표 로딩 중…</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import type { IndicatorConfig } from '../../types';
|
||||
import type { Theme, Timeframe } from '../../types';
|
||||
import TradeSignalMiniChart from './TradeSignalMiniChart';
|
||||
import type { TradeSignalChartMarker } from '../../utils/tradeSignalChartMarkers';
|
||||
|
||||
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>
|
||||
@@ -36,6 +37,7 @@ export interface TradeSignalChartCardProps {
|
||||
onExpand: () => void;
|
||||
onRestore: () => void;
|
||||
onRealtimeActivity?: () => void;
|
||||
signalMarker?: TradeSignalChartMarker;
|
||||
}
|
||||
|
||||
const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
|
||||
@@ -50,6 +52,7 @@ const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
|
||||
onExpand,
|
||||
onRestore,
|
||||
onRealtimeActivity,
|
||||
signalMarker,
|
||||
}) => (
|
||||
<section
|
||||
className={[
|
||||
@@ -70,6 +73,7 @@ const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
|
||||
enabled={enabled}
|
||||
fillHeight={expanded}
|
||||
onRealtimeActivity={onRealtimeActivity}
|
||||
signalMarker={signalMarker}
|
||||
/>
|
||||
{expanded ? (
|
||||
<button
|
||||
|
||||
@@ -19,6 +19,10 @@ import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
import { useSessionCandleOverlayControls } from '../../hooks/useSessionCandleOverlayControls';
|
||||
import { ensurePaperChartOverlays } from '../../utils/strategyToChartIndicators';
|
||||
import {
|
||||
applyNotificationSignalMarkers,
|
||||
type TradeSignalChartMarker,
|
||||
} from '../../utils/tradeSignalChartMarkers';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
@@ -30,6 +34,8 @@ interface Props {
|
||||
fillHeight?: boolean;
|
||||
/** STOMP/WS 실시간 봉 수신 시 (목록 행 형광 플래시 등) */
|
||||
onRealtimeActivity?: () => void;
|
||||
/** 알림 발생 시점 매매 시그널 (캔들 마커) */
|
||||
signalMarker?: TradeSignalChartMarker;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
@@ -42,6 +48,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
enabled = true,
|
||||
fillHeight = false,
|
||||
onRealtimeActivity,
|
||||
signalMarker,
|
||||
}) => {
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
const managerRef = useRef<ChartManager | null>(null);
|
||||
@@ -53,6 +60,8 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
const chartLiveReadyRef = useRef(false);
|
||||
const marketRef = useRef(market);
|
||||
marketRef.current = market;
|
||||
const signalMarkerRef = useRef(signalMarker);
|
||||
signalMarkerRef.current = signalMarker;
|
||||
|
||||
const baseIndicators = useMemo(
|
||||
() => ensurePaperChartOverlays(indicators, getParams, getVisualConfig),
|
||||
@@ -66,6 +75,15 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
syncToManager,
|
||||
} = 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 wrap = canvasWrapRef.current;
|
||||
const mgr = managerRef.current;
|
||||
@@ -81,11 +99,19 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
chartLiveReadyRef.current = true;
|
||||
const pending = pendingBarRef.current;
|
||||
const mgr = managerRef.current;
|
||||
if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return;
|
||||
if (pending && mgr && barsMarketRef.current === marketRef.current) {
|
||||
pendingBarRef.current = null;
|
||||
mgr.updateBar(pending);
|
||||
}
|
||||
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) => {
|
||||
if (barsMarketRef.current !== marketRef.current) return;
|
||||
@@ -134,8 +160,17 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
chartLiveReadyRef.current = false;
|
||||
pendingBarRef.current = null;
|
||||
chartReloadTriggeredRef.current = false;
|
||||
managerRef.current?.clearBacktestMarkers();
|
||||
}, [market, timeframe, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
managerRef.current?.clearBacktestMarkers();
|
||||
return;
|
||||
}
|
||||
applySignalMarkers();
|
||||
}, [enabled, signalMarker, applySignalMarkers, barsReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const wrap = canvasWrapRef.current;
|
||||
@@ -223,10 +258,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
drawingsLocked
|
||||
drawingsVisible={false}
|
||||
onCrosshair={noop as (d: LegendData | null) => void}
|
||||
onManagerReady={mgr => {
|
||||
managerRef.current = mgr;
|
||||
syncToManager(mgr);
|
||||
}}
|
||||
onManagerReady={handleManagerReady}
|
||||
onAddDrawing={noop as (d: Drawing) => void}
|
||||
onCandlesReady={handleCandlesReady}
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user