Parabolic Time Price System
Tracks price trends with dynamic stop-loss levels that accelerate with trend momentum.
About the 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.
Example Usage
use centaur_technical_indicators::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 centaur_technical_indicators as cti
# fetch the data in your preferred way
# high = [...] # high prices
# low = [...] # low prices
para = cti.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/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 para = trend_bulk_parabolicTimePriceSystem(high, low, 0.02, 0.2, 0.02, "Long", 0.0);
console.log(para);
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::trend_indicators::bulk::parabolic_time_price_system;
use centaur_technical_indicators::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
Example output from running the optimization code above 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, ... ]
Trading Simulation
In order to determine whether the optimized parameters beat the defaults, a trading simulation was run, below are the results.
Optimized Trading Simulation
- SideLONG
- Shares0.0356
- Entry$5638.94
- Value$5638.94
Default Trading Simulation
- SideSHORT
- Shares0.0327
- Entry$5983.25
- Value$5638.94
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.
Trading Simulation Code
For those who 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);
}