Payroll News Timing in FX
Log in to collectAcademic paper
Strategy in a nutshell
The investment universe includes FX futures for the euro (Deutsche mark before euro), British pound, Swiss franc, Japanese yen, Canadian dollar, Australian dollar, and New Zealand dollar. Data sources include speculator FX exposures from the CFTC’s Commitments of Traders (COT) reports, bid-ask spreads from futures and the OTC market (via Refinitiv Tick History and Eikon), and futures margin data from the Chicago Mercantile Exchange (CME).
First, the long and short open interest in U.S. dollars is evaluated for each currency futures contract for the speculator investor type. The daily net open interest is defined as long minus short positions. FX exposure of speculators for each currency is calculated by dividing its net open interest by the sum of absolute net open interests across all currencies. Portfolio weights are equal to these FX exposures.
On U.S. payroll announcement days, the strategy mimics speculator FX exposure by investing in currency futures using 10-day lagged portfolio weights. Positions are opened five minutes before the announcements and closed one hour after release.
The FX target portfolio is composed of (100−α)%(100-\alpha)\%(100−α)% risky assets and α%\alpha\%α% cash holdings (between 5% and 20%), to meet futures margin requirements. The remainder is invested in risky assets such as the S&P 500 total return index, Bank of America U.S. corporate bond total return index, or JP Morgan aggregate commodity total return index. Results are presented for a strategy investing in the S&P 500 with 20% cash, achieving a Sharpe ratio gain of 0.39 relative to a strategy that ignores payroll announcements.
Economic rationale
The strategy leverages the informational content of speculators’ FX portfolio exposures, which contain predictive signals about upcoming payroll announcements. This predictive power persists due to long-lived information in FX positions, driven by timing and stealth motives:
Patience in Execution: Speculators may delay trades to avoid noise trading or take advantage of market liquidity.
Access to Private Information: Some speculators possess information unavailable to other market participants.
By mimicking informed speculators’ FX portfolio exposures, investors can capture these signals. FX exposures are measured as the net position of long and short open interests in U.S. dollars and rescaled so that portfolio weights sum to unity across different futures, reflecting the relative allocation of informed market activity.
Backtest performance
Full Python code
from AlgorithmImports import *
import numpy as np
import data_tools
#endregion
class PayrollNewsTiminginFX(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols:dict[str, str] = {
"AUDUSD" : "QAD", # Australian Dollar Futures, Continuous Contract #1
"GBPUSD" : "QBP", # British Pound Futures, Continuous Contract #1
"CADUSD" : "QCD", # Canadian Dollar Futures, Continuous Contract #1
"EURUSD" : "QEC", # Euro FX Futures, Continuous Contract #1
"JPYUSD" : "QJY", # Japanese Yen Futures, Continuous Contract #1
"NZDUSD" : "QNE", # New Zealand Dollar Futures, Continuous Contract #1
"CHFUSD" : "QSF" # Swiss Franc Futures, Continuous Contract #1
}
self.SetTimeZone(TimeZones.NewYork)
# storing most recent open interest for every currency
self.recent_net_open_interest:dict[str, float] = {}
for forex_symbol, cot_symbol in self.symbols.items():
# forex data
data = self.AddForex(forex_symbol, Resolution.Minute, Market.Oanda)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(10)
# COT data
self.AddData(data_tools.CommitmentsOfTraders, cot_symbol, Resolution.Daily)
self.rebalance_flag:bool = False
self.recent_month:int = -1
def OnData(self, data):
# liquidate one hour after announcement
if self.Time.hour == 9 and self.Time.minute == 30:
if self.Portfolio.Invested:
self.Liquidate()
# first friday of the month
if self.Time.weekday() == 4 and self.Time.month != self.recent_month:
self.rebalance_flag = True
if self.Time.month != self.recent_month:
self.recent_month = self.Time.month
net_open_interest:dict[str, float] = {}
# store most recent open interest for every currency
for forex_symbol, cot_symbol in self.symbols.items():
if cot_symbol in data and data[cot_symbol]:
# forex data is still comming in
if self.Securities[forex_symbol].GetLastData() and (self.Time.date() - self.Securities[forex_symbol].GetLastData().Time.date()).days < 5:
long_count:float = data[cot_symbol].GetProperty("LARGE_SPECULATOR_LONG")
short_count:float = data[cot_symbol].GetProperty("LARGE_SPECULATOR_SHORT")
# recent daily net open interest as the open long minus the short interest
self.recent_net_open_interest[forex_symbol] = long_count - short_count
# trade
if self.rebalance_flag and self.Time.hour == 8 and self.Time.minute == 25:
self.rebalance_flag = False
if self.recent_net_open_interest:
oi_abs_sum:float = sum([abs(x) for x in list(self.recent_net_open_interest.values())])
# the portfolio weights are equal to the FX exposures
fx_exposure:dict[str, float] = { forex_symbol:oi/oi_abs_sum for forex_symbol, oi in self.recent_net_open_interest.items() }
if fx_exposure != 0:
# trade
for forex_symbol, exposure in fx_exposure.items():
self.SetHoldings(forex_symbol, exposure)