Slow Stochastic Oscillator
Smoothed version of the stochastic oscillator that reduces false signals and noise.
About the Slow Stochastic Oscillator
The Slow Stochastic Oscillator (%D) is a smoothed version of the Stochastic Oscillator (%K). It is calculated by taking a moving average of the Stochastic Oscillator values, which reduces volatility and provides a clearer signal for overbought and oversold conditions. The Slow Stochastic is often considered more reliable than the fast Stochastic Oscillator because it filters out market noise.
The Slow Stochastic is commonly used in conjunction with the Stochastic Oscillator (%K) to generate trading signals. When the fast stochastic crosses above the slow stochastic, it can signal a buying opportunity, and when it crosses below, it may signal a selling opportunity.
What It Measures
The Slow Stochastic Oscillator measures the momentum of price changes by smoothing the regular Stochastic Oscillator values. This smoothing helps traders identify more reliable overbought and oversold conditions while reducing false signals.
When to Use
- Confirm Trend Changes: The Slow Stochastic can help confirm potential trend reversals when used together with the Stochastic Oscillator.
- Identify Overbought/Oversold Conditions: Values above 80 typically indicate that an asset is overbought, while values below 20 suggest it is oversold.
- Generate Trading Signals: Crossovers between the fast and slow stochastic lines can provide entry and exit points.
Interpretation
- High values (e.g., above 80): Overbought conditions, potential sell signal
- Low values (e.g., below 20): Oversold conditions, potential buy signal
- Crossovers: When %K crosses above %D, it may signal a bullish trend; when %K crosses below %D, it may signal a bearish trend
Example Usage
use centaur_technical_indicators::momentum_indicators::bulk::{slow_stochastic, stochastic_oscillator};
use centaur_technical_indicators::ConstantModelType::SimpleMovingAverage;
pub fn main() {
// fetch the data in your preferred way
// let close = vec![...]; // closing prices
let stochastics = stochastic_oscillator(&close, 14);
let slow_stochastics = slow_stochastic(&stochastics, SimpleMovingAverage, 3);
println!("{:?}", slow_stochastics);
}
import centaur_technical_indicators as cti
# fetch the data in your preferred way
# close = [...] # closing prices
stochastics = cti.momentum_indicators.bulk.stochastic_oscillator(close, period=14)
slow_stochastics = cti.momentum_indicators.bulk.slow_stochastic(stochastics, constant_model_type="simple_moving_average", period=3)
print(slow_stochastics)
// WASM import
import init, { momentum_bulk_slowStochastic, momentum_bulk_stochasticOscillator, 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 stochasticSeries = momentum_bulk_stochasticOscillator(close, 14);
const slowStochasticSeries = momentum_bulk_slowStochastic(stochasticSeries, ConstantModelType.SimpleMovingAverage, 3);
console.log(slowStochasticSeries);
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::momentum_indicators::bulk::{slow_stochastic, stochastic_oscillator};
use centaur_technical_indicators::ConstantModelType;
use centaur_technical_indicators::chart_trends::{peaks, valleys};
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()
}
pub fn main() {
// fetch the data in your preferred way
// let close = vec![...]; // closing prices
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 = 20;
let min_period = 2;
let min_oversold = 0;
let max_oversold = 50;
let min_overbought = 50;
let max_overbought = 100;
let fuzz_parameter = 5; // Allowable distance from buy/sell points
// Store the best parameters found
let mut best_rating = 0.0;
let mut best_period = 0;
let mut best_model = ConstantModelType::SimpleMovingAverage;
let mut best_oversold = 0;
let mut best_overbought = 0;
let mut best_indicators = vec![];
// Optimized SO
let optimized_so = stochastic_oscillator(&close, 13);
for oversold in min_oversold..=max_oversold {
for overbought in min_overbought..=max_overbought {
for &ma_type in &[ConstantModelType::SimpleMovingAverage, ConstantModelType::ExponentialMovingAverage, ConstantModelType::SmoothedMovingAverage] {
for period in min_period..=max_period {
let indicators = slow_stochastic(&optimized_so, ma_type, period);
let mut rating = vec![];
let mut matched_sell = vec![];
let mut matched_buy = vec![];
for i in 0..indicators.len() {
let price_location = i + period;
if indicators[i] > overbought as f64 {
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 - 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 indicators[i] < oversold as f64 {
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_model = ma_type;
best_oversold = oversold;
best_overbought = overbought;
best_indicators = indicators.clone();
}
}
}
}
}
println!(
"Indicators optimization loop took {} ms to run",
indicator_loop.elapsed().as_millis()
);
println!("
Best Slow Stochastic parameters found:");
println!("Period: {}", best_period);
println!("Model: {:?}", best_model);
println!("Oversold threshold: {}", best_oversold);
println!("Overbought threshold: {}", best_overbought);
println!("Rating: {}", best_rating);
println!("Best Slow Stochastic values: {:?}", best_indicators);
}
Optimization Output
Example output from running the optimization code above on a year of S&P data.
Period: 19
Model: SimpleMovingAverage
Oversold threshold: 50
Overbought threshold: 84
Best Slow Stochastic values: [29.04536316035683, 29.024778730753603, 27.712118166355456, ...]
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.0334
- Entry$5872.16
- Value$188.52
Default Trading Simulation
- SideLONG
- Shares0.0308
- Entry$6025.99
- Value$173.70
Analysis
The optimized Slow Stochastic Oscillator parameters generate different trading signals compared to the default settings. A trading simulation was conducted to evaluate the effectiveness of 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 Slow Stochastic fell below the oversold level and closed when it rose above the overbought level. Short positions were opened when the Slow Stochastic rose above the overbought level and closed when it fell below the oversold level.
The results are shown in the tables below. The optimized Slow Stochastic strategy yielded a loss of $26.23, with a $188.52 open position. While this performed better than the default Slow Stochastic strategy which resulted in a loss of $83.81 (with a $173.70 open position), both strategies struggled in this particular market condition.
Trading Simulation Code
For those who want to run their own simulation to compare results.
use centaur_technical_indicators::momentum_indicators::bulk::{slow_stochastic, stochastic_oscillator};
use centaur_technical_indicators::ConstantModelType;
fn simulate_trading(best_indicator: &[f64], best_period: usize, close: &[f64], best_oversold: usize, best_overbought: usize) {
// --- TRADING SIMULATION CODE ---
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", "Stochastic", "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 stoch_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 stoch_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)", stoch_val, current_price, long_pos.shares, capital, profit);
} else {
open_long = Some(long_pos); // Put it back if not selling
}
} else if stoch_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)", stoch_val, current_price, shares_bought, capital, "-");
}
// --- Handle Short Position ---
if let Some(short_pos) = open_short.take() {
if stoch_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)", stoch_val, current_price, short_pos.shares, capital, profit);
} else {
open_short = Some(short_pos); // Put it back if not covering
}
} else if stoch_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)", stoch_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_slow_stochastics, best_period, &close, best_oversold, best_overbought);
// Compare with default parameters
let default_stochastics = stochastic_oscillator(&close, 14);
let default_slow_stochastics = slow_stochastic(&default_stochastics, ConstantModelType::SimpleMovingAverage, 3);
simulate_trading(&default_slow_stochastics, 3, &close, 20, 80);
}