Chaikin Oscillator

Momentum oscillator measuring the acceleration of the Accumulation/Distribution line using fast and slow moving averages.

About the Chaikin Oscillator

The Chaikin Oscillator is a momentum indicator that measures the acceleration of the Accumulation/Distribution line. Developed by Marc Chaikin, it applies the MACD (Moving Average Convergence Divergence) concept to the Accumulation/Distribution line, using the difference between a fast and slow exponential moving average to identify changes in buying and selling pressure.

What It Measures

The Chaikin Oscillator measures the momentum of the Accumulation/Distribution line by calculating the difference between a short-period moving average (typically 3-period) and a long-period moving average (typically 10-period) of the A/D line. Positive values indicate accumulation momentum (buying pressure accelerating), while negative values indicate distribution momentum (selling pressure accelerating).

When to Use

  • Trend Confirmation: Positive oscillator values during uptrends confirm buying pressure; negative values during downtrends confirm selling pressure
  • Crossover Signals: When the oscillator crosses above zero, it signals increasing accumulation (potential buy); crossing below zero signals increasing distribution (potential sell)
  • Divergence Detection: When price makes new highs but the oscillator doesn't, it may indicate weakening momentum and potential reversal

Interpretation

  • Positive Values: Indicate accumulation momentum is increasing; the A/D line is rising faster
  • Negative Values: Indicate distribution momentum is increasing; the A/D line is falling faster
  • Zero Line Crossovers: Primary trading signals occur when the oscillator crosses above (bullish) or below (bearish) the zero line
  • Divergences: When the oscillator moves opposite to price, it may signal potential trend reversals

Example Usage

use centaur_technical_indicators::momentum_indicators::bulk::chaikin_oscillator;
use centaur_technical_indicators::ConstantModelType;

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
    // let volume = vec![...]; // volume data
    
    let short_period = 3;
    let long_period = 10;
    let previous_ad = 0.0;
    let short_ma_type = ConstantModelType::ExponentialMovingAverage;
    let long_ma_type = ConstantModelType::ExponentialMovingAverage;
    
    let chaikin = chaikin_oscillator(
        &high, 
        &low, 
        &close, 
        &volume, 
        short_period, 
        long_period, 
        previous_ad,
        short_ma_type,
        long_ma_type
    );
    println!("Chaikin Oscillator: {:?}", chaikin);
}
import centaur_technical_indicators as cti

# Chaikin Oscillator calculation
short_period = 3
long_period = 10
previous_ad = 0.0
short_ma_type = "exponential_moving_average"
long_ma_type = "exponential_moving_average"

chaikin = cti.momentum_indicators.bulk.chaikin_oscillator(
    high, 
    low, 
    close, 
    volume, 
    short_period, 
    long_period, 
    previous_ad,
    short_ma_type,
    long_ma_type
)
print("Chaikin Oscillator:", chaikin)
import init, { 
    momentum_bulk_chaikinOscillator,
    ConstantModelType
} 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
// const volume = [...]; // volume data

const shortPeriod = 3;
const longPeriod = 10;
const previousAd = 0.0;
const shortMaType = ConstantModelType.ExponentialMovingAverage;
const longMaType = ConstantModelType.ExponentialMovingAverage;

const chaikin = momentum_bulk_chaikinOscillator(
    high, 
    low, 
    close, 
    volume, 
    shortPeriod, 
    longPeriod, 
    previousAd,
    shortMaType,
    longMaType
);
console.log("Chaikin Oscillator:", chaikin);

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

//! # Chaikin Oscillator Optimization Example
//!
//! This example demonstrates how to optimize the Chaikin Oscillator indicator
//! from the centaur_technical_indicators library using the TradingNetwork optimization framework.
//!
//! ## Indicator Type: Crossover-based Oscillator
//!
//! The Chaikin Oscillator measures the momentum of the Accumulation/Distribution line.
//! It uses the difference between fast and slow EMAs of the A/D line.
//! - **Buy signal**: Oscillator crosses above zero (accumulation/buying pressure)
//! - **Sell signal**: Oscillator crosses below zero (distribution/selling pressure)
//!
//! ## Parameters Optimized:
//! - short_period: Fast EMA period (2-125)
//! - long_period: Slow EMA period (2-125, must be > short_period)
//! - short_ma_type: Moving average model for short period (SMA, EMA, SMMA, Median, Mode)
//! - long_ma_type: Moving average model for long period (SMA, EMA, SMMA, Median, Mode)
//!
//! ## Data Requirements:
//! - High prices
//! - Low prices
//! - Close prices
//! - Volume data
//!
//! ## Usage:
//! ```bash
//! cp src/examples/chaikin_oscillator_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::chaikin_oscillator;
use centaur_technical_indicators::ConstantModelType;
use serde::Deserialize;
use std::io;
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()
}

