From 7f29bbd309cfab0adeead2050006645bc3abb8d0 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 8 Jun 2026 12:19:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=EC=9D=B4=EB=A6=84,=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=ED=8F=89=EA=B7=A0=EC=84=A0=20=EA=B7=B8=EB=9E=98=ED=94=84=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/BacktestHistoryPage.tsx | 25 ++++++++++-- .../analysisReport/AnalysisReportPage.tsx | 32 +++++++++++---- .../backtest/BacktestAnalysisChart.tsx | 39 +++++++------------ frontend/src/utils/indicatorPaneMerge.ts | 8 ++++ frontend/src/utils/liveExecutionGroups.ts | 13 ++++--- frontend/src/utils/strategyNameResolver.ts | 38 ++++++++++++++++++ .../src/utils/strategyToChartIndicators.ts | 11 ++++-- 7 files changed, 122 insertions(+), 44 deletions(-) create mode 100644 frontend/src/utils/strategyNameResolver.ts diff --git a/frontend/src/components/BacktestHistoryPage.tsx b/frontend/src/components/BacktestHistoryPage.tsx index b12ba51..19cc53a 100644 --- a/frontend/src/components/BacktestHistoryPage.tsx +++ b/frontend/src/components/BacktestHistoryPage.tsx @@ -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)); diff --git a/frontend/src/components/analysisReport/AnalysisReportPage.tsx b/frontend/src/components/analysisReport/AnalysisReportPage.tsx index 0be1027..e81576b 100644 --- a/frontend/src/components/analysisReport/AnalysisReportPage.tsx +++ b/frontend/src/components/analysisReport/AnalysisReportPage.tsx @@ -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; diff --git a/frontend/src/components/backtest/BacktestAnalysisChart.tsx b/frontend/src/components/backtest/BacktestAnalysisChart.tsx index d7a464e..20b9347 100644 --- a/frontend/src/components/backtest/BacktestAnalysisChart.tsx +++ b/frontend/src/components/backtest/BacktestAnalysisChart.tsx @@ -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 = ({ const [error, setError] = useState(null); const [chartType] = useState('candlestick'); const [strategy, setStrategy] = useState(); + const [strategyLoading, setStrategyLoading] = useState(false); /** 개별 지표 숨김 ID 집합 (실시간 차트와 동일한 per-indicator 숨김 지원) */ const [hiddenIndicatorIds, setHiddenIndicatorIds] = useState>(new Set()); const managerRef = useRef(null); @@ -162,11 +149,16 @@ const BacktestAnalysisChart: React.FC = ({ 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 = ({ 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 diff --git a/frontend/src/utils/indicatorPaneMerge.ts b/frontend/src/utils/indicatorPaneMerge.ts index c154aba..7b95c33 100644 --- a/frontend/src/utils/indicatorPaneMerge.ts +++ b/frontend/src/utils/indicatorPaneMerge.ts @@ -2,6 +2,7 @@ import type { IndicatorConfig } from '../types'; import { enrichIndicatorConfig, getIndicatorDef } from './indicatorRegistry'; import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from './smaConfig'; import { normalizeIchimokuConfig } from './ichimokuConfig'; +import { buildMainIndicatorConfig } from './indicatorMainConfig'; import type { PlotDef, HLineDef } from './indicatorRegistry'; import type { IchimokuCloudColors } from './ichimokuConfig'; @@ -195,6 +196,13 @@ export function buildChartIndicatorConfig( }; if (type === 'SMA') { + const mainSma = buildMainIndicatorConfig(type, [], getParams, getVisualConfig); + if (mainSma?.plotVisibility) { + cfg.plotVisibility = mainSma.plotVisibility; + } + if (mainSma?.plots?.length) { + cfg.plots = mainSma.plots; + } cfg = normalizeSmaConfig({ ...cfg, plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(), diff --git a/frontend/src/utils/liveExecutionGroups.ts b/frontend/src/utils/liveExecutionGroups.ts index 3c6cd33..4408340 100644 --- a/frontend/src/utils/liveExecutionGroups.ts +++ b/frontend/src/utils/liveExecutionGroups.ts @@ -32,12 +32,15 @@ function sourceLabel(source: string): string { } function resolveStrategyLabel( - t: PaperTradeDto, + trades: PaperTradeDto[], strategyNames: Record, ): string { - if (t.strategyName) return t.strategyName; - if (t.strategyId != null) { - return strategyNames[t.strategyId] ?? `전략 #${t.strategyId}`; + for (const t of trades) { + if (t.strategyName?.trim()) return t.strategyName.trim(); + } + const first = trades[0]; + if (first?.strategyId != null) { + return strategyNames[first.strategyId] ?? `전략 #${first.strategyId}`; } return '수동 매매'; } @@ -83,7 +86,7 @@ export function buildLiveExecutionItems( return { id: key, symbol: first.symbol, - strategyLabel: resolveStrategyLabel(first, strategyNames), + strategyLabel: resolveStrategyLabel(sorted, strategyNames), sourceLabel: sourceLabel(first.source), timeframe: first.candleType ?? 'unknown', executionType: first.executionType ?? undefined, diff --git a/frontend/src/utils/strategyNameResolver.ts b/frontend/src/utils/strategyNameResolver.ts new file mode 100644 index 0000000..d791b93 --- /dev/null +++ b/frontend/src/utils/strategyNameResolver.ts @@ -0,0 +1,38 @@ +/** + * 전략 ID → 이름 해석 (목록에 없거나 삭제된 전략 포함) + */ +import { loadStrategyForNotification, type StrategyDto } from './backendApi'; + +/** 전략 목록 + 누락 ID별 단건 조회로 이름 맵 보강 */ +export async function enrichStrategyNameMap( + base: Record, + ids: Iterable, +): Promise> { + const out = { ...base }; + const missing = [...new Set( + [...ids] + .map(id => (id != null && id > 0 ? Math.trunc(id) : null)) + .filter((id): id is number => id != null && !out[id]), + )]; + if (!missing.length) return out; + + await Promise.all(missing.map(async id => { + try { + const s = await loadStrategyForNotification(id); + if (s?.name?.trim()) out[id] = s.name.trim(); + } catch { + /* ignore */ + } + })); + return out; +} + +export function strategyNamesFromList(list: StrategyDto[]): Record { + const out: Record = {}; + for (const s of list) { + if (s.id != null && s.id > 0 && s.name?.trim()) { + out[s.id] = s.name.trim(); + } + } + return out; +} diff --git a/frontend/src/utils/strategyToChartIndicators.ts b/frontend/src/utils/strategyToChartIndicators.ts index f058f97..1cf8704 100644 --- a/frontend/src/utils/strategyToChartIndicators.ts +++ b/frontend/src/utils/strategyToChartIndicators.ts @@ -10,7 +10,8 @@ import { getHLineLabel, type HLineDef, } from './indicatorRegistry'; -import { createDefaultSmaPlotVisibility, normalizeSmaConfig, smaPeriodKey } from './smaConfig'; +import { buildMainIndicatorConfig } from './indicatorMainConfig'; +import { normalizeSmaConfig, smaPeriodKey } from './smaConfig'; import { normalizeIchimokuConfig } from './ichimokuConfig'; import { extractVirtualConditions } from './virtualStrategyConditions'; @@ -183,18 +184,20 @@ function buildOneIndicator( }; if (ref.registryType === 'SMA') { + const mainSma = buildMainIndicatorConfig('SMA', [], getParams, getVisual as Parameters[3]); const periods = [ref.period, ref.leftPeriod, ref.rightPeriod] .filter((p): p is number => typeof p === 'number' && p > 0); + const p = { ...cfg.params }; if (periods.length > 0) { - const p = { ...cfg.params }; periods.slice(0, 11).forEach((val, i) => { p[smaPeriodKey(i + 1)] = val; }); - cfg = { ...cfg, params: p }; } cfg = normalizeSmaConfig({ ...cfg, - plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(), + params: p, + plots: mainSma?.plots ?? cfg.plots, + plotVisibility: mainSma?.plotVisibility ?? cfg.plotVisibility, }); } else if (ref.period != null && ref.period > 0) { cfg = {