전략편집기 평가탭 전략평가후 수익률 표시
This commit is contained in:
@@ -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' ? (
|
||||
<>
|
||||
|
||||
@@ -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<Props> = ({
|
||||
@@ -35,6 +42,11 @@ const StrategyEditorEvaluationPanel: React.FC<Props> = ({
|
||||
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<Props> = ({
|
||||
refreshAllTickers: refreshMarketTickers,
|
||||
} = useMarketTicker();
|
||||
|
||||
const handleMarketChange = (next: string) => {
|
||||
setMarket(next);
|
||||
onMarketChange?.(next);
|
||||
};
|
||||
|
||||
if (!draftStrategy) {
|
||||
return (
|
||||
<div className="se-eval-panel se-eval-panel--empty">
|
||||
@@ -111,12 +128,16 @@ const StrategyEditorEvaluationPanel: React.FC<Props> = ({
|
||||
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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user