Rate of Change Strategy
Build a rate-of-change momentum strategy that measures price velocity as percentage change over configurable lookback periods, generating entry signals when momentum exceeds threshold extremes in either bullish or bearish direction.
Strategy — Rate of Change Momentum
1–2. Installation and Imports
import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
!pip install pandas numpy plotlyRequirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2) Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1) Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4) Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
3. Strategy Overview
Rate of Change (ROC) measures the percentage difference between the current close and the close N candles ago:
ROC = (Close[t] − Close[t−N]) / Close[t−N] × 100
Signal logic:
- ROC > +threshold → Buy (+1): price is higher than N periods ago by more than the threshold — upward momentum is present and significant.
- ROC < −threshold → Sell (−1): price is lower than N periods ago by more than the threshold — downward momentum is confirmed.
- |ROC| ≤ threshold → No signal (0): momentum is insufficient; the move may be noise rather than a genuine directional impulse.
Why it works: ROC is the most direct measure of momentum — it simply asks "how much has price moved over N periods?" without any smoothing or normalization. Persistent positive ROC indicates a sustained buying pressure; persistent negative ROC indicates sustained selling. The threshold filters out minor oscillations that do not represent actionable momentum.
Relationship to other momentum indicators:
- MACD uses two EMAs to measure momentum indirectly through convergence/divergence.
- RSI normalizes momentum to a 0–100 scale relative to historical gains and losses.
- ROC is direct, unsmoothed momentum — the raw material that other momentum indicators transform.
4. Data Generation
def generate_data(periods: int) -> pd.DataFrame:
start_date = pd.to_datetime("2024-01-01 00:00:00+00:00")
datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
price_data = []; last_close = 42000
for i in range(periods):
open_price = last_close + np.random.normal(0, last_close * 0.0005)
close_price = open_price + np.random.normal(0, last_close * 0.005)
body_high = max(open_price, close_price)
body_low = min(open_price, close_price)
high_price = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
low_price = min(body_low - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
if high_price < low_price: high_price, low_price = low_price, high_price
price_data.append({"open": max(1,int(open_price)), "high": max(1,int(high_price)),
"low": max(1,int(low_price)), "close": max(1,int(close_price))})
last_close = close_price
df = pd.DataFrame(price_data, index=datetime_index)
df.index.name = "datetime"
df["volume"] = np.random.uniform(100.0, 500.0, periods)
df["datetime"] = df.index.to_series()
return df.reset_index(drop=True)
df = generate_data(500)
display(df.head())| open | high | low | close | volume | datetime | |
|---|---|---|---|---|---|---|
| 0 | 41976 | 42266 | 41950 | 42163 | 368.127168 | 2024-01-01 00:00:00+00:00 |
| 1 | 42184 | 42203 | 41911 | 42002 | 457.427928 | 2024-01-01 00:01:00+00:00 |
| 2 | 42019 | 42048 | 41654 | 41686 | 190.011867 | 2024-01-01 00:02:00+00:00 |
| 3 | 41703 | 41706 | 41277 | 41308 | 378.205699 | 2024-01-01 00:03:00+00:00 |
| 4 | 41297 | 41297 | 41099 | 41202 | 185.551343 | 2024-01-01 00:04:00+00:00 |
5. Strategy Function
def rate_of_change_strategy(
df: pd.DataFrame,
roc_period: int = 10,
threshold: float = 0.1, # In percentage points
) -> pd.DataFrame:
"""
Calculates the Rate of Change (ROC) and generates buy/sell signals.
Args:
df (pd.DataFrame): Input DataFrame with 'close' prices and 'datetime'.
roc_period (int): The number of periods to look back for ROC calculation.
threshold (float): The percentage threshold for generating buy/sell signals.
Returns:
pd.DataFrame: The original DataFrame with 'roc' and 'signal' columns added.
'signal' values are 1 for buy, -1 for sell, and 0 for no signal.
"""
df = df.copy().sort_values("datetime", ignore_index=True)
# Calculate Rate of Change (ROC)
# ROC = ((Current Close - Close N periods ago) / Close N periods ago) * 100
df["roc"] = (df["close"] - df["close"].shift(roc_period)) / df["close"].shift(roc_period) * 100
# Generate trading signals based on ROC and threshold
# +1 for buy (ROC > threshold), -1 for sell (ROC < -threshold), 0 for no signal
df["signal"] = np.where(df["roc"] > threshold, 1,
np.where(df["roc"] < -threshold, -1, 0))
return df
df_signals = rate_of_change_strategy(df, roc_period=10, threshold=0.1)
print("--- Signal Distribution ---")
print(df_signals["signal"].value_counts())
print("\n--- ROC Statistics ---")
print(df_signals["roc"].describe().round(4))--- Signal Distribution --- signal -1 267 1 202 0 31 Name: count, dtype: int64 --- ROC Statistics --- count 490.0000 mean -0.0777 std 1.4963 min -3.8415 25% -1.1336 50% -0.2255 75% 1.0483 max 4.4184 Name: roc, dtype: float64
Explanation:
df["close"].shift(roc_period): Retrieves the close price from N candles ago, providing the reference point for the momentum measurement.threshold: Expressed in percentage points. A threshold of 0.1% means the price must have moved at least 0.1% in either direction over N candles to generate a signal. This prevents signals on negligible moves caused by random noise.- ROC is a pure look-back comparison — no smoothing is applied, making it highly responsive but also potentially noisy on short periods.
6. Visualization
buy_signals = df_signals[df_signals["signal"] == 1]
sell_signals = df_signals[df_signals["signal"] == -1]
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
subplot_titles=["Price + Signals", "Rate of Change (%)"],
row_heights=[0.65, 0.35])
fig.add_trace(go.Candlestick(
x=df_signals["datetime"],
open=df_signals["open"], high=df_signals["high"],
low=df_signals["low"], close=df_signals["close"],
name="Price"), row=1, col=1)
fig.add_trace(go.Scatter(
x=buy_signals["datetime"], y=buy_signals["low"] * 0.999,
mode="markers", marker=dict(symbol="triangle-up", size=10, color="green"), name="Buy (+1)"),
row=1, col=1)
fig.add_trace(go.Scatter(
x=sell_signals["datetime"], y=sell_signals["high"] * 1.001,
mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"), name="Sell (−1)"),
row=1, col=1)
fig.add_trace(go.Scatter(
x=df_signals["datetime"], y=df_signals["roc"],
mode="lines", name="ROC (%)", line=dict(color="purple", width=1)), row=2, col=1)
fig.add_hline(y= 0.1, line_dash="dash", line_color="green", row=2, col=1, annotation_text="+threshold")
fig.add_hline(y=-0.1, line_dash="dash", line_color="red", row=2, col=1, annotation_text="−threshold")
fig.add_hline(y= 0, line_dash="dot", line_color="gray", row=2, col=1)
fig.update_layout(
title_text="Rate of Change Momentum Strategy",
xaxis_rangeslider_visible=False,
height=700, yaxis=dict(autorange=True),
xaxis2_title="Datetime", yaxis2_title="ROC (%)",
)
fig.show()Conclusion
This notebook successfully implemented and visualized a Rate of Change (ROC) Momentum strategy. The core idea is to identify significant price momentum by comparing the current closing price to a closing price from N periods ago.
Key Takeaways:
- Direct Momentum Measurement: The ROC indicator provides a straightforward and direct measure of how much an asset's price has changed over a specified period. Unlike other oscillators, it is not smoothed or normalized, making it highly responsive to price movements.
- Threshold-Based Signaling: By applying positive and negative thresholds, the strategy filters out minor price fluctuations, generating clear buy (+1) and sell (-1) signals only when momentum is significant enough to warrant action. This helps in distinguishing genuine directional moves from market noise.
- Visualization Clarity: The interactive plots effectively demonstrate how ROC values correspond to price action and signal generation. Buy signals typically appear when ROC crosses above the positive threshold, coinciding with upward price movement, while sell signals occur when ROC dips below the negative threshold during downward trends.
Strengths of ROC Momentum:
- Simplicity and Intuitiveness: Easy to understand and implement.
- Responsiveness: Captures momentum shifts quickly due to its direct calculation.
- Flexibility: The
roc_periodandthresholdparameters can be adjusted to suit different market conditions and trading styles.
Limitations and Further Considerations:
- Parameter Sensitivity: The performance of the strategy is highly dependent on the chosen
roc_periodandthreshold. Optimal values may vary significantly across different assets and timeframes. - Synthetic Data: The current analysis is based on generated synthetic data. Real-world market data exhibits more complex behaviors, including gaps, sudden spikes, and varying volatility, which might affect strategy performance.
- Lack of Context: The strategy as implemented does not incorporate other market factors, fundamental analysis, or broader market trends.
- No Risk Management: The current implementation lacks essential trading components such as stop-loss orders, take-profit levels, or position sizing, which are crucial for real-world application.
- No Backtesting: The notebook focuses on signal generation and visualization, but does not include a comprehensive backtest to evaluate the strategy's historical profitability, drawdowns, or other performance metrics.
Next Steps:
To build upon this foundation, future work could include:
- Real-World Data Testing: Apply the strategy to actual historical market data for various assets (e.g., stocks, cryptocurrencies, forex).
- Parameter Optimization: Conduct a systematic study to find optimal
roc_periodandthresholdvalues using backtesting techniques. - Risk Management Integration: Add features like stop-loss, take-profit, and position sizing to simulate realistic trading scenarios.
- Performance Evaluation: Develop a robust backtesting framework to calculate key metrics such as profit/loss, Sharpe ratio, maximum drawdown, and win rate.
- Combination with Other Indicators: Explore combining ROC momentum with other technical indicators (e.g., moving averages, volume, volatility measures) to enhance signal confirmation and reduce false positives.
- Adapting to Volatility: Implement adaptive thresholds or periods that dynamically adjust based on market volatility.