Developing a successful algorithmic trading strategy requires a blend
of well-defined rules, robust indicators, and effective risk management.
This article delves into a Backtrader-based strategy that
attempts to capture directional moves while also identifying potential
trend exhaustion for contrarian trades. The core of this system is a
custom Trend Intensity Index (TII), complemented by a
suite of popular technical analysis tools and a dynamic trailing
stop.
The Trend Intensity Index (TII) is a custom oscillator designed to quantify the strength of a prevailing trend. Unlike indicators that focus on momentum or direction, TII specifically measures how consistently the price has been moving above or below a short-term moving average within a defined lookback period.
Concept & Calculation:
The TII is calculated as the percentage of periods, within a
period lookback window, where the Close price
is greater than a ma_period-period Simple Moving Average
(SMA) of the Close price.
\[\text{TII} = \frac{\text{Count of periods where Close > SMA(Close, ma\_period)}}{\text{period}} \times 100\]
Interpretation:
Backtrader Implementation (TrendIntensityIndex
class):
class TrendIntensityIndex(bt.Indicator):
lines = ('tii',)
params = (
('period', 30), # Lookback period for TII calculation
('ma_period', 7), # Moving average period for comparison
)
def __init__(self):
self.ma = bt.indicators.SMA(self.data, period=self.params.ma_period)
# Manual history tracking as Backtrader indicators don't directly expose past line values for arbitrary calculations easily
self.price_history = []
self.ma_history = []
def next(self):
# Store current values and maintain history window
self.price_history.append(self.data.close[0])
self.ma_history.append(self.ma[0])
if len(self.price_history) > self.params.period:
self.price_history = self.price_history[-self.params.period:]
self.ma_history = self.ma_history[-self.params.period:]
# Calculate TII once enough history is available
if len(self.price_history) >= self.params.period:
count_above = sum(1 for i in range(self.params.period)
if self.price_history[i] > self.ma_history[i])
self.lines.tii[0] = (count_above / self.params.period) * 100
else:
self.lines.tii[0] = 50 # Default to neutral during warm-upThis custom indicator collects price and MA history, then calculates the TII based on the proportion of periods where the closing price was above the MA.
The TIIStrategy class in Backtrader orchestrates the
entire trading logic. It combines the custom TII indicator with several
standard technical analysis tools to create a multi-layered
decision-making process.
2.1. Strategy Parameters: The strategy is highly
configurable through its params, allowing for easy
optimization and experimentation:
tii_period, ma_period: For the TII
calculation.tii_strong_up, tii_moderate_up,
tii_moderate_down, tii_strong_down: Thresholds
for TII-based trend classification.volume_multiplier, volume_period: For
volume confirmation.rsi_period: For RSI momentum confirmation.atr_period: For ATR calculation used in stop loss.trailing_stop_pct: Percentage for the trailing
stop.trend_filter_period: Period for a long-term SMA trend
filter.2.2. Indicator Stack and Trend Classification: Beyond the TII, the strategy initializes several other indicators:
bt.indicators.RSI: Relative Strength Index for
momentum.bt.indicators.ATR: Average True Range for
volatility.bt.indicators.SMA(volume): Simple Moving Average of
volume for confirmation.bt.indicators.SMA(close): A long-term SMA
(trend_filter) for overall trend alignment.tii_momentum: Calculated as the
current TII minus the TII from 5 periods ago. This measures the rate of
change of trend intensity.tii_smooth: A 3-period SMA of the TII
to reduce noise.trend_strength: A custom
classification (0-4) based on tii_smooth and predefined
thresholds, categorizing the market into strong downtrend (0), moderate
downtrend (1), neutral (2), moderate uptrend (3), and strong uptrend
(4). This is built using bt.indicators.If statements.2.3. Confirmation Functions: The strategy uses helper functions to check various confirmation signals before placing trades:
check_volume_confirmation(): Ensures current volume is
volume_multiplier times higher than its SMA, suggesting
conviction behind the move.check_rsi_confirmation(signal_type): Filters out trades
if RSI indicates an overbought (for long signals) or oversold (for short
signals) condition, preventing entries at potential exhaustion
points.check_trend_filter(signal_type): Confirms that the
current price aligns with the long-term trend (above SMA for longs,
below for shorts).2.4. TII Signal Generation
(get_tii_signal): This critical function analyzes
the tii_smooth value and tii_momentum to
generate various types of signals:
tii_smooth crosses into the strong trend zone with
positive/negative tii_momentum.tii_smooth crosses into the moderate trend zone with
stronger tii_momentum.tii_smooth reaches extreme levels (e.g.,
> 90 for bullish exhaustion) but tii_momentum is moving
in the opposite direction, suggesting a potential reversal.2.5. Dynamic Trailing Stop Loss: This is a key risk management component. Instead of fixed stops, it uses a percentage-based trailing stop that adjusts with price movement.
notify_order: This method is called by Backtrader when
an order’s status changes. Upon a successful buy/sell order completion
(meaning a position is opened), it sets the initial
trailing_stop_price at a fixed percentage
(trailing_stop_pct) away from the entry price and
immediately places a bt.Order.Stop order.update_trailing_stop: This method is called at every
bar.
highest_price_since_entry. If the current price makes a new
high, it recalculates
new_stop_price = highest_price_since_entry * (1 - self.params.trailing_stop_pct).
If this new_stop_price is higher than the current
trailing_stop_price, the old stop order is cancelled, and a
new one is placed at the higher price.lowest_price_since_entry
and moves the stop lower if the price makes a new low, protecting
profits.This ensures that once a trade moves into profit, the stop-loss order follows the price, locking in gains while still allowing the trade to run.
2.6. The next Method - The Trading
Engine: The next method is the heart of the
strategy, executed on every new bar of data:
self.update_trailing_stop() to adjust any active trailing
stops.self.get_tii_signal() to determine the primary TII-based
signal.tii_signal (strong/moderate
bullish/bearish, exhaustion), it checks volume, RSI, and long-term trend
filters for confirmation.buy() or sell() order.trend_strength (from smoothed TII classification)
significantly weakens against the current position’s direction,
especially if combined with adverse tii_momentum. This acts
as a dynamic profit-taking or risk-reduction mechanism.The strategy is put to the test using the Backtrader
framework, a powerful tool for backtesting trading strategies.
# Download data (e.g., BTC-USD from 2021 to 2024)
data = yf.download('BTC-USD', '2021-01-01', '2024-01-01')
data.columns = data.columns.droplevel(1) # Clean column names
# Create a Backtrader data feed
data_feed = bt.feeds.PandasData(dataname=data)
cerebro = bt.Cerebro() # Initialize Cerebro engine
cerebro.addstrategy(TIIStrategy) # Add the strategy
cerebro.adddata(data_feed) # Add the data feed
cerebro.addsizer(bt.sizers.PercentSizer, percents=95) # Allocate 95% of cash to trades
cerebro.broker.setcash(100000) # Starting capital
cerebro.broker.setcommission(commission=0.001) # Set commission (0.1%)
# Add analyzers for detailed performance reporting
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
# Run the backtest
results = cerebro.run()This setup efficiently simulates trading over the specified historical period, applying commissions and sizing rules.
After execution, Backtrader’s analyzers provide comprehensive insights into the strategy’s performance:
stop method output):
Provides average and final TII values during the backtest, giving
context to market regimes encountered.# Print analyzer results
strat = results[0]
print('\nPerformance Metrics:')
print('Sharpe Ratio:', strat.analyzers.sharpe.get_analysis())
# ... (other print statements) ...
# Plotting the results
cerebro.plot(iplot=False, style='candlestick', volume=False)
plt.show()Finally, cerebro.plot() generates interactive charts
that visualize the price action, indicators, and every trade executed by
the strategy, providing invaluable visual confirmation and debugging
capabilities.
This Backtrader strategy offers a robust framework for
an adaptive trend-following and reversal system. By meticulously
defining a custom Trend Intensity Index, integrating it with multiple
confirmation indicators (RSI, Volume, long-term MA), and employing a
dynamic percentage-based trailing stop, the strategy attempts to capture
market trends efficiently while managing risk proactively. The
comprehensive backtesting setup and detailed performance analyzers
provide the tools necessary to rigorously evaluate and refine its
potential.
This strategy serves as an excellent foundation for further exploration, including parameter optimization, integration of more advanced risk management techniques, and testing across diverse asset classes and market conditions.