quantitivecalc
Version:
A TypeScript library providing advanced quantitative finance functions for risk analysis, performance metrics, and technical indicators. (Currently in development)
245 lines (244 loc) • 9.19 kB
JavaScript
;
/**
* Technical Indicators Utilities
*
* Functions:
* - calculateMovingAverage: Calculates simple or exponential moving averages for a given data series.
* - calculateRSI: Calculates Relative Strength Index momentum oscillator
* - calculateMACD: Calculates Moving Average Convergence Divergence
* - calculateBollingerBands: Calculates price channels based on standard deviation
* - calculateStochasticOscillator: Calculates %K and %D momentum indicators
*
* The functions operate on arrays of objects (list of dicts) and allow you to specify source/result columns and parameters.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateMovingAverage = calculateMovingAverage;
exports.calculateRSI = calculateRSI;
exports.calculateMACD = calculateMACD;
exports.calculateBollingerBands = calculateBollingerBands;
exports.calculateStochasticOscillator = calculateStochasticOscillator;
function calculateMovingAverage(data, sourceColumn, resultColumn, windowSize = 20, type = 'simple') {
if (!data || data.length === 0) {
return [];
}
const result = data.map(row => ({ ...row }));
if (type === 'simple') {
// Simple Moving Average
for (let i = 0; i < result.length; i++) {
if (i < windowSize - 1) {
result[i][resultColumn] = null;
}
else {
let sum = 0;
let count = 0;
for (let j = i - windowSize + 1; j <= i; j++) {
const value = result[j][sourceColumn];
if (typeof value === 'number' && !isNaN(value)) {
sum += value;
count++;
}
}
result[i][resultColumn] = count > 0 ? sum / count : null;
}
}
}
else {
// Exponential Moving Average
const multiplier = 2 / (windowSize + 1);
let ema = null;
for (let i = 0; i < result.length; i++) {
const value = result[i][sourceColumn];
if (typeof value === 'number' && !isNaN(value)) {
if (ema === null) {
ema = value; // First valid value becomes initial EMA
}
else {
ema = (value * multiplier) + (ema * (1 - multiplier));
}
result[i][resultColumn] = ema;
}
else {
result[i][resultColumn] = ema; // Keep previous EMA if current value is invalid
}
}
}
return result;
}
function calculateRSI(data, sourceColumn, resultColumn = 'rsi', windowSize = 14) {
if (!data || data.length === 0) {
return [];
}
const result = data.map(row => ({ ...row }));
for (let i = 0; i < result.length; i++) {
if (i < windowSize) {
result[i][resultColumn] = null;
continue;
}
let gains = 0;
let losses = 0;
let gainCount = 0;
let lossCount = 0;
// Calculate gains and losses over the window
for (let j = i - windowSize + 1; j <= i; j++) {
const currentValue = result[j][sourceColumn];
const previousValue = result[j - 1][sourceColumn];
if (typeof currentValue === 'number' && typeof previousValue === 'number' &&
!isNaN(currentValue) && !isNaN(previousValue)) {
const change = currentValue - previousValue;
if (change > 0) {
gains += change;
gainCount++;
}
else if (change < 0) {
losses += Math.abs(change);
lossCount++;
}
}
}
if (gainCount === 0 && lossCount === 0) {
result[i][resultColumn] = null;
continue;
}
const avgGain = gainCount > 0 ? gains / windowSize : 0;
const avgLoss = lossCount > 0 ? losses / windowSize : 0;
if (avgLoss === 0) {
result[i][resultColumn] = 100;
}
else {
const rs = avgGain / avgLoss;
result[i][resultColumn] = 100 - (100 / (1 + rs));
}
}
return result;
}
function calculateMACD(data, sourceColumn, macdColumn = 'macd', signalColumn = 'macd_signal', histogramColumn = 'macd_histogram', fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
if (!data || data.length === 0) {
return [];
}
// Calculate fast and slow EMAs
const withFastEMA = calculateMovingAverage(data, sourceColumn, '__fast_ema', fastPeriod, 'exponential');
const withBothEMAs = calculateMovingAverage(withFastEMA, sourceColumn, '__slow_ema', slowPeriod, 'exponential');
// Calculate MACD line
const result = withBothEMAs.map(row => {
const macdValue = (row.__fast_ema !== null && row.__slow_ema !== null)
? row.__fast_ema - row.__slow_ema
: null;
return {
...row,
[macdColumn]: macdValue,
__temp_macd: macdValue
};
});
// Calculate signal line (EMA of MACD)
const withSignal = calculateMovingAverage(result, '__temp_macd', signalColumn, signalPeriod, 'exponential');
// Calculate histogram and clean up temporary columns
return withSignal.map(row => {
const histogramValue = (row[macdColumn] !== null && row[signalColumn] !== null)
? row[macdColumn] - row[signalColumn]
: null;
const cleanRow = { ...row };
delete cleanRow.__fast_ema;
delete cleanRow.__slow_ema;
delete cleanRow.__temp_macd;
return {
...cleanRow,
[histogramColumn]: histogramValue
};
});
}
function calculateBollingerBands(data, sourceColumn, upperColumn = 'bb_upper', middleColumn = 'bb_middle', lowerColumn = 'bb_lower', windowSize = 20, numStdDev = 2) {
if (!data || data.length === 0) {
return [];
}
// Calculate the middle band (Simple Moving Average)
const withMA = calculateMovingAverage(data, sourceColumn, middleColumn, windowSize, 'simple');
return withMA.map((row, i) => {
if (i < windowSize - 1 || row[middleColumn] === null) {
return {
...row,
[upperColumn]: null,
[lowerColumn]: null
};
}
// Calculate standard deviation for the window
let sumSquaredDeviations = 0;
let count = 0;
const mean = row[middleColumn];
for (let j = i - windowSize + 1; j <= i; j++) {
const value = withMA[j][sourceColumn];
if (typeof value === 'number' && !isNaN(value)) {
sumSquaredDeviations += Math.pow(value - mean, 2);
count++;
}
}
if (count === 0) {
return {
...row,
[upperColumn]: null,
[lowerColumn]: null
};
}
const variance = sumSquaredDeviations / count;
const stdDev = Math.sqrt(variance);
return {
...row,
[upperColumn]: mean + (numStdDev * stdDev),
[lowerColumn]: mean - (numStdDev * stdDev)
};
});
}
function calculateStochasticOscillator(data, highColumn, lowColumn, closeColumn, kColumn = 'stoch_k', dColumn = 'stoch_d', kPeriod = 14, dPeriod = 3) {
if (!data || data.length === 0) {
return [];
}
// Calculate %K
const withK = data.map((row, i) => {
if (i < kPeriod - 1) {
return {
...row,
[kColumn]: null
};
}
let highestHigh = -Infinity;
let lowestLow = Infinity;
let validData = false;
// Find highest high and lowest low over the period
for (let j = i - kPeriod + 1; j <= i; j++) {
const high = data[j][highColumn];
const low = data[j][lowColumn];
if (typeof high === 'number' && typeof low === 'number' &&
!isNaN(high) && !isNaN(low)) {
highestHigh = Math.max(highestHigh, high);
lowestLow = Math.min(lowestLow, low);
validData = true;
}
}
if (!validData || highestHigh === lowestLow) {
return {
...row,
[kColumn]: null
};
}
const currentClose = row[closeColumn];
if (typeof currentClose !== 'number' || isNaN(currentClose)) {
return {
...row,
[kColumn]: null
};
}
const kValue = ((currentClose - lowestLow) / (highestHigh - lowestLow)) * 100;
return {
...row,
[kColumn]: kValue,
__temp_k: kValue
};
});
// Calculate %D (Simple Moving Average of %K)
const withD = calculateMovingAverage(withK, '__temp_k', dColumn, dPeriod, 'simple');
// Clean up temporary column
return withD.map(row => {
const cleanRow = { ...row };
delete cleanRow.__temp_k;
return cleanRow;
});
}