Moving Average Convergence Divergence (MACD)

Tracks the relationship between two moving averages of price to identify momentum shifts and trend direction changes.

About the Moving Average Convergence Divergence (MACD)

The Moving Average Convergence Divergence (MACD) is a widely used momentum indicator that reveals changes in trend strength, direction, momentum, and duration. It shows the relationship between two moving averages of a security's price to help traders identify potential buy and sell signals. The MACD was developed by Gerald Appel in the late 1970s and has since become one of the most popular technical indicators.

A signal line (a moving average of the MACD) is then plotted on top of the MACD line, which can function as a trigger for buy and sell signals. The difference between the MACD and its signal line forms the MACD histogram (not charted here to keep the charts clean), which provides a visual representation of the convergence and divergence between these two lines.

Unlike oscillators that are bounded within specific ranges, the MACD is an unbounded indicator, meaning it can continue to move higher or lower indefinitely. This characteristic makes it particularly useful for identifying both the strength and direction of trends across various market conditions.

What It Measures

The MACD measures the relationship between two moving averages of price, revealing momentum shifts and trend changes. By comparing fast and slow moving averages, it captures both the speed of price movements and the underlying trend direction. The convergence and divergence between these moving averages provide insights into the strength of current market momentum.

When to Use

  • Trend Identification: The MACD helps identify the presence and strength of trends. When the MACD line is above the signal line and both are positive, it indicates strong upward momentum. When below the signal line and both are negative, it signals strong downward momentum.
  • Crossover Signals: The primary trading signal occurs when the MACD line crosses above or below the signal line. A bullish crossover (MACD crosses above signal) suggests buying opportunity, while a bearish crossover (MACD crosses below signal) suggests selling.
  • Divergence Analysis: MACD divergences from price action can signal potential trend reversals. Bullish divergence occurs when price makes new lows but MACD doesn't, indicating weakening downward momentum. Bearish divergence occurs when price makes new highs but MACD doesn't.
  • Zero Line Crossovers: When the MACD crosses above the zero line, it indicates the shorter-term moving average is above the longer-term average, suggesting bullish momentum. Crossing below zero suggests bearish momentum.

Interpretation

  • Above zero: Indicates that the short-term moving average is above the long-term average, suggesting upward momentum and a potential bullish trend
  • Below zero: Indicates that the short-term moving average is below the long-term average, suggesting downward momentum and a potential bearish trend
  • MACD above signal line: Suggests bullish momentum is strengthening
  • MACD below signal line: Suggests bearish momentum is strengthening
  • Histogram expansion: Growing histogram bars indicate momentum is accelerating in the current direction
  • Histogram contraction: Shrinking histogram bars indicate momentum is weakening, possibly signaling a trend reversal or consolidation
  • Divergences: When MACD moves in the opposite direction of price, it can signal potential trend reversals or weakening momentum

Example Usage

use centaur_technical_indicators::momentum_indicators::bulk::{macd_line, signal_line};
use centaur_technical_indicators::ConstantModelType::ExponentialMovingAverage;

pub fn main() {
    // fetch the data in your preferred way
    // let close = vec![...];  // closing prices

    let macd = macd_line(&close, 12, ExponentialMovingAverage, 26, ExponentialMovingAverage)
        .expect("Failed to calculate MACD line");
    let signal = signal_line(&macd, ExponentialMovingAverage, 9)
        .expect("Failed to calculate Signal line");
    
    println!("MACD: {:?}", macd);
    println!("Signal: {:?}", signal);
}
import centaur_technical_indicators as cti

# fetch the data in your preferred way
# close = [...]  # closing prices

macd = cti.momentum_indicators.bulk.macd_line(close, short_period=12, short_period_model="ExponentialMovingAverage", long_period=26, long_period_model="ExponentialMovingAverage")
signal = cti.momentum_indicators.bulk.signal_line(macd, signal_model="ExponentialMovingAverage", signal_period=9)

