/**
* Trend Indicators
* ADX, Parabolic SAR, Aroon, Support/Resistance calculations
*/
export function calculateADX(highs, lows, closes, period = 14) {
if (highs.length < period + 1 || lows.length < period + 1 || closes.length < period + 1) {
return { adx: [], plusDI: [], minusDI: [] };
}
// Calculate +DM and -DM (Directional Movement)
const plusDM = [];
const minusDM = [];
for (let i = 1; i < highs.length; i++) {
const highDiff = highs[i] - highs[i - 1];
const lowDiff = lows[i - 1] - lows[i];
if (highDiff > lowDiff && highDiff > 0) {
plusDM.push(highDiff);
minusDM.push(0);
}
else if (lowDiff > highDiff && lowDiff > 0) {
plusDM.push(0);
minusDM.push(lowDiff);
}
else {
plusDM.push(0);
minusDM.push(0);
}
}
// Calculate True Range (same as ATR calculation)
const trueRanges = [];
for (let i = 1; i < closes.length; i++) {
const tr1 = highs[i] - lows[i];
const tr2 = Math.abs(highs[i] - closes[i - 1]);
const tr3 = Math.abs(lows[i] - closes[i - 1]);
trueRanges.push(Math.max(tr1, tr2, tr3));
}
// Calculate smoothed +DM, -DM, and TR using Wilder's smoothing
const smoothedPlusDM = [];
const smoothedMinusDM = [];
const smoothedTR = [];
// Initial values (SMA)
let sumPlusDM = 0;
let sumMinusDM = 0;
let sumTR = 0;
for (let i = 0; i < period && i < plusDM.length; i++) {
sumPlusDM += plusDM[i];
sumMinusDM += minusDM[i];
sumTR += trueRanges[i];
}
if (plusDM.length >= period) {
smoothedPlusDM.push(sumPlusDM / period);
smoothedMinusDM.push(sumMinusDM / period);
smoothedTR.push(sumTR / period);
// Subsequent values use Wilder's smoothing
for (let i = period; i < plusDM.length; i++) {
smoothedPlusDM.push((smoothedPlusDM[smoothedPlusDM.length - 1] * (period - 1) + plusDM[i]) / period);
smoothedMinusDM.push((smoothedMinusDM[smoothedMinusDM.length - 1] * (period - 1) + minusDM[i]) / period);
smoothedTR.push((smoothedTR[smoothedTR.length - 1] * (period - 1) + trueRanges[i]) / period);
}
}
// Calculate +DI and -DI (Directional Indicators)
const plusDI = [];
const minusDI = [];
const dx = [];
const adx = [];
for (let i = 0; i < smoothedTR.length; i++) {
if (smoothedTR[i] > 0) {
plusDI.push((smoothedPlusDM[i] / smoothedTR[i]) * 100);
minusDI.push((smoothedMinusDM[i] / smoothedTR[i]) * 100);
}
else {
plusDI.push(0);
minusDI.push(0);
}
// Calculate DX (Directional Index)
const diSum = plusDI[i] + minusDI[i];
if (diSum > 0) {
const diDiff = Math.abs(plusDI[i] - minusDI[i]);
dx.push((diDiff / diSum) * 100);
}
else {
dx.push(0);
}
}
// Calculate ADX as smoothed average of DX
if (dx.length >= period) {
// Initial ADX (SMA of first period DX values)
let sumDX = 0;
for (let i = 0; i < period; i++) {
sumDX += dx[i];
}
adx.push(sumDX / period);
// Subsequent ADX values use Wilder's smoothing
for (let i = period; i < dx.length; i++) {
adx.push((adx[adx.length - 1] * (period - 1) + dx[i]) / period);
}
}
return {
adx: adx,
plusDI: plusDI,
minusDI: minusDI
};
}
export function calculateParabolicSAR(highs, lows, closes, afStart = 0.02, afIncrement = 0.02, afMax = 0.2) {
if (highs.length < 2 || lows.length < 2 || closes.length < 2) {
return [];
}
const sar = [];
let trend = null; // 1 for uptrend, -1 for downtrend
let ep = null; // Extreme Point
let af = afStart; // Acceleration Factor
let currentSAR = null;
// Initialize
if (closes[1] > closes[0]) {
trend = 1; // Uptrend
ep = highs[1];
currentSAR = lows[0];
}
else {
trend = -1; // Downtrend
ep = lows[1];
currentSAR = highs[0];
}
sar.push(currentSAR);
// Calculate SAR for remaining periods
for (let i = 2; i < closes.length; i++) {
const prevSAR = currentSAR;
const prevEP = ep;
const prevAF = af;
// Calculate new SAR
if (trend === 1) {
// Uptrend
currentSAR = prevSAR + prevAF * (prevEP - prevSAR);
currentSAR = Math.min(currentSAR, lows[i - 1], lows[i - 2] || lows[i - 1]);
// Check for reversal
if (currentSAR >= lows[i]) {
// Reverse to downtrend
trend = -1;
ep = lows[i];
af = afStart;
currentSAR = Math.max(highs[i - 1], highs[i - 2] || highs[i - 1]);
}
else {
// Continue uptrend
if (highs[i] > prevEP) {
ep = highs[i];
af = Math.min(af + afIncrement, afMax);
}
}
}
else {
// Downtrend
currentSAR = prevSAR + prevAF * (prevEP - prevSAR);
currentSAR = Math.max(currentSAR, highs[i - 1], highs[i - 2] || highs[i - 1]);
// Check for reversal
if (currentSAR <= highs[i]) {
// Reverse to uptrend
trend = 1;
ep = highs[i];
af = afStart;
currentSAR = Math.min(lows[i - 1], lows[i - 2] || lows[i - 1]);
}
else {
// Continue downtrend
if (lows[i] < prevEP) {
ep = lows[i];
af = Math.min(af + afIncrement, afMax);
}
}
}
sar.push(currentSAR);
}
return sar;
}
export function calculateAroon(highs, lows, period = 14) {
if (highs.length < period || lows.length < period) {
return { up: [], down: [] };
}
const aroonUp = [];
const aroonDown = [];
for (let i = period - 1; i < highs.length; i++) {
const periodHighs = highs.slice(i - period + 1, i + 1);
const periodLows = lows.slice(i - period + 1, i + 1);
const highestHigh = Math.max(...periodHighs);
const lowestLow = Math.min(...periodLows);
// Find position of highest high and lowest low (most recent occurrence)
let highestIndex = 0;
let lowestIndex = 0;
// Search from most recent to oldest to find the most recent occurrence
for (let j = periodHighs.length - 1; j >= 0; j--) {
// Use tolerance for floating point comparison
if (Math.abs(periodHighs[j] - highestHigh) < 0.0000001) {
highestIndex = j;
break;
}
}
for (let j = periodLows.length - 1; j >= 0; j--) {
if (Math.abs(periodLows[j] - lowestLow) < 0.0000001) {
lowestIndex = j;
break;
}
}
// Calculate Aroon Up and Down
// Aroon Up = ((period - periods since highest high) / period) * 100
const periodsSinceHigh = (period - 1) - highestIndex;
const periodsSinceLow = (period - 1) - lowestIndex;
const aroonUpValue = ((period - periodsSinceHigh) / period) * 100;
const aroonDownValue = ((period - periodsSinceLow) / period) * 100;
aroonUp.push(aroonUpValue);
aroonDown.push(aroonDownValue);
}
return {
up: aroonUp,
down: aroonDown
};
}
export function calculateSupportResistance(highs, lows, closes, lookbackPeriod = 20) {
if (highs.length < lookbackPeriod || lows.length < lookbackPeriod || closes.length < lookbackPeriod) {
return {
support: null,
resistance: null,
pivotPoints: null,
fibonacciLevels: null,
previousHighs: [],
previousLows: [],
swingHighs: [],
swingLows: []
};
}
// Find recent swing highs and lows
const swingHighs = [];
const swingLows = [];
// Simple swing detection: local maxima and minima
for (let i = 2; i < closes.length - 2; i++) {
// Swing high: higher than previous 2 and next 2 candles
if (highs[i] > highs[i - 1] && highs[i] > highs[i - 2] &&
highs[i] > highs[i + 1] && highs[i] > highs[i + 2]) {
swingHighs.push({ price: highs[i], index: i });
}
// Swing low: lower than previous 2 and next 2 candles
if (lows[i] < lows[i - 1] && lows[i] < lows[i - 2] &&
lows[i] < lows[i + 1] && lows[i] < lows[i + 2]) {
swingLows.push({ price: lows[i], index: i });
}
}
// Get recent swing points (last lookbackPeriod)
const recentSwingHighs = swingHighs.slice(-lookbackPeriod);
const recentSwingLows = swingLows.slice(-lookbackPeriod);
// Calculate support as average of recent swing lows
const support = recentSwingLows.length > 0
? recentSwingLows.reduce((sum, swing) => sum + swing.price, 0) / recentSwingLows.length
: null;
// Calculate resistance as average of recent swing highs
const resistance = recentSwingHighs.length > 0
? recentSwingHighs.reduce((sum, swing) => sum + swing.price, 0) / recentSwingHighs.length
: null;
// Calculate pivot points (standard pivot point calculation)
const lastHigh = highs[highs.length - 1];
const lastLow = lows[lows.length - 1];
const lastClose = closes[closes.length - 1];
const pivot = (lastHigh + lastLow + lastClose) / 3;
const pivotPoints = {
pivot: pivot,
resistance1: 2 * pivot - lastLow,
resistance2: pivot + (lastHigh - lastLow),
support1: 2 * pivot - lastHigh,
support2: pivot - (lastHigh - lastLow)
};
// Calculate Fibonacci retracements (38.2%, 50%, 61.8%)
// Use recent high and low for Fibonacci levels
const recentHigh = Math.max(...highs.slice(-lookbackPeriod));
const recentLow = Math.min(...lows.slice(-lookbackPeriod));
const range = recentHigh - recentLow;
const fibonacciLevels = {
level0: recentHigh, // 0% (High)
level236: recentHigh - (range * 0.236), // 23.6%
level382: recentHigh - (range * 0.382), // 38.2%
level500: recentHigh - (range * 0.500), // 50%
level618: recentHigh - (range * 0.618), // 61.8%
level786: recentHigh - (range * 0.786), // 78.6%
level100: recentLow // 100% (Low)
};
// Find previous highs and lows for additional key levels
const previousHighs = swingHighs.length > 1
? swingHighs.slice(-5).map(s => s.price).sort((a, b) => b - a)
: [];
const previousLows = swingLows.length > 1
? swingLows.slice(-5).map(s => s.price).sort((a, b) => a - b)
: [];
return {
support: support,
resistance: resistance,
pivotPoints: pivotPoints,
fibonacciLevels: fibonacciLevels,
previousHighs: previousHighs,
previousLows: previousLows,
swingHighs: recentSwingHighs,
swingLows: recentSwingLows
};
}