128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
/**
|
|
* 볼린저밴드 (업비트 BB 설정)
|
|
* - 길이(length) 기본 20, 곱(mult) 기본 2, 오프셋(offset) 기본 0
|
|
* - 기준선: 종가 SMA, 표준편차는 모집단(N) 기준
|
|
*/
|
|
import type { OHLCVBar, Timeframe } from '../types';
|
|
import type { PlotData } from './indicatorRegistry';
|
|
import { fetchUpbitCandles, isUpbitMarket } from './upbitApi';
|
|
|
|
export const BB_DEFAULT_LENGTH = 20;
|
|
export const BB_DEFAULT_MULT = 2;
|
|
export const BB_DEFAULT_OFFSET = 0;
|
|
|
|
/** 업비트 BB 백그라운드 그리기 기본 (반투명 파랑) */
|
|
export const DEFAULT_BB_BAND_BACKGROUND = {
|
|
visible: true,
|
|
color: '#42A5F528',
|
|
} as const;
|
|
|
|
const BB_PLOT_TITLES = ['중앙값', '어퍼', '로우어'] as const;
|
|
const BB_PLOT_IDS = ['plot0', 'plot1', 'plot2'] as const;
|
|
const BB_DEFAULT_COLORS = ['#FF9800', '#42A5F5', '#42A5F5'] as const;
|
|
|
|
export type BbSymbolMode = 'main' | 'other';
|
|
|
|
export type BbBandBackground = { visible: boolean; color: string };
|
|
|
|
export function resolveBbBandBackground(
|
|
bg?: Partial<BbBandBackground> | null,
|
|
): BbBandBackground {
|
|
return {
|
|
visible: bg?.visible !== false,
|
|
color: bg?.color ?? DEFAULT_BB_BAND_BACKGROUND.color,
|
|
};
|
|
}
|
|
|
|
/** 업비트 플롯 정의 병합 (중앙값·어퍼·로우어, 기본 색) */
|
|
export function mergeBbPlots(
|
|
saved: import('./indicatorRegistry').PlotDef[] | undefined,
|
|
defaults: import('./indicatorRegistry').PlotDef[],
|
|
): import('./indicatorRegistry').PlotDef[] {
|
|
const byId = new Map((saved ?? []).map(p => [p.id, p]));
|
|
return BB_PLOT_IDS.map((id, i) => {
|
|
const def = defaults.find(p => p.id === id) ?? defaults[i];
|
|
const prev = byId.get(id);
|
|
return {
|
|
...def,
|
|
...prev,
|
|
id,
|
|
title: BB_PLOT_TITLES[i],
|
|
color: prev?.color ?? BB_DEFAULT_COLORS[i],
|
|
};
|
|
});
|
|
}
|
|
|
|
/** 차트 레전드: BB 20 2 0 SMA */
|
|
export function formatBbLegendLabel(
|
|
params: Record<string, number | string | boolean>,
|
|
): string {
|
|
const p = normalizeBollingerParams(params);
|
|
const ma = String(p.maType ?? 'SMA').replace(/\s*\(.*\)\s*/g, '').trim();
|
|
return `BB ${p.length} ${p.mult} ${p.offset} ${ma}`;
|
|
}
|
|
|
|
export function normalizeBollingerParams(
|
|
params: Record<string, number | string | boolean>,
|
|
): Record<string, number | string | boolean> {
|
|
const length = Math.max(1, Math.round(Number(params.length ?? BB_DEFAULT_LENGTH) || BB_DEFAULT_LENGTH));
|
|
const mult = Math.max(0.001, Number(params.mult ?? BB_DEFAULT_MULT) || BB_DEFAULT_MULT);
|
|
const offset = Math.round(Number(params.offset ?? BB_DEFAULT_OFFSET) || 0);
|
|
const rawMode = String(params.symbolMode ?? 'main');
|
|
const symbolMode: BbSymbolMode = rawMode === 'other' ? 'other' : 'main';
|
|
const refSymbol = String(params.refSymbol ?? '').trim().toUpperCase();
|
|
return {
|
|
...params,
|
|
length,
|
|
mult,
|
|
offset,
|
|
symbolMode,
|
|
refSymbol,
|
|
src: 'close',
|
|
maType: 'SMA',
|
|
};
|
|
}
|
|
|
|
/** TradingView/업비트 오프셋: 양수면 플롯을 오른쪽(미래)으로 이동 */
|
|
export function applyPlotOffset(data: PlotData, offset: number): PlotData {
|
|
if (!offset) return data;
|
|
const n = data.length;
|
|
return data.map((point, i) => {
|
|
const srcIdx = i - offset;
|
|
if (srcIdx < 0 || srcIdx >= n) {
|
|
return { time: point.time, value: NaN };
|
|
}
|
|
return { time: point.time, value: data[srcIdx].value };
|
|
});
|
|
}
|
|
|
|
/** 다른 심볼 캔들을 메인 차트 시간축에 맞춤 (같은 time 키) */
|
|
export function alignBarsToMainTimeline(mainBars: OHLCVBar[], refBars: OHLCVBar[]): OHLCVBar[] {
|
|
const byTime = new Map(refBars.map(b => [b.time, b]));
|
|
return mainBars.map(mb => {
|
|
const ref = byTime.get(mb.time);
|
|
if (!ref) {
|
|
return { ...mb, open: NaN, high: NaN, low: NaN, close: NaN, volume: 0 };
|
|
}
|
|
return ref;
|
|
});
|
|
}
|
|
|
|
export async function resolveBollingerBars(
|
|
mainBars: OHLCVBar[],
|
|
params: Record<string, number | string | boolean>,
|
|
timeframe: Timeframe,
|
|
): Promise<OHLCVBar[]> {
|
|
const p = normalizeBollingerParams(params);
|
|
if (p.symbolMode !== 'other') return mainBars;
|
|
const ref = String(p.refSymbol ?? '');
|
|
if (!ref || !isUpbitMarket(ref)) return mainBars;
|
|
try {
|
|
const refBars = await fetchUpbitCandles(ref, timeframe, mainBars.length);
|
|
return alignBarsToMainTimeline(mainBars, refBars);
|
|
} catch (e) {
|
|
console.warn('[BB] ref symbol fetch failed:', ref, e);
|
|
return mainBars;
|
|
}
|
|
}
|