Strategy performance
Each leg holds the most or least ghost-like 10% of scored firms, rebalanced quarterly with a 45-day publication lag and 20 bps transaction costs, from 2019 to 2026. In the table, ≈ marks a figure read off the report’s chart and — means the report gives no figure.
| Strategy | Total return | CAGR | Sharpe | Max drawdown |
|---|---|---|---|---|
| Ghost Barbell | +216.9% | +17.1% | 0.84 | −37.4% |
| Long high ghost | ≈ +220% | — | — | — |
| Long low ghost | ≈ +120% | — | — | — |
| SPY (benchmark) | +180.1% | +15.2% | 0.82 | −33.7% |
| Universe, equal weight | +139.4% | +12.7% | 0.64 | −39.4% |
| Long / Short | −22.1% | −3.4% | −0.32 | −43.4% |
How the score is built
Five steps, in the order the notebook runs them. The numbers are the report’s; the code for steps 1–4 is notebooks/Ghost_Job_Pipeline_v2.ipynb, for step 5 the two scripts in scripts/.
Predict next-quarter hiring
An XGBoost regressor predicts each firm-quarter’s hires in the following quarter from 14 features: ten about how the firm posts (volume, how long postings stay up, share never removed, salary disclosure…) and four about the company (headcount, its log and four-quarter growth, industry).
Test R² 0.486 against Ridge 0.279; train 0.947, validation 0.549 (README); MAE 3.1 · report p. 2
Flag ghost posters
A firm whose predicted fill rate — predicted hires per posting — falls below the 21st percentile is flagged, following Ng (2024).
Threshold 0.2898 (README) · 43 of 204 out-of-sample firms, 21.1% · report p. 2
Score every firm-quarter
The report ranks firms each quarter, so the score measures how a firm compares with the rest of the market that quarter rather than its size.
GhostScore(i, t) = 1 − Rankt[PredFillRate(i, t)] / NtReport p. 2, where the score carries the report’s own name. The notebook’s
ghost_scores_fq.parquetcomputes a related score, 1 − predicted hires ÷ the firm’s own median prediction; the backtest reads whichever score file it is given.Check the structure without labels
A Gaussian mixture on the same 14 features, with no ghost labels, separates firms that post like ghosts from firms that hire.
7 components (BIC −1,508) · cluster 0: 1.6% ghost rate; clusters 3 and 4: 44.2% and 50.0% (README; the report rounds to 44% and 50%) · report pp. 2, 7
Backtest without looking ahead
On each quarterly rebalance only scores published at least 45 days after their quarter ended are used; daily returns are capped at ±25%, and illiquid or extremely volatile tickers are dropped before trading.
2019–2026 · 10% legs · 20 bps · report pp. 2–3 · return cap and liquidity and volatility filters: README
What it was built on
Revelio Labs job postings
Licensed workforce data: individual postings, positions and company references. It is not in this repository and may not be redistributed; data/README.md lists what the notebook reads and how researchers can get access.
- Postings
- 310,821
- US public firms
- 2,919
- Years
- 2009–2024
- Firms with ≥ 3 postings a quarter
- 1,017
Report p. 1
What drives the prediction
Headcount features carry the most weight in the model’s explanations. Among the posting features, the share of postings never taken down is the clearest ghost signal in the report.
headcount- 4.451
log_headcount- 2.152
headcount_growth_4q- 0.670
Mean |SHAP|, top three (README) · still_active_rate: report p. 3
Run the pipeline without the data
No licence and no network: a generator writes made-up scores and prices for 200 firms (tickers SYN001…, benchmark SYNMKT), and both backtest scripts read them with --synthetic. Scores and returns are independent by construction, so the numbers these runs print are not findings — they show that the code runs.
pip install -r requirements-synthetic.txt
python scripts/make_synthetic.py
python scripts/ghost_backtest_v2.py --synthetic
python scripts/check.py
check.pyruns both scripts on fresh synthetic data and checks their outputs, and that a score file missing a column is refused by name.- It also tests the no-lookahead promise: on data where each quarter’s returns follow scores that are only published later, a backtest that respects the 45-day lag must not profit from them, and one allowed to read scores early must.
check.py --breakremoves a column and loosens the backtester’s time gate on purpose; both must turn the checks red.