Files
goldenChart/frontend/src/utils/indicatorSettingsLayout.ts
T
2026-05-29 23:03:47 +09:00

143 lines
5.5 KiB
TypeScript

/**
* 보조지표 설정 화면 — 플롯 행·전역 파라미터 배치 (일목/SMA 단일 화면 컨셉)
*/
import type { PlotDef } from './indicatorRegistry';
/** 플롯 행에 붙지 않고 상단에만 표시할 키 (우선 순서) */
const GLOBAL_FIRST_KEYS: string[] = [
'src', 'maType', 'mult', 'multiplier', 'percent', 'bbMult', 'offset', 'sigma',
];
const GLOBAL_FIRST_SET = new Set(GLOBAL_FIRST_KEYS);
/** 지표별 플롯 ID → 파라미터 키 (빈 배열 = 자동/공유) */
export const INDICATOR_PLOT_PARAMS: Record<string, Record<string, string[]>> = {
MACross: { plot0: ['fastLength'], plot1: ['slowLength'] },
Stochastic: { plot0: ['kLength', 'smooth'], plot1: ['dSmoothing'] },
StochRSI: { plot0: ['lengthRSI', 'lengthStoch', 'smoothK'], plot1: ['smoothD'] },
CCI: { plot0: ['length'], plot1: ['maLength'] },
RSI: { plot0: ['length'], plot1: ['maLength'] },
/** 업비트: OBV 행=이동평균 종류, 신호선 행=기간 (색상 컨트롤 좌측) */
OBV: { plot0: ['maType'], plot1: ['maLength'] },
/** 업비트: 길이·곱·오프셋은 상단 전역 행, 플롯 행은 스타일만 */
BollingerBands: { plot0: [], plot1: [], plot2: [] },
/** 업비트: DI 길이·ADX 스무딩은 상단 전역, 플롯 행은 스타일만 */
DMI: { plot0: [], plot1: [], plot2: [], plot3: [], plot4: [] },
KeltnerChannels: { plot0: ['length', 'multiplier'], plot1: [], plot2: [] },
DonchianChannels: { plot0: ['length'], plot1: [], plot2: [] },
Envelope: { plot0: ['length', 'percent'], plot1: [], plot2: [] },
RVI: { plot0: ['length'], plot1: [] },
FisherTransform: { plot0: ['length'], plot1: [] },
MACD: { plot0: ['fastLength', 'slowLength', 'signalLength'], plot1: [], plot2: [] },
TSI: { plot0: ['longLength', 'shortLength'], plot1: ['signalLength'] },
TRIX: { plot0: ['length'], plot1: ['signalLength'] },
SMIErgodic: { plot0: ['shortLength', 'longLength'], plot1: ['signalLength'] },
SMIErgodicOscillator: { plot0: ['shortLength', 'longLength'], plot1: ['signalLength'] },
UltimateOscillator: { plot0: ['length1', 'length2', 'length3'] },
ConnorsRSI: { plot0: ['rsiLength', 'upDownLength', 'rocLength'] },
Psychological: { plot0: ['length'] },
NewPsychological: { plot0: ['length'] },
InvestPsychological: { plot0: ['length'] },
Disparity: {
plot0: ['ultraLength'],
plot1: ['shortLength'],
plot2: ['midLength'],
plot3: ['longLength'],
},
IchimokuCloud: {
plot0: ['conversionPeriods'],
plot1: ['basePeriods'],
/** 후행스팬 — 과거 이동 기간 */
plot2: ['chikouDisplacement'],
/** 선행스팬1 — 미래 이동 기간 */
plot3: ['senkouDisplacement'],
/** 선행스팬2 — Donchian 기간 */
plot4: ['laggingSpan2Periods'],
},
};
const PLOT_INDEX_FALLBACK: Record<number, string[]> = {
0: [
'length', 'len', 'kLength', 'fastLength', 'length1', 'lengthRSI', 'rsiLength',
'shortLength', 'ultraLength', 'conversionPeriods', 'turbolen',
],
1: ['dSmoothing', 'slowLength', 'length2', 'smoothD', 'maLength', 'signalLength', 'midLength', 'basePeriods'],
2: ['smooth', 'length3', 'longLength', 'laggingSpan2Periods'],
3: ['longLength', 'displacement'],
4: ['displacement'],
};
function keysInParams(keys: string[], params: Record<string, unknown>): string[] {
return keys.filter(k => k in params);
}
/** 플롯 한 줄에 표시할 숫자·선택 파라미터 키 */
export function getPlotParamKeys(
indicatorType: string,
plotId: string,
plotIndex: number,
plots: PlotDef[],
params: Record<string, number | string | boolean>,
): string[] {
const mapped = INDICATOR_PLOT_PARAMS[indicatorType]?.[plotId];
if (mapped !== undefined) {
return keysInParams(mapped, params);
}
if (plots.length === 1) {
return Object.keys(params).filter(
k => !GLOBAL_FIRST_SET.has(k) && typeof params[k] === 'number',
);
}
const fallback = keysInParams(PLOT_INDEX_FALLBACK[plotIndex] ?? [], params);
if (fallback.length) return fallback;
return [];
}
/** 업비트 BB 인풋 탭 순서 */
const BOLLINGER_GLOBAL_KEYS = ['length', 'mult', 'offset'] as const;
/** 플롯에 할당되지 않은 전역 파라미터 (상단 행) */
export function getGlobalParamKeys(
indicatorType: string,
params: Record<string, number | string | boolean>,
plots: PlotDef[],
): string[] {
if (indicatorType === 'BollingerBands') {
return BOLLINGER_GLOBAL_KEYS.filter(k => k in params);
}
const assigned = new Set<string>();
for (let i = 0; i < plots.length; i++) {
for (const k of getPlotParamKeys(indicatorType, plots[i].id, i, plots, params)) {
assigned.add(k);
}
}
const remaining = Object.keys(params).filter(
k => !assigned.has(k) && k !== 'symbolMode' && k !== 'refSymbol' && k !== 'src' && k !== 'maType',
);
const ordered = GLOBAL_FIRST_KEYS.filter(k => remaining.includes(k));
const rest = remaining.filter(k => !ordered.includes(k));
return [...ordered, ...rest];
}
/** 플롯 행 입력란에 표시할 내용 없음 */
export function plotRowHasNoInputs(
indicatorType: string,
plotId: string,
plotIndex: number,
plots: PlotDef[],
params: Record<string, number | string | boolean>,
): boolean {
return getPlotParamKeys(indicatorType, plotId, plotIndex, plots, params).length === 0;
}
/** 파라미터가 상단 전역 행에만 있음 (OBV: maType·maLength) — 플롯 행 '자동' 미표시 */
export function plotParamsOnGlobalRowOnly(indicatorType: string, plotId: string): boolean {
const mapped = INDICATOR_PLOT_PARAMS[indicatorType]?.[plotId];
return mapped !== undefined && mapped.length === 0;
}