The strategy invests in NYSE, AMEX, and NASDAQ stocks, trading on institutional ownership turnover. It goes long on low-turnover deciles, shorts high-turnover deciles, and rebalances equally weighted portfolios quarterly.

I. STRATEGY IN A NUTSHELL

Targets NYSE, AMEX, and NASDAQ stocks above $1. Quarterly, stocks are sorted by institutional ownership turnover (InstTurnIndiv). Go long on the lowest decile (least turnover) and short the highest decile (most turnover). Portfolio equally weighted and rebalanced quarterly.

II. ECONOMIC RATIONALE

Stocks with stable institutional ownership tend to have higher expected returns, as significant institutional-to-retail flows reduce potential gains.

III. SOURCE PAPER

Do Institutions Pay to Play? Turnover of Institutional Ownership and Stock Returns [Click to Open PDF]

Dimitrov, Gatchev

<Abstract>

We examine the relation between stock returns and turnover of institutional ownership. Based on ten portfolios, we find that the portfolio of stocks with the highest turnover of institutional ownership earns 8.9% lower subsequent one-year returns than the portfolio of stocks with the lowest turnover of institutional ownership. These findings are consistent with the theory of Harrison and Kreps (1978) and Scheinkman and Xiong (2003), where the expectation of trading profits leads to high turnover of ownership and a premium in asset prices. We further examine the two components of turnover of institutional ownership — one due to trading of institutions with individuals and the other due to trading among institutions. We find that only turnover of ownership between institutions and individuals is negatively related to subsequent stock returns, which is consistent with the idea that institutions expect higher profits when trading against individual investors than when trading against other institutions. We also find that the negative relation between future returns and turnover of ownership between institutions and individuals is stronger for stocks that, according to the theory, are more likely to be subject to speculative trading (i.e., stocks with higher overall trading activity, higher return volatility, and lower book-to-market). Overall, our findings support the prediction that investors may pay a premium in anticipation of profitable trading opportunities.

IV. BACKTEST PERFORMANCE

Annualised Return15%
Volatility20.1%
Beta-0.022
Sharpe Ratio0.71
Sortino Ratio-0.016
Maximum DrawdownN/A
Win Rate49%

V. FULL PYTHON CODE

