/**
* Percentage Price Oscillator (PPO) Indicator
* MACD expressed as a percentage for easier comparison across different price levels
*/
import { calculateEMA } from './moving-averages';
export function calculatePPO(closes, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
if (closes.length < slowPeriod + signalPeriod) {
return {
ppo: null,
signal: null,
histogram: null,
signal_type: null,
};
}
// Calculate EMAs
const fastEMA = calculateEMA(closes, fastPeriod);
const slowEMA = calculateEMA(closes, slowPeriod);
if (fastEMA.length < slowPeriod - fastPeriod + 1 || slowEMA.length === 0) {
return {
ppo: null,
signal: null,
histogram: null,
signal_type: null,
};
}
// Align arrays and calculate PPO line: ((EMA_fast - EMA_slow) / EMA_slow) * 100
const ppoLine = [];
const startIdx = slowPeriod - fastPeriod;
for (let i = 0; i < slowEMA.length; i++) {
const fastValue = fastEMA[startIdx + i];
const slowValue = slowEMA[i];
if (fastValue && slowValue && slowValue !== 0) {
const ppo = ((fastValue - slowValue) / slowValue) * 100;
ppoLine.push(ppo);
}
}
if (ppoLine.length === 0) {
return {
ppo: null,
signal: null,
histogram: null,
signal_type: null,
};
}
// Calculate PPO Signal (EMA of PPO line)
const ppoSignal = calculateEMA(ppoLine, signalPeriod);
if (ppoSignal.length === 0) {
return {
ppo: ppoLine[ppoLine.length - 1],
signal: null,
histogram: null,
signal_type: null,
};
}
// Get current values
const currentPPO = ppoLine[ppoLine.length - 1];
const currentSignal = ppoSignal[ppoSignal.length - 1];
const histogram = currentPPO - currentSignal;
// Determine signal type
let signal_type = null;
if (histogram > 0)
signal_type = 'bullish';
else if (histogram < 0)
signal_type = 'bearish';
else
signal_type = 'neutral';
return {
ppo: currentPPO,
signal: currentSignal,
histogram,
signal_type,
};
}