전략평가 수정
This commit is contained in:
@@ -7,17 +7,18 @@ import '../styles/virtualTradingDashboard.css';
|
|||||||
import '../styles/strategyEditorTheme.css';
|
import '../styles/strategyEditorTheme.css';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { Theme, Timeframe } from '../types';
|
import type { Theme, Timeframe } from '../types';
|
||||||
|
import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnapshots';
|
||||||
import {
|
import {
|
||||||
loadStrategies,
|
loadStrategies,
|
||||||
loadStrategy,
|
loadStrategy,
|
||||||
|
type BacktestSignal,
|
||||||
type StrategyDto,
|
type StrategyDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import { hydrateStrategyDto } from '../utils/strategyHydrate';
|
import { hydrateStrategyDto } from '../utils/strategyHydrate';
|
||||||
import { repairUtf8Mojibake } from '../utils/textEncoding';
|
import { repairUtf8Mojibake } from '../utils/textEncoding';
|
||||||
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
|
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
|
||||||
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
|
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
|
||||||
import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnapshots';
|
import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel';
|
||||||
import VirtualTargetSignalPanel from './virtual/VirtualTargetSignalPanel';
|
|
||||||
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
|
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
|
||||||
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
||||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
resolveBacktestExecTimeframe,
|
resolveBacktestExecTimeframe,
|
||||||
type BacktestRunTimeframeChoice,
|
type BacktestRunTimeframeChoice,
|
||||||
} from '../utils/backtestRunTimeframe';
|
} from '../utils/backtestRunTimeframe';
|
||||||
|
import { resolveBarSignalHighlight } from '../utils/strategyEvaluationBarSignals';
|
||||||
import type { OHLCVBar } from '../types';
|
import type { OHLCVBar } from '../types';
|
||||||
|
|
||||||
const LEFT_KEY = 'seval-left-width';
|
const LEFT_KEY = 'seval-left-width';
|
||||||
@@ -71,6 +73,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
const [appliedIndicatorParams, setAppliedIndicatorParams] = useState<EvalIndicatorParams | null>(null);
|
const [appliedIndicatorParams, setAppliedIndicatorParams] = useState<EvalIndicatorParams | null>(null);
|
||||||
const [paramsRevision, setParamsRevision] = useState(0);
|
const [paramsRevision, setParamsRevision] = useState(0);
|
||||||
const [chartLoading, setChartLoading] = useState(false);
|
const [chartLoading, setChartLoading] = useState(false);
|
||||||
|
const [backtestSignals, setBacktestSignals] = useState<BacktestSignal[]>([]);
|
||||||
const evalSessionRef = useRef(0);
|
const evalSessionRef = useRef(0);
|
||||||
const paramsRevisionRef = useRef(paramsRevision);
|
const paramsRevisionRef = useRef(paramsRevision);
|
||||||
|
|
||||||
@@ -189,6 +192,15 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
|
|
||||||
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
|
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
|
||||||
|
|
||||||
|
const barSignalHighlight = useMemo(
|
||||||
|
() => resolveBarSignalHighlight(backtestSignals, selectedBarTimeSec),
|
||||||
|
[backtestSignals, selectedBarTimeSec],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSignalsChange = useCallback((signals: BacktestSignal[]) => {
|
||||||
|
setBacktestSignals(signals);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const refreshEvaluation = useCallback(async () => {
|
const refreshEvaluation = useCallback(async () => {
|
||||||
if (!selectedStrategyId || !selectedStrategy) {
|
if (!selectedStrategyId || !selectedStrategy) {
|
||||||
setSnapshot(undefined);
|
setSnapshot(undefined);
|
||||||
@@ -410,6 +422,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
getParamsOverride={getEvalParams}
|
getParamsOverride={getEvalParams}
|
||||||
getVisualConfigOverride={getVisualConfig}
|
getVisualConfigOverride={getVisualConfig}
|
||||||
evalLoading={evalLoading}
|
evalLoading={evalLoading}
|
||||||
|
onSignalsChange={handleSignalsChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -443,9 +456,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
<span className="seval-eval-loading-text">전략 조건 확인 중…</span>
|
<span className="seval-eval-loading-text">전략 조건 확인 중…</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<VirtualTargetSignalPanel
|
<StrategyEvaluationSignalPanel
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
viewMode="summary"
|
barSignalHighlight={barSignalHighlight}
|
||||||
loading={evalLoading}
|
loading={evalLoading}
|
||||||
loadingMessage="전략 조건 확인 중…"
|
loadingMessage="전략 조건 확인 중…"
|
||||||
hasStrategy={selectedStrategyId != null}
|
hasStrategy={selectedStrategyId != null}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ interface Props {
|
|||||||
};
|
};
|
||||||
/** 선택 봉 조건 평가 진행 중 */
|
/** 선택 봉 조건 평가 진행 중 */
|
||||||
evalLoading?: boolean;
|
evalLoading?: boolean;
|
||||||
|
/** 백테스트 시그널 목록 변경 시 (우측 패널 하이라이트 등) */
|
||||||
|
onSignalsChange?: (signals: BacktestSignal[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
|
const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true;
|
||||||
@@ -89,6 +91,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
getParamsOverride,
|
getParamsOverride,
|
||||||
getVisualConfigOverride,
|
getVisualConfigOverride,
|
||||||
evalLoading = false,
|
evalLoading = false,
|
||||||
|
onSignalsChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams: baseGetParams, getVisualConfig: baseGetVisual } = useIndicatorSettings();
|
const { getParams: baseGetParams, getVisualConfig: baseGetVisual } = useIndicatorSettings();
|
||||||
const getParams = getParamsOverride ?? baseGetParams;
|
const getParams = getParamsOverride ?? baseGetParams;
|
||||||
@@ -217,6 +220,10 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
applyMarkers();
|
applyMarkers();
|
||||||
}, [signals, applyMarkers]);
|
}, [signals, applyMarkers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSignalsChange?.(signals);
|
||||||
|
}, [signals, onSignalsChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChartLoadingChange?.(loading);
|
onChartLoadingChange?.(loading);
|
||||||
}, [loading, onChartLoadingChange]);
|
}, [loading, onChartLoadingChange]);
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
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 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 matchRate = useMemo(() => computeMatchRate(metrics), [metrics]);
|
||||||
|
const trafficState = useMemo(() => getTrafficLightState(matchRate, metrics), [matchRate, metrics]);
|
||||||
|
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>
|
||||||
|
<div className="seval-side-card-head-signal">
|
||||||
|
<VirtualSignalTrafficLight state={trafficState} matchRate={matchRate} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="seval-side-card-body">
|
||||||
|
<VirtualConditionList metrics={metrics} />
|
||||||
|
</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;
|
||||||
@@ -33,6 +33,10 @@ export interface VirtualIndicatorSnapshot {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
/** 백엔드 Ta4j 집계 일치율 (0~100) */
|
/** 백엔드 Ta4j 집계 일치율 (0~100) */
|
||||||
matchRate?: number;
|
matchRate?: number;
|
||||||
|
/** 매수 DSL 전체 Rule 충족 (AND/OR 반영) */
|
||||||
|
overallEntryMet?: boolean | null;
|
||||||
|
/** 매도 DSL 전체 Rule 충족 (AND/OR 반영) */
|
||||||
|
overallExitMet?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VirtualIndicatorSnapshotsState {
|
export interface VirtualIndicatorSnapshotsState {
|
||||||
|
|||||||
@@ -378,7 +378,9 @@
|
|||||||
.seval-signal-panel {
|
.seval-signal-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +424,168 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 매수/매도 분리 시그널 패널 ─────────────────────────────────────────── */
|
||||||
|
.seval-signal-split {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--se-gold, #e6c200) 32%, var(--se-border, var(--btd-divider)));
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--vtd-sig-panel-bg, color-mix(in srgb, #0d111f 60%, transparent));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card--buy {
|
||||||
|
border-top-color: color-mix(in srgb, #3fb950 55%, var(--se-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card--sell {
|
||||||
|
border-top-color: color-mix(in srgb, #f85149 55%, var(--se-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px 6px;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--se-border) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-badge {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-badge--buy {
|
||||||
|
color: #3fb950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-badge--sell {
|
||||||
|
color: #f85149;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-overall {
|
||||||
|
display: inline-flex;
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-overall--met {
|
||||||
|
color: #3fb950;
|
||||||
|
background: color-mix(in srgb, #3fb950 14%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, #3fb950 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-overall--unmet {
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
background: color-mix(in srgb, var(--se-text-muted) 10%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--se-text-muted) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head-signal {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head-signal .vtd-sig-light--card-right {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head-signal .vtd-sig-light-housing {
|
||||||
|
transform: scale(0.82);
|
||||||
|
transform-origin: top right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head-signal .vtd-sig-light-info--below {
|
||||||
|
align-items: flex-end;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head-signal .vtd-sig-light-rate {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-head-signal .vtd-sig-light-label {
|
||||||
|
font-size: 0.58rem;
|
||||||
|
max-width: 72px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 4px 8px 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-body .vtd-heat-list {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card-body .vtd-heat-list-empty {
|
||||||
|
padding: 12px 4px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 선택 봉에 백테스트 매매 시그널이 있을 때 — 형광연두 그라데이션 아웃라인·음영 */
|
||||||
|
.seval-side-card--signal-active {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
160deg,
|
||||||
|
color-mix(in srgb, #ccff00 14%, var(--vtd-sig-panel-bg, color-mix(in srgb, #0d111f 60%, transparent))),
|
||||||
|
var(--vtd-sig-panel-bg, color-mix(in srgb, #0d111f 60%, transparent))
|
||||||
|
) padding-box,
|
||||||
|
linear-gradient(145deg, #e8ff00 0%, #39ff14 38%, #c6ff00 62%, #7fff00 100%) border-box;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, #ccff00 30%, transparent),
|
||||||
|
0 0 14px color-mix(in srgb, #ccff00 42%, transparent),
|
||||||
|
0 0 28px color-mix(in srgb, #39ff14 22%, transparent),
|
||||||
|
inset 0 0 32px color-mix(in srgb, #ccff00 11%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card--signal-active.seval-side-card--buy,
|
||||||
|
.seval-side-card--signal-active.seval-side-card--sell {
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card--signal-active .seval-side-card-head {
|
||||||
|
border-bottom-color: color-mix(in srgb, #ccff00 28%, var(--se-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-side-card--signal-active .seval-side-card-badge::after {
|
||||||
|
content: ' · 시그널';
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ccff00;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 전략설정 탭 ─────────────────────────────────────────────────────────── */
|
/* ── 전략설정 탭 ─────────────────────────────────────────────────────────── */
|
||||||
.seval-settings-tab {
|
.seval-settings-tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { BacktestSignal } from './backendApi';
|
||||||
|
|
||||||
|
export interface BarSignalHighlight {
|
||||||
|
buy: boolean;
|
||||||
|
sell: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBuySignalType = (type: BacktestSignal['type']): boolean =>
|
||||||
|
type === 'BUY' || type === 'SHORT_EXIT';
|
||||||
|
|
||||||
|
const isSellSignalType = (type: BacktestSignal['type']): boolean =>
|
||||||
|
type === 'SELL' || type === 'SHORT_ENTRY' || type === 'PARTIAL_SELL';
|
||||||
|
|
||||||
|
/** 선택 봉 시각에 백테스트 매매 시그널이 있는지 (차트 마커와 동일 기준) */
|
||||||
|
export function resolveBarSignalHighlight(
|
||||||
|
signals: BacktestSignal[],
|
||||||
|
barTimeSec: number | null | undefined,
|
||||||
|
): BarSignalHighlight {
|
||||||
|
if (barTimeSec == null || signals.length === 0) {
|
||||||
|
return { buy: false, sell: false };
|
||||||
|
}
|
||||||
|
const atBar = signals.filter(s => s.time === barTimeSec);
|
||||||
|
return {
|
||||||
|
buy: atBar.some(s => isBuySignalType(s.type)),
|
||||||
|
sell: atBar.some(s => isSellSignalType(s.type)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -51,6 +51,8 @@ export async function fetchEvaluationSnapshotAtBar(
|
|||||||
rows: baseRows.map(r => normalizeConditionRow({ ...r, currentValue: null, satisfied: null })),
|
rows: baseRows.map(r => normalizeConditionRow({ ...r, currentValue: null, satisfied: null })),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
matchRate: 0,
|
matchRate: 0,
|
||||||
|
overallEntryMet: null,
|
||||||
|
overallExitMet: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,5 +64,7 @@ export async function fetchEvaluationSnapshotAtBar(
|
|||||||
rows,
|
rows,
|
||||||
updatedAt: status.updatedAt || Date.now(),
|
updatedAt: status.updatedAt || Date.now(),
|
||||||
matchRate: coerceFiniteNumber(status.matchRate) ?? 0,
|
matchRate: coerceFiniteNumber(status.matchRate) ?? 0,
|
||||||
|
overallEntryMet: status.overallEntryMet ?? null,
|
||||||
|
overallExitMet: status.overallExitMet ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user