McGinley Dynamic Commodity Channel Index (MDCCI)
An adaptive momentum oscillator combining the McGinley Dynamic moving average with the Commodity Channel Index to measure price deviation with reduced lag.
About the McGinley Dynamic Commodity Channel Index (MDCCI)
The McGinley Dynamic Commodity Channel Index (MDCCI) is an adaptive momentum oscillator that combines the CCI framework with the McGinley Dynamic moving average. While the traditional CCI uses a simple or exponential moving average as its smoothing mechanism, the MDCCI replaces this with John McGinley's dynamic moving average, which automatically adjusts its smoothing factor based on the speed of market movement. This adaptive behaviour makes the MDCCI more responsive in fast-moving markets and more stable in slow-moving or ranging conditions, while retaining the CCI's core ability to identify overbought and oversold extremes.
What It Measures
The MDCCI measures the deviation of typical price from the McGinley Dynamic moving average, normalised by a deviation model over a given period. A high MDCCI value indicates that price is significantly above the adaptive average, signalling overbought conditions, while a low value indicates the price is well below the adaptive average, signalling oversold conditions. Because the McGinley Dynamic adapts its smoothing speed dynamically, the MDCCI responds more quickly to price acceleration than a fixed-MA CCI.
When to Use
Like all CCI-based indicators, the MDCCI is best applied in trending environments rather than low-volatility, sideways markets where repeated threshold crosses can generate false signals.
Interpretation
- Above overbought threshold (default +100): Price is trading significantly above the McGinley Dynamic average; a potential sell signal or trend exhaustion point.
- Below oversold threshold (default -100): Price is trading significantly below the McGinley Dynamic average; a potential buy signal or reversal zone.
- Near zero: Price is tracking closely with the adaptive average, indicating no strong momentum signal.
- Divergences: When price makes a new high but the MDCCI fails to reach a new high (bearish divergence), or price makes a new low without a corresponding MDCCI low (bullish divergence), a reversal may be developing.
Example Usage
use centaur_technical_indicators::momentum_indicators::bulk::mcginley_dynamic_commodity_channel_index;
use centaur_technical_indicators::DeviationModel;
pub fn main() {
// fetch the data in your preferred way
// let high = vec![...]; // high prices
// let low = vec![...]; // low prices
// let close = vec![...]; // close prices
// Calculate typical price: (high + low + close) / 3
let typical_price: Vec<f64> = high.iter()
.zip(low.iter())
.zip(close.iter())
.map(|((h, l), c)| (h + l + c) / 3.0)
.collect();
let mdcci = mcginley_dynamic_commodity_channel_index(
&typical_price,
0.0,
DeviationModel::MeanAbsoluteDeviation,
0.015,
20
).expect("Failed to calculate MDCCI");
// Each element is (cci_value, mcginley_dynamic_value)
println!("{:?}", mdcci);
}
import centaur_technical_indicators as cti
# fetch the data in your preferred way
# high = [...] # high prices
# low = [...] # low prices
# close = [...] # close prices
# Calculate typical price: (high + low + close) / 3
typical_price = [(h + l + c) / 3.0 for h, l, c in zip(high, low, close)]
mdcci = cti.momentum_indicators.bulk.mcginley_dynamic_commodity_channel_index(
typical_price,
previous_mcginley_dynamic=0.0,
deviation_model="mean_absolute_deviation",
constant_multiplier=0.015,
period=20
)
# Each element is a tuple (cci_value, mcginley_dynamic_value)
print(mdcci)
import init, { momentum_bulk_mcginleyDynamicCommodityChannelIndex, DeviationModel } from 'https://cdn.jsdelivr.net/npm/centaur-technical-indicators@latest/dist/web/centaur-technical-indicators.js';
await init();
// fetch the data in your preferred way
// const high = [...]; // high prices
// const low = [...]; // low prices
// const close = [...]; // close prices
// Calculate typical price: (high + low + close) / 3
const typicalPrice = high.map((h, i) => (h + low[i] + close[i]) / 3.0);
const mdcci = momentum_bulk_mcginleyDynamicCommodityChannelIndex(
typicalPrice,
0.0,
DeviationModel.MeanAbsoluteDeviation,
0.015,
20
);
// Each element is [cci_value, mcginley_dynamic_value]
console.log(mdcci);
Optimization
Even if the defaults have stood the test of time, tuning them to your specific asset and timeframe gives you signals that fit your market. Below shows you how to achieve this.
Optimization Code
//! # McGinley Dynamic Commodity Channel Index (MDCCI) Optimization Example
//!
//! This example demonstrates how to optimize the McGinley Dynamic CCI indicator
//! from the centaur_technical_indicators library using the TradingNetwork optimization framework.
//!
//! ## Indicator Type: Threshold-Based Oscillator
//!
//! The McGinley Dynamic CCI is an adaptive variant of CCI that uses the McGinley Dynamic
//! moving average instead of a traditional moving average. Like CCI, it oscillates around
//! zero with no fixed upper/lower bounds.
//! - **Buy signal**: MDCCI drops below oversold threshold
//! - **Sell signal**: MDCCI rises above overbought threshold
//!
//! ## Parameters Optimized:
//! - Period: 4-60
//! - Constant Multiplier: 0.005-0.030
//! - Deviation Model: StandardDeviation, MeanAbsoluteDeviation, MedianAbsoluteDeviation, ModeAbsoluteDeviation, UlcerIndex
//! - Oversold threshold: -200 to 0
//! - Overbought threshold: 0 to 200
//!
//! ## Data Requirements:
//! - High, Low, Close prices (to calculate typical price)
//!
//! ## Baseline Comparison:
//! Uses optimized CCI parameters as baseline:
//! - Period: 24, Deviation: MeanAbsoluteDeviation
//! - Constant Multiplier: 0.0080, Oversold: -12, Overbought: 148
//!
//! ## Usage:
//! ```bash
//! cp src/examples/mcginley_dynamic_cci_optimization.rs src/main.rs
//! cat src/year_spx.csv | cargo run --release
//! ```
use chrono::NaiveDate;
use centaur_technical_indicators::chart_trends::{peaks, valleys};
use centaur_technical_indicators::momentum_indicators::bulk::{mcginley_dynamic_commodity_channel_index, commodity_channel_index};
use centaur_technical_indicators::{ConstantModelType, DeviationModel};
use rayon::prelude::*;
use serde::Deserialize;
use std::io;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
fn proximity_rating(fuzzed_location: &usize, price_location: &usize) -> f64 {
1.0 / (*fuzzed_location as f64 - *price_location as f64).abs()
}
struct OptimizationResult {
rating: f64,
period: usize,
deviation_model: DeviationModel,
constant_multiplier: f64,
oversold: i32,
overbought: i32,
indicators: Vec<(f64, f64)>,
}
#[derive(Deserialize, Debug)]
struct Ohlc {
#[serde(with = "csv_date_format")]
#[allow(dead_code)]
date: NaiveDate,
#[allow(dead_code)]
open: f64,
high: f64,
low: f64,
close: f64,
#[allow(dead_code)]
volume: f64,
}
mod csv_date_format {
use chrono::NaiveDate;
use serde::{self, Deserialize, Deserializer};
const FORMAT: &'static str = "%Y-%m-%d";
pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let dt = NaiveDate::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)?;
Ok(dt)
}
}
fn get_data() -> Vec<Ohlc> {
let mut prices = Vec::new();
let mut rdr = csv::Reader::from_reader(io::stdin());
for line in rdr.deserialize() {
let ohlc: Ohlc = line.expect("Failed to parse CSV line");
prices.push(ohlc);
}
prices
}
fn format_duration(seconds: u64) -> String {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
let secs = seconds % 60;
if hours > 0 {
format!("{}h {}m {}s", hours, minutes, secs)
} else if minutes > 0 {
format!("{}m {}s", minutes, secs)
} else {
format!("{}s", secs)
}
}
pub fn main() {
let data = get_data();
println!("Extracting values from data");
let mut close: Vec<f64> = Vec::new();
let mut typical_price: Vec<f64> = Vec::new();
for i in data.iter() {
close.push(i.close);
// MDCCI uses typical price: (high + low + close) / 3
typical_price.push((i.high + i.low + i.close) / 3.0);
}
println!("Values extracted, processing data");
let optimization_start = Instant::now();
// Get buy and sell points
let sell_points = peaks(&close, 20, 5)
.expect("Failed to calculate peaks")
.into_iter()
.map(|(_, i)| i)
.collect::<Vec<usize>>();
let buy_points = valleys(&close, 20, 5)
.expect("Failed to calculate valleys")
.into_iter()
.map(|(_, i)| i)
.collect::<Vec<usize>>();
println!(
"Found {} buy points and {} sell points",
buy_points.len(),
sell_points.len()
);
let max_period = 60;
let min_period = 4;
let oversold_values: Vec<i32> = (-200..=0).collect();
let overbought_values: Vec<i32> = (0..=200).collect();
let multiplier_values: Vec<i32> = (5..=30).collect(); // 0.005 to 0.030 in steps of 0.001
let fuzz_parameter = 5;
let deviation_models = vec![
DeviationModel::StandardDeviation,
DeviationModel::MeanAbsoluteDeviation,
DeviationModel::MedianAbsoluteDeviation,
DeviationModel::ModeAbsoluteDeviation,
DeviationModel::UlcerIndex,
];
let best_result = Arc::new(Mutex::new(OptimizationResult {
rating: f64::NEG_INFINITY,
period: 0,
deviation_model: DeviationModel::StandardDeviation,
constant_multiplier: 0.0,
oversold: 0,
overbought: 0,
indicators: vec![],
}));
let valid_threshold_combinations = oversold_values.iter()
.flat_map(|&oversold| {
overbought_values.iter().filter(move |&&overbought| oversold < overbought)
})
.count();
let total_iterations = valid_threshold_combinations
* multiplier_values.len()
* deviation_models.len()
* (max_period - min_period + 1);
println!(
"
Running parallel optimization with {} total iterations...",
total_iterations
);
println!("Using {} threads", rayon::current_num_threads());
let iteration_count = Arc::new(AtomicUsize::new(0));
let last_logged_percent = Arc::new(AtomicUsize::new(0));
let threshold_pairs: Vec<(i32, i32)> = oversold_values.iter()
.flat_map(|&oversold| {
overbought_values.iter()
.filter(move |&&overbought| oversold < overbought)
.map(move |&overbought| (oversold, overbought))
})
.collect();
threshold_pairs.par_iter().for_each(|&(oversold, overbought)| {
for &multiplier_int in &multiplier_values {
let multiplier = multiplier_int as f64 / 1000.0;
for &deviation_type in &deviation_models {
for period in min_period..=max_period {
if typical_price.len() < period + 2 {
continue;
}
// MDCCI returns Vec<(f64, f64)> where .0 is CCI value, .1 is McGinley Dynamic
let indicators = match mcginley_dynamic_commodity_channel_index(
&typical_price,
0.0,
deviation_type,
multiplier,
period,
) {
Ok(ind) => ind,
Err(_) => continue,
};
let mut rating = vec![];
let mut matched_sell = vec![];
let mut matched_buy = vec![];
for i in 0..indicators.len() {
let price_location = i + period;
if price_location >= close.len() {
break;
}
let mdcci_value = indicators[i].0; // Use the CCI value from the tuple
if mdcci_value > overbought as f64 {
if sell_points.contains(&price_location) {
rating.push(1.0);
matched_sell.push(price_location);
} else if buy_points.contains(&price_location) {
rating.push(-1.0);
} else {
let mut found_sell = false;
for fuzzed_location in price_location.saturating_sub(fuzz_parameter)
..=price_location + fuzz_parameter
{
if sell_points.contains(&fuzzed_location)
&& !matched_sell.contains(&fuzzed_location)
{
rating.push(proximity_rating(
&fuzzed_location,
&price_location,
));
matched_sell.push(fuzzed_location);
found_sell = true;
break;
}
}
if !found_sell {
for fuzzed_location in price_location.saturating_sub(fuzz_parameter)
..=price_location + fuzz_parameter
{
if buy_points.contains(&fuzzed_location) {
rating.push(-proximity_rating(
&fuzzed_location,
&price_location,
));
break;
}
}
}
}
} else if mdcci_value < oversold as f64 {
if buy_points.contains(&price_location) {
rating.push(1.0);
matched_buy.push(price_location);
} else if sell_points.contains(&price_location) {
rating.push(-1.0);
} else {
let mut found_buy = false;
for fuzzed_location in price_location.saturating_sub(fuzz_parameter)
..=price_location + fuzz_parameter
{
if buy_points.contains(&fuzzed_location)
&& !matched_buy.contains(&fuzzed_location)
{
rating.push(proximity_rating(
&fuzzed_location,
&price_location,
));
matched_buy.push(fuzzed_location);
found_buy = true;
break;
}
}
if !found_buy {
for fuzzed_location in price_location.saturating_sub(fuzz_parameter)
..=price_location + fuzz_parameter
{
if sell_points.contains(&fuzzed_location) {
rating.push(-proximity_rating(
&fuzzed_location,
&price_location,
));
break;
}
}
}
}
}
}
for &missed_sell in &sell_points {
if !matched_sell.contains(&missed_sell) {
rating.push(-1.0);
}
}
for &missed_buy in &buy_points {
if !matched_buy.contains(&missed_buy) {
rating.push(-1.0);
}
}
if !rating.is_empty() {
let total_rating: f64 =
rating.iter().sum::<f64>() / (rating.len() as f64);
let mut best = best_result.lock().unwrap();
if total_rating > best.rating {
best.rating = total_rating;
best.period = period;
best.deviation_model = deviation_type;
best.constant_multiplier = multiplier;
best.oversold = oversold;
best.overbought = overbought;
best.indicators = indicators;
}
}
let current_count = iteration_count.fetch_add(1, Ordering::Relaxed) + 1;
let current_percent = (current_count * 100) / total_iterations;
let last_percent = last_logged_percent.load(Ordering::Relaxed);
if current_percent > last_percent && current_percent % 5 == 0 {
if last_logged_percent
.compare_exchange(last_percent, current_percent, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
let elapsed = optimization_start.elapsed().as_secs();
let estimated_total = if current_count > 0 {
(elapsed * total_iterations as u64) / current_count as u64
} else {
0
};
let estimated_remaining = estimated_total.saturating_sub(elapsed);
println!(
"Optimization is {}% complete... (Elapsed: {}, Est. remaining: {})",
current_percent,
format_duration(elapsed),
format_duration(estimated_remaining)
);
}
}
}
}
}
});
let optimization_elapsed = optimization_start.elapsed();
println!(
"
Optimization complete in {}",
format_duration(optimization_elapsed.as_secs())
);
let best = best_result.lock().unwrap();
println!("
Best McGinley Dynamic CCI parameters found:");
println!("Period: {}", best.period);
println!("Deviation Model: {:?}", best.deviation_model);
println!("Constant Multiplier: {:.4}", best.constant_multiplier);
println!("Oversold threshold: {}", best.oversold);
println!("Overbought threshold: {}", best.overbought);
println!("Rating: {:.4}", best.rating);
}
Optimization Output
Example output from running the optimization code above on a year of S&P data.
Best McGinley Dynamic CCI parameters found:
Period: 4
Deviation Model: StandardDeviation
Constant Multiplier: 0.0270
Oversold threshold: -49
Overbought threshold: 79
Rating: 0.3699
Trading Simulation
In order to determine whether the optimized parameters beat the defaults, a trading simulation was run, below are the results.
Optimized Trading Simulation
- SideLONG
- Shares0.0349
- Entry$5956.06
- Value$196.65
Default Trading Simulation
- SideLONG
- Shares0.0348
- Entry$5983.25
- Value$196.00
Analysis
Both the optimized MDCCI and the baseline CCI deliver nearly identical final returns, with the baseline CCI marginally ahead at $27.96 (2.80%) versus the optimized MDCCI at $27.52 (2.75%). However, the optimized MDCCI achieves this result with a more active trading style, generating 15 trades against the baseline CCI's 17 trades, using a much tighter period (4 vs 24) combined with a tighter overbought threshold (79 vs 148) and a wider oversold threshold (-49 vs -12).
The key distinction of the MDCCI over the standard CCI is its use of the McGinley Dynamic as the underlying moving average, which adapts its smoothing based on market speed. This allows the MDCCI to respond more quickly in fast-moving markets while remaining stable during slower periods, as demonstrated by the optimized configuration converging on a very short period (4) paired with the Standard Deviation model, which amplifies sensitivity to rapid price changes.
It is worth noting that this is the only McGinley Dynamic optimized example currently available; the research will go into more depth on this adaptive CCI variant in future updates. The baseline comparison uses the previously optimized CCI parameters (ExponentialMovingAverage, MeanAbsoluteDeviation, period 24) to provide a meaningful reference point.
Trading Simulation Code
For those who want to run their own simulation to compare results.
fn simulate_trading(
indicators: &[(f64, f64)],
period: usize,
close: &[f64],
oversold: i32,
overbought: i32,
) {
println!("
--- Trading Simulation ---");
let initial_capital = 1000.0;
let mut capital = initial_capital;
let investment_pct = 0.20;
let mut position: Option<(f64, f64)> = None; // (entry_price, shares)
println!(
"{:<5} | {:<12} | {:<10} | {:<10} | {:<10} | {:<12} | {:<10}",
"Day", "Event", "MDCCI", "Price", "Shares", "Capital", "P/L"
);
println!("{}", "-".repeat(85));
for i in 0..indicators.len() {
let price_location = i + period;
if price_location >= close.len() {
break;
}
let mdcci_val = indicators[i].0; // CCI value from the tuple
let current_price = close[price_location];
if let Some((entry_price, shares)) = position.take() {
// SELL CONDITION: MDCCI rises above overbought
if mdcci_val > overbought as f64 {
let sale_value = shares * current_price;
let profit = sale_value - (shares * entry_price);
capital += sale_value;
println!(
"{:<5} | {:<12} | {:<10.2} | ${:<9.2} | {:<10.4} | ${:<11.2} | ${:<9.2}",
price_location, "Sell", mdcci_val, current_price, shares, capital, profit
);
} else {
position = Some((entry_price, shares));
}
} else {
// BUY CONDITION: MDCCI drops below oversold
if mdcci_val < oversold as f64 {
let investment = capital * investment_pct;
let shares = investment / current_price;
capital -= investment;
println!(
"{:<5} | {:<12} | {:<10.2} | ${:<9.2} | {:<10.4} | ${:<11.2} | {}",
price_location, "Buy", mdcci_val, current_price, shares, capital, "-"
);
position = Some((current_price, shares));
}
}
}
// Final results
println!("
--- Final Results ---");
if let Some((entry_price, shares)) = position {
let last_price = close.last().unwrap_or(&0.0);
let current_value = shares * last_price;
capital += current_value;
let pnl = current_value - (shares * entry_price);
println!("Position still open:");
println!(" Entry: ${:.2}, Current: ${:.2}", entry_price, last_price);
println!(" Unrealized P/L: ${:.2}", pnl);
}
let final_pnl = capital - initial_capital;
let pnl_pct = (final_pnl / initial_capital) * 100.0;
println!("
Initial Capital: ${:.2}", initial_capital);
println!("Final Capital: ${:.2}", capital);
println!("Total P/L: ${:.2} ({:.2}%)", final_pnl, pnl_pct);
}
fn simulate_cci_trading(
indicators: &[f64],
period: usize,
close: &[f64],
oversold: i32,
overbought: i32,
) {
println!("
--- Trading Simulation ---");
let initial_capital = 1000.0;
let mut capital = initial_capital;
let investment_pct = 0.20;
let mut position: Option<(f64, f64)> = None; // (entry_price, shares)
println!(
"{:<5} | {:<12} | {:<10} | {:<10} | {:<10} | {:<12} | {:<10}",
"Day", "Event", "CCI", "Price", "Shares", "Capital", "P/L"
);
println!("{}", "-".repeat(85));
for i in 0..indicators.len() {
let price_location = i + period;
if price_location >= close.len() {
break;
}
let cci_val = indicators[i];
let current_price = close[price_location];
if let Some((entry_price, shares)) = position.take() {
// SELL CONDITION: CCI rises above overbought
if cci_val > overbought as f64 {
let sale_value = shares * current_price;
let profit = sale_value - (shares * entry_price);
capital += sale_value;
println!(
"{:<5} | {:<12} | {:<10.2} | ${:<9.2} | {:<10.4} | ${:<11.2} | ${:<9.2}",
price_location, "Sell", cci_val, current_price, shares, capital, profit
);
} else {
position = Some((entry_price, shares));
}
} else {
// BUY CONDITION: CCI drops below oversold
if cci_val < oversold as f64 {
let investment = capital * investment_pct;
let shares = investment / current_price;
capital -= investment;
println!(
"{:<5} | {:<12} | {:<10.2} | ${:<9.2} | {:<10.4} | ${:<11.2} | {}",
price_location, "Buy", cci_val, current_price, shares, capital, "-"
);
position = Some((current_price, shares));
}
}
}
// Final results
println!("
--- Final Results ---");
if let Some((entry_price, shares)) = position {
let last_price = close.last().unwrap_or(&0.0);
let current_value = shares * last_price;
capital += current_value;
let pnl = current_value - (shares * entry_price);
println!("Position still open:");
println!(" Entry: ${:.2}, Current: ${:.2}", entry_price, last_price);
println!(" Unrealized P/L: ${:.2}", pnl);
}
let final_pnl = capital - initial_capital;
let pnl_pct = (final_pnl / initial_capital) * 100.0;
println!("
Initial Capital: ${:.2}", initial_capital);
println!("Final Capital: ${:.2}", capital);
println!("Total P/L: ${:.2} ({:.2}%)", final_pnl, pnl_pct);
}
pub fn main() {
// After optimization (see optimization code above)
// Run trading simulation with optimized MDCCI parameters
simulate_trading(
&best_indicators,
best_period,
&close,
best_oversold,
best_overbought,
);
// Compare with baseline CCI parameters
println!("
Baseline CCI values (from optimized CCI params) for comparison:");
let baseline_period = 24;
let baseline_multiplier = 0.0080;
let baseline_oversold = -12;
let baseline_overbought = 148;
let baseline_indicators = commodity_channel_index(
&typical_price,
ConstantModelType::ExponentialMovingAverage,
DeviationModel::MeanAbsoluteDeviation,
baseline_multiplier,
baseline_period,
)
.expect("Failed to calculate Commodity Channel Index");
simulate_cci_trading(
&baseline_indicators,
baseline_period,
&close,
baseline_oversold,
baseline_overbought,
);
}