가상투자 화면 추가
This commit is contained in:
@@ -1,135 +1,34 @@
|
||||
/**
|
||||
* 가상투자 — 종목×전략 차트 팝업 (캔들 + 전략 보조지표)
|
||||
* 가상투자 — 종목×전략 차트 팝업 (레거시·외부 호출용)
|
||||
*/
|
||||
import React, {
|
||||
useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
import React from 'react';
|
||||
import AppPopup from '../AppPopup';
|
||||
import TradingChart from '../TradingChart';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import { pinCandleWatch } from '../../utils/backendApi';
|
||||
import type {
|
||||
ChartMode, ChartType, Drawing, LegendData, OHLCVBar, Theme, Timeframe,
|
||||
} from '../../types';
|
||||
import type { Theme } from '../../types';
|
||||
import { getKoreanName } from '../../utils/marketNameCache';
|
||||
import { isUpbitMarket } from '../../utils/upbitApi';
|
||||
import { useChartRealtimeData, type WsStatus } from '../../hooks/useChartRealtimeData';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
import type { ChartManager } from '../../utils/ChartManager';
|
||||
import {
|
||||
buildStrategyChartIndicators,
|
||||
resolveStrategyPrimaryTimeframe,
|
||||
resolveStrategyTimeframes,
|
||||
} from '../../utils/strategyToChartIndicators';
|
||||
import { timeframeToCandleType } from '../../utils/chartCandleType';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
||||
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||
import VirtualTargetCardChart from './VirtualTargetCardChart';
|
||||
import { useAppSettings, resolveAppDefaults } from '../../hooks/useAppSettings';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
strategy: StrategyDto | undefined;
|
||||
theme?: Theme;
|
||||
running?: boolean;
|
||||
chartRealtimeSource?: ChartRealtimeSource;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function WsBadge({ status }: { status: WsStatus }) {
|
||||
const map: Record<WsStatus, { cls: string; label: string }> = {
|
||||
connecting: { cls: 'vtd-chart-ws--connecting', label: '연결 중' },
|
||||
connected: { cls: 'vtd-chart-ws--live', label: '실시간' },
|
||||
disconnected: { cls: 'vtd-chart-ws--off', label: '연결 끊김' },
|
||||
error: { cls: 'vtd-chart-ws--off', label: '오류' },
|
||||
};
|
||||
const { cls, label } = map[status];
|
||||
return (
|
||||
<span className={`vtd-chart-ws ${cls}`}>
|
||||
<span className="vtd-chart-ws-dot" aria-hidden />
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const VirtualStrategyChartPopup: React.FC<Props> = ({
|
||||
market,
|
||||
strategy,
|
||||
theme = 'dark',
|
||||
running = false,
|
||||
chartRealtimeSource = 'BACKEND_STOMP',
|
||||
onClose,
|
||||
}) => {
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
|
||||
const [timeframe, setTimeframe] = useState<Timeframe>(() => resolveStrategyPrimaryTimeframe(strategy));
|
||||
|
||||
useEffect(() => {
|
||||
setTimeframe(resolveStrategyPrimaryTimeframe(strategy));
|
||||
}, [strategy, market]);
|
||||
|
||||
const indicators = useMemo(
|
||||
() => buildStrategyChartIndicators(strategy, getParams, getVisualConfig),
|
||||
[strategy, getParams, getVisualConfig],
|
||||
);
|
||||
|
||||
const managerRef = useRef<ChartManager | null>(null);
|
||||
const pendingBarRef = useRef<OHLCVBar | null>(null);
|
||||
const barsMarketRef = useRef<string | null>(null);
|
||||
const chartLiveReadyRef = useRef(false);
|
||||
const marketRef = useRef(market);
|
||||
marketRef.current = market;
|
||||
|
||||
const handleCandlesReady = useCallback(() => {
|
||||
chartLiveReadyRef.current = true;
|
||||
const pending = pendingBarRef.current;
|
||||
const mgr = managerRef.current;
|
||||
if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return;
|
||||
pendingBarRef.current = null;
|
||||
mgr.updateBar(pending);
|
||||
}, []);
|
||||
|
||||
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
|
||||
if (barsMarketRef.current !== marketRef.current) return;
|
||||
if (!chartLiveReadyRef.current || !managerRef.current) {
|
||||
pendingBarRef.current = bar;
|
||||
return;
|
||||
}
|
||||
if (append) managerRef.current.appendBar(bar);
|
||||
else managerRef.current.updateBar(bar);
|
||||
}, []);
|
||||
|
||||
const handleTickUpdate = useCallback((bar: OHLCVBar) => {
|
||||
applyRealtimeBar(bar, false);
|
||||
}, [applyRealtimeBar]);
|
||||
|
||||
const handleNewCandle = useCallback((bar: OHLCVBar) => {
|
||||
applyRealtimeBar(bar, true);
|
||||
}, [applyRealtimeBar]);
|
||||
|
||||
const useUpbit = isUpbitMarket(market);
|
||||
|
||||
const { bars, barsMarket, wsStatus, isLoading } = useChartRealtimeData(
|
||||
market,
|
||||
timeframe,
|
||||
useMemo(() => ({
|
||||
onTickUpdate: handleTickUpdate,
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: useUpbit,
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit]),
|
||||
);
|
||||
|
||||
barsMarketRef.current = barsMarket;
|
||||
|
||||
useEffect(() => {
|
||||
chartLiveReadyRef.current = false;
|
||||
pendingBarRef.current = null;
|
||||
}, [market, timeframe]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useUpbit) return;
|
||||
void pinCandleWatch(market, timeframeToCandleType(timeframe));
|
||||
if (timeframeToCandleType(timeframe) !== '1m') {
|
||||
void pinCandleWatch(market, '1m');
|
||||
}
|
||||
}, [market, timeframe, useUpbit, running]);
|
||||
const { settings: appSettings } = useAppSettings();
|
||||
const chartSeriesPriceLabels = resolveAppDefaults(appSettings).chartSeriesPriceLabels;
|
||||
|
||||
const ko = getKoreanName(market);
|
||||
const sym = market.replace(/^KRW-/, '');
|
||||
@@ -147,58 +46,18 @@ const VirtualStrategyChartPopup: React.FC<Props> = ({
|
||||
backdrop
|
||||
closeOnBackdrop
|
||||
bodyClassName="app-popup-body vtd-chart-popup-body"
|
||||
headerExtra={running ? <WsBadge status={wsStatus} /> : undefined}
|
||||
>
|
||||
<div className="vtd-chart-popup-meta">
|
||||
<span className="vtd-chart-popup-strat">{stratName}</span>
|
||||
{indicators.length > 0 && (
|
||||
<span className="vtd-chart-popup-ind-count">보조지표 {indicators.length}개</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="vtd-chart-popup-tf-row">
|
||||
{tfOptions.map(tf => (
|
||||
<button
|
||||
key={tf}
|
||||
type="button"
|
||||
className={`vtd-chart-popup-tf${timeframe === tf ? ' vtd-chart-popup-tf--on' : ''}`}
|
||||
onClick={() => setTimeframe(tf)}
|
||||
>
|
||||
{tf}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="vtd-chart-popup-canvas-wrap">
|
||||
{isLoading && <div className="vtd-chart-popup-loading">차트 로딩…</div>}
|
||||
<TradingChart
|
||||
bars={bars}
|
||||
barsMarket={barsMarket}
|
||||
market={market}
|
||||
timeframe={timeframe}
|
||||
chartType={'candlestick' as ChartType}
|
||||
theme={theme}
|
||||
mode={'chart' as ChartMode}
|
||||
indicators={indicators}
|
||||
drawingTool="cursor"
|
||||
drawings={[] as Drawing[]}
|
||||
logScale={false}
|
||||
drawingsLocked
|
||||
drawingsVisible={false}
|
||||
displayTimezone={DEFAULT_DISPLAY_TIMEZONE}
|
||||
onCrosshair={noop as (d: LegendData | null) => void}
|
||||
onManagerReady={mgr => { managerRef.current = mgr; }}
|
||||
onAddDrawing={noop as (d: Drawing) => void}
|
||||
onCandlesReady={handleCandlesReady}
|
||||
magnifierEnabled={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{indicators.length === 0 && (
|
||||
<p className="vtd-muted vtd-chart-popup-empty">
|
||||
전략에 차트로 표시할 보조지표가 없습니다.
|
||||
</p>
|
||||
)}
|
||||
<VirtualTargetCardChart
|
||||
market={market}
|
||||
strategy={strategy}
|
||||
theme={theme}
|
||||
running={running}
|
||||
chartRealtimeSource={chartRealtimeSource}
|
||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||
/>
|
||||
</AppPopup>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user