← Back to Home
Volatility-Responsive Trading with Time Decay Adaptive EMA Strategy

Volatility-Responsive Trading with Time Decay Adaptive EMA Strategy

The Time Decay Adaptive EMA Strategy is an advanced trend-following system that dynamically adjusts its sensitivity to market price changes based on prevailing volatility. Unlike traditional Exponential Moving Averages (EMAs) with fixed periods, this strategy’s EMA adapts its smoothing factor (alpha) to market conditions. This allows the EMA to be more responsive during low volatility and smoother during high volatility, aiming to provide more robust signals. The strategy also integrates robust risk management through an ATR-based trailing stop.

Strategy Overview

The TimeDecayAdaptiveEMA strategy adapts its core indicator and manages trades through the following mechanisms:

Adaptive EMA Calculation: The core of this strategy is a custom adaptive EMA. Its smoothing factor, alpha, is not constant but varies inversely with market volatility.

Entry Logic: The strategy uses a simple crossover mechanism for entries:

Exit Logic (ATR Trailing Stop): All open positions are managed with a dynamic ATR-based trailing stop:

The strategy also tracks internal statistics such as trade counts, entry types (long/short), profitable trades, ATR stop exits, adaptive signal generation, and volatility regime changes. It also categorizes trades by volatility regime at entry for further analysis.

Backtrader Implementation

Here’s the TimeDecayAdaptiveEMA class, including its helper methods:

import backtrader as bt
import numpy as np
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import dateutil.relativedelta as rd
import warnings
from scipy import stats # This import might not be directly used in the strategy class itself, but required for the main script
import seaborn as sns # This import might not be directly used in the strategy class itself, but required for the main script
import multiprocessing

warnings.filterwarnings("ignore")

