Commodity Channel Index (CCI)
Momentum oscillator measuring deviation from statistical average to identify overbought and oversold conditions.
About the Commodity Channel Index (CCI)
The Commodity Channel Index (CCI) is a versatile momentum-based oscillator that measures the deviation of an asset's price from its statistical average. Developed by Donald Lambert in 1980, the CCI was originally designed for commodity markets but has since been widely adopted for stocks, forex, and other financial instruments. Unlike bounded oscillators such as RSI, the CCI has no theoretical upper or lower limits, making it particularly effective at identifying extreme price movements and potential reversal points.
What It Measures
The CCI measures how far the current price has moved from its average price over a specified period, normalized by the mean deviation. A high CCI value indicates that the price is unusually high compared to its average, while a low CCI value suggests the price is unusually low. The default indicator uses typical price (the average of high, low, and close) rather than just closing prices, providing a more comprehensive view of price action.
When to Use
The CCI is most effective in identifying cyclical trends and overbought/oversold conditions. It excels at spotting extreme conditions that may precede trend reversals or continuations. Traders commonly use CCI values above +100 to indicate overbought conditions (potential sell signals) and values below -100 to indicate oversold conditions (potential buy signals). The indicator is particularly useful in trending markets where prices tend to oscillate between extremes, and it can also help identify divergences between price and momentum.
Interpretation
- Above +100: Price is in overbought territory, suggesting a potential reversal or pullback may be imminent. Very high values (above +200) indicate exceptionally strong upward momentum.
- Below -100: Price is in oversold territory, indicating a potential buying opportunity or upward reversal. Very low values (below -200) suggest extreme downward pressure.
- Between +100 and -100: Price is trading within normal ranges relative to its statistical average.
- Zero-line crossovers: When CCI crosses above zero, it may signal the beginning of an uptrend; crossing below zero may indicate the start of a downtrend.
- Divergences: When price makes new highs but CCI fails to confirm (bearish divergence), or when price makes new lows but CCI doesn't (bullish divergence), it may signal an impending trend reversal.
Example Usage
use centaur_technical_indicators::momentum_indicators::bulk::commodity_channel_index;
use centaur_technical_indicators::{ConstantModelType, 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 cci = commodity_channel_index(
&typical_price,
ConstantModelType::SimpleMovingAverage,
DeviationModel::MeanAbsoluteDeviation,
0.015,
20
).expect("Failed to calculate CCI");
println!("{:?}", cci);
}
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)]
cci = cti.momentum_indicators.bulk.commodity_channel_index(
typical_price,
constant_model_type="simple_moving_average",
deviation_model="mean_absolute_deviation",
constant_multiplier=0.015,
period=20
)
print(cci)
import init, { momentum_bulk_commodityChannelIndex, ConstantModelType, 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 cci = momentum_bulk_commodityChannelIndex(
typicalPrice,
ConstantModelType.SimpleMovingAverage,
DeviationModel.MeanAbsoluteDeviation,
0.015,
20
);
console.log(cci);
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
//! # Commodity Channel Index (CCI) Optimization Example
//!
//! This example demonstrates how to optimize the Commodity Channel Index (CCI) indicator
//! from the centaur_technical_indicators library using the TradingNetwork optimization framework.
//!
//! ## Indicator Type: Threshold-Based Oscillator
//!
//! The CCI measures the deviation of price from its statistical average. It oscillates
//! around zero with no fixed upper/lower bounds, though typical thresholds are:
//! - **Buy signal**: CCI drops below oversold threshold (typically -100 to -200)
//! - **Sell signal**: CCI rises above overbought threshold (typically +100 to +200)
//!
//! ## Parameters Optimized:
//! - Period: 4-60
//! - Constant Multiplier: 0.001-0.030 (traditional is 0.015)
//! - MA Model: SMA, EMA, SMMA, Median, Mode
//! - Deviation Model: StandardDeviation, MeanAbsoluteDeviation
//! - Oversold threshold: -200 to 0
//! - Overbought threshold: 0 to 200
//!
//! ## Data Requirements:
//! - High, Low, Close prices (to calculate typical price)
//!
//! ## Usage:
//! ```bash
//! cp src/examples/commodity_channel_index_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::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,
model: ConstantModelType,
deviation_model: DeviationModel,
constant_multiplier: f64,
oversold: i32,
overbought: i32,
indicators: Vec<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 high: Vec<f64> = Vec::new();
let mut low: Vec<f64> = Vec::new();
let mut typical_price: Vec<f64> = Vec::new();
for i in data.iter() {
close.push(i.close);
high.push(i.high);
low.push(i.low);
// CCI 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()
);
// Define the ranges for optimization
let max_period = 60;
let min_period = 4;
// Oversold: negative values (CCI below this triggers buy)
let min_oversold = -200;
let max_oversold = 0;
// Overbought: positive values (CCI above this triggers sell)
let min_overbought = 0;
let max_overbought = 200;
// Constant multiplier typically around 0.015, test range 0.001 to 0.030
let min_constant_multiplier = 1; // 0.001
let max_constant_multiplier = 30; // 0.030 (divide by 1000)
let fuzz_parameter = 5; // Allowable distance from buy/sell points
let models = vec![
ConstantModelType::SimpleMovingAverage,
ConstantModelType::ExponentialMovingAverage,
ConstantModelType::SmoothedMovingAverage,
ConstantModelType::SimpleMovingMedian,
ConstantModelType::SimpleMovingMode,
];
let deviation_models = vec![
DeviationModel::StandardDeviation,
DeviationModel::MeanAbsoluteDeviation,
DeviationModel::MedianAbsoluteDeviation,
DeviationModel::ModeAbsoluteDeviation,
DeviationModel::UlcerIndex,
];
// Store the best parameters found using thread-safe wrappers
let best_result = Arc::new(Mutex::new(OptimizationResult {
rating: f64::NEG_INFINITY,
period: 0,
model: ConstantModelType::SimpleMovingAverage,
deviation_model: DeviationModel::StandardDeviation,
constant_multiplier: 0.0,
oversold: 0,
overbought: 0,
indicators: vec![],
}));
// Calculate total iterations (excluding invalid oversold >= overbought combinations)
let valid_threshold_combinations = (min_oversold..=max_oversold)
.flat_map(|oversold| {
(min_overbought..=max_overbought).filter(move |&overbought| oversold < overbought)
})
.count();
let total_iterations = valid_threshold_combinations
* (max_constant_multiplier - min_constant_multiplier + 1)
* models.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());
// Progress tracking with atomic counter for less contention
let iteration_count = Arc::new(AtomicUsize::new(0));
let last_logged_percent = Arc::new(AtomicUsize::new(0));
// Process parameter space in parallel using par_bridge
// We iterate over threshold combinations and parallelize across those
let threshold_pairs: Vec<(i32, i32)> = (min_oversold..=max_oversold)
.flat_map(|oversold| {
(min_overbought..=max_overbought)
.filter(move |&overbought| oversold < overbought)
.map(move |overbought| (oversold, overbought))
})
.collect();
threshold_pairs.par_iter().for_each(|&(oversold, overbought)| {
for multiplier_int in min_constant_multiplier..=max_constant_multiplier {
let multiplier = multiplier_int as f64 / 1000.0;
for &ma_type in &models {
for &deviation_type in &deviation_models {
for period in min_period..=max_period {
// Skip if not enough data
if typical_price.len() < period + 2 {
continue;
}
// Calculate indicators
let indicators = match commodity_channel_index(
&typical_price,
ma_type,
deviation_type,
multiplier,
period,
) {
Ok(ind) => ind,
Err(_) => continue,
};
let mut rating = vec![];
let mut matched_sell = vec![];
let mut matched_buy = vec![];
// CCI signal detection (threshold-based)
for i in 0..indicators.len() {
let price_location = i + period;
if price_location >= close.len() {
break;
}
let cci_value = indicators[i];
// Sell signal: CCI rises above overbought threshold
if cci_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;
}
}
}
}
}
// Buy signal: CCI drops below oversold threshold
else if cci_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;
}
}
}
}
}
}
// Penalize missed points
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);
// Acquire lock once and check inside
let mut best = best_result.lock().unwrap();
if total_rating > best.rating {
best.rating = total_rating;
best.period = period;
best.model = ma_type;
best.deviation_model = deviation_type;
best.constant_multiplier = multiplier;
best.oversold = oversold;
best.overbought = overbought;
best.indicators = indicators;
}
}
// Update progress with atomic operations
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 {
// Only acquire lock for logging
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())
);
// Extract best results
let best = best_result.lock().unwrap();
let best_rating = best.rating;
let best_period = best.period;
let best_model = best.model;
let best_deviation_model = best.deviation_model;
let best_constant_multiplier = best.constant_multiplier;
let best_oversold = best.oversold;
let best_overbought = best.overbought;
let best_indicators = best.indicators.clone();
println!("
Best Commodity Channel Index parameters found:");
println!("Period: {}", best_period);
println!("Model: {:?}", best_model);
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 Commodity Channel Index parameters found:
Period: 24
Model: ExponentialMovingAverage
Deviation Model: MeanAbsoluteDeviation
Constant Multiplier: 0.0080
Oversold threshold: -12
Overbought threshold: 148
Rating: 0.3792
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.0348
- Entry$5983.25
- Value$196.00
Default Trading Simulation
- SideLONG
- Shares0.0345
- Entry$5955.25
- Value$194.54
Analysis
The optimized CCI parameters demonstrate improved performance over the default configuration in this trading simulation. The optimized strategy yielded a profit of $27.96 (2.80%), significantly outperforming the default strategy which produced a profit of $16.40 (1.64%).
The optimized parameters use a tighter oversold threshold (-12 vs -100) and a higher overbought threshold (148 vs 100), along with a lower constant multiplier (0.008 vs 0.015) and a longer period (24 vs 20). This configuration generated more trading signals (17 trades vs 9 trades), allowing the strategy to capture more short-term price movements while maintaining profitability.
It's important to note that this optimization uses parallel processing with 24 threads to evaluate over 1.7 billion parameter combinations, which took over 7 hours to complete. This demonstrates the computational intensity of thorough indicator optimization and provides readers with a practical example of how to leverage multi-threading when optimizing complex indicators for their own trading strategies.
Trading Simulation Code
For those who want to run their own simulation to compare results.
fn simulate_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 parameters
simulate_trading(
&best_indicators,
best_period,
&close,
best_oversold,
best_overbought,
);
// Compare with default parameters
println!("
Default CCI values for comparison:");
let default_period = 20;
let default_multiplier = 0.015;
let default_oversold = -100;
let default_overbought = 100;
let default_indicators = commodity_channel_index(
&typical_price,
ConstantModelType::SimpleMovingAverage,
DeviationModel::MeanAbsoluteDeviation,
default_multiplier,
default_period,
)
.expect("Failed to calculate Commodity Channel Index");
simulate_trading(
&default_indicators,
default_period,
&close,
default_oversold,
default_overbought,
);
}