“该策略通过长期机构交易对CRSP股票进行排序,做多减少交易的股票,做空增加交易的股票,使用价值加权投资组合,每季度重新平衡。”

I. 策略概要

该策略针对CRSP数据库中价值在5美元至1,000美元之间的股票。每个季度初,股票根据长期机构交易(LIT)进行排序,LIT定义为金融机构在过去10个季度累计净交易量,并按已发行股票数量进行缩放。机构持股数据来源于汤森路透13F数据库。该策略涉及做多机构交易减少幅度最大的十分位股票,做空机构交易增加幅度最大的十分位股票。投资组合采用价值加权,每季度重新平衡,以利用机构交易动态。

II. 策略合理性

该研究支持以下假设:机构长期买卖行为会导致与基本面出现显著偏离。机构倾向于在多个时期内朝同一方向交易,这是由购买基本面改善的股票和出售基本面下降的股票所产生的动量效应驱动的。然而,公司基本面的转变最终会导致机构交易和股票回报的显著逆转,使价格回归其基本面价值。

III. 来源论文

Long-Term Institutional Trades and the Cross-Section of Returns [点击查看论文]

<摘要>

我研究了长期机构交易与未来回报之间的关系,发现金融机构在过去十个季度累计净买入的股票数量与未来回报呈负相关。根据这一指标构建的多空投资组合获得了9.9%的年化平均Carhart阿尔法。总的来说,我发现长期机构交易包含的未来回报信息是现有短期机构交易指标尚未捕捉到的。

IV. 回测表现

年化回报8.24%
波动率11.04%
β值0.013
夏普比率0.75
索提诺比率-0.025
最大回撤N/A
胜率53%

V. 完整的 Python 代码

from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
#endregion
class LongTermInstitutionalTradesAndTheCrossSectionOfReturns(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2005, 1, 1) # institutional ownership data starts at 2005
        self.SetCash(100_000)
        
        self.period: int = 10 # need n data points of institutional ownership data
        self.quantile: int = 10
        self.leverage: int = 5
        
        self.weight: Dict[Symbol, float] = {}
        self.institutional_data: Dict[str, RollingWindow] = {}
            
        market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        # subscribe to Quantpedia institutional ownership data   
        self.institutional_symbol: Symbol = self.AddData(QuantpediaInstitutionalOwnership, 'INSTITUTIONAL_OWNERSHIP_IN_PERCENTS', Resolution.Daily).Symbol
        
        self.selection_flag: bool = False
        self.settings.daily_precise_end_time = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
        
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
    
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        # monthly rebalance
        if not self.selection_flag:
            return Universe.Unchanged
        if self.Time.date() > QuantpediaInstitutionalOwnership.get_last_update_date():
            return Universe.Unchanged
        selected: List[Fundamental] = [
            x for x in fundamental 
            if x.Symbol.Value in self.institutional_data
            and x.MarketCap != 0
        ]
        LIT: Dict[Fundamental, float] = {
            stock: self.CalculateLIT(self.institutional_data[stock.Symbol.Value]) 
            for stock in selected if self.institutional_data[stock.Symbol.Value].IsReady}
        
        # not enough stocks for decile selection
        if len(LIT) < self.quantile:
            return Universe.Unchanged
            
        # perform decile selection
        quantile: int = int(len(LIT) / self.quantile)
        sorted_by_LIT: List[Fundamental] = sorted(LIT, key=LIT.get)
        
        # long stocks with the highest decrease
        long: List[Fundamental] = sorted_by_LIT[:quantile]
        # short stocks with the lowest decrease
        short: List[Fundamental] = sorted_by_LIT[-quantile:]
        
        # calculate weights
        for i, portfolio in enumerate([long, short]):
            mc_sum: float = sum(map(lambda x: x.MarketCap, portfolio))
            for stock in portfolio:
                self.weight[stock.Symbol] = ((-1) ** i) * stock.MarketCap / mc_sum
        return list(self.weight.keys())
    def OnData(self, data: Slice) -> None:
        if self.institutional_symbol in data and data[self.institutional_symbol]:
            # get objects with tickers and their values from data object
            tickers_values_objects: List = [x for x in data[self.institutional_symbol].GetProperty('Tickers_Values')]
            # update LIT rolling window for each stock ticker
            self.UpdateInstitutionalDataRollingWindow(tickers_values_objects)
        
        # monthly rebalance
        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 symbol in data and data[symbol]
        ]
        self.SetHoldings(portfolio, True)
        self.weight.clear()
        
    def UpdateInstitutionalDataRollingWindow(self, tickers_values_objects: List[float]) -> None:
        ''' update rolling window for each stock in self.institutional_data with new institutional data point '''
        ''' if rolling window doesn't exists it will be created '''
        
        for ticker_value in tickers_values_objects:
            # create list from ticker and value object
            ticker_value = [x for x in ticker_value]
            # get ticker from list
            ticker: str = ticker_value[0]
            # get value from list
            value: float = ticker_value[1]
            
            # initialize rolling window for institutional data for specific stock ticker
            if ticker not in self.institutional_data:
                self.institutional_data[ticker] = RollingWindow[float](self.period)
            
            # make sure value isn't missing
            if value:
                self.institutional_data[ticker].Add(value)
                
    def CalculateLIT(self, institutional_data_rolling_window: RollingWindow) -> float:
        ''' calculates and returns stock's LIT(total change of institutional ownership data) '''
        
        institutional_data: List[float] = list(institutional_data_rolling_window)
        
        prev_data_point: float = institutional_data[0]
        total_change: float = 0
        
        for data_point in institutional_data[1:]: # offset first data point
            # increase of institutional data
            if prev_data_point < data_point:
                total_change += (data_point - prev_data_point)
            # decrease of institutional data
            else:
                total_change -= (prev_data_point - data_point)
                
            prev_data_point = data_point
            
        return total_change
        
    def Selection(self) -> None:
        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"))
        
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
# Name of the csv file has to be in uppercase
class QuantpediaInstitutionalOwnership(PythonData):
    _last_update_date: datetime.date = datetime(1,1,1).date()
    @staticmethod
    def get_last_update_date() -> datetime.date:
       return QuantpediaInstitutionalOwnership._last_update_date
    def __init__(self):
        self.header = None
    
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/economic/institutional_ownership/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    def Reader(self, config, line, date, isLiveMode):
        data = QuantpediaInstitutionalOwnership()
        data.Symbol = config.Symbol
        
        # initialize header with stock tickers from csv files
        if not self.header:
            # split header of csv file
            line_split = line.split(',')
            
            # store stock tickers in header list
            self.header = [ticker for ticker in line_split[1:]] # exclude 'date'
            
        if not line[0].isdigit():
            return None
            
        line_split = line.split(',')
        # make two months lag of data
        data.Time = datetime.strptime(line_split[0], '%Y-%m-%d') + relativedelta(months=2)
        # store last update date
        if data.Time.date() > QuantpediaInstitutionalOwnership._last_update_date:
            QuantpediaInstitutionalOwnership._last_update_date = data.Time.date()
        tickers_values = []
        # store values with their tickers
        for (ticker), (value) in zip(self.header, line_split[1:]):
            if value == '':
                tickers_values.append([ticker, None])
            else:
                tickers_values.append([ticker, float(value)])
                
        data['tickers_values'] = tickers_values
        
        return data

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读