
“该策略采用Bakshi的无模型方法,根据高低隐含偏度构建多空商品投资组合,进行每日再平衡,并持有一个月(21个工作日)。“
资产类别: 差价合约、期货 | 地区: 全球 | 周期: 每日 | 市场: 大宗商品 | 关键词: 隐含偏度
I. 策略概要
投资标的包括8种商品:玉米、大豆、小麦、铜、白银、黄金、原油和天然气。该策略采用Bakshi的无模型方法,通过期货合约的一月期期权来估算隐含偏度。投资者根据隐含偏度的高低,将前25%的商品做多,后25%的商品做空,构建多空组合。每个组合包含两种等权重的做多商品期货合约和两种等权重的做空商品期货合约。投资组合每日再平衡,持有期为一个月(21个工作日),每个组合占总权重的1/21。
II. 策略合理性
该论文发现商品与衍生品市场(期权)之间存在套利机会,正隐含偏度与未来商品回报相关。这种关系与股票市场中观察到的知情交易和对冲策略一致。尽管预期正隐含偏度会导致负回报,但研究表明商品市场并非如此。基于隐含偏度的策略提供了最佳的风险回报权衡,与债券、股票和商品市场的风险相匹配。结果稳健且具有经济意义,动量和展期收益部分解释了这些发现。该策略优于所有基准投资组合,并且即使采用各种控制和投资组合选择措施,时间序列和横截面结果也保持一致。
III. 来源论文
Commodity Return Predictability: Evidence from Implied Variance, Skewness and their Risk Premia [点击查看论文]
- 芬塔(Marinela Adriana Finta)和奥尔内拉斯(Jose Renato Haas Ornelas),新加坡管理大学,巴西中央银行
<摘要>
本论文研究了已实现和隐含矩及其风险溢价(方差和偏度)对商品未来回报的作用。我们从高频和商品期货期权数据中估计这些矩,从而得出前瞻性指标。风险溢价计算为隐含矩与已实现矩之间的差值。我们从横截面和时间序列的角度强调了商品回报与隐含偏度之间的强烈正相关关系。此外,我们强调了偏度风险溢价的高表现。此外,我们表明它们的投资组合表现出最佳的风险回报权衡。我们的大部分结果对动量和展期收益等其他因素具有鲁棒性。


