The strategy trades G10 currencies using daily momentum signals, going long on positive trends and short on negative, with trading limited to days 20–29 monthly, equally weighting currencies.

I. STRATEGY IN A NUTSHELL

This strategy trades G10 currencies using a simple daily momentum signal, going long if today’s exchange rate exceeds yesterday’s and short otherwise. The trades are equally weighted across currencies. A seasonality filter restricts trading to the final third of each month (days 20–29), where historical evidence shows momentum performs better. Positions are adjusted daily within this window to capture short-term trends.

II. ECONOMIC RATIONALE

The rationale is that momentum profitability in FX markets varies with volatility patterns. Research shows volatility itself follows a seasonal rhythm, strongly influenced by the timing of U.S. macroeconomic announcements, which cluster late in the month. This clustering amplifies price movements and improves the effectiveness of momentum-based trading.

III. SOURCE PAPER

Day-of-the-Month Effects in the Performance of Momentum Trading Strategies in the Foreign Exchange Market [Click to Open PDF]

Harris, Stoja, Yilmaz

<Abstract>

In this paper, we document a very strong day-of-the-month effect in the performance of momentum strategies in the foreign exchange market. We show that this seasonality in trading strategy performance is attributable to seasonality in the conditional volatility of foreign exchange returns, and in the volatility of conditional volatility. Indeed a two-factor model employing conditional volatility and the volatility of conditional volatility explains as much as 70 percent of the intra-month variation in the Sharpe ratio. We further show that the seasonality in volatility is in turn closely linked to the pattern of US macroeconomic news announcements, which tend to be clustered around certain days of the month.

IV. BACKTEST PERFORMANCE

Annualised Return3.5%
Volatility10%
Beta-0.012
Sharpe Ratio0.35
Sortino Ratio-0.707
Maximum DrawdownN/A
Win Rate49%

V. FULL PYTHON CODE

from AlgorithmImports import *
class FXMomentumSeasonality(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.symbols = [
            'CME_AD1', # Australian Dollar Futures, Continuous Contract #1
            'CME_CD1', # Canadian Dollar Futures, Continuous Contract #1
            'CME_SF1', # Swiss Franc Futures, Continuous Contract #1
            'CME_EC1', # Euro FX Futures, Continuous Contract #1
            'CME_BP1', # British Pound Futures, Continuous Contract #1
            'CME_JY1', # Japanese Yen Futures, Continuous Contract #1
            'CME_NE1', # New Zealand Dollar Futures, Continuous Contract #1
            'CME_MP1' # Mexican Peso Futures, Continuous Contract #1
        ]
        
        self.current_prices = {}
        self.yesterday_prices = {}
        
        # Momentum strategy is traded only during the last 1/3 of each month (days 20-29).
        self.start_day = 20
        self.end_day = 29
        leverage: int = 5
        
        for currency_future in self.symbols:
            security = self.AddData(QuantpediaFutures, currency_future, Resolution.Daily)
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(leverage)
            
            self.current_prices[currency_future] = 0
            self.yesterday_prices[currency_future] = 0
        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self.settings.daily_precise_end_time = False
    def OnData(self, data):
        custom_data_last_update_date: Dict[str, datetime.date] = QuantpediaFutures.get_last_update_date()
        # Storing daily data about future currencies
        for symbol in self.symbols:
            if self.securities[symbol].get_last_data() and self.time.date() > custom_data_last_update_date[symbol]:
                self.liquidate()
                return
            if symbol in data:
                if data[symbol]:
                    price = data[symbol].Value
                    if price != 0:
                        self.yesterday_prices[symbol] = self.current_prices[symbol]
                        self.current_prices[symbol] = price
            
        long = []
        short = []
        
        if self.start_day <= self.Time.day <= self.end_day: # Momentum strategy is traded only during the last 1/3 of each month (days 20-29).
            for symbol in self.symbols:
                if self.current_prices[symbol] > self.yesterday_prices[symbol]:
                    long.append(symbol)
                else:
                    short.append(symbol)
        
        targets: List[PortfolioTarget] = []
        for i, portfolio in enumerate([long, short]):
            for symbol in portfolio:
                if symbol in data and data[symbol]:
                    targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
        
        self.SetHoldings(targets, True)
		
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))
        
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaFutures(PythonData):
    _last_update_date:Dict[Symbol, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[Symbol, datetime.date]:
       return QuantpediaFutures._last_update_date
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/futures/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaFutures()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%d.%m.%Y") + timedelta(days=1)
        data['back_adjusted'] = float(split[1])
        data['spliced'] = float(split[2])
        data.Value = float(split[1])
        if config.Symbol.Value not in QuantpediaFutures._last_update_date:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaFutures._last_update_date[config.Symbol.Value]:
            QuantpediaFutures._last_update_date[config.Symbol.Value] = data.Time.date()
        return data

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