/**
* Coppock Curve Indicator
* Long-term momentum indicator for identifying major market bottoms
*/
/**
* Calculate Coppock Curve
* @param closes Array of closing prices
* @param roc1Period First ROC period (default 14)
* @param roc2Period Second ROC period (default 11)
* @param wmaPeriod WMA period (default 10)
* @returns CoppockCurveData object
*/
export function calculateCoppockCurve(closes, roc1Period = 14, roc2Period = 11, wmaPeriod = 10) {
// Minimum 10 data points required
if (closes.length < 10) {
return null;
}
// Use adaptive periods based on available data
const dataRatio = Math.min(1, closes.length / 35);
const effectiveRoc1Period = Math.max(3, Math.floor(roc1Period * dataRatio));
const effectiveRoc2Period = Math.max(3, Math.floor(roc2Period * dataRatio));
const effectiveWmaPeriod = Math.max(3, Math.floor(wmaPeriod * dataRatio));
// Calculate ROC values using effective periods
const roc14 = calculateROC(closes, effectiveRoc1Period);
const roc11 = calculateROC(closes, effectiveRoc2Period);
// Use fallback values if ROC calculation fails
const r14 = roc14 ?? 0;
const r11 = roc11 ?? 0;
// Sum the ROCs
const rocSum = r14 + r11;
// Apply Weighted Moving Average using effective period
const rocSums = calculateROCsums(closes, effectiveRoc1Period, effectiveRoc2Period);
const wma = calculateWMA(rocSums, effectiveWmaPeriod);
// Use fallback if WMA fails
const wmaValue = wma ?? rocSum;
const coppock = wmaValue;
// Determine trend
let trend = 'neutral';
if (coppock > 0) {
trend = 'bullish';
}
else if (coppock < 0) {
trend = 'bearish';
}
// Calculate signal strength based on Coppock magnitude
const strength = Math.min(100, Math.abs(coppock) * 2);
// Check for zero line crosses (simplified without recursion to avoid infinite loops)
let bullishSignal = false;
let bearishSignal = false;
if (rocSums.length >= 2) {
const prevRocSum = rocSums[rocSums.length - 2];
if (prevRocSum <= 0 && rocSum > 0) {
bullishSignal = true;
}
else if (prevRocSum >= 0 && rocSum < 0) {
bearishSignal = true;
}
}
// Check for major bottom signals (extreme negative turning up)
let majorBottomSignal = false;
if (coppock > -10 && coppock < 10 && rocSums.length >= 5) {
// Check if recently came from extreme negative territory
let hadExtremeNegative = false;
for (let i = 1; i <= Math.min(5, rocSums.length - 1); i++) {
if (rocSums[rocSums.length - 1 - i] < -20) {
hadExtremeNegative = true;
break;
}
}
if (hadExtremeNegative && coppock > 0) {
majorBottomSignal = true;
}
}
// Determine market phase based on Coppock value and trend
let marketPhase = 'recovery';
if (coppock > 10) {
marketPhase = 'expansion';
}
else if (coppock < -10 && trend === 'bearish') {
marketPhase = 'crisis';
}
else if (coppock < 0) {
marketPhase = 'contraction';
}
// Generate trading signal
let signal = 'neutral';
if (majorBottomSignal) {
signal = 'buy'; // Major bottom signal has highest priority
}
else if (bullishSignal) {
signal = 'buy';
}
else if (bearishSignal) {
signal = 'sell';
}
else if (marketPhase === 'expansion' && trend === 'bullish') {
signal = 'buy';
}
else if (marketPhase === 'crisis' && trend === 'bearish') {
signal = 'sell';
}
return {
coppock,
roc14: r14,
roc11: r11,
wma: wmaValue,
trend,
strength,
bullishSignal,
bearishSignal,
majorBottomSignal,
marketPhase,
signal
};
}
/**
* Helper function to calculate ROC
*/
function calculateROC(closes, period) {
if (closes.length < period + 1) {
return null;
}
const currentPrice = closes[closes.length - 1];
const pastPrice = closes[closes.length - 1 - period];
return ((currentPrice - pastPrice) / pastPrice) * 100;
}
/**
* Helper function to calculate ROC sums for WMA
*/
function calculateROCsums(closes, roc1Period, roc2Period) {
const rocSums = [];
const maxPeriod = Math.max(roc1Period, roc2Period);
for (let i = maxPeriod; i < closes.length; i++) {
const slice = closes.slice(0, i + 1);
const roc1 = calculateROC(slice, roc1Period);
const roc2 = calculateROC(slice, roc2Period);
if (roc1 !== null && roc2 !== null) {
rocSums.push(roc1 + roc2);
}
}
return rocSums;
}
/**
* Helper function to calculate Weighted Moving Average
*/
function calculateWMA(values, period) {
if (values.length < period) {
return null;
}
const recentValues = values.slice(-period);
let weightedSum = 0;
let weightSum = 0;
for (let i = 0; i < period; i++) {
const weight = i + 1; // Linear weighting (1, 2, 3, ..., n)
weightedSum += recentValues[i] * weight;
weightSum += weight;
}
return weightedSum / weightSum;
}
/**
* Get Coppock Curve interpretation
* @param coppock CoppockCurveData object
* @returns Human-readable interpretation
*/
export function getCoppockInterpretation(coppock) {
const { coppock: value, majorBottomSignal, marketPhase, bullishSignal, bearishSignal } = coppock;
let interpretation = `Coppock Curve: ${value.toFixed(2)}`;
if (majorBottomSignal) {
interpretation += ' - MAJOR BOTTOM SIGNAL (High probability long-term buy)';
}
else if (bullishSignal) {
interpretation += ' - Bullish zero line crossover';
}
else if (bearishSignal) {
interpretation += ' - Bearish zero line crossover';
}
else {
interpretation += ` - ${marketPhase} phase`;
}
return interpretation;
}
/**
* Analyze Coppock Curve for long-term market timing
* @param coppock CoppockCurveData object
* @returns Long-term market analysis
*/
export function analyzeCoppockLongTerm(coppock) {
const { coppock: value, majorBottomSignal, marketPhase } = coppock;
let longTermBias = 'neutral';
let timeHorizon = 'medium';
let confidenceLevel = 50;
let recommendedAction = 'Monitor for signal confirmation';
if (majorBottomSignal) {
longTermBias = 'bullish';
timeHorizon = 'long';
confidenceLevel = 85;
recommendedAction = 'Major bottom signal - consider long-term investment';
}
else if (value > 10) {
longTermBias = 'bullish';
timeHorizon = 'medium';
confidenceLevel = 70;
recommendedAction = 'Positive momentum - favorable for long positions';
}
else if (value < -10) {
longTermBias = 'bearish';
timeHorizon = 'medium';
confidenceLevel = 70;
recommendedAction = 'Negative momentum - consider defensive positions';
}
else if (value > 0) {
longTermBias = 'bullish';
timeHorizon = 'short';
confidenceLevel = 60;
recommendedAction = 'Moderately bullish - watch for continuation';
}
else {
longTermBias = 'bearish';
timeHorizon = 'short';
confidenceLevel = 60;
recommendedAction = 'Moderately bearish - consider reducing exposure';
}
return { longTermBias, timeHorizon, confidenceLevel, recommendedAction };
}
/**
* Calculate Coppock Curve for different parameter sets
* @param closes Array of closing prices
* @param parameterSets Array of [roc1Period, roc2Period, wmaPeriod] combinations
* @returns Array of CoppockCurveData objects
*/
export function calculateMultipleCoppock(closes, parameterSets = [[14, 11, 10], [21, 14, 10]]) {
return parameterSets
.map(([roc1Period, roc2Period, wmaPeriod]) => calculateCoppockCurve(closes, roc1Period, roc2Period, wmaPeriod))
.filter((coppock) => coppock !== null);
}