← Back to Home
Vortex Trend Capture - A Filtered Approach with Adaptive Trailing Stops

Vortex Trend Capture - A Filtered Approach with Adaptive Trailing Stops

Trend-following strategies are a cornerstone of systematic trading, aiming to profit from sustained price movements. While simple indicators can identify trends, robust strategies often integrate additional filters to enhance signal quality and manage risk. This article introduces the VortexMAConditionedATRStopStrategy, a professional trend-following system that leverages the Vortex Indicator (VI) for entry signals, combined with a long-term moving average (MA) and a volatility filter, and crucially, employs adaptive trailing stops for dynamic risk management.

Pasted image 20250730013941.png

The Strategy’s Core Components

The strategy is implemented as a backtrader class, VortexMAConditionedATRStopStrategy, incorporating key technical indicators and logic.

import backtrader as bt
import numpy as np # For handling potential NaN values in indicators

class VortexMAConditionedATRStopStrategy(bt.Strategy):
    params = (
        ('vortex_period', 30),
        ('long_term_ma_period', 30),
        ('atr_period', 7),
        ('atr_threshold', 0.05), # Max ATR as % of price to allow trades
        ('atr_stop_multiplier', 3.0),
    )

    def __init__(self):
        self.order = None
        self.trailing_stop_order = None # To track the backtrader-managed trailing stop order

        # --- Indicators ---
        self.vortex = bt.indicators.Vortex(self.data, period=self.p.vortex_period)
        self.long_term_ma = bt.indicators.SimpleMovingAverage(self.data.close, period=self.p.long_term_ma_period)
        self.atr = bt.indicators.AverageTrueRange(self.data, period=self.p.atr_period)

        # Crossover for the Vortex signal (VI+ over VI- indicates bullish)
        self.vortex_cross = bt.indicators.CrossOver(self.vortex.lines.vi_plus, self.vortex.lines.vi_minus)
  1. Vortex Indicator (VI) for Trend Initiation: The strategy utilizes the Vortex Indicator, which comprises two lines, VI+ and VI-. A buy signal is generated when VI+ crosses above VI-, indicating positive trend momentum, while a sell signal occurs when VI- crosses above VI+. This crossover serves as the primary trigger for potential trades.

  2. Long-Term Moving Average (MA) for Macro Trend Confirmation: To ensure trades align with the broader market direction, a long-term Simple Moving Average acts as a trend filter. Long entries are only considered when the price is above this MA, signifying an uptrend. Conversely, short entries are restricted to periods when the price is below the MA, confirming a downtrend. This prevents counter-trend entries and focuses on robust, sustained movements.

  3. Average True Range (ATR) for Volatility Filtering: High volatility can lead to erratic signals and increased risk. The strategy incorporates an ATR-based filter, requiring that the market’s current volatility (ATR as a percentage of its close price) remains below a predefined atr_threshold. This ensures that trades are initiated during periods of relatively stable market conditions, suitable for trend capture.

  4. Adaptive Trailing Stops for Dynamic Risk Management: Adhering to the principle of always using trailing stops, this strategy integrates an ATR-based trailing stop for every position. Upon trade entry, a trailing stop order is immediately placed, with its distance from the market price calculated as a multiple of the current ATR. This mechanism allows profits to run during strong trends while dynamically adjusting the stop-loss level to protect gains as the market moves.

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return
    
        if order.status in [order.Completed]:
            if order.isbuy(): # If a buy order completed (entered long position)
                if self.position.size > 0: # Ensure we are actually long
                    self.trailing_stop_order = self.sell(
                        exectype=bt.Order.StopTrail,
                        trailamount=self.atr[0] * self.p.atr_stop_multiplier # ATR-based trailing amount
                    )
            elif order.issell(): # If a sell order completed (entered short position OR exited long)
                if self.position.size < 0: # If entered short position
                    self.trailing_stop_order = self.buy(
                        exectype=bt.Order.StopTrail,
                        trailamount=self.atr[0] * self.p.atr_stop_multiplier
                    )
                else: # If a sell order completed but we are no longer in position (implies exit)
                    if self.trailing_stop_order:
                        self.cancel(self.trailing_stop_order) # Cancel any outstanding trailing stop
                    self.trailing_stop_order = None
    
        # Handle order cancellation/rejection, reset order tracker
        if order.status in [order.Canceled, order.Rejected, order.Margin]:
            self.order = None
            if order == self.trailing_stop_order:
                self.trailing_stop_order = None
    
        if order.status in [order.Completed, order.Canceled, order.Rejected, order.Margin]:
            self.order = None

Execution Logic

A trade is initiated only when all three conditions converge, as checked in the next method:

    def next(self):
        if self.order: return # Do not act if an order is already pending

        # Ensure enough data for indicators to be valid
        if len(self.data) < max(self.p.vortex_period, self.p.long_term_ma_period, self.p.atr_period):
            return

        # Check for NaN values in indicators before using them
        if np.isnan(self.atr[0]) or np.isnan(self.long_term_ma[0]) or np.isnan(self.vortex_cross[0]):
            return

        # --- Filter Conditions ---
        # 1. Volatility Filter: Is market volatility stable (ATR relative to price is low)?
        current_atr_pct = (self.atr[0] / self.data.close[0]) if self.data.close[0] != 0 else float('inf')
        is_stable_volatility = current_atr_pct < self.p.atr_threshold

        # 2. Macro Trend Filter: Is price aligned with the long-term trend?
        is_macro_uptrend = self.data.close[0] > self.long_term_ma[0]
        is_macro_downtrend = self.data.close[0] < self.long_term_ma[0]

        # 3. Vortex Crossover Signal
        is_buy_signal = self.vortex_cross[0] > 0
        is_sell_signal = self.vortex_cross[0] < 0

        # --- Entry Logic ---
        if not self.position: # Only enter if no position is open
            if is_stable_volatility and is_macro_uptrend and is_buy_signal:
                self.order = self.buy()
            elif is_stable_volatility and is_macro_downtrend and is_sell_signal:
                self.order = self.sell()

Once a position is opened, the adaptive trailing stop takes over, ensuring that the trade is exited automatically when the market reverses by a predefined ATR-based amount, securing profits and limiting potential losses. This strategy specifically relies on backtrader’s built-in StopTrail orders for exits, maintaining the commitment to consistently use trailing stops for risk management.

Conclusion

The VortexMAConditionedATRStopStrategy offers a disciplined and robust approach to trend following. By combining the directional insights of the Vortex Indicator with macro trend and volatility filters, it aims to capture significant market movements while mitigating risk through adaptive trailing stops. This multi-layered approach makes it a compelling candidate for systematic trading applications.