
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.
ASSET CLASS: CFDs, futures | REGION: Global | FREQUENCY:
Daily | MARKET: currencies | KEYWORD: FX, Momentum, Seasonality
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 Return | 3.5% |
| Volatility | 10% |
| Beta | -0.012 |
| Sharpe Ratio | 0.35 |
| Sortino Ratio | -0.707 |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
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