The Mean Reversion strategy trades on the assumption that prices tend to revert to their historical average. It uses z-score analysis to identify statistically significant deviations and takes positions expecting mean reversion.
Strategy logic
The strategy uses statistical measures to identify trading opportunities:
Calculate rolling mean and standard deviation over lookback period
Compute z-score: (current_price - mean) / std_dev
Buy signal - Z-score < -entry_threshold (price significantly below mean)
Sell signal - Z-score > entry_threshold (price significantly above mean)
Exit long - Z-score rises back above -exit_threshold
Exit short - Z-score falls back below exit_threshold
Scale into positions incrementally up to max position size
Implementation
Source: crates/gb-types/src/strategy.rs:699-912
pub struct MeanReversionStrategy {
config : StrategyConfig ,
initialized : bool ,
lookback_period : usize ,
entry_threshold : Decimal , // Standard deviations
exit_threshold : Decimal ,
position_size : Decimal ,
max_position_size : Decimal ,
}
Z-score calculation
From strategy.rs:747-773:
fn calculate_z_score ( & self , prices : & [ Decimal ]) -> Option < Decimal > {
if prices . len () < self . lookback_period {
return None ;
}
let recent_prices : Vec < Decimal > = prices . iter ()
. rev ()
. take ( self . lookback_period)
. cloned ()
. collect ();
let current_price = * prices . last () ? ;
// Calculate mean
let mean : Decimal = recent_prices . iter () . sum :: < Decimal >()
/ Decimal :: from ( recent_prices . len ());
// Calculate standard deviation
let variance : Decimal = recent_prices . iter ()
. map ( | price | ( * price - mean ) * ( * price - mean ))
. sum :: < Decimal >() / Decimal :: from ( recent_prices . len ());
let std_dev = variance . to_f64 ()
. map ( | v | v . sqrt ())
. and_then ( Decimal :: from_f64 )
. unwrap_or ( Decimal :: ZERO );
if std_dev != Decimal :: ZERO {
Some (( current_price - mean ) / std_dev )
} else {
None
}
}
Entry logic
From strategy.rs:808-848:
if z_score < - self . entry_threshold {
// Price is significantly below mean - buy opportunity
let available_cash = context . get_available_cash ();
if let Some ( price ) = context . get_current_price ( symbol ) {
let max_quantity = ( context . get_portfolio_value () * self . max_position_size) / price ;
let position_increment = ( context . get_portfolio_value () * self . position_size) / price ;
if current_quantity < max_quantity {
let quantity = ( max_quantity - current_quantity ) . min ( position_increment );
if quantity > Decimal :: new ( 1 , 4 ) && available_cash > quantity * price {
let order = Order :: market_order (
symbol . clone (),
Side :: Buy ,
quantity ,
self . config . strategy_id . clone ()
);
actions . push ( StrategyAction :: PlaceOrder ( order ));
}
}
}
} else if z_score > self . entry_threshold {
// Price is significantly above mean - sell opportunity
// Similar logic for short positions
}
Exit logic
From strategy.rs:851-884:
if current_quantity > Decimal :: ZERO && z_score > - self . exit_threshold {
// Long position, price moving back up toward mean
let exit_quantity = current_quantity . min (
( context . get_portfolio_value () * self . position_size) /
context . get_current_price ( symbol ) . unwrap_or ( Decimal :: ONE )
);
if exit_quantity > Decimal :: new ( 1 , 4 ) {
let order = Order :: market_order (
symbol . clone (),
Side :: Sell ,
exit_quantity ,
self . config . strategy_id . clone ()
);
actions . push ( StrategyAction :: PlaceOrder ( order ));
}
}
Parameters
lookback_period
usize
default: "20"
required
Number of bars for calculating mean and standard deviation
Z-score threshold for entry signals (standard deviations from mean)
Z-score threshold for exit signals (standard deviations from mean)
Percentage of portfolio per trade increment (0.0 to 1.0)
Maximum total position size as percentage of portfolio (0.0 to 1.0)
Symbols to trade with this strategy
Usage
Basic setup
use gb_types :: strategy :: { MeanReversionStrategy , StrategyConfig , Strategy };
use gb_types :: market :: Symbol ;
// Create strategy: 15-day lookback, 2.0σ entry, 1.0σ exit
let mut strategy = MeanReversionStrategy :: new ( 15 , 2.0 , 1.0 );
// Configure
let mut config = StrategyConfig :: new (
"mean_reversion" . to_string (),
"Mean Reversion 15/2.0/1.0" . to_string ()
);
config . add_symbol ( Symbol :: equity ( "AAPL" ));
config . set_parameter ( "position_size" , 0.25 f64 ); // 25% increments
config . set_parameter ( "max_position_size" , 0.75 f64 ); // Max 75% total
// Initialize
strategy . initialize ( & config ) ? ;
From example code
From crates/gb-types/examples/basic_usage.rs:157-168:
let mut mean_rev_strategy = MeanReversionStrategy :: new ( 15 , 2.0 , 1.0 );
let mut mean_rev_config = StrategyConfig :: new (
"mean_reversion" . to_string (),
"Mean Reversion" . to_string ()
);
mean_rev_config . add_symbol ( symbol . clone ());
mean_rev_config . set_parameter ( "position_size" , 0.25 f64 );
mean_rev_config . set_parameter ( "max_position_size" , 0.75 f64 );
if let Ok (()) = mean_rev_strategy . initialize ( & mean_rev_config ) {
println! ( " • Initialized: 15-day mean reversion with 2.0σ entry threshold" );
println! ( " • Position size: 25% increments, max 75%" );
}
Testing
From strategy.rs:1178-1209:
#[test]
fn test_mean_reversion_strategy () {
let mut strategy = MeanReversionStrategy :: new ( 10 , 2.0 , 1.0 );
let mut config = StrategyConfig :: new (
"test_mean_rev" . to_string (),
"Test Mean Reversion" . to_string ()
);
config . add_symbol ( create_test_symbol ());
assert! ( strategy . initialize ( & config ) . is_ok ());
// Create test data with mean-reverting pattern
let mut bars = Vec :: new ();
let base_time = Utc :: now ();
let base_price = dec! ( 100 );
// Stable prices around 100
for i in 0 .. 10 {
let price = base_price + Decimal :: from ( i % 3 - 1 ); // 99, 100, 101 pattern
let bar = create_test_bar ( price , base_time + chrono :: Duration :: days ( i ));
bars . push ( bar );
}
let context = create_test_context_with_data ( bars );
// Test with price significantly below mean (should trigger buy)
let low_bar = create_test_bar ( dec! ( 90 ), base_time + chrono :: Duration :: days ( 11 ));
let event = MarketEvent :: Bar ( low_bar );
let actions = strategy . on_market_event ( & event , & context );
assert! ( actions . is_ok ());
// Should generate buy signal
}
Z-score calculation test
From strategy.rs:1258-1269:
#[test]
fn test_z_score_calculation () {
let strategy = MeanReversionStrategy :: new ( 4 , 2.0 , 1.0 );
let prices = vec! [ dec! ( 98 ), dec! ( 100 ), dec! ( 102 ), dec! ( 100 ), dec! ( 110 )];
let z_score = strategy . calculate_z_score ( & prices );
assert! ( z_score . is_some ());
let result = z_score . unwrap ();
// With mean around 100 and std dev around 4.47, z-score should be positive
assert! ( result > dec! ( 1 )); // 110 is above the mean
}
Common configurations
Aggressive mean reversion
let strategy = MeanReversionStrategy :: new ( 10 , 1.5 , 0.5 );
config . set_parameter ( "position_size" , 0.33 f64 ); // Larger increments
// Trades more frequently on smaller deviations
Conservative mean reversion
let strategy = MeanReversionStrategy :: new ( 30 , 2.5 , 1.5 );
config . set_parameter ( "position_size" , 0.20 f64 ); // Smaller increments
config . set_parameter ( "max_position_size" , 0.60 f64 ); // Lower max exposure
// Only trades extreme deviations, scales in gradually
High-frequency mean reversion
let strategy = MeanReversionStrategy :: new ( 5 , 2.0 , 0.5 );
config . set_parameter ( "position_size" , 0.25 f64 );
// Fast-moving window, quick exits
Behavior details
Incremental position building
The strategy scales into positions rather than going all-in:
Each trade adds position_size percentage of portfolio value
Maximum total position capped at max_position_size
Allows pyramiding as price moves further from mean
Reduces risk of catching a falling knife
Asymmetric entry/exit
Entry threshold (e.g., 2.0σ) is wider than exit threshold (e.g., 1.0σ)
Ensures trades only taken on significant deviations
Exits taken as price moves partway back to mean
Locks in profits before full mean reversion
Use cases
Pairs trading - Trade spread between correlated assets
Statistical arbitrage - Exploit temporary mispricings
Range-bound markets - Profit from oscillating prices
Volatility trading - Trade around stable price levels
Limitations
Mean reversion strategies assume prices will return to historical averages. This assumption fails during regime changes or structural shifts.
Regime changes - New price levels invalidate historical mean
Trending markets - Continuous losses fading strong trends
Minimum trade size - 0.0001 threshold may miss small adjustments
Requires stationarity - Works best on mean-reverting assets
Capital intensive - Scaling into positions requires available capital
Best in - Range-bound, mean-reverting markets
Worst in - Strongly trending markets or during regime shifts
Win rate - Typically high (60-70%) but with occasional large losses
Profit factor - Many small wins offset by infrequent large losses
Drawdowns - Can be severe during persistent trends
Advanced usage
Dynamic lookback periods
Adjust lookback based on market volatility:
let volatility = calculate_volatility ( & prices );
let adaptive_lookback = base_lookback * volatility_factor ;
config . set_parameter ( "lookback_period" , adaptive_lookback );
Multi-leg mean reversion
Combine with correlation analysis for pairs trading:
config . add_symbol ( Symbol :: equity ( "AAPL" ));
config . add_symbol ( Symbol :: equity ( "MSFT" ));
// Calculate spread and apply mean reversion to spread
Momentum Opposite philosophy - follows trends instead of fading them
RSI Similar concept using RSI oscillator instead of z-scores