投资策略针对股价代码为10或11的美国股票,排除股价低于5美元的股票。数据来源包括OptionMetrics和CRSP。策略使用12个月的形成期,基于56个股票和期权特征构建期权回报因子。每月将期权排序为五分位数,计算等权高-低五分位数的回报。对于形成期正回报的因子采取多头头寸,负回报的因子采取空头头寸,组合每月再平衡,各因子等权重持有。

策略概述

投资范围包括股价代码为10或11的美国股票(避免在流动性较差的基础股票上进行期权交易,并排除上月收盘价低于5美元的股票)。 (主要数据来源是OptionMetrics IvyDB,提供所有美国单一股票期权的历史价格和插值波动率表面数据。基础股票的历史价格来自CRSP。每日无风险利率取自Kenneth French的在线数据图书馆。)

a) 使用12个月的形成期。 b) 基于各种股票和期权特征构建期权回报因子。具体来说,考虑一系列在现有文献中对delta对冲期权回报具有解释力的特征,以及易于获取的常见股票和期权特征。总共考虑56个特征,详见表2(附录A中提供所有特征的更多细节)。

通过在每个月底将所有可用期权排序为五分位数来计算每月因子回报。将因子回报定义为随后一个月的等权高-低五分位数回报。

TSFM策略如下: a) 对于具有正形成期回报的因子,采取多头头寸; b) 对于负回报的因子,采取空头头寸。

该策略每月再平衡(持有期为1个月),最终投资组合中的各个因子等权重。

策略合理性

已发布的研究为因子动量的存在及其对因子基础资产动量的解释力提供了证据:首先,时间序列和横截面因子动量策略是有利可图的。它们的回报与等权因子投资组合的回报不同,并且对Horenstein等人(2020年)的因子模型具有稳健性。其次,依赖一个月形成期的策略主要受因子自相关驱动。然而,形成期越长,高均值因子的回报及其持续变动作为动量驱动因素的重要性越高。第三,正如Ehsani和Linnainmaa(2022年)以及Arnott等人(2023年)所指出,动量效应在期权因子的最大特征值主成分中最为强烈。第四,延伸Heston等人(2022年)的发现至单个期权回报,我们发现期权层面的动量。跨越测试表明,期权因子动量涵盖了期权动量,而非反之。尽管这些结果与关注股票市场的研究相似,但仍存在一些显著差异。虽然期权因子的自相关性较高,但有些因子也展现出异常的均值回报和夏普比率,从而推动因子动量。然而,未来的研究仍建议分析对最佳期权投资组合的影响,因为目前仍缺乏充分的证据来得出明确的结论。

论文来源

Option Factor Momentum [点击浏览原文]

<摘要>

我们记录了在一组由每日delta对冲期权头寸的每月排序构建的56个期权因子中,横截面和时间序列动量的获利能力。期权因子的回报高度自相关,但具有较长形成期的策略的动量利润主要受持续不同的高均值回报驱动。动量效应在因子的最大主成分中最为强烈,与股票因子动量的发现一致。最后,我们发现了期权市场中的一种新动量形式:单一delta对冲期权回报的动量。期权因子动量完全涵盖了期权动量,而期权动量无法解释期权因子动量。我们的发现为驱动期权动量的渠道提供了见解,并对设计盈利的期权交易策略具有重要意义。

回测表现

年化收益率13.62%
波动率14.93%
Beta0.413
夏普比率0.91
索提诺比率N/A
最大回撤N/A
胜率77%

完整python代码