class TimeDecayAdaptiveEMA(bt.Strategy):
    params = (
        ('vol_calc_window', 20),
        ('vol_ema_period', 10),
        ('period_ema_min_for_alpha0', 10),
        ('lambda_decay_param', 50.0),
        ('period_ema_max_cap', 150),
        ('atr_window_sl', 14),
        ('atr_multiplier_sl', 2.0),
        ('order_percentage', 0.95),
        ('printlog', False),
    )
    
    def __init__(self):
        # Calculate returns and historical volatility
        self.returns = bt.indicators.PctChange(self.data.close, period=1)
        self.hist_vol = bt.indicators.StandardDeviation(self.returns, period=self.params.vol_calc_window)
        
        # ATR for stop loss
        self.atr = bt.indicators.AverageTrueRange(self.data, period=self.params.atr_window_sl)
        
        # Initialize state variables
        self.ewma_vol = None # Exponentially Weighted Moving Average of volatility
        self.adaptive_ema = None
        self.trailing_stop = None
        self.entry_price = None # Stores entry price for initial stop calculation
        
        # Constants for alpha calculation
        self.alpha_0 = 2 / (self.params.period_ema_min_for_alpha0 + 1) # Max alpha (min smoothing)
        self.alpha_min = 2 / (self.params.period_ema_max_cap + 1) # Min alpha (max smoothing)
        
        # Strategy statistics
        self.trade_count = 0
        self.long_entries = 0
        self.short_entries = 0
        self.atr_stop_exits = 0
        self.profitable_trades = 0
        self.adaptive_signals = 0 # Count of signals generated by adaptive EMA
        self.vol_regime_changes = 0 # Count of significant volatility shifts
        self.high_vol_trades = 0 # Trades entered during high volatility
        self.low_vol_trades = 0 # Trades entered during low volatility
        
    def log(self, txt, dt=None):
        if self.params.printlog:
            dt = dt or self.datas[0].datetime.date(0)
            print(f'{dt.isoformat()}: {txt}')
            
    def _update_ewma_volatility(self):
        """Update EWMA of historical volatility"""
        current_hist_vol = self.hist_vol[0]
        
        if np.isnan(current_hist_vol):
            return
            
        prev_ewma_vol = self.ewma_vol
        
        if self.ewma_vol is None:
            self.ewma_vol = current_hist_vol
        else:
            # EWMA calculation: alpha_vol = 2 / (period + 1)
            alpha_vol = 2 / (self.params.vol_ema_period + 1)
            self.ewma_vol = alpha_vol * current_hist_vol + (1 - alpha_vol) * self.ewma_vol
            
        # Detect volatility regime changes (e.g., 10% change in EWMA vol)
        if prev_ewma_vol is not None and self.ewma_vol is not None:
            if prev_ewma_vol != 0: # Avoid division by zero
                vol_change = abs(self.ewma_vol - prev_ewma_vol) / prev_ewma_vol
                if vol_change > 0.1: # Threshold for significant change
                    self.vol_regime_changes += 1
            
    def _calculate_adaptive_alpha(self):
        """Calculate adaptive alpha based on volatility"""
        if self.ewma_vol is None:
            return self.alpha_0 # Use max alpha if EWMA vol not yet available
            
        # Time-decay function: alpha = alpha_0 * exp(-lambda * volatility)
        alpha = self.alpha_0 * np.exp(-self.params.lambda_decay_param * self.ewma_vol)
        
        # Clip alpha to be within defined bounds [alpha_min, alpha_0]
        return np.clip(alpha, self.alpha_min, self.alpha_0)
            
    def _update_adaptive_ema(self):
        """Update the time-decay adaptive EMA"""
        current_price = self.data.close[0]
        alpha = self._calculate_adaptive_alpha()
        
        if self.adaptive_ema is None:
            self.adaptive_ema = current_price # Initialize with current price if first time
        else:
            self.adaptive_ema = alpha * current_price + (1 - alpha) * self.adaptive_ema

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return
        if order.status in [order.Completed]:
            if order.isbuy():
                self.trade_count += 1
                self.long_entries += 1
                self.log(f'LONG ENTRY: Price {order.executed.price:.2f}')
                
                # Track volatility regime at entry
                if self.ewma_vol is not None and self.ewma_vol > 0.02: # Example threshold for 'high' vol (can be parametrized)
                    self.high_vol_trades += 1
                else:
                    self.low_vol_trades += 1
                    
            elif order.issell():
                # Check if it's a new short entry or closing a long position (which triggers notify_trade PnL)
                if self.position.size <= 0: # Indicates a new short position (size is 0 or negative after sell)
                    self.trade_count += 1
                    self.short_entries += 1
                    self.log(f'SHORT ENTRY: Price {order.executed.price:.2f}')
                    
                    # Track volatility regime at entry
                    if self.ewma_vol is not None and self.ewma_vol > 0.02:
                        self.high_vol_trades += 1
                    else:
                        self.low_vol_trades += 1
                else: # This is a closing sell order for a long position (e.g. stop loss)
                    self.atr_stop_exits += 1
                    self.log(f'ATR STOP EXIT: Price {order.executed.price:.2f}')
            
            # Reset trailing stop and entry price data when a position is closed (size is 0)
            if not self.position:
                self.trailing_stop = None
                self.entry_price = None
        self.order = None # Clear pending order

    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        if trade.pnl > 0:
            self.profitable_trades += 1
        self.log(f'TRADE CLOSED: PnL {trade.pnl:.2f}')
    
    def next(self):
        # Skip if insufficient data for indicators to warm up
        if len(self.data) < max(self.params.vol_calc_window, self.params.atr_window_sl, 
                                self.params.period_ema_min_for_alpha0, self.params.vol_ema_period):
            return
            
        # Update custom adaptive indicators before core logic
        self._update_ewma_volatility()
        self._update_adaptive_ema()
        
        # Ensure adaptive EMA has warmed up
        if self.adaptive_ema is None:
            return
            
        position = self.position.size
        current_close = self.data.close[0]
        current_atr = self.atr[0]
        prev_close = self.data.close[-1]
        
        # Handle existing positions - check trailing stops first
        # For long positions
        if position > 0:
            if self.trailing_stop is None: # Initialize stop on the first bar after entry
                self.trailing_stop = current_close - self.params.atr_multiplier_sl * current_atr
            elif self.data.low[0] <= self.trailing_stop: # Check if price hit stop
                self.close() # Close position
                # notify_order will increment atr_stop_exits if this is a sell to close
                self.trailing_stop = None # Reset stop
                self.entry_price = None # Reset entry price
                return # Exit next() after closing
            else: # Update trailing stop if price moved favorably
                new_stop = current_close - self.params.atr_multiplier_sl * current_atr
                if new_stop > self.trailing_stop: # Stop only moves up
                    self.trailing_stop = new_stop
                    
        # For short positions
        elif position < 0:
            if self.trailing_stop is None: # Initialize stop on the first bar after entry
                self.trailing_stop = current_close + self.params.atr_multiplier_sl * current_atr
            elif self.data.high[0] >= self.trailing_stop: # Check if price hit stop
                self.close() # Close position
                # notify_order will increment atr_stop_exits if this is a buy to close
                self.trailing_stop = None # Reset stop
                self.entry_price = None # Reset entry price
                return # Exit next() after closing
            else: # Update trailing stop if price moved favorably
                new_stop = current_close + self.params.atr_multiplier_sl * current_atr
                if new_stop < self.trailing_stop: # Stop only moves down
                    self.trailing_stop = new_stop
                    
        # Entry signals based on EMA crossover - only if no position open
        if position == 0:
            # Long signal: previous close above adaptive EMA
            if prev_close > self.adaptive_ema:
                self.adaptive_signals += 1
                cash = self.broker.get_cash()
                size = (cash * self.params.order_percentage) / self.data.close[0]
                self.buy(size=size) # Place buy order
                self.entry_price = self.data.open[0] # Record entry price for initial stop
                # Initial trailing stop set in notify_order for long, based on entry price and current ATR
                
            # Short signal: previous close below adaptive EMA
            elif prev_close < self.adaptive_ema:
                self.adaptive_signals += 1
                cash = self.broker.get_cash()
                size = (cash * self.params.order_percentage) / self.data.close[0]
                self.sell(size=size) # Place sell order
                self.entry_price = self.data.open[0] # Record entry price for initial stop
                # Initial trailing stop set in notify_order for short, based on entry price and current ATR

