
“该策略根据LinkedIn用户的净劳动力流动数据投资于罗素1000指数的股票。它做多就业增长最高的公司,做空就业增长最低的公司。”
资产类别: 股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 领英、员工数据
I. 策略概要
该策略针对罗素1000指数中的股票,使用LinkedIn上受雇于上市公司的100万用户的数据。公司按净劳动力流动进行排名,计算方法是员工增加量减去员工流失量除以月初员工总数。投资者对就业增长最高的五分之一公司持有多头头寸,对最低的五分之一公司持有空头头寸。该策略采用价值加权,每月重新平衡,旨在利用就业增长与股票表现之间的潜在联系。
II. 策略合理性
对该异常现象的各种解释进行了考虑,但没有一个能够完全解释它。结果表明,该异常现象在很长的时间跨度内持续存在,不受就业调整成本或受限数据访问的影响。一个可能的解释是,员工观察到公司未来前景的信号,并相应地调整他们的工资预期。负面信号可能导致工人流失,而正面信号则吸引他们。异常回报也可能源于投资者对这种劳动力流动信息的不关注,投资者认为它已经反映在其他数据中。对LinkedIn用户的调查证实,工人根据公司的未来前景做出就业决定,这些因素比个人考虑更重要。
III. 来源论文
Information Dispersion Across Employees and Stock Returns [点击查看论文]
- 阿什维尼·阿加瓦尔(Ashwini Agrawal)、艾萨克·哈卡莫(Isaac Hacamo)和胡中晨(Zhongchen Hu),伦敦政治经济学院,印第安纳大学,伦敦政治经济学院
<摘要>
普通员工对许多公司来说变得越来越重要,但我们对他们的就业动态如何影响股价知之甚少。我们分析了上市公司员工个人简历的新数据,发现普通劳动力流动可以用来预测异常股票回报。会计数据和调查证据表明,工人的劳动力市场决策反映了有关未来公司收益的信息。然而,投资者似乎没有将这些信息完全纳入他们的收益预期。研究结果支持以下假设:普通员工的进入和退出决策为雇主的未来股票表现提供了有价值的见解。


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