herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
552 lines (459 loc) • 17.4 kB
JavaScript
/**
* Technical Analysis module for herta.js
* Provides mathematical functions for technical analysis indicators and patterns
* used in forex and cryptocurrency trading
*/
const statistics = require('../core/statistics');
const arithmetic = require('../core/arithmetic');
const technicalAnalysis = {};
/**
* Calculate Simple Moving Average (SMA)
* @param {Array} prices - Array of price data
* @param {number} period - Period for the moving average
* @returns {Array} - Simple Moving Average values
*/
technicalAnalysis.sma = function (prices, period) {
if (period <= 0 || period > prices.length) {
throw new Error(`Invalid period: ${period}`);
}
const result = [];
for (let i = period - 1; i < prices.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
sum += prices[i - j];
}
result.push(sum / period);
}
return result;
};
/**
* Calculate Exponential Moving Average (EMA)
* @param {Array} prices - Array of price data
* @param {number} period - Period for the moving average
* @returns {Array} - Exponential Moving Average values
*/
technicalAnalysis.ema = function (prices, period) {
if (period <= 0 || period > prices.length) {
throw new Error(`Invalid period: ${period}`);
}
const k = 2 / (period + 1);
const result = [prices[0]];
for (let i = 1; i < prices.length; i++) {
result.push(prices[i] * k + result[i - 1] * (1 - k));
}
return result;
};
/**
* Calculate Relative Strength Index (RSI)
* @param {Array} prices - Array of price data
* @param {number} period - Period for RSI calculation (typically 14)
* @returns {Array} - RSI values
*/
technicalAnalysis.rsi = function (prices, period = 14) {
if (period <= 0 || period >= prices.length) {
throw new Error(`Invalid period: ${period}`);
}
const gains = [];
const losses = [];
// Calculate price changes
for (let i = 1; i < prices.length; i++) {
const change = prices[i] - prices[i - 1];
gains.push(change > 0 ? change : 0);
losses.push(change < 0 ? -change : 0);
}
// Calculate average gains and losses
const avgGains = [];
const avgLosses = [];
// First average is simple average
let gainSum = 0;
let lossSum = 0;
for (let i = 0; i < period; i++) {
gainSum += gains[i];
lossSum += losses[i];
}
avgGains.push(gainSum / period);
avgLosses.push(lossSum / period);
// Subsequent averages use smoothing formula
for (let i = period; i < gains.length; i++) {
avgGains.push((avgGains[avgGains.length - 1] * (period - 1) + gains[i]) / period);
avgLosses.push((avgLosses[avgLosses.length - 1] * (period - 1) + losses[i]) / period);
}
// Calculate RS and RSI
const rsi = [];
for (let i = 0; i < avgGains.length; i++) {
if (avgLosses[i] === 0) {
rsi.push(100);
} else {
const rs = avgGains[i] / avgLosses[i];
rsi.push(100 - (100 / (1 + rs)));
}
}
return rsi;
};
/**
* Calculate Moving Average Convergence Divergence (MACD)
* @param {Array} prices - Array of price data
* @param {number} fastPeriod - Fast EMA period (typically 12)
* @param {number} slowPeriod - Slow EMA period (typically 26)
* @param {number} signalPeriod - Signal EMA period (typically 9)
* @returns {Object} - MACD line, signal line, and histogram
*/
technicalAnalysis.macd = function (prices, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
if (slowPeriod <= fastPeriod || prices.length <= slowPeriod) {
throw new Error('Invalid MACD parameters');
}
// Calculate fast and slow EMAs
const fastEMA = this.ema(prices, fastPeriod);
const slowEMA = this.ema(prices, slowPeriod);
// Calculate MACD line (fast EMA - slow EMA)
const macdLine = [];
// Align MACD line with the slow EMA (which starts later)
const diff = slowPeriod - fastPeriod;
for (let i = 0; i < fastEMA.length - diff; i++) {
macdLine.push(fastEMA[i + diff] - slowEMA[i]);
}
// Calculate signal line (EMA of MACD line)
const signalLine = this.ema(macdLine, signalPeriod);
// Calculate histogram (MACD line - signal line)
const histogram = [];
for (let i = 0; i < signalLine.length; i++) {
histogram.push(macdLine[i + signalPeriod - 1] - signalLine[i]);
}
return {
macdLine,
signalLine,
histogram
};
};
/**
* Calculate Bollinger Bands
* @param {Array} prices - Array of price data
* @param {number} period - Period for moving average (typically 20)
* @param {number} stdDevMultiplier - Multiplier for standard deviation (typically 2)
* @returns {Object} - Upper band, middle band (SMA), and lower band
*/
technicalAnalysis.bollingerBands = function (prices, period = 20, stdDevMultiplier = 2) {
if (period <= 0 || period >= prices.length) {
throw new Error(`Invalid period: ${period}`);
}
const middleBand = this.sma(prices, period);
const upperBand = [];
const lowerBand = [];
for (let i = period - 1; i < prices.length; i++) {
// Calculate standard deviation for the period
let sum = 0;
for (let j = 0; j < period; j++) {
sum += (prices[i - j] - middleBand[i - (period - 1)]) ** 2;
}
const stdDev = Math.sqrt(sum / period);
// Calculate upper and lower bands
upperBand.push(middleBand[i - (period - 1)] + stdDevMultiplier * stdDev);
lowerBand.push(middleBand[i - (period - 1)] - stdDevMultiplier * stdDev);
}
return {
upperBand,
middleBand,
lowerBand
};
};
/**
* Calculate Average True Range (ATR)
* @param {Array} highPrices - Array of high prices
* @param {Array} lowPrices - Array of low prices
* @param {Array} closePrices - Array of closing prices
* @param {number} period - Period for ATR calculation (typically 14)
* @returns {Array} - ATR values
*/
technicalAnalysis.atr = function (highPrices, lowPrices, closePrices, period = 14) {
if (period <= 0 || highPrices.length !== lowPrices.length || highPrices.length !== closePrices.length) {
throw new Error('Invalid inputs for ATR calculation');
}
// Calculate true ranges
const trueRanges = [highPrices[0] - lowPrices[0]]; // Initial TR is just the range
for (let i = 1; i < highPrices.length; i++) {
const tr1 = highPrices[i] - lowPrices[i]; // Current high - current low
const tr2 = Math.abs(highPrices[i] - closePrices[i - 1]); // Current high - previous close
const tr3 = Math.abs(lowPrices[i] - closePrices[i - 1]); // Current low - previous close
trueRanges.push(Math.max(tr1, tr2, tr3));
}
// Calculate ATR using Wilder's smoothing method
const atr = [trueRanges.slice(0, period).reduce((a, b) => a + b, 0) / period];
for (let i = period; i < trueRanges.length; i++) {
atr.push((atr[atr.length - 1] * (period - 1) + trueRanges[i]) / period);
}
return atr;
};
/**
* Calculate Stochastic Oscillator
* @param {Array} highPrices - Array of high prices
* @param {Array} lowPrices - Array of low prices
* @param {Array} closePrices - Array of closing prices
* @param {number} kPeriod - %K period (typically 14)
* @param {number} dPeriod - %D period (typically 3)
* @returns {Object} - %K and %D values
*/
technicalAnalysis.stochasticOscillator = function (highPrices, lowPrices, closePrices, kPeriod = 14, dPeriod = 3) {
if (kPeriod <= 0 || dPeriod <= 0 || highPrices.length !== lowPrices.length || highPrices.length !== closePrices.length) {
throw new Error('Invalid inputs for Stochastic Oscillator calculation');
}
const kValues = [];
// Calculate %K values
for (let i = kPeriod - 1; i < closePrices.length; i++) {
// Find highest high and lowest low in the period
let highestHigh = -Infinity;
let lowestLow = Infinity;
for (let j = 0; j < kPeriod; j++) {
highestHigh = Math.max(highestHigh, highPrices[i - j]);
lowestLow = Math.min(lowestLow, lowPrices[i - j]);
}
// Calculate %K: (Current Close - Lowest Low) / (Highest High - Lowest Low) * 100
if (highestHigh === lowestLow) {
kValues.push(50); // If price doesn't change, default to middle value
} else {
kValues.push(((closePrices[i] - lowestLow) / (highestHigh - lowestLow)) * 100);
}
}
// Calculate %D values (SMA of %K)
const dValues = this.sma(kValues, dPeriod);
return {
k: kValues,
d: dValues
};
};
/**
* Calculate Fibonacci Retracement levels
* @param {number} high - High price of the trend
* @param {number} low - Low price of the trend
* @returns {Object} - Fibonacci retracement levels
*/
technicalAnalysis.fibonacciRetracement = function (high, low) {
if (high <= low) {
throw new Error('High price must be greater than low price');
}
const diff = high - low;
return {
level0: high, // 0% retracement (high)
level236: high - 0.236 * diff, // 23.6% retracement
level382: high - 0.382 * diff, // 38.2% retracement
level500: high - 0.5 * diff, // 50% retracement
level618: high - 0.618 * diff, // 61.8% retracement
level786: high - 0.786 * diff, // 78.6% retracement
level1000: low // 100% retracement (low)
};
};
/**
* Calculate Ichimoku Cloud components
* @param {Array} highPrices - Array of high prices
* @param {Array} lowPrices - Array of low prices
* @param {Array} closePrices - Array of closing prices
* @param {number} tenkanPeriod - Tenkan-sen period (typically 9)
* @param {number} kijunPeriod - Kijun-sen period (typically 26)
* @param {number} senkouBPeriod - Senkou Span B period (typically 52)
* @returns {Object} - Ichimoku Cloud components
*/
technicalAnalysis.ichimokuCloud = function (highPrices, lowPrices, closePrices, tenkanPeriod = 9, kijunPeriod = 26, senkouBPeriod = 52) {
if (highPrices.length !== lowPrices.length || highPrices.length !== closePrices.length) {
throw new Error('Input arrays must have the same length');
}
// Tenkan-sen (Conversion Line): (highest high + lowest low) / 2 for tenkanPeriod
const tenkanSen = [];
for (let i = tenkanPeriod - 1; i < highPrices.length; i++) {
let highestHigh = -Infinity;
let lowestLow = Infinity;
for (let j = 0; j < tenkanPeriod; j++) {
highestHigh = Math.max(highestHigh, highPrices[i - j]);
lowestLow = Math.min(lowestLow, lowPrices[i - j]);
}
tenkanSen.push((highestHigh + lowestLow) / 2);
}
// Kijun-sen (Base Line): (highest high + lowest low) / 2 for kijunPeriod
const kijunSen = [];
for (let i = kijunPeriod - 1; i < highPrices.length; i++) {
let highestHigh = -Infinity;
let lowestLow = Infinity;
for (let j = 0; j < kijunPeriod; j++) {
highestHigh = Math.max(highestHigh, highPrices[i - j]);
lowestLow = Math.min(lowestLow, lowPrices[i - j]);
}
kijunSen.push((highestHigh + lowestLow) / 2);
}
// Senkou Span A (Leading Span A): (Tenkan-sen + Kijun-sen) / 2, shifted forward kijunPeriod bars
const senkouSpanA = [];
const minTenkanKijunLength = Math.min(tenkanSen.length, kijunSen.length);
for (let i = 0; i < minTenkanKijunLength; i++) {
senkouSpanA.push((tenkanSen[i] + kijunSen[i]) / 2);
}
// Senkou Span B (Leading Span B): (highest high + lowest low) / 2 for senkouBPeriod, shifted forward kijunPeriod bars
const senkouSpanB = [];
for (let i = senkouBPeriod - 1; i < highPrices.length; i++) {
let highestHigh = -Infinity;
let lowestLow = Infinity;
for (let j = 0; j < senkouBPeriod; j++) {
highestHigh = Math.max(highestHigh, highPrices[i - j]);
lowestLow = Math.min(lowestLow, lowPrices[i - j]);
}
senkouSpanB.push((highestHigh + lowestLow) / 2);
}
// Chikou Span (Lagging Span): Current closing price, shifted backwards by kijunPeriod bars
const chikouSpan = [...closePrices];
return {
tenkanSen,
kijunSen,
senkouSpanA,
senkouSpanB,
chikouSpan
};
};
/**
* Detect support and resistance levels using Pivot Points
* @param {number} high - High price of the period
* @param {number} low - Low price of the period
* @param {number} close - Closing price of the period
* @returns {Object} - Pivot point, support levels, and resistance levels
*/
technicalAnalysis.pivotPoints = function (high, low, close) {
// Calculate pivot point
const pivot = (high + low + close) / 3;
// Calculate support levels
const s1 = 2 * pivot - high;
const s2 = pivot - (high - low);
const s3 = low - 2 * (high - pivot);
// Calculate resistance levels
const r1 = 2 * pivot - low;
const r2 = pivot + (high - low);
const r3 = high + 2 * (pivot - low);
return {
pivot,
resistance: { r1, r2, r3 },
support: { s1, s2, s3 }
};
};
/**
* Calculate On-Balance Volume (OBV)
* @param {Array} closePrices - Array of closing prices
* @param {Array} volumes - Array of volume data
* @returns {Array} - OBV values
*/
technicalAnalysis.obv = function (closePrices, volumes) {
if (closePrices.length !== volumes.length) {
throw new Error('Price and volume arrays must have the same length');
}
const obv = [volumes[0]]; // Initial OBV is just the first volume
for (let i = 1; i < closePrices.length; i++) {
if (closePrices[i] > closePrices[i - 1]) {
// Price up, add volume
obv.push(obv[obv.length - 1] + volumes[i]);
} else if (closePrices[i] < closePrices[i - 1]) {
// Price down, subtract volume
obv.push(obv[obv.length - 1] - volumes[i]);
} else {
// Price unchanged, OBV unchanged
obv.push(obv[obv.length - 1]);
}
}
return obv;
};
/**
* Calculate Parabolic SAR (Stop and Reverse)
* @param {Array} highPrices - Array of high prices
* @param {Array} lowPrices - Array of low prices
* @param {number} initialAF - Initial acceleration factor (typically 0.02)
* @param {number} maxAF - Maximum acceleration factor (typically 0.2)
* @returns {Array} - Parabolic SAR values
*/
technicalAnalysis.parabolicSar = function (highPrices, lowPrices, initialAF = 0.02, maxAF = 0.2) {
if (highPrices.length !== lowPrices.length || highPrices.length < 2) {
throw new Error('Invalid inputs for Parabolic SAR calculation');
}
const sar = [];
let isUptrend = true; // Initial trend (assume uptrend)
let ep = highPrices[0]; // Extreme point
let af = initialAF; // Acceleration factor
// Start with second bar as PSAR needs a prior bar
sar.push(lowPrices[0]); // Initial SAR is the first low (for uptrend)
for (let i = 1; i < highPrices.length; i++) {
// Calculate current SAR
const prevSar = sar[sar.length - 1];
let currentSar = prevSar + af * (ep - prevSar);
// Ensure SAR doesn't penetrate the previous two candles
if (isUptrend) {
currentSar = Math.min(currentSar, lowPrices[i - 1], i >= 2 ? lowPrices[i - 2] : lowPrices[i - 1]);
} else {
currentSar = Math.max(currentSar, highPrices[i - 1], i >= 2 ? highPrices[i - 2] : highPrices[i - 1]);
}
// Check for trend reversal
if ((isUptrend && currentSar > lowPrices[i]) || (!isUptrend && currentSar < highPrices[i])) {
// Trend reversal
isUptrend = !isUptrend;
currentSar = isUptrend ? Math.min(lowPrices[i - 1], lowPrices[i]) : Math.max(highPrices[i - 1], highPrices[i]);
ep = isUptrend ? highPrices[i] : lowPrices[i];
af = initialAF;
} else {
// No reversal
if (isUptrend) {
if (highPrices[i] > ep) {
ep = highPrices[i];
af = Math.min(af + initialAF, maxAF); // Increment AF
}
} else if (lowPrices[i] < ep) {
ep = lowPrices[i];
af = Math.min(af + initialAF, maxAF); // Increment AF
}
}
sar.push(currentSar);
}
return sar;
};
/**
* Detect Double Top pattern
* @param {Array} prices - Array of price data
* @param {number} tolerance - Percentage tolerance for top equality (e.g., 0.02 for 2%)
* @returns {Array} - Array of indices where double tops occur
*/
technicalAnalysis.detectDoubleTop = function (prices, tolerance = 0.02) {
const peaks = [];
const doubleTops = [];
// Find local peaks (price is higher than both neighbors)
for (let i = 1; i < prices.length - 1; i++) {
if (prices[i] > prices[i - 1] && prices[i] > prices[i + 1]) {
peaks.push(i);
}
}
// Look for pairs of peaks with similar heights and a valley between
for (let i = 0; i < peaks.length - 1; i++) {
const peak1 = peaks[i];
const peak1Price = prices[peak1];
for (let j = i + 1; j < peaks.length; j++) {
const peak2 = peaks[j];
const peak2Price = prices[peak2];
// Check if peaks are similar in height
const priceDiff = Math.abs(peak1Price - peak2Price) / peak1Price;
if (priceDiff <= tolerance) {
// Find lowest price between the peaks
let valley = Infinity;
let valleyIndex = -1;
for (let k = peak1 + 1; k < peak2; k++) {
if (prices[k] < valley) {
valley = prices[k];
valleyIndex = k;
}
}
// Check if valley is significantly lower than peaks
const valleyDepth1 = (peak1Price - valley) / peak1Price;
const valleyDepth2 = (peak2Price - valley) / peak2Price;
if (valleyDepth1 > 0.05 && valleyDepth2 > 0.05) {
doubleTops.push({
peak1Index: peak1,
peak2Index: peak2,
valleyIndex,
peak1Price,
peak2Price,
valleyPrice: valley
});
}
}
}
}
return doubleTops;
};
module.exports = technicalAnalysis;