
“每周交易45种货币对,基于相关股票指数的12-1动量,在等权重投资组合中做多动量较高的基础货币,做空动量较低的报价货币。”
资产类别: 差价合约、远期、期货 | 地区: 全球 | 周期: 每周 | 市场: 外汇 | 关键词: 动量、溢出效应
I. 策略概要
投资范围包括澳元、加元、瑞郎、欧元、英镑、日元、新西兰元、挪威克朗、瑞典克朗和美元之间的45种货币对。每种货币都与其各自的股票指数相关联(例如,澳元与ASX,美元与S&P500)。该策略比较每对货币相关股票指数的12-1动量(过去12个月的回报,不包括最近一个月)。如果基础货币的股票指数显示出比报价货币指数更高的动量,则采取多头头寸,否则采取空头头寸。投资组合等权重,每周重新平衡,利用股票指数动量作为货币对表现的预测因子。
II. 策略合理性
该论文证明了溢出效应的存在,并且可以作为一种有利可图的交易策略加以利用。它强调该策略对回溯期具有鲁棒性,并且独立于利差、动量、反转、趋势跟踪和价值等既定因素。研究表明,货币中的溢出动量不能用这些因素来解释。一个可能的解释是,一个国家股票市场的出色表现吸引了外国投资者,增加了对股票和该国货币的需求,从而导致升值。这一新颖的见解强调了股票市场表现对货币估值和交易策略的独特影响。
III. 来源论文
Do Equities Spill Over to Currencies? [点击查看论文]
- Philippe Declerck,汇丰环球资产管理
<摘要>
我们记录到股票指数会溢出到货币:基于股票回报的横截面动量信号可以帮助在货币领域构建投资策略。与动量一样,这种溢出效应在短期/中期回溯期内往往效果更好,但溢出似乎不仅仅是一种动量现象。溢出对信号和投资组合构建修改也具有鲁棒性。


IV. 回测表现
| 年化回报 | 2.2% |
| 波动率 | 7.1% |
| β值 | 0.116 |
| 夏普比率 | 0.23 |
| 索提诺比率 | N/A |
| 最大回撤 | -25.2% |
| 胜率 | 43% |
V. 完整的 Python 代码
from AlgorithmImports import *
#endregion
class EquityMomentumSpillovertoCurrencies(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2007, 1, 1)
self.SetCash(100000)
# Country symbol and currency future symbol.
self.symbols = {
"CME_CD1" : "LIFFE_FCE1", # Canadian Dollar Futures, Continuous Contract #1
"CME_SF1" : "EUREX_FSMI1", # Swiss Franc Futures, Continuous Contract #1
"CME_EC1" : "EUREX_FSTX1", # Euro FX Futures, Continuous Contract #1
"CME_BP1" : "LIFFE_Z1", # British Pound Futures, Continuous Contract #1
"CME_JY1" : "SGX_NK1", # Japanese Yen Futures, Continuous Contract #1
}
self.period = 12 * 21
self.pairs = []
self.data = {} # momentum data
self.max_missing_days = 5
for i, curr_symbol1 in enumerate(self.symbols):
# Equity index futures data.
index_symbol = self.symbols[curr_symbol1]
self.AddData(QuantpediaFutures, index_symbol, Resolution.Daily)
self.data[index_symbol] = RollingWindow[float](self.period)
# Currency futures data.
if curr_symbol1 != "": # except US dollar
data = self.AddData(QuantpediaFutures, curr_symbol1, Resolution.Daily)
data.SetLeverage(20)
data.SetFeeModel(CustomFeeModel())
else:
continue
for j, curr_symbol2 in enumerate(self.symbols):
if j <= i: continue
self.pairs.append((curr_symbol1, curr_symbol2))
def OnData(self, data):
# store daily equity index data
for curr_symbol in self.symbols:
index_symbol = self.symbols[curr_symbol]
if curr_symbol in data and index_symbol in data and data[curr_symbol] and data[index_symbol]:
index_price = data[index_symbol].Value
self.data[index_symbol].Add(index_price)
# weekly rebalance
if self.Time.date().weekday() != 3:
return
# currency position
curr_position = {}
for pair in self.pairs:
eq_symbol1 = self.symbols[pair[0]]
eq_symbol2 = self.symbols[pair[1]]
if not all(self.Securities[x].GetLastData() and (self.Time.date() - self.Securities[x].GetLastData().Time.date()).days <= self.max_missing_days for x in [eq_symbol1, eq_symbol2, pair[0], pair[1]]):
continue
# calculate equity index momentum
if self.data[eq_symbol1].IsReady and self.data[eq_symbol2].IsReady:
eq_momentum1 = self.data[eq_symbol1][21] / self.data[eq_symbol1][self.period-1] - 1
eq_momentum2 = self.data[eq_symbol2][21] / self.data[eq_symbol2][self.period-1] - 1
if pair[0] not in curr_position:
curr_position[pair[0]] = 0
if pair[1] not in curr_position:
curr_position[pair[1]] = 0
# add long pair position
if eq_momentum1 > eq_momentum2:
curr_position[pair[0]] += 1
curr_position[pair[1]] -= 1
# add short pair position
else:
curr_position[pair[0]] -= 1
curr_position[pair[1]] += 1
if len(curr_position) != 0:
futures_invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for currency_future in futures_invested:
if currency_future not in curr_position:
self.Liquidate(currency_future)
total_position = sum([abs(x[1]) for x in curr_position.items()])
for currency_future, country_signal in curr_position.items():
self.SetHoldings(currency_future, country_signal)
else:
self.Liquidate()
# 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):
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])
return data