Files
goldenChart/frontend/src/components/analysisReport/AnalysisReportPage.tsx
T
2026-06-08 10:08:39 +09:00

192 lines
6.5 KiB
TypeScript

/**
* 분석레포트 — 가상매매와 동일 3패널 (좌: 목록 · 중: 분석 · 우: 시그널)
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Theme } from '../../types';
import {
loadBacktestResults,
loadPaperTrades,
loadPaperSummary,
loadStrategies,
type BacktestResultRecord,
type StrategyDto,
} from '../../utils/backendApi';
import {
buildContextFromBacktest,
buildContextFromLive,
} from '../../utils/analysisReportContext';
import { buildLiveExecutionItems, type LiveExecutionItem } from '../../utils/liveExecutionGroups';
import { PAPER_TRADES_CHANGED_EVENT } from '../../utils/paperTradeEvents';
import BacktestExecutionList, { type ExecutionListTab } from '../backtest/BacktestExecutionList';
import BuilderPageShell from '../layout/BuilderPageShell';
import AnalysisReportCenterPanel from './AnalysisReportCenterPanel';
import AnalysisReportSignalsPanel from './AnalysisReportSignalsPanel';
import AnalysisReportContextBar from './AnalysisReportContextBar';
import BacktestAnalysisReportModal from '../backtest/BacktestAnalysisReportModal';
import {
buildBacktestReportModel,
buildLiveReportModel,
} from '../../utils/backtestReportModel';
import '../../styles/backtestDashboard.css';
import '../../styles/analysisReportPage.css';
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 [sourceTab, setSourceTab] = useState<ExecutionListTab>('backtest');
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
const [liveItems, setLiveItems] = useState<LiveExecutionItem[]>([]);
const [selectedBacktest, setSelectedBacktest] = useState<BacktestResultRecord | null>(null);
const [selectedLive, setSelectedLive] = useState<LiveExecutionItem | null>(null);
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const [loading, setLoading] = useState(true);
const [reportOpen, setReportOpen] = useState(false);
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 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 ?? []);
}
return null;
}, [sourceTab, selectedBacktest, selectedLive, ctx, strategyNamesById]);
const headerActions = (
<>
<button type="button" className="btd-btn btd-btn--ghost" onClick={() => void fetchAll()}>새로고침</button>
<button
type="button"
className="btd-btn btd-btn--gold"
disabled={!reportModel}
onClick={() => setReportOpen(true)}
>
인쇄 / PDF
</button>
</>
);
return (
<>
<BuilderPageShell
theme={theme}
title="분석레포트"
subtitle="Golden Analysis Command Center"
pageClassName="bps-page--arp"
loading={loading}
loadingText="분석레포트 로딩…"
collapsiblePanels
leftStorageKey="arp-left-width"
leftDefaultWidth={380}
leftCollapsedStorageKey="arp-left-open"
rightStorageKey="arp-right-width"
rightDefaultWidth={320}
rightCollapsedStorageKey="arp-right-open"
leftTitle="실행 목록"
headerActions={headerActions}
banner={<AnalysisReportContextBar model={reportModel} />}
left={(
<BacktestExecutionList
tab={sourceTab}
onTabChange={setSourceTab}
backtestRecords={records}
liveItems={liveItems}
selectedBacktestId={selectedBacktest?.id ?? null}
selectedLiveId={selectedLive?.id ?? null}
onSelectBacktest={r => {
setSelectedBacktest(r);
setSourceTab('backtest');
}}
onSelectLive={item => {
setSelectedLive(item);
setSourceTab('live');
}}
/>
)}
center={<AnalysisReportCenterPanel model={reportModel} />}
right={<AnalysisReportSignalsPanel model={reportModel} />}
/>
<BacktestAnalysisReportModal
open={reportOpen}
onClose={() => setReportOpen(false)}
model={reportModel}
/>
</>
);
}
export default AnalysisReportPage;