백테스팅 적용

This commit is contained in:
Macbook
2026-05-25 20:23:54 +09:00
parent 02d14e4b2b
commit cae1c3a624
33 changed files with 3399 additions and 385 deletions
@@ -0,0 +1,213 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import TradingChart from '../TradingChart';
import type { BacktestSignal, StrategyDto } from '../../utils/backendApi';
import { loadStrategy } from '../../utils/backendApi';
import type { ChartType, IndicatorConfig, OHLCVBar, Theme, Timeframe } from '../../types';
import { loadAnalysisCandles } from '../../utils/analysisChartData';
import type { ChartManager } from '../../utils/ChartManager';
import { useHistoryLoader } from '../../hooks/useHistoryLoader';
import { isUpbitMarket } from '../../utils/upbitApi';
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
import { normalizeChartTimeframe, timeframeBarSeconds } from '../../utils/backtestUiUtils';
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
import { enrichIndicatorConfig, getIndicatorDef } from '../../utils/indicatorRegistry';
import { normalizeSmaConfig } from '../../utils/smaConfig';
import {
buildVirtualTradingChartIndicators,
resolveStrategyPrimaryTimeframe,
} from '../../utils/strategyToChartIndicators';
import BacktestOscillatorPanes from './BacktestOscillatorPanes';
interface Props {
symbol: string;
timeframe: string;
toTimeSec: number;
fromTimeSec?: number;
barCount: number;
signals: BacktestSignal[];
strategyId?: number | null;
theme?: Theme;
}
const noop = () => {};
let _indSeq = 0;
function newIndId() {
_indSeq += 1;
return `btd_${_indSeq}_${Date.now()}`;
}
function defaultOverlayIndicators(
getParams: (t: string, d?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
): IndicatorConfig[] {
const def = getIndicatorDef('SMA');
if (!def) return [];
const params: Record<string, number | string | boolean> = { ...def.defaultParams, ...getParams('SMA', def.defaultParams) };
for (let i = 1; i <= 11; i++) {
params[`length${i}`] = i === 1 ? 14 : 0;
params[`visible${i}`] = i === 1;
}
const cfg = normalizeSmaConfig(enrichIndicatorConfig({
id: newIndId(),
type: 'SMA',
params,
plots: def.plots,
hlines: def.hlines ?? [],
timeframeVisibility: { '1m': true, '3m': true, '5m': true, '10m': true, '15m': true, '30m': true, '1h': true, '4h': true, '1D': true, '1W': true, '1M': true },
}));
return [cfg];
}
const BacktestAnalysisChart: React.FC<Props> = ({
symbol,
timeframe: timeframeRaw,
toTimeSec,
fromTimeSec,
barCount,
signals,
strategyId,
theme = 'dark',
}) => {
const timeframe = normalizeChartTimeframe(timeframeRaw);
const { getParams, getVisualConfig } = useIndicatorSettings();
const [bars, setBars] = useState<OHLCVBar[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [chartType] = useState<ChartType>('candlestick');
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
const managerRef = useRef<ChartManager | null>(null);
const signalsRef = useRef(signals);
signalsRef.current = signals;
const market = symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
useEffect(() => {
if (!strategyId) {
setStrategy(undefined);
return;
}
let cancelled = false;
void loadStrategy(strategyId).then(s => {
if (!cancelled) setStrategy(s ?? undefined);
});
return () => { cancelled = true; };
}, [strategyId]);
const indicators = useMemo(() => {
if (strategy) {
const inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
if (inds.length) return inds.filter(i => i.timeframeVisibility?.[timeframe] !== false);
}
return defaultOverlayIndicators(getParams);
}, [strategy, getParams, getVisualConfig, timeframe]);
const effectiveBarCount = useMemo(() => {
const barSec = timeframeBarSeconds(timeframeRaw);
if (fromTimeSec && toTimeSec > fromTimeSec) {
return Math.max(barCount, Math.min(500, Math.ceil((toTimeSec - fromTimeSec) / barSec) + 50));
}
return barCount;
}, [barCount, fromTimeSec, toTimeSec, timeframeRaw]);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
void loadAnalysisCandles(market, timeframe, toTimeSec, effectiveBarCount)
.then(data => {
if (cancelled) return;
setBars(data);
})
.catch(e => {
if (cancelled) return;
setError(e instanceof Error ? e.message : '차트 로드 실패');
setBars([]);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, [market, timeframe, toTimeSec, effectiveBarCount]);
const applyMarkers = useCallback((attempt = 0) => {
const mgr = managerRef.current;
if (!mgr?.hasMainSeries()) {
if (attempt < 80) requestAnimationFrame(() => applyMarkers(attempt + 1));
return;
}
mgr.setBacktestMarkers(signalsRef.current, true);
}, []);
const onManagerReady = useCallback((mgr: ChartManager) => {
managerRef.current = mgr;
applyMarkers();
}, [applyMarkers]);
const onCandlesReady = useCallback(() => {
applyMarkers();
}, [applyMarkers]);
useEffect(() => {
applyMarkers();
}, [signals, bars, applyMarkers]);
useHistoryLoader({
symbol: market,
timeframe,
isUpbit: isUpbitMarket(market),
managerRef,
});
const symLabel = market.replace(/^KRW-/, '');
const stratTf = strategy ? resolveStrategyPrimaryTimeframe(strategy) : timeframe;
return (
<div className="btd-analysis-chart">
<div className="btd-analysis-toolbar">
<div className="btd-analysis-toolbar-left">
<span className="btd-analysis-select btd-analysis-select--static">{symLabel}</span>
<span className="btd-analysis-select btd-analysis-select--static">{timeframe}</span>
<span className="btd-analysis-select btd-analysis-select--static">Candle</span>
</div>
<div className="btd-analysis-toolbar-right">
<span className="btd-analysis-tool" title="자석 모드"></span>
<span className="btd-analysis-tool" title="드로잉"></span>
<span className="btd-analysis-tool" title="삭제"></span>
<span className="btd-analysis-count">{effectiveBarCount.toLocaleString()}</span>
</div>
</div>
<div className="btd-analysis-main">
<div className="btd-analysis-canvas-wrap">
{loading && <div className="btd-analysis-loading"> </div>}
{error && !loading && <div className="btd-analysis-error">{error}</div>}
{!loading && !error && bars.length > 0 && (
<TradingChart
key={`${market}-${timeframe}-${toTimeSec}-${effectiveBarCount}`}
bars={bars}
barsMarket={market}
market={market}
timeframe={stratTf}
chartType={chartType}
theme={theme}
mode="chart"
indicators={indicators}
drawingTool="cursor"
drawings={[]}
logScale={false}
onCrosshair={noop}
onManagerReady={onManagerReady}
onCandlesReady={onCandlesReady}
onAddDrawing={noop}
displayTimezone={DEFAULT_DISPLAY_TIMEZONE}
/>
)}
{!loading && !error && bars.length === 0 && (
<div className="btd-analysis-error"> .</div>
)}
</div>
{!loading && bars.length > 0 && <BacktestOscillatorPanes bars={bars} />}
</div>
</div>
);
};
export default BacktestAnalysisChart;