Files
goldenChart/frontend/src/utils/indicatorPaneMerge.ts
T

255 lines
8.0 KiB
TypeScript

import type { IndicatorConfig } from '../types';
import { enrichIndicatorConfig, getIndicatorDef } from './indicatorRegistry';
import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from './smaConfig';
import { normalizeIchimokuConfig } from './ichimokuConfig';
import { buildMainIndicatorConfig } from './indicatorMainConfig';
import type { PlotDef, HLineDef } from './indicatorRegistry';
import type { IchimokuCloudColors } from './ichimokuConfig';
/** 보조지표 인스턴스 복제 (새 id, 별도 pane) */
export function cloneIndicatorConfig(src: IndicatorConfig, newId: string): IndicatorConfig {
return enrichIndicatorConfig({
...src,
id: newId,
mergedWith: undefined,
params: { ...src.params },
plots: src.plots?.map(p => ({ ...p })),
hlines: src.hlines?.map(h => ({ ...h })),
plotVisibility: src.plotVisibility ? { ...src.plotVisibility } : undefined,
timeframeVisibility: src.timeframeVisibility
? { ...src.timeframeVisibility }
: undefined,
cloudColors: src.cloudColors ? { ...src.cloudColors } : undefined,
hlinesBackground: src.hlinesBackground
? { ...src.hlinesBackground }
: undefined,
bandBackground: src.bandBackground
? { ...src.bandBackground }
: undefined,
});
}
/** sourceId 바로 아래에 동일 설정의 보조지표 복사본 추가 */
export function duplicateIndicatorInList(
sourceId: string,
prev: IndicatorConfig[],
newId: () => string,
): IndicatorConfig[] | null {
const idx = prev.findIndex(i => i.id === sourceId);
if (idx === -1) return null;
const src = prev[idx];
const def = getIndicatorDef(src.type);
if (!def || def.overlay || def.returnsMarkers) return null;
const clone = cloneIndicatorConfig(src, newId());
const next = [...prev];
next.splice(idx + 1, 0, clone);
return next;
}
/** mergedWith 체인의 루트(공유 pane 소유) 지표 id */
export function getPaneHostId(id: string, indicators: IndicatorConfig[]): string {
const seen = new Set<string>();
let cur = id;
for (;;) {
if (seen.has(cur)) return id;
seen.add(cur);
const ind = indicators.find(i => i.id === cur);
if (!ind?.mergedWith) return cur;
cur = ind.mergedWith;
}
}
/** 같은 pane 에 묶인 지표 id 목록 (배열 순서 유지) */
export function getMergedGroupIds(hostId: string, indicators: IndicatorConfig[]): string[] {
const host = getPaneHostId(hostId, indicators);
return indicators
.filter(i => getPaneHostId(i.id, indicators) === host)
.map(i => i.id);
}
/** 드롭 대상 → 실제 pane 호스트 id */
export function resolveMergeTargetId(targetId: string, indicators: IndicatorConfig[]): string {
return getPaneHostId(targetId, indicators);
}
/** reload 시 호스트 지표를 먼저 addIndicator 하도록 정렬 */
export function sortIndicatorsForPaneLoad(inds: IndicatorConfig[]): IndicatorConfig[] {
const byId = new Map(inds.map(i => [i.id, i]));
const out: IndicatorConfig[] = [];
const added = new Set<string>();
const addOne = (ind: IndicatorConfig) => {
if (added.has(ind.id)) return;
if (ind.mergedWith) {
const host = byId.get(ind.mergedWith);
if (host) addOne(host);
}
out.push(ind);
added.add(ind.id);
};
for (const ind of inds) addOne(ind);
return out;
}
/** 보조지표 그룹(병합 pane) 통째로 순서 변경 */
export function reorderIndicatorGroup(
fromId: string,
insertBeforeId: string | null,
prev: IndicatorConfig[],
): IndicatorConfig[] {
const hostId = getPaneHostId(fromId, prev);
const groupSet = new Set(getMergedGroupIds(hostId, prev));
const groupItems = prev.filter(i => groupSet.has(i.id));
const without = prev.filter(i => !groupSet.has(i.id));
if (insertBeforeId === null) {
return [...without, ...groupItems];
}
const beforeHost = getPaneHostId(insertBeforeId, prev);
const toIdx = without.findIndex(i => i.id === beforeHost);
without.splice(toIdx === -1 ? without.length : toIdx, 0, ...groupItems);
return without;
}
/** fromId 를 intoId pane 에 병합 */
export function mergeIndicators(
fromId: string,
intoId: string,
prev: IndicatorConfig[],
): IndicatorConfig[] {
const host = resolveMergeTargetId(intoId, prev);
if (fromId === host) return prev;
if (getPaneHostId(fromId, prev) === host) return prev;
return prev.map(i => (i.id === fromId ? { ...i, mergedWith: host } : i));
}
/** 병합 pane 분리 — 호스트 그룹의 mergedWith 제거 */
export function splitIndicatorPane(
hostOrMemberId: string,
prev: IndicatorConfig[],
): IndicatorConfig[] {
const host = getPaneHostId(hostOrMemberId, prev);
return prev.map(i => {
if (getPaneHostId(i.id, prev) !== host || !i.mergedWith) return i;
const { mergedWith: _m, ...rest } = i;
return rest;
});
}
export function layoutKey(inds: IndicatorConfig[]): string {
return inds.map(i => `${i.id}>${i.mergedWith ?? ''}`).join(';');
}
type ParamRecord = Record<string, number | string | boolean>;
type GetParams = (type: string, defaults?: ParamRecord) => ParamRecord;
type GetVisual = (
type: string,
defaultPlots?: PlotDef[],
defaultHlines?: HLineDef[],
) => {
plots: PlotDef[];
hlines: HLineDef[];
cloudColors?: IchimokuCloudColors;
bandBackground?: import('../types').HlinesBackground;
};
/** 탭 전체 추가 — 기존 차트 지표를 제거하고 types 목록만 새로 구성 */
export function replaceIndicatorConfigsFromTypes(
types: string[],
getParams: GetParams,
getVisualConfig: GetVisual,
newId: () => string,
): IndicatorConfig[] {
return buildIndicatorConfigsFromTypes(types, [], getParams, getVisualConfig, newId);
}
/** DB 보조지표 설정(파라미터·색상·선굵기·유형)을 반영한 단일 IndicatorConfig */
export function buildChartIndicatorConfig(
type: string,
id: string,
getParams: GetParams,
getVisualConfig: GetVisual,
extras: Partial<IndicatorConfig> = {},
): IndicatorConfig | null {
const def = getIndicatorDef(type);
if (!def) return null;
const params = getParams(type, def.defaultParams as ParamRecord);
const { plots, hlines, cloudColors, bandBackground } = getVisualConfig(
type,
def.plots,
def.hlines ?? [],
);
let cfg: IndicatorConfig = {
id,
type: def.type,
params,
plots,
hlines,
...(cloudColors ? { cloudColors } : {}),
...(bandBackground ? { bandBackground } : {}),
...extras,
};
if (type === 'SMA') {
const mainSma = buildMainIndicatorConfig(type, [], getParams, getVisualConfig);
if (mainSma?.plotVisibility) {
cfg.plotVisibility = mainSma.plotVisibility;
}
if (mainSma?.plots?.length) {
cfg.plots = mainSma.plots;
}
cfg = normalizeSmaConfig({
...cfg,
plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(),
});
} else if (type === 'IchimokuCloud') {
cfg = normalizeIchimokuConfig(cfg);
}
return enrichIndicatorConfig(cfg);
}
/** 지표 타입 목록 → 차트에 추가할 IndicatorConfig 배열 (이미 활성인 type 제외) */
export function buildIndicatorConfigsFromTypes(
types: string[],
existing: IndicatorConfig[],
getParams: GetParams,
getVisualConfig: GetVisual,
newId: () => string,
): IndicatorConfig[] {
const seen = new Set(existing.map(i => i.type));
const out: IndicatorConfig[] = [];
for (const type of types) {
if (seen.has(type)) continue;
seen.add(type);
const cfg = buildChartIndicatorConfig(type, newId(), getParams, getVisualConfig);
if (cfg) out.push(cfg);
}
return out;
}
/** id 제거 — 병합 호스트 제거 시 해당 pane 멤버도 함께 제거 */
export function removeIndicatorFromList(
prev: IndicatorConfig[],
id: string,
): IndicatorConfig[] {
return prev.filter(i => i.id !== id && i.mergedWith !== id);
}
/** type 제거 — 해당 type 인스턴스 및 병합 멤버 함께 제거 */
export function removeIndicatorTypeFromList(
prev: IndicatorConfig[],
type: string,
): IndicatorConfig[] {
const removedIds = new Set(prev.filter(i => i.type === type).map(i => i.id));
return prev.filter(
i => i.type !== type && (!i.mergedWith || !removedIds.has(i.mergedWith)),
);
}