137 lines
4.1 KiB
TypeScript
137 lines
4.1 KiB
TypeScript
function mean(values: number[]): number {
|
|
if (values.length === 0) return 0;
|
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
}
|
|
|
|
function stddev(values: number[]): number {
|
|
if (values.length === 0) return 0;
|
|
const m = mean(values);
|
|
const v = values.reduce((acc, x) => acc + (x - m) * (x - m), 0) / values.length;
|
|
return Math.sqrt(v);
|
|
}
|
|
|
|
export function sma(values: number[], period: number): Array<number | null> {
|
|
if (period <= 0) throw new Error('period must be > 0');
|
|
const out: Array<number | null> = new Array(values.length).fill(null);
|
|
let sum = 0;
|
|
for (let i = 0; i < values.length; i++) {
|
|
sum += values[i];
|
|
if (i >= period) sum -= values[i - period];
|
|
if (i >= period - 1) out[i] = sum / period;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function ema(values: number[], period: number): Array<number | null> {
|
|
if (period <= 0) throw new Error('period must be > 0');
|
|
const out: Array<number | null> = new Array(values.length).fill(null);
|
|
const k = 2 / (period + 1);
|
|
if (values.length < period) return out;
|
|
|
|
const first = mean(values.slice(0, period));
|
|
out[period - 1] = first;
|
|
let prev = first;
|
|
for (let i = period; i < values.length; i++) {
|
|
const next = values[i] * k + prev * (1 - k);
|
|
out[i] = next;
|
|
prev = next;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function rsi(values: number[], period: number): Array<number | null> {
|
|
if (period <= 0) throw new Error('period must be > 0');
|
|
const out: Array<number | null> = new Array(values.length).fill(null);
|
|
if (values.length <= period) return out;
|
|
|
|
let gains = 0;
|
|
let losses = 0;
|
|
for (let i = 1; i <= period; i++) {
|
|
const change = values[i] - values[i - 1];
|
|
if (change >= 0) gains += change;
|
|
else losses -= change;
|
|
}
|
|
let avgGain = gains / period;
|
|
let avgLoss = losses / period;
|
|
|
|
const rs = avgLoss === 0 ? Number.POSITIVE_INFINITY : avgGain / avgLoss;
|
|
out[period] = 100 - 100 / (1 + rs);
|
|
|
|
for (let i = period + 1; i < values.length; i++) {
|
|
const change = values[i] - values[i - 1];
|
|
const gain = Math.max(change, 0);
|
|
const loss = Math.max(-change, 0);
|
|
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
const rs2 = avgLoss === 0 ? Number.POSITIVE_INFINITY : avgGain / avgLoss;
|
|
out[i] = 100 - 100 / (1 + rs2);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function bollingerBands(values: number[], period: number, stdDevMult: number) {
|
|
if (period <= 0) throw new Error('period must be > 0');
|
|
const upper: Array<number | null> = new Array(values.length).fill(null);
|
|
const lower: Array<number | null> = new Array(values.length).fill(null);
|
|
const mid = sma(values, period);
|
|
|
|
for (let i = period - 1; i < values.length; i++) {
|
|
const window = values.slice(i - period + 1, i + 1);
|
|
const sd = stddev(window);
|
|
const m = mid[i];
|
|
if (m == null) continue;
|
|
upper[i] = m + stdDevMult * sd;
|
|
lower[i] = m - stdDevMult * sd;
|
|
}
|
|
|
|
return { upper, lower, mid };
|
|
}
|
|
|
|
export function macd(values: number[], fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
|
|
const fast = ema(values, fastPeriod);
|
|
const slow = ema(values, slowPeriod);
|
|
const macdLine: Array<number | null> = values.map((_, i) => {
|
|
const f = fast[i];
|
|
const s = slow[i];
|
|
return f == null || s == null ? null : f - s;
|
|
});
|
|
|
|
// EMA over a nullable series, aligned by index.
|
|
const signal: Array<number | null> = new Array(values.length).fill(null);
|
|
const k = 2 / (signalPeriod + 1);
|
|
let seeded = false;
|
|
let prev = 0;
|
|
const buf: number[] = [];
|
|
|
|
for (let i = 0; i < macdLine.length; i++) {
|
|
const v = macdLine[i];
|
|
if (v == null) continue;
|
|
|
|
if (!seeded) {
|
|
buf.push(v);
|
|
if (buf.length === signalPeriod) {
|
|
const first = mean(buf);
|
|
signal[i] = first;
|
|
prev = first;
|
|
seeded = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const next = v * k + prev * (1 - k);
|
|
signal[i] = next;
|
|
prev = next;
|
|
}
|
|
|
|
return { macd: macdLine, signal };
|
|
}
|
|
|
|
export function lastNonNull(values: Array<number | null>): number | null {
|
|
for (let i = values.length - 1; i >= 0; i--) {
|
|
const v = values[i];
|
|
if (v != null && Number.isFinite(v)) return v;
|
|
}
|
|
return null;
|
|
}
|
|
|