print("MACD:", macd)
print("Signal:", signal)
// WASM import
import init, { momentum_bulk_macdLine, momentum_bulk_signalLine, 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 close = [...];  // closing prices

const macd = momentum_bulk_macdLine(close, 12, ConstantModelType["ExponentialMovingAverage"], 26, ConstantModelType["ExponentialMovingAverage"]);
const signal = momentum_bulk_signalLine(macd, ConstantModelType["ExponentialMovingAverage"], 9);

console.log("MACD:", macd);
console.log("Signal:", signal);

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

//! # MACD (Moving Average Convergence Divergence) Optimization Example
//!
//! ## Indicator Type
//! Signal Line Crossover (MACD line crosses Signal line)
//!
//! ## Signal Detection
//! - **Buy Signal**: Signal line crosses below MACD line (bullish crossover)
//! - **Sell Signal**: Signal line crosses above MACD line (bearish crossover)
//!
//! ## Parameters Optimized
//! - `short_period`: Fast MA period (2-60)
//! - `long_period`: Slow MA period (2-120, must be > short_period)
//! - `short_model`: Moving average model/type for short period
//! - `long_model`: Moving average model/type for long period
//! - `signal_period`: Signal line period (2-60)
//! - `signal_model`: Moving average model/type for signal line
//!
//! ## Trading Simulation
//! - Opens long positions when signal line crosses below MACD line
//! - Opens short positions when signal line crosses above MACD line
//! - Prevents simultaneous long and short positions
//! - Invests 20% of capital per trade
//!
//! ## Usage
//! ```bash
//! cp src/examples/macd_optimization.rs src/main.rs
//! cat src/year_spx.csv | cargo run --release
//! ```

use centaur_technical_indicators::chart_trends::{peaks, valleys};
use centaur_technical_indicators::momentum_indicators::bulk::{macd_line, signal_line};
use centaur_technical_indicators::ConstantModelType;
use chrono::NaiveDate;
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()
}

