Skip to main content
The RSI (Relative Strength Index) strategy uses a momentum oscillator to identify overbought and oversold conditions. It generates buy signals when the market is oversold and sell signals when overbought.

Strategy logic

The strategy uses the RSI indicator to time entries and exits:
  1. Calculate RSI over lookback period (typically 14 bars)
  2. Buy signal - RSI < oversold threshold (typically 30)
  3. Sell signal - RSI > overbought threshold (typically 70)
  4. Position sizing based on configurable percentage of portfolio value

Implementation

Source: crates/gb-types/src/strategy.rs:714-1059
pub struct RsiStrategy {
    config: StrategyConfig,
    initialized: bool,
    lookback_period: usize,
    oversold_threshold: Decimal,
    overbought_threshold: Decimal,
    position_size: Decimal,
}

RSI calculation

From strategy.rs:935-971:
fn calculate_rsi(&self, prices: &[Decimal]) -> Option<Decimal> {
    if prices.len() < self.lookback_period + 1 {
        return None;
    }

    let mut gains = Decimal::ZERO;
    let mut losses = Decimal::ZERO;

    let recent_prices = prices.iter()
        .rev()
        .take(self.lookback_period + 1)
        .cloned()
        .collect::<Vec<_>>();
    let mut recent_prices = recent_prices.into_iter().rev();
    let mut previous = recent_prices.next()?;

    for price in recent_prices {
        let change = price - previous;
        if change > Decimal::ZERO {
            gains += change;
        } else if change < Decimal::ZERO {
            losses += change.abs();
        }
        previous = price;
    }

    let period = Decimal::from(self.lookback_period);
    let avg_gain = gains / period;
    let avg_loss = losses / period;

    if avg_loss == Decimal::ZERO {
        return Some(Decimal::from(100));
    }
    if avg_gain == Decimal::ZERO {
        return Some(Decimal::ZERO);
    }

    let rs = avg_gain / avg_loss;
    let rsi = Decimal::from(100) - (Decimal::from(100) / (Decimal::ONE + rs));
    Some(rsi)
}

Trading logic

From strategy.rs:989-1037:
fn on_market_event(
    &mut self,
    event: &MarketEvent,
    context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
    if !self.initialized {
        return Ok(vec![]);
    }

    let symbol = event.symbol();
    if let Some(buffer) = context.get_market_data(symbol) {
        let bars = buffer.get_bars(self.lookback_period + 1);
        let prices: Vec<Decimal> = bars.iter().map(|bar| bar.close).collect();

        if let Some(rsi) = self.calculate_rsi(&prices) {
            let mut actions = Vec::new();
            let current_position = context.get_position(symbol);
            let current_quantity = current_position.map(|p| p.quantity).unwrap_or(Decimal::ZERO);

            if rsi < self.oversold_threshold {
                // Oversold - buy signal
                let target_quantity = if let Some(price) = context.get_current_price(symbol) {
                    (context.get_portfolio_value() * self.position_size) / price
                } else {
                    Decimal::ZERO
                };

                let quantity_diff = target_quantity - current_quantity;
                if quantity_diff > Decimal::new(1, 4) {
                    let order = Order::market_order(
                        symbol.clone(),
                        Side::Buy,
                        quantity_diff,
                        self.config.strategy_id.clone()
                    );
                    actions.push(StrategyAction::PlaceOrder(order));
                }
            } else if rsi > self.overbought_threshold {
                // Overbought - sell signal
                if current_quantity > Decimal::ZERO {
                    let order = Order::market_order(
                        symbol.clone(),
                        Side::Sell,
                        current_quantity,
                        self.config.strategy_id.clone()
                    );
                    actions.push(StrategyAction::PlaceOrder(order));
                }
            }

            return Ok(actions);
        }
    }

    Ok(vec![])
}

Parameters

lookback_period
usize
default:"14"
required
Number of bars for RSI calculation (typically 14)
oversold_threshold
f64
default:"30.0"
required
RSI level below which market is considered oversold (0-100)
overbought_threshold
f64
default:"70.0"
required
RSI level above which market is considered overbought (0-100)
position_size
f64
default:"0.95"
Percentage of portfolio value to allocate (0.0 to 1.0)
symbols
Vec<Symbol>
required
Symbols to trade with this strategy

Usage

Basic setup

use gb_types::strategy::{RsiStrategy, StrategyConfig, Strategy};
use gb_types::market::Symbol;

// Create 14-period RSI strategy with 30/70 thresholds
let mut strategy = RsiStrategy::new(14, 30.0, 70.0);

// Configure
let mut config = StrategyConfig::new(
    "rsi".to_string(),
    "RSI 14/30/70".to_string()
);
config.add_symbol(Symbol::equity("AAPL"));
config.set_parameter("position_size", 0.95f64);

// Initialize
strategy.initialize(&config)?;

Testing

RSI calculation test

From strategy.rs:1211-1220:
#[test]
fn test_rsi_calculation() {
    let strategy = RsiStrategy::new(5, 30.0, 70.0);
    let prices = vec![dec!(100), dec!(102), dec!(104), dec!(103), dec!(105), dec!(107)];

    let rsi = strategy.calculate_rsi(&prices);
    assert!(rsi.is_some());
    let value = rsi.unwrap();
    assert!(value > dec!(50)); // Mostly rising prices should produce RSI above 50
}

Buy signal test

From strategy.rs:1222-1242:
#[test]
fn test_rsi_strategy_buy_signal() {
    let mut strategy = RsiStrategy::new(5, 30.0, 70.0);
    let mut config = StrategyConfig::new(
        "test_rsi".to_string(),
        "Test RSI".to_string()
    );
    config.add_symbol(create_test_symbol());

    assert!(strategy.initialize(&config).is_ok());

    let mut bars = Vec::new();
    let base_time = Utc::now();
    let prices = vec![dec!(100), dec!(98), dec!(96), dec!(95), dec!(94), dec!(93)];
    for (i, price) in prices.into_iter().enumerate() {
        let bar = create_test_bar(price, base_time + chrono::Duration::days(i as i64));
        bars.push(bar);
    }

    let context = create_test_context_with_data(bars);
    let actions = strategy.on_market_event(&event, &context);
    assert!(actions.is_ok());
}

Common configurations

Conservative (wide bands)

let strategy = RsiStrategy::new(14, 20.0, 80.0);
// Trades only extreme oversold/overbought conditions

Aggressive (narrow bands)

let strategy = RsiStrategy::new(14, 40.0, 60.0);
// Trades more frequently on smaller deviations from neutral

Short-term RSI

let strategy = RsiStrategy::new(7, 30.0, 70.0);
// Faster-moving, more responsive to recent price action

Long-term RSI

let strategy = RsiStrategy::new(21, 30.0, 70.0);
// Smoother, filters out short-term noise

Behavior details

RSI formula

RSI = 100 - (100 / (1 + RS)) Where:
  • RS (Relative Strength) = Average Gain / Average Loss
  • Average Gain = Sum of gains over period / Period
  • Average Loss = Sum of losses over period / Period

Position management

  • Oversold entry - Buys to reach target position size
  • Overbought exit - Sells entire position
  • No partial exits - All-or-nothing on overbought signals
  • Incremental buys - Only adds to position when below target

Data requirements

Requires lookback_period + 1 bars of price data to calculate RSI.

Use cases

  • Mean reversion - Trade reversals at extremes
  • Trend confirmation - Filter entries in trending markets
  • Divergence trading - Spot when RSI diverges from price (requires custom logic)
  • Oversold bounces - Capture short-term rebounds

Limitations

RSI can remain overbought or oversold for extended periods during strong trends. The indicator may generate premature reversal signals.
  • False signals in trends - RSI can stay extreme during sustained moves
  • No trend filter - Trades against strong trends
  • Lagging indicator - Based on historical price changes
  • Fixed thresholds - Same levels used across all market conditions
  • Binary exits - Sells entire position on overbought, no scaling out

Performance characteristics

  • Best in - Range-bound, oscillating markets
  • Worst in - Strong trending markets with persistent directional moves
  • Win rate - Moderate to high in mean-reverting environments
  • Drawdowns - Can be significant when fading strong trends

Advanced usage

Multiple timeframes

Combine different RSI periods:
let rsi_short = RsiStrategy::new(7, 30.0, 70.0);
let rsi_long = RsiStrategy::new(21, 30.0, 70.0);
// Only trade when both agree

Dynamic thresholds

Adjust thresholds based on volatility:
let volatility = calculate_volatility(&prices);
let oversold = 30.0 - (volatility * 10.0);
let overbought = 70.0 + (volatility * 10.0);
config.set_parameter("oversold_threshold", oversold);
config.set_parameter("overbought_threshold", overbought);

RSI divergence

Detect bullish/bearish divergences (requires custom implementation):
// Bullish divergence: Price makes lower low, RSI makes higher low
// Bearish divergence: Price makes higher high, RSI makes lower high

Mean reversion

Similar concept using z-score analysis instead of RSI

Momentum

Trend-following approach - opposite philosophy

Build docs developers (and LLMs) love