111 lines
3.6 KiB
TypeScript
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;
|