Supertrend indicator

Trend-following indicator that provides buy and sell signals based on ATR volatility.

About the Supertrend indicator

Supertrend Indicator

The Supertrend indicator is a trend-following overlay that combines average true range (ATR) with a moving average to create dynamic support and resistance levels. It plots a single line that stays above price during downtrends and below price during uptrends, switching sides when the trend reverses.

What It Measures

Supertrend measures the current market trend direction by calculating bands based on ATR multiplied by a constant factor, then tracking whether price is trading above or below these bands. The indicator adapts to volatility because ATR increases during volatile periods and decreases during calm periods. When price closes above the Supertrend line, it signals an uptrend; when price closes below, it signals a downtrend.

When to Use

Use Supertrend in trending markets to identify entry and exit points aligned with the prevailing trend. It's particularly effective for: (1) Following strong trends in liquid markets like major indices and forex pairs, (2) Setting trailing stops that adapt to market volatility, and (3) Filtering trade signals from other indicators by only taking trades in the direction of the Supertrend. Avoid using Supertrend in choppy, sideways markets where it may generate frequent whipsaws.

Interpretation

  • Buy Signal: Price crosses above the Supertrend line, indicating a potential uptrend beginning
  • Sell Signal: Price crosses below the Supertrend line, indicating a potential downtrend beginning
  • Trend Continuation: Price staying consistently above (uptrend) or below (downtrend) the line confirms trend strength
  • Line Position: The Supertrend line acts as dynamic support during uptrends and dynamic resistance during downtrends

Parameters

The Supertrend indicator has three main parameters:

  • Period: The lookback period for calculating the average true range (ATR)

    • Default: 10 periods
    • Optimized: 3 periods (more responsive to recent price action)
    • Range: Typically 2-20 periods
  • Model: The type of moving average used for smoothing ATR

    • Default: SimpleMovingAverage
    • Optimized: ExponentialMovingAverage (gives more weight to recent data)
    • Options: SimpleMovingAverage, ExponentialMovingAverage, SmoothedMovingAverage, SimpleMovingMedian, SimpleMovingMode
  • Multiplier: The factor applied to ATR to set the distance of bands from the moving average

    • Default: 3.0
    • Optimized: 0.58 (tighter bands for quicker signals)
    • Range: Typically 0.5-5.0

Example Usage

use centaur_technical_indicators::candle_indicators::bulk::supertrend;
use centaur_technical_indicators::ConstantModelType;

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

    let supertrend = supertrend(&high, &low, &close, ConstantModelType::SimpleMovingAverage, 3.0, 10);
    println!("{:?}", supertrend);
}
import centaur_technical_indicators as cti

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

supertrend = cti.candle_indicators.bulk.supertrend(high, low, close, model="SimpleMovingAverage", multiplier=3.0, period=10)
print(supertrend)
// WASM import
import init, { momentum_bulk_supertrend } 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 high = [...];   // high prices
// const low = [...];    // low prices

const supertrend = momentum_bulk_supertrend(high, low, close, ConstantModelType["SimpleMovingAverage"], 3.0, 10);
console.log(supertrend);

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

use centaur_technical_indicators::chart_trends::{peaks, valleys};
use centaur_technical_indicators::candle_indicators::bulk::supertrend;
use centaur_technical_indicators::ConstantModelType;

fn proximity_rating(fuzzed_location: &usize, price_location: &usize) -> f64 {
    1.0 / (*fuzzed_location as f64 - *price_location as f64).abs()
}

pub fn main() {

    // fetch the data in your preferred way
    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).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();
        let buy_points = valleys(&close, 20, 5).into_iter().map(|(_, i)| i).collect::<Vec<usize>>();

        // Define the ranges for optimization
        let max_period = 126;
        let min_period = 2;

        let min_difference = 0;
        let max_difference = 1000;

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

        let min_constant_multiplier = 0;
        let max_constant_multiplier = 1000;

        // Store the best parameters found
        let mut best_rating = 0.0;
        let mut best_period = 0;
        let mut best_constant_multiplier = 0.0;
        let mut best_model = ConstantModelType::SimpleMovingAverage;
        let mut best_indicators = vec![];

        let total_count = (max_period - min_period) * (max_constant_multiplier - min_constant_multiplier) * 5;
        let mut iteration_count = 0;
        println!("
Running optimization loop with {} total iterations...", total_count);

                for multiplier in min_constant_multiplier..=max_constant_multiplier {
                    let multiplier = multiplier as f64 / 100.0;
                    for &ma_type in &[ConstantModelType::SimpleMovingAverage, ConstantModelType::ExponentialMovingAverage, ConstantModelType::SmoothedMovingAverage, ConstantModelType::SimpleMovingMedian, ConstantModelType::SimpleMovingMode] {
                        for period in min_period..=max_period {
                                iteration_count += 1;
                                if iteration_count % (total_count / 20) == 0 {
                                    let next_log_percent = (iteration_count * 100) / total_count;
                                    println!("Optimization is {}% complete...", next_log_percent);
                                }
                                let indicators = supertrend(&high, &low, &close, ma_type, multiplier, period);
                                let mut rating = vec![];
                                let mut matched_sell = vec![];
                                let mut matched_buy = vec![];
                                for i in 1..indicators.len() {
                                    let price_location = i + period + 1; // Adjust for indicator lag
                                    if i >= price_location { break; }
                                    if price_location >= close.len() { break; }
                                    if close[price_location] < indicators[i] && close[price_location-1] >= indicators[i-1] {
                                        if sell_points.contains(&price_location) {
                                            rating.push(1.0);
                                            matched_sell.push(price_location);
                                        } else if buy_points.contains(&price_location) {
                                            // If buy point == rsi, rate negatively
                                            rating.push(-1.0);
                                        } else {
                                            let mut found_sell = false;
                                            for fuzzed_location in (price_location - fuzz_parameter)..=(price_location + fuzz_parameter) {
                                                // It's ok if we count multiple times for fuzzed locations as we reduce the rating
                                                // based off of distance from the actual sell point which will impact the final rating
                                                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) {
                                                    // Note the `-` here to penalize for selling instead of buying
                                                    if !matched_sell.contains(&fuzzed_location) {
                                                        rating.push(-proximity_rating(&fuzzed_location, &price_location));
                                                    }
                                                }
                                            }
                                            if !found_sell {
                                                rating.push(0.0);
                                            }
                                        }
                                    } else if close[price_location] > indicators[i] && close[price_location-1] <= indicators[i-1] {
                                        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 - fuzz_parameter)..=(price_location + fuzz_parameter) {
                                                // It's ok if we count multiple times for fuzzed locations as we reduce the rating
                                                // based off of distance from the actual sell point which will impact the final rating
                                                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) {
                                                    // Note the `-` here to penalize for buying instead of selling
                                                    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);
                                    }
                                }
                                let total_rating: f64 = rating.iter().sum::<f64>() / (rating.len() as f64);
                                if total_rating > best_rating {
                                    best_rating = total_rating;
                                    best_period = period;
                                    best_constant_multiplier = multiplier;
                                    best_model = ma_type;
                                    best_indicators = indicators.clone();
                            }
                }
            }
        }

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

        println!("
Best Indicator parameters found:");
        println!("period = {}", best_period);
        println!("model = {:?}", best_model);
        println!("constant_multiplier = {}", best_constant_multiplier);
        println!("Rating: {}", best_rating);
        println!("Best Indicator values: {:?}", best_indicators);
}

