The strategy trades 9 weighted currency pairs using a 3x MACD for trend signals, adjusting positions based on volatility thresholds, with daily rebalancing to adapt to market conditions.

I. STRATEGY IN A NUTSHELL

The strategy trades nine major currency pairs weighted by BIS trading volume, using a 3x MACD indicator (1/32, 1/61, 1/117) to identify trends. Daily volatility is measured via the Risk Metrics method, with thresholds optimized in the first year of data. In low-volatility environments, positions follow trend signals (long when short-term MA > long-term MA, short otherwise), while in high-volatility periods the signals are reversed. The portfolio is rebalanced daily to adapt to changing market conditions.

II. ECONOMIC RATIONALE

Periods of high volatility often coincide with frequent price reversals, which undermine trend-following strategies. By reversing trading rules during such periods, the strategy mitigates losses and aligns trades with prevailing market dynamics, improving overall performance.

III. SOURCE PAPER

Volatility Filters for Asset Management: An Application to Managed Futures [Click to Open PDF]

Dunis, Miao, Professor of Banking and Finance at Liverpool John Moores University and Director of CIBEF, Associate Researcher with CIBEF and currently working on his PhD thesis at Liverpool John Moores University

<Abstract>

Technical trading rules are known to perform poorly in periods when volatility is high. The objective of this paper is to study whether addition of volatility filters can improve model performance. Different from previous studies on technical trading rules which base their findings from an academic perspective, this paper tries to relate to the real world business: two portfolios, which are highly correlated with a managed futures index and a currency traders’ benchmark index are formed to replicate the performance of the typical managed futures and managed currency funds. The volatility filters proposed are then applied directly to these two portfolios with the hope that proposed techniques will then have both academic and industrial significance. Two volatility filters are proposed, namely a “no-trade” filter where all market positions are closed in volatile periods, and a “reverse” filter where signals from a simple Moving Average Convergence and Divergence (MACD) are reversed if market volatility is higher than a given threshold. To assess the consistency of model performance, the whole period (04/01/1999 to 31/12/2004) is split into 3 sub-periods. Our results show that the addition of the two volatility filters adds value to the models performance in terms of annualised return, maximum drawdown, risk-adjusted Sharpe ratio and Calmar ratio in all the 3 sub-periods.

IV. BACKTEST PERFORMANCE

Annualised Return4.63%
Volatility5%
Beta0.004
Sharpe Ratio0.93
Sortino RatioN/A
Maximum Drawdown-9.3%
Win Rate31%

V. FULL PYTHON CODE

