백테스트 전략이름, 이동평균선 그래프 오류 수정

This commit is contained in:
Macbook
2026-06-08 12:19:20 +09:00
parent 9fde6868e2
commit 7f29bbd309
7 changed files with 122 additions and 44 deletions
@@ -41,6 +41,7 @@ import {
import '../styles/strategyEditorTheme.css';
import { getKoreanName } from '../utils/marketNameCache';
import { repairUtf8Mojibake } from '../utils/textEncoding';
import { enrichStrategyNameMap, strategyNamesFromList } from '../utils/strategyNameResolver';
const LEFT_KEY = 'btd-left-width';
const RIGHT_KEY = 'btd-right-width';
@@ -114,7 +115,11 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
loadPaperSummary(),
loadStrategies(),
]);
const strategyNames = Object.fromEntries(strategies.map(s => [s.id, s.name]));
const baseNames = strategyNamesFromList(strategies);
const strategyNames = await enrichStrategyNameMap(
baseNames,
trades.map(t => t.strategyId),
);
const live = buildLiveExecutionItems(trades, summary, strategyNames);
setLiveItems(live);
setSelectedLive(prev => (prev && live.some(x => x.id === prev.id) ? prev : live[0] ?? null));
@@ -128,10 +133,24 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
loadPaperSummary(),
loadStrategies(),
]);
const strategyNames = Object.fromEntries(strategies.map(s => [s.id, s.name]));
const baseNames = strategyNamesFromList(strategies);
const strategyNames = await enrichStrategyNameMap(
baseNames,
[
...trades.map(t => t.strategyId),
...list.map(r => r.strategyId),
],
);
const live = buildLiveExecutionItems(trades, summary, strategyNames);
const enrichedRecords = list.map(r => ({
...r,
strategyName: r.strategyName?.trim()
|| (r.strategyId != null ? strategyNames[r.strategyId] : undefined)
|| r.strategyName
|| '전략 없음',
}));
setStrategies(strategies);
setRecords(list);
setRecords(enrichedRecords);
setLiveItems(live);
setSelectedBacktest(prev => (prev && list.some(x => x.id === prev.id) ? prev : list[0] ?? null));
setSelectedLive(prev => (prev && live.some(x => x.id === prev.id) ? prev : live[0] ?? null));
@@ -27,6 +27,7 @@ import {
buildBacktestReportModel,
buildLiveReportModel,
} from '../../utils/backtestReportModel';
import { enrichStrategyNameMap, strategyNamesFromList } from '../../utils/strategyNameResolver';
import '../../styles/backtestDashboard.css';
import '../../styles/analysisReportPage.css';
@@ -53,17 +54,25 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
const [loading, setLoading] = useState(true);
const [reportOpen, setReportOpen] = useState(false);
const refreshLive = useCallback(async () => {
const refreshLive = useCallback(async (backtestRecords: BacktestResultRecord[] = []) => {
const [trades, summary, stratList] = await Promise.all([
loadPaperTrades(),
loadPaperSummary(),
loadStrategies(),
]);
setStrategies(stratList);
const strategyNames = Object.fromEntries(stratList.map(s => [s.id, s.name]));
const baseNames = strategyNamesFromList(stratList);
const strategyNames = await enrichStrategyNameMap(
baseNames,
[
...trades.map(t => t.strategyId),
...backtestRecords.map(r => r.strategyId),
],
);
const live = buildLiveExecutionItems(trades, summary, strategyNames);
setLiveItems(live);
setSelectedLive(prev => (prev && live.some(x => x.id === prev.id) ? prev : live[0] ?? null));
return strategyNames;
}, []);
const fetchAll = useCallback(async () => {
@@ -73,10 +82,19 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
loadBacktestResults(),
loadStrategies(),
]);
setRecords(recs);
setStrategies(stratList);
setSelectedBacktest(prev => (prev && recs.some(r => r.id === prev.id) ? prev : recs[0] ?? null));
await refreshLive();
const strategyNames = await refreshLive(recs);
const enrichedRecords = recs.map(r => ({
...r,
strategyName: r.strategyName?.trim()
|| (r.strategyId != null ? strategyNames[r.strategyId] : undefined)
|| r.strategyName
|| '전략 없음',
}));
setRecords(enrichedRecords);
setSelectedBacktest(prev => (
prev && enrichedRecords.some(r => r.id === prev.id) ? prev : enrichedRecords[0] ?? null
));
} finally {
setLoading(false);
}
@@ -84,10 +102,10 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
useEffect(() => { void fetchAll(); }, [fetchAll]);
useEffect(() => {
const onPaper = () => { void refreshLive(); };
const onPaper = () => { void refreshLive(records); };
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onPaper);
return () => window.removeEventListener(PAPER_TRADES_CHANGED_EVENT, onPaper);
}, [refreshLive]);
}, [refreshLive, records]);
const compareBacktest = useMemo(() => {
if (!selectedBacktest) return null;
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from 'react';
import TradingChart from '../TradingChart';
import type { BacktestSignal, StrategyDto } from '../../utils/backendApi';
import { loadStrategy } from '../../utils/backendApi';
import { loadStrategyForNotification } from '../../utils/backendApi';
import type { ChartType, IndicatorConfig, OHLCVBar, Theme, Timeframe } from '../../types';
import { loadAnalysisCandles } from '../../utils/analysisChartData';
import type { ChartManager } from '../../utils/ChartManager';
@@ -22,12 +22,6 @@ import {
chartPaneFlexRatio,
countNonOverlayIndicatorPanes,
} from '../../utils/strategyOscillatorSeries';
import {
createDefaultSmaPlotVisibility,
normalizeSmaConfig,
smaPeriodKey,
smaPlotId,
} from '../../utils/smaConfig';
import { buildChartIndicatorConfig } from '../../utils/indicatorPaneMerge';
import {
applyPaperOverlayVisibility,
@@ -87,15 +81,7 @@ function defaultOverlayIndicators(
const cfg = buildChartIndicatorConfig('SMA', newIndId(), getParams, getVisualConfig, {
timeframeVisibility: ALL_TF_VISIBLE,
});
if (!cfg) return [];
// 전략 미선택 시 MA1(14)만 표시
const p = { ...cfg.params };
p[smaPeriodKey(1)] = 14;
const plotVisibility = createDefaultSmaPlotVisibility();
for (let i = 0; i < 11; i++) {
plotVisibility[smaPlotId(i)] = i === 0;
}
return [normalizeSmaConfig({ ...cfg, params: p, plotVisibility })];
return cfg ? [cfg] : [];
}
/** 전략 미선택 시 기본 오실레이터(RSI, MACD)를 TradingChart sub-pane 으로 추가 */
@@ -139,6 +125,7 @@ const BacktestAnalysisChart: React.FC<Props> = ({
const [error, setError] = useState<string | null>(null);
const [chartType] = useState<ChartType>('candlestick');
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
const [strategyLoading, setStrategyLoading] = useState(false);
/** 개별 지표 숨김 ID 집합 (실시간 차트와 동일한 per-indicator 숨김 지원) */
const [hiddenIndicatorIds, setHiddenIndicatorIds] = useState<ReadonlySet<string>>(new Set());
const managerRef = useRef<ChartManager | null>(null);
@@ -162,11 +149,16 @@ const BacktestAnalysisChart: React.FC<Props> = ({
useEffect(() => {
if (!strategyId) {
setStrategy(undefined);
setStrategyLoading(false);
return;
}
let cancelled = false;
void loadStrategy(strategyId).then(s => {
if (!cancelled) setStrategy(s ?? undefined);
setStrategyLoading(true);
void loadStrategyForNotification(strategyId).then(s => {
if (!cancelled) {
setStrategy(s ?? undefined);
setStrategyLoading(false);
}
});
return () => { cancelled = true; };
}, [strategyId]);
@@ -181,27 +173,24 @@ const BacktestAnalysisChart: React.FC<Props> = ({
const showOscillatorPanel = !overlayVisibility;
const baseIndicators = useMemo(() => {
if (strategyId && strategyLoading) return [];
let inds: IndicatorConfig[];
if (strategy) {
inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
inds = inds.filter(i => i.timeframeVisibility?.[chartTimeframe] !== false);
// 전략 지표가 모두 캔들 오버레이(SMA/EMA/일목 등)인 경우 기본 오실레이터(RSI·MACD)를 추가.
// 기존 SVG 기반 StrategyOscillatorPanes 의 fallback 동작과 동일.
if (showOscillatorPanel && inds.every(i => isOverlayType(i.type))) {
inds = [...inds, ...defaultOscillatorIndicators(getParams, getVisualConfig)];
}
} else {
inds = defaultOverlayIndicators(getParams, getVisualConfig);
// 전략 미선택 시 기본 오실레이터(RSI, MACD)도 TradingChart sub-pane 으로 추가
if (showOscillatorPanel) {
inds = [...inds, ...defaultOscillatorIndicators(getParams, getVisualConfig)];
}
}
if (overlayVisibility) {
inds = ensurePaperChartOverlays(inds, getParams, getVisualConfig);
}
inds = ensurePaperChartOverlays(inds, getParams, getVisualConfig);
return inds;
}, [strategy, getParams, getVisualConfig, chartTimeframe, overlayVisibility, showOscillatorPanel]);
}, [strategy, strategyId, strategyLoading, getParams, getVisualConfig, chartTimeframe, overlayVisibility, showOscillatorPanel]);
const indicators = useMemo(() => {
let inds = overlayVisibility