该策略每月卖出原油、黄金和天然气期货的一月期平值跨式期权(straddles),头寸按等权分配,并通过每日Delta对冲来保持主要敞口集中于市场波动率的变化。

I. 策略概述

该策略每月持续卖出原油、黄金和天然气期货的一月期平值跨式期权(straddles)。跨式期权由相同执行价和到期日的看跌期权与看涨期权组成。各头寸按等权分配,并通过每日Delta对冲(针对标的资产价格变动进行对冲)来保持对市场波动率变化的主要敞口。

II. 策略合理性

该策略通过卖出原油、黄金和天然气期货的跨式期权捕捉大宗商品中的波动率风险溢价。跨式期权每日进行Delta对冲,以确保主要敞口来自市场波动率变化。尽管方差互换(variance swaps)也能捕捉波动率风险溢价,并且自带Delta对冲功能,但该策略未涉及此类工具。

波动率风险溢价源于结构性失衡:对冲者和投机者对受限下行风险敞口的需求较高,而自然卖方的供应较少。这种失衡在危机期间尤为显著,因资本要求更加严格和杠杆容忍度降低。

III. 论文来源

The Volatility Risk Premium [点击浏览原文]

<摘要>

全球宏观经济不确定性上升及极端市场动荡频繁,促使投资者寻找传统资产类别以外的多元化投资机会。本文研究了旨在捕获时间内溢价的期权策略,以补偿市场波动率突然上升时可能出现的损失风险。研究表明,这些“波动率风险溢价”策略在1994年6月至2012年6月的14个期权市场中实现了可观的风险调整后收益,尤其在2008年危机后表现显著改善(见图1)。我们得出结论,波动率策略的风险回报权衡优于传统投资(如股票和债券),且与股票风险的相关性较低。因此,想要分散股票风险敞口的投资者应考虑将部分资产配置于波动率风险溢价策略。然而,成功实施需跨主要期权市场(如股票、利率、货币和大宗商品)实现多元化,并结合积极的风险管理和谨慎的规模控制。

IV. 回测表现

年化收益率6.1%
波动率5.2%
Beta0.109
夏普比率1.17
索提诺比率-0.299
最大回撤-8.5%
胜率45%

V. 完整python代码

from AlgorithmImports import *
class VolatilityRiskPremiuminCommodities(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.min_expiry = 30
        self.max_expiry = 45
        
        self.symbols = []
        self.contracts = {}
        
        tickers = ['USO', 'GLD', 'UNG']
        
        for ticker in tickers:
            # equity data
            data = self.AddEquity(ticker, Resolution.Daily)
            data.SetLeverage(10)
            data.SetFeeModel(CustomFeeModel())
            
            # change normalization to raw to allow adding etf contracts
            data.SetDataNormalizationMode(DataNormalizationMode.Raw)
            
            self.symbols.append(data.Symbol)
        self.settings.daily_precise_end_time = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self.last_day:int = -1
        self.trade_flag = False
    def OnData(self, data):
        # check once a day
        if self.Time.day == self.last_day:
            return
        self.last_day = self.Time.day
        
        # trade option contracts after selection and make sure, they are ready
        if self.trade_flag and data.OptionChains.Count != 0:
            # next rebalance will be after next selection
            self.trade_flag = False
            
            # sell atm straddle of subscribed contracts
            for symbol, contract_obj in self.contracts.items():
                # get call and put contract
                call, put = contract_obj.contracts
                
                # get underlying price
                underlying_price = contract_obj.underlying_price
                
                options_q = int((self.Portfolio.TotalPortfolioValue / len(self.symbols)) / (underlying_price * 100))
                
                self.Sell(call, options_q)
                self.Sell(put, options_q)
                
                # delta hedge
                self.MarketOrder(symbol, options_q*50)
        # check contracs expiration
        for symbol in self.symbols:
            # close trade once option is 
            if symbol in self.contracts and self.contracts[symbol].expiry_date-timedelta(days=1) <= self.Time.date():
                # liquidate expired contracts
                for contract in self.contracts[symbol].contracts:
                    self.Liquidate(contract)
                self.Liquidate(symbol)
                
                # remove Contracts object for current symbol
                del self.contracts[symbol]
        
        # rebalance, when all option contracts expiried
        if len(self.contracts) == 0:
            self.Liquidate()
            
            # subscribe to new option contracts
            for symbol in self.symbols:
                # get all contracts for current etf
                contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
                # get current price for etf
                underlying_price = self.Securities[symbol].Price
                
                # get strikes from etf contracts
                strikes = [i.ID.StrikePrice for i in contracts]
                
                # can't filter contracts, if there isn't any strike price
                if len(strikes) <= 0:
                    continue
                
                # filter calls and puts contracts with one month expiry
                calls, puts = self.FilterContracts(strikes, contracts, underlying_price)
                
                # make sure, there is at least one call and put contract
                if len(calls) and len(puts):
                    # sort by expiry
                    call = sorted(calls, key = lambda x: x.ID.Date)[0]
                    put = sorted(puts, key = lambda x: x.ID.Date)[0]
                    
                    subscription = self.SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(call.Underlying)
                    
                    # make sure put and call contract subscription is valid
                    if subscription:
                        # add call contract
                        self.AddContract(call)
                        # add put contract
                        self.AddContract(put)
                        
                        # retrieve expiry date for contracts
                        expiry_date = call.ID.Date.date() if call.ID.Date.date() <= put.ID.Date.date() else put.ID.Date.date()
                        # store contracts with expiry date under etf symbol
                        self.contracts[symbol] = Contracts(expiry_date, underlying_price, [call, put])
            
            # strategy sells subscribed contracts on next day
            self.trade_flag = True
        
    def FilterContracts(self, strikes, contracts, underlying_price):
        ''' filter call and put contracts from contracts parameter '''
        ''' return call and put contracts '''
        
        # Straddle
        call_strike:float = min(strikes, key=lambda x: abs(x-underlying_price))
        put_strike = call_strike
        
        calls = [] # storing call contracts
        puts = [] # storing put contracts
        
        for contract in contracts:
            # check if contract has one month expiry
            if self.min_expiry < (contract.ID.Date - self.Time).days < self.max_expiry:
                # check if contract is call
                if contract.ID.OptionRight == OptionRight.Call and contract.ID.StrikePrice == call_strike:
                    calls.append(contract)
                # check if contract is put
                elif contract.ID.OptionRight == OptionRight.Put and contract.ID.StrikePrice == put_strike:
                    puts.append(contract)
        
        # return filtered calls and puts with one month expiry
        return calls, puts
        
    def AddContract(self, contract):
        ''' subscribe option contract, set price mondel and normalization mode '''
        option = self.AddOptionContract(contract, Resolution.Daily)
        option.PriceModel = OptionPriceModels.CrankNicolsonFD()
class Contracts():
    def __init__(self, expiry_date, underlying_price, contracts):
        self.expiry_date = expiry_date
        self.underlying_price = underlying_price
        self.contracts = contracts
           
# custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))




发表评论

了解 Quant Buffet 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读