goldenChat base source add
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* lightweight-charts-indicators 미등록 / 확장 지표 계산
|
||||
* (업비트 차트 기준 공식)
|
||||
*/
|
||||
import type { OHLCVBar } from '../types';
|
||||
import type { PlotData } from './indicatorRegistry';
|
||||
import { applyPlotOffset, normalizeBollingerParams } from './bollingerConfig';
|
||||
|
||||
function sma(values: number[], period: number): number[] {
|
||||
return values.map((_, i) => {
|
||||
if (i < period - 1) return NaN;
|
||||
let sum = 0;
|
||||
for (let j = i - period + 1; j <= i; j++) sum += values[j];
|
||||
return sum / period;
|
||||
});
|
||||
}
|
||||
|
||||
function ema(values: number[], period: number): number[] {
|
||||
const out = new Array<number>(values.length).fill(NaN);
|
||||
if (values.length < period) return out;
|
||||
|
||||
// 선행 NaN(1차 EMA 워밍업 등) 이후 첫 유효 구간에서 SMA 시드
|
||||
let start = -1;
|
||||
for (let i = period - 1; i < values.length; i++) {
|
||||
let ok = true;
|
||||
for (let j = i - period + 1; j <= i; j++) {
|
||||
if (!Number.isFinite(values[j])) { ok = false; break; }
|
||||
}
|
||||
if (ok) { start = i; break; }
|
||||
}
|
||||
if (start < 0) return out;
|
||||
|
||||
let sum = 0;
|
||||
for (let j = start - period + 1; j <= start; j++) sum += values[j];
|
||||
let prev = sum / period;
|
||||
out[start] = prev;
|
||||
const k = 2 / (period + 1);
|
||||
for (let i = start + 1; i < values.length; i++) {
|
||||
const v = values[i];
|
||||
if (!Number.isFinite(v)) { out[i] = NaN; continue; }
|
||||
prev = v * k + prev * (1 - k);
|
||||
out[i] = prev;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function sourceAt(bar: OHLCVBar, src: string): number {
|
||||
switch (src) {
|
||||
case 'open': return bar.open;
|
||||
case 'high': return bar.high;
|
||||
case 'low': return bar.low;
|
||||
case 'hl2': return (bar.high + bar.low) / 2;
|
||||
case 'hlc3': return (bar.high + bar.low + bar.close) / 3;
|
||||
case 'ohlc4': return (bar.open + bar.high + bar.low + bar.close) / 4;
|
||||
default: return bar.close;
|
||||
}
|
||||
}
|
||||
|
||||
function toPlotData(bars: OHLCVBar[], values: number[]): PlotData {
|
||||
return bars.map((b, i) => ({
|
||||
time: b.time,
|
||||
value: Number.isFinite(values[i]) ? values[i] : NaN,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 업비트 이격도: 종가 ÷ 이동평균 × 100 */
|
||||
export function calcDisparityUpbit(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const src = String(params.src ?? 'close');
|
||||
const closes = bars.map(b => sourceAt(b, src));
|
||||
const ultra = Number(params.ultraLength ?? 5);
|
||||
const short = Number(params.shortLength ?? 10);
|
||||
const mid = Number(params.midLength ?? 20);
|
||||
const long = Number(params.longLength ?? 60);
|
||||
|
||||
const plot = (period: number, id: string) => {
|
||||
const ma = sma(closes, period);
|
||||
const vals = closes.map((c, i) =>
|
||||
Number.isFinite(ma[i]) && ma[i] !== 0 ? (c / ma[i]) * 100 : NaN,
|
||||
);
|
||||
return { [id]: toPlotData(bars, vals) };
|
||||
};
|
||||
|
||||
return {
|
||||
...plot(ultra, 'plot0'),
|
||||
...plot(short, 'plot1'),
|
||||
...plot(mid, 'plot2'),
|
||||
...plot(long, 'plot3'),
|
||||
};
|
||||
}
|
||||
|
||||
/** VR: 상승일 거래량 합 / 하락일 거래량 합 × 100 */
|
||||
export function calcVR(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const period = Number(params.length ?? 10);
|
||||
const closes = bars.map(b => b.close);
|
||||
const volumes = bars.map(b => b.volume);
|
||||
const vals = closes.map((_, i) => {
|
||||
if (i < period) return NaN;
|
||||
let upVol = 0;
|
||||
let downVol = 0;
|
||||
for (let j = i - period + 1; j <= i; j++) {
|
||||
if (closes[j] > closes[j - 1]) upVol += volumes[j];
|
||||
else if (closes[j] < closes[j - 1]) downVol += volumes[j];
|
||||
}
|
||||
if (downVol === 0) return NaN;
|
||||
return (upVol / downVol) * 100;
|
||||
});
|
||||
return { plot0: toPlotData(bars, vals) };
|
||||
}
|
||||
|
||||
/** 심리도(업비트): N일 중 전일 대비 상승일 비율 × 100 */
|
||||
export function calcPsychological(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const period = Number(params.length ?? 12);
|
||||
const closes = bars.map(b => b.close);
|
||||
const vals = closes.map((_, i) => {
|
||||
if (i < period) return NaN;
|
||||
let up = 0;
|
||||
for (let j = i - period + 1; j <= i; j++) {
|
||||
if (closes[j] > closes[j - 1]) up++;
|
||||
}
|
||||
return (up / period) * 100;
|
||||
});
|
||||
return { plot0: toPlotData(bars, vals) };
|
||||
}
|
||||
|
||||
/** @deprecated Psychological 사용 */
|
||||
export const calcNewPsychological = calcPsychological;
|
||||
|
||||
/** 투자심리도(업비트): N일 중 상승일 거래량 비중 × 100 */
|
||||
export function calcInvestPsychological(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const period = Number(params.length ?? 10);
|
||||
const closes = bars.map(b => b.close);
|
||||
const volumes = bars.map(b => b.volume);
|
||||
const vals = closes.map((_, i) => {
|
||||
if (i < period) return NaN;
|
||||
let upVol = 0;
|
||||
let total = 0;
|
||||
for (let j = i - period + 1; j <= i; j++) {
|
||||
total += volumes[j];
|
||||
if (closes[j] > closes[j - 1]) upVol += volumes[j];
|
||||
}
|
||||
if (total === 0) return NaN;
|
||||
return (upVol / total) * 100;
|
||||
});
|
||||
return { plot0: toPlotData(bars, vals) };
|
||||
}
|
||||
|
||||
/**
|
||||
* TRIX + 신호선 (업비트·TradingView 동일)
|
||||
* 1) ln(종가) → 2) EMA × 3 → 3) 1봉 차분 × 10000 → 4) 신호선 = TRIX의 EMA(signalLength)
|
||||
*/
|
||||
export function calcTRIXWithSignal(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const len = Number(params.length ?? 12);
|
||||
const sigLen = Number(params.signalLength ?? 9);
|
||||
const logClose = bars.map(b => (b.close > 0 ? Math.log(b.close) : NaN));
|
||||
|
||||
const e1 = ema(logClose, len);
|
||||
const e2 = ema(e1.map(v => (Number.isFinite(v) ? v : NaN)), len);
|
||||
const e3 = ema(e2.map(v => (Number.isFinite(v) ? v : NaN)), len);
|
||||
|
||||
const trix = e3.map((v, i) => {
|
||||
if (i === 0 || !Number.isFinite(v) || !Number.isFinite(e3[i - 1])) return NaN;
|
||||
return (v - e3[i - 1]) * 10_000;
|
||||
});
|
||||
|
||||
const signal = ema(trix.map(v => (Number.isFinite(v) ? v : NaN)), sigLen);
|
||||
|
||||
return {
|
||||
plot0: toPlotData(bars, trix),
|
||||
plot1: toPlotData(bars, signal),
|
||||
};
|
||||
}
|
||||
|
||||
/** OBV + 신호선 MA (업비트: 단순·지수·가중, 기본 단순 9) */
|
||||
export function calcOBVWithMA(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const raw = String(params.maType ?? 'SMA');
|
||||
const maType = raw === 'EMA' || raw === 'WMA' ? raw : 'SMA';
|
||||
const maLen = Math.max(1, Number(params.maLength ?? 9) || 9);
|
||||
|
||||
const obv: number[] = [];
|
||||
let acc = 0;
|
||||
for (let i = 0; i < bars.length; i++) {
|
||||
if (i === 0) {
|
||||
obv.push(0);
|
||||
continue;
|
||||
}
|
||||
const vol = bars[i].volume;
|
||||
if (bars[i].close > bars[i - 1].close) acc += vol;
|
||||
else if (bars[i].close < bars[i - 1].close) acc -= vol;
|
||||
obv.push(acc);
|
||||
}
|
||||
|
||||
let ma: number[];
|
||||
switch (maType) {
|
||||
case 'EMA':
|
||||
ma = ema(obv, maLen);
|
||||
break;
|
||||
case 'WMA':
|
||||
ma = obv.map((_, i) => {
|
||||
if (i < maLen - 1) return NaN;
|
||||
let wSum = 0;
|
||||
let w = 0;
|
||||
for (let j = 0; j < maLen; j++) {
|
||||
const weight = j + 1;
|
||||
wSum += obv[i - maLen + 1 + j] * weight;
|
||||
w += weight;
|
||||
}
|
||||
return wSum / w;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
ma = sma(obv, maLen);
|
||||
}
|
||||
|
||||
return {
|
||||
plot0: toPlotData(bars, obv),
|
||||
plot1: toPlotData(bars, ma),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 표준편차 (TradingView ta.stdev / 업비트 BB)
|
||||
* - Pine v5 ta.stdev: 모집단 분산 (N으로 나눔)
|
||||
*/
|
||||
function stdevBb(values: number[], period: number): number[] {
|
||||
const avg = sma(values, period);
|
||||
return values.map((_, i) => {
|
||||
if (i < period - 1) return NaN;
|
||||
let sumSq = 0;
|
||||
for (let j = i - period + 1; j <= i; j++) {
|
||||
const d = values[j] - avg[i];
|
||||
sumSq += d * d;
|
||||
}
|
||||
return Math.sqrt(sumSq / period);
|
||||
});
|
||||
}
|
||||
|
||||
/** 볼린저밴드 (업비트): SMA(종가) ± mult × σ, 오프셋 적용 */
|
||||
export function calcBollingerBands(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const p = normalizeBollingerParams(params);
|
||||
const period = p.length as number;
|
||||
const mult = p.mult as number;
|
||||
const offset = p.offset as number;
|
||||
const src = String(p.src ?? 'close');
|
||||
const values = bars.map(b => sourceAt(b, src));
|
||||
|
||||
const basis = sma(values, period);
|
||||
const dev = stdevBb(values, period).map(d => (Number.isFinite(d) ? d * mult : NaN));
|
||||
const upper = basis.map((m, i) =>
|
||||
Number.isFinite(m) && Number.isFinite(dev[i]) ? m + dev[i] : NaN,
|
||||
);
|
||||
const lower = basis.map((m, i) =>
|
||||
Number.isFinite(m) && Number.isFinite(dev[i]) ? m - dev[i] : NaN,
|
||||
);
|
||||
|
||||
let plot0 = toPlotData(bars, basis);
|
||||
let plot1 = toPlotData(bars, upper);
|
||||
let plot2 = toPlotData(bars, lower);
|
||||
if (offset) {
|
||||
plot0 = applyPlotOffset(plot0, offset);
|
||||
plot1 = applyPlotOffset(plot1, offset);
|
||||
plot2 = applyPlotOffset(plot2, offset);
|
||||
}
|
||||
return { plot0, plot1, plot2 };
|
||||
}
|
||||
|
||||
/** Wilder RMA — TradingView ta.rma / oakscriptjs 와 동일 시드·유지 */
|
||||
function rmaWilder(values: Array<number | null>, period: number): number[] {
|
||||
const len = Math.max(1, Math.floor(period));
|
||||
const alpha = 1 / len;
|
||||
const out = new Array<number>(values.length).fill(NaN);
|
||||
let initSum = 0;
|
||||
let validCount = 0;
|
||||
let firstIdx = -1;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const v = values[i];
|
||||
if (v != null && Number.isFinite(v)) {
|
||||
initSum += v;
|
||||
validCount++;
|
||||
if (validCount === len) {
|
||||
firstIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let rmaVal = validCount > 0 ? initSum / validCount : NaN;
|
||||
const initialized = firstIdx >= 0;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (!initialized || i < firstIdx) {
|
||||
out[i] = NaN;
|
||||
} else if (i === firstIdx) {
|
||||
out[i] = rmaVal;
|
||||
} else {
|
||||
const v = values[i];
|
||||
if (v != null && Number.isFinite(v)) {
|
||||
rmaVal = alpha * v + (1 - alpha) * rmaVal;
|
||||
}
|
||||
out[i] = rmaVal;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** ADXR = (ADX + ADX[n]) / 2 (Wilder·업비트) */
|
||||
function calcAdxr(adx: number[], lag: number): number[] {
|
||||
return adx.map((cur, i) => {
|
||||
if (!Number.isFinite(cur) || i < lag) return NaN;
|
||||
const prev = adx[i - lag];
|
||||
return Number.isFinite(prev) ? (cur + prev) / 2 : NaN;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DMI (업비트) — +DI, -DI, DX, ADX, ADXR
|
||||
* 파라미터: diLength(DI 길이), adxSmoothing(ADX 스무딩)
|
||||
*/
|
||||
export function calcDMI(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const diLength = Math.max(1, Number(params.diLength ?? params.length ?? 14));
|
||||
const adxSmoothing = Math.max(1, Number(params.adxSmoothing ?? 14));
|
||||
const len = bars.length;
|
||||
|
||||
const plusDM: Array<number | null> = [];
|
||||
const minusDM: Array<number | null> = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (i === 0) {
|
||||
plusDM.push(null);
|
||||
minusDM.push(null);
|
||||
} else {
|
||||
const upMove = bars[i].high - bars[i - 1].high;
|
||||
const downMove = bars[i - 1].low - bars[i].low;
|
||||
plusDM.push(upMove > downMove && upMove > 0 ? upMove : 0);
|
||||
minusDM.push(downMove > upMove && downMove > 0 ? downMove : 0);
|
||||
}
|
||||
}
|
||||
|
||||
const tr: Array<number | null> = bars.map((b, i) => {
|
||||
if (i === 0) return null;
|
||||
const hl = b.high - b.low;
|
||||
const hc = Math.abs(b.high - bars[i - 1].close);
|
||||
const lc = Math.abs(b.low - bars[i - 1].close);
|
||||
return Math.max(hl, hc, lc);
|
||||
});
|
||||
|
||||
const smPlusDM = rmaWilder(plusDM, diLength);
|
||||
const smMinusDM = rmaWilder(minusDM, diLength);
|
||||
const smTR = rmaWilder(tr, diLength);
|
||||
|
||||
let lastPlus = 0;
|
||||
let lastMinus = 0;
|
||||
const plusDI: number[] = [];
|
||||
const minusDI: number[] = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
const t = smTR[i];
|
||||
const pDm = smPlusDM[i];
|
||||
const mDm = smMinusDM[i];
|
||||
if (!Number.isFinite(t) || t === 0 || !Number.isFinite(pDm)) {
|
||||
plusDI.push(lastPlus);
|
||||
} else {
|
||||
const val = (100 * pDm) / t;
|
||||
plusDI.push(val);
|
||||
lastPlus = val;
|
||||
}
|
||||
if (!Number.isFinite(t) || t === 0 || !Number.isFinite(mDm)) {
|
||||
minusDI.push(lastMinus);
|
||||
} else {
|
||||
const val = (100 * mDm) / t;
|
||||
minusDI.push(val);
|
||||
lastMinus = val;
|
||||
}
|
||||
}
|
||||
|
||||
const dx: number[] = plusDI.map((p, i) => {
|
||||
const m = minusDI[i];
|
||||
const sum = p + m;
|
||||
return sum === 0 ? 0 : (100 * Math.abs(p - m)) / sum;
|
||||
});
|
||||
|
||||
const adx = rmaWilder(dx, adxSmoothing);
|
||||
const adxr = calcAdxr(adx, adxSmoothing);
|
||||
|
||||
return {
|
||||
plot0: toPlotData(bars, plusDI),
|
||||
plot1: toPlotData(bars, minusDI),
|
||||
plot2: toPlotData(bars, dx),
|
||||
plot3: toPlotData(bars, adx),
|
||||
plot4: toPlotData(bars, adxr),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user