The strategy invests in Russell 1000 stocks based on net labor flow data from LinkedIn users. It goes long on firms with the highest employment growth and shorts those with the lowest.

I. STRATEGY IN A NUTSHELL

The strategy invests in Russell 1000 stocks using LinkedIn data on one million employees. Firms are ranked by net labor flow—the ratio of employee additions minus exits over total employees at the start of the month. Each month, the investor goes long the top quintile (highest employment growth) and shorts the bottom quintile. The portfolio is value-weighted and rebalanced monthly, aiming to exploit the potential predictive link between labor flows and stock performance.

II. ECONOMIC RATIONALE

The anomaly appears persistent over long horizons and is not explained by employment adjustment costs or restricted data. Employees often observe signals of a firm’s future prospects: positive signals attract workers, while negative signals drive exits. Abnormal stock returns may arise because investors underweight or overlook labor flow information. LinkedIn surveys indicate that employment decisions are guided more by firm prospects than personal considerations, suggesting labor flows can signal future firm performance before it is reflected in conventional financial metrics.

III. SOURCE PAPER

Information Dispersion Across Employees and Stock Returns [Click to Open PDF]

Ashwini Agrawal, London School of Economics; Isaac Hacamo, Indiana University; Zhongchen Hu, London School of Economics

<Abstract>

Rank-and-file employees are becoming increasingly critical for many firms, yet we know little about how their employment dynamics matter for stock prices. We analyze new data from the individual CV’s of public company employees, and find that rank-and-file labor flows can be used to predict abnormal stock returns. Accounting data and survey evidence indicate that workers’ labor market decisions reflect information about future corporate earnings. Investors, however, do not appear to fully incorporate this information into their earnings expectations. The findings support the hypothesis that rank-and-file employees’ entry and exit decisions convey valuable insight into their employers’ future stock performance.

IV. BACKTEST PERFORMANCE

Annualised Return5.41%
Volatility4.88%
Beta0.029
Sharpe Ratio0.37
Sortino Ratio-0.176
Maximum DrawdownN/A
Win Rate50%

V. FULL PYTHON CODE

from AlgorithmImports import *
from data_tools import CustomFeeModel, SymbolData
# endregion
class TheImpactOfLinkedinDataAboutEmployeesOnStockReturns(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.leverage:int = 5
        self.quantile:int = 5
        self.min_share_price:float = 5.
        self.max_missing_days:int = 35
        self.data:Dict[Symbol, SymbolData] = {}
        self.weight:Dict[Symbol, float] = {}
        market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.fundamental_count:int = 500
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.BeforeMarketClose(market, 0), self.Selection)
        self.settings.daily_precise_end_time = False
    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]:
        if not self.selection_flag:
            return Universe.Unchanged
        selected:List[Fundamental] = [
            x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and \
            x.MarketCap != 0 and x.Price >= self.min_share_price
        ]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
        curr_date:datetime.date = self.Time.date()
        total_employees_change:Dict[Fundamental, float] = {}
        for stock in selected:
            # The number of employees as indicated on the latest Annual Report, 10-K filing, Form 20-F or equivalent report indicating the employee count at the end of latest fiscal year.
            total_employees:float = stock.CompanyProfile.TotalEmployeeNumber
            if total_employees == 0:
                continue
            symbol:Symbol = stock.Symbol
            if symbol not in self.data:
                self.data[symbol] = SymbolData()
            if self.data[symbol].prev_total_employees_ready() and not self.data[symbol].missed_prev_month(curr_date, self.max_missing_days) \
                and total_employees != self.data[symbol].get_prev_total_employees():
                total_employees_change_value:float = self.data[symbol].get_total_employees_change(total_employees)
            
                total_employees_change[stock] = total_employees_change_value
            self.data[symbol].set_prev_total_employees(curr_date, total_employees)
        if len(total_employees_change) < self.quantile:
            return Universe.Unchanged
        
        quantile:int = int(len(total_employees_change) / self.quantile)
        sorted_by_change:List[Fundamental] = [x[0] for x in sorted(total_employees_change.items(), key=lambda item: item[1])]
        long_leg:List[Fundamental] = sorted_by_change[-quantile:]
        short_leg:List[Fundamental] = sorted_by_change[:quantile]
        # calculate weights
        for i, portfolio in enumerate([long_leg, short_leg]):
            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 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 Selection(self) -> None:
        self.selection_flag = True  

Leave a Reply

Discover more from Quant Buffet

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

Continue reading