IV. 回测表现
| 年化回报 | 17.21% |
| 波动率 | 28% |
| β值 | -0.121 |
| 夏普比率 | 0.62 |
| 索提诺比率 | 0.043 |
| 最大回撤 | N/A |
| 胜率 | 48% |
V. 完整的 Python 代码
from AlgorithmImports import *
class ImpliedSkewnessStrategyInCommodities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.symbols = [] # storing commodities symbols
self.managed_queue = [] # storing parts of portfolio with their current holding period
self.contracts_expiry = {} # storing contracts expiry date under symbols
self.tickers_symbols = {} # storing commodities symbols under their tickers
self.futures = ['GLD', 'USO', 'UNG', 'SLV', 'CORN', 'WEAT', 'SOYB', 'CPER']
self.quantile = 4
for ticker in self.futures:
# subscribe to commodity future
security = self.AddEquity(ticker, Resolution.Minute)
# change normalization to raw to allow adding futures contracts
security.SetDataNormalizationMode(DataNormalizationMode.Raw)
# set fee model and leverage
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(5)
# get commodity symbol
symbol = security.Symbol
# store future symbol under future ticker
self.tickers_symbols[ticker] = symbol
# add commodity symbol to symbols, which can be traded in this strategy
self.symbols.append(symbol)
self.min_expiry = 25
self.max_expiry = 35
self.holding_period = 21 # holding each part of portfolio n days
self.current_day = -1
def OnData(self, data):
# rebalance daily
if self.current_day == self.Time.day:
return
self.current_day = self.Time.day
for symbol in self.symbols:
# subscribe to new contracts, because current ones has expiried
if symbol not in self.contracts_expiry or self.contracts_expiry[symbol] <= self.Time.date():
# subsribe to new itm and otm contracts
self.SubscribeOptionContracts(symbol)
# storing implied skewness for each commodity
implied_skewness = {}
for kvp in data.OptionChains:
chain = kvp.Value
contracts = [x for x in chain]
# check if there are enough contracts for option
if len(contracts) < 4:
continue
# get future symbol
future_symbol = self.tickers_symbols[chain.Underlying.Symbol.Value]
# get implied volatility for each contract
itm_put_iv, itm_call_iv, otm_put_iv, otm_call_iv = self.GetImpliedVolatilities(contracts)
# make sure, there is implied volatility for each contract
if itm_put_iv and itm_call_iv and otm_put_iv and otm_call_iv:
# calculate otm mean
otm_mean = (otm_put_iv + otm_call_iv) / 2
# calculate itm mean
itm_mean = (itm_put_iv + itm_call_iv) / 2
# calculate and store skewness
implied_skewness[future_symbol] = otm_mean - itm_mean
# make sure, there are enough symbols for quartile selection
if len(implied_skewness) < self.quantile:
# try to liquidate old part of portfolio
self.TradeAndLiquidate()
return
# quartile selection
quantile = int(len(implied_skewness) / self.quantile)
sorted_by_skewness = [x[0] for x in sorted(implied_skewness.items(), key=lambda item: item[1])]
# long top quartile
long = [x for x in sorted_by_skewness[-quantile:] if self.GetLastPrice(x, data)]
# short bottom quartile
short = [x for x in sorted_by_skewness[:quantile] if self.GetLastPrice(x, data)]
if len(long) != 0 and len(short) != 0:
# calculate long and short weight for new portfolio part
long_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(long)
short_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(short)
# create long and shory symbols_q
long_symbols_q = [(x, np.floor(long_w / self.GetLastPrice(x, data))) for x in long]
short_symbols_q = [(x, -np.floor(short_w / self.GetLastPrice(x, data))) for x in short]
# add new part of portfolio to managed_queue list
self.managed_queue.append(RebalanceQueueItem(long_symbols_q + short_symbols_q))
# trade new part of portfolio and try to liquidate old one
self.TradeAndLiquidate()
def SubscribeOptionContracts(self, symbol):
''' get itm and otm strike for specific symbol '''
''' then it filters itm and otm puts and calls '''
''' if there are enough itm and otm puts and calls this function subscribes one of their contracts based on expiry and store expiry date '''
# get all contracts for current commodity future
contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
# get current price for commodity future
underlying_price = self.Securities[symbol].Price
# get strikes from commodity future contracts
strikes = [i.ID.StrikePrice for i in contracts]
# check if there is at least one strike
if len(strikes) <= 0:
return
# in the money with 95%
itm_strike:float = min(strikes, key=lambda x: abs(x-(underlying_price*0.95)))
# out the money with 105%
otm_strike:float = min(strikes, key=lambda x: abs(x-(underlying_price*1.05)))
# filtred contracts based on option rights and strikes
itm_puts:list = self.FilterContracts(contracts, OptionRight.Put, itm_strike)
itm_calls:list = self.FilterContracts(contracts, OptionRight.Call, itm_strike)
otm_puts:list = self.FilterContracts(contracts, OptionRight.Put, otm_strike)
otm_calls:list = self.FilterContracts(contracts, OptionRight.Call, otm_strike)
# make sure there are enough contracts
if len(itm_puts) > 0 and len(itm_calls) > 0 and len(otm_puts) > 0 and len(otm_calls) > 0:
# sort by expiry
itm_put, itm_call, otm_put, otm_call = self.SortByExpiry(itm_puts, itm_calls, otm_puts, otm_calls)
# add contracts
for contract in [itm_put, itm_call, otm_put, otm_call]:
self.AddContract(contract)
# store expiry date
self.contracts_expiry[symbol] = itm_put.ID.Date.date()
def FilterContracts(self, contracts, option_right, strike):
''' filter contracts based on option_right and strike parameter from contracts parameter'''
# filter contracts based on option right and strike parameters
filtered_contracts:list = [i for i in contracts if i.ID.OptionRight == option_right and
i.ID.StrikePrice == strike and
self.min_expiry < (i.ID.Date - self.Time).days < self.max_expiry]
# return filtered contracts
return filtered_contracts
def SortByExpiry(self, itm_puts, itm_calls, otm_puts, otm_calls):
''' sort contracts based on expiry and returns contracts with nearest expiry date '''
# sort by expiry
itm_put = sorted(itm_puts, key = lambda x: x.ID.Date)[0]
itm_call = sorted(itm_calls, key = lambda x: x.ID.Date)[0]
otm_put = sorted(otm_puts, key = lambda x: x.ID.Date)[0]
otm_call = sorted(otm_calls, key = lambda x: x.ID.Date)[0]
return [itm_put, itm_call, otm_put, otm_call]
def AddContract(self, contract):
''' subcribe to contract, set price model and normalization mode '''
# add contract
option = self.AddOptionContract(contract, Resolution.Minute)
option.PriceModel = OptionPriceModels.CrankNicolsonFD()
option.SetDataNormalizationMode(DataNormalizationMode.Raw)
def GetImpliedVolatilities(self, contracts):
''' return implied volatility for itm_put, itm_call, otm_put and otm_call contract '''
itm_put_iv = None
itm_call_iv = None
otm_put_iv = None
otm_call_iv = None
# go through option contracts
for c in contracts:
# get underlying price of contract
underlying_price = self.Securities[c.UnderlyingSymbol].Price
if c.Right == OptionRight.Call and c.Strike < underlying_price:
# found itm call
itm_call_iv = c.ImpliedVolatility
elif c.Right == OptionRight.Call and c.Strike >= underlying_price:
# found otm call
otm_call_iv = c.ImpliedVolatility
elif c.Right == OptionRight.Put and c.Strike < underlying_price:
# found itm put
itm_put_iv = c.ImpliedVolatility
else:
# found otm put
otm_put_iv = c.ImpliedVolatility
# return implied volatility for each contract
return itm_put_iv, itm_call_iv, otm_put_iv, otm_call_iv
def GetLastPrice(self, symbol, data):
''' return symbol price from data object, or returns None if there isn't the price '''
# check if symbol has price in data object
if symbol in data and data[symbol]:
# return price
return data[symbol].Value
else:
# return None, if symbol doesn't have price in data object
return None
def TradeAndLiquidate(self):
''' handles daily rebalancing of portfolio parts '''
''' it trades new parts and liquidate parts, which has holding_period equal to self.holding_period '''
''' after liquidation these parts are removed from self.managed_queue '''
remove_portfolio_part = None
for portfolio_part in self.managed_queue:
# liquidate this part of portfolio
if portfolio_part.holding_period == self.holding_period:
for symbol, quantity in portfolio_part.symbol_q:
# liquidate symbol from this portfolio part
self.MarketOrder(symbol, -quantity)
# reinitialize remove_portfolio_part
remove_portfolio_part = portfolio_part
# trade new portfolio part
elif portfolio_part.holding_period == 0:
# store symbols, which were opened
opened_symbol_q = []
for symbol, quantity in portfolio_part.symbol_q:
if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:
# trade symbol
self.MarketOrder(symbol, quantity)
# store symbol in opened symbol_q
opened_symbol_q.append((symbol, quantity))
# change symbol_q of current portfolio part
portfolio_part.symbol_q = opened_symbol_q
# increase holding period of current portfolio part
portfolio_part.holding_period += 1
# in the end try to remove portfolio part from self.managed_queue
if remove_portfolio_part:
self.managed_queue.remove(remove_portfolio_part)
class RebalanceQueueItem():
def __init__(self, symbol_q):
# symbol/quantity collections
self.symbol_q = symbol_q
self.holding_period = 0
# custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))