“该策略涵盖27种商品期货,数据来自彭博社。便利收益风险(CYR)基于前两个和第二、第三期货合约的隐含便利收益,通过计算月度波动率得出CYR信号。我们根据过去12个月的便利收益波动率差的移动平均对商品排序,形成“高”和“低”投资组合。策略执行“高-低”套利:对便利收益较高的商品做多,对较低的商品做空。每个投资组合中的商品均等加权,投资组合每月再平衡。”
资产类别:差价合约,期货 | 地区:美国 | 频率:每月 | 市场:大宗商品 | 关键词:便利收益,大宗商品
策略概述
投资宇宙包括27种商品期货。(数据集可从彭博社获取。)
便利收益风险(CYR)的计算过程如下:
- 对于每个商品市场,计算由
- 为了简化说明,分别将这两个量称为第一个和第二个便利收益估计。
- 然后,在每个月结束时,使用该月的所有日常数据计算这两个便利收益序列的月度波动率。
- 最后,获得CYR信号,即前12个月第一和第二便利收益序列波动率之差的移动平均。
我们按每个月结束时的CYR对27种商品进行排序,形成一个“高”投资组合,包含便利收益大于中位数的商品,以及一个“低”投资组合,包含剩余商品。 因此,我们现在有了根据便利收益风险排序的投资组合。
最终投资组合执行“高-低”(多空)套利策略:投资者在高投资组合中的商品期货上做多(买入),在低投资组合中的商品期货上做空(卖出)。
每个投资组合中的商品均等加权,最终投资组合每月进行再平衡。
策略合理性
作者量化了与个别商品市场相关的便利收益风险,因此提出了一个新变量,称为便利收益风险(CYR)。现有的商品因子,例如套利、动量、基差动量,以及显著的宏观经济变量,并不能完全解释便利收益风险策略的表现。应将其视为动量、套利和对冲压力、特质波动性以及偏斜信号策略的良好替代方案。交易成本不会侵蚀基于CYR的策略的盈利能力,并且这些策略经受住了一系列稳健性检验。
论文来源
Convenience Yield Risk [点击浏览原文]
- Marcel Prokopczuk, Lazaros Symeonidis, Chardin Wese Simen, Robert Wichmann, 汉诺威大学经济与管理学院;雷丁大学ICMA中心,埃塞克斯大学商学院,利物浦大学管理学院,雷丁大学ICMA中心
<摘要>
这篇论文开发了一个框架来量化每个商品期货市场固有的便利收益风险(CYR)。通过实施我们的方法,我们证明了我们的新型CYR指标对未来商品收益具有信息价值。在面板回归中,CYR以正号预测未来收益。从经济角度看,在便利收益信号高于中位数的商品市场开立多头头寸,并卖出其余商品的策略每年平均收益为6.93%。CYR策略的表现不能通过对现有商品策略或捕捉投资机会变化的其他变量的暴露来解释。