#[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,
    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 main() {
    let data = get_data();
    println!("Extracting values from data");

    let mut high: Vec<f64> = Vec::new();
    let mut low: Vec<f64> = Vec::new();
    let mut close: Vec<f64> = Vec::new();
    let mut volume: Vec<f64> = Vec::new();

    println!("Values extracted, processing data");

    for i in data.iter() {
        high.push(i.high);
        low.push(i.low);
        close.push(i.close);
        volume.push(i.volume);
    }

    let indicator_loop = Instant::now();

    // get buy and sell points, in an ideal world we would buy at the lowest point in the dip and sell at the highest point in the peak
    // In the course of a 20-day period (1 month of trading days), we want to find the highest peak and lowest valley within 5 days of each other
    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>>();

    // Define the ranges for optimization
    let max_short_period: usize = 125;
    let min_short_period: usize = 2;
    let max_long_period: usize = 125;
    let min_long_period: usize = 2;

    let fuzz_parameter: usize = 5; // Allowable distance from buy/sell points

    // Store the best parameters found
    let mut best_rating = 0.0;
    let mut best_short_period: usize = 0;
    let mut best_long_period: usize = 0;
    let mut best_long_ma_type = ConstantModelType::SimpleMovingAverage;
    let mut best_short_ma_type = ConstantModelType::SimpleMovingAverage;
    let mut best_indicators = vec![];

    let total_iterations = (max_short_period - min_short_period + 1) * (max_long_period - min_long_period + 1) * 5 * 5;
    let mut iteration_count = 0;
    println!("\nRunning optimization loop with {} total iterations...", total_iterations);

    for &long_ma_type in &[ConstantModelType::SimpleMovingAverage, ConstantModelType::ExponentialMovingAverage, ConstantModelType::SmoothedMovingAverage, ConstantModelType::SimpleMovingMedian, ConstantModelType::SimpleMovingMode] {
        for &short_ma_type in &[ConstantModelType::SimpleMovingAverage, ConstantModelType::ExponentialMovingAverage, ConstantModelType::SmoothedMovingAverage, ConstantModelType::SimpleMovingMedian, ConstantModelType::SimpleMovingMode] {
            for short_period in min_short_period..=max_short_period {
                for long_period in min_long_period..=max_long_period {
                    // Skip invalid combinations where short >= long
                    if short_period >= long_period {
                        continue;
                    }

                    iteration_count += 1;
                    if iteration_count % (total_iterations / 20).max(1) == 0 {
                        let next_log_percent = (iteration_count * 100) / total_iterations;
                        println!("Optimization is {}% complete...", next_log_percent);
                    }

                    let indicators = chaikin_oscillator(&high, &low, &close, &volume, short_period, long_period, 0.0, short_ma_type, long_ma_type)
                        .expect("Failed to calculate Chaikin Oscillator");
                    let mut rating = vec![];
                    let mut matched_sell = vec![];
                    let mut matched_buy = vec![];

                    for i in 1..indicators.len() {
                        let price_location = i + long_period; // Adjust for indicator lag
                        if price_location >= close.len() { break; }

                        // Chaikin Oscillator: positive values suggest accumulation (buying pressure), negative values suggest distribution (selling pressure)
                        // Crossovers are key signals
                        let current_value = indicators[i].0;
                        let previous_value = indicators[i - 1].0;

                        // Buy signal: Oscillator crosses above zero (accumulation)
                        if current_value > 0.0 && previous_value <= 0.0 {
                            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) {
                                        rating.push(proximity_rating(&fuzzed_location, &price_location));
                                        matched_buy.push(fuzzed_location);
                                        found_buy = true;
                                    }
                                    if sell_points.contains(&fuzzed_location) {
                                        if !matched_buy.contains(&fuzzed_location) {
                                            rating.push(-proximity_rating(&fuzzed_location, &price_location));
                                        }
                                    }
                                }
                                if !found_buy {
                                    rating.push(0.0);
                                }
                            }
                        }
                        // Sell signal: Oscillator crosses below zero (distribution)
                        else if current_value < 0.0 && previous_value >= 0.0 {
                            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) {
                                        rating.push(proximity_rating(&fuzzed_location, &price_location));
                                        matched_sell.push(fuzzed_location);
                                        found_sell = true;
                                    }
                                    if buy_points.contains(&fuzzed_location) {
                                        if !matched_sell.contains(&fuzzed_location) {
                                            rating.push(-proximity_rating(&fuzzed_location, &price_location));
                                        }
                                    }
                                }
                                if !found_sell {
                                    rating.push(0.0);
                                }
                            }
                        }
                    }

                    // Look for any missed buy/sell points and penalize
                    for missed_sell in sell_points.iter() {
                        if !matched_sell.contains(missed_sell) {
                            rating.push(-1.0);
                        }
                    }
                    for missed_buy in buy_points.iter() {
                        if !matched_buy.contains(missed_buy) {
                            rating.push(-1.0);
                        }
                    }

                    let total_rating: f64 = if rating.is_empty() {
                        0.0
                    } else {
                        rating.iter().sum::<f64>() / (rating.len() as f64)
                    };
                    if total_rating > best_rating {
                        best_rating = total_rating;
                        best_short_period = short_period;
                        best_long_period = long_period;
                        best_indicators = indicators.clone();
                        best_long_ma_type = long_ma_type;
                        best_short_ma_type = short_ma_type;
                    }
                }
            }
        }
    }

    println!(
        "Indicators optimization loop took {} ms to run",
        indicator_loop.elapsed().as_millis()
    );

    println!("\nBest Chaikin Oscillator parameters found:");
    println!("short_period = {}", best_short_period);
    println!("long_period = {}", best_long_period);
    println!("short_ma_type = {:?}", best_short_ma_type);
    println!("long_ma_type = {:?}", best_long_ma_type);
    println!("Rating: {}", best_rating);
}