from AlgorithmImports import *
import datetime
import numpy as np
#endregion
class TimeSeriesMomentumCombinedwithVolatilityFiltersinFOREX(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        self.period = 12*21
        self.SetWarmUp(self.period)
        
        self.current_date = -1
        
        self.symbols = ["USDJPY", "GBPUSD", "EURUSD", "USDCHF", "USDCAD", "AUDUSD", "EURGBP", "EURJPY", "EURCHF"]
        self.data = {}
                       
        for symbol in self.symbols:
            data = self.AddForex(symbol, Resolution.Minute, Market.FXCM)
            data.SetFeeModel(CustomFeeModel())
        
            if symbol not in self.data:
                ma32 = SimpleMovingAverage(symbol, 32)
                ma61 = SimpleMovingAverage(symbol, 61)
                ma117 = SimpleMovingAverage(symbol, 117)
                
                self.data[symbol] = SymbolData(symbol, self.period, ma32, ma61, ma117, self.GetWeight(symbol))
    def OnData(self, data):
        if self.Time.time() == datetime.time(0,0,0):
            # Updating last day close.
            for symbol in self.symbols:
                if symbol in data and data[symbol]:
                    close = data[symbol].Value
                    self.data[symbol].update(self.Time, close)
        
        # Trading one minute before day ends.
        if (self.Time + timedelta(minutes=1)).date() ==  self.current_date: 
            return
                
        self.current_date = (self.Time + timedelta(minutes=1)).date()
        
        # Wait until data are warmed up.
        if self.IsWarmingUp: return
    
        # Trade execution
        self.Liquidate()
        
        for symbol in self.symbols:
            if self.data[symbol].is_ready():
                yearly_vol = self.data[symbol].volatility(self.period)
                monthly_vol = self.data[symbol].volatility(21)
    
                price = self.data[symbol].Price
                ma32 = self.data[symbol].ma32()
                ma61 = self.data[symbol].ma61()
                ma117 = self.data[symbol].ma117()
    
                traded_weight = 0
                weight = self.data[symbol].Weight
                    
                # Trend startegy
                if monthly_vol < yearly_vol:
                    #Long
                    if ma32 > ma61 and ma61 > ma117:
                        if price > ma32 and price > ma61 and price > ma117:
                            traded_weight = weight
                        elif price > ma61 and price > ma117:
                            traded_weight = weight * 2/3
                        elif price > ma117:
                            traded_weight = weight * 1/3
                        else:
                            continue
                    #Short        
                    elif ma32 < ma61 and ma61 < ma117:
                        if price < ma32 and price < ma61 and price < ma117:
                            traded_weight = -weight
                        elif price < ma61 and price < ma117:
                            traded_weight = -weight * 2/3
                        elif price < ma117:
                            traded_weight = -weight * 1/3
                        else:
                            continue
                    
                    self.SetHoldings(symbol, traded_weight)
    
                # Counter-Trend startegy    
                elif monthly_vol > yearly_vol:
                    #Long        
                    if ma32 < ma61 and ma61 < ma117:
                        if price < ma32 and price < ma61 and price < ma117:
                            traded_weight = weight
                        elif price < ma61 and price < ma117:
                            traded_weight = weight * 2/3
                        elif price < ma117:
                            traded_weight = weight * 1/3
                        else:
                            continue
                    #Short
                    elif ma32 > ma61 and ma61 > ma117:
                        if price > ma32 and price > ma61 and price > ma117:
                            traded_weight = -weight
                        elif price > ma61 and price > ma117:
                            traded_weight = -weight * 2/3
                        elif price > ma117:
                            traded_weight = -weight * 1/3
                        else:
                            continue
                            
                    self.SetHoldings(symbol, traded_weight)
                        
    def GetWeight(self, argument):
        switcher = {
            "USDJPY": 0.2113,
            "GBPUSD": 0.1749,
            "EURUSD": 0.3576,
            "USDCHF": 0.0557,
            "USDCAD": 0.0507,
            "AUDUSD": 0.0642,
            "EURGBP": 0.0307,
            "EURJPY": 0.0364,
            "EURCHF": 0.0186,
        }
        return switcher.get(argument, "0.0")
class SymbolData:
    def __init__(self, symbol, lookback, ma32, ma61, ma117, weight):
        self.Symbol = symbol
        self.Price = None
        self.MA32 = ma32
        self.MA61 = ma61
        self.MA117 = ma117
        self.Weight = weight
        self.History = RollingWindow[float](lookback)
    def update(self, time, value):
        self.Price = value
        self.History.Add(value)
        self.MA32.Update(time, value)
        self.MA61.Update(time, value)
        self.MA117.Update(time, value)
        
    def is_ready(self):
        return self.MA32.IsReady and self.MA61.IsReady and self.MA32.IsReady and self.History.IsReady
    
    def ma32(self):
        return self.MA32.Current.Value
    def ma61(self):
        return self.MA61.Current.Value
    def ma117(self):
        return self.MA117.Current.Value
        
    def volatility(self, period):
        prices = np.array([x for x in self.History])[:period]
        returns = (prices[:-1]-prices[1:])/prices[1:]
        return np.std(returns) * np.sqrt(period)
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading