- 新增 indicators.ts 指标计算库:SMA/EMA/MACD(12,26,9)/RSI(14)/KDJ(9,3,3) - KLineChart 支持指标开关:MA(5/10/20/30) 主图叠加、MACD/RSI/KDJ 独立副图 pane - 图表高度随指标 pane 数量动态增长(主图 300 + 每个副图 95),主图不被压缩 - 指标全部前端本地计算,无需后端
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
/**
|
||
* 技术指标计算(纯函数,前端本地计算)
|
||
* 输入 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 };
|
||
}
|