/** 보조지표 차트 hline 역할 — DSL에 숫자(K_70) 대신 저장해 DB 설정 변경 시 자동 반영 */ export const THRESHOLD_HL_OVER = 'HL_OVER'; export const THRESHOLD_HL_MID = 'HL_MID'; export const THRESHOLD_HL_UNDER = 'HL_UNDER'; export type ThresholdHlineRole = typeof THRESHOLD_HL_OVER | typeof THRESHOLD_HL_MID | typeof THRESHOLD_HL_UNDER; export function isThresholdSymbol(field?: string): boolean { return field === THRESHOLD_HL_OVER || field === THRESHOLD_HL_MID || field === THRESHOLD_HL_UNDER; } export function isThresholdFieldOrSymbol(field?: string): boolean { return !!field && (field.startsWith('K_') || isThresholdSymbol(field)); } type HlThresh = { over?: number; mid?: number; under?: number }; export function resolveThresholdFromDef( field: string | undefined, indicatorType: string, hlThresh: Record, ): number | null { if (!field) return null; const th = hlThresh[indicatorType] ?? {}; if (field === THRESHOLD_HL_OVER) return th.over ?? null; if (field === THRESHOLD_HL_MID) return th.mid ?? null; if (field === THRESHOLD_HL_UNDER) return th.under ?? null; if (field.startsWith('K_')) { const n = parseFloat(field.slice(2)); return Number.isFinite(n) ? n : null; } return null; } /** 레거시 K_ 값 → hline 역할 심볼 (정확 일치 우선, 없으면 가장 가까운 역할) */ export function inferThresholdSymbolFromLegacy( field: string | undefined, indicatorType: string, hlThresh: Record, ): string | undefined { if (!field?.startsWith('K_')) return field; const val = parseFloat(field.slice(2)); if (!Number.isFinite(val)) return field; const th = hlThresh[indicatorType] ?? {}; const eps = 0.0001; if (th.over != null && Math.abs(th.over - val) < eps) return THRESHOLD_HL_OVER; if (th.mid != null && Math.abs(th.mid - val) < eps) return THRESHOLD_HL_MID; if (th.under != null && Math.abs(th.under - val) < eps) return THRESHOLD_HL_UNDER; const candidates: { role: ThresholdHlineRole; v: number }[] = []; if (th.over != null) candidates.push({ role: THRESHOLD_HL_OVER, v: th.over }); if (th.mid != null) candidates.push({ role: THRESHOLD_HL_MID, v: th.mid }); if (th.under != null) candidates.push({ role: THRESHOLD_HL_UNDER, v: th.under }); if (candidates.length === 0) return defaultInheritedThresholdField(indicatorType); let best = candidates[0]; let bestDiff = Math.abs(best.v - val); for (const c of candidates.slice(1)) { const d = Math.abs(c.v - val); if (d < bestDiff) { best = c; bestDiff = d; } } return best.role; } /** 상속 모드(thresholdOverride=false) — K_ 스냅샷을 hline 역할 심볼로 정규화 */ export function resolveInheritedThresholdField( field: string | undefined, indicatorType: string, hlThresh: Record, ): string | undefined { if (!field) return field; if (isThresholdSymbol(field)) return field; if (field.startsWith('K_')) { return inferThresholdSymbolFromLegacy(field, indicatorType, hlThresh) ?? defaultInheritedThresholdField(indicatorType); } return field; } /** 상속 모드 기본 rightField — 과열선 역할 */ export function defaultInheritedThresholdField(indicatorType: string): string { switch (indicatorType) { case 'DISPARITY': case 'VOLUME_OSC': case 'TRIX': return THRESHOLD_HL_MID; default: return THRESHOLD_HL_OVER; } }