“该策略根据LinkedIn用户的净劳动力流动数据投资于罗素1000指数的股票。它做多就业增长最高的公司,做空就业增长最低的公司。”

I. 策略概要

该策略针对罗素1000指数中的股票,使用LinkedIn上受雇于上市公司的100万用户的数据。公司按净劳动力流动进行排名,计算方法是员工增加量减去员工流失量除以月初员工总数。投资者对就业增长最高的五分之一公司持有多头头寸,对最低的五分之一公司持有空头头寸。该策略采用价值加权,每月重新平衡,旨在利用就业增长与股票表现之间的潜在联系。

II. 策略合理性

对该异常现象的各种解释进行了考虑,但没有一个能够完全解释它。结果表明,该异常现象在很长的时间跨度内持续存在,不受就业调整成本或受限数据访问的影响。一个可能的解释是,员工观察到公司未来前景的信号,并相应地调整他们的工资预期。负面信号可能导致工人流失,而正面信号则吸引他们。异常回报也可能源于投资者对这种劳动力流动信息的不关注,投资者认为它已经反映在其他数据中。对LinkedIn用户的调查证实,工人根据公司的未来前景做出就业决定,这些因素比个人考虑更重要。

III. 来源论文

Information Dispersion Across Employees and Stock Returns [点击查看论文]

<摘要>

普通员工对许多公司来说变得越来越重要,但我们对他们的就业动态如何影响股价知之甚少。我们分析了上市公司员工个人简历的新数据,发现普通劳动力流动可以用来预测异常股票回报。会计数据和调查证据表明,工人的劳动力市场决策反映了有关未来公司收益的信息。然而,投资者似乎没有将这些信息完全纳入他们的收益预期。研究结果支持以下假设:普通员工的进入和退出决策为雇主的未来股票表现提供了有价值的见解。

IV. 回测表现

年化回报5.41%
波动率4.88%
β值0.029
夏普比率0.37
索提诺比率-0.176
最大回撤N/A
胜率50%

V. 完整的 Python 代码

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  

发表评论

了解 Quant Buffet 的更多信息

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

继续阅读