from AlgorithmImports import *
#endregion
class InstitutionalOwnershipEffect(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2005, 1, 1)
        self.SetCash(100_000)
        
        self.dates: List[datetime.date] = []
        self.institutional_ownership: Dict[Symbol, Dict] = {}
        
        self.data: Dict[Symbol, SymbolData] = {}
        self.period: int = 3 * 8 * 21 # Eight quarter period
        self.one_quarter_data: Dict[Symbol, List[float]] = {} # Storing one quarter data about each stock from our universe
        self.eight_quarter_data: Dict[Symbol, RollingWindow] = {} # Storing eight quarter data about each stock from our universe
        
        self.quantile: int = 10
        self.leverage: int = 5
        self.last_investment_date = None
        self.symbol: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        
        self.sp100_stocks: List[Symbol] = [] # 'AGN', 'UTX', 'BRKB' # No data about institutional ownership
        
        csv_string_file: str = self.Download('data.quantpedia.com/backtesting_data/economic/institutional_ownership/institutional_ownership_in_millions.csv')
        lines: List[str] = csv_string_file.split('\r\n')
        for i in range(len(lines)):
            line_split: List[str] = lines[i].split(',')
            if i == 0: # First row are headers of columns
                for j in range(1, len(line_split)): # Take all headers, which are stock tickers
                    ticker: str = line_split[j]
                    symbol: Symbol = self.AddEquity(ticker, Resolution.Daily).Symbol
                    
                    # Warmup volume data.
                    self.data[symbol] = SymbolData(self.period)
                    history: dataframe = self.History(symbol, self.period, Resolution.Daily)
                    if history.empty:
                        self.Log(f"Not enough data for {symbol} yet")
                        continue
                    volumes: Series = history.loc[symbol].volume
                    for time, volume in volumes.items():
                        self.data[symbol].update(volume)
                    
                    self.one_quarter_data[symbol] = []
                    self.eight_quarter_data[symbol] = RollingWindow[float](8)
                    self.institutional_ownership[symbol] = {} # Create dictionary for all institutional ownership data
                    
                    # Subscript current stock and append symbol to universe, which we will use for this strategy 
                    self.sp100_stocks.append(symbol)
            else:
                date: datetime.date = datetime.strptime(line_split[0], "%Y-%m-%d").date()
                
                self.dates.append(date) # Create list of dates for institutional annoucements
                
                for j, symbol in zip(range(1, len(line_split)), self.sp100_stocks):
                    # Based on symbol store date and institutional ownership value as a nested dictionary
                    # In nested dictionary date figures as a key and institutional ownership as a value
                    self.institutional_ownership[symbol][date] = (line_split[j]) 
        
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.count_month: int = 1
        self.selection_flag: bool = False
        self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
        
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
    def OnData(self, slice: Slice) -> None:
        date = None
        inst_turn_in_div: Dict[Symbol, float] = {}
        
        current_date: datetime.date = self.Time.date()
        day_before: datetime.date = (self.Time - timedelta(days=1)).date()
        two_days_before: datetime.date = (self.Time - timedelta(days=2)).date()
        three_days_before: datetime.date = (self.Time - timedelta(days=3)).date()
        
        for symbol in self.sp100_stocks:
            # Update daily volume data.
            if symbol in slice:
                if slice[symbol]:
                    self.data[symbol].update(slice[symbol].Volume)
            
            # Institutional ownership could be annouced on weekend. This prevents missing it.
            # Without three days look ahead it was missing some annoucements
            if (current_date in self.dates) and (current_date != self.last_investment_date):
                self.last_investment_date = current_date
                date = current_date
            elif (day_before in self.dates) and (day_before != self.last_investment_date):
                self.last_investment_date = day_before
                date = day_before
            elif (two_days_before in self.dates) and (two_days_before != self.last_investment_date):
                self.last_investment_date = two_days_before
                date = two_days_before
            elif (three_days_before in self.dates) and (three_days_before != self.last_investment_date):
                self.last_investment_date = three_days_before
                date = three_days_before
                
            # If institutional ownership was announced store institutional_ownership value about each stock from our universe
            if date in self.dates:
                if self.institutional_ownership[symbol][date] != '':
                    self.one_quarter_data[symbol].append(float(self.institutional_ownership[symbol][date]))
        
            if not self.selection_flag:
                continue
            absolute_change = None
            if len(self.one_quarter_data[symbol]) >= 2:
                absolute_change: float = self.one_quarter_data[symbol][-1] - self.one_quarter_data[symbol][0]
                total_one_quarter: float = sum(self.one_quarter_data[symbol])
                self.eight_quarter_data[symbol].Add(total_one_quarter)
        
                # If eight quarter institutional ownership data are ready for chosen stock, then trade
                if self.data[symbol].is_ready() and self.eight_quarter_data[symbol].IsReady:  
                    if symbol != 'ADP':
                        inst_turn_in_div_num: float = absolute_change / self.data[symbol].total_volume()
                        
                        total_inst_shares: float = sum([x for x in self.eight_quarter_data[symbol]])
                        inst_turn_in_div_num: float = inst_turn_in_div_num / total_inst_shares
                        
                        inst_turn_in_div[symbol] = inst_turn_in_div_num
            
            self.one_quarter_data[symbol].clear()
        
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        long: List[Symbol] = []
        short: List[Symbol] = []
        if len(inst_turn_in_div) >= self.quantile:
            quantile: int = int(len(inst_turn_in_div) / self.quantile)
            sorted_by_inst_turn_in_div = [x[0] for x in sorted(inst_turn_in_div.items(), key=lambda item: item[1])]
            # long the lowest ‘InstTurnIndiv’ decile
            long = sorted_by_inst_turn_in_div[:quantile]
            # short the highest ‘InstTurnIndiv’ decile
            short = sorted_by_inst_turn_in_div[-quantile:]
        
        # Trade execution.
        invested: List[Symbol] = [x.Key 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))
    def Selection(self) -> None:
        if self.count_month == 3: # The portfolio is rebalance quarterly
            self.selection_flag = True
            self.count_month = 1
        else:
            self.count_month += 1
            
class SymbolData():
    def __init__(self, period: int) -> None:
        self._volumes: RollingWindow = RollingWindow[float](period)
        
    def update(self, volume) -> None:
        self._volumes.Add(volume)
        
    def is_ready(self) -> bool:
        return self._volumes.IsReady
        
    def total_volume(self) -> float:
        return sum(list(self._volumes))
        
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