goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
+411
View File
@@ -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),
};
}