Paper_Trading / frontend /src /lib /indicators.ts
superxuu
feat: Complete stock trading simulation system
2d4ab0a
/**
* 技术指标计算库
*/
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;
}