from AlgorithmImports import *
from pandas.core.frame import DataFrame
from typing import List, Dict
#endregion
class MultiRiskPremiaStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000)
        self.period:int = 12 * 21
        self.leverage:int = 3
        self.quantile:int = 5
        self.data:Dict[str, float] = {}
        self.equity_ids:List[str] = [
            '173',    # Volatility Term Structure Predicts Option Returns
            '20',     # Volatility Risk Premium Effect
            '216',    # Active Collar Strategy
            '233',    # Using Straddles to Trade on Earnings Announcements
            '237',    # Dispersion Trading
            '257',    # Cloning Hedge Fund Indexes
            '280',    # Trading the VIX Futures Roll and Volatility Premiums with VIX Options
            '329',    # Portfolio Hedging Using VIX Options
            '335',    # Cross-Sectional One-Month Equity ATM Straddle Trading Strategy
            '336',    # Cross-Sectional Six-Month Equity ATM Straddle Trading Strategy
            '337',    # Cross-Sectional Six- Minus One-Month Equity ATM Straddle Calendar Trading Strategy
            '338',    # Timing of Option Returns
            '347',    # Mispricing of Equity Options With Different Time To Maturity
            '349',    # Trading Options During Expiration Weekends
            '402',    # International Volatility Arbitrage
            '405',    # Using VIX to Time Options Writing
            '41',     # Turn of the Month in Equity Indexes
            '481',    # Holding Artificial VIX in a Portfolio
            '511',    # Cheap Options Are Expensive
            '599',    # Barbell Strategy
            '604',    # Reversal on Straddles
            '605',    # Momentum on Straddles
            '627',    # Hedging Portfolio
            '63',     # Trendfollowing Combined with Volatility Premium 
            '72',     # Combined Mean Reversion and Momentum in Foreign Exchange Markets
            '786',    # Option Trading and Returns versus the 52-Week High
            '787',    # Option Trading and Returns versus the 52-Week Low
            '855',    # Avoid Equity Bear Markets with a Market Timing Strategy
        ]
        for equity_id in self.equity_ids:
            data:Security = self.AddData(QuantpediaEquity, equity_id, Resolution.Daily)
            data.SetLeverage(self.leverage)
            data.SetFeeModel(CustomFeeModel())
            self.data[equity_id] = self.ROC(equity_id, self.period, Resolution.Daily)
        self.SetWarmUp(self.period)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.recent_month:int = -1
    
    def OnData(self, data: Slice) -> None:
        if self.IsWarmingUp:
            return
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        _last_update_date:Dict[str, datetime.date] = QuantpediaEquity.get_last_update_date()
        
        # calculate performance
        performance:Dict[str, float] = { x : self.data[x].Current.Value for x in self.data \
                    if self.data[x].IsReady and \
                    x in data and data[x] and \
                    _last_update_date[x] > self.Time.date() }
    
        long:List[str] = []
        short:List[str] = []
        
        # performance sorting
        if len(performance) >= self.quantile:
            sorted_by_perf:List[str] = sorted(performance.items(), key = lambda x: x[1], reverse = True)
            quantile:int = int(len(sorted_by_perf) / self.quantile)
            long = [x[0] for x in sorted_by_perf[:quantile]]
            short = [x[0] for x in sorted_by_perf[-quantile:]]
        # trade execution
        invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in long + short:
                self.Liquidate(symbol)
        
        for symbol in long:
            self.SetHoldings(symbol, 1 / len(long))
        for symbol in short:
            self.SetHoldings(symbol, -1 / len(short))
        
# Quantpedia strategy equity curve data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaEquity(PythonData):
    def GetSource(self, config:SubscriptionDataConfig, date:datetime, isLiveMode:bool) -> SubscriptionDataSource:
        return SubscriptionDataSource(f"data.quantpedia.com/backtesting_data/equity/quantpedia_strategies/925_related/{config.Symbol.Value}.csv", SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    _last_update_date:Dict[str, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[str, datetime.date]:
       return QuantpediaEquity._last_update_date
    def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLive: bool) -> BaseData:
        data:config = QuantpediaEquity()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split:List[str] = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data['close'] = float(split[1])
        data.Value = float(split[1])
        
        # store last update date
        if config.Symbol.Value not in QuantpediaEquity._last_update_date:
            QuantpediaEquity._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaEquity._last_update_date[config.Symbol.Value]:
            QuantpediaEquity._last_update_date[config.Symbol.Value] = data.Time.date()
        
        return data
        
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading