알림목록 캔들차트 매매 시그널 표시
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user