/** * 技术指标计算库 */ import { KLine } from './api'; export interface IndicatorData { time: string; value: number; [key: string]: any; } // 移动平均线 (MA) export function calculateMA(data: KLine[], dayCount: number): IndicatorData[] { const result: IndicatorData[] = []; for (let i = 0; i < data.length; i++) { if (i < dayCount - 1) { result.push({ time: data[i].date, value: NaN }); continue; } let sum = 0; for (let j = 0; j < dayCount; j++) { sum += data[i - j].close; } result.push({ time: data[i].date, value: sum / dayCount }); } return result; } // 布林带 (BOLL) export function calculateBOLL(data: KLine[], period: number = 20, multiplier: number = 2) { const ma = calculateMA(data, period); const result = []; for (let i = 0; i < data.length; i++) { if (i < period - 1) { result.push({ time: data[i].date, up: NaN, mb: NaN, dn: NaN }); continue; } let sumSqDiff = 0; const mb = ma[i].value; for (let j = 0; j < period; j++) { sumSqDiff += Math.pow(data[i - j].close - mb, 2); } const stdDev = Math.sqrt(sumSqDiff / period); const up = mb + multiplier * stdDev; const dn = mb - multiplier * stdDev; result.push({ time: data[i].date, up, mb, dn }); } return result; } // 轨道线 (ENE) export function calculateENE(data: KLine[], period: number = 25, m1: number = 11, m2: number = 9) { const ma = calculateMA(data, period); const result = []; for (let i = 0; i < data.length; i++) { if (i < period - 1) { result.push({ time: data[i].date, up: NaN, mb: NaN, dn: NaN }); continue; } const mb = ma[i].value; const up = mb * (1 + m1 / 100); const dn = mb * (1 - m2 / 100); result.push({ time: data[i].date, up, mb, dn }); } return result; } // MACD export function calculateMACD(data: KLine[], short: number = 12, long: number = 26, mid: number = 9) { const result = []; let emaShort = 0; let emaLong = 0; let dea = 0; for (let i = 0; i < data.length; i++) { const close = data[i].close; if (i === 0) { emaShort = close; emaLong = close; dea = 0; } else { emaShort = (2 * close + (short - 1) * emaShort) / (short + 1); emaLong = (2 * close + (long - 1) * emaLong) / (long + 1); } const diff = emaShort - emaLong; if (i === 0) dea = diff; else dea = (2 * diff + (mid - 1) * dea) / (mid + 1); const macd = 2 * (diff - dea); result.push({ time: data[i].date, diff, dea, macd }); } return result; } // KDJ export function calculateKDJ(data: KLine[], n: number = 9, m1: number = 3, m2: number = 3) { const result = []; let k = 50; let d = 50; for (let i = 0; i < data.length; i++) { let low = data[i].low; let high = data[i].high; // Find min low and max high in last n days for (let j = 0; j < n && i - j >= 0; j++) { low = Math.min(low, data[i - j].low); high = Math.max(high, data[i - j].high); } const rsv = high === low ? 50 : (data[i].close - low) / (high - low) * 100; k = (1 * rsv + (m1 - 1) * k) / m1; d = (1 * k + (m2 - 1) * d) / m2; const jVal = 3 * k - 2 * d; result.push({ time: data[i].date, k, d, j: jVal }); } return result; } // RSI export function calculateRSI(data: KLine[], period: number = 14) { const result = []; let avgGain = 0; let avgLoss = 0; for (let i = 0; i < data.length; i++) { if (i === 0) { result.push({ time: data[i].date, value: NaN }); continue; } const change = data[i].close - data[i - 1].close; const gain = Math.max(0, change); const loss = Math.max(0, -change); if (i < period) { avgGain += gain; avgLoss += loss; result.push({ time: data[i].date, value: NaN }); if (i === period - 1) { avgGain /= period; avgLoss /= period; } } else { avgGain = (avgGain * (period - 1) + gain) / period; avgLoss = (avgLoss * (period - 1) + loss) / period; const rs = avgLoss === 0 ? 100 : avgGain / avgLoss; const rsi = 100 - (100 / (1 + rs)); result.push({ time: data[i].date, value: rsi }); } } return result; } // WR (Williams %R) export function calculateWR(data: KLine[], period: number = 14) { const result = []; for (let i = 0; i < data.length; i++) { if (i < period - 1) { result.push({ time: data[i].date, value: NaN }); continue; } let high = -Infinity; let low = Infinity; for (let j = 0; j < period; j++) { high = Math.max(high, data[i - j].high); low = Math.min(low, data[i - j].low); } const wr = (high - data[i].close) / (high - low) * 100; result.push({ time: data[i].date, value: -wr }); // 通常显示为负数或 0-100 反转,这里用 -0 到 -100 } return result; } // CCI export function calculateCCI(data: KLine[], period: number = 14) { const result = []; const typPrices = data.map(k => (k.high + k.low + k.close) / 3); for (let i = 0; i < data.length; i++) { if (i < period - 1) { result.push({ time: data[i].date, value: NaN }); continue; } let sum = 0; for (let j = 0; j < period; j++) { sum += typPrices[i - j]; } const ma = sum / period; let sumDev = 0; for (let j = 0; j < period; j++) { sumDev += Math.abs(typPrices[i - j] - ma); } const md = sumDev / period; const cci = md === 0 ? 0 : (typPrices[i] - ma) / (0.015 * md); result.push({ time: data[i].date, value: cci }); } return result; }