전략평가 수정
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user