/** 상승추세 검색그룹 — 5개 평가항목 배점 (합계 100 고정) */ export type BullishWeightKey = | 'weightMaAlignment' | 'weightMaSlope' | 'weightAdxTrend' | 'weightMacdMomentum' | 'weightPricePosition'; export interface BullishWeightItem { key: BullishWeightKey; label: string; desc: string; } export const BULLISH_WEIGHT_ITEMS: BullishWeightItem[] = [ { key: 'weightMaAlignment', label: '이평선 정배열', desc: 'EMA(20) > EMA(60) > EMA(120)', }, { key: 'weightMaSlope', label: '이평선 기울기', desc: 'EMA(20) 현재 > 5봉 전', }, { key: 'weightAdxTrend', label: '추세강도 (ADX)', desc: '+DI > -DI · ADX ≥ 20', }, { key: 'weightMacdMomentum', label: '모멘텀 (MACD)', desc: 'MACD > Signal · MACD > 0', }, { key: 'weightPricePosition', label: '가격위치', desc: '종가 > EMA(20)', }, ]; export const DEFAULT_BULLISH_WEIGHTS: Record = { weightMaAlignment: 30, weightMaSlope: 15, weightAdxTrend: 25, weightMacdMomentum: 15, weightPricePosition: 15, }; export function bullishWeightsFromRequest( req: Pick, ): number[] { return BULLISH_WEIGHT_ITEMS.map(item => req[item.key] ?? DEFAULT_BULLISH_WEIGHTS[item.key]); } export const WEIGHT_STEP = 5; export function snapToWeightStep(value: number): number { return Math.max(0, Math.min(100, Math.round(value / WEIGHT_STEP) * WEIGHT_STEP)); } /** range 슬라이더 트랙 클릭 위치 → 5점 단위 배점 */ export function weightFromSliderPointer(clientX: number, rect: DOMRect): number { const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); return snapToWeightStep(ratio * 100); } export function sumWeights(weights: number[]): number { return weights.reduce((a, b) => a + b, 0); } /** * 한 항목 배점 변경 시 나머지 항목에 차이를 균등 분배해 합계 100 유지. */ export function adjustBullishWeight( current: Record, changedKey: BullishWeightKey, newValue: number, ): Record { const keys = BULLISH_WEIGHT_ITEMS.map(i => i.key); const idx = keys.indexOf(changedKey); if (idx < 0) return current; const clamped = snapToWeightStep(newValue); const oldValue = snapToWeightStep(current[changedKey]); if (clamped === oldValue) return current; const result: Record = { ...current }; for (const k of keys) result[k] = snapToWeightStep(result[k]); result[changedKey] = clamped; const delta = clamped - oldValue; const otherKeys = keys.filter(k => k !== changedKey); const distribute = (amount: number, mode: 'take' | 'give') => { let remaining = amount; let idx = 0; let guard = 0; while (remaining > 0 && guard < 200) { const k = otherKeys[idx % otherKeys.length]; if (mode === 'take') { const take = Math.min(WEIGHT_STEP, remaining, result[k]); if (take > 0) { result[k] -= take; remaining -= take; } } else { const give = Math.min(WEIGHT_STEP, remaining, 100 - result[k]); if (give > 0) { result[k] += give; remaining -= give; } } idx += 1; guard += 1; } }; if (delta > 0) distribute(delta, 'take'); else if (delta < 0) distribute(-delta, 'give'); const total = keys.reduce((s, k) => s + result[k], 0); if (total !== 100) { const fixKey = otherKeys[0] ?? changedKey; result[fixKey] = snapToWeightStep(Math.max(0, Math.min(100, result[fixKey] + (100 - total)))); } return result; }