Quant BuffetRelax, Not Over Thinking

Time Series Momentum Combined with Volatility Filters in FOREX

Log in to collect

Academic paper

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.

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.

Backtest performance

Annualised return4.63%
Volatility5%
Beta0.004
Sharpe ratio0.93
Maximum drawdown-9.3%
Win rate31%

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"))