실시간 차트 백테스팅 결과 인쇄 기능 적용

This commit is contained in:
Macbook
2026-06-03 12:19:03 +09:00
parent ee7b1a6123
commit ba592b0789
2 changed files with 94 additions and 14 deletions
+37 -12
View File
@@ -111,7 +111,9 @@ import {
import { NotifyTopMenuBar, ChartToolbarNotify } from './components/NotifyUiBindings'; import { NotifyTopMenuBar, ChartToolbarNotify } from './components/NotifyUiBindings';
import { AppNotificationLayer } from './components/AppNotificationLayer'; import { AppNotificationLayer } from './components/AppNotificationLayer';
import BacktestSettingsModal from './components/BacktestSettingsModal'; import BacktestSettingsModal from './components/BacktestSettingsModal';
import { BacktestResultModal } from './components/BacktestResultModal'; import BacktestAnalysisReportModal from './components/backtest/BacktestAnalysisReportModal';
import { buildChartBacktestReportModel } from './utils/backtestReportModel';
import { fmtListTimestamp } from './utils/backtestUiUtils';
const DashboardPage = lazy(() => import('./components/DashboardPage')); const DashboardPage = lazy(() => import('./components/DashboardPage'));
const StrategyPage = lazy(() => import('./components/StrategyPage')); const StrategyPage = lazy(() => import('./components/StrategyPage'));
@@ -760,6 +762,7 @@ function App() {
const [showBtSettings, setShowBtSettings] = useState(false); const [showBtSettings, setShowBtSettings] = useState(false);
const [showBtResult, setShowBtResult] = useState(false); const [showBtResult, setShowBtResult] = useState(false);
const [btStrategyName, setBtStrategyName] = useState('전략'); const [btStrategyName, setBtStrategyName] = useState('전략');
const [btReportCreatedAt, setBtReportCreatedAt] = useState<string | undefined>();
/** 현재 차트와 일치하는 백테스팅 결과만 UI·마커에 반영 */ /** 현재 차트와 일치하는 백테스팅 결과만 UI·마커에 반영 */
const btMatchesChart = useMemo( const btMatchesChart = useMemo(
@@ -1228,6 +1231,34 @@ function App() {
const barsMarket = useUpbit ? upbitBarsMarket : symbol; const barsMarket = useUpbit ? upbitBarsMarket : symbol;
barsMarketRef.current = barsMarket; barsMarketRef.current = barsMarket;
const chartBtReportModel = useMemo(() => {
if (!btMatchesChart) return null;
const reportSymbol = btSymbol ?? symbol;
const reportTf = btTimeframe ?? timeframe;
return buildChartBacktestReportModel({
analysis: btAnalysis,
stats: btStats,
signals: btSignals,
strategyName: btStrategyName,
symbol: reportSymbol,
timeframe: reportTf,
barCount: bars.length,
createdAt: btReportCreatedAt,
});
}, [
btMatchesChart,
btAnalysis,
btStats,
btSignals,
btStrategyName,
btSymbol,
btTimeframe,
symbol,
timeframe,
bars.length,
btReportCreatedAt,
]);
/** 종목·타임프레임 전환 시 이전 틱 버퍼·실시간 허용 플래그 초기화 (paint 전에 실행) */ /** 종목·타임프레임 전환 시 이전 틱 버퍼·실시간 허용 플래그 초기화 (paint 전에 실행) */
useLayoutEffect(() => { useLayoutEffect(() => {
chartLiveReadyRef.current = false; chartLiveReadyRef.current = false;
@@ -2315,6 +2346,7 @@ function App() {
}; };
btMarkerPayloadRef.current = payload; btMarkerPayloadRef.current = payload;
applyBacktestMarkersWhenReady(payload); applyBacktestMarkersWhenReady(payload);
setBtReportCreatedAt(fmtListTimestamp(new Date().toISOString()));
} }
if (appDefaults.btAutoPopup) setShowBtResult(true); if (appDefaults.btAutoPopup) setShowBtResult(true);
} }
@@ -2677,19 +2709,12 @@ function App() {
/> />
)} )}
{/* 백테스팅 결과 모달 */} {/* 백테스팅 결과 — 투자분석 상세 레포트 (인쇄·PDF) */}
{showBtResult && ( <BacktestAnalysisReportModal
<BacktestResultModal open={showBtResult && chartBtReportModel != null}
analysis={btAnalysis ?? null}
stats={btStats ?? null}
signals={btSignals}
strategyName={btStrategyName}
symbol={symbol}
timeframe={timeframe}
barCount={bars.length}
onClose={() => setShowBtResult(false)} onClose={() => setShowBtResult(false)}
model={chartBtReportModel}
/> />
)}
{/* 시리즈 클릭/호버 컨텍스트 툴바 */} {/* 시리즈 클릭/호버 컨텍스트 툴바 */}
{ctxToolbar && (() => { {ctxToolbar && (() => {
+55
View File
@@ -2,6 +2,7 @@ import type {
BacktestAnalysis, BacktestAnalysis,
BacktestResultRecord, BacktestResultRecord,
BacktestSignal, BacktestSignal,
BacktestStats,
} from './backendApi'; } from './backendApi';
import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal'; import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal';
import type { LiveExecutionItem } from './liveExecutionGroups'; import type { LiveExecutionItem } from './liveExecutionGroups';
@@ -42,6 +43,60 @@ export function buildAnalysisFromLive(
}; };
} }
function buildAnalysisFromStats(s: BacktestStats): BacktestAnalysis {
return {
initialCapital: 10_000_000,
finalEquity: s.finalEquity ?? 0,
totalReturnPct: s.totalReturn ?? 0,
totalProfitLoss: (s.finalEquity ?? 0) - 10_000_000,
grossProfit: 0,
grossLoss: 0,
avgReturnPct: s.avgReturn ?? 0,
profitLossRatio: 0,
numberOfPositions: s.totalTrades ?? 0,
numberOfWinning: s.winTrades ?? 0,
numberOfLosing: (s.totalTrades ?? 0) - (s.winTrades ?? 0),
numberOfBreakEven: 0,
winRate: s.winRate ?? 0,
maxDrawdownPct: s.maxDrawdown ?? 0,
maxRunupPct: 0,
sharpeRatio: 0,
sortinoRatio: 0,
calmarRatio: 0,
valueAtRisk95: 0,
expectedShortfall: 0,
buyAndHoldReturnPct: 0,
vsBuyAndHold: 0,
};
}
/** 실시간 차트에서 실행한 백테스트 → 투자분석 상세 레포트 모델 */
export function buildChartBacktestReportModel(opts: {
analysis: BacktestAnalysis | null;
stats: BacktestStats | null;
signals: BacktestSignal[];
strategyName: string;
symbol: string;
timeframe: string;
barCount: number;
createdAt?: string;
}): BacktestAnalysisReportModel | null {
const analysis = opts.analysis ?? (opts.stats ? buildAnalysisFromStats(opts.stats) : null);
if (!analysis) return null;
const market = toUpbitMarket(opts.symbol);
return {
analysis,
signals: opts.signals,
strategyName: repairUtf8Mojibake(opts.strategyName || '전략'),
symbol: opts.symbol,
symbolKo: repairUtf8Mojibake(getKoreanName(market)),
timeframe: opts.timeframe,
barCount: opts.barCount || 300,
createdAt: opts.createdAt,
reportKind: 'backtest',
};
}
export function buildBacktestReportModel( export function buildBacktestReportModel(
record: BacktestResultRecord, record: BacktestResultRecord,
analysis: BacktestAnalysis, analysis: BacktestAnalysis,