Parabolic Time Price System

The Parabolic Time Price System, also known as Parabolic SAR (Stop and Reverse), is a trend-following indicator developed by J. Welles Wilder. It provides potential entry and exit points by plotting dots above or below price bars, creating a parabolic curve that follows price movements and accelerates with the trend.

What It Measures

The Parabolic SAR measures the direction and potential reversal points of an asset's price trend. The indicator uses an acceleration factor that increases over time as the trend develops, causing the SAR to accelerate toward the price and eventually trigger a reversal signal when price crosses the SAR level.

When to Use

Use the Parabolic SAR when you want to identify trending markets and potential exit points for existing positions. It works best in strongly trending markets where prices make sustained moves in one direction. The indicator is particularly effective for setting trailing stops that tighten as trends develop and mature.

Interpretation

When the SAR dots appear below the price, it indicates an uptrend and suggests holding long positions. When the dots flip above the price, it signals a downtrend and suggests holding short positions. The crossover from one side to the other generates trading signals: price crossing above the SAR is a buy signal, while price crossing below the SAR is a sell signal. The acceleration factor causes the SAR to move faster as the trend develops, making it increasingly sensitive to reversals as positions age.

Default Usage

use rust_ti::trend_indicators::bulk::parabolic_time_price_system;

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

    let para = parabolic_time_price_system(&high, &low, 0.02, 0.2, 0.02, Position::Long, 0.0);
    println!("{:?}", para);
}
import pytechnicalindicators as pti

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

para = pti.trend_indicators.bulk.parabolic_time_price_system(high, low, 0.02, 0.2, 0.02, "Long", 0.0);
print(para)
// WASM import
import init, { trend_bulk_parabolicTimePriceSystem } from 'https://cdn.jsdelivr.net/npm/ti-engine@latest/dist/web/ti_engine.js';

await init();

// fetch the data in your preferred way
// const high = [...];   // high prices
// const low = [...];    // low prices

const para = trend_bulk_parabolicTimePriceSystem(high, low, 0.02, 0.2, 0.02, "Long", 0.0);
console.log(para);

Optimization

The best way to determine what the best parameters for your indicator are is to build a simple optimization loop that tests all possible parameter combinations between a defined min and max value, and rate the output.

Below is an example of how to do this in Rust.

