“通过流动性增长交易美国股票,做多高流动性增长(LIQG)股票,做空低流动性增长(LIQG)股票(流动性强和流动性弱的),使用美元交易量加权,每月重新平衡的投资组合。”

I. 策略概要

投资范围包括CRSP数据库中的美国股票(纽约证券交易所、美国证券交易所、纳斯达克)。流动性计算为每月交易股数乘以月末价格。流动性增长(LIQG)通过比较当月(t月)与去年同月(t-12月)的流动性得出。对于非负累计回报的股票,LIQG是流动性的相对差异;对于负累计回报的股票,则是负绝对差异。

股票被分为2×3组:流动性强/流动性弱和低/中性/高LIQG。该策略做多高LIQG股票(包括流动性强和流动性弱的),做空低LIQG股票。投资组合按美元交易量加权,每月重新平衡。

II. 策略合理性

该策略的功能源于流动性作为股票回报因素的重要性及其与长期以交易量为重点的投资者的相关性。低流动性增长的股票,通常不被注意,预计会产生更高的回报,而高流动性增长的股票,通常交易过度,应该产生更低的回报。然而,研究并未完全支持这种行为解释。相反,流动性增长被认为是风险因素,反映了投资者对流动性增长风险的担忧。此外,流动性增长因子与动量密切相关,表明存在与流动性增长相关的潜在风险溢价,进一步增强了其在解释股票回报中的作用。

III. 来源论文

Earnings and Liquidity Factors [点击查看论文]

<摘要>

一个包含盈利、流动性、各自增长以及市场因素的模型可以提供一个具有低定价误差的消费理由。它还包含了为期一年的动量和剔除反转后的动量,即通常所说的“动量”因子。这些盈利和流动性因子都非常重要,并且结合起来形成了一个没有因子冗余的模型。受投资者建立头寸能力的启发,我们基于交易量构建投资组合,并将流动性整合到补充基于公司因子的简化形式的基于特征的因子模型中。

IV. 回测表现

年化回报7.18%
波动率11.29%
β值0
夏普比率0.63
索提诺比率-94.825
最大回撤N/A
胜率44%

V. 完整的 Python 代码

from AlgorithmImports import *
import pandas as pd
from collections import deque
import data_tools
#endregion
class LiquidityGrowthFactor(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2002, 1, 1)
        self.SetCash(100_000)
        
        market: Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.fundamental_count: int = 1_000
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        
        self.data: Dict[Symbol, data_tools.SymbolData] = {}
        self.weight: Dict[Symbol, float] = {}
        
        self.monthly_period: int = 12
        self.daily_period: int = 30
        self.volume_period: int = 21
        self.leverage: int = 5
        self.exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']
        
        self.selection_flag: bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.settings.daily_precise_end_time = False
        self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
    
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(data_tools.CustomFeeModel())
            security.SetLeverage(self.leverage)
            
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        for stock in fundamental:
            symbol: Symbol = stock.Symbol
            if symbol in self.data:
                # store price and volume and dollar volume every day
                self.data[symbol].update_price(stock.AdjustedPrice)
                self.data[symbol].update_volume(stock.Volume)
                self.data[symbol].update_dollar_volume(stock.DollarVolume)
                if self.selection_flag and self.data[symbol].volume_is_ready() and self.data[symbol].price_is_ready():
                    # store monthly liquidity
                    liquidity: float = self.data[symbol].monthly_liquidity()
                    if liquidity != 0:
                        self.data[symbol].update_liquidity(liquidity, self.Time)
        
        if not self.selection_flag:
            return Universe.Unchanged
            
        selected: List[Fundamental] = [
            f for f in fundamental if f.HasFundamentalData 
            and f.Market == 'usa'
            and f.SecurityReference.ExchangeId in self.exchange_codes
        ]
        
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        liquidity: Dict[Symbol, float] = {}
        liquidity_growth: Dict[Symbol, float] = {}
        # warmup price rolling windows
        for stock in selected:
            symbol: Symbol = stock.Symbol
            if symbol not in self.data:
                self.data[symbol] = data_tools.SymbolData(self.monthly_period+1, self.monthly_period+1, self.volume_period)
                
                history: dataframe = self.History(symbol, self.monthly_period*self.daily_period, Resolution.Daily)
                if history.empty:
                    self.Log(f"Not enough data for {symbol} yet.")
                    continue
                
                if 'close' in history and 'volume' in history:
                    closes: Series = history.loc[symbol]['close']
                    volumes: Series = history.loc[symbol]['volume']
                    
                    # find monthly closes and update rolling window
                    closes_len: int = len(closes.keys())
                    for index, time_close in enumerate(closes.items()):
                        # index out of bounds check
                        if index + 1 < closes_len:
                            date_month: int = time_close[0].date().month
                            next_date_month: int = closes.keys()[index + 1].month
                        
                            # found last day of month
                            if date_month != next_date_month:
                                self.data[symbol].update_price(time_close[1])
                
                # monthly grouped data
                shares_traded_grouped = volumes.groupby(pd.Grouper(freq='M')).sum()
                closes_grouped = closes.groupby(pd.Grouper(freq='M')).last()
                liquidity_grouped = shares_traded_grouped * closes_grouped
                for i, liq in enumerate(liquidity_grouped):
                    self.data[symbol].update_liquidity(liq, closes_grouped.index[i])
            
            # liq and liq growth calc
            if self.data[symbol].liquidity_is_ready() \
                and self.data[symbol].price_is_ready() \
                and self.data[symbol].dollar_volume_is_ready():
                lg: float = self.data[stock.Symbol].liquidity_growth()
                if lg != -1:
                    liquidity[symbol] = self.data[stock.Symbol].recent_liquidity()
                    liquidity_growth[symbol] = lg
            
        long: List[Symbol] = []
        short: List[Symbol] = []
        
        # sorting
        min_stock_cnt: int = 6
        if len(liquidity) >= min_stock_cnt and len(liquidity_growth) >= min_stock_cnt:
            sorted_by_liq = sorted(liquidity, key = liquidity.get, reverse = True)
            half: int = int(len(sorted_by_liq) / 2)
            liquid: List[Symbol] = sorted_by_liq[:half]
            iliquid: List[Symbol] = sorted_by_liq[-half:]
            
            percentile: int = int(len(liquid) * 0.3)
            liquid_by_growth: List[Symbol] = sorted(liquid, key = lambda x: liquidity_growth[x], reverse = True)
            high_liquid: List[Symbol] = liquid_by_growth[:percentile]
            low_liquid: List[Symbol] = liquid_by_growth[-percentile:]
            iliquid_by_growth: List[Symbol] = sorted(iliquid, key = lambda x: liquidity_growth[x], reverse = True)
            high_iliquid: List[Symbol] = iliquid_by_growth[:percentile]
            low_iliquid: List[Symbol] = iliquid_by_growth[-percentile:]
            
            long = high_liquid + high_iliquid
            short = low_liquid + low_iliquid
        
        # dollar volume weighting
        for i, portfolio in enumerate([long, short]):
            total_dollar_vol: float = sum([self.data[x].monthly_dollar_volume() for x in portfolio])
            for stock in portfolio:
                self.weight[symbol] = ((-1) ** i) * (self.data[symbol].monthly_dollar_volume() / total_dollar_vol)
        return list(self.weight.keys())
        
    def OnData(self, slice: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # trade execution
        portfolio: List[PortfolioTarget] = [
            PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if slice.contains_key(symbol) and slice[symbol]
        ]
        
        self.SetHoldings(portfolio, True)
        self.weight.clear()
        
    def Selection(self):
        self.selection_flag = True

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读