回测表现
| 年化收益率 | 6.93% |
| 波动率 | 15.07% |
| Beta | -0.037 |
| 夏普比率 | 0.46 |
| 索提诺比率 | -0.112 |
| 最大回撤 | N/A |
| 胜率 | 50% |
完整python代码
from AlgorithmImports import *
import data_tools
import numpy as np
from typing import List, Dict, Tuple
# endregion
class ConvenienceYieldRiskFactorPredictsCommodityFuturesReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2008, 1, 1)
self.SetCash(100000)
self.period:int = 12
self.filter_period:int = 182
self.tickers:dict[str, str] = {
"CME_S1" : Futures.Grains.Soybeans,
"CME_W1" : Futures.Grains.Wheat,
"CME_SM1" : Futures.Grains.SoybeanMeal,
"CME_BO1" : Futures.Grains.SoybeanOil,
"CME_C1" : Futures.Grains.Corn,
"CME_O1" : Futures.Grains.Oats,
"CME_LC1" : Futures.Meats.LiveCattle,
"CME_FC1" : Futures.Meats.FeederCattle,
"CME_LN1" : Futures.Meats.LeanHogs,
"CME_GC1" : Futures.Metals.Gold,
"CME_SI1" : Futures.Metals.Silver,
"CME_PL1" : Futures.Metals.Platinum,
"CME_HG1" : Futures.Metals.Copper,
"CME_LB1" : Futures.Forestry.RandomLengthLumber,
"CME_NG1" : Futures.Energies.NaturalGas,
"CME_PA1" : Futures.Metals.Palladium,
"CME_CU1" : Futures.Energies.ChicagoEthanolPlatts,
"CME_DA1" : Futures.Dairy.ClassIIIMilk,
"ICE_CC1" : Futures.Softs.Cocoa,
"ICE_CT1" : Futures.Softs.Cotton2,
"ICE_KC1" : Futures.Softs.Coffee,
"ICE_O1" : Futures.Energies.HeatingOil,
"ICE_OJ1" : Futures.Softs.OrangeJuice,
"ICE_SB1" : Futures.Softs.Sugar11CME,
}
self.leverage:int = 2
self.data:Dict[Symbol, Tuple[float, float]] = {}
self.cyr_signal:Dict[Symbol, RollingWindow] = {}
self.futures_data:dict[Symbol, data_tools.FuturesData] = {}
for qp_ticker, qc_ticker in self.tickers.items():
# subscribe Quantpedia data
security:Security = self.AddData(data_tools.QuantpediaFutures, qp_ticker, Resolution.Daily)
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
qp_symbol:Symbol = security.Symbol
# QC futures
future:Future = self.AddFuture(qc_ticker, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.Raw)
future.SetFilter(0, self.filter_period)
future_symbol:str = future.Symbol
self.futures_data[future_symbol] = qp_symbol
self.recent_month:int = -1
def OnData(self, data:Slice):
qp_custom_data_last_update_date:Dict[Symbol, datetime.date] = data_tools.QuantpediaFutures._last_update_date
if all([self.Securities[x].GetLastData() for x in list(self.futures_data.keys())]) and any([self.Time.date() >= qc_custom_data_last_update_date[x] for x in qc_custom_data_last_update_date]):
self.Liquidate()
return
# save daily data
for contract_symbol, chain in data.FutureChains.items():
if len([i for i in chain]) >= 3:
sorted_by_date:List[Symbol] = sorted(chain, key=lambda x: x.Expiry)
first_convenience_yield:float = (365 * (sorted_by_date[0].LastPrice - sorted_by_date[1].LastPrice)) / ((sorted_by_date[1].Expiry - self.Time).days - (sorted_by_date[0].Expiry - self.Time).days)
second_convenience_yield:float = (365 * (sorted_by_date[1].LastPrice - sorted_by_date[2].LastPrice)) / ((sorted_by_date[2].Expiry - self.Time).days - (sorted_by_date[1].Expiry - self.Time).days)
if contract_symbol not in self.data:
self.data[contract_symbol] = []
self.data[contract_symbol].append((first_convenience_yield, second_convenience_yield))
# monthly rebalance
if self.Time.month == self.recent_month:
return
self.recent_month = self.Time.month
if len(self.data) == 0:
self.Liquidate()
return
# save convenience yield risk values
for contract_symbol, cyr_values in self.data.items():
if len(cyr_values) != 0:
first_std:float = np.std([i[0] for i in cyr_values])
second_std:float = np.std([i[1] for i in cyr_values])
if contract_symbol not in self.cyr_signal:
self.cyr_signal[contract_symbol] = RollingWindow[float](self.period)
self.cyr_signal[contract_symbol].Add(first_std - second_std)
self.data.clear()
if len(self.cyr_signal) == 0:
self.Liquidate()
return
cyr_mean:Dict[Symbol, float] = {}
# mean of 12 months convenience yield risk values
for contract_symbol, cyr_signals in self.cyr_signal.items():
if cyr_signals.IsReady:
if contract_symbol not in cyr_mean:
cyr_mean[contract_symbol] = np.mean(list(cyr_signals))
# sort and divide
if len(cyr_mean) != 0:
sorted_cyr:List[Symbol] = sorted(cyr_mean.items(), key=lambda x:x[1])
CYR_median:float = np.median([i[1] for i in sorted_cyr])
high:List[Symbol] = [symbol for symbol, cyr_value in sorted_cyr if cyr_value > CYR_median]
low:List[Symbol] = [symbol for symbol, cyr_value in sorted_cyr if cyr_value <= CYR_median]
# trade execution
invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in high + low:
self.Liquidate(symbol)
for symbol in high:
if self.futures_data[symbol] in data and data[self.futures_data[symbol]]:
self.SetHoldings(self.futures_data[symbol], 1 / len(high))
for symbol in low:
if self.futures_data[symbol] in data and data[self.futures_data[symbol]]:
self.SetHoldings(self.futures_data[symbol], -1 / len(low))