Optimization Output

Example output from running the optimization code above on a year of S&P data.

Best Chaikin Oscillator parameters found:
short_period = 6
long_period = 9
short_ma_type = SimpleMovingAverage
long_ma_type = ExponentialMovingAverage
Rating: 0.02916666666666666

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

Initial Investment
$1000.00
Final Capital
$1007.15
Total P&L
$7.15
Open Position
None

Default Trading Simulation

Initial Investment
$1000.00
Final Capital
$1004.43
Total P&L
$4.43
Open Position
  • SideSHORT
  • Shares0.0356
  • Entry$5638.94
  • Value$0.00

Analysis

The optimized Chaikin Oscillator parameters (short period 6, long period 9 with SMA/EMA) yielded a profit of $7.15, compared to the default parameters (short period 3, long period 10 with EMA/EMA) which yielded a profit of $4.43. While both strategies were profitable, the optimized parameters provided approximately 61% higher returns.

The optimized version generated more frequent trading signals (46 trades vs 27 trades), resulting in better capture of short-term momentum shifts in the Accumulation/Distribution line. The optimization ended with a closed position, while the default simulation ended with an open short position, making direct comparison clearer. The optimized strategy demonstrated more consistent small gains and better risk management through its faster response to accumulation/distribution changes.

Trading Simulation Code

For those who want to run their own simulation to compare results.

