/** 전략편집기 우측 팔레트 — 보조·복합 지표 목록 (DB ui_preferences) */ import { getUiPreferences, patchUiPreferences } from './uiPreferencesDb'; import { COMPOSITE_INDICATOR_ITEMS } from './compositeIndicators'; import { STOCH_OVERBOUGHT_PAIR_VALUE, stochPairPaletteDesc, } from './stochOverboughtPair'; import { INFLECTION_33_BUY_VALUE, INFLECTION_33_SELL_VALUE, inflection33PaletteDesc, } from './inflection33Strategy'; import { STABLE_STRATEGY_PALETTE_ITEMS, } from './stableStrategyPairs'; import { NEW_HIGH_9_20_BUY_VALUE, NEW_LOW_9_20_SELL_VALUE, priceExtremePairPaletteDesc, } from './priceExtremeBreakoutPair'; export type PaletteItemKind = 'auxiliary' | 'composite'; export interface PaletteItem { id: string; kind: PaletteItemKind; /** 지표 타입 (RSI, CCI, …) */ value: string; label: string; desc: string; /** 단일 보조지표 기간 */ period?: number; /** 복합지표 단기·장기 기간 */ shortPeriod?: number; longPeriod?: number; /** Stoch 과열×보조 복합 — 기본 보조지표 (CCI 등) */ secondaryIndicator?: string; builtIn?: boolean; } export const DEFAULT_AUXILIARY_ITEMS: Omit[] = [ { value: 'RSI', label: 'RSI', desc: '상대강도지수', builtIn: true }, { value: 'MACD', label: 'MACD', desc: '이동평균 수렴확산', builtIn: true }, { value: 'STOCHASTIC', label: 'Stochastic', desc: '스토캐스틱', builtIn: true }, { value: 'CCI', label: 'CCI', desc: '상품채널지수', builtIn: true }, { value: 'ADX', label: 'ADX', desc: '평균방향지수', builtIn: true }, { value: 'DMI', label: 'DMI', desc: '방향성 지표', builtIn: true }, { value: 'OBV', label: 'OBV', desc: '거래량 균형', builtIn: true }, { value: 'WILLIAMS_R', label: 'Williams %R', desc: '윌리엄스 %R', builtIn: true }, { value: 'TRIX', label: 'TRIX', desc: '삼중지수이동평균', builtIn: true }, { value: 'VOLUME_OSC', label: 'Volume OSC', desc: '거래량 오실레이터', builtIn: true }, { value: 'VR', label: 'VR', desc: '거래량 비율', builtIn: true }, { value: 'DISPARITY', label: '이격도', desc: 'Disparity', builtIn: true }, { value: 'PSYCHOLOGICAL', label: '심리도', desc: 'Psychological', builtIn: true }, { value: 'NEW_PSYCHOLOGICAL', label: '신심리도', desc: 'New Psychological', builtIn: true }, { value: 'INVEST_PSYCHOLOGICAL', label: '투자심리도', desc: 'Invest PSY', builtIn: true }, { value: 'VOLUME', label: '거래량', desc: 'Volume', builtIn: true }, { value: 'NEW_HIGH', label: '신고가', desc: '직전 N봉 최고가 돌파 — 종가/고가 vs N일 신고가선 (9·20일)', builtIn: true, period: 9 }, { value: 'NEW_LOW', label: '신저가', desc: '직전 N봉 최저가 이탈 — 종가/저가 vs N일 신저가선 (9·20일)', builtIn: true, period: 9 }, ]; export const DEFAULT_COMPOSITE_ITEMS: Omit[] = [ { value: STOCH_OVERBOUGHT_PAIR_VALUE, label: 'Stoch 과열×보조', desc: stochPairPaletteDesc('CCI'), builtIn: true, secondaryIndicator: 'CCI', }, { value: NEW_HIGH_9_20_BUY_VALUE, label: '9·20일 신고가 매수', desc: priceExtremePairPaletteDesc(NEW_HIGH_9_20_BUY_VALUE), builtIn: true, shortPeriod: 9, longPeriod: 20, }, { value: NEW_LOW_9_20_SELL_VALUE, label: '9·20일 신저가 매도', desc: priceExtremePairPaletteDesc(NEW_LOW_9_20_SELL_VALUE), builtIn: true, shortPeriod: 9, longPeriod: 20, }, { value: INFLECTION_33_BUY_VALUE, label: '33변곡 매수', desc: inflection33PaletteDesc(INFLECTION_33_BUY_VALUE), builtIn: true, period: 33, }, { value: INFLECTION_33_SELL_VALUE, label: '33변곡 매도', desc: inflection33PaletteDesc(INFLECTION_33_SELL_VALUE), builtIn: true, period: 33, }, ...STABLE_STRATEGY_PALETTE_ITEMS.map(i => ({ value: i.value, label: i.label, desc: i.desc, builtIn: true, })), ...COMPOSITE_INDICATOR_ITEMS.map(i => ({ value: i.value, label: i.label, desc: i.desc, builtIn: true, })), ]; export const AUXILIARY_TYPE_OPTIONS = DEFAULT_AUXILIARY_ITEMS.map(i => ({ value: i.value, label: i.label, })); /** 팔레트 추가 모달 — 동일 지표 2기간 교차만 선택 (Stoch×보조는 내장 칩) */ export const COMPOSITE_TYPE_OPTIONS = COMPOSITE_INDICATOR_ITEMS.map(i => ({ value: i.value, label: i.label, })); function builtinId(kind: PaletteItemKind, value: string): string { return `builtin-${kind}-${value}`; } function seedItems( kind: PaletteItemKind, defaults: Omit[], ): PaletteItem[] { return defaults.map(d => ({ ...d, id: builtinId(kind, d.value), kind, })); } function parseStored(raw: string | null): PaletteItem[] | null { if (!raw) return null; try { const arr = JSON.parse(raw) as PaletteItem[]; if (!Array.isArray(arr)) return null; return arr.filter( i => i && typeof i.id === 'string' && typeof i.value === 'string' && i.label, ); } catch { return null; } } /** 복합지표 팔레트에서 단일 지표로 이전된 항목 제거 */ const DEPRECATED_COMPOSITE_VALUES = new Set(['NEW_HIGH', 'NEW_LOW']); function mergeMissingBuiltIns( stored: PaletteItem[], kind: PaletteItemKind, defaults: Omit[], ): PaletteItem[] { let base = stored; if (kind === 'composite') { base = stored.filter(i => !DEPRECATED_COMPOSITE_VALUES.has(i.value)); } const existing = new Set(base.map(i => i.value)); const added = defaults .filter(d => !existing.has(d.value)) .map(d => ({ ...d, id: builtinId(kind, d.value), kind, })); return added.length > 0 ? [...base, ...added] : base; } export function loadPaletteItems(kind: PaletteItemKind): PaletteItem[] { const defaults = kind === 'auxiliary' ? DEFAULT_AUXILIARY_ITEMS : DEFAULT_COMPOSITE_ITEMS; const prefs = getUiPreferences().strategyEditor; const raw = kind === 'auxiliary' ? prefs?.paletteAuxiliary : prefs?.paletteComposite; const stored = raw?.length ? raw : null; if (stored && stored.length > 0) { return mergeMissingBuiltIns( stored.map(i => ({ ...i, kind, ...(i.builtIn ? { period: undefined, shortPeriod: undefined, longPeriod: undefined } : {}), })), kind, defaults, ); } return seedItems(kind, defaults); } export function savePaletteItems(kind: PaletteItemKind, items: PaletteItem[]): void { if (kind === 'auxiliary') { patchUiPreferences({ strategyEditor: { paletteAuxiliary: items } }); } else { patchUiPreferences({ strategyEditor: { paletteComposite: items } }); } } export function resetPaletteItems(kind: PaletteItemKind): PaletteItem[] { const defaults = kind === 'auxiliary' ? DEFAULT_AUXILIARY_ITEMS : DEFAULT_COMPOSITE_ITEMS; const items = seedItems(kind, defaults); savePaletteItems(kind, items); return items; } /** 전략편집기·전략빌더에 표시할 지표명 (우측 팔레트 label 우선) */ const STRATEGY_INDICATOR_TYPE_ALIASES: Record = {}; export function getStrategyIndicatorDisplayName(indicatorType: string): string { const resolved = STRATEGY_INDICATOR_TYPE_ALIASES[indicatorType] ?? indicatorType; for (const kind of ['auxiliary', 'composite'] as const) { const item = loadPaletteItems(kind).find(i => i.value === resolved); if (item?.label) return item.label; } return resolved; } export function createPaletteItemId(): string { return `custom-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; }