feat: 详情页K线增加技术指标 MA/MACD/RSI/KDJ
- 新增 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),主图不被压缩 - 指标全部前端本地计算,无需后端
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 技术指标计算(纯函数,前端本地计算)
|
||||
* 输入 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user