전략편집기 평가탭 전략평가후 수익률 표시
This commit is contained in:
@@ -5,7 +5,7 @@ import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
import DraggableModalFrame from './DraggableModalFrame';
|
import DraggableModalFrame from './DraggableModalFrame';
|
||||||
import type { Theme } from '../types/index';
|
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 { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
|
||||||
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
||||||
import { syncStrategyTimeframesFromLayoutIfNeeded } from '../utils/strategyTimeframeSync';
|
import { syncStrategyTimeframesFromLayoutIfNeeded } from '../utils/strategyTimeframeSync';
|
||||||
@@ -168,7 +168,7 @@ import StrategyDescriptionModal from './strategyEditor/StrategyDescriptionModal'
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { MarketSearchPanel } from './MarketSearchPanel';
|
import { MarketSearchPanel } from './MarketSearchPanel';
|
||||||
import { getKoreanName } from '../utils/marketNameCache';
|
import { getKoreanName } from '../utils/marketNameCache';
|
||||||
import type { Timeframe } from '../types';
|
import type { Timeframe, OHLCVBar } from '../types';
|
||||||
import {
|
import {
|
||||||
BACKTEST_STRATEGY_TIMEFRAME,
|
BACKTEST_STRATEGY_TIMEFRAME,
|
||||||
buildQuickRunTimeframeSelectOptions,
|
buildQuickRunTimeframeSelectOptions,
|
||||||
@@ -178,6 +178,14 @@ import {
|
|||||||
} from '../utils/backtestRunTimeframe';
|
} from '../utils/backtestRunTimeframe';
|
||||||
import '../styles/strategyEditor.css';
|
import '../styles/strategyEditor.css';
|
||||||
import '../styles/strategyEditorTheme.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 SignalTab = 'buy' | 'sell' | 'eval';
|
||||||
type BuySellTab = 'buy' | 'sell';
|
type BuySellTab = 'buy' | 'sell';
|
||||||
@@ -359,6 +367,20 @@ export default function StrategyEditorPage({
|
|||||||
const [qbMarketQ, setQbMarketQ] = useState('');
|
const [qbMarketQ, setQbMarketQ] = useState('');
|
||||||
const qbMarketBtnRef = useRef<HTMLButtonElement>(null);
|
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 [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
|
||||||
const [rightWidth, setRightWidth] = useState(() => readRightPanelWidth());
|
const [rightWidth, setRightWidth] = useState(() => readRightPanelWidth());
|
||||||
const [rightOpen, setRightOpen] = useState(() => readStoredBool('se-right-open', true));
|
const [rightOpen, setRightOpen] = useState(() => readStoredBool('se-right-open', true));
|
||||||
@@ -1690,6 +1712,54 @@ export default function StrategyEditorPage({
|
|||||||
[qbStrategyTimeframe],
|
[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 = [
|
const operators = [
|
||||||
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
||||||
{ type: 'operator' as const, value: 'OR', label: 'OR', color: 'logic-or' },
|
{ type: 'operator' as const, value: 'OR', label: 'OR', color: 'logic-or' },
|
||||||
@@ -1982,6 +2052,36 @@ export default function StrategyEditorPage({
|
|||||||
평가
|
평가
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>}
|
{stratName && <span className="se-editing-name">{stratName}</span>}
|
||||||
{signalTab !== 'eval' && hasMultipleStartSections(currentEditorState) && (
|
{signalTab !== 'eval' && hasMultipleStartSections(currentEditorState) && (
|
||||||
<StartCombineOpControl
|
<StartCombineOpControl
|
||||||
@@ -2044,6 +2144,11 @@ export default function StrategyEditorPage({
|
|||||||
stratDesc={stratDesc}
|
stratDesc={stratDesc}
|
||||||
buyEditorState={buyEditorState}
|
buyEditorState={buyEditorState}
|
||||||
sellEditorState={sellEditorState}
|
sellEditorState={sellEditorState}
|
||||||
|
onSignalsChange={handleEvalSignalsChange}
|
||||||
|
onBarsLoaded={handleEvalBarsLoaded}
|
||||||
|
onEvalWindowMetaChange={handleEvalWindowMetaChange}
|
||||||
|
onBacktestRunningChange={setEvalSignalScanRunning}
|
||||||
|
onMarketChange={setEvalMarket}
|
||||||
/>
|
/>
|
||||||
) : editorMode === 'graph' ? (
|
) : editorMode === 'graph' ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
import '../../styles/backtestDashboard.css';
|
import '../../styles/backtestDashboard.css';
|
||||||
import '../../styles/strategyEvaluation.css';
|
import '../../styles/strategyEvaluation.css';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
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 StrategyEvaluationChart from '../strategyEvaluation/StrategyEvaluationChart';
|
||||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||||
import { useMarketTicker } from '../../hooks/useMarketTicker';
|
import { useMarketTicker } from '../../hooks/useMarketTicker';
|
||||||
@@ -27,6 +29,11 @@ interface Props {
|
|||||||
stratDesc: string;
|
stratDesc: string;
|
||||||
buyEditorState: EditorConditionState;
|
buyEditorState: EditorConditionState;
|
||||||
sellEditorState: 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> = ({
|
const StrategyEditorEvaluationPanel: React.FC<Props> = ({
|
||||||
@@ -35,6 +42,11 @@ const StrategyEditorEvaluationPanel: React.FC<Props> = ({
|
|||||||
stratDesc,
|
stratDesc,
|
||||||
buyEditorState,
|
buyEditorState,
|
||||||
sellEditorState,
|
sellEditorState,
|
||||||
|
onSignalsChange,
|
||||||
|
onBarsLoaded,
|
||||||
|
onEvalWindowMetaChange,
|
||||||
|
onBacktestRunningChange,
|
||||||
|
onMarketChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams, getVisualConfig, settingsRevision } = useIndicatorSettings();
|
const { getParams, getVisualConfig, settingsRevision } = useIndicatorSettings();
|
||||||
const [market, setMarket] = useState('KRW-BTC');
|
const [market, setMarket] = useState('KRW-BTC');
|
||||||
@@ -91,6 +103,11 @@ const StrategyEditorEvaluationPanel: React.FC<Props> = ({
|
|||||||
refreshAllTickers: refreshMarketTickers,
|
refreshAllTickers: refreshMarketTickers,
|
||||||
} = useMarketTicker();
|
} = useMarketTicker();
|
||||||
|
|
||||||
|
const handleMarketChange = (next: string) => {
|
||||||
|
setMarket(next);
|
||||||
|
onMarketChange?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
if (!draftStrategy) {
|
if (!draftStrategy) {
|
||||||
return (
|
return (
|
||||||
<div className="se-eval-panel se-eval-panel--empty">
|
<div className="se-eval-panel se-eval-panel--empty">
|
||||||
@@ -111,12 +128,16 @@ const StrategyEditorEvaluationPanel: React.FC<Props> = ({
|
|||||||
strategies={chartStrategies}
|
strategies={chartStrategies}
|
||||||
selectedStrategyId={DRAFT_EVAL_STRATEGY_ID}
|
selectedStrategyId={DRAFT_EVAL_STRATEGY_ID}
|
||||||
onStrategySelect={() => {}}
|
onStrategySelect={() => {}}
|
||||||
onMarketChange={setMarket}
|
onMarketChange={handleMarketChange}
|
||||||
onTimeframeChoiceChange={setTimeframeChoice}
|
onTimeframeChoiceChange={setTimeframeChoice}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
strategy={draftStrategy}
|
strategy={draftStrategy}
|
||||||
selectedBarIndex={selectedBarIndex}
|
selectedBarIndex={selectedBarIndex}
|
||||||
onSelectedBarIndexChange={setSelectedBarIndex}
|
onSelectedBarIndexChange={setSelectedBarIndex}
|
||||||
|
onBarsLoaded={onBarsLoaded}
|
||||||
|
onEvalWindowMetaChange={onEvalWindowMetaChange}
|
||||||
|
onSignalsChange={onSignalsChange}
|
||||||
|
onBacktestRunningChange={onBacktestRunningChange}
|
||||||
paramsRevision={paramsRevision}
|
paramsRevision={paramsRevision}
|
||||||
getParamsOverride={getParams}
|
getParamsOverride={getParams}
|
||||||
getVisualConfigOverride={getVisualConfig}
|
getVisualConfigOverride={getVisualConfig}
|
||||||
|
|||||||
@@ -902,6 +902,10 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.se-eval-return-summary {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.se-editing-name {
|
.se-editing-name {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: color-mix(in srgb, var(--se-gold) 85%, transparent);
|
color: color-mix(in srgb, var(--se-gold) 85%, transparent);
|
||||||
|
|||||||
Reference in New Issue
Block a user