/** * 技术指标计算(纯函数,前端本地计算) * 输入 K 线数组(时间升序),输出与输入等长、前段为 null 的指标序列。 */ export interface KBar { time: string; open: number; close: number; high: number; low: number; volume: number; } export type Nums = (number | null)[]; /** 简单移动平均 */ export function sma(closes: number[], period: number): Nums { const out: Nums = new Array(closes.length).fill(null); let sum = 0; for (let i = 0; i < closes.length; i++) { sum += closes[i]; if (i >= period) sum -= closes[i - period]; if (i >= period - 1) out[i] = sum / period; } return out; } /** EMA(标准 MACD 用的指数平滑) */ export function ema(values: number[], period: number): Nums { const out: Nums = new Array(values.length).fill(null); const k = 2 / (period + 1); let prev: number | null = null; for (let i = 0; i < values.length; i++) { if (i === period - 1) { // 首值取前 period 个的 SMA let s = 0; for (let j = 0; j < period; j++) s += values[j]; prev = s / period; out[i] = prev; } else if (prev !== null) { prev = values[i] * k + prev * (1 - k); out[i] = prev; } } return out; } /** MACD(12,26,9):返回 DIF、DEA、MACD 柱(柱 = (DIF-DEA)*2,国内口径) */ export function macd( closes: number[], fast = 12, slow = 26, signal = 9, ): { dif: Nums; dea: Nums; hist: Nums } { const ef = ema(closes, fast); const es = ema(closes, slow); const dif: Nums = closes.map((_, i) => ef[i] != null && es[i] != null ? (ef[i] as number) - (es[i] as number) : null, ); // DEA = DIF 的 9 日 EMA(跳过 null) const dea: Nums = new Array(closes.length).fill(null); const difVals: number[] = []; const difIdx: number[] = []; for (let i = 0; i < dif.length; i++) { if (dif[i] != null) { difVals.push(dif[i] as number); difIdx.push(i); } } const deaVals = ema(difVals, signal); for (let j = 0; j < difIdx.length; j++) { if (deaVals[j] != null) dea[difIdx[j]] = deaVals[j]; } const hist: Nums = closes.map((_, i) => dif[i] != null && dea[i] != null ? ((dif[i] as number) - (dea[i] as number)) * 2 : null, ); return { dif, dea, hist }; } /** RSI(Wilder 平滑,国内软件常用 SMA(X,N,1) 等价 Wilder) */ export function rsi(closes: number[], period = 14): Nums { const out: Nums = new Array(closes.length).fill(null); if (closes.length <= period) return out; let avgGain = 0; let avgLoss = 0; // 首段:前 period 个变动的简单均值 for (let i = 1; i <= period; i++) { const ch = closes[i] - closes[i - 1]; if (ch > 0) avgGain += ch; else avgLoss -= ch; } avgGain /= period; avgLoss /= period; out[period] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss); // Wilder 平滑 for (let i = period + 1; i < closes.length; i++) { const ch = closes[i] - closes[i - 1]; const gain = ch > 0 ? ch : 0; const loss = ch < 0 ? -ch : 0; avgGain = (avgGain * (period - 1) + gain) / period; avgLoss = (avgLoss * (period - 1) + loss) / period; out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss); } return out; } /** KDJ(9,3,3) */ export function kdj( bars: KBar[], n = 9, m1 = 3, m2 = 3, ): { k: Nums; d: Nums; j: Nums } { const len = bars.length; const k: Nums = new Array(len).fill(null); const d: Nums = new Array(len).fill(null); const j: Nums = new Array(len).fill(null); let prevK = 50; let prevD = 50; for (let i = 0; i < len; i++) { const from = Math.max(0, i - n + 1); let hh = -Infinity; let ll = Infinity; for (let x = from; x <= i; x++) { hh = Math.max(hh, bars[x].high); ll = Math.min(ll, bars[x].low); } const rsv = hh === ll ? 50 : ((bars[i].close - ll) / (hh - ll)) * 100; const kv = (2 * prevK + rsv) / m1; // 国内口径 K = 2/3 前值 + 1/3 RSV const dv = (2 * prevD + kv) / m2; prevK = kv; prevD = dv; k[i] = kv; d[i] = dv; j[i] = 3 * kv - 2 * dv; } return { k, d, j }; }