fn simulate_trading(
    best_macd: &[f64],
    best_signal: &[f64],
    best_long_period: usize,
    best_signal_period: usize,
    close: &[f64],
) {
    println!("
--- 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} | {:<10} | {:<12} | {:<15} | {:<10}",
        "Day", "Event", "MACD", "Signal", "Price", "Shares", "Capital", "P/L"
    );
    println!("{}", "-".repeat(105));

    // Start from signal_period to ensure we have signal line values
    // Note: signal.len() will be less than macd.len() due to the signal period calculation
    // Start from 1 to track previous values for crossover detection
    for i in 1..best_signal.len() {
        // We need to account for signal period when calculating price location
        let price_index = i + best_signal_period + best_long_period;
        if price_index >= close.len() {
            break;
        }

        let macd_val = best_macd[i + best_signal_period];
        let signal_val = best_signal[i];
        let prev_macd_val = best_macd[i + best_signal_period - 1];
        let prev_signal_val = best_signal[i - 1];
        let current_price = close[price_index];
        let day = price_index;

        // --- Handle Long Position ---
        if let Some(long_pos) = open_long.take() {
            // Sell signal: signal crosses above MACD
            if signal_val > macd_val && prev_signal_val <= prev_macd_val {
                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.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                    day,
                    "Sell (Close Long)",
                    macd_val,
                    signal_val,
                    current_price,
                    long_pos.shares,
                    capital,
                    profit
                );
            } else {
                open_long = Some(long_pos); // Put it back if not selling
            }
        } else if signal_val < macd_val && prev_signal_val >= prev_macd_val && open_short.is_none() {
            // Buy signal: signal crosses below MACD (don't buy if short is open)
            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.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                day,
                "Buy (Open Long)",
                macd_val,
                signal_val,
                current_price,
                shares_bought,
                capital,
                "-"
            );
        }

        // --- Handle Short Position ---
        if let Some(short_pos) = open_short.take() {
            // Cover signal: signal crosses below MACD
            if signal_val < macd_val && prev_signal_val >= prev_macd_val {
                let cost_to_cover = short_pos.shares * current_price;
                let profit = (short_pos.shares * short_pos.entry_price) - cost_to_cover;
                capital += profit; // Add profit to capital
                println!(
                    "{:<5} | {:<19} | {:<10.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                    day,
                    "Cover (Close Short)",
                    macd_val,
                    signal_val,
                    current_price,
                    short_pos.shares,
                    capital,
                    profit
                );
            } else {
                open_short = Some(short_pos); // Put it back if not covering
            }
        } else if signal_val > macd_val && prev_signal_val <= prev_macd_val && open_long.is_none() {
            // Short signal: signal crosses above MACD (don't short if long is open)
            let short_value = capital * investment_pct;
            let shares_shorted = short_value / current_price;
            open_short = Some(Position {
                entry_price: current_price,
                shares: shares_shorted,
            });
            // Capital doesn't change when opening a short, it's held as collateral
            println!(
                "{:<5} | {:<19} | {:<10.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                day,
                "Short (Open Short)",
                macd_val,
                signal_val,
                current_price,
                shares_shorted,
                capital,
                "-"
            );
        }
    }

    println!("
--- 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!("
Initial Capital: ${:.2}", initial_capital);
    println!("Final Capital:   ${:.2}", capital);
    println!("Total P/L:       ${:.2}", final_pnl);
}

#[derive(Deserialize, Debug)]
struct Ohlc {
    #[serde(with = "csv_date_format")]
    date: NaiveDate,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
}

mod csv_date_format {
    use chrono::NaiveDate;
    use serde::{self, Deserialize, Deserializer};

    const FORMAT: &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
}

pub fn main() {
    let data = get_data();
    println!("Extracting values from data");

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

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

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

    let indicator_loop = Instant::now();

    // Get buy and sell points
    // 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>>();

    println!("
Buy points found: {}", buy_points.len());
    println!("Sell points found: {}", sell_points.len());

    // Define the ranges for optimization
    let max_long_period = 120;
    let min_long_period = 2;
    let max_short_period = 60;
    let min_short_period = 2;
    let max_signal_period = 60;
    let min_signal_period = 2;

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

    let models = vec![
        ConstantModelType::SimpleMovingAverage,
        ConstantModelType::ExponentialMovingAverage,
        ConstantModelType::SmoothedMovingAverage,
        ConstantModelType::SimpleMovingMedian,
        ConstantModelType::SimpleMovingMode,
    ];

    // Store the best parameters found
    let mut best_rating = f64::NEG_INFINITY;
    let mut best_short_period = 0;
    let mut best_long_period = 0;
    let mut best_short_model = ConstantModelType::SimpleMovingAverage;
    let mut best_long_model = ConstantModelType::SimpleMovingAverage;
    let mut best_signal_period = 0;
    let mut best_signal_model = ConstantModelType::SimpleMovingAverage;
    let mut best_macd = vec![];
    let mut best_signal = vec![];

    // Calculate total iterations for progress tracking
    let total_count = (max_long_period - min_long_period + 1)
        * (max_short_period - min_short_period + 1)
        * models.len()
        * models.len()
        * (max_signal_period - min_signal_period + 1)
        * models.len();
    let mut iteration_count = 0;
    let mut next_log_percent = 5;

    println!("
Running optimization loop with approximately {} total iterations...", total_count);

    for &long_ma_type in &models {
        for &short_ma_type in &models {
            for long_period in min_long_period..=max_long_period {
                for short_period in min_short_period..=max_short_period {
                    if short_period >= long_period {
                        continue;
                    }
                    if close.len() < long_period + short_period {
                        continue;
                    }

                    // Calculate MACD line
                    let macd = macd_line(&close, short_period, short_ma_type, long_period, long_ma_type)
                        .expect("Failed to calculate MACD line");

                    // Now optimize signal line parameters
                    for signal_period in min_signal_period..=max_signal_period {
                        if macd.len() < signal_period + 1 {
                            continue;
                        }

                        for &signal_ma_type in &models {
                            iteration_count += 1;
                            let percent_complete = (iteration_count * 100 / total_count) as u64;
                            if percent_complete >= next_log_percent {
                                let elapsed = indicator_loop.elapsed().as_secs();
                                let estimated_total = if iteration_count > 0 {
                                    elapsed * total_count as u64 / iteration_count as u64
                                } else {
                                    0
                                };
                                let estimated_remaining = estimated_total.saturating_sub(elapsed);
                                println!(
                                    "Optimization is {}% complete... (Elapsed: {}s, Est. remaining: {}s)",
                                    next_log_percent, elapsed, estimated_remaining
                                );
                                next_log_percent += 5;
                            }

                            // Calculate signal line
                            let signal = signal_line(&macd, signal_ma_type, signal_period)
                                .expect("Failed to calculate Signal line");

                            let mut rating = vec![];
                            let mut matched_sell = vec![];
                            let mut matched_buy = vec![];

                            // Signal detection: crossover between MACD and signal line
                            // Start from 1 to compare with previous value for crossover detection
                            for i in 1..signal.len() {
                                // signal[i] corresponds to macd[i + signal_period] which corresponds to close[i + signal_period + long_period]
                                let price_location = i + signal_period + long_period;
                                if price_location >= close.len() {
                                    break;
                                }

                                let macd_val = macd[i + signal_period];
                                let signal_val = signal[i];
                                let prev_macd_val = macd[i + signal_period - 1];
                                let prev_signal_val = signal[i - 1];

                                // Sell signal: signal crosses above MACD (bearish)
                                if signal_val > macd_val && prev_signal_val <= prev_macd_val {
                                    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);
                                        }
                                    }
                                }
                                // Buy signal: signal crosses below MACD (bullish)
                                else if signal_val < macd_val && prev_signal_val >= prev_macd_val {
                                    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);
                                        }
                                    }
                                }
                            }

                            // 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);
                                }
                            }

                            if !rating.is_empty() {
                                let total_rating: f64 = rating.iter().sum::<f64>() / (rating.len() as f64);
                                if total_rating > best_rating {
                                    best_rating = total_rating;
                                    best_long_period = long_period;
                                    best_short_period = short_period;
                                    best_long_model = long_ma_type;
                                    best_short_model = short_ma_type;
                                    best_signal_period = signal_period;
                                    best_signal_model = signal_ma_type;
                                    best_macd = macd.clone();
                                    best_signal = signal.clone();
                                }
                            }
                        }
                    }
                }
            }
        }
    }

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

    println!("
Best MACD parameters found:");
    println!("Long Period: {}", best_long_period);
    println!("Short Period: {}", best_short_period);
    println!("Long Model: {:?}", best_long_model);
    println!("Short Model: {:?}", best_short_model);
    println!("Signal Period: {}", best_signal_period);
    println!("Signal Model: {:?}", best_signal_model);
    println!("Rating: {:.4}", best_rating);

    simulate_trading(
        &best_macd,
        &best_signal,
        best_long_period,
        best_signal_period,
        &close,
    );

    println!("

Default MACD values for comparison:");
    let default_macd = macd_line(
        &close,
        12,
        ConstantModelType::ExponentialMovingAverage,
        26,
        ConstantModelType::ExponentialMovingAverage,
    )
        .expect("Failed to calculate default MACD line");
    let default_signal = signal_line(&default_macd, ConstantModelType::ExponentialMovingAverage, 9)
        .expect("Failed to calculate default Signal line");
    simulate_trading(&default_macd, &default_signal, 26, 9, &close);
}

Optimization Output

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

Best MACD parameters found:
Long Period: 7
Short Period: 4
Long Model: SimpleMovingMedian
Short Model: SimpleMovingAverage
Signal Period: 13
Signal Model: SimpleMovingMode
Rating: 0.0972

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
$1002.46
Total P&L
$2.46
Open Position
None

Default Trading Simulation

Initial Investment
$1000.00
Final Capital
$992.13
Total P&L
$-7.87
Open Position
  • SideSHORT
  • Shares0.0328
  • Entry$5983.25
  • Value$11.29

Analysis

The optimized MACD parameters demonstrate improved trading performance compared to the default settings.

The optimized MACD strategy yielded a profit of $2.46, ending with a final capital of $1002.46. This outperformed the default MACD strategy which resulted in a loss of $7.87. The optimized parameters provided better-timed crossover signals, leading to more successful trades and improved capital preservation across market conditions.

Trading Simulation Code

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

use centaur_technical_indicators::momentum_indicators::bulk::{macd_line, signal_line};
use centaur_technical_indicators::ConstantModelType;

fn simulate_trading(
    best_macd: &[f64],
    best_signal: &[f64],
    best_long_period: usize,
    best_signal_period: usize,
    close: &[f64],
) {
    println!("
--- 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} | {:<10} | {:<12} | {:<15} | {:<10}",
        "Day", "Event", "MACD", "Signal", "Price", "Shares", "Capital", "P/L"
    );
    println!("{}", "-".repeat(105));

    // Start from signal_period to ensure we have signal line values
    // Note: signal.len() will be less than macd.len() due to the signal period calculation
    // Start from 1 to track previous values for crossover detection
    for i in 1..best_signal.len() {
        // We need to account for signal period when calculating price location
        let price_index = i + best_signal_period + best_long_period;
        if price_index >= close.len() {
            break;
        }

        let macd_val = best_macd[i + best_signal_period];
        let signal_val = best_signal[i];
        let prev_macd_val = best_macd[i + best_signal_period - 1];
        let prev_signal_val = best_signal[i - 1];
        let current_price = close[price_index];
        let day = price_index;

        // --- Handle Long Position ---
        if let Some(long_pos) = open_long.take() {
            // Sell signal: signal crosses above MACD
            if signal_val > macd_val && prev_signal_val <= prev_macd_val {
                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.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                    day,
                    "Sell (Close Long)",
                    macd_val,
                    signal_val,
                    current_price,
                    long_pos.shares,
                    capital,
                    profit
                );
            } else {
                open_long = Some(long_pos); // Put it back if not selling
            }
        } else if signal_val < macd_val && prev_signal_val >= prev_macd_val && open_short.is_none() {
            // Buy signal: signal crosses below MACD (don't buy if short is open)
            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.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                day,
                "Buy (Open Long)",
                macd_val,
                signal_val,
                current_price,
                shares_bought,
                capital,
                "-"
            );
        }

        // --- Handle Short Position ---
        if let Some(short_pos) = open_short.take() {
            // Cover signal: signal crosses below MACD
            if signal_val < macd_val && prev_signal_val >= prev_macd_val {
                let cost_to_cover = short_pos.shares * current_price;
                let profit = (short_pos.shares * short_pos.entry_price) - cost_to_cover;
                capital += profit; // Add profit to capital
                println!(
                    "{:<5} | {:<19} | {:<10.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                    day,
                    "Cover (Close Short)",
                    macd_val,
                    signal_val,
                    current_price,
                    short_pos.shares,
                    capital,
                    profit
                );
            } else {
                open_short = Some(short_pos); // Put it back if not covering
            }
        } else if signal_val > macd_val && prev_signal_val <= prev_macd_val && open_long.is_none() {
            // Short signal: signal crosses above MACD (don't short if long is open)
            let short_value = capital * investment_pct;
            let shares_shorted = short_value / current_price;
            open_short = Some(Position {
                entry_price: current_price,
                shares: shares_shorted,
            });
            // Capital doesn't change when opening a short, it's held as collateral
            println!(
                "{:<5} | {:<19} | {:<10.4} | {:<10.4} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                day,
                "Short (Open Short)",
                macd_val,
                signal_val,
                current_price,
                shares_shorted,
                capital,
                "-"
            );
        }
    }

    println!("
--- 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!("
Initial 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

    // Run simulation with optimized parameters
    simulate_trading(&best_macd, &best_signal, best_long_period, best_signal_period, &close);

    println!("

Default MACD values for comparison:");
    let default_macd = macd_line(
        &close,
        12,
        ConstantModelType::ExponentialMovingAverage,
        26,
        ConstantModelType::ExponentialMovingAverage,
    )
        .expect("Failed to calculate default MACD line");
    let default_signal = signal_line(&default_macd, ConstantModelType::ExponentialMovingAverage, 9)
        .expect("Failed to calculate default Signal line");
    simulate_trading(&default_macd, &default_signal, 26, 9, &close);
}