fn simulate_trading(best_indicator: &[(f64, f64)], best_long_period: usize, close: &[f64]) {
    println!("\n--- Trading Simulation ---");

    let initial_capital = 1000.0;
    let mut capital = initial_capital;
    let investment_pct = 0.20;

    struct Position {
        entry_price: f64,
        shares: f64,
    }

    let mut open_long: Option<Position> = None;
    let mut open_short: Option<Position> = None;

    // Print table header
    println!("{:<5} | {:<19} | {:<10} | {:<10} | {:<12} | {:<15} | {:<10}",
             "Day", "Event", "Chaikin", "Price", "Shares", "Capital", "P/L");
    println!("{}", "-".repeat(95));

    for i in 1..best_indicator.len() {
        let price_index = i + best_long_period;
        if price_index >= close.len() { break; }

        let current_value = best_indicator[i].0;
        let previous_value = best_indicator[i - 1].0;
        let current_price = close[price_index];
        let day = price_index;

        // --- Handle Long Position ---
        if let Some(long_pos) = open_long.take() {
            // Sell signal: Oscillator crosses below zero
            if current_value < 0.0 && previous_value >= 0.0 {
                let sale_value = long_pos.shares * current_price;
                let profit = sale_value - (long_pos.shares * long_pos.entry_price);
                capital += sale_value;
                println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                         day, "Sell (Close Long)", current_value, current_price, long_pos.shares, capital, profit);
            } else {
                open_long = Some(long_pos); // Put it back if not selling
            }
        } else if current_value > 0.0 && previous_value <= 0.0 && open_short.is_none() {
            // Buy signal: Oscillator crosses above zero
            let investment = capital * investment_pct;
            let shares_bought = investment / current_price;
            open_long = Some(Position { entry_price: current_price, shares: shares_bought });
            capital -= investment;
            println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                     day, "Buy (Open Long)", current_value, current_price, shares_bought, capital, "-");
        }

        // --- Handle Short Position ---
        if let Some(short_pos) = open_short.take() {
            // Cover signal: Oscillator crosses above zero
            if current_value > 0.0 && previous_value <= 0.0 {
                let cost_to_cover = short_pos.shares * current_price;
                let profit = (short_pos.shares * short_pos.entry_price) - cost_to_cover;
                capital += profit;
                println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                         day, "Cover (Close Short)", current_value, current_price, short_pos.shares, capital, profit);
            } else {
                open_short = Some(short_pos); // Put it back if not covering
            }
        } else if current_value < 0.0 && previous_value >= 0.0 && open_long.is_none() {
            // Short signal: Oscillator crosses below zero
            let short_value = capital * investment_pct;
            let shares_shorted = short_value / current_price;
            open_short = Some(Position { entry_price: current_price, shares: shares_shorted });
            println!("{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                     day, "Short (Open Short)", current_value, current_price, shares_shorted, capital, "-");
        }
    }

    println!("\n--- Final Results ---");
    if let Some(pos) = open_long {
        println!("Simulation ended with an OPEN LONG position:");
        println!("  - Shares: {:.4}", pos.shares);
        println!("  - Entry Price: ${:.2}", pos.entry_price);
        let last_price = close.last().unwrap_or(&0.0);
        let current_value = pos.shares * last_price;
        capital += current_value;
        println!("  - Position value at last price (${:.2}): ${:.2}", last_price, current_value);
    }
    if let Some(pos) = open_short {
        println!("Simulation ended with an OPEN SHORT position:");
        println!("  - Shares: {:.4}", pos.shares);
        println!("  - Entry Price: ${:.2}", pos.entry_price);
        let last_price = close.last().unwrap_or(&0.0);
        let cost_to_cover = pos.shares * last_price;
        let pnl = (pos.shares * pos.entry_price) - cost_to_cover;
        capital += pnl;
        println!("  - Unrealized P/L at last price (${:.2}): ${:.2}", last_price, pnl);
    }

    let final_pnl = capital - initial_capital;
    println!("\nInitial Capital: ${:.2}", initial_capital);
    println!("Final Capital:   ${:.2}", capital);
    println!("Total P/L:       ${:.2}", final_pnl);
}

fn main() {
    // Fetch data and perform optimization as shown in the optimization code above
    simulate_trading(&best_indicators, best_long_period, &close);
    
    // Compare with default parameters
    let default_chaikin = chaikin_oscillator(&high, &low, &close, &volume, 3, 10, 0.0, ConstantModelType::ExponentialMovingAverage, ConstantModelType::ExponentialMovingAverage)
        .expect("Failed to calculate Chaikin Oscillator");
    simulate_trading(&default_chaikin, 10, &close);
}