
“该策略在多个资产类别中构建基于偏度的零成本投资组合,按偏度对资产进行排名,并将其等权重组合成一个全球偏度因子,并根据波动率进行调整。”
资产类别: 差价合约、期货 | 地区: 全球 | 周期: 每月 | 市场: 债券、大宗商品、外汇、股票 | 关键词: 偏度效应
I. 策略概要
该策略涉及19个股指期货、9个政府债券期货、9个货币远期和24个商品期货。每个月计算日回报,并使用皮尔逊矩偏度系数,基于12个月滚动窗口估计回报分布的偏度。为每个资产类别构建一个可交易的偏度零成本投资组合,根据偏度指标的横截面排名进行1美元多头和1美元空头投资。投资组合根据资产的偏度排名分配权重,负偏度最大的资产获得最大的正权重。通过将所有资产类别的偏度投资组合等权重组合,并将其波动率调整为10%,构建一个全球偏度因子。该策略旨在从多个市场资产回报的不对称性中获利。
II. 策略合理性
投资者对偏度的偏好促成了偏度风险溢价的出现,这可能源于对彩票式资产的非理性需求,从而推高其价格并降低预期回报。在期货或远期市场中,由于不适用卖空限制,这种动态可能看起来不太适用。然而,偏度偏好的一个合理原因是具有累积前景理论偏好的投资者对正偏度资产的过度需求。这些投资者过高地评估低概率事件,并由于出现巨额负回报的风险而避免卖空正偏度资产。这推高了正偏度资产的价格,导致其表现不佳。此外,选择性对冲策略,即投资者在最小化风险的同时最大化偏度,也促成了偏度风险溢价。在这种情况下,投资者不仅关注风险降低,还关注最大化预期回报中的偏度。这些动态,不受卖空限制的影响,进一步解释了偏度风险溢价在不同资产类别中存在和持续的原因。
III. 来源论文
Cross-Asset Skew [点击查看论文]
- 尼克·巴尔塔斯(Nick Baltas)和加布里埃尔·萨利纳斯(Gabriel Salinas),帝国理工商学院;高盛国际;得克萨斯教师退休系统
<摘要>
我们发现,已实现偏度是不同资产类别(即商品、政府债券、股指和货币)中一系列资产回报的重要指标。承担偏度风险在资产类别内部得到广泛补偿,但在资产类别之间得到更实质性的补偿。在1990年至2017年期间,这四类资产中,对负偏度最大(或正偏度最小)的资产持有多头头寸,对负偏度最小(或正偏度最大)的资产持有空头头寸的投资组合平均夏普比率为0.35。我们发现这些投资组合之间共同风险驱动因素的证据很少,以至于它们的组合从多元化中获得了 substantial 收益,夏普比率为0.72。这些模式不被其他已知的驱动回报的因素所 subsume,例如价值、动量或套利因素,因此,均值-方差有效多因素投资组合对偏度分配正权重。我们的结果对不同的偏度衡量方法和子样本仍然稳健。


