248 lines
8.0 KiB
TypeScript
248 lines
8.0 KiB
TypeScript
/** 전략편집기 우측 팔레트 — 보조·복합 지표 목록 (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 {
|
|
ICHIMOKU_BB_BUY_VALUE,
|
|
ICHIMOKU_BB_SELL_VALUE,
|
|
ichimokuBbPaletteDesc,
|
|
} from './ichimokuBbStrategy';
|
|
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<PaletteItem, 'id' | 'kind'>[] = [
|
|
{ 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<PaletteItem, 'id' | 'kind'>[] = [
|
|
{
|
|
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,
|
|
},
|
|
{
|
|
value: ICHIMOKU_BB_BUY_VALUE,
|
|
label: '일목×BB 매수',
|
|
desc: ichimokuBbPaletteDesc(ICHIMOKU_BB_BUY_VALUE),
|
|
builtIn: true,
|
|
period: 26,
|
|
},
|
|
{
|
|
value: ICHIMOKU_BB_SELL_VALUE,
|
|
label: '일목×BB 매도',
|
|
desc: ichimokuBbPaletteDesc(ICHIMOKU_BB_SELL_VALUE),
|
|
builtIn: true,
|
|
period: 26,
|
|
},
|
|
...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, 'id' | 'kind'>[],
|
|
): 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, 'id' | 'kind'>[],
|
|
): 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<string, string> = {};
|
|
|
|
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)}`;
|
|
}
|