The Relative Rotation Graph (RRG) algorithm, developed by Julius de Kempenaer, visualizes the relative strength and momentum of securities against a benchmark. It plots securities on a 2D graph where:
- X-axis (RS-Ratio): Relative strength indicator
- Y-axis (RS-Momentum): Rate of change of relative strength
# Simple ratio of security price to benchmark price
RS = (security_price / benchmark_price) * 100
The RS-Ratio uses double smoothing to normalize the relative strength around 100:
# First smoothing: Apply WMA to RS values
RS_smooth = WMA(RS, window=10)
# Second smoothing: Create a benchmark by smoothing again
RS_benchmark = WMA(RS_smooth, window=10)
# Normalize by dividing
RS_Ratio = (RS_smooth / RS_benchmark) * 100
Two methods are commonly used:
Method 1: Percentage Change (Default)
# Compare current RS-Ratio to N periods ago
RS_Momentum = (RS_Ratio / RS_Ratio_N_periods_ago) * 100
# Example: If RS-Ratio was 98 ten periods ago and is 102 now
# RS_Momentum = (102/98) * 100 = 104.08
Method 2: Z-Score Normalization
# Calculate rate of change
ROC = (RS_Ratio - RS_Ratio_previous) / RS_Ratio_previous
# Normalize using z-score
RS_Momentum = 100 + (ROC - mean(ROC)) / std(ROC)
RS-Momentum ↑
|
Improving | Leading
RS-Ratio < 100 | RS-Ratio > 100
Momentum > 100 | Momentum > 100
←-----------------|------------------→ RS-Ratio
100 | 100
Lagging | Weakening
RS-Ratio < 100 | RS-Ratio > 100
Momentum < 100 | Momentum < 100
|
- Leading: Strong and accelerating (best performers)
- Weakening: Strong but decelerating (losing momentum)
- Lagging: Weak and decelerating (worst performers)
- Improving: Weak but accelerating (potential opportunities)
Unlike simple moving averages, WMA gives more weight to recent data:
def calculate_wma(data, window):
"""
WMA with linearly increasing weights
Weights: [1, 2, 3, ..., window]
"""
weights = np.arange(1, window + 1)
wma = []
for i in range(window - 1, len(data)):
window_data = data[i - window + 1:i + 1]
weighted_sum = np.sum(window_data * weights)
wma.append(weighted_sum / weights.sum())
return np.array(wma)
# Example: data=[10, 20, 30], window=3
# Weights=[1, 2, 3]
# WMA = (10*1 + 20*2 + 30*3) / (1+2+3) = 140/6 = 23.33
import numpy as np
import pandas as pd
def calculate_rrg(security_prices, benchmark_prices, window=10, momentum_period=10):
"""
Calculate RRG indicators for a security relative to benchmark
Parameters:
- security_prices: array of security closing prices
- benchmark_prices: array of benchmark closing prices
- window: smoothing window (default 10)
- momentum_period: lookback for momentum calculation (default 10)
Returns:
- DataFrame with RS, RS-Ratio, and RS-Momentum
"""
# Step 1: Calculate Relative Strength
rs = (security_prices / benchmark_prices) * 100
# Step 2: Calculate RS-Ratio (double smoothing)
rs_smooth = calculate_wma(rs, window)
rs_benchmark = calculate_wma(rs_smooth, window)
rs_ratio = (rs_smooth / rs_benchmark) * 100
# Step 3: Calculate RS-Momentum (percentage method)
rs_momentum = []
for i in range(momentum_period, len(rs_ratio)):
current = rs_ratio[i]
previous = rs_ratio[i - momentum_period]
momentum = (current / previous) * 100
rs_momentum.append(momentum)
# Align arrays (due to window reductions)
min_length = len(rs_momentum)
return pd.DataFrame({
'RS': rs[-min_length:],
'RS_Ratio': rs_ratio[-min_length:],
'RS_Momentum': rs_momentum
})
- Uses WMA for better responsiveness
- Double smoothing for RS-Ratio
- Two momentum calculation methods
- Handles missing data and edge cases
# Common simplified approach
rs = prices / benchmark
rs_ratio = sma(rs, 10) / sma(sma(rs, 10), 10) * 100
rs_momentum = (rs_ratio - rs_ratio.shift(10)) / rs_ratio.shift(10) * 100 + 100
- Uses SMA instead of WMA (less responsive)
- Basic momentum calculation
- Minimal error handling
- Proprietary smoothing algorithms (often enhanced WMA or custom)
- May use different default periods (some use 14 or 20)
- Additional filters to reduce whipsaws
- Optimized for their specific user base (institutional vs retail)
The choice of smoothing algorithm significantly impacts RRG signals:
# Example: Same data, different smoothing methods
import numpy as np
# Simulated price movement: sudden 5% jump
prices = np.array([100, 100, 100, 100, 105, 105, 105, 105, 105, 105])
# SMA (Simple Moving Average) - Equal weights
sma_weights = [0.2, 0.2, 0.2, 0.2, 0.2] # All equal
sma_result = 103 # Slow to react
# WMA (Weighted Moving Average) - Linear weights
wma_weights = [0.067, 0.133, 0.200, 0.267, 0.333] # Recent data weighted more
wma_result = 104 # Faster reaction
# EMA (Exponential Moving Average) - Exponential weights
ema_alpha = 0.3 # Recent data gets 30% weight
ema_result = 103.5 # Moderate reaction
Method | Signal Timing | False Signals | Best For |
---|---|---|---|
WMA | Early (1-2 days faster) | More frequent | Active traders |
SMA | Late (1-2 days slower) | Less frequent | Long-term investors |
EMA | Moderate | Balanced | General use |
# Sector rotation signal detection
# Scenario: Tech sector starting to outperform
# Day 10: Tech enters "Improving" quadrant
wma_rs_ratio = 99.5 # Just below 100
wma_momentum = 101 # Above 100 - WMA detects improvement
# Same day with SMA
sma_rs_ratio = 98 # Further below 100
sma_momentum = 99.5 # Still below 100 - SMA misses early signal
# Result: WMA users enter position 2-3 days earlier
Method | Formula | Interpretation |
---|---|---|
Percentage | (Current/Previous) × 100 | 105 = 5% increase |
Rate of Change | ((Current-Previous)/Previous) × 100 | 5 = 5% increase |
Z-Score | Normalized ROC | Statistical measure |
- Shorter windows (5-7): More responsive, more noise
- Standard (10): Balanced for most use cases
- Longer windows (15-20): Smoother, better for trends
# Example: Calculate RRG for a sector ETF vs SPY
sector_data = pd.read_csv('XLK_prices.csv')
spy_data = pd.read_csv('SPY_prices.csv')
rrg_results = calculate_rrg(
sector_data['close'].values,
spy_data['close'].values,
window=10,
momentum_period=10
)
# Current position
current_rs_ratio = rrg_results['RS_Ratio'].iloc[-1]
current_rs_momentum = rrg_results['RS_Momentum'].iloc[-1]
# Determine quadrant
if current_rs_ratio > 100 and current_rs_momentum > 100:
quadrant = "Leading"
elif current_rs_ratio > 100 and current_rs_momentum < 100:
quadrant = "Weakening"
elif current_rs_ratio < 100 and current_rs_momentum < 100:
quadrant = "Lagging"
else:
quadrant = "Improving"
- RRG measures relative performance in two dimensions: strength (RS-Ratio) and momentum
- Double smoothing in RS-Ratio calculation reduces noise while maintaining responsiveness
- WMA vs SMA makes a significant difference in catching trend changes
- Window size is the main tuning parameter - adjust based on your time horizon
- Quadrant transitions are important signals but should be confirmed over multiple periods
The algorithm's elegance lies in its simplicity: it transforms absolute prices into relative measures that oscillate predictably around 100, making cross-sectional comparison intuitive and actionable.