IV. 回测表现
| 年化回报 | 3.5% |
| 波动率 | 4.86% |
| β值 | 0.142 |
| 夏普比率 | 0.72 |
| 索提诺比率 | 0.157 |
| 最大回撤 | N/A |
| 胜率 | 55% |
V. 完整的 Python 代码
import numpy as np
from scipy.stats import skew
from AlgorithmImports import *
class CrossAssetSkewnessEffect(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.leverage = 2
commodities = ["CME_S1", # Soybean Futures, Continuous Contract
"CME_W1", # Wheat Futures, Continuous Contract
"CME_SM1", # Soybean Meal Futures, Continuous Contract
"CME_BO1", # Soybean Oil Futures, Continuous Contract
"CME_C1", # Corn Futures, Continuous Contract
"CME_O1", # Oats Futures, Continuous Contract
"CME_LC1", # Live Cattle Futures, Continuous Contract
"CME_FC1", # Feeder Cattle Futures, Continuous Contract
"CME_LN1", # Lean Hog Futures, Continuous Contract
"CME_GC1", # Gold Futures, Continuous Contract
"CME_SI1", # Silver Futures, Continuous Contract
"CME_PL1", # Platinum Futures, Continuous Contract
"CME_CL1", # Crude Oil Futures, Continuous Contract
"CME_HG1", # Copper Futures, Continuous Contract
"CME_LB1", # Random Length Lumber Futures, Continuous Contract
# "CME_NG1", # Natural Gas (Henry Hub) Physical Futures, Continuous Contract
"CME_PA1", # Palladium Futures, Continuous Contract
"CME_RR1", # Rough Rice Futures, Continuous Contract
"ICE_CC1", # Cocoa Futures, Continuous Contract
"ICE_CT1", # Cotton No. 2 Futures, Continuous Contract
"ICE_KC1", # Coffee C Futures, Continuous Contract
"ICE_O1", # Heating Oil Futures, Continuous Contract
"ICE_OJ1", # Orange Juice Futures, Continuous Contract
"ICE_SB1", # Sugar No. 11 Futures, Continuous Contract
]
currencies = ["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
]
equities = ["ICE_DX1", # US Dollar Index Futures, Continuous Contract #1
"CME_NQ1", # E-mini NASDAQ 100 Futures, Continuous Contract #1
"EUREX_FDAX1", # DAX Futures, Continuous Contract #1
"CME_ES1", # E-mini S&P 500 Futures, Continuous Contract #1
"EUREX_FSMI1", # SMI Futures, Continuous Contract #1
"EUREX_FSTX1", # STOXX Europe 50 Index Futures, Continuous Contract #1
"LIFFE_FCE1", # CAC40 Index Futures, Continuous Contract #1
"LIFFE_Z1", # FTSE 100 Index Futures, Continuous Contract #1
"SGX_NK1", # SGX Nikkei 225 Index Futures, Continuous Contract #1
]
bonds = ["CME_TY1", # 10 Yr Note Futures, Continuous Contract #1
"CME_FV1", # 5 Yr Note Futures, Continuous Contract #1
"CME_TU1", # 2 Yr Note Futures, Continuous Contract #1
"ASX_XT1", # 10 Year Commonwealth Treasury Bond Futures, Continuous Contract #1
"ASX_YT1", # 3 Year Commonwealth Treasury Bond Futures, Continuous Contract #1
"EUREX_FGBL1", # Euro-Bund (10Y) Futures, Continuous Contract #1
"EUREX_FBTP1", # Long-Term Euro-BTP Futures, Continuous Contract #1
"EUREX_FGBM1", # Euro-Bobl Futures, Continuous Contract #1
"EUREX_FGBS1", # Euro-Schatz Futures, Continuous Contract #1
"SGX_JB1", # SGX 10-Year Mini Japanese Government Bond Futures
"LIFFE_R1" # Long Gilt Futures, Continuous Contract #1
"MX_CGB1", # Ten-Year Government of Canada Bond Futures, Continuous Contract #1
]
self.asset_classes = {}
self.asset_classes['commodities'] = commodities
self.asset_classes['currencies'] = currencies
self.asset_classes['equities'] = equities
self.asset_classes['bonds'] = bonds
self.data = {}
self.period = 12 * 21
for symbol in commodities + currencies + equities + bonds:
# Quantpedia #1 Contract.
data = self.AddData(QuantpediaFutures, symbol, Resolution.Daily)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(self.leverage)
self.data[symbol] = RollingWindow[float](self.period)
self.rebalance_flag: bool = False
self.Schedule.On(self.DateRules.MonthStart(commodities[0]), self.TimeRules.At(0, 0), self.Rebalance)
self.settings.minimum_order_margin_portfolio_percentage = 0.
def OnData(self, data):
# store daily prices
for asset_class in self.asset_classes:
for symbol in self.asset_classes[asset_class]:
if symbol in data and data[symbol]:
price = data[symbol].Value
self.data[symbol].Add(price)
if not self.rebalance_flag:
return
self.rebalance_flag = False
class_count = len(self.asset_classes)
weight = {}
for asset_class in self.asset_classes:
class_symbols = self.asset_classes[asset_class]
class_symbols_count = len(class_symbols)
skewness_data = {}
for symbol in class_symbols:
if self.data[symbol].IsReady:
if self.Securities[symbol].GetLastData() and self.Time.date() < QuantpediaFutures.get_last_update_date()[symbol]:
prices = np.array([x for x in self.data[symbol]])
returns = (prices[:-1]-prices[1:])/prices[1:]
if len(returns) == self.period-1:
skewness_data[symbol] = skew(returns)
if len(skewness_data) == 0: continue
# Sort by skewness return.
sorted_by_skewness = sorted(skewness_data.items(), key = lambda x: x[1], reverse = True)
positive_skewness = [x for x in sorted_by_skewness if x[1] > 0]
negative_skewness = [x for x in sorted_by_skewness if x[1] < 0]
# Ranking.
rank = {}
score = len(negative_skewness)
for symbol_data in negative_skewness:
rank[symbol_data[0]] = score
score -= 1
score = -1
for symbol_data in positive_skewness:
rank[symbol_data[0]] = score
score -= 1
# Weighting within class. - Skewness based.
total_items = len(positive_skewness + negative_skewness)
if total_items == 0: continue
partial_weight = 1 / sum([abs(score) for symbol, score in rank.items()])
# Weighting within portfolio. - Equally weighted.
for symbol, r in rank.items():
weight[symbol] = (1 / class_count) * (r * partial_weight)
if len(weight) == 0: return
# Trade execution
portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, self.leverage * w) for symbol, w in weight.items() if data.contains_key(symbol) and data[symbol]]
self.SetHoldings(portfolio, True)
def Rebalance(self):
self.rebalance_flag = 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