Parameters (params):

Initialization (__init__):

Helper Methods for Adaptive EMA:

Order and Trade Notifications (notify_order, notify_trade):

Main Logic (next):

The next method contains the core strategy logic, executed on each new bar of data:

  1. Data Sufficiency Check: Ensures enough historical data is available for all indicators to warm up.
  2. Adaptive Indicator Updates: Calls _update_ewma_volatility() and _update_adaptive_ema() to ensure the adaptive EMA and volatility measures are up-to-date for the current bar.
  3. Position Management (Trailing Stop): This section prioritizes managing existing positions:
    • For long positions (position > 0):
      • If trailing_stop is None (meaning this is the first bar after entry), it initializes the trailing_stop.
      • It checks if the current data.low[0] has hit or fallen below the trailing_stop. If so, the position is closed, and the method returns.
      • Otherwise, it calculates a new_stop and updates self.trailing_stop only if new_stop is higher (trailing the price upwards).
    • For short positions (position < 0):
      • If trailing_stop is None, it initializes the trailing_stop.
      • It checks if the current data.high[0] has hit or risen above the trailing_stop. If so, the position is closed, and the method returns.
      • Otherwise, it calculates a new_stop and updates self.trailing_stop only if new_stop is lower (trailing the price downwards).
  4. Entry Signals (if no position open):
    • If position == 0 (no open position), it checks for crossover signals from the adaptive_ema:
      • Long Entry: If prev_close is above self.adaptive_ema, a buy order is placed. self.adaptive_signals is incremented, and self.entry_price is recorded for initial stop placement (which occurs in notify_order).
      • Short Entry: If prev_close is below self.adaptive_ema, a sell order is placed. self.adaptive_signals is incremented, and self.entry_price is recorded.

Comprehensive Analysis Framework

This section presents the full code for running optimization, a rolling backtest, and generating detailed results with plots for the TimeDecayAdaptiveEMA strategy.

