From 2ae79ff71d6218c35e4a52007d8a7befe0f4b7ea Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 25 Jun 2026 01:42:12 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0?= =?UTF-8?q?=20=ED=8F=89=EA=B0=80=ED=83=AD=20=EC=A0=84=EB=9E=B5=ED=8F=89?= =?UTF-8?q?=EA=B0=80=ED=9B=84=20=EC=88=98=EC=9D=B5=EB=A5=A0=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/StrategyEditorPage.tsx | 109 +++++++++++++++++- .../StrategyEditorEvaluationPanel.tsx | 25 +++- frontend/src/styles/strategyEditor.css | 4 + 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index 5f00bf6..ef433c0 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -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(null); + // ── 평가 탭 — 수익률 요약 ───────────────────────────────────────────────── + const [evalBacktestSignals, setEvalBacktestSignals] = useState([]); + const [evalBars, setEvalBars] = useState([]); + const [evalWindowMeta, setEvalWindowMeta] = useState({ + 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({ 평가 + {signalTab === 'eval' && ( + evalSignalScanRunning ? ( + 시그널 계산 중… + ) : evalAnalysisSummary ? ( + + 0 + ? ' seval-analysis-return--pos' + : evalAnalysisSummary.fullBuyReturnPct < 0 + ? ' seval-analysis-return--neg' + : '' + }`}> + 일괄:{formatEvaluationReturnPct(evalAnalysisSummary.fullBuyReturnPct)} + + · + 0 + ? ' seval-analysis-return--pos' + : evalAnalysisSummary.splitBuyReturnPct < 0 + ? ' seval-analysis-return--neg' + : '' + }`}> + 분할:{formatEvaluationReturnPct(evalAnalysisSummary.splitBuyReturnPct)} + + + ) : null + )} {stratName && {stratName}} {signalTab !== 'eval' && hasMultipleStartSections(currentEditorState) && ( ) : editorMode === 'graph' ? ( <> diff --git a/frontend/src/components/strategyEditor/StrategyEditorEvaluationPanel.tsx b/frontend/src/components/strategyEditor/StrategyEditorEvaluationPanel.tsx index dfed2ab..ed9ad18 100644 --- a/frontend/src/components/strategyEditor/StrategyEditorEvaluationPanel.tsx +++ b/frontend/src/components/strategyEditor/StrategyEditorEvaluationPanel.tsx @@ -4,7 +4,9 @@ import '../../styles/backtestDashboard.css'; import '../../styles/strategyEvaluation.css'; import React, { useEffect, useMemo, useState } from 'react'; -import type { Theme, Timeframe } from '../../types'; +import type { Theme, Timeframe, OHLCVBar } from '../../types'; +import type { BacktestSignal } from '../../utils/backendApi'; +import type { StrategyEvaluationWindowMeta } from '../strategyEvaluation/StrategyEvaluationChart'; import StrategyEvaluationChart from '../strategyEvaluation/StrategyEvaluationChart'; import { useIndicatorSettings } from '../../hooks/useIndicatorSettings'; import { useMarketTicker } from '../../hooks/useMarketTicker'; @@ -27,6 +29,11 @@ interface Props { stratDesc: string; buyEditorState: EditorConditionState; sellEditorState: EditorConditionState; + onSignalsChange?: (signals: BacktestSignal[]) => void; + onBarsLoaded?: (bars: OHLCVBar[]) => void; + onEvalWindowMetaChange?: (meta: StrategyEvaluationWindowMeta) => void; + onBacktestRunningChange?: (running: boolean) => void; + onMarketChange?: (market: string) => void; } const StrategyEditorEvaluationPanel: React.FC = ({ @@ -35,6 +42,11 @@ const StrategyEditorEvaluationPanel: React.FC = ({ stratDesc, buyEditorState, sellEditorState, + onSignalsChange, + onBarsLoaded, + onEvalWindowMetaChange, + onBacktestRunningChange, + onMarketChange, }) => { const { getParams, getVisualConfig, settingsRevision } = useIndicatorSettings(); const [market, setMarket] = useState('KRW-BTC'); @@ -91,6 +103,11 @@ const StrategyEditorEvaluationPanel: React.FC = ({ refreshAllTickers: refreshMarketTickers, } = useMarketTicker(); + const handleMarketChange = (next: string) => { + setMarket(next); + onMarketChange?.(next); + }; + if (!draftStrategy) { return (
@@ -111,12 +128,16 @@ const StrategyEditorEvaluationPanel: React.FC = ({ strategies={chartStrategies} selectedStrategyId={DRAFT_EVAL_STRATEGY_ID} onStrategySelect={() => {}} - onMarketChange={setMarket} + onMarketChange={handleMarketChange} onTimeframeChoiceChange={setTimeframeChoice} theme={theme} strategy={draftStrategy} selectedBarIndex={selectedBarIndex} onSelectedBarIndexChange={setSelectedBarIndex} + onBarsLoaded={onBarsLoaded} + onEvalWindowMetaChange={onEvalWindowMetaChange} + onSignalsChange={onSignalsChange} + onBacktestRunningChange={onBacktestRunningChange} paramsRevision={paramsRevision} getParamsOverride={getParams} getVisualConfigOverride={getVisualConfig} diff --git a/frontend/src/styles/strategyEditor.css b/frontend/src/styles/strategyEditor.css index c391f46..b24beb3 100644 --- a/frontend/src/styles/strategyEditor.css +++ b/frontend/src/styles/strategyEditor.css @@ -902,6 +902,10 @@ line-height: 1.5; } +.se-eval-return-summary { + flex-shrink: 0; +} + .se-editing-name { font-size: 0.72rem; color: color-mix(in srgb, var(--se-gold) 85%, transparent);