该策略涵盖NYSE和Amex的股票,排除纳斯达克股票,数据来自CRSP和COMPUSTAT。首先,根据日均换手率计算流动性,并将高于中位数的股票分类为高流动性股票。其次,按机构持股比例(IO)分为三组,排除不属于下三分位(低IO)的股票。使用一年内单日最大回报(MAX)作为彩票效应指标。交易策略为每年12月根据MAX分类股票,做多低彩票效应股票,做空高彩票效应股票,组合按市值加权并每年重平衡。

策略概述

投资范围包括美国纽约证券交易所(NYSE)和美国证券交易所(Amex)的股票,股票代码为10和11,不包括纳斯达克股票。(数据可从证券价格研究中心(CRSP)和COMPUSTAT获取的股票会计信息中获得。)

首先,从股票样本中得出以下两个预过滤条件:

<交易执行>

每年12月,股票根据所选彩票效应指标(即最大回报MAX)独立分类至不同投资组合中。 创建差异组合(DIFF):做空(卖出)高彩票效应的股票,做多(买入)低彩票效应的股票。彩票效应通过所有考虑股票的最大回报中位数来定义。

所有投资组合按市值加权,每年重平衡一次。

策略合理性

该论文的结果与Shleifer和Summers(1990)以及Shleifer和Vishny(1997)的观点一致,即流动性交易者是因基本面信息、对冲或流动性冲击以外的原因进行交易的噪音交易者,他们对市场定价效率的问题缺乏认识。然而,作者发现他们的结果与标准市场微观结构模型的预测相反,后者认为流动性可以减少知情交易者的交易成本,促进信息的融入股票价格。本文对此主题的现有文献作出了重要的补充。

论文来源

Liquidity, Favorite-Longshot Bias, and the Return of Lottery-Like Stocks [点击浏览原文]

<摘要>

我们记录了新的结果,发现彩票效应(类似彩票的股票回报较低)在高流动性股票中更为显著。我们认为噪音交易者主导了流动性较高的彩票类股票的交易,正如套利限制文献中所描述的那样,这些交易者被彩票类股票的高头奖概率所吸引,并犯下了偏爱长尾的偏见,即投资者高估了低概率事件的发生概率。这种高估并非像前景理论所建议的那样由偏好驱动,更可能是由估计误差引发的。

回测表现

年化收益率10.56%
波动率9.85%
Beta-0.259
夏普比率1.07
索提诺比率N/A
最大回撤N/A
胜率50%

完整python代码

from AlgorithmImports import *
from pandas.core.frame import DataFrame
from dateutil.relativedelta import relativedelta
# endregion
class InfluenceofLiquidityInstitutionalOwnershipLotteryEffectonStocks(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2004, 1, 1)
        self.SetCash(100000)
        
        data_delay_months:int = 3
        file_contents:str = self.Download('data.quantpedia.com/backtesting_data/economic/institutional_ownership/institutional_ownership_in_percents.csv')
        lines:List[str] = file_contents.split('\r\n')
        self.tickers:List[str] = lines[0].split(',')[1:]
        dict_list:List[Dict[str, float]] = []
        
        for line in lines[1:]:
            line_split:List[str] = line.split(',')
            date = (datetime.strptime(line_split[0], "%Y-%m-%d") + relativedelta(months=data_delay_months)).date()
            temp_dict:Dict[str, float] = { 'date' : date }
            for i in range(1, len(line_split)):
                ticker:str = self.tickers[i-1]
                temp_dict[ticker] = float(line_split[i]) if line_split[i] != '' else 0.
            
            dict_list.append(temp_dict)
        
        io_df:DataFrame = pd.DataFrame(dict_list, columns=['date'] + self.tickers)
        self.io_df:DataFrame = io_df.set_index('date')
        self.leverage:int = 3
        self.selection_month:int = 12
        self.period:int = 12 * 21
        self.quantile:int = 3
        self.price_data:Dict[Symbol, RollingWindow] = {}
        self.weight:Dict[Symbol, float] = {}
        self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.selection_flag:bool = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.AfterMarketOpen(self.market), self.Selection)
    def OnSecuritiesChanged(self, changes:SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
    def CoarseSelectionFunction(self, coarse:List[CoarseFundamental]) -> List[Symbol]:
        # store daily stock prices
        for stock in coarse:
            symbol:Symbol = stock.Symbol.Value
            if symbol in self.price_data:
                self.price_data[symbol].Add(stock.AdjustedPrice)
        # monthly selection
        if not self.selection_flag:
            return Universe.Unchanged
        
        selection:List[Symbol] = [x.Symbol for x in coarse if x.HasFundamentalData and x.Symbol.Value in list(self.io_df.columns)]
        # warmup price rolling windows
        for symbol in selection:
            if symbol in self.price_data:
                continue
            
            self.price_data[symbol] = RollingWindow[float](self.period)
           
            history:DataFrame = self.History(symbol, self.period, Resolution.Daily)
            if history.empty:
                self.Log(f"Not enough data for {symbol} yet.")
                continue
            closes:pd.Series = history.loc[symbol].close
            for time, close in closes.iteritems():
                self.price_data[symbol].Add(close)
        return [x for x in selection if self.price_data[x].IsReady]
    def FineSelectionFunction(self, fine:List[FineFundamental]) -> List[Symbol]:
        stock_by_ticker:Dict[str, FineFundamental] = { x.Symbol.Value : x for x in fine }
        # filter stocks
        if not self.io_df.empty and len(stock_by_ticker) != 0:
            last_io_values_sorted:DataFrame = self.io_df[self.io_df.index <= self.Time.date()]
            if len(last_io_values_sorted) != 0:
                last_io_values_sorted:pd.Series = last_io_values_sorted.iloc[-1].sort_values(ascending=False)
                last_io_values_sorted = last_io_values_sorted[last_io_values_sorted != 0]
                quantile:int = len(last_io_values_sorted) // self.quantile
                bottom_io_tickers:List[str] = list(last_io_values_sorted[-quantile:].index)
                bottom_io_MAX:Dict[FineFundamental, float] = { stock_by_ticker[x] : max(np.array(list(self.price_data[stock_by_ticker[x].Symbol]))[:-1] / np.array(list(self.price_data[stock_by_ticker[x].Symbol]))[1:] - 1) for x in bottom_io_tickers if x in stock_by_ticker }
                MAX_median:float = np.median(list(bottom_io_MAX.values()))
                
                long:List[FineFundamental] = [stock for stock, MAX in bottom_io_MAX.items() if MAX <= MAX_median]
                short:List[FineFundamental] = [stock for stock, MAX in bottom_io_MAX.items() if MAX > MAX_median]
                # value weighting
                market_cap_long:float = sum([x.MarketCap for x in long])
                market_cap_short:float = sum([x.MarketCap for x in short])
                for stock in long:
                    self.weight[stock.Symbol] = stock.MarketCap / market_cap_long
                for stock in short:
                    self.weight[stock.Symbol] = -stock.MarketCap / market_cap_short
        return list(self.weight.keys())
    def OnData(self, data: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        # trade execution
        invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
        for symbol in invested:
            if symbol not in self.weight:
                self.Liquidate(symbol)
        for symbol, w in self.weight.items():
            self.SetHoldings(symbol, w)
        
        self.weight.clear()
    
    def Selection(self) -> None:
        if self.Time.month == self.selection_month:
            self.selection_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"))

Leave a Reply

Discover more from Quant Buffet

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

Continue reading