Quant Buffet放轻松,别过度思虑

商品中的隐含偏度策略

登录后收藏

学术论文

Commodity Return Predictability: Evidence from Implied Variance, Skewness and their Risk Premia

作者Marinela Adriana Finta; José Renato Haas Ornelas

机构
  • SGSingapore Management University
  • BRCentral Bank of Brazil
  • ?Banco Central do Brasil

策略概要

投资标的包括8种商品:玉米、大豆、小麦、铜、白银、黄金、原油和天然气。该策略采用Bakshi的无模型方法,通过期货合约的一月期期权来估算隐含偏度。投资者根据隐含偏度的高低,将前25%的商品做多,后25%的商品做空,构建多空组合。每个组合包含两种等权重的做多商品期货合约和两种等权重的做空商品期货合约。投资组合每日再平衡,持有期为一个月(21个工作日),每个组合占总权重的1/21。

II. 策略合理性

该论文发现商品与衍生品市场(期权)之间存在套利机会,正隐含偏度与未来商品回报相关。这种关系与股票市场中观察到的知情交易和对冲策略一致。尽管预期正隐含偏度会导致负回报,但研究表明商品市场并非如此。基于隐含偏度的策略提供了最佳的风险回报权衡,与债券、股票和商品市场的风险相匹配。结果稳健且具有经济意义,动量和展期收益部分解释了这些发现。该策略优于所有基准投资组合,并且即使采用各种控制和投资组合选择措施,时间序列和横截面结果也保持一致。

回测表现

波动率28%
夏普比率0.62
索提诺比率0.043
胜率48%

完整 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"))