252 lines
7.1 KiB
TypeScript
252 lines
7.1 KiB
TypeScript
import { SMA_MA_COUNT, smaPlotId, formatSmaMaDisplayLabel } from './smaConfig';
|
|
|
|
/** Custom / Custom1 — 동일 기능, 설정만 분리 (세션만, DB 미저장) */
|
|
export type ChartCustomOverlaySlotId = 'custom' | 'custom1';
|
|
|
|
export const CHART_CUSTOM_OVERLAY_SLOTS: ChartCustomOverlaySlotId[] = ['custom', 'custom1'];
|
|
|
|
/** Custom 적용 시 표시할 항목 (세션만, DB 미저장) */
|
|
export type ChartCustomOverlaySelection = {
|
|
smaPlots: Record<string, boolean>;
|
|
bollinger: {
|
|
basis: boolean;
|
|
upper: boolean;
|
|
lower: boolean;
|
|
background: boolean;
|
|
};
|
|
ichimoku: {
|
|
conversion: boolean;
|
|
base: boolean;
|
|
lagging: boolean;
|
|
spanA: boolean;
|
|
spanB: boolean;
|
|
bullishCloud: boolean;
|
|
bearishCloud: boolean;
|
|
};
|
|
candle: boolean;
|
|
};
|
|
|
|
export const BB_CUSTOM_PLOT_IDS = ['plot0', 'plot1', 'plot2'] as const;
|
|
export const BB_CUSTOM_LABELS: Record<(typeof BB_CUSTOM_PLOT_IDS)[number], string> = {
|
|
plot0: '중앙값',
|
|
plot1: '어퍼',
|
|
plot2: '로우어',
|
|
};
|
|
|
|
export function createDefaultChartCustomOverlaySelection(): ChartCustomOverlaySelection {
|
|
const smaPlots: Record<string, boolean> = {};
|
|
for (let i = 0; i < SMA_MA_COUNT; i++) {
|
|
smaPlots[smaPlotId(i)] = true;
|
|
}
|
|
return {
|
|
smaPlots,
|
|
bollinger: {
|
|
basis: true,
|
|
upper: true,
|
|
lower: true,
|
|
background: true,
|
|
},
|
|
ichimoku: {
|
|
conversion: true,
|
|
base: true,
|
|
lagging: true,
|
|
spanA: true,
|
|
spanB: true,
|
|
bullishCloud: true,
|
|
bearishCloud: true,
|
|
},
|
|
candle: true,
|
|
};
|
|
}
|
|
|
|
export function cloneChartCustomOverlaySelection(
|
|
sel: ChartCustomOverlaySelection,
|
|
): ChartCustomOverlaySelection {
|
|
return {
|
|
smaPlots: { ...sel.smaPlots },
|
|
bollinger: { ...sel.bollinger },
|
|
ichimoku: { ...sel.ichimoku },
|
|
candle: sel.candle,
|
|
};
|
|
}
|
|
|
|
export function smaMaLabel(
|
|
plotIndex: number,
|
|
params?: Record<string, number | string | boolean>,
|
|
): string {
|
|
return formatSmaMaDisplayLabel(params, plotIndex + 1);
|
|
}
|
|
|
|
export const ICHIMOKU_CUSTOM_ROWS: Array<{
|
|
key: keyof ChartCustomOverlaySelection['ichimoku'];
|
|
plotId?: string;
|
|
label: string;
|
|
}> = [
|
|
{ key: 'conversion', plotId: 'plot0', label: '전환선' },
|
|
{ key: 'base', plotId: 'plot1', label: '기준선' },
|
|
{ key: 'lagging', plotId: 'plot2', label: '후행스팬' },
|
|
{ key: 'spanA', plotId: 'plot3', label: '선행스팬1' },
|
|
{ key: 'spanB', plotId: 'plot4', label: '선행스팬2' },
|
|
{ key: 'bullishCloud', label: '상승 구름' },
|
|
{ key: 'bearishCloud', label: '하락 구름' },
|
|
];
|
|
|
|
/** Custom 적용 시 plot 단위로 표시를 제어하는 지표 type */
|
|
export const CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES = [
|
|
'SMA',
|
|
'BollingerBands',
|
|
'IchimokuCloud',
|
|
] as const;
|
|
|
|
export function isCustomOverlayPlotControlledType(type: string): boolean {
|
|
return (CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES as readonly string[]).includes(type);
|
|
}
|
|
|
|
/** 지표 설정 plotVisibility 와 무관 — custom 적용 시 이 맵만 사용 */
|
|
export function isCustomPlotSelected(
|
|
sel: ChartCustomOverlaySelection,
|
|
type: string,
|
|
plotId: string,
|
|
): boolean {
|
|
if (type === 'SMA') {
|
|
return sel.smaPlots[plotId] === true;
|
|
}
|
|
if (type === 'BollingerBands') {
|
|
if (plotId === 'plot0') return sel.bollinger.basis;
|
|
if (plotId === 'plot1') return sel.bollinger.upper;
|
|
if (plotId === 'plot2') return sel.bollinger.lower;
|
|
return false;
|
|
}
|
|
if (type === 'IchimokuCloud') {
|
|
const row = ICHIMOKU_CUSTOM_ROWS.find(r => r.plotId === plotId);
|
|
if (!row) return false;
|
|
return sel.ichimoku[row.key] === true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function isCustomIchimokuCloudVisible(
|
|
sel: ChartCustomOverlaySelection,
|
|
which: 'bullish' | 'bearish',
|
|
): boolean {
|
|
return which === 'bullish'
|
|
? sel.ichimoku.bullishCloud
|
|
: sel.ichimoku.bearishCloud;
|
|
}
|
|
|
|
export type CustomOverlaySectionId = 'sma' | 'bollinger' | 'ichimoku' | 'candle';
|
|
|
|
export function isCustomSectionAllSelected(
|
|
sel: ChartCustomOverlaySelection,
|
|
section: CustomOverlaySectionId,
|
|
): boolean {
|
|
switch (section) {
|
|
case 'sma':
|
|
return Array.from({ length: SMA_MA_COUNT }, (_, i) =>
|
|
sel.smaPlots[smaPlotId(i)] !== false,
|
|
).every(Boolean);
|
|
case 'bollinger':
|
|
return sel.bollinger.basis
|
|
&& sel.bollinger.upper
|
|
&& sel.bollinger.lower
|
|
&& sel.bollinger.background;
|
|
case 'ichimoku':
|
|
return ICHIMOKU_CUSTOM_ROWS.every(r => sel.ichimoku[r.key]);
|
|
case 'candle':
|
|
return sel.candle;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function setCustomSectionAllSelected(
|
|
sel: ChartCustomOverlaySelection,
|
|
section: CustomOverlaySectionId,
|
|
on: boolean,
|
|
): void {
|
|
switch (section) {
|
|
case 'sma':
|
|
for (let i = 0; i < SMA_MA_COUNT; i++) {
|
|
sel.smaPlots[smaPlotId(i)] = on;
|
|
}
|
|
break;
|
|
case 'bollinger':
|
|
sel.bollinger.basis = on;
|
|
sel.bollinger.upper = on;
|
|
sel.bollinger.lower = on;
|
|
sel.bollinger.background = on;
|
|
break;
|
|
case 'ichimoku':
|
|
for (const row of ICHIMOKU_CUSTOM_ROWS) {
|
|
sel.ichimoku[row.key] = on;
|
|
}
|
|
break;
|
|
case 'candle':
|
|
sel.candle = on;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
export type ChartCustomOverlaySlotState = {
|
|
active: boolean;
|
|
selection: ChartCustomOverlaySelection;
|
|
};
|
|
|
|
/** UI·Manager 공통: 한 번에 하나의 슬롯만 적용 (다른 슬롯 적용 시 기존 슬롯 자동 해제) */
|
|
export function isCustomOverlaySlotActive(
|
|
activeSlot: ChartCustomOverlaySlotId | null,
|
|
slot: ChartCustomOverlaySlotId,
|
|
): boolean {
|
|
return activeSlot === slot;
|
|
}
|
|
|
|
export function resolveActiveCustomOverlaySlot(
|
|
slot: ChartCustomOverlaySlotId,
|
|
turnOn: boolean,
|
|
current: ChartCustomOverlaySlotId | null,
|
|
): ChartCustomOverlaySlotId | null {
|
|
if (turnOn) return slot;
|
|
return current === slot ? null : current;
|
|
}
|
|
|
|
type CustomOverlayManagerApi = {
|
|
setChartCustomOverlaySelection(
|
|
selection: ChartCustomOverlaySelection,
|
|
slot: ChartCustomOverlaySlotId,
|
|
): void;
|
|
setChartCustomOverlayActive(active: boolean, slot: ChartCustomOverlaySlotId): void;
|
|
};
|
|
|
|
/** React → ChartManager: 두 슬롯 선택값 동기화 후 적용 중인 슬롯만 활성화 */
|
|
export function syncChartManagerCustomOverlays(
|
|
mgr: CustomOverlayManagerApi,
|
|
custom: ChartCustomOverlaySlotState,
|
|
custom1: ChartCustomOverlaySlotState,
|
|
): void {
|
|
mgr.setChartCustomOverlaySelection(custom.selection, 'custom');
|
|
mgr.setChartCustomOverlaySelection(custom1.selection, 'custom1');
|
|
mgr.setChartCustomOverlayActive(false, 'custom');
|
|
mgr.setChartCustomOverlayActive(false, 'custom1');
|
|
if (custom1.active) {
|
|
mgr.setChartCustomOverlayActive(true, 'custom1');
|
|
} else if (custom.active) {
|
|
mgr.setChartCustomOverlayActive(true, 'custom');
|
|
}
|
|
}
|
|
|
|
/** 적용 슬롯 하나만 — Custom 적용 중 Custom1 적용 시 Custom 자동 미적용 */
|
|
export function syncChartManagerCustomOverlaysByActiveSlot(
|
|
mgr: CustomOverlayManagerApi,
|
|
activeSlot: ChartCustomOverlaySlotId | null,
|
|
customSelection: ChartCustomOverlaySelection,
|
|
custom1Selection: ChartCustomOverlaySelection,
|
|
): void {
|
|
syncChartManagerCustomOverlays(
|
|
mgr,
|
|
{ active: activeSlot === 'custom', selection: customSelection },
|
|
{ active: activeSlot === 'custom1', selection: custom1Selection },
|
|
);
|
|
}
|