/**
* Ultimate Oscillator Indicator
* Combines momentum from three different timeframes
*/
export function calculateUltimateOscillator(highs, lows, closes, shortPeriod = 7, mediumPeriod = 14, longPeriod = 28) {
// Minimum 5 data points required
if (closes.length < 5) {
return {
ultimateOsc: null,
signal: null,
};
}
// Use adaptive periods
const dataRatio = Math.min(1, closes.length / 29);
const effectiveShortPeriod = Math.max(3, Math.floor(shortPeriod * dataRatio));
const effectiveMediumPeriod = Math.max(5, Math.floor(mediumPeriod * dataRatio));
const effectiveLongPeriod = Math.max(7, Math.floor(longPeriod * dataRatio));
// Calculate Buying Pressure and True Range
const buyingPressures = [];
const trueRanges = [];
for (let i = 1; i < closes.length; i++) {
const close = closes[i];
const closePrev = closes[i - 1];
const high = highs[i];
const low = lows[i];
// Buying Pressure = Close - min(Low, Close_prev)
const buyingPressure = close - Math.min(low, closePrev);
buyingPressures.push(buyingPressure);
// True Range = max(High, Close_prev) - min(Low, Close_prev)
const trueRange = Math.max(high, closePrev) - Math.min(low, closePrev);
trueRanges.push(trueRange);
}
if (buyingPressures.length < effectiveShortPeriod) {
return {
ultimateOsc: null,
signal: null,
};
}
// Calculate Average for each timeframe using effective periods
function calculateAverageBP_TR(period) {
const usePeriod = Math.min(period, buyingPressures.length);
const bpSum = buyingPressures.slice(-usePeriod).reduce((sum, bp) => sum + bp, 0);
const trSum = trueRanges.slice(-usePeriod).reduce((sum, tr) => sum + tr, 0);
return trSum > 0 ? bpSum / trSum : 0;
}
const avg7 = calculateAverageBP_TR(effectiveShortPeriod);
const avg14 = calculateAverageBP_TR(effectiveMediumPeriod);
const avg28 = calculateAverageBP_TR(effectiveLongPeriod);
// Ultimate Oscillator = (4 × avg7 + 2 × avg14 + avg28) / (4 + 2 + 1)
const rawUO = (4 * avg7) + (2 * avg14) + avg28;
const ultimateOsc = rawUO / 7;
// Determine signal
let signal = null;
if (ultimateOsc >= 70)
signal = 'overbought';
else if (ultimateOsc <= 30)
signal = 'oversold';
else
signal = 'neutral';
return {
ultimateOsc,
signal,
};
}