Quant BuffetRelax, Not Over Thinking

A Multi Strategy Approach to Trading Foreign Exchange Futures

Log in to collect

Strategy in a nutshell

Invest in the eight most liquid current-month FX futures on CME (AUD, GBP, CAD, EUR, JPY, MXN, NZD, CHF) using multiple indicators: Interest Rate Carry, Momentum, Mean Reversion, Equity Momentum, and Commodity Momentum. Each indicator is normalized to [-0.5, +0.5] with bias adjustment, and risk is allocated proportionally to indicator strength, targeting 10% annualized risk. Strategies are equally weighted, allowing negative weights, and rebalanced monthly.

Economic rationale

The strategy exploits proven return patterns across FX, equities, and commodities. Carry trades capture interest-rate differentials, equity momentum links global equity trends to FX returns, and commodity exposures reflect currency sensitivity. Mean reversion among developed currency pairs adds further predictive power. Combining multiple strategies enhances consistency and Sharpe ratios relative to any single strategy, demonstrating the value of systematic diversification in FX futures.

III. URCE PAPER

Realized Semibetas: Signs of Things to Come [Click to Open PDF]

Sonam Srivastava — Wright Research; Gaurav Chakravorty — Qplum; Sanchit Gupta — Qplum; Ankit Awasthi — Qplum.

In this article we present a systematic multi-strategy approach to trading foreign exchange futures for a managed futures portfolio. Our central finding is that there is more alpha to be derived from combining different indicators compared to hand engineering each indicator. We show that combining technical indicators like momentum and mean reversion with fx carry indicators leads to significant improvement over individual indicators. Through an end to end systematic portfolio construction methodology, including indicator construction, normalization and combination we are able to improve the Sharpe Ratio of the resulting portfolio over the best performing single indicator by 60% when evaluated in an unbiased walk forward backtest.

Backtest performance

Annualised return9.76%
Volatility9.3%
Beta0.033
Sharpe ratio0.5
Sortino ratio-0.965
Win rate48%

Full Python code

from AlgorithmImports import *
import numpy as np
from typing import List, Dict, Tuple
from scipy import stats
class AMultiStrategyApproachtoTradingForeignExchangeFutures(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)

# Symbols - Currency future, equity future, 10Y bond yield, cash rate data.
# Cash rate source: https://www.quandl.com/data/OECD-Organisation-for-Economic-Co-operation-and-Development
# 10Y bond yield source: www.investing.com
self.symbols:List[Tuple[str, str, str, str]] = [
    ('CME_AD1', 'ASX_YAP1', 'AU10YT', 'IR3TIB01AUM156N'),    # Australian Dollar Futures, Continuous Contract #1
    ('CME_CD1', 'LIFFE_FCE1', 'CA10YT', 'IR3TIB01CAM156N'),  # Canadian Dollar Futures, Continuous Contract #1
    ('CME_SF1', 'EUREX_FSMI1', 'CH10YT', 'IR3TIB01CHM156N'), # Swiss Franc Futures, Continuous Contract #1
    ('CME_EC1', 'EUREX_FSTX1', 'DE10YT', 'IR3TIB01EZM156N'), # Euro FX Futures, Continuous Contract #1
    ('CME_BP1', 'LIFFE_Z1', 'GB10YT', 'LIOR3MUKM'),          # British Pound Futures, Continuous Contract #1
    ('CME_JY1', 'SGX_NK1', 'JP10YT', 'IR3TIB01JPM156N'),     # Japanese Yen Futures, Continuous Contract #1
]
            
# Symbol data.
self.data:Dict[Symbol, SymbolData] = {}
self.short_period:int = 3
self.long_period:int = 12
self.leverage:int = 20

# Target risk of the allocation.
self.target_risk:float = 0.1

for currency_future, equity_future, bond_yield_symbol, cash_rate_symbol in self.symbols:
    # Currency future data.
    data = self.AddData(data_tools.QuantpediaFutures, currency_future, Resolution.Daily)
    data.SetFeeModel(data_tools.CustomFeeModel())
    data.SetLeverage(self.leverage)
    self.data[currency_future] = data_tools.SymbolData(currency_future, self, self.short_period*21, self.long_period*21, True)
    
    # Equity future data.
    self.AddData(data_tools.QuantpediaFutures, equity_future, Resolution.Daily)
    self.data[equity_future] = data_tools.SymbolData(equity_future, self, self.short_period*21, self.long_period*21, False)
    
    # Bond yield data.
    self.AddData(data_tools.QuantpediaBondYield, bond_yield_symbol, Resolution.Daily)
    # Interbank rate data.
    self.AddData(data_tools.InterestRate3M, cash_rate_symbol, Resolution.Daily)

