货币的趋势跟踪交易策略
登录后收藏学术论文
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