/** * 분석레포트 — 좌: 백테스팅/실시간 목록(백테스팅 화면과 동일) · 우: PDF 10 Variations 탭 */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { Theme } from '../../types'; import { loadBacktestResults, loadPaperTrades, loadPaperSummary, loadStrategies, type BacktestResultRecord, type StrategyDto, } from '../../utils/backendApi'; import type { AnalysisReportTypeId } from '../../utils/analysisReportTypes'; import { ANALYSIS_REPORT_TYPES } from '../../utils/analysisReportTypes'; import { buildContextFromBacktest, buildContextFromLive, } from '../../utils/analysisReportContext'; import { buildLiveExecutionItems, type LiveExecutionItem } from '../../utils/liveExecutionGroups'; import { PAPER_TRADES_CHANGED_EVENT } from '../../utils/paperTradeEvents'; import { readStoredSize, storeSize, usePanelResize, } from '../strategyEditor/usePanelResize'; import BacktestExecutionList, { type ExecutionListTab } from '../backtest/BacktestExecutionList'; import AnalysisReportViews from './AnalysisReportViews'; import BacktestAnalysisReportModal from '../backtest/BacktestAnalysisReportModal'; import { buildBacktestReportModel, buildLiveReportModel, } from '../../utils/backtestReportModel'; import '../../styles/strategyEditorTheme.css'; import '../../styles/backtestDashboard.css'; import '../../styles/analysisReportPage.css'; const LEFT_KEY = 'arp-left-width'; const LEFT_DEFAULT = 380; interface Props { theme?: Theme; } function pickCompareBacktest( selected: BacktestResultRecord, records: BacktestResultRecord[], ): BacktestResultRecord | null { const others = records.filter(r => r.id !== selected.id); const sameSymbol = others.filter(r => r.symbol === selected.symbol); return sameSymbol[0] ?? others[0] ?? null; } export function AnalysisReportPage({ theme = 'dark' }: Props) { const [reportTypeId, setReportTypeId] = useState('standard'); const [sourceTab, setSourceTab] = useState('backtest'); const [records, setRecords] = useState([]); const [liveItems, setLiveItems] = useState([]); const [selectedBacktest, setSelectedBacktest] = useState(null); const [selectedLive, setSelectedLive] = useState(null); const [strategies, setStrategies] = useState([]); const [loading, setLoading] = useState(true); const [reportOpen, setReportOpen] = useState(false); const [leftWidth, setLeftWidth] = useState(() => Math.max(readStoredSize(LEFT_KEY, LEFT_DEFAULT), 300)); const leftRef = useRef(leftWidth); leftRef.current = leftWidth; const onLeftSplit = usePanelResize('vertical', setLeftWidth, () => leftRef.current, 300, 520, v => storeSize(LEFT_KEY, v)); const refreshLive = useCallback(async () => { const [trades, summary, stratList] = await Promise.all([ loadPaperTrades(), loadPaperSummary(), loadStrategies(), ]); setStrategies(stratList); const strategyNames = Object.fromEntries(stratList.map(s => [s.id, s.name])); const live = buildLiveExecutionItems(trades, summary, strategyNames); setLiveItems(live); setSelectedLive(prev => (prev && live.some(x => x.id === prev.id) ? prev : live[0] ?? null)); }, []); const fetchAll = useCallback(async () => { setLoading(true); try { const [recs, stratList] = await Promise.all([ loadBacktestResults(), loadStrategies(), ]); setRecords(recs); setStrategies(stratList); setSelectedBacktest(prev => (prev && recs.some(r => r.id === prev.id) ? prev : recs[0] ?? null)); await refreshLive(); } finally { setLoading(false); } }, [refreshLive]); useEffect(() => { void fetchAll(); }, [fetchAll]); useEffect(() => { const onPaper = () => { void refreshLive(); }; window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onPaper); return () => window.removeEventListener(PAPER_TRADES_CHANGED_EVENT, onPaper); }, [refreshLive]); const compareBacktest = useMemo(() => { if (!selectedBacktest) return null; return pickCompareBacktest(selectedBacktest, records); }, [records, selectedBacktest]); const compareLive = useMemo(() => { if (!selectedLive) return null; return liveItems.find(x => x.id !== selectedLive.id) ?? null; }, [liveItems, selectedLive]); const ctx = useMemo(() => { if (sourceTab === 'backtest' && selectedBacktest) { return buildContextFromBacktest(selectedBacktest, compareBacktest); } if (sourceTab === 'live' && selectedLive) { return buildContextFromLive(selectedLive); } return null; }, [sourceTab, selectedBacktest, selectedLive, compareBacktest]); const strategyNamesById = useMemo( () => Object.fromEntries(strategies.map(s => [s.id, s.name])), [strategies], ); const reportModel = useMemo(() => { if (sourceTab === 'backtest' && selectedBacktest && ctx?.analysis) { return buildBacktestReportModel( selectedBacktest, ctx.analysis, ctx.signals, strategyNamesById, ); } if (sourceTab === 'live' && selectedLive) { return buildLiveReportModel(selectedLive, ctx?.signals ?? [], '1h'); } return null; }, [sourceTab, selectedBacktest, selectedLive, ctx, strategyNamesById]); if (loading) { return (

분석레포트

데이터 로딩…
); } return (

분석레포트

TradingReport UI Design · 10 Variations
{ANALYSIS_REPORT_TYPES.map(t => ( ))}
setReportOpen(false)} model={reportModel} />
); } export default AnalysisReportPage;