This tutorial uses HullMaSlopeRiderStrategy from
the Mega
Backtrader Strategy Pack. The package includes the complete strategy
source and the backtest runner used to produce these
results.
The Hull MA Slope Rider strategy returned 29.33% on BNB-USD while BNB buy-and-hold lost 27.23%. Its Sharpe ratio was 1.62 and maximum drawdown was 14.39%.
Each backtest started with $10,000 and used 1-hour bars from August 14, 2025 at 15:00 UTC through August 14, 2026 at 15:00 UTC, 0.10% commission per execution, 95% position sizing, and long-only execution. Every run ended with zero open trades.
The strategy uses the faster response of a Hull Moving Average to identify slope direction, confirms the broader trend with a 50-hour SMA, and protects the position with a three-ATR trailing stop.
Install the minimal dependencies:
pip install backtrader yfinance pandas matplotlib numpyThen fetch and normalize the hourly OHLCV frame:
from datetime import datetime, timezone
import backtrader as bt
import pandas as pd
import yfinance as yf
symbol = "BNB-USD"
data = yf.download(
symbol,
start=datetime(2025, 8, 14, 15, tzinfo=timezone.utc),
end=datetime(2026, 8, 14, 16, tzinfo=timezone.utc),
interval="1h",
auto_adjust=False,
progress=False,
)
if isinstance(data.columns, pd.MultiIndex):
data.columns = data.columns.get_level_values(0)
data = data[["Open", "High", "Low", "Close", "Volume"]].dropna()Hull MA combines weighted averages to reduce lag while retaining smoothing.
def hull_ma(data, period):
half = max(2, period // 2)
root = max(2, int(round(np.sqrt(period))))
fast_wma = bt.indicators.WMA(data, period=half)
slow_wma = bt.indicators.WMA(data, period=period)
raw = fast_wma * 2 - slow_wma
return bt.indicators.WMA(raw, period=root)The Hull MA supplies the fast signal, the SMA supplies context, and ATR determines the trailing distance.
def __init__(self):
self.hull = hull_ma(self.data.close, self.p.hull_period)
self.sma = bt.indicators.SMA(
self.data.close, period=self.p.sma_filter
)
self.atr = bt.indicators.ATR(self.data, period=self.p.atr_period)
self.order = None
self.trail = NoneA three-bar difference turns the smoothed average into a directional signal.
def _slope(self):
if len(self.hull) <= self.p.slope_lookback:
return 0.0
return self.hull[0] - self.hull[-self.p.slope_lookback]The long entry needs both a rising Hull MA and price above the slower SMA.
slope = self._slope()
long_signal = slope > 0 and self.data.close[0] > self.sma[0]
if not self.position and long_signal:
self.order = self.buy()The trade can exit through the stop or when the Hull MA slope turns negative.
trail_amount = self.atr[0] * self.p.stop_atr_mult
self.trail = self.sell(
exectype=bt.Order.StopTrail,
trailamount=trail_amount,
)
if self.position.size > 0 and self._slope() < 0:
self.cancel(self.trail)
self.order = self.close()Replace StrategyClass with the class built above. This
is the minimal engine configuration: $10,000 starting cash, 0.10%
commission, and 95% position sizing.
cerebro = bt.Cerebro()
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001) # 0.10% per execution
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.adddata(bt.feeds.PandasData(dataname=data))
cerebro.addstrategy(StrategyClass)
result = cerebro.run()[0]
print(f"Final value: ${cerebro.broker.getvalue():,.2f}")The following script includes the complete strategy, data download,
long-only execution rule, analyzers, and printed results. Save it as a
.py file and run it directly.
import math
from datetime import datetime, timezone
import backtrader as bt
import backtrader.indicators as btind
import numpy as np
import pandas as pd
import yfinance as yf
# Complete strategy implementation
def hull_ma(data, period):
"""WMA(2*WMA(n/2) - WMA(n)), sqrt(n))."""
half = max(2, period // 2)
sqrtn = max(2, int(round(np.sqrt(period))))
wma_half = bt.indicators.WeightedMovingAverage(data, period=half)
wma_full = bt.indicators.WeightedMovingAverage(data, period=period)
raw = wma_half * 2.0 - wma_full
return bt.indicators.WeightedMovingAverage(raw, period=sqrtn)
class HullMaSlopeRiderStrategy(bt.Strategy):
"""Ride the slope of HullMA filtered by a slow SMA.
Entry (long): HullMA slope > 0 AND close > sma_filter.
Entry (short): HullMA slope < 0 AND close < sma_filter.
Exit: opposite slope.
Risk: Chandelier-style ATR trail (3 * ATR).
"""
params = (
('hull_period', 20),
('sma_filter', 50),
('slope_lookback', 3),
('atr_period', 14),
('stop_atr_mult', 3.0),
('printlog', False),
)
def __init__(self):
d = self.datas[0]
self.hull = hull_ma(d.close, self.p.hull_period)
self.sma = bt.indicators.SMA(d.close, period=self.p.sma_filter)
self.atr = bt.indicators.ATR(d, period=self.p.atr_period)
self.order = None
self.trail = None
def _slope(self):
if len(self.hull) <= self.p.slope_lookback:
return 0.0
return self.hull[0] - self.hull[-self.p.slope_lookback]
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
if order.status == order.Completed:
self._place_trail(self.position.size > 0)
if order is self.order:
self.order = None
elif order is self.trail:
self.trail = None
def _place_trail(self, is_long):
self._cancel_trail()
amt = self.atr[0] * self.p.stop_atr_mult
side = self.sell if is_long else self.buy
self.trail = side(exectype=bt.Order.StopTrail, trailamount=amt)
def _cancel_trail(self):
if self.trail:
self.cancel(self.trail)
self.trail = None
def next(self):
if self.order:
return
slope = self._slope()
up_trend = self.data.close[0] > self.sma[0]
dn_trend = self.data.close[0] < self.sma[0]
if not self.position:
if slope > 0 and up_trend:
self.order = self.buy()
elif slope < 0 and dn_trend:
self.order = self.sell()
return
if self.position.size > 0 and slope < 0:
self._cancel_trail(); self.order = self.close()
elif self.position.size < 0 and slope > 0:
self._cancel_trail(); self.order = self.close()
# Keep this tutorial backtest long-only.
original_sell = bt.Strategy.sell
def long_only_sell(self, *args, **kwargs):
if self.position.size <= 0:
return None
size = kwargs.get("size")
if size is None or size > self.position.size:
kwargs["size"] = self.position.size
return original_sell(self, *args, **kwargs)
bt.Strategy.sell = long_only_sell
# Download the exact hourly test window.
symbol = "BNB-USD"
data = yf.download(
symbol,
start=datetime(2025, 8, 14, 15, tzinfo=timezone.utc),
end=datetime(2026, 8, 14, 16, tzinfo=timezone.utc),
interval="1h",
auto_adjust=False,
progress=False,
)
if isinstance(data.columns, pd.MultiIndex):
data.columns = data.columns.get_level_values(0)
data = data[["Open", "High", "Low", "Close", "Volume"]].dropna()
if getattr(data.index, "tz", None) is not None:
data.index = data.index.tz_localize(None)
# Configure and run Backtrader.
cerebro = bt.Cerebro()
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.adddata(bt.feeds.PandasData(dataname=data))
cerebro.addstrategy(HullMaSlopeRiderStrategy)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
result = cerebro.run()[0]
final_value = cerebro.broker.getvalue()
total_return = (final_value / 10_000 - 1) * 100
buy_hold_prices = data.loc[data.index.hour == 0, "Close"]
buy_hold_return = (buy_hold_prices.iloc[-1] / buy_hold_prices.iloc[0] - 1) * 100
excess_return = total_return - buy_hold_return
drawdown = result.analyzers.drawdown.get_analysis()
trades = result.analyzers.trades.get_analysis()
print(f"Final value: ${final_value:,.2f}")
print(f"Strategy return: {total_return:.2f}%")
print(f"Buy-and-hold return: {buy_hold_return:.2f}%")
print(f"Excess return: {excess_return:.2f} percentage points")
print(f"Maximum drawdown: {drawdown['max']['drawdown']:.2f}%")
print(f"Closed trades: {trades.get('total', {}).get('closed', 0)}")If you have the Mega Backtrader Strategy Pack, this command runs the full reporting pipeline for BNB-USD and automatically creates the CSV metrics and charts used below:
python run_backtest.py --symbol BNB-USD --period 1y --interval 1h --benchmark BNB-USD --strategy-filter HullMaSlopeRiderStrategy.py --out hourly_crypto_results--allow-short.HullMaSlopeRiderStrategy comes from the Mega
Backtrader Strategy Pack. The package includes the complete
HullMaSlopeRiderStrategy.py source file and the backtest
runner used in this tutorial, so you can run the strategy, change its
parameters, and generate the same result files yourself.
This material is for research and education only. Backtests are hypothetical, sensitive to data and assumptions, and do not guarantee future performance.