use rust_ti::trend_indicators::bulk::parabolic_time_price_system;
use rust_ti::chart_trends::{peaks, valleys};

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 high = vec![...];   // high prices
    // let low = vec![...];    // low prices
    // let close = vec![...];  // closing prices

    // get buy and sell points
    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 min_af_start = 0.01;
    let max_af_start = 0.05;
    let af_start_step = 0.01;
    
    let min_af_max = 0.15;
    let max_af_max = 0.25;
    let af_max_step = 0.01;
    
    let min_af_step = 0.01;
    let max_af_step = 0.15;
    let af_step_increment = 0.01;

    let fuzz_parameter = 5;

    // Store the best parameters found
    let mut best_rating = 0.0;
    let mut best_af_start = 0.0;
    let mut best_af_max = 0.0;
    let mut best_af_step = 0.0;
    let mut best_position = Position::Long;
    let mut best_indicators = vec![];

    let positions = vec![Position::Long, Position::Short];

    for &position in &positions {
        let mut af_start = min_af_start;
        while af_start <= max_af_start {
            let mut af_max = min_af_max;
            while af_max <= max_af_max {
                let mut af_step = min_af_step;
                while af_step <= max_af_step {
                    let indicators = parabolic_time_price_system(
                        &high,
                        &low,
                        af_start,
                        af_max,
                        af_step,
                        position,
                        0.0,
                    );

                    let mut rating = 0.0;

                    // Rate sell points
                    for &sell_point in &sell_points {
                        for fuzz in 0..=fuzz_parameter {
                            if sell_point + fuzz < indicators.len() && sell_point >= fuzz {
                                let fuzzed_location = sell_point + fuzz;
                                let indicator_value = indicators[fuzzed_location];
                                let price_value = close[sell_point];

                                // For selling, we want SAR below price (bullish becomes bearish)
                                if indicator_value < price_value {
                                    rating += proximity_rating(&fuzzed_location, &sell_point);
                                }

                                if fuzz > 0 {
                                    let fuzzed_location = sell_point - fuzz;
                                    let indicator_value = indicators[fuzzed_location];
                                    if indicator_value < price_value {
                                        rating += proximity_rating(&fuzzed_location, &sell_point);
                                    }
                                }
                            }
                        }
                    }

                    // Rate buy points
                    for &buy_point in &buy_points {
                        for fuzz in 0..=fuzz_parameter {
                            if buy_point + fuzz < indicators.len() && buy_point >= fuzz {
                                let fuzzed_location = buy_point + fuzz;
                                let indicator_value = indicators[fuzzed_location];
                                let price_value = close[buy_point];

                                // For buying, we want SAR above price (bearish becomes bullish)
                                if indicator_value > price_value {
                                    rating += proximity_rating(&fuzzed_location, &buy_point);
                                }

                                if fuzz > 0 {
                                    let fuzzed_location = buy_point - fuzz;
                                    let indicator_value = indicators[fuzzed_location];
                                    if indicator_value > price_value {
                                        rating += proximity_rating(&fuzzed_location, &buy_point);
                                    }
                                }
                            }
                        }
                    }

                    if rating > best_rating {
                        best_rating = rating;
                        best_af_start = af_start;
                        best_af_max = af_max;
                        best_af_step = af_step;
                        best_position = position;
                        best_indicators = indicators;
                    }

                    af_step += af_step_increment;
                }
                af_max += af_max_step;
            }
            af_start += af_start_step;
        }
    }

    println!("Best Parabolic SAR parameters found:");
    println!("acceleration_factor_start = {}", best_af_start);
    println!("acceleration_factor_max = {}", best_af_max);
    println!("acceleration_factor_step = {}", best_af_step);
    println!("start_position = {:?}", best_position);
    println!("Rating: {}", best_rating);
    println!("Best Indicator values: {:?}", &best_indicators[0..3.min(best_indicators.len())]);
}

Optimization Output

Below is an example output from the optimization code above run on a year of S&P data.

Best Parabolic SAR parameters found:
acceleration_factor_start = 0.01
acceleration_factor_max = 0.22
acceleration_factor_step = 0.1
start_position = Short
Rating: 0.10524691358024692
Best Indicator values: [5176.85, 5176.85, 5175.6, ... ]

Interactive Chart

To better illustrate how the indicator performs with different parameters, an interactive chart is provided below comparing default parameters (blue) with optimized parameters (green).

Analysis

The optimized Parabolic SAR parameters (AF start=0.01, AF max=0.22, AF step=0.1, starting Short) generated significantly better results compared to the default parameters (AF start=0.02, AF max=0.2, AF step=0.02, starting Long). The optimized strategy achieved a profit of $3.20 with 50 trades, while the default strategy resulted in a loss of $-10.97 with 34 trades. The lower initial acceleration factor in the optimized parameters allows the SAR to be more patient before accelerating, which reduces false signals and whipsaws, leading to better overall performance across the trading period.

Optimized trading simulation

Initial Investment
$1000.00
Final Capital
$1003.20
Total P&L
$3.20
Open Position
  • SideLONG
  • Shares0.0356
  • Entry$5638.94
  • Value$5638.94

Default trading simulation

Initial Investment
$1000.00
Final Capital
$989.03
Total P&L
$-10.97
Open Position
  • SideSHORT
  • Shares0.0327
  • Entry$5983.25
  • Value$5638.94

Trading simulation code

For those you want to run their own simulation to compare results


