전략평가 화면 수정
This commit is contained in:
@@ -222,6 +222,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
selectedStrategy,
|
||||
selectedBarTimeSec,
|
||||
appliedIndicatorParams,
|
||||
bars,
|
||||
chartTimeframe,
|
||||
);
|
||||
if (session !== evalSessionRef.current) return;
|
||||
setSnapshot(snap ?? undefined);
|
||||
@@ -240,7 +242,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
selectedStrategyId,
|
||||
selectedStrategy,
|
||||
selectedBarTimeSec,
|
||||
bars.length,
|
||||
selectedBarIndex,
|
||||
bars,
|
||||
chartTimeframe,
|
||||
appliedIndicatorParams,
|
||||
]);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const SideCard: React.FC<SideCardProps> = ({ side, metrics, overallMet, signalAc
|
||||
<VirtualSignalMatchVisual
|
||||
metrics={metrics}
|
||||
title="SIGNAL INTELLIGENCE & MATCH RATES"
|
||||
overallMet={overallMet}
|
||||
panelClassName="seval-side-sig-panel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
buildConditionMetrics,
|
||||
computeMatchRate,
|
||||
getTrafficLightState,
|
||||
resolveSideHeadlineMatchRate,
|
||||
resolveSideTrafficState,
|
||||
type ConditionMetric,
|
||||
} from '../../utils/virtualSignalMetrics';
|
||||
import VirtualSignalEqualizer from './VirtualSignalEqualizer';
|
||||
@@ -15,6 +17,8 @@ interface Props {
|
||||
title?: React.ReactNode;
|
||||
/** 백엔드 집계 matchRate — 미지정 시 metrics 로컬 계산 */
|
||||
backendMatchRate?: number | null;
|
||||
/** DSL 전체 Rule 충족 — 헤드라인 일치율·신호등 우선 */
|
||||
overallMet?: boolean | null;
|
||||
className?: string;
|
||||
panelClassName?: string;
|
||||
}
|
||||
@@ -23,16 +27,21 @@ const VirtualSignalMatchVisual: React.FC<Props> = ({
|
||||
metrics,
|
||||
title,
|
||||
backendMatchRate,
|
||||
overallMet,
|
||||
className = '',
|
||||
panelClassName = '',
|
||||
}) => {
|
||||
const matchRate = useMemo(
|
||||
() => computeMatchRate(metrics, backendMatchRate),
|
||||
[metrics, backendMatchRate],
|
||||
() => (overallMet != null
|
||||
? resolveSideHeadlineMatchRate(metrics, overallMet)
|
||||
: computeMatchRate(metrics, backendMatchRate)),
|
||||
[metrics, backendMatchRate, overallMet],
|
||||
);
|
||||
const trafficState = useMemo(
|
||||
() => getTrafficLightState(matchRate, metrics),
|
||||
[matchRate, metrics],
|
||||
() => (overallMet != null
|
||||
? resolveSideTrafficState(metrics, overallMet)
|
||||
: getTrafficLightState(matchRate, metrics)),
|
||||
[matchRate, metrics, overallMet],
|
||||
);
|
||||
const { visualRef, lightRef } = useSignalVisualRailHeight([
|
||||
metrics.length,
|
||||
|
||||
@@ -1472,18 +1472,41 @@ export async function fetchLiveConditionStatus(
|
||||
strategyId: number,
|
||||
barTimeSec?: number | null,
|
||||
indicatorParams?: Record<string, Record<string, unknown>> | null,
|
||||
chartBars?: Array<{
|
||||
time: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
}> | null,
|
||||
chartTimeframe?: string | null,
|
||||
): Promise<LiveConditionStatusDto | null> {
|
||||
if (indicatorParams && Object.keys(indicatorParams).length > 0) {
|
||||
const normalizedBarTime = barTimeSec != null && Number.isFinite(barTimeSec)
|
||||
? Math.floor(barTimeSec)
|
||||
: null;
|
||||
const useChartBars = chartBars != null && chartBars.length > 0 && chartTimeframe;
|
||||
|
||||
if (useChartBars || (indicatorParams && Object.keys(indicatorParams).length > 0)) {
|
||||
return request<LiveConditionStatusDto>('/strategy/live-conditions/evaluate', {
|
||||
method: 'POST',
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({
|
||||
market,
|
||||
strategyId,
|
||||
barTimeSec: barTimeSec != null && Number.isFinite(barTimeSec)
|
||||
? Math.floor(barTimeSec)
|
||||
: null,
|
||||
indicatorParams,
|
||||
barTimeSec: normalizedBarTime,
|
||||
indicatorParams: indicatorParams ?? undefined,
|
||||
bars: useChartBars
|
||||
? chartBars.map(b => ({
|
||||
time: b.time,
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
}))
|
||||
: undefined,
|
||||
timeframe: useChartBars ? chartTimeframe : undefined,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -1491,8 +1514,8 @@ export async function fetchLiveConditionStatus(
|
||||
market,
|
||||
strategyId: String(strategyId),
|
||||
});
|
||||
if (barTimeSec != null && Number.isFinite(barTimeSec)) {
|
||||
params.set('barTimeSec', String(Math.floor(barTimeSec)));
|
||||
if (normalizedBarTime != null) {
|
||||
params.set('barTimeSec', String(normalizedBarTime));
|
||||
}
|
||||
return request<LiveConditionStatusDto>(`/strategy/live-conditions?${params}`, {
|
||||
cache: 'no-store',
|
||||
|
||||
@@ -13,12 +13,16 @@ import { normalizeConditionRow } from './virtualSignalMetrics';
|
||||
import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnapshots';
|
||||
import { mergeRows } from './strategyEvaluationMerge';
|
||||
|
||||
import type { OHLCVBar } from '../types';
|
||||
|
||||
export async function fetchEvaluationSnapshotAtBar(
|
||||
market: string,
|
||||
strategyId: number,
|
||||
strategy: StrategyDto | undefined,
|
||||
barTimeSec: number | null,
|
||||
indicatorParams?: Record<string, Record<string, unknown>> | null,
|
||||
chartBars?: OHLCVBar[] | null,
|
||||
chartTimeframe?: string | null,
|
||||
): Promise<VirtualIndicatorSnapshot | null> {
|
||||
const normalizedMarket = normalizeMarketCode(market);
|
||||
const hydrated = strategy ? hydrateStrategyDto(strategy) : undefined;
|
||||
@@ -37,6 +41,8 @@ export async function fetchEvaluationSnapshotAtBar(
|
||||
strategyId,
|
||||
barTimeSec,
|
||||
indicatorParams,
|
||||
chartBars,
|
||||
chartTimeframe,
|
||||
);
|
||||
} catch {
|
||||
status = null;
|
||||
|
||||
@@ -126,6 +126,25 @@ export function getTrafficLightState(matchRate: number, metrics: ConditionMetric
|
||||
return 'red';
|
||||
}
|
||||
|
||||
/** side 카드 헤드라인 — DSL 전체 Rule 충족 시 100% (리프 비율과 구분) */
|
||||
export function resolveSideHeadlineMatchRate(
|
||||
metrics: ConditionMetric[],
|
||||
overallMet?: boolean | null,
|
||||
): number {
|
||||
if (overallMet === true) return 100;
|
||||
return computeMatchRate(metrics);
|
||||
}
|
||||
|
||||
export function resolveSideTrafficState(
|
||||
metrics: ConditionMetric[],
|
||||
overallMet?: boolean | null,
|
||||
): TrafficLightState {
|
||||
const rate = resolveSideHeadlineMatchRate(metrics, overallMet);
|
||||
if (overallMet === true) return 'blue';
|
||||
if (overallMet === false && rate === 0) return 'red';
|
||||
return getTrafficLightState(rate, metrics);
|
||||
}
|
||||
|
||||
export function formatUpdatedTime(ts: number | undefined): string {
|
||||
if (!ts) return '—';
|
||||
return new Date(ts).toLocaleTimeString('ko-KR', {
|
||||
|
||||
Reference in New Issue
Block a user