def run_momentum_analysis():
    # OPTIMIZATION
    print("="*60)
    print("MOMENTUM IGNITION STRATEGY OPTIMIZATION")
    print("="*60)

    df = yf.download('BTC-USD', start='2020-01-01', end='2025-01-01', auto_adjust=False, progress=False)
    if isinstance(df.columns, pd.MultiIndex):
        df = df.droplevel(1, axis=1)

    print(f"Fetched data: {df.shape}")

    cerebro = bt.Cerebro()
    data = bt.feeds.PandasData(dataname=df)
    cerebro.adddata(data)
    cerebro.broker.setcash(10000.0)
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', timeframe=bt.TimeFrame.Days, annualize=True, riskfreerate=0.0)
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')

    print("Testing Adaptive EMA parameter combinations...")
    cerebro.optstrategy(
        TimeDecayAdaptiveEMA,
        vol_calc_window=[15, 20, 25],          # Volatility calculation window
        vol_ema_period=[8, 10, 12],            # Volatility EWMA period
        period_ema_min_for_alpha0=[8, 10, 12], # Min EMA period for alpha0
        lambda_decay_param=[30.0, 50.0, 70.0], # Decay parameter
        period_ema_max_cap=[100, 150, 200],    # Max EMA period cap
        atr_multiplier_sl=[1.5, 2.0, 2.5]      # ATR stop multiplier
    )

    stratruns = cerebro.run(maxcpus=1) # Force single-threaded to avoid multiprocessing issues with Jupyter/IDE
    print(f"Optimization complete! Tested {len(stratruns)} combinations.")

    # Find best parameters
    best_result = None
    best_sharpe = -999

    for run in stratruns:
        strategy = run[0]
        sharpe_analysis = strategy.analyzers.sharpe.get_analysis()
        returns_analysis = strategy.analyzers.returns.get_analysis()
        
        sharpe_ratio = sharpe_analysis.get('sharperatio', None)
        if sharpe_ratio is not None and not np.isnan(sharpe_ratio) and sharpe_ratio > best_sharpe:
            best_sharpe = sharpe_ratio
            best_result = {
                'vol_calc_window': strategy.p.vol_calc_window,
                'vol_ema_period': strategy.p.vol_ema_period,
                'period_ema_min_for_alpha0': strategy.p.period_ema_min_for_alpha0,
                'lambda_decay_param': strategy.p.lambda_decay_param,
                'period_ema_max_cap': strategy.p.period_ema_max_cap,
                'atr_multiplier_sl': strategy.p.atr_multiplier_sl,
                'sharpe': sharpe_ratio,
                'return': returns_analysis.get('rtot', 0) * 100,
                'trades': getattr(strategy, 'trade_count', 0),
                'adaptive_signals': getattr(strategy, 'adaptive_signals', 0),
                'vol_regime_changes': getattr(strategy, 'vol_regime_changes', 0)
            }

    if best_result:
        print(f"\nBEST ADAPTIVE EMA PARAMETERS:")
        print(f"Vol Calc Window: {best_result['vol_calc_window']}")
        print(f"Vol EMA Period: {best_result['vol_ema_period']}")
        print(f"Min EMA Period: {best_result['period_ema_min_for_alpha0']}")
        print(f"Lambda Decay: {best_result['lambda_decay_param']:.1f}")
        print(f"Max EMA Cap: {best_result['period_ema_max_cap']}")
        print(f"ATR Multiplier: {best_result['atr_multiplier_sl']:.1f}")
        print(f"Sharpe: {best_result['sharpe']:.3f}")
        print(f"Return: {best_result['return']:.1f}%")
        print(f"Trades: {best_result['trades']}")
        print(f"Adaptive Signals: {best_result['adaptive_signals']}")
    else:
        print("No valid results found, using defaults")
        best_result = {
            'vol_calc_window': 20,
            'vol_ema_period': 10,
            'period_ema_min_for_alpha0': 10,
            'lambda_decay_param': 50.0,
            'period_ema_max_cap': 150,
            'atr_multiplier_sl': 2.0
        }

    # ROLLING BACKTEST
    print(f"\n{'='*60}")
    print("RUNNING YEARLY ROLLING BACKTESTS")
    print(f"{'='*60}")

    strategy_params = {
        'vol_calc_window': best_result['vol_calc_window'],
        'vol_ema_period': best_result['vol_ema_period'],
        'period_ema_min_for_alpha0': best_result['period_ema_min_for_alpha0'],
        'lambda_decay_param': best_result['lambda_decay_param'],
        'period_ema_max_cap': best_result['period_ema_max_cap'],
        'atr_multiplier_sl': best_result['atr_multiplier_sl'],
        'order_percentage': 0.95,
        'printlog': False
    }

    results = []
    start_dt = pd.to_datetime("2018-01-01")
    end_dt = pd.to_datetime("2025-01-01")
    current_start = start_dt

    while True:
        current_end = current_start + rd.relativedelta(months=12)
        if current_end > end_dt:
            break
            
        print(f"Period: {current_start.date()} to {current_end.date()}")
        
        data = yf.download("BTC-USD", start=current_start, end=current_end, auto_adjust=False, progress=False)
        if data.empty or len(data) < 90:
            current_start += rd.relativedelta(months=12)
            continue
            
        if isinstance(data.columns, pd.MultiIndex):
            data = data.droplevel(1, 1)
        
        start_price = data['Close'].iloc[0]
        end_price = data['Close'].iloc[-1]
        benchmark_return = (end_price - start_price) / start_price
        
        feed = bt.feeds.PandasData(dataname=data)
        cerebro = bt.Cerebro()
        cerebro.addstrategy(TimeDecayAdaptiveEMA, **strategy_params)
        cerebro.adddata(feed)
        cerebro.broker.setcash(100000)
        cerebro.broker.setcommission(commission=0.001)
        
        start_val = cerebro.broker.getvalue()
        result = cerebro.run()
        final_val = cerebro.broker.getvalue()
        strategy_return = (final_val - start_val) / start_val
        
        strategy_instance = result[0]
        trades = getattr(strategy_instance, 'trade_count', 0)
        long_entries = getattr(strategy_instance, 'long_entries', 0)
        short_entries = getattr(strategy_instance, 'short_entries', 0)
        atr_stops = getattr(strategy_instance, 'atr_stop_exits', 0)
        profitable = getattr(strategy_instance, 'profitable_trades', 0)
        adaptive_signals = getattr(strategy_instance, 'adaptive_signals', 0)
        vol_regime_changes = getattr(strategy_instance, 'vol_regime_changes', 0)
        high_vol_trades = getattr(strategy_instance, 'high_vol_trades', 0)
        low_vol_trades = getattr(strategy_instance, 'low_vol_trades', 0)
        
        results.append({
            'year': current_start.year,
            'strategy_return': strategy_return,
            'benchmark_return': benchmark_return,
            'trades': trades,
            'long_entries': long_entries,
            'short_entries': short_entries,
            'atr_stop_exits': atr_stops,
            'profitable_trades': profitable,
            'adaptive_signals': adaptive_signals,
            'vol_regime_changes': vol_regime_changes,
            'high_vol_trades': high_vol_trades,
            'low_vol_trades': low_vol_trades
        })
        
        print(f"Strategy: {strategy_return:.1%} | Buy&Hold: {benchmark_return:.1%} | Trades: {trades} | VolRegimes: {vol_regime_changes}")
        current_start += rd.relativedelta(months=12)

    # RESULTS & PLOTTING
    results_df = pd.DataFrame(results)

    if not results_df.empty:
        print(f"\n{'='*80}")
        print("YEARLY RESULTS")
        print(f"{'='*80}")
        print("Year | Strategy | Buy&Hold | Outperf | Trades | Long | Short | WinRate")
        print("-" * 80)
        
        for _, row in results_df.iterrows():
            strat_ret = row['strategy_return'] * 100
            bench_ret = row['benchmark_return'] * 100
            outperf = strat_ret - bench_ret
            win_rate = (row['profitable_trades'] / max(1, row['trades'])) * 100
            
            print(f"{int(row['year'])} | {strat_ret:7.1f}% | {bench_ret:7.1f}% | {outperf:+6.1f}% | "
                  f"{int(row['trades']):6d} | {int(row['long_entries']):4d} | {int(row['short_entries']):5d} | {win_rate:6.1f}%")
        
        # PLOTS
        fig, axes = plt.subplots(2, 2, figsize=(16, 10))
        fig.suptitle('Time Decay Adaptive EMA Strategy Analysis', fontsize=16, fontweight='bold')
        
        # Cumulative performance
        strategy_cumulative = (1 + results_df['strategy_return']).cumprod()
        benchmark_cumulative = (1 + results_df['benchmark_return']).cumprod()
        
        ax1 = axes[0, 0]
        ax1.plot(results_df['year'], strategy_cumulative, 'o-', linewidth=3, color='purple', label='Strategy')
        ax1.plot(results_df['year'], benchmark_cumulative, 's-', linewidth=3, color='orange', label='Buy & Hold')
        ax1.set_title('Cumulative Performance')
        ax1.set_ylabel('Cumulative Return')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # Annual returns
        ax2 = axes[0, 1]
        x_pos = range(len(results_df))
        width = 0.35
        ax2.bar([x - width/2 for x in x_pos], results_df['strategy_return'] * 100, width, 
                label='Strategy', color='purple', alpha=0.8)
        ax2.bar([x + width/2 for x in x_pos], results_df['benchmark_return'] * 100, width,
                label='Buy & Hold', color='orange', alpha=0.8)
        ax2.set_title('Annual Returns')
        ax2.set_ylabel('Return (%)')
        ax2.set_xticks(x_pos)
        ax2.set_xticklabels([int(year) for year in results_df['year']])
        ax2.legend()
        ax2.grid(True, alpha=0.3)
        
        # Long vs Short
        ax3 = axes[1, 0]
        ax3.bar(x_pos, results_df['long_entries'], label='Long', color='green', alpha=0.8)
        ax3.bar(x_pos, results_df['short_entries'], bottom=results_df['long_entries'], 
                label='Short', color='red', alpha=0.8)
        ax3.set_title('Long vs Short Entries')
        ax3.set_ylabel('Number of Entries')
        ax3.set_xticks(x_pos)
        ax3.set_xticklabels([int(year) for year in results_df['year']])
        ax3.legend()
        ax3.grid(True, alpha=0.3)
        
        # Summary stats
        ax4 = axes[1, 1]
        ax4.axis('off')
        
        total_strategy_return = strategy_cumulative.iloc[-1] - 1
        total_benchmark_return = benchmark_cumulative.iloc[-1] - 1
        total_trades = results_df['trades'].sum()
        total_profitable = results_df['profitable_trades'].sum()
        
        summary_text = f"""
SUMMARY STATISTICS

Total Strategy Return: {total_strategy_return:.1%}
Total Buy & Hold Return: {total_benchmark_return:.1%}
Outperformance: {total_strategy_return - total_benchmark_return:+.1%}

Total Trades: {int(total_trades)}
Overall Win Rate: {total_profitable/max(1,total_trades):.1%}
Long Entries: {int(results_df['long_entries'].sum())}
Short Entries: {int(results_df['short_entries'].sum())}

Strategy wins in {(results_df['strategy_return'] > results_df['benchmark_return']).sum()}/{len(results_df)} years
        """
        
        ax4.text(0.1, 0.9, summary_text, transform=ax4.transAxes, va='top', ha='left',
                  fontsize=12, fontfamily='monospace',
                  bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.8))
        
        plt.tight_layout()
        plt.show()
        
        # Final verdict
        print(f"\n{'='*60}")
        if total_strategy_return > total_benchmark_return:
            print(f"STRATEGY WINS by {total_strategy_return - total_benchmark_return:+.1%}!")
        else:
            print(f"Strategy underperformed by {total_strategy_return - total_benchmark_return:.1%}")
        print(f"{'='*60}")

    else:
        print("No results generated!")

    print("ANALYSIS COMPLETE!")

