← Back to Home
Testing an Adaptive Trading Strategy Idea with the Trend Intensity Index (TII) in Python and Backtrader

Testing an Adaptive Trading Strategy Idea with the Trend Intensity Index (TII) in Python and Backtrader

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.

1. The Custom Indicator: Trend Intensity Index (TII)

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-up

This 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.


2. The TII Strategy: Combining Indicators for Robust Signals

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:

2.2. Indicator Stack and Trend Classification: Beyond the TII, the strategy initializes several other indicators:

2.3. Confirmation Functions: The strategy uses helper functions to check various confirmation signals before placing trades:

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:

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.

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:

  1. Update Trailing Stop: First, it calls self.update_trailing_stop() to adjust any active trailing stops.
  2. Order Check: It skips if a pending order exists to avoid placing multiple orders.
  3. Data Warm-up Check: Ensures enough historical data is available for all indicators to be calculated.
  4. TII Signal Retrieval: Calls self.get_tii_signal() to determine the primary TII-based signal.
  5. Entry Logic:
    • Based on the tii_signal (strong/moderate bullish/bearish, exhaustion), it checks volume, RSI, and long-term trend filters for confirmation.
    • If all conditions are met and no position is currently open, it places a buy() or sell() order.
  6. Additional Exit Conditions (TII Weakness/Strength):
    • Beyond the trailing stop, the strategy also exits positions if the 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.

3. Backtesting Setup and Execution with Backtrader

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.

4. Performance Metrics and Visualization

After execution, Backtrader’s analyzers provide comprehensive insights into the strategy’s performance:

# 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.

Pasted image 20250610162547.png

Conclusion

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.