← Back to Home
Fractal Adaptive Moving Average Strategy Backtesting using Python and Backtrader

Fractal Adaptive Moving Average Strategy Backtesting using Python and Backtrader

In the intricate world of algorithmic trading, the journey from a promising idea to a deployable, profitable strategy is a multi-stage process of rigorous validation and iterative refinement. The quantitative trading model presented here, centered on the Fractal Adaptive Moving Average (FRAMA), exemplifies the initial research and development phase—a critical step in evaluating a hypothesis rather than deploying a finalized solution. This article delves into the design and preliminary testing of this FRAMA-based strategy, highlighting its adaptive core and the rationale behind its comprehensive parameterization.

The Adaptive Heart: Understanding FRAMA

Traditional moving averages suffer from a fundamental trade-off: fast averages are responsive but prone to whipsaws, while slow averages are smoother but lag significantly. The Fractal Adaptive Moving Average (FRAMA), conceived by John Ehlers, seeks to overcome this by dynamically adjusting its smoothing period based on the inherent “fractal dimension” of price data.

At its essence, the fractal dimension quantifies how much a pattern fills space as scale changes. In financial markets, a lower fractal dimension (closer to 1) indicates a trending, “smoother” market, while a higher dimension (closer to 2) signifies a choppy, “rougher” market. FRAMA leverages this insight, adapting its smoothing factor (alpha) to market conditions.

Our implementation of the FRAMA indicator within the Backtrader framework demonstrates this adaptive mechanism:

class FRAMAIndicator(bt.Indicator):
    lines = ('frama', 'fractal_dim', 'alpha',)
    params = (
        ('period', 20),         # Lookback for fractal calculation
        ('slow_period', 200),   # Slow EMA period (choppy)
        ('fast_period', 4),     # Fast EMA period (trending)
    )

    def fractal_to_alpha(self, fractal_dim):
        """Converts fractal dimension to an adaptive smoothing factor (alpha)."""
        # Alpha smoothly transitions between fast_alpha (trending) and slow_alpha (choppy)
        slow_alpha = 2.0 / (self.params.slow_period + 1)
        fast_alpha = 2.0 / (self.params.fast_period + 1)
        alpha = fast_alpha + (slow_alpha - fast_alpha) * (fractal_dim - 1.0)
        return np.clip(alpha, slow_alpha, fast_alpha)

As the code illustrates, a fractal_dim closer to 1 (trending) will result in an alpha closer to fast_alpha, making the FRAMA more responsive. Conversely, a fractal_dim closer to 2 (choppy) will yield an alpha nearer to slow_alpha, resulting in a smoother, less reactive FRAMA. This dynamic behavior is the central hypothesis we aim to validate: can a market-adaptive moving average provide superior signals to static ones?

Constructing the Trading Hypothesis: The FRAMA Strategy

Building upon the adaptive FRAMA, the strategy integrates a multi-layered approach to signal generation, reflecting a comprehensive trading hypothesis. Signals are not solely dependent on simple crossovers but are filtered and confirmed by additional market context.

Key elements of the strategy’s logic include:

The extensive parameterization of the FRAMAStrategy is critical for its role as a research tool:

class FRAMAStrategy(bt.Strategy):
    params = (
        ('frama_period', 30),        # Lookback for FRAMA fractal calculations
        ('trend_threshold', 0.01),   # Minimum FRAMA slope for trend detection
        ('price_threshold_tight', 1.01), # Tight price vs FRAMA crossover
        ('fractal_filter', 1.5),     # Max fractal dimension for trend-following signals
        ('adaptive_thresholds', True), # Enable volatility-based threshold adjustment
        ('enable_fallback', True),   # Use SMA crossover if FRAMA signals are absent
        ('stop_loss_pct', 0.05),     # Percentage for stop loss
        ('take_profit_pct', 0.5),    # Percentage for take profit
        # ... (numerous other parameters for fine-tuning)
    )

    def generate_frama_signal(self):
        # ... (logic to calculate and apply dynamic thresholds)
        if self.params.adaptive_thresholds and len(self.volatility) > 0:
            vol_factor = min(2.0, max(0.5, self.volatility[0] / 0.02))
            tight_threshold = 1 + (self.params.price_threshold_tight - 1) * vol_factor
            # ... (apply vol_factor to other thresholds, like trend_threshold)

Each parameter in this configuration represents a testable hypothesis. For example, by varying fractal_filter, we can assess the strategy’s performance strictly in trending markets versus allowing trades in more ambiguous conditions. The adaptive_thresholds parameter directly investigates the benefit of dynamic sensitivity to market volatility. This flexibility is essential for the iterative tuning process.

The Backtesting Process: Gathering Evidence

To evaluate the initial viability of this strategy, it is put through a simulated trading environment using historical market data. This backtesting phase is crucial for generating preliminary performance metrics and identifying potential strengths and weaknesses.

The Backtrader framework is used to set up the simulation, incorporating realistic trading conditions such as starting capital, commission fees, and position sizing. Standard performance analyzers—Sharpe Ratio, Max Drawdown, and Total Return—are attached to provide a quantitative snapshot of the strategy’s hypothetical performance.

# Initialize Cerebro (the backtesting engine)
cerebro = bt.Cerebro()
cerebro.addstrategy(FRAMAStrategy)
cerebro.adddata(bt_data) # Add historical data (e.g., BTC-USD)
cerebro.broker.setcash(10000.0) # Starting capital
cerebro.broker.setcommission(commission=0.001) # 0.1% commission
cerebro.addsizer(bt.sizers.PercentSizer, percents=95) # Allocate 95% of capital per trade

# Add performance analyzers
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')

# Execute the backtest
results = cerebro.run()
strat = results[0]

# Print the results
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')
# ... (Print Sharpe Ratio, Max Drawdown, Total Return with error handling)

The output from this backtest, including metrics like the Sharpe Ratio and Max Drawdown, offers the first tangible feedback on the strategy’s potential. A favorable Sharpe Ratio might suggest reasonable risk-adjusted returns, while a manageable Max Drawdown indicates resilience. However, these are directional insights only. They guide the next steps of research rather than confirming profitability.

Beyond the First Pass: The Path Forward

It is crucial to reiterate: the current stage represents iteration, not implementation. The FRAMA strategy, as presented, is a sophisticated research tool designed to test a hypothesis about adaptive moving averages. The initial backtest results, regardless of how promising they may appear, are merely data points in a much larger validation process.

The subsequent stages of development typically involve:

In conclusion, this FRAMA-based trading model serves as an excellent framework for quantitative research. Its advanced features, meticulous parameterization, and detailed analytical outputs provide a solid foundation for exploring adaptive trading concepts. However, like all promising ideas in quantitative finance, it remains firmly in the realm of research and development, awaiting further rigorous validation before any consideration for live deployment.