fn simulate_trading(best_indicator: &[f64], high: &[f64], low: &[f64], close: &[f64]) {
    println!("
--- Trading Simulation ---");

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

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

    let mut open_position: Option<Trade> = None;

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

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

        let sar = best_indicator[i];
        let prev_sar = best_indicator[i - 1];
        let current_price = close[i];
        let prev_price = close[i - 1];
        let day = i;

        // Detect position changes based on SAR crossovers
        if let Some(trade) = open_position.take() {
            // Check if we need to close and reverse position
            let should_reverse = match trade.position_type {
                Position::Long => current_price < sar && prev_price >= prev_sar,
                Position::Short => current_price > sar && prev_price <= prev_sar,
            };

            if should_reverse {
                // Close current position
                let exit_value = match trade.position_type {
                    Position::Long => {
                        let sale_value = trade.shares * current_price;
                        let profit = sale_value - (trade.shares * trade.entry_price);
                        capital += sale_value;
                        println!(
                            "{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                            day,
                            "Close Long",
                            sar,
                            current_price,
                            trade.shares,
                            capital,
                            profit
                        );
                        profit
                    }
                    Position::Short => {
                        let cost_to_cover = trade.shares * current_price;
                        let profit = (trade.shares * trade.entry_price) - cost_to_cover;
                        capital += profit;
                        println!(
                            "{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | ${:<9.2}",
                            day,
                            "Close Short",
                            sar,
                            current_price,
                            trade.shares,
                            capital,
                            profit
                        );
                        profit
                    }
                };

                // Open reverse position
                let investment = capital * investment_pct;
                let shares = investment / current_price;
                let new_position_type = match trade.position_type {
                    Position::Long => {
                        println!(
                            "{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                            day,
                            "Open Short (Reverse)",
                            sar,
                            current_price,
                            shares,
                            capital,
                            "-"
                        );
                        Position::Short
                    }
                    Position::Short => {
                        capital -= investment;
                        println!(
                            "{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                            day,
                            "Open Long (Reverse)",
                            sar,
                            current_price,
                            shares,
                            capital,
                            "-"
                        );
                        Position::Long
                    }
                };

                open_position = Some(Trade {
                    entry_price: current_price,
                    shares,
                    position_type: new_position_type,
                });
            } else {
                // Keep position open
                open_position = Some(trade);
            }
        } else {
            // No position open, check for entry signal
            if prev_price <= prev_sar && current_price > sar {
                // Bullish crossover - go long
                let investment = capital * investment_pct;
                let shares = investment / current_price;
                capital -= investment;
                println!(
                    "{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                    day,
                    "Open Long",
                    sar,
                    current_price,
                    shares,
                    capital,
                    "-"
                );
                open_position = Some(Trade {
                    entry_price: current_price,
                    shares,
                    position_type: Position::Long,
                });
            } else if prev_price >= prev_sar && current_price < sar {
                // Bearish crossover - go short
                let investment = capital * investment_pct;
                let shares = investment / current_price;
                println!(
                    "{:<5} | {:<19} | {:<10.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
                    day,
                    "Open Short",
                    sar,
                    current_price,
                    shares,
                    capital,
                    "-"
                );
                open_position = Some(Trade {
                    entry_price: current_price,
                    shares,
                    position_type: Position::Short,
                });
            }
        }
    }

    println!("
--- Final Results ---");
    if let Some(trade) = open_position {
        let last_price = close.last().unwrap_or(&0.0);
        match trade.position_type {
            Position::Long => {
                println!("Simulation ended with an OPEN LONG position:");
                println!("  - Shares: {:.4}", trade.shares);
                println!("  - Entry Price: ${:.2}", trade.entry_price);
                let current_value = trade.shares * last_price;
                capital += current_value;
                println!(
                    "  - Position value at last price (${:.2}): ${:.2}",
                    last_price, current_value
                );
            }
            Position::Short => {
                println!("Simulation ended with an OPEN SHORT position:");
                println!("  - Shares: {:.4}", trade.shares);
                println!("  - Entry Price: ${:.2}", trade.entry_price);
                let cost_to_cover = trade.shares * last_price;
                let pnl = (trade.shares * trade.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 trading simulation with optimized parameters
    simulate_trading(&best_indicators, &high, &low, &close);

    // Compare with default parameters (typically 0.02, 0.2, 0.02, Long)
    println!("

Default Indicator values for comparison:");
    let default_sar = parabolic_time_price_system(&high, &low, 0.02, 0.2, 0.02, Position::Long, 0.0);
    println!("{:?}", default_sar);
    simulate_trading(&default_sar, &high, &low, &close);


}