
“该策略通过每日动量信号交易G10货币,在趋势为正时建立多头仓位(买入),在趋势为负时建立空头仓位(卖出)。交易仅限于每月的20日至29日,并在投资组合中对所有货币进行等权分配。”
资产类别:差价合约(CFDs)、期货 | 地域:全球 | 频率:每日 | 市场:外汇 | 关键词:动量、季节性
I. 策略概述
该策略利用每日动量信号交易G10货币。在趋势为正时建立多头仓位(买入),在趋势为负时建立空头仓位(卖出)。交易仅限于每月的最后三分之一时间(20日至29日),并将货币在投资组合中等权分配。具体操作上,通过比较每日汇率判断趋势信号:当天汇率高于前一天时视为正趋势,反之为负趋势。根据每日更新的趋势信号,策略会在指定交易窗口内调整仓位。
II. 策略合理性
研究表明,动量交易策略表现的季节性与外汇收益条件波动率的季节性密切相关。这种波动率动态的季节性又可能与美国宏观经济公告的时间分布密切相关,因为这些公告通常集中在每月的特定几天。
III. 论文来源
Day-of-the-Month Effects in the Performance of Momentum Trading Strategies in the Foreign Exchange Market [点击浏览原文]
- Harris, Stoja, Yilmaz
<摘要>
本文记录了外汇市场中动量策略表现的显著月内效应。研究发现,这种交易策略表现的季节性可以通过外汇收益条件波动率及其波动率的季节性变化来解释。一个基于条件波动率和波动率的两因素模型可以解释多达70%的夏普比率月内波动。此外,我们进一步发现,这种波动率的季节性与美国宏观经济新闻发布的时间分布密切相关,而这些公告通常集中在每月的特定时间。

IV. 回测表现
| 年化收益率 | 3.5% |
| 波动率 | 10% |
| Beta | -0.012 |
| 夏普比率 | 0.35 |
| 索提诺比率 | -0.707 |
| 最大回撤 | N/A |
| 胜率 | 49% |
V. 完整python代码
from AlgorithmImports import *
class FXMomentumSeasonality(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbols = [
'CME_AD1', # Australian Dollar Futures, Continuous Contract #1
'CME_CD1', # Canadian Dollar Futures, Continuous Contract #1
'CME_SF1', # Swiss Franc Futures, Continuous Contract #1
'CME_EC1', # Euro FX Futures, Continuous Contract #1
'CME_BP1', # British Pound Futures, Continuous Contract #1
'CME_JY1', # Japanese Yen Futures, Continuous Contract #1
'CME_NE1', # New Zealand Dollar Futures, Continuous Contract #1
'CME_MP1' # Mexican Peso Futures, Continuous Contract #1
]
self.current_prices = {}
self.yesterday_prices = {}
# Momentum strategy is traded only during the last 1/3 of each month (days 20-29).
self.start_day = 20
self.end_day = 29
leverage: int = 5
for currency_future in self.symbols:
security = self.AddData(QuantpediaFutures, currency_future, Resolution.Daily)
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(leverage)
self.current_prices[currency_future] = 0
self.yesterday_prices[currency_future] = 0
self.settings.minimum_order_margin_portfolio_percentage = 0.
self.settings.daily_precise_end_time = False
def OnData(self, data):
custom_data_last_update_date: Dict[str, datetime.date] = QuantpediaFutures.get_last_update_date()
# Storing daily data about future currencies
for symbol in self.symbols:
if self.securities[symbol].get_last_data() and self.time.date() > custom_data_last_update_date[symbol]:
self.liquidate()
return
if symbol in data:
if data[symbol]:
price = data[symbol].Value
if price != 0:
self.yesterday_prices[symbol] = self.current_prices[symbol]
self.current_prices[symbol] = price
long = []
short = []
if self.start_day <= self.Time.day <= self.end_day: # Momentum strategy is traded only during the last 1/3 of each month (days 20-29).
for symbol in self.symbols:
if self.current_prices[symbol] > self.yesterday_prices[symbol]:
long.append(symbol)
else:
short.append(symbol)
targets: List[PortfolioTarget] = []
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
if symbol in data and data[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
# 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['back_adjusted'] = float(split[1])
data['spliced'] = float(split[2])
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