Quant Buffet放轻松,别过度思虑

货币的趋势跟踪交易策略

登录后收藏

学术论文

Momentum and trend following trading strategies for currencies revisited : combining academia and industry

作者Janick Rohrbach; Silvan Suremann; Joerg Osterrieder

机构
  • CHZHAW Zurich University of Applied Sciences
  • ?Zurich University of Applied Sciences
  • ?Industrial Engineering & Business Information Systems

策略概要

该策略的目标是G10货币,使用外币作为基础货币,美元作为报价货币。计算短期(8、16、32天)和长期(24、48、96天)的指数移动平均线(EMA)。当短期EMA超过长期EMA时,出现正趋势;当短期EMA低于长期EMA时,出现负趋势。信号是短期和长期EMA之间的差值,使用3个月的移动标准差进行标准化,并使用1年的移动标准差进一步调整,以考虑波动性。响应函数将标准化序列映射到-1到1之间的范围,最终信号是过去信号的加权总和。对具有正信号的货币采取多头头寸,对具有负信号的货币采取空头头寸。投资组合每日重新平衡,在货币之间分配信号值除以10,杠杆比率为5:1。

II. 策略合理性

动量回报挑战了有效市场假说,因为交易算法可以识别货币趋势以提高回报。该策略使用具有衰减权重的指数移动平均线(EMA),优先考虑近期数据,跨越三个时间框架,以捕捉短期、中期和长期趋势。标准化根据波动率调整信号,在低波动率期间增强信号,在高波动率期间降低信号。响应函数将信号映射到-1到1之间的范围。最终加权总和确定信号的幅度,指导投资量。这种系统性的方法利用趋势和波动率调整来提高货币交易表现。

回测表现

波动率22.9%
夏普比率0.53
索提诺比率-1.547
胜率52%

完整 Python 代码

import numpy as np
from AlgorithmImports import *
from math import sqrt
from numpy import exp
class TrendFollowingTradingStrategiesforCurrencies(QCAlgorithm):
def Initialize(self):
 self.SetStartDate(2000, 1, 1)
 self.SetCash(100000)
 
 self.symbols = [
                 "CME_AD1", # Australian Dollar Futures, Continuous Contract #1
                 "CME_BP1", # British Pound Futures, Continuous Contract #1
                 "CME_CD1", # Canadian Dollar Futures, Continuous Contract #1
                 "CME_EC1", # Euro FX Futures, Continuous Contract #1
                 "CME_JY1", # Japanese Yen Futures, Continuous Contract #1
                 "CME_MP1", # Mexican Peso Futures, Continuous Contract #1
                 "CME_NE1", # New Zealand Dollar Futures, Continuous Contract #1
                 "CME_SF1", # Swiss Franc Futures, Continuous Contract #1
                 ]
 
 self.period = 12 * 21
 self.SetWarmUp(self.period)
 
 self.leverage = 5
 # Daily price data.
 self.data = {}
 
 # EMA pairs.
 self.ema_8_24 = {}
 self.ema_16_48 = {}
 self.ema_32_96 = {}
 
 self.Settings.MinAbsolutePortfolioTargetPercentage = 1e-30
 for symbol in self.symbols:
     data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
     data.SetFeeModel(CustomFeeModel())
     data.SetLeverage(self.leverage * 2)
     
     self.data[symbol] = RollingWindow[float](self.period)

     self.ema_8_24[symbol] = [self.EMA(symbol, 8, Resolution.Daily), self.EMA(symbol, 24, Resolution.Daily)]
     self.ema_16_48[symbol] = [self.EMA(symbol, 16, Resolution.Daily), self.EMA(symbol, 48, Resolution.Daily)]
     self.ema_32_96[symbol] = [self.EMA(symbol, 32, Resolution.Daily), self.EMA(symbol, 96, Resolution.Daily)]
     
def OnData(self, data):
 # Store daily price data.
 for symbol in self.symbols:
     if symbol in data and data[symbol]:
         price = data[symbol].Value
         self.data[symbol].Add(price)
 
 if self.IsWarmingUp: return
 signal = {}
 for symbol in self.symbols:
     if self.securities[symbol].get_last_data() and self.time.date() > QuantpediaFutures.get_last_update_date()[symbol]:
         self.liquidate(symbol)
         continue
     # Daily data is ready.
     if not self.data[symbol].IsReady:
         continue
 
     # First signal.
     # Source paper equation 5, page 5.
     x1 = self.ema_8_24[symbol][0].Current.Value - self.ema_8_24[symbol][1].Current.Value
     x2 = self.ema_16_48[symbol][0].Current.Value - self.ema_16_48[symbol][1].Current.Value
     x3 = self.ema_32_96[symbol][0].Current.Value - self.ema_32_96[symbol][1].Current.Value
     
     x = np.array([x1, x2, x3])
     prices = [x for x in self.data[symbol]]
     price_std_short = np.std(prices[:3 * 21])
     price_std_long = np.std(prices)
     
     # Normalization.
     # Source paper equation 6, page 6
     y = x / price_std_short
     
     # Normalization #2.
     # Source paper equation 7, page 6
     z = y / price_std_long
     
     # Response function.
     u = (z * exp((-z**2) / 4)) / (sqrt(2) * exp(-1/2))
     signal[symbol] = sum(1/len(z) * u)
 
 if len(signal) == 0:
     self.Liquidate()
     return
             
 # Trade execution
 for symbol, weight in signal.items():
     w = (1/len(signal)) * (self.leverage * weight)
     self.SetHoldings(symbol, w)
     
# 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['settle'] = float(split[1])
 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