전략평가 화면 수정

This commit is contained in:
Macbook
2026-06-12 15:22:48 +09:00
parent 7579ed9de4
commit 8aae8489ae
8 changed files with 298 additions and 152 deletions
@@ -69,6 +69,20 @@ function indexFromClientX(
return findNearestBarIndex(bars, time);
}
/** 캔들 pane 클릭 좌표 → 봉 index (전략선택봉 이동) */
export function resolveBarIndexAtChartClick(
manager: ChartManager,
bars: OHLCVBar[],
clientX: number,
clientY: number,
): number {
const rect = manager.getContainer().getBoundingClientRect();
const clickY = clientY - rect.top;
const main = manager.getPaneLayouts().find(l => l.paneIndex === 0);
if (!main || clickY < main.topY || clickY >= main.topY + main.height) return -1;
return indexFromClientX(manager, bars, clientX);
}
const StrategyEvaluationBarSelector: React.FC<Props> = ({
manager,
bars,
@@ -20,7 +20,9 @@ import { buildVirtualTradingChartIndicators } from '../../utils/strategyToChartI
import { buildEvalParamsFromStrategy } from '../../utils/strategyEvaluationParams';
import { resolveEvaluationFromLoadedBars } from '../../utils/backtestWarmup';
import { repairUtf8Mojibake } from '../../utils/textEncoding';
import StrategyEvaluationBarSelector from './StrategyEvaluationBarSelector';
import StrategyEvaluationBarSelector, {
resolveBarIndexAtChartClick,
} from './StrategyEvaluationBarSelector';
import {
parseBacktestRunTimeframeChoice,
type BacktestRunTimeframeChoice,
@@ -300,6 +302,26 @@ const StrategyEvaluationChart: React.FC<Props> = ({
onSelectedBarIndexChange(bars.length - 1);
}, [bars.length, onSelectedBarIndexChange]);
useEffect(() => {
const mgr = chartMgr;
if (!mgr || bars.length === 0) return;
const onChartClick = (e: MouseEvent) => {
if (e.button !== 0) return;
const target = e.target as HTMLElement | null;
if (target?.closest('[data-no-chart-pan]')) return;
const idx = resolveBarIndexAtChartClick(mgr, bars, e.clientX, e.clientY);
if (idx >= 0 && idx !== selectedBarIndex) {
onSelectedBarIndexChange(idx);
}
};
const container = mgr.getContainer();
container.addEventListener('click', onChartClick);
return () => container.removeEventListener('click', onChartClick);
}, [chartMgr, bars, selectedBarIndex, onSelectedBarIndexChange]);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
@@ -372,10 +394,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
))}
</select>
{selectedBar && (
<span className={`seval-chart-bar-time${evalLoading ? ' seval-chart-bar-time--loading' : ''}`}>
{evalLoading && (
<span className="btd-run-spinner seval-chart-bar-spinner" aria-hidden />
)}
<span className="seval-chart-bar-time">
{new Date(selectedBar.time * 1000).toLocaleString('ko-KR')}
</span>
)}
@@ -388,11 +407,17 @@ const StrategyEvaluationChart: React.FC<Props> = ({
<span className="seval-chart-backtest-busy"> </span>
)}
</div>
<span className="seval-chart-hint"> · · </span>
<span className="seval-chart-hint"> · · · </span>
</div>
<div className="btd-analysis-main">
<div className="seval-chart-body btd-analysis-canvas-wrap btd-analysis-canvas-wrap--live">
{evalLoading && (
<div className="seval-chart-eval-backdrop" aria-live="polite" aria-busy="true">
<span className="btd-run-spinner seval-chart-eval-backdrop-spinner" aria-hidden />
<span className="seval-chart-eval-backdrop-text"> </span>
</div>
)}
{loading && <div className="seval-chart-status"> </div>}
{error && !loading && <div className="btd-analysis-error">{error}</div>}
{backtestError && !loading && !backtestRunning && !error && (
@@ -2,12 +2,9 @@ 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 VirtualSignalMatchVisual from '../virtual/VirtualSignalMatchVisual';
import type { BarSignalHighlight } from '../../utils/strategyEvaluationBarSignals';
interface Props {
@@ -28,8 +25,6 @@ interface SideCardProps {
}
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;
@@ -41,7 +36,7 @@ const SideCard: React.FC<SideCardProps> = ({ side, metrics, overallMet, signalAc
`seval-side-card--${side}`,
signalActive ? 'seval-side-card--signal-active' : '',
].filter(Boolean).join(' ')}
aria-label={`${title} 지표 목록${signalActive ? ' · 백테스트 시그널 발생 봉' : ''}`}
aria-label={`${title} 지표 일치율${signalActive ? ' · 백테스트 시그널 발생 봉' : ''}`}
>
<header className="seval-side-card-head">
<div className="seval-side-card-head-main">
@@ -55,12 +50,13 @@ const SideCard: React.FC<SideCardProps> = ({ side, metrics, overallMet, signalAc
</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} />
<VirtualSignalMatchVisual
metrics={metrics}
title="SIGNAL INTELLIGENCE & MATCH RATES"
panelClassName="seval-side-sig-panel"
/>
</div>
</section>
);