/** live-conditions 행 ↔ 전략 DSL 행 병합 (useVirtualIndicatorSnapshots 와 동일) */ import type { LiveConditionRowDto } from './backendApi'; import { formatIndicatorDisplayLabel } from './indicatorRegistry'; import { normalizeConditionRow } from './virtualSignalMetrics'; import type { VirtualConditionRow } from './virtualStrategyConditions'; function rowFallbackKey(row: Pick): string { return `${row.side}:${row.timeframe}:${row.indicatorType}:${row.conditionType}:${row.targetValue ?? ''}:${row.plotKey}`; } function liveFallbackKey(r: LiveConditionRowDto): string { const plotKey = r.plotKey ?? r.indicatorType; return `${r.side}:${r.timeframe}:${r.indicatorType}:${r.conditionType}:${r.targetValue ?? ''}:${plotKey}`; } export function mergeRows( base: VirtualConditionRow[], live: LiveConditionRowDto[], ): Array { if (live.length === 0) { return base.map(row => normalizeConditionRow({ ...row, currentValue: null, satisfied: null })); } const baseById = new Map(base.map(r => [r.id, r])); const baseByKey = new Map(base.map(r => [rowFallbackKey(r), r])); const matchedBaseIds = new Set(); const merged = live.map(lr => { const baseRow = baseById.get(lr.id) ?? baseByKey.get(liveFallbackKey(lr)); if (baseRow) matchedBaseIds.add(baseRow.id); const plotKey = lr.plotKey ?? lr.indicatorType; return normalizeConditionRow({ id: lr.id, indicatorType: lr.indicatorType, displayName: baseRow?.displayName ?? formatIndicatorDisplayLabel(lr.indicatorType), conditionType: lr.conditionType, conditionLabel: lr.conditionLabel ?? baseRow?.conditionLabel ?? lr.conditionType, targetValue: lr.targetValue ?? baseRow?.targetValue ?? null, timeframe: lr.timeframe, side: lr.side as 'buy' | 'sell', plotKey, satisfied: lr.satisfied, thresholdLabel: lr.thresholdLabel ?? baseRow?.thresholdLabel, currentValue: lr.currentValue, }); }); for (const row of base) { if (matchedBaseIds.has(row.id)) continue; const hasLive = live.some( lr => lr.id === row.id || liveFallbackKey(lr) === rowFallbackKey(row), ); if (!hasLive) { merged.push(normalizeConditionRow({ ...row, currentValue: null, satisfied: null })); } } return merged; }