Files
goldenChart/frontend/src/components/strategyEvaluation/StrategyEvaluationSignalPanel.tsx
T
2026-06-12 15:22:48 +09:00

111 lines
3.6 KiB
TypeScript

import React, { useMemo } from 'react';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import {
buildConditionMetrics,
type ConditionMetric,
} from '../../utils/virtualSignalMetrics';
import VirtualSignalMatchVisual from '../virtual/VirtualSignalMatchVisual';
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 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>
</header>
<div className="seval-side-card-body">
<VirtualSignalMatchVisual
metrics={metrics}
title="SIGNAL INTELLIGENCE & MATCH RATES"
panelClassName="seval-side-sig-panel"
/>
</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;