Percentage Price Oscillator (PPO)
Shows the percentage difference between two moving averages to identify momentum shifts.
Export Optimization Code
Advanced Options & Examples
Optimization Output Example
Example output from running the optimization code above on a year of S&P data.
Best Indicator parameters found:
long_period = 9
short_period = 5
model = SimpleMovingAverage
oversold = -14
overbought = 43
Rating: 0.3730701754385966
Best Indicator values: [0.646822610750545, 0.5278922991711282, 0.2831605394941842, ...]
Analysis
The optimized PPO parameters demonstrate significantly improved trading performance compared to the default settings. A comprehensive trading simulation was conducted to evaluate both parameter sets. Both strategies started with an initial capital of $1000 and invested 20% of the remaining capital on each trade.
Long positions were opened when the PPO fell below the oversold level and closed when it rose above the overbought level. Short positions were opened when the PPO rose above the overbought level and closed when it fell below the oversold level.
The results are shown in the tables below. The optimized PPO strategy yielded a profit of $5.51, with a $194.94 open position. This substantially outperformed the default PPO strategy which resulted in a loss of $97.14 (with a $168.50 open position). The optimized parameters provided more reliable trading signals and better timing for entries and exits.
Optimized Trading Simulation
- SideLONG
- Shares0.0346
- Entry$5861.57
- Value$194.94
Default Trading Simulation
- SideLONG
- Shares0.0299
- Entry$6144.15
- Value$168.50
Trading Simulation Code
For those who want to run their own simulation to compare results.
use centaur_technical_indicators::momentum_indicators::bulk::percentage_price_oscillator;
use centaur_technical_indicators::ConstantModelType;
fn simulate_trading(best_indicator: &[f64], best_period: usize, close: &[f64], best_oversold: usize, best_overbought: usize) {
// --- NEW TRADING SIMULATION CODE ---
println!("
--- Trading Simulation ---");
let best_oversold = (best_oversold as f64 / 100.0) * -1.0;
let best_overbought = best_overbought as f64 / 100.0;
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;
if price_index >= close.len() { break; }
let ppo_val = best_indicator[i];
let current_price = close[price_index];
let day = price_index;
// --- Handle Long Position ---
if let Some(long_pos) = open_long.take() {
if ppo_val > best_overbought as f64 {
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)", ppo_val, current_price, long_pos.shares, capital, profit);
} else {
open_long = Some(long_pos); // Put it back if not selling
}
} else if ppo_val < best_oversold as f64 && open_short.is_none() { // 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.2} | ${:<9.2} | {:<12.4} | ${:<14.2} | {}",
day, "Buy (Open Long)", ppo_val, current_price, shares_bought, capital, "-");
}
// --- Handle Short Position ---
if let Some(short_pos) = open_short.take() {
if ppo_val < best_oversold as f64 {
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)", ppo_val, current_price, short_pos.shares, capital, profit);
} else {
open_short = Some(short_pos); // Put it back if not covering
}
} else if ppo_val > best_overbought as f64 && 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)", ppo_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
simulate_trading(&best_indicators, best_long_period, &close, best_oversold, best_overbought);
println!("
Default Indicator values for comparison:");
let default_wr = percentage_price_oscillator(&close, 9, 26, ConstantModelType::ExponentialMovingAverage);
println!("{:?}", default_wr);
simulate_trading(&default_wr, 20, &close, 10, 10);
}