共动量策略
登录后收藏学术论文
Co-Momentum: Inferring Arbitrage Capital from Return Correlations
从收益相关性推断套利活 [点击查看论文]
- London School of Economics and Political Science
- Centre for Economic Policy Research
- ?Centre for Economic Policy Research (CEPR)
- ?London School of Economics & Political Science (LSE)
- ?London School of Economics
策略概要
该策略专注于纽约证券交易所、美国证券交易所和纳斯达克的股票,排除那些价格低于5美元或位于纽约证券交易所规模最小的十分位数的股票。每月,股票按其前12个月的回报(不包括最后一个月)进行排名,并分为十分位数。对于每个十分位数,计算52周回报的成对偏相关,控制Fama-French三个因子以消除共同风险因子效应。计算输家十分位数的平均相关性(ComomL)。仅当当月的ComomL处于历史最低五分之一时,才实施经典的动量策略(做多赢家,做空输家),以确保最佳交易条件。
II. 策略合理性
学术研究表明,价格动量的表现因投资的资本而异。当动量策略中的资本较低时,动量反映的是反应不足,导致短期价格持续而没有长期反转。相反,高套利资本会产生过度反应,即价格超调并最终在长期内反转。此外,拥挤的动量策略容易出现突然崩溃,如果套利者因追加保证金或业绩不佳而被强制撤回资本,从而引发快速平仓和重大的市场影响。这些动态凸显了监控动量策略中的资本流动以有效管理风险和回报的重要性。
回测表现
波动率12.18%
夏普比率0.38
索提诺比率0.106
胜率52%
完整 Python 代码
import itertools as it
import numpy as np
from AlgorithmImports import *
from pandas.core.frame import DataFrame
class ComomentumStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.long:List[Symbol] = []
self.short:List[Symbol] = []
market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
# Weekly price data.
self.data:Dict[Symbol, SymbolData] = {}
self.period:int = 52
self.quantile:int = 10
self.leverage:int = 3
self.min_share_price:float = 5.
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.fundamental_count:int = 500
self.fundamental_sorting_key = lambda x: x.DollarVolume
# Historical commonL values.
self.historical_CommonL:List[float] = []
self.min_historical_CommonL_period:int = 12
self.last_month:int = -1
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.schedule.on(self.date_rules.month_start(market),
self.time_rules.after_market_open(market),
self.selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
for stock in fundamental:
symbol:Symbol = stock.Symbol
# Store monthly price.
if symbol in self.data:
self.data[symbol].update(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and \
x.Price >= self.min_share_price and x.SecurityReference.ExchangeId in self.exchange_codes]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
performance:Dict[Symbol, float] = {}
for stock in selected:
symbol:Symbol = stock.Symbol
if symbol not in self.data:
self.data[symbol] = SymbolData(self.period)
history:DataFrame = self.History(symbol, self.period * 5, Resolution.Daily)
if history.empty:
self.Log(f"Not warmed up yet: {symbol}")
continue
closes:pd.Series = history.loc[symbol].close
for index, close in enumerate(closes):
if index % 5 == 0:
self.data[symbol].update(close)
if self.data[symbol].is_ready():
performance[symbol] = self.data[symbol].performance()
if len(performance) < self.quantile:
return Universe.Unchanged
# Performance sorting.
sorted_by_perf:List = sorted(performance.items(), key = lambda x: x[1], reverse = True)
quantile:int = int(len(sorted_by_perf) / self.quantile)
high_by_perf:List[Symbol] = [x[0] for x in sorted_by_perf[:quantile]]
low_by_perf:List[Symbol] = [x[0] for x in sorted_by_perf[-quantile:]]
decile_corr:List[float] = []
symbol_pairs:List = list(it.combinations(low_by_perf, 2))
for pair in symbol_pairs:
symbol1_weekly_closes:List[float] = [x for x in self.data[pair[0]]._price][:-1]
symbol2_weekly_closes:List[float] = [x for x in self.data[pair[1]]._price][:-1]
decile_corr.append(np.corrcoef(symbol1_weekly_closes, symbol2_weekly_closes)[0, 1])
if len(decile_corr) != 0:
commonL:float = np.average(decile_corr)
self.historical_CommonL.append(commonL)
# At least year of monthly ComomL data is ready.
if len(self.historical_CommonL) >= self.min_historical_CommonL_period:
bottom_commonL_quintile:float = np.percentile(self.historical_CommonL, 20)
if commonL < bottom_commonL_quintile:
# Trade classical momentum strategy.
self.long = high_by_perf
self.short = low_by_perf
return self.long + self.short
def OnData(self, slice: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# order execution
targets:List[PortfolioTarget] = []
for i, portfolio in enumerate([self.long, self.short]):
for symbol in portfolio:
if symbol in slice and slice[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
self.long.clear()
self.short.clear()
def selection(self) -> None:
self.selection_flag = True
class SymbolData():
def __init__(self, period: int):
self._price:RollingWindow = RollingWindow[float](period)
def is_ready(self) -> bool:
return self._price.IsReady
def update(self, close: float) -> None:
self._price.Add(close)
# Performance for previous 52 weeks, one month skipped.
def performance(self) -> float:
values = [x for x in self._price][4:]
return (values[3] / values[-1] - 1)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))