전략평가 수정

This commit is contained in:
Macbook
2026-06-12 14:59:23 +09:00
parent c562a4eab2
commit 7579ed9de4
7 changed files with 338 additions and 5 deletions
@@ -7,17 +7,18 @@ import '../styles/virtualTradingDashboard.css';
import '../styles/strategyEditorTheme.css';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { Theme, Timeframe } from '../types';
import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnapshots';
import {
loadStrategies,
loadStrategy,
type BacktestSignal,
type StrategyDto,
} from '../utils/backendApi';
import { hydrateStrategyDto } from '../utils/strategyHydrate';
import { repairUtf8Mojibake } from '../utils/textEncoding';
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnapshots';
import VirtualTargetSignalPanel from './virtual/VirtualTargetSignalPanel';
import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel';
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
@@ -34,6 +35,7 @@ import {
resolveBacktestExecTimeframe,
type BacktestRunTimeframeChoice,
} from '../utils/backtestRunTimeframe';
import { resolveBarSignalHighlight } from '../utils/strategyEvaluationBarSignals';
import type { OHLCVBar } from '../types';
const LEFT_KEY = 'seval-left-width';
@@ -71,6 +73,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
const [appliedIndicatorParams, setAppliedIndicatorParams] = useState<EvalIndicatorParams | null>(null);
const [paramsRevision, setParamsRevision] = useState(0);
const [chartLoading, setChartLoading] = useState(false);
const [backtestSignals, setBacktestSignals] = useState<BacktestSignal[]>([]);
const evalSessionRef = useRef(0);
const paramsRevisionRef = useRef(paramsRevision);
@@ -189,6 +192,15 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
const barSignalHighlight = useMemo(
() => resolveBarSignalHighlight(backtestSignals, selectedBarTimeSec),
[backtestSignals, selectedBarTimeSec],
);
const handleSignalsChange = useCallback((signals: BacktestSignal[]) => {
setBacktestSignals(signals);
}, []);
const refreshEvaluation = useCallback(async () => {
if (!selectedStrategyId || !selectedStrategy) {
setSnapshot(undefined);
@@ -410,6 +422,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
getParamsOverride={getEvalParams}
getVisualConfigOverride={getVisualConfig}
evalLoading={evalLoading}
onSignalsChange={handleSignalsChange}
/>
</div>
</main>
@@ -443,9 +456,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
<span className="seval-eval-loading-text"> </span>
</div>
)}
<VirtualTargetSignalPanel
<StrategyEvaluationSignalPanel
snapshot={snapshot}
viewMode="summary"
barSignalHighlight={barSignalHighlight}
loading={evalLoading}
loadingMessage="전략 조건 확인 중…"
hasStrategy={selectedStrategyId != null}
@@ -58,6 +58,8 @@ interface Props {
};
/** 선택 봉 조건 평가 진행 중 */
evalLoading?: boolean;
/** 백테스트 시그널 목록 변경 시 (우측 패널 하이라이트 등) */
onSignalsChange?: (signals: BacktestSignal[]) => void;
}
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
@@ -89,6 +91,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
getParamsOverride,
getVisualConfigOverride,
evalLoading = false,
onSignalsChange,
}) => {
const { getParams: baseGetParams, getVisualConfig: baseGetVisual } = useIndicatorSettings();
const getParams = getParamsOverride ?? baseGetParams;
@@ -217,6 +220,10 @@ const StrategyEvaluationChart: React.FC<Props> = ({
applyMarkers();
}, [signals, applyMarkers]);
useEffect(() => {
onSignalsChange?.(signals);
}, [signals, onSignalsChange]);
useEffect(() => {
onChartLoadingChange?.(loading);
}, [loading, onChartLoadingChange]);
@@ -0,0 +1,114 @@
import React, { useMemo } from 'react';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import {
buildConditionMetrics,
computeMatchRate,
getTrafficLightState,
type ConditionMetric,
} from '../../utils/virtualSignalMetrics';
import VirtualConditionList from '../virtual/VirtualConditionList';
import VirtualSignalTrafficLight from '../virtual/VirtualSignalTrafficLight';
import type { BarSignalHighlight } from '../../utils/strategyEvaluationBarSignals';
interface Props {
snapshot: VirtualIndicatorSnapshot | undefined;
/** 선택 봉에 백테스트 매수/매도 시그널 존재 여부 */
barSignalHighlight?: BarSignalHighlight;
loading?: boolean;
loadingMessage?: string;
hasStrategy?: boolean;
className?: string;
}
interface SideCardProps {
side: 'buy' | 'sell';
metrics: ConditionMetric[];
overallMet?: boolean | null;
signalActive?: boolean;
}
const SideCard: React.FC<SideCardProps> = ({ side, metrics, overallMet, signalActive = false }) => {
const matchRate = useMemo(() => computeMatchRate(metrics), [metrics]);
const trafficState = useMemo(() => getTrafficLightState(matchRate, metrics), [matchRate, metrics]);
const title = side === 'buy' ? '매수 조건' : '매도 조건';
const overallLabel =
overallMet === true ? '전체 충족' : overallMet === false ? '전체 미충족' : null;
return (
<section
className={[
'seval-side-card',
`seval-side-card--${side}`,
signalActive ? 'seval-side-card--signal-active' : '',
].filter(Boolean).join(' ')}
aria-label={`${title} 지표 목록${signalActive ? ' · 백테스트 시그널 발생 봉' : ''}`}
>
<header className="seval-side-card-head">
<div className="seval-side-card-head-main">
<span className={`seval-side-card-badge seval-side-card-badge--${side}`}>{title}</span>
{overallLabel && (
<span
className={`seval-side-overall seval-side-overall--${overallMet ? 'met' : 'unmet'}`}
title="DSL 전체 Rule 충족 여부 (AND/OR 반영)"
>
{overallLabel}
</span>
)}
</div>
<div className="seval-side-card-head-signal">
<VirtualSignalTrafficLight state={trafficState} matchRate={matchRate} />
</div>
</header>
<div className="seval-side-card-body">
<VirtualConditionList metrics={metrics} />
</div>
</section>
);
};
const StrategyEvaluationSignalPanel: React.FC<Props> = ({
snapshot,
barSignalHighlight,
loading = false,
loadingMessage,
hasStrategy = true,
className = '',
}) => {
const rows = snapshot?.rows ?? [];
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
const buyMetrics = useMemo(() => metrics.filter(m => m.row.side === 'buy'), [metrics]);
const sellMetrics = useMemo(() => metrics.filter(m => m.row.side === 'sell'), [metrics]);
if (rows.length === 0) {
return (
<p
className={`vtd-muted vtd-card-empty${loading ? ' vtd-card-empty--loading' : ''}${className ? ` ${className}` : ''}`}
>
{loading
? (loadingMessage ?? '지표 데이터 수집 중… (업비트 캔들 · 실시간 연결)')
: !hasStrategy
? '투자전략을 선택하세요 (상단 또는 종목별 전략)'
: '전략 조건·지표 데이터 없음'}
</p>
);
}
return (
<div className={['seval-signal-split', className].filter(Boolean).join(' ')}>
<SideCard
side="buy"
metrics={buyMetrics}
overallMet={snapshot?.overallEntryMet}
signalActive={barSignalHighlight?.buy ?? false}
/>
<SideCard
side="sell"
metrics={sellMetrics}
overallMet={snapshot?.overallExitMet}
signalActive={barSignalHighlight?.sell ?? false}
/>
</div>
);
};
export default StrategyEvaluationSignalPanel;