투자관리 차트탭 기능 수정
This commit is contained in:
@@ -1,30 +1,195 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { Theme, Timeframe } from '../../types';
|
||||
import PaperMiniChart from './PaperMiniChart';
|
||||
import PaperIndicatorPanel from './PaperIndicatorPanel';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import BacktestAnalysisChart from '../backtest/BacktestAnalysisChart';
|
||||
import {
|
||||
DEFAULT_BACKTEST_SETTINGS,
|
||||
loadStrategy,
|
||||
runBacktest,
|
||||
type BacktestSignal,
|
||||
type StrategyDto,
|
||||
} from '../../utils/backendApi';
|
||||
import { loadAnalysisCandles } from '../../utils/analysisChartData';
|
||||
import { normalizeChartTimeframe } from '../../utils/backtestUiUtils';
|
||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||
import type { OHLCVBar, Theme } from '../../types';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
|
||||
const BAR_COUNT = 300;
|
||||
const HISTORY_BACKTEST_DEBOUNCE_MS = 450;
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
strategyId?: number | null;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
/** 모의투자 중앙 — 상단 캔들 + 하단 보조지표 (실시간 차트 레이아웃) */
|
||||
const PaperAnalysisChart: React.FC<Props> = ({ market, theme = 'dark' }) => {
|
||||
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
||||
/**
|
||||
* 모의투자 차트 탭 — 전략 백테스트 시그널 + 캔들·보조지표 (백테스팅 분석 차트와 동일)
|
||||
* 좌측(과거) 스크롤 시 추가 캔들·보조지표 로드 후 전체 구간 시그널 재계산.
|
||||
*/
|
||||
const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark' }) => {
|
||||
const { getParams } = useIndicatorSettings();
|
||||
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||
const [timeframe, setTimeframe] = useState('1h');
|
||||
const [running, setRunning] = useState(false);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const toTimeSec = useMemo(() => Math.floor(Date.now() / 1000), [market, strategyId]);
|
||||
const backtestGenRef = useRef(0);
|
||||
const historyDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const strategyRef = useRef(strategy);
|
||||
const strategyIdRef = useRef(strategyId);
|
||||
strategyRef.current = strategy;
|
||||
strategyIdRef.current = strategyId;
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategyId) {
|
||||
setStrategy(undefined);
|
||||
setSignals([]);
|
||||
setError('가상매매 투자대상에 종목 전략 또는 기본 전략이 설정되어 있지 않습니다.');
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setError(null);
|
||||
void loadStrategy(strategyId).then(s => {
|
||||
if (!cancelled) setStrategy(s ?? undefined);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [strategyId]);
|
||||
|
||||
const runSignalsForBars = useCallback(async (
|
||||
bars: OHLCVBar[],
|
||||
tf: string,
|
||||
strat: StrategyDto,
|
||||
sid: number,
|
||||
) => {
|
||||
const sym = market.startsWith('KRW-') ? market : `KRW-${market}`;
|
||||
if (bars.length < 10) {
|
||||
setSignals([]);
|
||||
setError('캔들 데이터가 부족합니다.');
|
||||
return;
|
||||
}
|
||||
const gen = ++backtestGenRef.current;
|
||||
setRunning(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await runBacktest({
|
||||
strategyId: sid,
|
||||
bars: bars.map(b => ({
|
||||
time: b.time,
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
})),
|
||||
timeframe: tf,
|
||||
symbol: sym,
|
||||
strategyName: strat.name,
|
||||
settings: DEFAULT_BACKTEST_SETTINGS,
|
||||
indicatorParams: Object.fromEntries(
|
||||
['RSI', 'MACD', 'CCI', 'SMA', 'EMA', 'IchimokuCloud', 'Stochastic', 'ADX', 'MFI', 'NewPsychological'].map(t => [
|
||||
t,
|
||||
getParams(t) as Record<string, unknown>,
|
||||
]),
|
||||
),
|
||||
});
|
||||
if (gen !== backtestGenRef.current) return;
|
||||
if (!res) {
|
||||
setSignals([]);
|
||||
setError('시그널 계산에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
setSignals(res.signals);
|
||||
} catch (e) {
|
||||
if (gen !== backtestGenRef.current) return;
|
||||
setSignals([]);
|
||||
setError(e instanceof Error ? e.message : '차트 분석 실패');
|
||||
} finally {
|
||||
if (gen === backtestGenRef.current) setRunning(false);
|
||||
}
|
||||
}, [market, getParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategyId || !strategy) {
|
||||
setSignals([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy));
|
||||
setTimeframe(tf);
|
||||
setError(null);
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const sym = market.startsWith('KRW-') ? market : `KRW-${market}`;
|
||||
const bars = await loadAnalysisCandles(sym, tf, toTimeSec, BAR_COUNT);
|
||||
if (cancelled) return;
|
||||
await runSignalsForBars(bars, tf, strategy, strategyId);
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
setSignals([]);
|
||||
setError(e instanceof Error ? e.message : '차트 분석 실패');
|
||||
setRunning(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
++backtestGenRef.current;
|
||||
};
|
||||
}, [market, strategyId, strategy, toTimeSec, runSignalsForBars]);
|
||||
|
||||
const handleHistoryBarsLoaded = useCallback((bars: OHLCVBar[]) => {
|
||||
const strat = strategyRef.current;
|
||||
const sid = strategyIdRef.current;
|
||||
if (!strat || !sid) return;
|
||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strat));
|
||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||
historyDebounceRef.current = setTimeout(() => {
|
||||
historyDebounceRef.current = null;
|
||||
void runSignalsForBars(bars, tf, strat, sid);
|
||||
}, HISTORY_BACKTEST_DEBOUNCE_MS);
|
||||
}, [runSignalsForBars]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (historyDebounceRef.current) clearTimeout(historyDebounceRef.current);
|
||||
}, []);
|
||||
|
||||
if (!strategyId) {
|
||||
return (
|
||||
<div className="ptd-analysis-chart ptd-analysis-chart--empty">
|
||||
<p className="ptd-muted">
|
||||
가상매매 투자대상 목록에서 종목별 전략 또는 기본 전략을 설정하면 차트에 매매 시그널이 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statusBusy = running || historyLoading;
|
||||
|
||||
return (
|
||||
<div className="ptd-analysis-chart">
|
||||
<div className="ptd-main-pane">
|
||||
<PaperMiniChart
|
||||
market={market}
|
||||
theme={theme}
|
||||
timeframe={timeframe}
|
||||
onTimeframeChange={setTimeframe}
|
||||
/>
|
||||
</div>
|
||||
<div className="ptd-sub-panes">
|
||||
<PaperIndicatorPanel market={market} timeframe={timeframe} stacked />
|
||||
</div>
|
||||
<div className="ptd-analysis-chart ptd-analysis-chart--backtest">
|
||||
{statusBusy && (
|
||||
<div className="ptd-analysis-chart-status">
|
||||
{historyLoading && !running ? '과거 캔들 로드 중…' : '전략 시그널 계산 중…'}
|
||||
</div>
|
||||
)}
|
||||
{error && !statusBusy && (
|
||||
<div className="ptd-analysis-chart-status ptd-analysis-chart-status--err">{error}</div>
|
||||
)}
|
||||
<BacktestAnalysisChart
|
||||
symbol={market}
|
||||
timeframe={timeframe}
|
||||
toTimeSec={toTimeSec}
|
||||
barCount={BAR_COUNT}
|
||||
signals={signals}
|
||||
strategyId={strategyId}
|
||||
theme={theme}
|
||||
onHistoryBarsLoaded={handleHistoryBarsLoaded}
|
||||
onHistoryLoadingChange={setHistoryLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user