stock-indicator
Version:
Stock Indicator Library
384 lines (362 loc) • 11.6 kB
text/typescript
export type TMixed = number | null; // - mixed type
/**
* - check if the parameter is an array of numbers or null values
* @param {number[]} arr - array to be checked
* @param {string} parameName - name of the parameter
*/
export function checkArrayOfNumbers(arr: number[], parameName: string) {
if (!Array.isArray(arr) || arr.some((item) => typeof item !== "number")) {
throw new Error(`The ${parameName} parameter must be an array of numbers`);
}
}
/**
* - check if the parameter is a number between min and max
* @param {number} num - number to be checked
* @param {number} min - minimum value
* @param {number} max - maximum value
* @param {string} parameName - name of the parameter
* @param {string} maxName - name of the array
*/
export function checkNumber(
num: number,
min: number,
max: TMixed[] | number,
parameName: string,
maxName: string
) {
const maxStr = Array.isArray(max)
? `the length of the ${maxName} array`
: maxName;
if (
typeof num !== "number" ||
num < min ||
num > (Array.isArray(max) ? max.length : max)
) {
throw new Error(
`The ${parameName} parameter must be an array of numbers between ${min} and ${maxStr}`
);
}
}
/**
* - Sum of n-period moving averages (SUM)
* @param {TMixed[]} arr - closing prices of the stock
* @param {number} n - number of periods to calculate the SUM, must be greater than 1 and less than the length of the CLOSE array
*/
export function funMS(arr: TMixed[], n: number): TMixed[] {
const doSum = (tArr: TMixed[]): TMixed => {
if (tArr.every((item) => item === null || isNaN(item))) {
return null;
}
return tArr.reduce(
(acc: number, cur) => (cur !== null && !isNaN(cur) ? acc + cur : acc),
0
);
};
// calculate the SUM for each period, starting from the second period
const result: TMixed[] = [];
for (let i = 0; i < arr.length; i++) {
result.push(doSum(arr.slice(Math.max(0, i - n + 1), i + 1)));
}
return result;
}
/**
* - Moving Average (MA)
* @param {TMixed[]} arr - - closing prices of the stock
* @param {number} n - - number of periods to calculate the moving average, must be between 2 and the length of the CLOSE array
* @returns { TMixed[] } } - array of moving average values
*/
export function funMA(arr: TMixed[], n: number): TMixed[] {
const result: TMixed[] = [];
//对给定的数组arr进行检查,如果包含null,则返回null,否则计算n个周期的移动平均值
const doMA = (iarr: TMixed[], count: number) => {
if (iarr.length < count) {
return null;
}
let sum = 0;
for (let i = 0; i < iarr.length; i++) {
if (typeof iarr[i] === "number" && !isNaN(iarr[i] as number)) {
sum += iarr[i] as number;
} else {
return null;
}
}
return sum / count;
};
for (let i = 0; i < arr.length; i++) {
result.push(doMA(arr.slice(Math.max(0, i - n + 1), i + 1), n));
}
return result;
}
/**
* SMA (Simple Moving Average)
* @param {TMixed[]} arr - An array of the closing price of the stock
* @param {number} n - The number of periods to calculate the SMA for. Must be between 2 and the length of the CLOSE array.
* @param {number} m - The number of periods to calculate the SMA for. Must be between 0 and N-1.
* @returns {{ SMA: TMixed[] }} - The SMA values for the given data and parameters
*/
export function funSMA(arr: TMixed[], n: number, m: number): TMixed[] {
return arr.slice(1).reduce(
(acc, cur, i) => {
if (cur === null) {
acc.push(null);
} else if (i === 0 && (acc[0] === null || acc[0] === 0)) {
acc.push(cur);
} else {
acc.push((cur * m + (acc[i] as number) * (n - m)) / n);
}
return acc;
},
[arr[0]]
);
}
/**
* - Exponential Moving Average (EMA)
* @param {TMixed[]} arr - closing prices of the stock
* @param {number} n - number of periods to calculate the EMA,must be greater than 1 and less than the length of the CLOSE array
* @param {number} m - number of periods to calculate the initial EMA, default is 1,must be less than N
* @returns {{ EMA: TMixed[] }} - object with the EMA values
*/
export function funEMA(arr: TMixed[], n: number, m: number = 1): TMixed[] {
return arr.slice(1).reduce(
(acc, cur) => {
acc.push(
cur
? (cur * (m + 1) + (acc.slice(-1)[0] ?? cur) * (n - m)) / (n + 1)
: null
);
return acc;
},
[arr[0]]
);
}
/**
* WMA - Weighted Moving Average
* @param {TMixed[]} arr should be an array of numbers or null values
* @param {number} n should be a number between 2 and the length of the data array
* @returns {TMixed[]} WMA values
*/
export function funWMA(arr: TMixed[], n: number): TMixed[] {
// calculate WMA
const cumsum = (arr: TMixed[]): TMixed => {
const sum = arr
.slice(1)
.reduce(
(acc, cur, i) => (acc && cur ? acc + cur * (i + 1) : null),
arr[0]
);
return sum ? sum / ((arr.length * (arr.length + 1)) / 2) : null;
};
return arr.reduce((acc: TMixed[], cur, i) => {
if (i < n - 1) {
acc.push(null);
} else {
const res = cumsum(arr.slice(i - n + 1, i + 1));
acc.push(res);
}
return acc;
}, []);
}
/**
* - Standard Deviation (STD)
* @param {TMixed[]} arr - closing prices of the stock
* @param {number} n - number of periods to calculate the STD, must be greater than 1 and less than the length of the CLOSE array
* @returns { TMixed[] } - array of standard deviation values
*/
export function funSTD(arr: TMixed[], n: number): TMixed[] {
return arr.reduce((acc: TMixed[], cur, i) => {
if (i < n - 1) {
acc.push(null);
} else {
const mean =
arr
.slice(i - n + 1, i + 1)
.reduce((acc: number, cur) => acc + (cur ?? 0), 0) / n;
const variance = arr
.slice(i - n + 1, i + 1)
.reduce(
(acc: number, cur) => acc + ((cur ?? 0) - mean) ** 2 / (n - 1),
0
);
const std = Math.sqrt(variance);
acc.push(std);
}
return acc;
}, []);
}
/**
* - Average Variance Enhanced (AVEDEV)
* @param {TMixed[]} arr - closing prices of the stock
* @param {number} n - number of periods to calculate the AVEDEV, must be greater than 1 and less than the length of the CLOSE array
* @returns { TMixed[] } - array of average variance enhanced values
*/
export function funAVEDEV(arr: TMixed[], n: number): TMixed[] {
const result: TMixed[] = [];
for (let i = 0; i < arr.length; i++) {
const curArr = arr.slice(Math.max(0, i - n + 1), i + 1);
if (
curArr.some((item) => item === null || isNaN(item)) ||
curArr.length < n
) {
result.push(null);
continue;
}
const mean: number =
curArr.reduce((acc: number, cur) => acc + (cur ?? 0), 0) / n;
result.push(
(curArr as number[]).reduce(
(acc: number, cur: number) => acc + Math.abs(cur - mean),
0
) / n
);
}
return result;
}
/**
* - subtract two arrays element by element
* @param {TMixed[]} subtractor - array to be subtracted from
* @param {TMixed[]} minuend - array to be subtracted
* @returns {TMixed[]} - array of subtracted values
*/
export function funArrSub(subtractor: TMixed[], minuend: TMixed[]): TMixed[] {
const count = Math.max(subtractor.length, minuend.length);
const results: TMixed[] = [];
for (let i = 0; i < count; i++) {
results.push(
subtractor[i] !== null && minuend[i] !== null
? (subtractor[i] ?? 0) - (minuend[i] ?? 0)
: null
);
}
return results;
}
/**
* - add two or more arrays element by element
* @param {TMixed[]}...args - arrays to be added
* @returns {TMixed[]} - array of added values
*/
export function funArrAdd(...args: TMixed[][]): TMixed[] {
const result: number[] = [];
const maxLength = args.reduce((p, v) => Math.max(p, v.length), 0);
for (let i = 0; i < maxLength; i++) {
let sum = 0;
for (let j = 0; j < args.length; j++) {
if (i < args[j].length) {
sum += args[j][i] ?? 0;
}
}
result.push(sum);
}
return result;
}
/**
* - divide two arrays element by element
* @param {TMixed[]} divisor - array to be divided
* @param {TMixed[]} dividend - array to divide by
* @returns {TMixed[]}- array of divided values
*/
export function funArrDiv(divisor: TMixed[], dividend: TMixed[]): TMixed[] {
const count = Math.max(divisor.length, dividend.length);
const results: TMixed[] = [];
for (let i = 0; i < count; i++) {
if (dividend[i] === null || divisor[i] === null || dividend[i] === 0) {
results.push(null);
continue;
} else {
results.push((divisor[i] as number) / (dividend[i] as number));
}
}
return results;
}
/**
* - multiply two arrays element by element
* @param {TMixed[]} arr1 - first array to be multiplied
* @param {TMixed[]} arr2 - second array to be multiplied
* @returns {TMixed[]} - array of multiplied values
*/
export function funArrMul(arr1: TMixed[], arr2: TMixed[]): TMixed[] {
const count = Math.max(arr1.length, arr2.length);
const results: TMixed[] = [];
for (let i = 0; i < count; i++) {
if (arr1[i] === null || arr2[i] === null) {
results.push(null);
continue;
} else {
results.push((arr1[i] as number) * (arr2[i] as number));
}
}
return results;
}
/**
* - absolute value of an array
* @param {TMixed[]} arr - array to be absolute value of
* @returns {TMixed[]} - array of absolute values
*/
export function funArrAbs(arr: TMixed[]): TMixed[] {
return arr.map((v) => (v === null ? null : Math.abs(v)));
}
/**
* - rounded to n decimal places
* @param {number} num - number to be rounded
* @param {number} n - number of decimal places to round to
* @returns {number} - rounded number
*/
export function funRound(num: TMixed, n: number): TMixed {
if (typeof num !== "number" || isNaN(num)) return null;
return Math.round(num * Math.pow(10, n)) / Math.pow(10, n);
}
/**
* - check if a is greater than b with a given precision
* @param {number} a - first number
* @param {number} b - second number
* @param {number} precision - precision to compare the numbers
* @returns {boolean} - true if a is greater than b, false otherwise
*/
export function aGtB(
a: number,
b: number,
precision: number = 0.0001
): boolean {
return a - b > precision;
}
/**
* - check if a is less than b with a given precision
* @param {number} a - first number
* @param {number} b - second number
* @param {number} precision - precision to compare the numbers
* @returns {boolean} - true if a is less than b, false otherwise
*/
export function aLtB(
a: number,
b: number,
precision: number = 0.0001
): boolean {
return b - a > precision;
}
/**
* - check if a is equal to b with a given precision
* @param {number} a - first number
* @param {number} b - second number
* @param {number} precision - precision to compare the numbers
* @returns {boolean} - true if a is equal to b, false otherwise
*/
export function aEqB(
a: number,
b: number,
precision: number = 0.0001
): boolean {
return Math.abs(a - b) <= precision;
}
/**
* - check if a is not equal to b with a given precision
* @param {number} a - first number
* @param {number} b - second number
* @param {number} precision - precision to compare the numbers
* @returns {boolean} - true if a is not equal to b, false otherwise
*/
export function aNeqB(
a: number,
b: number,
precision: number = 0.0001
): boolean {
return Math.abs(a - b) > precision;
}