전략편집기 평가탭 전략평가후 수익률 표시

This commit is contained in:
Macbook
2026-06-25 01:42:12 +09:00
parent a2816b9163
commit 2ae79ff71d
3 changed files with 134 additions and 4 deletions
+107 -2
View File
@@ -5,7 +5,7 @@ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { ReactFlowProvider } from '@xyflow/react';
import DraggableModalFrame from './DraggableModalFrame';
import type { Theme } from '../types/index';
import { hasRegisteredUser, loadStrategies, loadStrategy, saveStrategy, deleteStrategy, runBacktest, loadBacktestSettings, loadIndicatorSettings, type StrategyDto as ApiStrategyDto } from '../utils/backendApi';
import { hasRegisteredUser, loadStrategies, loadStrategy, saveStrategy, deleteStrategy, runBacktest, loadBacktestSettings, loadIndicatorSettings, type BacktestSignal, type StrategyDto as ApiStrategyDto } from '../utils/backendApi';
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
import { syncStrategyTimeframesFromLayoutIfNeeded } from '../utils/strategyTimeframeSync';
@@ -168,7 +168,7 @@ import StrategyDescriptionModal from './strategyEditor/StrategyDescriptionModal'
import ReactDOM from 'react-dom';
import { MarketSearchPanel } from './MarketSearchPanel';
import { getKoreanName } from '../utils/marketNameCache';
import type { Timeframe } from '../types';
import type { Timeframe, OHLCVBar } from '../types';
import {
BACKTEST_STRATEGY_TIMEFRAME,
buildQuickRunTimeframeSelectOptions,
@@ -178,6 +178,14 @@ import {
} from '../utils/backtestRunTimeframe';
import '../styles/strategyEditor.css';
import '../styles/strategyEditorTheme.css';
import '../styles/strategyEvaluation.css';
import { BACKTEST_DISPLAY_BAR_COUNT, resolveEvaluationBarSlice } from '../utils/backtestWarmup';
import {
buildStrategyEvaluationToolbarSummary,
formatEvaluationReturnPct,
} from '../utils/strategyEvaluationReport';
import { resolveEvaluationCommissionRate } from '../utils/strategyEvaluationSignals';
import type { StrategyEvaluationWindowMeta } from './strategyEvaluation/StrategyEvaluationChart';
type SignalTab = 'buy' | 'sell' | 'eval';
type BuySellTab = 'buy' | 'sell';
@@ -359,6 +367,20 @@ export default function StrategyEditorPage({
const [qbMarketQ, setQbMarketQ] = useState('');
const qbMarketBtnRef = useRef<HTMLButtonElement>(null);
// ── 평가 탭 — 수익률 요약 ─────────────────────────────────────────────────
const [evalBacktestSignals, setEvalBacktestSignals] = useState<BacktestSignal[]>([]);
const [evalBars, setEvalBars] = useState<OHLCVBar[]>([]);
const [evalWindowMeta, setEvalWindowMeta] = useState<StrategyEvaluationWindowMeta>({
evaluationBarCount: BACKTEST_DISPLAY_BAR_COUNT,
warmupBarCount: 0,
isAtLatestWindow: true,
windowEndTimeSec: null,
});
const [evalSignalScanRunning, setEvalSignalScanRunning] = useState(false);
const [evalMarket, setEvalMarket] = useState('KRW-BTC');
const [evalInitialCapital, setEvalInitialCapital] = useState(10_000_000);
const [evalCommissionRate, setEvalCommissionRate] = useState(0.0015);
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
const [rightWidth, setRightWidth] = useState(() => readRightPanelWidth());
const [rightOpen, setRightOpen] = useState(() => readStoredBool('se-right-open', true));
@@ -1690,6 +1712,54 @@ export default function StrategyEditorPage({
[qbStrategyTimeframe],
);
useEffect(() => {
void loadBacktestSettings().then(s => {
if (s?.initialCapital) setEvalInitialCapital(s.initialCapital);
setEvalCommissionRate(resolveEvaluationCommissionRate(s));
});
}, []);
useEffect(() => {
if (signalTab !== 'eval') {
setEvalBacktestSignals([]);
setEvalBars([]);
setEvalSignalScanRunning(false);
}
}, [signalTab]);
const evalAnalysisSummary = useMemo(() => {
if (signalTab !== 'eval' || evalBacktestSignals.length === 0) return null;
const { evalBars: slice } = resolveEvaluationBarSlice(evalBars, evalWindowMeta.evaluationBarCount);
const lastEvalBar = slice[slice.length - 1];
return buildStrategyEvaluationToolbarSummary(evalBacktestSignals, {
initialCapital: evalInitialCapital,
symbol: evalMarket.replace(/^KRW-/, ''),
commissionRate: evalCommissionRate,
lastBarClose: lastEvalBar?.close,
lastBarTimeSec: lastEvalBar?.time,
});
}, [
signalTab,
evalBacktestSignals,
evalBars,
evalWindowMeta.evaluationBarCount,
evalInitialCapital,
evalMarket,
evalCommissionRate,
]);
const handleEvalSignalsChange = useCallback((signals: BacktestSignal[]) => {
setEvalBacktestSignals(signals);
}, []);
const handleEvalBarsLoaded = useCallback((bars: OHLCVBar[]) => {
setEvalBars(bars);
}, []);
const handleEvalWindowMetaChange = useCallback((meta: StrategyEvaluationWindowMeta) => {
setEvalWindowMeta(meta);
}, []);
const operators = [
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
{ type: 'operator' as const, value: 'OR', label: 'OR', color: 'logic-or' },
@@ -1982,6 +2052,36 @@ export default function StrategyEditorPage({
</button>
</div>
{signalTab === 'eval' && (
evalSignalScanRunning ? (
<span className="se-eval-return-summary seval-header-analysis-busy"> </span>
) : evalAnalysisSummary ? (
<span
className="se-eval-return-summary seval-analysis-summary"
title="일괄매수(최초 전액) · 분할매수(40/30/잔여) · 전략평가와 동일 기준"
>
<span className={`seval-analysis-item seval-analysis-return${
evalAnalysisSummary.fullBuyReturnPct > 0
? ' seval-analysis-return--pos'
: evalAnalysisSummary.fullBuyReturnPct < 0
? ' seval-analysis-return--neg'
: ''
}`}>
:<strong>{formatEvaluationReturnPct(evalAnalysisSummary.fullBuyReturnPct)}</strong>
</span>
<span className="seval-analysis-sep" aria-hidden>·</span>
<span className={`seval-analysis-item seval-analysis-return${
evalAnalysisSummary.splitBuyReturnPct > 0
? ' seval-analysis-return--pos'
: evalAnalysisSummary.splitBuyReturnPct < 0
? ' seval-analysis-return--neg'
: ''
}`}>
:<strong>{formatEvaluationReturnPct(evalAnalysisSummary.splitBuyReturnPct)}</strong>
</span>
</span>
) : null
)}
{stratName && <span className="se-editing-name">{stratName}</span>}
{signalTab !== 'eval' && hasMultipleStartSections(currentEditorState) && (
<StartCombineOpControl
@@ -2044,6 +2144,11 @@ export default function StrategyEditorPage({
stratDesc={stratDesc}
buyEditorState={buyEditorState}
sellEditorState={sellEditorState}
onSignalsChange={handleEvalSignalsChange}
onBarsLoaded={handleEvalBarsLoaded}
onEvalWindowMetaChange={handleEvalWindowMetaChange}
onBacktestRunningChange={setEvalSignalScanRunning}
onMarketChange={setEvalMarket}
/>
) : editorMode === 'graph' ? (
<>