Files
goldenChart/frontend/src/utils/indicatorRegistry.ts
T
Macbook b69a8c8901 일목균형표 서버 미표시 버그 수정
1. isIchimokuCloudVisible에서 areIchimokuSpanLinesVisible 검사 제거
   - 서버 DB 구버전 설정(plot3/plot4=false)에서 구름이 미표시되는 회귀 수정
   - 라인 표시 여부와 구름 표시 여부를 독립적으로 동작하도록 변경

2. calculateIndicator: ind.calculate에 calcParams 전달하도록 버그 수정
   - senkouDisplacement 커스텀 값이 실제 계산에 반영되지 않던 문제 수정

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 03:41:53 +09:00

667 lines
65 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { OHLCVBar } from '../types';
import type { Bar } from 'oakscriptjs';
import {
createDefaultSmaParams,
createDefaultSmaPlots,
calculateSmaMulti,
normalizeSmaConfig,
} from './smaConfig';
import {
normalizeIchimokuConfig,
mergeIchimokuPlots,
resolveIchimokuDisplacements,
} from './ichimokuConfig';
import {
normalizePsychologicalParams,
resolvePsychologicalIndicatorType,
} from './psychologicalConfig';
import {
normalizeBollingerParams,
resolveBollingerBars,
resolveBbBandBackground,
mergeBbPlots,
} from './bollingerConfig';
import type { Timeframe } from '../types';
export type IndicatorCategory =
| 'Moving Averages'
| 'Oscillators'
| 'Momentum'
| 'Trend'
| 'Volatility'
| 'Channels & Bands'
| 'Volume'
| 'Candlestick Patterns';
export interface PlotDef {
id: string;
title: string;
color: string;
type: 'line' | 'histogram' | 'area' | 'scatter';
lineWidth?: number;
/** 라인 시리즈 선 유형 (기본 solid) */
lineStyle?: HLineStyle;
/** histogram — 이전 봉 대비 상승·0선 위 막대 색 */
histogramUpColor?: string;
/** histogram — 이전 봉 대비 하락 또는 0선 아래 막대 색 */
histogramDownColor?: string;
}
export type HLineStyle = 'solid' | 'dashed' | 'dotted';
export interface HLineDef {
price: number;
color: string;
/** 표시 이름: '과열선' | '중앙선' | '침체선' | '기준선' 등 */
label?: string;
visible?: boolean; // false 이면 차트에서 숨김 (기본 true)
lineStyle?: HLineStyle; // 선 유형 (기본 'dashed')
lineWidth?: number; // 선 굵기 1~4 (기본 1)
}
/**
* price 값과 같은 hlines 배열 내 다른 값들을 기반으로
* 기본 레이블을 자동 결정합니다.
*/
/** 가격 값으로 hline 역할 레이블을 결정 — 용어 통일: 과열선/중앙선/침체선 */
export function getHLineLabel(price: number, allPrices: number[]): string {
if (price === 0) return '중앙선';
const uniq = [...new Set(allPrices)].sort((a, b) => b - a);
if (uniq.length === 1) return '중앙선';
if (price === uniq[0]) return '과열선';
if (price === uniq[uniq.length - 1]) return '침체선';
return '중앙선';
}
function migrateHLineLabel(label: string | undefined): string | undefined {
if (!label) return undefined;
if (label === '과매수' || label === '상한선') return '과열선';
if (label === '과매도' || label === '하한선') return '침체선';
if (label === '0선' || label === '중립선' || label === '중간선' || label === '기준선') return '중앙선';
return label;
}
/** normalizeHLines 합성 중앙선(0) 등 레거시 저장값을 registry 기본 가격으로 교정 */
function resolveMergedHLinePrice(
roleLabel: string,
savedPrice: number,
defPrice: number,
): number {
if (roleLabel === '중앙선' && savedPrice === 0 && defPrice !== 0) {
return defPrice;
}
return savedPrice;
}
/** 지표별 누락 수평선 합성 기본값 */
const HL_SYNTH_DEFAULTS: Record<string, { over: number; mid: number; under: number }> = {
ADX: { over: 40, mid: 25, under: 20 },
DMI: { over: 30, mid: 20, under: 10 },
Stochastic: { over: 80, mid: 50, under: 20 },
StochRSI: { over: 80, mid: 50, under: 20 },
};
/** Stochastic Slow 기본 수평선 — 과열 80 / 중앙 50(기본 off) / 침체 20 */
export const STOCHASTIC_DEFAULT_HLINES: HLineDef[] = [
{ price: 80, color: '#EF5350', label: '과열선', lineStyle: 'dashed', lineWidth: 1 },
{ price: 50, color: '#607D8B', label: '중앙선', lineStyle: 'dashed', lineWidth: 1, visible: false },
{ price: 20, color: '#4CAF50', label: '침체선', lineStyle: 'dashed', lineWidth: 1 },
];
/** ADX 기본 수평선 — 강한 추세(과열) / 기준(중앙) / 약한 추세(침체) */
export const ADX_DEFAULT_HLINES: HLineDef[] = [
{ price: 40, color: '#EF5350', label: '과열선', lineStyle: 'dashed', lineWidth: 1 },
{ price: 25, color: '#607D8B', label: '중앙선', lineStyle: 'dashed', lineWidth: 1 },
{ price: 20, color: '#4CAF50', label: '침체선', lineStyle: 'dashed', lineWidth: 1 },
];
/** DMI 기본 수평선 (업비트: 과열 30 / 중간 20 / 침체 10) */
export const DMI_DEFAULT_HLINES: HLineDef[] = [
{ price: 30, color: '#EF5350', label: '과열선', lineStyle: 'dashed', lineWidth: 1 },
{ price: 20, color: '#607D8B', label: '중앙선', lineStyle: 'dashed', lineWidth: 1 },
{ price: 10, color: '#4CAF50', label: '침체선', lineStyle: 'dashed', lineWidth: 1 },
];
export function normalizeDmiParams(
params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
const diLength = Math.max(1, Number(params.diLength ?? params.length ?? 14));
const adxSmoothing = Math.max(1, Number(params.adxSmoothing ?? 14));
return { ...params, diLength, adxSmoothing };
}
/** 저장된 hlines를 registry 기본값과 역할(라벨) 기준으로 병합 */
export function mergeHlines(
saved: HLineDef[] | undefined,
defaults: HLineDef[] | undefined,
indicatorType?: string,
): HLineDef[] {
const defs = (defaults ?? []).map(h => ({ ...h }));
if (!defs.length) return (saved ?? []).map(h => ({ ...h }));
const savedList = (saved ?? []).map(h => {
const label = migrateHLineLabel(h.label);
return label ? { ...h, label } : { ...h };
});
const prices = savedList.map(h => h.price);
const labelOf = (h: HLineDef) => h.label ?? getHLineLabel(h.price, prices);
const legacyMid =
(indicatorType === 'ADX' || indicatorType === 'DMI') && savedList.length === 1
? savedList[0]
: undefined;
return defs
.map(def => {
const wantLabel = def.label ?? getHLineLabel(def.price, defs.map(d => d.price));
const match =
savedList.find(h => labelOf(h) === wantLabel)
?? (legacyMid && wantLabel === '중앙선' ? legacyMid : undefined);
if (match) {
const price = resolveMergedHLinePrice(wantLabel, match.price, def.price);
return { ...def, ...match, label: wantLabel, price };
}
return { ...def, label: wantLabel };
})
.concat(
savedList.filter(h => {
const lbl = labelOf(h);
return !defs.some(d => (d.label ?? getHLineLabel(d.price, defs.map(x => x.price))) === lbl);
}).map(h => ({ ...h, label: labelOf(h) })),
)
.sort((a, b) => b.price - a.price);
}
/** 수평선 배열을 과열·중앙·침체 3종으로 정규화 (설정 모달·차트 공통) */
export function normalizeHLines(
hlines: HLineDef[],
indicatorType?: string,
defaults?: HLineDef[],
): HLineDef[] {
let list = defaults?.length ? mergeHlines(hlines, defaults, indicatorType) : [...hlines];
if (list.length === 0) return list;
list = list.map(h => {
const migrated = migrateHLineLabel(h.label);
return migrated ? { ...h, label: migrated } : h;
});
const initPrices = list.map(h => h.price);
const labeled = list.map(h => ({
...h,
label: h.label ?? getHLineLabel(h.price, initPrices),
}));
const withFixedLabels = labeled.map(h =>
h.label === '기준선' ? { ...h, label: '중앙선' } : h,
);
const prices = withFixedLabels.map(h => h.price);
const getLabel = (h: HLineDef) => h.label ?? getHLineLabel(h.price, prices);
const hasOver = withFixedLabels.some(h => getLabel(h) === '과열선');
const hasCenter = withFixedLabels.some(h => getLabel(h) === '중앙선');
const hasUnder = withFixedLabels.some(h => getLabel(h) === '침체선');
if (hasOver && hasCenter && hasUnder) {
return withFixedLabels.sort((a, b) => b.price - a.price);
}
const result = [...withFixedLabels];
const spec = indicatorType ? HL_SYNTH_DEFAULTS[indicatorType] : undefined;
const maxAbsPrice = prices.reduce((m, p) => Math.max(m, Math.abs(p)), 0);
const synthUpper = maxAbsPrice > 0 ? parseFloat((maxAbsPrice * 1.5).toPrecision(3)) : 100;
const synthLower = maxAbsPrice > 0 ? -parseFloat((maxAbsPrice * 1.5).toPrecision(3)) : -100;
if (!hasOver) {
result.push({
price: spec?.over ?? synthUpper,
color: '#EF5350',
label: '과열선',
visible: false,
lineStyle: 'dashed',
lineWidth: 1,
});
}
if (!hasCenter) {
result.push({
price: spec?.mid ?? 0,
color: '#607D8B',
label: '중앙선',
visible: false,
lineStyle: 'dashed',
lineWidth: 1,
});
}
if (!hasUnder) {
result.push({
price: spec?.under ?? synthLower,
color: '#4CAF50',
label: '침체선',
visible: false,
lineStyle: 'dashed',
lineWidth: 1,
});
}
return result.sort((a, b) => b.price - a.price);
}
export interface IndicatorDef {
type: string;
name: string;
koreanName: string;
shortName: string;
category: IndicatorCategory;
overlay: boolean;
defaultParams: Record<string, number | string | boolean>;
plots: PlotDef[];
hlines?: HLineDef[];
returnsMarkers?: boolean;
description: string;
}
// ─────────────────────────────────────────────────────────────────────────────
// Full Indicator Registry (82 standard + 44 candlestick patterns)
// ─────────────────────────────────────────────────────────────────────────────
export const INDICATOR_REGISTRY: IndicatorDef[] = [
// ── Moving Averages ──────────────────────────────────────────────────────
{ type:'SMA', name:'Simple Moving Average', koreanName:'단순이동평균', shortName:'SMA', category:'Moving Averages', overlay:true, defaultParams:createDefaultSmaParams(), plots:createDefaultSmaPlots(), description:'단순 이동평균 (MA1~MA11)' },
{ type:'EMA', name:'Exponential Moving Average', koreanName:'지수이동평균', shortName:'EMA', category:'Moving Averages', overlay:true, defaultParams:{length:21, src:'close'}, plots:[{id:'plot0',title:'EMA', color:'#FF6D00',type:'line',lineWidth:2}], description:'지수 이동평균' },
{ type:'WMA', name:'Weighted Moving Average', koreanName:'가중이동평균', shortName:'WMA', category:'Moving Averages', overlay:true, defaultParams:{length:20, src:'close'}, plots:[{id:'plot0',title:'WMA', color:'#00BCD4',type:'line',lineWidth:2}], description:'가중 이동평균' },
{ type:'HMA', name:'Hull Moving Average', koreanName:'헐이동평균', shortName:'HMA', category:'Moving Averages', overlay:true, defaultParams:{length:16, src:'close'}, plots:[{id:'plot0',title:'HMA', color:'#4CAF50',type:'line',lineWidth:2}], description:'헐 이동평균 (래그 최소)' },
{ type:'VWMA', name:'Volume Weighted Moving Average', koreanName:'거래량가중이동평균', shortName:'VWMA', category:'Moving Averages', overlay:true, defaultParams:{length:20, src:'close'}, plots:[{id:'plot0',title:'VWMA', color:'#9C27B0',type:'line',lineWidth:2}], description:'거래량 가중 이동평균' },
{ type:'DEMA', name:'Double Exponential MA', koreanName:'이중지수이동평균', shortName:'DEMA', category:'Moving Averages', overlay:true, defaultParams:{length:21, src:'close'}, plots:[{id:'plot0',title:'DEMA', color:'#F44336',type:'line',lineWidth:2}], description:'이중 지수 이동평균' },
{ type:'TEMA', name:'Triple Exponential MA', koreanName:'삼중지수이동평균', shortName:'TEMA', category:'Moving Averages', overlay:true, defaultParams:{length:21, src:'close'}, plots:[{id:'plot0',title:'TEMA', color:'#FF9800',type:'line',lineWidth:2}], description:'삼중 지수 이동평균' },
{ type:'LSMA', name:'Least Squares MA', koreanName:'최소제곱이동평균', shortName:'LSMA', category:'Moving Averages', overlay:true, defaultParams:{length:25, src:'close', offset:0}, plots:[{id:'plot0',title:'LSMA', color:'#00E5FF',type:'line',lineWidth:2}], description:'최소제곱 이동평균' },
{ type:'ALMA', name:'Arnaud Legoux MA', koreanName:'아르노-레구이동평균', shortName:'ALMA', category:'Moving Averages', overlay:true, defaultParams:{length:21, offset:0.85, sigma:6}, plots:[{id:'plot0',title:'ALMA', color:'#E040FB',type:'line',lineWidth:2}], description:'아르노 레구 이동평균' },
{ type:'RMA', name:'Smoothed MA (SMMA/RMA)', koreanName:'윌더스무딩이동평균', shortName:'RMA', category:'Moving Averages', overlay:true, defaultParams:{length:14, src:'close'}, plots:[{id:'plot0',title:'RMA', color:'#8BC34A',type:'line',lineWidth:2}], description:'와일더 스무딩 이동평균' },
{ type:'SMMA', name:'Smoothed Moving Average', koreanName:'스무딩이동평균', shortName:'SMMA', category:'Moving Averages', overlay:true, defaultParams:{length:14, src:'close'}, plots:[{id:'plot0',title:'SMMA', color:'#CDDC39',type:'line',lineWidth:2}], description:'스무딩 이동평균' },
{ type:'McGinleyDynamic', name:'McGinley Dynamic', koreanName:'맥긴리다이나믹', shortName:'MGD', category:'Moving Averages', overlay:true, defaultParams:{length:14}, plots:[{id:'plot0',title:'MGD', color:'#FF5722',type:'line',lineWidth:2}], description:'맥긴리 다이나믹 MA' },
{ type:'Median', name:'Median Price', koreanName:'중간값이동평균', shortName:'Med', category:'Moving Averages', overlay:true, defaultParams:{length:14}, plots:[{id:'plot0',title:'Median',color:'#607D8B',type:'line',lineWidth:1}], description:'중간값 (Median) 이동평균' },
{ type:'TWAP', name:'Time Weighted Average Price', koreanName:'시간가중평균가격', shortName:'TWAP', category:'Moving Averages', overlay:true, defaultParams:{}, plots:[{id:'plot0',title:'TWAP', color:'#795548',type:'line',lineWidth:2}], description:'시간 가중 평균 가격' },
{ type:'MACross',name:'MA Cross', koreanName:'이동평균교차', shortName:'MACross',category:'Moving Averages', overlay:true, defaultParams:{fastLength:9, slowLength:21, src:'close'},plots:[{id:'plot0',title:'Fast', color:'#2962FF',type:'line',lineWidth:2},{id:'plot1',title:'Slow',color:'#FF6D00',type:'line',lineWidth:2}], description:'이동평균 교차 (골든크로스/데드크로스)' },
// ── Channels & Bands ──────────────────────────────────────────────────────
{ type:'BollingerBands', name:'Bollinger Bands', koreanName:'볼린저밴드', shortName:'BB', category:'Channels & Bands', overlay:true, defaultParams:{symbolMode:'main', refSymbol:'', length:20, mult:2, offset:0, src:'close', maType:'SMA'}, plots:[{id:'plot0',title:'중앙값',color:'#FF9800',type:'line',lineWidth:1},{id:'plot1',title:'어퍼',color:'#42A5F5',type:'line',lineWidth:1},{id:'plot2',title:'로우어',color:'#42A5F5',type:'line',lineWidth:1}], description:'볼린저 밴드 (SMA ± σ×곱)' },
{ type:'KeltnerChannels',name:'Keltner Channels', koreanName:'켈트너채널', shortName:'KC', category:'Channels & Bands', overlay:true, defaultParams:{length:20, multiplier:2}, plots:[{id:'plot0',title:'Upper', color:'#7E57C2',type:'line',lineWidth:1},{id:'plot1',title:'Middle',color:'#EF5350',type:'line',lineWidth:1},{id:'plot2',title:'Lower',color:'#7E57C2',type:'line',lineWidth:1}], description:'켈트너 채널' },
{ type:'DonchianChannels',name:'Donchian Channels', koreanName:'돈치안채널', shortName:'DC', category:'Channels & Bands', overlay:true, defaultParams:{length:20}, plots:[{id:'plot0',title:'Upper', color:'#4CAF50',type:'line',lineWidth:1},{id:'plot1',title:'Basis',color:'#FFC107',type:'line',lineWidth:1},{id:'plot2',title:'Lower',color:'#4CAF50',type:'line',lineWidth:1}], description:'돈치안 채널' },
{ type:'Envelope', name:'Envelope', koreanName:'엔벨로프', shortName:'Env', category:'Channels & Bands', overlay:true, defaultParams:{length:20, percent:0.1, src:'close'}, plots:[{id:'plot0',title:'Upper', color:'#009688',type:'line',lineWidth:1},{id:'plot1',title:'Basis',color:'#607D8B',type:'line',lineWidth:1},{id:'plot2',title:'Lower',color:'#009688',type:'line',lineWidth:1}], description:'엔벨로프 채널' },
{ type:'BBPercentB', name:'BB %B', koreanName:'볼린저밴드 %B', shortName:'BB%B', category:'Channels & Bands', overlay:false,defaultParams:{length:20, mult:2, src:'close'}, plots:[{id:'plot0',title:'%B', color:'#2196F3',type:'line',lineWidth:2}], hlines:[{price:1,color:'#EF5350'},{price:0.5,color:'#607D8B'},{price:0,color:'#4CAF50'}], description:'볼린저밴드 내 가격 위치 (%B)' },
{ type:'BBBandWidth', name:'BB BandWidth', koreanName:'볼린저밴드폭', shortName:'BBW', category:'Channels & Bands', overlay:false,defaultParams:{length:20, mult:2, src:'close'}, plots:[{id:'plot0',title:'BBW', color:'#9C27B0',type:'line',lineWidth:2}], description:'볼린저밴드 폭 (변동성 측정)' },
// ── Oscillators ───────────────────────────────────────────────────────────
{ type:'RSI', name:'Relative Strength Index', koreanName:'상대강도지수', shortName:'RSI', category:'Oscillators', overlay:false, defaultParams:{length:14, src:'close', maType:'SMA', maLength:14}, plots:[{id:'plot0',title:'RSI',color:'#7E57C2',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#F23645',type:'line',lineWidth:1}], hlines:[{price:70,color:'#EF5350'},{price:50,color:'#607D8B'},{price:30,color:'#4CAF50'}], description:'상대 강도 지수 (0~100)' },
{ type:'Stochastic', name:'Stochastic Slow', koreanName:'스토케스틱 슬로우', shortName:'Stoch', category:'Oscillators', overlay:false, defaultParams:{kLength:14, dSmoothing:3, smooth:3}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:STOCHASTIC_DEFAULT_HLINES, description:'Stochastic Slow %K/%D (Slowing 적용)' },
{ type:'StochRSI', name:'Stochastic RSI', koreanName:'스토캐스틱RSI', shortName:'StochRSI',category:'Oscillators',overlay:false, defaultParams:{lengthRSI:14, lengthStoch:14, smoothK:3, smoothD:3, src:'close'}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:STOCHASTIC_DEFAULT_HLINES, description:'스토캐스틱 RSI' },
{ type:'CCI', name:'Commodity Channel Index', koreanName:'상품채널지수', shortName:'CCI', category:'Oscillators', overlay:false, defaultParams:{length:13, src:'hlc3', maType:'SMA', maLength:10}, plots:[{id:'plot0',title:'CCI',color:'#F23645',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#2962FF',type:'line',lineWidth:1}], hlines:[{price:100,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-100,color:'#4CAF50'}], description:'상품 채널 지수 (업비트·TradingView: hlc3·신호 SMA 10)' },
{ type:'WilliamsPercentRange',name:'Williams %R', koreanName:'윌리엄스 %R', shortName:'W%R', category:'Oscillators', overlay:false, defaultParams:{length:14, src:'close'}, plots:[{id:'plot0',title:'%R', color:'#9C27B0',type:'line',lineWidth:2}], hlines:[{price:-20,color:'#EF5350'},{price:-50,color:'#607D8B'},{price:-80,color:'#4CAF50'}], description:'윌리엄스 %R 오실레이터' },
{ type:'AwesomeOscillator', name:'Awesome Oscillator', koreanName:'어썸오실레이터', shortName:'AO', category:'Oscillators', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'AO',color:'#26A69A',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'AO 오실레이터' },
{ type:'ChandeMO', name:'Chande Momentum Oscillator', koreanName:'챈드모멘텀오실레이터', shortName:'CMO', category:'Oscillators', overlay:false, defaultParams:{length:9, src:'close'}, plots:[{id:'plot0',title:'CMO',color:'#4DB6AC',type:'line',lineWidth:2}], hlines:[{price:50,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-50,color:'#4CAF50'}], description:'챈드 모멘텀 오실레이터' },
{ type:'DPO', name:'Detrended Price Oscillator', koreanName:'비트렌드가격오실레이터',shortName:'DPO', category:'Oscillators', overlay:false, defaultParams:{length:21, src:'close'}, plots:[{id:'plot0',title:'DPO',color:'#FF7043',type:'line',lineWidth:2}], hlines:[{price:0,color:'#607D8B'}], description:'비트렌드 가격 오실레이터' },
{ type:'RVI', name:'Relative Vigor Index', koreanName:'상대활력지수', shortName:'RVI', category:'Oscillators', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'RVI', color:'#26A69A',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#EF5350',type:'line',lineWidth:1}], hlines:[{price:0,color:'#607D8B'}], description:'상대 활력 지수' },
{ type:'FisherTransform', name:'Fisher Transform', koreanName:'피셔변환', shortName:'Fish', category:'Oscillators', overlay:false, defaultParams:{length:9}, plots:[{id:'plot0',title:'Fisher', color:'#2962FF',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#FF6D00',type:'line',lineWidth:1}], hlines:[{price:2.5,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-2.5,color:'#4CAF50'}], description:'피셔 변환 오실레이터' },
{ type:'UltimateOscillator', name:'Ultimate Oscillator', koreanName:'울티메이트오실레이터', shortName:'UO', category:'Oscillators', overlay:false, defaultParams:{length1:7, length2:14, length3:28}, plots:[{id:'plot0',title:'UO', color:'#7E57C2',type:'line',lineWidth:2}], hlines:[{price:70,color:'#EF5350'},{price:50,color:'#607D8B'},{price:30,color:'#4CAF50'}], description:'울티메이트 오실레이터' },
{ type:'ConnorsRSI', name:'Connors RSI', koreanName:'코너스RSI', shortName:'CRSI', category:'Oscillators', overlay:false, defaultParams:{rsiLength:3, upDownLength:2, rocLength:100}, plots:[{id:'plot0',title:'CRSI',color:'#AB47BC',type:'line',lineWidth:2}], hlines:[{price:90,color:'#EF5350'},{price:10,color:'#4CAF50'}], description:'코너스 RSI' },
{ type:'SMIErgodic', name:'SMI Ergodic', koreanName:'SMI에르고딕', shortName:'SMI', category:'Oscillators', overlay:false, defaultParams:{longLength:20, shortLength:5, signalLength:5}, plots:[{id:'plot0',title:'SMI',color:'#26A69A',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#EF5350',type:'line',lineWidth:1}], hlines:[{price:0.4,color:'#EF535080'},{price:0,color:'#607D8B'},{price:-0.4,color:'#4CAF5080'}], description:'SMI 에르고딕' },
{ type:'SMIErgodicOscillator',name:'SMI Ergodic Oscillator', koreanName:'SMI에르고딕오실레이터', shortName:'SMIO', category:'Oscillators', overlay:false, defaultParams:{longLength:20, shortLength:5, signalLength:5}, plots:[{id:'plot0',title:'SMIO',color:'#00BCD4',type:'histogram'}], hlines:[{price:0.4,color:'#EF535080'},{price:0,color:'#607D8B'},{price:-0.4,color:'#4CAF5080'}], description:'SMI 에르고딕 오실레이터' },
{ type:'WoodiesCCI', name:'Woodies CCI', koreanName:'우디CCI', shortName:'WCCI', category:'Oscillators', overlay:false, defaultParams:{ccilength:14, turbolen:6}, plots:[{id:'plot0',title:'Turbo',color:'#FF6D00',type:'histogram'},{id:'plot1',title:'CCI',color:'#2962FF',type:'line',lineWidth:2}], hlines:[{price:100,color:'#EF535080'},{price:0,color:'#607D8B'},{price:-100,color:'#4CAF5080'}], description:'우디 CCI' },
{ type:'RelativeVolatilityIndex',name:'Relative Volatility Index',koreanName:'상대변동성지수', shortName:'RVI2', category:'Oscillators', overlay:false, defaultParams:{length:14, stdevLength:10}, plots:[{id:'plot0',title:'RVI',color:'#FF7043',type:'line',lineWidth:2}], hlines:[{price:60,color:'#EF5350'},{price:40,color:'#4CAF50'}], description:'상대 변동성 지수' },
{ type:'ChopZone', name:'Chop Zone', koreanName:'변동성구분지표', shortName:'CZ', category:'Oscillators', overlay:false, defaultParams:{length:30, atrLength:1}, plots:[{id:'plot0',title:'CZ', color:'#FF9800',type:'histogram'}], hlines:[], description:'변동성 vs 추세 구분 지표' },
// ── Momentum ──────────────────────────────────────────────────────────────
{ type:'MACD', name:'MACD', koreanName:'이동평균수렴발산', shortName:'MACD', category:'Momentum', overlay:false, defaultParams:{fastLength:12, slowLength:26, signalLength:9, src:'close'}, plots:[{id:'plot0',title:'Histogram',color:'#26A69A',type:'histogram',histogramUpColor:'#26A69A',histogramDownColor:'#EF5350'},{id:'plot1',title:'MACD',color:'#2962FF',type:'line',lineWidth:1},{id:'plot2',title:'Signal',color:'#FF6D00',type:'line',lineWidth:1}], hlines:[{price:0,color:'#607D8B'}], description:'이동평균 수렴·발산' },
{ type:'Momentum', name:'Momentum', koreanName:'모멘텀', shortName:'MOM', category:'Momentum', overlay:false, defaultParams:{length:10, src:'close'}, plots:[{id:'plot0',title:'MOM', color:'#26A69A',type:'line',lineWidth:2}], hlines:[{price:0,color:'#607D8B'}], description:'모멘텀 지표' },
{ type:'ROC', name:'Rate of Change', koreanName:'가격변화율', shortName:'ROC', category:'Momentum', overlay:false, defaultParams:{length:9, src:'close'}, plots:[{id:'plot0',title:'ROC', color:'#00BCD4',type:'line',lineWidth:2}], hlines:[{price:10,color:'#EF535080'},{price:0,color:'#607D8B'},{price:-10,color:'#4CAF5080'}], description:'가격 변화율 (%)' },
{ type:'TSI', name:'True Strength Index', koreanName:'참강도지수', shortName:'TSI', category:'Momentum', overlay:false, defaultParams:{longLength:25, shortLength:13, signalLength:13, src:'close'}, plots:[{id:'plot0',title:'TSI', color:'#2196F3',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#FF6D00',type:'line',lineWidth:1}], hlines:[{price:25,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-25,color:'#4CAF50'}], description:'참 강도 지수' },
{ type:'TRIX', name:'TRIX', koreanName:'삼중지수변화율', shortName:'TRIX', category:'Momentum', overlay:false, defaultParams:{length:12, signalLength:9}, plots:[{id:'plot0',title:'TRIX', color:'#26A69A',type:'line',lineWidth:2},{id:'plot1',title:'TRMA',color:'#FF6D00',type:'line',lineWidth:1}], hlines:[{price:0,color:'#607D8B', label:'중앙선'}], description:'삼중 지수 변화율 (종가 3×EMA ROC%)' },
{ type:'KnowSureThing', name:'Know Sure Thing (KST)', koreanName:'KST모멘텀', shortName:'KST', category:'Momentum', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'KST', color:'#9C27B0',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#FF9800',type:'line',lineWidth:1}], hlines:[{price:0,color:'#607D8B'}], description:'KST 모멘텀 지표' },
{ type:'CoppockCurve', name:'Coppock Curve', koreanName:'콥독커브', shortName:'Copp', category:'Momentum', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'Coppock',color:'#E91E63',type:'line',lineWidth:2}], hlines:[{price:0,color:'#607D8B'}], description:'콥독 커브 장기 모멘텀' },
{ type:'BullBearPower', name:'Bull Bear Power', koreanName:'매수매도세력', shortName:'BBP', category:'Momentum', overlay:false, defaultParams:{length:13, src:'close'}, plots:[{id:'plot0',title:'BBP', color:'#26A69A',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'매수/매도 세력 비교' },
{ type:'ElderForceIndex', name:'Elder Force Index', koreanName:'엘더강도지수', shortName:'EFI', category:'Momentum', overlay:false, defaultParams:{length:13}, plots:[{id:'plot0',title:'EFI', color:'#00BCD4',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'엘더 강도 지수' },
{ type:'PriceOscillator', name:'Price Oscillator (PPO)', koreanName:'가격오실레이터', shortName:'PPO', category:'Momentum', overlay:false, defaultParams:{fastLength:12, slowLength:26, src:'close'}, plots:[{id:'plot0',title:'PPO', color:'#FF7043',type:'line',lineWidth:2}], hlines:[{price:2,color:'#EF535080'},{price:0,color:'#607D8B'},{price:-2,color:'#4CAF5080'}], description:'가격 오실레이터 (MACD의 %)' },
{ type:'EaseOfMovement', name:'Ease of Movement', koreanName:'이동용이성', shortName:'EOM', category:'Momentum', overlay:false, defaultParams:{length:14}, plots:[{id:'plot0',title:'EOM', color:'#009688',type:'line',lineWidth:2}], hlines:[{price:0,color:'#607D8B'}], description:'이동 용이성 지표' },
// ── Trend ──────────────────────────────────────────────────────────────
{ type:'ADX', name:'Average Directional Index', koreanName:'평균방향성지수', shortName:'ADX', category:'Trend', overlay:false, defaultParams:{adxSmoothing:14, diLength:14}, plots:[{id:'plot0',title:'ADX',color:'#FF6D00',type:'line',lineWidth:2},{id:'plot1',title:'+DI',color:'#4CAF50',type:'line',lineWidth:1},{id:'plot2',title:'-DI',color:'#EF5350',type:'line',lineWidth:1}], hlines:ADX_DEFAULT_HLINES, description:'평균 방향성 지수 (추세 강도)' },
{ type:'DMI', name:'Directional Movement Index', koreanName:'방향성이동지수', shortName:'DMI', category:'Trend', overlay:false, defaultParams:{diLength:14, adxSmoothing:14}, plots:[{id:'plot0',title:'+DI',color:'#2962FF',type:'line',lineWidth:1},{id:'plot1',title:'-DI',color:'#FF6D00',type:'line',lineWidth:1},{id:'plot2',title:'DX',color:'#FF9800',type:'line',lineWidth:1},{id:'plot3',title:'ADX',color:'#E91E63',type:'line',lineWidth:1},{id:'plot4',title:'ADXR',color:'#9C27B0',type:'line',lineWidth:1}], hlines:DMI_DEFAULT_HLINES, description:'방향성 이동 지수 (+DI·-DI·DX·ADX·ADXR)' },
{ type:'Aroon', name:'Aroon', koreanName:'아룬지표', shortName:'Aroon', category:'Trend', overlay:false, defaultParams:{length:14}, plots:[{id:'plot0',title:'Up', color:'#4CAF50',type:'line',lineWidth:1},{id:'plot1',title:'Down',color:'#EF5350',type:'line',lineWidth:1}], hlines:[{price:70,color:'#4CAF5060'},{price:30,color:'#EF535060'}], description:'아룬 추세 지표' },
{ type:'IchimokuCloud', name:'Ichimoku Cloud', koreanName:'일목균형표', shortName:'Ichi', category:'Trend', overlay:true, defaultParams:{conversionPeriods:9, basePeriods:26, laggingSpan2Periods:52, displacement:26, chikouDisplacement:26, senkouDisplacement:26}, plots:[{id:'plot0',title:'전환선',color:'#2196F3',type:'line',lineWidth:1,lineStyle:'solid'},{id:'plot1',title:'기준선',color:'#EF5350',type:'line',lineWidth:1,lineStyle:'solid'},{id:'plot2',title:'후행스팬',color:'#4CAF50',type:'line',lineWidth:1,lineStyle:'solid'},{id:'plot3',title:'선행스팬1',color:'#EF5350',type:'line',lineWidth:1,lineStyle:'solid'},{id:'plot4',title:'선행스팬2',color:'#9C27B0',type:'line',lineWidth:1,lineStyle:'solid'}], description:'일목균형표' },
{ type:'Supertrend', name:'Supertrend', koreanName:'슈퍼트렌드', shortName:'ST', category:'Trend', overlay:true, defaultParams:{atrPeriod:10, factor:3}, plots:[{id:'plot0',title:'ST', color:'#4CAF50',type:'line',lineWidth:2}], description:'슈퍼트렌드 (ATR 기반)' },
{ type:'ParabolicSAR', name:'Parabolic SAR', koreanName:'파라볼릭SAR', shortName:'SAR', category:'Trend', overlay:true, defaultParams:{start:0.02, increment:0.02, maximum:0.2}, plots:[{id:'plot0',title:'SAR', color:'#FF6D00',type:'scatter'}], description:'파라볼릭 SAR' },
{ type:'Choppiness', name:'Choppiness Index', koreanName:'불규칙성지수', shortName:'CHOP', category:'Trend', overlay:false, defaultParams:{length:14}, plots:[{id:'plot0',title:'CHOP',color:'#FFC107',type:'line',lineWidth:2}], hlines:[{price:61.8,color:'#EF535080'},{price:38.2,color:'#4CAF5080'}], description:'변동성 vs 추세 구분 (61.8이하=추세)' },
{ type:'MassIndex', name:'Mass Index', koreanName:'매스인덱스', shortName:'MI', category:'Trend', overlay:false, defaultParams:{length:25, length2:9}, plots:[{id:'plot0',title:'MI', color:'#E91E63',type:'line',lineWidth:2}], hlines:[{price:27,color:'#EF535080'},{price:26.5,color:'#4CAF5080'}], description:'매스 인덱스 (반전 탐지)' },
{ type:'VortexIndicator', name:'Vortex Indicator', koreanName:'보텍스지표', shortName:'VI', category:'Trend', overlay:false, defaultParams:{length:14}, plots:[{id:'plot0',title:'VI+', color:'#4CAF50',type:'line',lineWidth:1},{id:'plot1',title:'VI-',color:'#EF5350',type:'line',lineWidth:1}], hlines:[{price:1,color:'#607D8B'}], description:'보텍스 지표 (추세 전환 확인)' },
{ type:'WilliamsAlligator',name:'Williams Alligator', koreanName:'윌리엄스악어', shortName:'Alli', category:'Trend', overlay:true, defaultParams:{}, plots:[{id:'plot0',title:'Jaw', color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'Teeth',color:'#4CAF50',type:'line',lineWidth:1},{id:'plot2',title:'Lips',color:'#EF5350',type:'line',lineWidth:1}], description:'윌리엄스 악어 (3개 SMMA)' },
{ type:'TrendStrengthIndex',name:'Trend Strength Index', koreanName:'추세강도지수', shortName:'TSI2', category:'Trend', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'TSI', color:'#9C27B0',type:'line',lineWidth:2}], description:'추세 강도 지수' },
{ type:'BBTrend', name:'BB Trend', koreanName:'볼린저밴드추세', shortName:'BBT', category:'Trend', overlay:false, defaultParams:{length:20, mult:2}, plots:[{id:'plot0',title:'BBTrend',color:'#FF7043',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'볼린저밴드 기반 추세 방향' },
{ type:'ChandeKrollStop', name:'Chande Kroll Stop', koreanName:'챈드크롤스탑', shortName:'CKS', category:'Trend', overlay:true, defaultParams:{p:10, q:9, x:3}, plots:[{id:'plot0',title:'Stop+',color:'#4CAF50',type:'line',lineWidth:2},{id:'plot1',title:'Stop-',color:'#EF5350',type:'line',lineWidth:2}], description:'챈드 크롤 스탑' },
{ type:'ZigZag', name:'ZigZag', koreanName:'지그재그', shortName:'ZZ', category:'Trend', overlay:true, defaultParams:{deviation:5, depth:10, backstep:3}, plots:[{id:'plot0',title:'ZZ', color:'#FF5722',type:'line',lineWidth:2}], description:'지그재그 (스윙 고점·저점 연결)' },
{ type:'WilliamsFractals',name:'Williams Fractals', koreanName:'윌리엄스프랙탈', shortName:'Frac', category:'Trend', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'윌리엄스 프랙탈 (고점·저점 마커)' },
{ type:'RankCorrelationIndex',name:'Rank Correlation Index (RCI)',koreanName:'순위상관지수', shortName:'RCI', category:'Oscillators', overlay:false, defaultParams:{length1:9, length2:26, length3:52}, plots:[{id:'plot0',title:'RCI9',color:'#2196F3',type:'line',lineWidth:2},{id:'plot1',title:'RCI26',color:'#FF9800',type:'line',lineWidth:1}], hlines:[{price:80,color:'#EF535080'},{price:-80,color:'#4CAF5080'}], description:'순위 상관 지수 (추세 강도)' },
// ── Volatility ────────────────────────────────────────────────────────────
{ type:'ATR', name:'Average True Range', koreanName:'평균실제범위', shortName:'ATR', category:'Volatility', overlay:false, defaultParams:{length:14, smoothing:'RMA'}, plots:[{id:'plot0',title:'ATR', color:'#FF9800',type:'line',lineWidth:2}], description:'평균 진범위 (변동성)' },
{ type:'ADR', name:'Average Day Range', koreanName:'평균일일범위', shortName:'ADR', category:'Volatility', overlay:false, defaultParams:{length:14}, plots:[{id:'plot0',title:'ADR', color:'#FF5722',type:'line',lineWidth:2}], description:'평균 일일 범위' },
{ type:'StandardDeviation', name:'Standard Deviation', koreanName:'표준편차', shortName:'StdDev', category:'Volatility', overlay:false, defaultParams:{length:20, src:'close'}, plots:[{id:'plot0',title:'StdDev',color:'#9C27B0',type:'line',lineWidth:2}], description:'표준 편차 (가격 변동성)' },
{ type:'HistoricalVolatility',name:'Historical Volatility', koreanName:'역사적변동성', shortName:'HV', category:'Volatility', overlay:false, defaultParams:{length:20, annualBars:252}, plots:[{id:'plot0',title:'HV', color:'#E91E63',type:'line',lineWidth:2}], description:'역사적 변동성 (연화)' },
// ── Volume ────────────────────────────────────────────────────────────────
{ type:'OBV', name:'On Balance Volume', koreanName:'잔량균형지수', shortName:'OBV', category:'Volume', overlay:false, defaultParams:{maType:'SMA', maLength:9}, plots:[{id:'plot0',title:'OBV', color:'#2196F3',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#FF9800',type:'line',lineWidth:1}], hlines:[], description:'잔량 균형 지수 (OBV + 이동평균 신호선)' },
{ type:'VR', name:'Volume Ratio', koreanName:'거래량비율', shortName:'VR', category:'Volume', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'VR', color:'#AB47BC',type:'line',lineWidth:2}], hlines:[{price:200,color:'#EF5350'},{price:100,color:'#607D8B'},{price:50,color:'#4CAF50'}], description:'거래량 비율 (VR)' },
{ type:'Disparity', name:'Disparity Index', koreanName:'이격도', shortName:'Disp', category:'Oscillators', overlay:false, defaultParams:{ultraLength:5, shortLength:10, midLength:20, longLength:60, src:'close'}, plots:[{id:'plot0',title:'초단기',color:'#E91E63',type:'line',lineWidth:1},{id:'plot1',title:'단기',color:'#FF9800',type:'line',lineWidth:1},{id:'plot2',title:'중기',color:'#4CAF50',type:'line',lineWidth:1},{id:'plot3',title:'장기',color:'#2196F3',type:'line',lineWidth:1}], hlines:[{price:100,color:'#607D8B'}], description:'이격도 (종가÷이평×100)' },
{ type:'Psychological', name:'Psychological Line', koreanName:'심리도', shortName:'Psy', category:'Oscillators', overlay:false, defaultParams:{length:12}, plots:[{id:'plot0',title:'심리도',color:'#00BCD4',type:'line',lineWidth:2}], hlines:[{price:75,color:'#EF5350',label:'과열선'},{price:50,color:'#607D8B',label:'중앙선'},{price:25,color:'#4CAF50',label:'침체선'}], description:'심리도 (상승일 비율)' },
{ type:'NewPsychological', name:'New Psychological Line', koreanName:'신심리도', shortName:'NPsy', category:'Oscillators', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'신심리도',color:'#F44336',type:'line',lineWidth:2}], hlines:[{price:50,color:'#EF5350',label:'과열선'},{price:0,color:'#607D8B',label:'중앙선'},{price:-50,color:'#4CAF50',label:'침체선'}], description:'신심리도 (상승·하락 폭 반영)' },
{ type:'InvestPsychological', name:'Invest Psychological Line', koreanName:'투자심리도', shortName:'IPsych',category:'Oscillators', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'투자심리도',color:'#7E57C2',type:'line',lineWidth:2}], hlines:[{price:75,color:'#EF5350',label:'과열선'},{price:50,color:'#607D8B',label:'중앙선'},{price:25,color:'#4CAF50',label:'침체선'}], description:'투자심리도 (상승일 비율)' },
{ type:'MFI', name:'Money Flow Index', koreanName:'자금흐름지수', shortName:'MFI', category:'Volume', overlay:false, defaultParams:{length:14}, plots:[{id:'plot0',title:'MFI', color:'#00BCD4',type:'line',lineWidth:2}], hlines:[{price:80,color:'#EF5350'},{price:20,color:'#4CAF50'}], description:'자금 흐름 지수' },
{ type:'ChaikinMF', name:'Chaikin Money Flow', koreanName:'차이킨자금흐름', shortName:'CMF', category:'Volume', overlay:false, defaultParams:{length:20}, plots:[{id:'plot0',title:'CMF', color:'#4CAF50',type:'histogram'}], hlines:[{price:0.25,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-0.25,color:'#4CAF50'}], description:'차이킨 자금 흐름' },
{ type:'ChaikinOscillator', name:'Chaikin Oscillator', koreanName:'차이킨오실레이터', shortName:'CO', category:'Volume', overlay:false, defaultParams:{fastLength:3, slowLength:10}, plots:[{id:'plot0',title:'CO', color:'#FF9800',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'차이킨 오실레이터' },
{ type:'PVT', name:'Price Volume Trend', koreanName:'가격거래량추세', shortName:'PVT', category:'Volume', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'PVT', color:'#7E57C2',type:'line',lineWidth:2}], description:'가격 거래량 추세' },
{ type:'NetVolume', name:'Net Volume', koreanName:'순거래량', shortName:'NV', category:'Volume', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'NV', color:'#26A69A',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'순 거래량 (매수-매도)' },
{ type:'VolumeDelta', name:'Volume Delta', koreanName:'거래량델타', shortName:'VD', category:'Volume', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'VD', color:'#2196F3',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'볼륨 델타 (매수/매도 차이)' },
{ type:'CumulativeVolumeDelta',name:'Cumulative Volume Delta', koreanName:'누적거래량델타', shortName:'CVD', category:'Volume', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'CVD', color:'#E91E63',type:'line',lineWidth:2}], hlines:[{price:0,color:'#607D8B'}], description:'누적 볼륨 델타' },
{ type:'VolumeOscillator', name:'Volume Oscillator', koreanName:'거래량오실레이터', shortName:'VO', category:'Volume', overlay:false, defaultParams:{shortLength:5, longLength:10}, plots:[{id:'plot0',title:'VO', color:'#FF7043',type:'histogram'}], hlines:[{price:10,color:'#EF535080'},{price:0,color:'#607D8B'},{price:-10,color:'#4CAF5080'}], description:'볼륨 오실레이터' },
{ type:'KlingerOscillator', name:'Klinger Oscillator', koreanName:'클링거오실레이터', shortName:'KVO', category:'Volume', overlay:false, defaultParams:{fastLength:34, slowLength:55, signalLength:13}, plots:[{id:'plot0',title:'KVO',color:'#2962FF',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#FF6D00',type:'line',lineWidth:1}], hlines:[{price:0,color:'#607D8B'}], description:'클링거 오실레이터' },
{ type:'RelativeVolumeAtTime',name:'Relative Volume at Time', koreanName:'시간대별상대거래량', shortName:'RVAT', category:'Volume', overlay:false, defaultParams:{length:24, sessionType:'Regular'}, plots:[{id:'plot0',title:'RVAT', color:'#9C27B0',type:'histogram'}], hlines:[{price:1,color:'#607D8B'}], description:'시간대별 상대 거래량' },
// ── Candlestick Patterns ───────────────────────────────────────────────────
{ type:'Hammer', name:'Hammer', koreanName:'망치형', shortName:'H', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'망치형 (상승 반전)' },
{ type:'InvertedHammer', name:'Inverted Hammer', koreanName:'역망치형', shortName:'IH', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'역망치형 (상승 반전)' },
{ type:'HangingMan', name:'Hanging Man', koreanName:'교수형', shortName:'HM', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'교수형 (하락 반전)' },
{ type:'ShootingStar', name:'Shooting Star', koreanName:'슈팅스타', shortName:'SS', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'슈팅스타 (하락 반전)' },
{ type:'Doji', name:'Doji', koreanName:'도지형', shortName:'Doji', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'도지형' },
{ type:'DragonflyDoji', name:'Dragonfly Doji', koreanName:'잠자리도지', shortName:'DDoji', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'잠자리 도지' },
{ type:'GravestoneDoji', name:'Gravestone Doji', koreanName:'비석도지', shortName:'GDoji', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'비석 도지' },
{ type:'EngulfingBullish', name:'Bullish Engulfing', koreanName:'강세포용형', shortName:'BE', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'강세 포용형' },
{ type:'EngulfingBearish', name:'Bearish Engulfing', koreanName:'약세포용형', shortName:'BrE', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'약세 포용형' },
{ type:'HaramiBullish', name:'Bullish Harami', koreanName:'강세하라미', shortName:'BH', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'강세 하라미' },
{ type:'HaramiBearish', name:'Bearish Harami', koreanName:'약세하라미', shortName:'BrH', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'약세 하라미' },
{ type:'MorningStar', name:'Morning Star', koreanName:'샛별형', shortName:'MS', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'샛별형 (강세 반전)' },
{ type:'EveningStar', name:'Evening Star', koreanName:'저녁별형', shortName:'ES', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'저녁별형 (약세 반전)' },
{ type:'MorningDojiStar', name:'Morning Doji Star', koreanName:'도지샛별형', shortName:'MDS', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'도지 샛별형' },
{ type:'EveningDojiStar', name:'Evening Doji Star', koreanName:'도지저녁별형', shortName:'EDS', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'도지 저녁별형' },
{ type:'ThreeWhiteSoldiers', name:'Three White Soldiers', koreanName:'삼백병사형', shortName:'3WS', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'삼병 (강세 패턴)' },
{ type:'ThreeBlackCrows', name:'Three Black Crows', koreanName:'흑삼병형', shortName:'3BC', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'흑삼병 (약세 패턴)' },
{ type:'Piercing', name:'Piercing Line', koreanName:'관통형', shortName:'PL', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'관통형 (상승 반전)' },
{ type:'DarkCloudCover', name:'Dark Cloud Cover', koreanName:'먹구름형', shortName:'DCC', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'먹구름형 (하락 반전)' },
{ type:'TweezerBottom', name:'Tweezer Bottom', koreanName:'집게바닥형', shortName:'TwB', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'집게바닥형 (상승 반전)' },
{ type:'TweezerTop', name:'Tweezer Top', koreanName:'집게천장형', shortName:'TwT', category:'Candlestick Patterns', overlay:true, defaultParams:{}, plots:[], returnsMarkers:true, description:'집게천장형 (하락 반전)' },
];
export function getIndicatorDef(type: string): IndicatorDef | undefined {
return INDICATOR_REGISTRY.find(d => d.type === type);
}
const DSL_DISPLAY_ALIASES: Record<string, { koreanName: string; shortName: string }> = {
NEW_HIGH: { koreanName: '신고가', shortName: 'NH' },
NEW_LOW: { koreanName: '신저가', shortName: 'NL' },
DONCHIAN: { koreanName: '돈치안채널', shortName: 'DC' },
};
/** 가상투자·조건 목록 — 한글명 (영문약어) 예: 상품채널지수 (CCI) */
export function formatIndicatorDisplayLabel(indicatorType: string): string {
const alias = DSL_DISPLAY_ALIASES[indicatorType];
if (alias) {
return `${alias.koreanName} (${alias.shortName})`;
}
const def = getIndicatorDef(indicatorType);
const abbr = def?.shortName ?? indicatorType;
const ko = def?.koreanName ?? abbr;
if (!ko || ko === abbr) return abbr;
if (ko.includes(`(${abbr})`)) return ko;
return `${ko} (${abbr})`;
}
/** 실시간 차트 pane·범례 타이틀 (심리도·투자심리도·이격도 등 한글명 우선) */
export function getIndicatorChartTitle(type: string): string {
const def = getIndicatorDef(type);
if (!def) return type;
if (
type === 'Psychological'
|| type === 'NewPsychological'
|| type === 'InvestPsychological'
|| type === 'Disparity'
) {
return def.koreanName;
}
return def.shortName;
}
/**
* DB/슬롯에 일부 플롯만 저장된 경우 registry 기본 플롯과 병합.
* (예: CCI plot0만 있으면 신호선 plot1 이 설정·차트에서 누락됨)
*/
export function mergePlotDefs(saved: PlotDef[] | undefined, defaults: PlotDef[]): PlotDef[] {
if (!defaults.length) return (saved ?? []).map(p => ({ ...p }));
const savedMap = new Map((saved ?? []).map(p => [p.id, p]));
return defaults.map(def => {
const s = savedMap.get(def.id);
return s ? { ...def, ...s, id: def.id, title: def.title } : { ...def };
});
}
/** 업비트 OBV 신호선 MA 종류 (단순·지수·가중만) */
export const OBV_MA_TYPE_OPTIONS = ['SMA', 'EMA', 'WMA'] as const;
export type ObvMaType = (typeof OBV_MA_TYPE_OPTIONS)[number];
export function normalizeObvParams(
params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
const raw = String(params.maType ?? 'SMA');
const maType: ObvMaType = raw === 'EMA' || raw === 'WMA' ? raw : 'SMA';
const maLength = Math.max(1, Number(params.maLength ?? 9) || 9);
return { ...params, maType, maLength };
}
/** 업비트·TradingView CCI: 기간 13·hlc3(고저종)·신호 SMA 10 */
export function normalizeCciParams(
params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
const rawMa = String(params.maType ?? 'SMA');
const maType = rawMa === 'EMA' || rawMa === 'WMA' || rawMa === 'None' ? rawMa : 'SMA';
let src = String(params.src ?? 'hlc3');
const length = Math.max(1, Number(params.length ?? 13) || 13);
const maLength = Math.max(1, Number(params.maLength ?? 10) || 10);
// 레거시: 잘못 저장된 close 기본값 → 업비트 동일 hlc3
if (src === 'close' && length === 13 && maLength === 10 && maType === 'SMA') {
src = 'hlc3';
}
return { ...params, src, maType, length, maLength };
}
/** 차트·설정 모달용 — 파라미터·플롯·가시성을 registry 기본값과 병합 */
export function enrichIndicatorConfig(
cfg: import('../types').IndicatorConfig
): import('../types').IndicatorConfig {
const resolvedType = resolvePsychologicalIndicatorType(cfg.type);
const cfgNorm = resolvedType !== cfg.type ? { ...cfg, type: resolvedType } : cfg;
const def = getIndicatorDef(cfgNorm.type);
if (!def) return cfgNorm;
if (cfgNorm.type === 'SMA') {
return normalizeSmaConfig({
...cfgNorm,
params: {
...(def.defaultParams as Record<string, number | string | boolean>),
...cfg.params,
},
plots: mergePlotDefs(cfg.plots, def.plots),
});
}
if (cfgNorm.type === 'IchimokuCloud') {
return normalizeIchimokuConfig({
...cfgNorm,
params: {
...(def.defaultParams as Record<string, number | string | boolean>),
...cfgNorm.params,
},
plots: mergeIchimokuPlots(cfgNorm.plots, def.plots),
});
}
let params = {
...(def.defaultParams as Record<string, number | string | boolean>),
...cfgNorm.params,
};
if (cfgNorm.type === 'OBV') {
params = normalizeObvParams(params);
}
if (cfgNorm.type === 'CCI') {
params = normalizeCciParams(params);
}
if (cfgNorm.type === 'Psychological' || cfgNorm.type === 'NewPsychological' || cfgNorm.type === 'InvestPsychological') {
params = normalizePsychologicalParams(cfgNorm.type, params);
}
if (cfgNorm.type === 'BollingerBands') {
params = normalizeBollingerParams(params);
}
if (cfgNorm.type === 'DMI') {
params = normalizeDmiParams(params);
}
const plots = cfgNorm.type === 'BollingerBands'
? mergeBbPlots(cfgNorm.plots, def.plots)
: mergePlotDefs(cfgNorm.plots, def.plots);
const plotVisibility: Record<string, boolean> = { ...cfgNorm.plotVisibility };
for (const p of plots) {
if (plotVisibility[p.id] === undefined) plotVisibility[p.id] = true;
}
if (cfgNorm.type === 'OBV') {
plotVisibility.plot1 = plotVisibility.plot1 !== false;
} else if ((cfgNorm.type === 'CCI' || cfgNorm.type === 'RSI') && params.maType === 'None') {
plotVisibility.plot1 = false;
}
const out: import('../types').IndicatorConfig = {
...cfgNorm,
params,
plots,
plotVisibility,
hlines: normalizeHLines(cfgNorm.hlines ?? [], cfgNorm.type, def.hlines),
};
if (cfgNorm.type === 'BollingerBands') {
out.bandBackground = resolveBbBandBackground(cfgNorm.bandBackground);
}
return out;
}
export function toBars(ohlcv: OHLCVBar[]): Bar[] {
return ohlcv as unknown as Bar[];
}
export type PlotData = Array<{ time: number; value: number; color?: string }>;
export type IndicatorResult = Record<string, PlotData>;
export interface MarkerData {
time: number;
position: 'aboveBar' | 'belowBar';
shape: 'arrowUp' | 'arrowDown' | 'circle' | 'square';
color: string;
text: string;
}
/**
* 프로그램 내부 파라미터 키 → lightweight-charts-indicators 라이브러리 파라미터 키 변환.
*
* 라이브러리가 기대하는 파라미터 이름이 내부 registry/DB 키와 다른 경우만 등록.
* indicatorRegistry.ts defaultParams 와 라이브러리 defaultInputs 를 대조하여 작성.
*/
const PARAM_KEY_MAP: Record<string, Record<string, string>> = {
// Stochastic: 내부 {kLength, smooth, dSmoothing} → 라이브러리 {periodK, smoothK, periodD}
Stochastic: {
kLength: 'periodK',
smooth: 'smoothK',
dSmoothing: 'periodD',
},
};
/**
* 지표 파라미터를 라이브러리가 인식하는 키 이름으로 변환한다.
* 매핑이 없는 지표는 원본을 그대로 반환.
*/
function translateParams(
type: string,
params: Record<string, number | string | boolean>
): Record<string, number | string | boolean> {
const keyMap = PARAM_KEY_MAP[type];
if (!keyMap) return params;
const translated: Record<string, number | string | boolean> = {};
for (const [k, v] of Object.entries(params)) {
translated[keyMap[k] ?? k] = v;
}
return translated;
}
const CUSTOM_CALCULATORS: Record<
string,
(bars: OHLCVBar[], params: Record<string, number | string | boolean>) => Record<string, PlotData>
> = {};
async function loadCustomCalculators() {
if (Object.keys(CUSTOM_CALCULATORS).length > 0) return;
const c = await import('./customIndicators');
CUSTOM_CALCULATORS.TRIX = c.calcTRIXWithSignal;
CUSTOM_CALCULATORS.OBV = c.calcOBVWithMA;
CUSTOM_CALCULATORS.VR = c.calcVR;
CUSTOM_CALCULATORS.Disparity = c.calcDisparityUpbit;
CUSTOM_CALCULATORS.Psychological = c.calcPsychological;
CUSTOM_CALCULATORS.NewPsychological = c.calcNewPsychological;
CUSTOM_CALCULATORS.InvestPsychological = c.calcInvestPsychological;
CUSTOM_CALCULATORS.BollingerBands = c.calcBollingerBands;
CUSTOM_CALCULATORS.DMI = c.calcDMI;
CUSTOM_CALCULATORS.CCI = c.calcCCI;
CUSTOM_CALCULATORS.RSI = c.calcRSI;
}
/** 차트 심볼·타임프레임 (BB 다른 심볼용) */
let chartContext: { market: string; timeframe: Timeframe } = { market: '', timeframe: '1D' };
export function setIndicatorChartContext(market: string, timeframe: Timeframe): void {
chartContext = { market, timeframe };
}
export async function calculateIndicator(
type: string,
bars: OHLCVBar[],
params: Record<string, number | string | boolean>
): Promise<{ plots: IndicatorResult; markers?: MarkerData[] }> {
if (type === 'SMA') {
return { plots: calculateSmaMulti(bars, params) };
}
let calcBars = bars;
let calcParams = params;
if (type === 'CCI') {
calcParams = normalizeCciParams(params);
}
if (type === 'IchimokuCloud') {
const { senkou } = resolveIchimokuDisplacements(params);
calcParams = { ...params, displacement: senkou };
}
if (type === 'BollingerBands') {
calcParams = normalizeBollingerParams(params);
calcBars = await resolveBollingerBars(bars, calcParams, chartContext.timeframe);
}
await loadCustomCalculators();
const custom = CUSTOM_CALCULATORS[type];
if (custom) {
return { plots: custom(calcBars, calcParams) as IndicatorResult };
}
const b = toBars(bars);
const lib = await import('lightweight-charts-indicators');
const ind = (lib as unknown as Record<string, {
calculate: (b: Bar[], p: unknown) => { plots?: Record<string, PlotData>; markers?: MarkerData[] }
}>)[type];
if (!ind?.calculate) throw new Error(`Unknown indicator: ${type}`);
// calcParams(displacement 등 정규화된 값)를 라이브러리에 전달
const result = ind.calculate(b, translateParams(type, calcParams));
return {
plots: (result.plots ?? {}) as IndicatorResult,
markers: result.markers as MarkerData[] | undefined,
};
}