Optimization (run_momentum_analysis - Optimization Section):

This section finds the best parameter combination for the TimeDecayAdaptiveEMA strategy:

Rolling Backtest (run_momentum_analysis - Rolling Backtest Section):

Following optimization, this section performs a rolling backtest with the identified best parameters to evaluate the strategy’s consistency over a broader period (2018-2025).

Results and Plotting

Pasted image 20250727210315.png
==================================================
SIMPLE SUMMARY
==================================================
Adaptive EMA Total Return: 1096.5%
Buy & Hold Total Return:   507.8%
Outperformance:           +588.8%
Total Trades:             297
Win Rate:                 19.5%
Strategy wins in 3/7 years

============================================================
🎉 ADAPTIVE EMA STRATEGY WINS by +588.8%!
============================================================

📊 ADAPTIVE EMA INSIGHTS:
- Total adaptive signals: 152
- Volatility regime changes: 50
- High volatility trades: 240 (80.8%)
- Low volatility trades: 57 (19.2%)
- Signal conversion rate: 195.4%
- EMA adapts faster in high volatility (α = α₀ × e^(-λ×σ))
- ATR trailing stops provide dynamic risk management

This comprehensive analysis framework allows for a thorough evaluation of the TimeDecayAdaptiveEMA strategy, from finding optimal parameters to understanding its performance consistency and behavioral characteristics across various market conditions.