
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.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Quarterly | MARKET: equities | KEYWORD: Institutional, Ownership, Effect
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 Return | 15% |
| Volatility | 20.1% |
| Beta | -0.022 |
| Sharpe Ratio | 0.71 |
| Sortino Ratio | -0.016 |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
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"))