self.usa_10Y_yield:str = 'US10Y'
self.usa_cash_rate:str = 'IR3TIB01USM156N'
self.AddData(data_tools.QuantpediaBondYield, self.usa_10Y_yield, Resolution.Daily)
self.AddData(data_tools.InterestRate3M, self.usa_cash_rate, Resolution.Daily)
self.commodity_index:Symbol = self.AddEquity('DBC', Resolution.Daily).Symbol
self.data[self.commodity_index] = data_tools.SymbolData(self.commodity_index, self, self.short_period*21, self.long_period*21, False)

self.last_month:int = -1

def OnData(self, data:Slice) -> None:
# Rebalance once a month.
if self.last_month != self.Time.month:
    self.last_month = self.Time.month
    ir_last_update_date:Dict[str, datetime.date] = data_tools.InterestRate3M.get_last_update_date()
    qp_futures_last_update_date:Dict[str, datetime.date] = data_tools.QuantpediaFutures.get_last_update_date()
    
    # Create indicators.
    weight:Dict[Symbol, float] = {}
    for currency_future, equity_future, bond_yield_symbol, cash_rate_symbol in self.symbols:
        # data is still coming
        if self.Securities[currency_future].GetLastData() and qp_futures_last_update_date[currency_future] <= self.Time.date() \
            or self.Securities[cash_rate_symbol].GetLastData() and ir_last_update_date[cash_rate_symbol] <= self.Time.date():
            continue
        # If data needed is ready (Momentum and SMA's).
        if self.data[currency_future].is_ready() and self.data[equity_future].is_ready() and self.data[self.commodity_index].is_ready():
            
            long_interest_rate_carry:Union[float,None] = self.Securities[bond_yield_symbol].Price - self.Securities[self.usa_10Y_yield].Price if self.Securities.ContainsKey(bond_yield_symbol) and self.Securities.ContainsKey(self.usa_10Y_yield) else None
            short_interest_rate_carry:Union[float,None] = self.Securities[cash_rate_symbol].Price - self.Securities[self.usa_cash_rate].Price if self.Securities.ContainsKey(cash_rate_symbol) and self.Securities.ContainsKey(self.usa_cash_rate) else None
            
            curr_short_term_momentum:float = self.data[currency_future].short_term_momentum()
            curr_long_term_momentum:float = self.data[currency_future].long_term_momentum()
            
            curr_long_term_reversal:Union[float,None] =  self.Securities[currency_future].Price / self.data[currency_future].long_term_sma() if self.Securities.ContainsKey(currency_future) else None
            curr_short_term_reversal:Union[float,None] =  self.Securities[currency_future].Price / self.data[currency_future].short_term_sma() if self.Securities.ContainsKey(currency_future) else None
            equity_short_term_momentum:float = self.data[equity_future].short_term_momentum()
            equity_long_term_momentum:float = self.data[equity_future].long_term_momentum()
            
            commodity_short_term_momentum:float = self.data[self.commodity_index].short_term_momentum()
            commodity_long_term_momentum:float = self.data[self.commodity_index].long_term_momentum()
            
            if long_interest_rate_carry and short_interest_rate_carry and curr_long_term_reversal and curr_short_term_reversal:
                indicator_value:Tuple[float] = (long_interest_rate_carry, short_interest_rate_carry, curr_long_term_momentum, curr_short_term_momentum,    \
                                                curr_long_term_reversal, curr_short_term_reversal, equity_long_term_momentum, equity_short_term_momentum,  \
                                                commodity_long_term_momentum, commodity_short_term_momentum)
                
                self.data[currency_future].add_indicator_value(indicator_value)
                
                if self.data[currency_future].indicator_history_is_ready():
                    num_of_properties:int = len(indicator_value)
                    scores:List[float] = []
                    for propert_index in range(num_of_properties):
                        scores.append(abs((stats.percentileofscore([x[propert_index] for x in self.data[currency_future].IndicatorsHistory], indicator_value[propert_index]) / 100 ) - 0.5))
                    
                    score_sum:float = sum(scores)
                    scores:np.ndarray = np.array(scores)
                    scores = (scores / score_sum) * self.target_risk
                    risk_budget:float = sum(scores)
                    weight[currency_future] = risk_budget
    
    # Trade execution.
    invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
    for symbol in invested:
        if symbol not in weight:
            self.Liquidate(symbol)
            
    for symbol, w in weight.items():
        self.SetHoldings(symbol, w)