전략평가 상세설명 표시

This commit is contained in:
Macbook
2026-06-16 23:56:17 +09:00
parent e4611404a9
commit a6e889a579
5 changed files with 294 additions and 136 deletions
@@ -0,0 +1,71 @@
import React, { useMemo } from 'react';
import type { StrategyDto } from '../../utils/backendApi';
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
import {
buildConditionContextMap,
buildSideInterpretation,
formatBarTimeLabel,
} from '../../utils/strategyEvaluationInterpret';
import StrategyEvaluationInterpretContent from './StrategyEvaluationInterpretContent';
interface Props {
buyMetrics: ConditionMetric[];
sellMetrics: ConditionMetric[];
buyOverallMet?: boolean | null;
sellOverallMet?: boolean | null;
strategy?: StrategyDto | null;
barTimeSec?: number | null;
getParams?: (
type: string,
defaults?: Record<string, number | string | boolean>,
) => Record<string, number | string | boolean>;
}
const StrategyEvaluationDetailPanel: React.FC<Props> = ({
buyMetrics,
sellMetrics,
buyOverallMet,
sellOverallMet,
strategy,
barTimeSec,
getParams,
}) => {
const contextMap = useMemo(
() => buildConditionContextMap(strategy, getParams),
[strategy, getParams],
);
const buyInterpretation = useMemo(
() => buildSideInterpretation('buy', buyMetrics, buyOverallMet, contextMap),
[buyMetrics, buyOverallMet, contextMap],
);
const sellInterpretation = useMemo(
() => buildSideInterpretation('sell', sellMetrics, sellOverallMet, contextMap),
[sellMetrics, sellOverallMet, contextMap],
);
const barLabel = formatBarTimeLabel(barTimeSec);
return (
<section className="seval-detail-panel" aria-label="매수·매도 상세 분석">
<header className="seval-detail-panel-head">
<span className="seval-detail-panel-title"> </span>
<span className="seval-detail-panel-meta">{barLabel}</span>
</header>
<div className="seval-detail-panel-body">
<div className="seval-detail-side seval-detail-side--buy">
<h3 className="seval-detail-side-label"></h3>
<StrategyEvaluationInterpretContent interpretation={buyInterpretation} compact />
</div>
<div className="seval-detail-side seval-detail-side--sell">
<h3 className="seval-detail-side-label"></h3>
<StrategyEvaluationInterpretContent interpretation={sellInterpretation} compact />
</div>
<footer className="seval-detail-footnote">
Ta4j Rule , AND/OR/NOT / .
</footer>
</div>
</section>
);
};
export default StrategyEvaluationDetailPanel;
@@ -0,0 +1,59 @@
import React from 'react';
import type { SideInterpretation } from '../../utils/strategyEvaluationInterpret';
interface Props {
interpretation: SideInterpretation;
compact?: boolean;
}
/** 매수/매도 조건 상세 해석 본문 (모달·우측 패널 공용) */
const StrategyEvaluationInterpretContent: React.FC<Props> = ({
interpretation,
compact = false,
}) => {
const overallClass = interpretation.overallMet === true
? 'seval-interpret-overall--met'
: interpretation.overallMet === false
? 'seval-interpret-overall--unmet'
: 'seval-interpret-overall--unknown';
return (
<div className={`seval-interpret-content${compact ? ' seval-interpret-content--compact' : ''}`}>
<div className={`seval-interpret-overall ${overallClass}`}>
<span className="seval-interpret-overall-badge">
{interpretation.overallMet === true
? '전체 충족'
: interpretation.overallMet === false
? '전체 미충족'
: '전체 평가 불명'}
</span>
<p className="seval-interpret-overall-text">{interpretation.overallSummary}</p>
</div>
{interpretation.conditions.length === 0 ? (
<p className="seval-interpret-empty"> .</p>
) : (
<ul className="seval-interpret-list">
{interpretation.conditions.map(item => (
<li
key={item.id}
className={`seval-interpret-item seval-interpret-item--${item.status}`}
>
<div className="seval-interpret-item-head">
<span className={`seval-interpret-status seval-interpret-status--${item.status}`}>
{item.statusLabel}
</span>
<span className="seval-interpret-item-title" title={item.summary}>
{item.summary || item.label}
</span>
</div>
<p className="seval-interpret-reason">{item.reason}</p>
</li>
))}
</ul>
)}
</div>
);
};
export default StrategyEvaluationInterpretContent;
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import DraggableModalFrame from '../DraggableModalFrame';
import type { SideInterpretation } from '../../utils/strategyEvaluationInterpret';
import { formatBarTimeLabel } from '../../utils/strategyEvaluationInterpret';
import StrategyEvaluationInterpretContent from './StrategyEvaluationInterpretContent';
interface Props {
side: 'buy' | 'sell';
@@ -21,12 +22,6 @@ const StrategyEvaluationInterpretModal: React.FC<Props> = ({
const title = side === 'buy' ? '매수 조건 해석' : '매도 조건 해석';
const barLabel = formatBarTimeLabel(barTimeSec);
const overallClass = interpretation.overallMet === true
? 'seval-interpret-overall--met'
: interpretation.overallMet === false
? 'seval-interpret-overall--unmet'
: 'seval-interpret-overall--unknown';
const intro = useMemo(() => {
const parts = [`${barLabel} 시점`, strategyName ? `${strategyName}` : null].filter(Boolean);
return parts.join(' · ');
@@ -44,47 +39,11 @@ const StrategyEvaluationInterpretModal: React.FC<Props> = ({
bodyClassName="seval-interpret-body app-popup-body"
zIndex={11000}
>
<div className="seval-interpret-content">
<p className="seval-interpret-intro">{intro}</p>
<div className={`seval-interpret-overall ${overallClass}`}>
<span className="seval-interpret-overall-badge">
{interpretation.overallMet === true
? '전체 충족'
: interpretation.overallMet === false
? '전체 미충족'
: '전체 평가 불명'}
</span>
<p className="seval-interpret-overall-text">{interpretation.overallSummary}</p>
</div>
{interpretation.conditions.length === 0 ? (
<p className="seval-interpret-empty"> .</p>
) : (
<ul className="seval-interpret-list">
{interpretation.conditions.map(item => (
<li
key={item.id}
className={`seval-interpret-item seval-interpret-item--${item.status}`}
>
<div className="seval-interpret-item-head">
<span className={`seval-interpret-status seval-interpret-status--${item.status}`}>
{item.statusLabel}
</span>
<span className="seval-interpret-item-title" title={item.summary}>
{item.summary || item.label}
</span>
</div>
<p className="seval-interpret-reason">{item.reason}</p>
</li>
))}
</ul>
)}
<footer className="seval-interpret-footnote">
Ta4j Rule , AND/OR/NOT / .
</footer>
</div>
<p className="seval-interpret-intro">{intro}</p>
<StrategyEvaluationInterpretContent interpretation={interpretation} />
<footer className="seval-interpret-footnote">
Ta4j Rule , AND/OR/NOT / .
</footer>
</DraggableModalFrame>
);
};
@@ -1,16 +1,9 @@
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import type { StrategyDto } from '../../utils/backendApi';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import {
buildConditionMetrics,
type ConditionMetric,
} from '../../utils/virtualSignalMetrics';
import {
buildConditionContextMap,
buildSideInterpretation,
} from '../../utils/strategyEvaluationInterpret';
import { buildConditionMetrics } from '../../utils/virtualSignalMetrics';
import StrategyEvaluationMatchVisual from './StrategyEvaluationMatchVisual';
import StrategyEvaluationInterpretModal from './StrategyEvaluationInterpretModal';
import StrategyEvaluationDetailPanel from './StrategyEvaluationDetailPanel';
import type { BarSignalHighlight } from '../../utils/strategyEvaluationBarSignals';
interface Props {
@@ -29,12 +22,9 @@ interface Props {
interface SideCardProps {
side: 'buy' | 'sell';
metrics: ConditionMetric[];
metrics: ReturnType<typeof buildConditionMetrics>;
overallMet?: boolean | null;
signalActive?: boolean;
strategy?: StrategyDto | null;
barTimeSec?: number | null;
getParams?: Props['getParams'];
}
const SideCard: React.FC<SideCardProps> = ({
@@ -42,70 +32,38 @@ const SideCard: React.FC<SideCardProps> = ({
metrics,
overallMet,
signalActive = false,
strategy,
barTimeSec,
getParams,
}) => {
const [interpretOpen, setInterpretOpen] = useState(false);
const sideLabel = side === 'buy' ? '매수' : '매도';
const title = side === 'buy' ? '매수 조건' : '매도 조건';
const overallLabel =
overallMet === true ? '충족' : overallMet === false ? '미충족' : null;
const contextMap = useMemo(() => buildConditionContextMap(strategy, getParams), [strategy, getParams]);
const interpretation = useMemo(
() => buildSideInterpretation(side, metrics, overallMet, contextMap),
[side, metrics, overallMet, contextMap],
);
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}`}>{sideLabel}</span>
{overallLabel && (
<span
className={`seval-side-overall seval-side-overall--${overallMet ? 'met' : 'unmet'}`}
title="DSL 전체 Rule 충족 여부 (AND/OR 반영)"
>
{overallLabel}
</span>
)}
</div>
<button
type="button"
className="seval-analyze-btn"
title="조건별 충족/미충족 분석"
aria-label={`${title} 분석보기`}
disabled={metrics.length === 0}
onClick={() => setInterpretOpen(true)}
>
</button>
</header>
<div className="seval-side-card-body">
<StrategyEvaluationMatchVisual metrics={metrics} overallMet={overallMet} />
<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}`}>{sideLabel}</span>
{overallLabel && (
<span
className={`seval-side-overall seval-side-overall--${overallMet ? 'met' : 'unmet'}`}
title="DSL 전체 Rule 충족 여부 (AND/OR 반영)"
>
{overallLabel}
</span>
)}
</div>
</section>
{interpretOpen && (
<StrategyEvaluationInterpretModal
side={side}
interpretation={interpretation}
barTimeSec={barTimeSec}
strategyName={strategy?.name}
onClose={() => setInterpretOpen(false)}
/>
)}
</>
</header>
<div className="seval-side-card-body">
<StrategyEvaluationMatchVisual metrics={metrics} overallMet={overallMet} />
</div>
</section>
);
};
@@ -141,20 +99,23 @@ const StrategyEvaluationSignalPanel: React.FC<Props> = ({
return (
<div className={['seval-signal-split', className].filter(Boolean).join(' ')}>
<SideCard
side="sell"
metrics={sellMetrics}
overallMet={snapshot?.overallExitMet}
signalActive={barSignalHighlight?.sell ?? false}
strategy={strategy}
barTimeSec={barTimeSec}
getParams={getParams}
/>
<SideCard
side="buy"
metrics={buyMetrics}
overallMet={snapshot?.overallEntryMet}
signalActive={barSignalHighlight?.buy ?? false}
/>
<SideCard
side="sell"
metrics={sellMetrics}
overallMet={snapshot?.overallExitMet}
signalActive={barSignalHighlight?.sell ?? false}
/>
<StrategyEvaluationDetailPanel
buyMetrics={buyMetrics}
sellMetrics={sellMetrics}
buyOverallMet={snapshot?.overallEntryMet}
sellOverallMet={snapshot?.overallExitMet}
strategy={strategy}
barTimeSec={barTimeSec}
getParams={getParams}