Optimization Output

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

Best Indicator parameters found:
period = 3
model = ExponentialMovingAverage
constant_multiplier = 0.58
Rating: 0.10472222222222219
Best Indicator values: [5160.410314285715, 5166.1638571428575, 5207.569342857142, ...]

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
$1025.19
Total P&L
$25.19
Open Position
  • SideSHORT
  • Shares0.03367914400016023
  • Entry$6013.13
  • Value$12.60

Default Trading Simulation

Initial Investment
$1000.00
Final Capital
$979.35
Total P&L
$-20.65
Open Position
  • SideSHORT
  • Shares0.03154228882259018
  • Entry$6114.63
  • Value$15.00

Analysis

The optimized Supertrend indicator (3-period EMA with 0.58 multiplier) generated significantly more trading signals compared to the default 10-period SMA with 3.0 multiplier configuration. A trading simulation was conducted with both parameter sets, starting with $1000 initial capital and investing 20% per trade. Long positions were opened when price crossed above the Supertrend line and closed when price crossed below, while short positions were opened when price crossed below and covered when price crossed above.

The optimized parameters demonstrated superior performance with a final profit of $25.19 (including a $12.60 open short position), substantially outperforming the default configuration which resulted in a loss of $20.65 (including a $15.00 open short position). The more responsive 3-period EMA with lower multiplier provided better trend-following signals that captured price movements more effectively in this dataset, generating 59 trades versus 46 trades with the default parameters.

Trading Simulation Code

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

use centaur_technical_indicators::candle_indicators::bulk::supertrend;
use centaur_technical_indicators::ConstantModelType;

fn chart_simulate_trading(best_indicator: &[f64], best_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} | {:<12} | {:<15} | {:<10}",
             "Day", "Event", "Indicator", "Price", "Shares", "Capital", "P/L");
    println!("{}", "-".repeat(95));

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

        let current_price = close[price_index];
        let day = price_index;

        // --- Handle Long Position ---
        if let Some(long_pos) = open_long.take() {
            if current_price < best_indicator[i] {
                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)", best_indicator[i], current_price, long_pos.shares, capital, profit);
            } else {
                open_long = Some(long_pos); // Put it back if not selling
            }
        } else if current_price > best_indicator[i] && open_short.is_none() {
            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)", best_indicator[i], current_price, shares_bought, capital, "-");
        }

        // --- Handle Short Position ---
        if let Some(short_pos) = open_short.take() {
            if current_price > best_indicator[i] {
                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.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                         day, "Cover (Close Short)", best_indicator[i], current_price, short_pos.shares, capital, profit);
            } else {
                open_short = Some(short_pos); // Put it back if not covering
            }
        } else if current_price < best_indicator[i] && open_long.is_none() { // 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.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                     day, "Short (Open Short)", best_indicator[i], 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);
        println!("{{ position = "LONG", shares = {}, entry_price = "${:.2}", position_value_at_last_price = "${:.2}" }}", pos.shares, pos.entry_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);
        println!("{{ position = "SHORT", shares = {}, entry_price = "${:.2}", position_value_at_last_price = "${:.2}" }}", pos.shares, pos.entry_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

    chart_simulate_trading(&best_indicators, best_period, &close);

    let default_period = 10;
    println!("
Default Indicator values for comparison:");
    let default_dc = supertrend(&high, &low, &close, ConstantModelType::SimpleMovingAverage, 3.0, default_period);
    println!("{:?}", default_dc);
    chart_simulate_trading(&default_dc, default_period, &close);
}