Labor-turn duration timing: buy Treasuries when the labor market cracks, because the Fed cuts more than markets price
Module labor_turn_duration · from the research cycle of Oct 16, 2026
Rejected The Skeptic showed the result isn't robust, or that a simpler alternative does as well.
Performance
Backtest after costs. Train is where the idea was developed; validation is where it was judged.
| Metric | Train 2007–15 | Validation 2016–21 |
|---|---|---|
| CAGR | 3.4% | 3.4% |
| Volatility | 9.1% | 6.9% |
| Sharpe | 0.33 | 0.38 |
| Sortino | 0.46 | 0.55 |
| Max drawdown | -20.4% | -16.1% |
| Calmar | 0.16 | 0.21 |
| Worst month | -9.6% | -4.5% |
| Months up | 53% | 54% |
| Turnover / yr | 13.2× | 9.6× |
| Avg gross exposure | 1.05× | 1.05× |
| Correlation to SPY | -0.45 | -0.38 |
| Correlation to 60/40 | -0.27 | -0.21 |
| Beta to SPY | -0.19 | -0.15 |
Average holdings
The idea
Hypothesis
Labor-market turns are non-linear. Once initial claims rise clearly off their cycle low, or unemployment rises 0.3pp off its 12-month low, the Fed's reaction function makes a cutting cycle very likely. Market rate expectations update too slowly because they anchor on Fed guidance and consensus forecasts. Duration therefore earns excess returns after labor turns. On the other side, when claims sit at cycle lows and the 2-year yield is rising faster than bills (markets pricing hikes), duration underperforms cash. Claims are published weekly with a 6-day lag, which makes them the fastest official labor data, so the signal arrives months before payroll revisions confirm a turn. Diversification: the long-duration states happen exactly when equities are under pressure, so the sleeve should be negatively correlated with SPY. Unlike 60/40, it is mostly in cash during hiking cycles.
Who is on the other side
Bond investors and forecasters anchored to the Fed's communicated path and the consensus rate forecast (the documented underreaction in Cieslak 2018). Also leveraged duration holders and banks that cut duration as rate-hike expectations build late in the cycle and are slow to add it back.
Inspiration
Cieslak (2018, RFS), 'Short-Rate Expectations and Unexpected Returns in Treasury Bonds'. Forecasters and markets have consistently underestimated how far the Fed cuts once unemployment starts rising, so bond excess returns are predictable from real activity. Related: Piazzesi & Swanson (2008) find that employment growth predicts excess returns on fed funds futures, and the Sahm rule identifies labor-market turning points. This is a carry/macro trade in rates, not a trend trade.
Expected Sharpe (before testing)
0.45
How it could fail
(1) Inflation shocks where labor weakens but the Fed hikes or holds: the long-duration state loses. Our sample has almost none, so this risk is under-tested by construction and must go in the shutdown rules. (2) Zero lower bound (Dec 2008–2015 and 2020–21): the front end is pinned, P carries little information and long yields can still rise (2009, 2013 taper). (3) Few independent episodes: real labor turns in sample are about 2007–08 and 2020, plus false alarms in 2011, 2016 and 2019. Statistical power is low, and the Skeptic should require a positive result in each subperiod rather than trusting the aggregate Sharpe. (4) Claims are distorted by seasonal-adjustment problems, hurricanes and strikes. The 4-week average and the 10% threshold reduce this but don't remove it. (5) Revisions: today's claims vintage is smoother than what was seen live.
Data & rules
Data
ICSA (weekly, lag 6d, revised: seasonal factors are revised every year; the 2020 spike is real). UNRATE (monthly, lag 38d, revised slightly). DGS2, DTB3, T10Y3M (daily, lag 1d, final). Prices: IEF, TLT, SHY. All series cover 2007–2021 in full, with history before 2007 for warm-up.
Rules
Rebalance weekly, on the first trading day after the claims release becomes usable (the engine applies the 6-day lag).
Labor signal L = 1 if either holds:
- ICSA 4-week average > 1.10 × its 52-week minimum, or
- UNRATE 3-month average − its 12-month minimum ≥ 0.30pp.
Otherwise L = 0.
Policy-path signal P = 1 if (DGS2 − DTB3) has fallen over the last 63 trading days, meaning the market is starting to price cuts. Otherwise 0.
Late-cycle hike state H = 1 if ICSA 4-week average < 1.03 × its 52-week minimum AND (DGS2 − DTB3) has risen over 63 days.
Duration basket = 50% IEF / 50% TLT. Basket weight:
- 0.0 if H = 1
- 1.25 if L = 1 and P = 1
- 0.75 if exactly one of L or P is 1
- 0.35 otherwise
The rest goes to SHY. Max gross 1.25; financing cost is charged on the excess.
Pre-declared trial budget (3 trials):
1. Base case.
2. Claims threshold 1.15 instead of 1.10.
3. Base case without P (labor signal only), to check whether the policy-path term adds anything.
Benchmark that the timing must beat: a constant 0.6 duration basket + 0.4 SHY, i.e. similar average exposure. 2007–2021 was a 15-year bond bull market, so beating SHY proves nothing. The test is whether timing adds Sharpe over static duration. Also report correlation to SPY and to 60/40, and returns during SPY's worst 5% of months.
Data review go
COVERAGE AND LAGS: fine. ICSA (from 1967, weekly, 6-day lag), UNRATE (from 1948, 38-day lag), DGS2 (from 1976), DTB3 and T10Y3M (daily, final). The 52-week and 12-month warm-ups are available well before 2007. IEF, TLT and SHY all predate 2005.
PREDICTIVE POWER FOR DURATION (weak, unstable):
- ICSA change_3m vs IEF: rank IC +0.05 (halves +0.14/−0.07)
- ICSA zscore_1y vs IEF: +0.04 (+0.09/−0.11)
- UNRATE change_3m vs IEF: +0.08 (−0.15/+0.26)
- DGS2 change_3m vs IEF: +0.04 (−0.08/+0.26)
The sign is the hypothesised one on average, but every series flips sign between halves and no t-stat reaches 1. Claims mainly predict equities, not bonds: ICSA change_3m vs SPY −0.32, vs EEM −0.19. A linear monthly IC is a poor test of a threshold rule that fires in about 2 real episodes plus about 3 false alarms, so this is a warning, not a kill.
STRUCTURAL ISSUES:
(1) ZLB. DGS2−DTB3 was compressed for most of Dec 2008–2015 and 2020–21, so P is close to a coin flip in those years (about 60% of the sample).
(2) 2020 artifact, a predictable validation loss. The 52-week ICSA minimum and the 12-month UNRATE minimum stay at pre-COVID levels until about Mar 2021. L therefore stays 1 from Mar 2020 into early 2021, while 10-year yields rose from about 0.5% to about 1.7% (Aug 2020–Mar 2021). With P mixed, the rule will hold 0.75–1.25 duration through that selloff.
(3) ICSA revisions: seasonal factors are re-estimated every year. The 2020 level spike is real.
Guidance to the engineer
Go with the 3 declared trials and the static benchmark (0.6 duration + 0.4 SHY).
Inputs:
- ICSA: 4-week mean and 52-week min on the release-aligned series (6-day lag).
- UNRATE: 3-month mean minus 12-month min, available 38 days after the reference month. Do not let the weekly rebalance read an UNRATE month before its release date.
- P and H: (DGS2 − DTB3) today minus 63 trading days ago, 1-day lag on both.
- Execution: rebalance on the first trading day on or after each claims release date plus lag.
The Skeptic will need these reports:
(a) Trading days and returns in each state: H, L&P, one of L/P, neither.
(b) Subperiods 2007–09, 2010–15 (ZLB), 2016–19, 2020–21, each against the static benchmark.
(c) Mar 2020–Jun 2021 on its own (expected loss from the stale-minimum artifact).
(d) Correlation to SPY and 60/40, and returns in SPY's worst 5% of months.
Do not add a fix for the 2020 artifact (such as a cap on how long a turn can last) after seeing results. If it is wanted, it must be declared as a new trial before running.
Implementation · implemented
Built as specified, evaluated once with default parameters (trial 1 of the 3 declared). No fixes were needed.
Implementation:
- ICSA and UNRATE statistics are computed on release-aligned points: the first trading day each new lagged value appears. ICSA uses the 4-week mean vs the 52-week min. UNRATE uses the 3-month mean minus the 12-month min of that 3-month mean (Sahm style), and the threshold is 0.30pp.
- P is the 63-trading-day change in DGS2−DTB3 being below 0. H is ICSA ratio < 1.03 AND that spread change > 0.
- Duration basket weight is 0 if H, 1.25 if L&P, 0.75 if one of L/P, 0.35 otherwise. The basket is split 50/50 IEF/TLT, and SHY = max(0, 1−b).
- Rebalances on each claims-release day.
- Trial 3 is available as {"use_policy": false}, which sets P=0. Under it the tiers become 0.75 when L=1 and 0.35 otherwise, and H still applies. Trial 2 is {"claims_mult": 1.15}.
- Caveat: if two consecutive weekly claims prints are exactly equal, release detection misses one point. This is rare.
Results:
| | Sharpe | CAGR | Vol | MaxDD | corr SPY | corr 60/40 |
|---|---|---|---|---|---|---|
| Train | 0.33 | 3.4% | 9.1% | −20.4% | −0.45 | −0.27 |
| Validation | 0.38 | 3.4% | 6.9% | −16.1% | −0.38 | −0.21 |
- Turnover is 13x/yr in train and 9.6x/yr in validation.
- Average duration weight is 0.69, slightly above the 0.6 static benchmark.
- Annual returns: 2008 +21%, 2009 −17%, 2013 −7%, 2019 +12%, 2020 +12%, 2021 −7.6%. The 2021 loss is consistent with the predicted stale-minimum artifact.
Not yet done:
- I have not run the static 0.6 IEF/TLT + 0.4 SHY benchmark, because it would cost a trial. Until it is run we cannot say whether the timing adds anything beyond the bond bull market. The Skeptic should run it.
- The engine output does not include the per-state, subperiod, or worst-5%-SPY-month reports.
Robustness
The Skeptic's tests: is the result a plateau or a lucky spike, does it hold out of sample, does it survive higher costs, and how did it behave in crises?
Parameter stability
| hike_mult | w_both | w_one | w_none | Sharpe |
|---|---|---|---|---|
| 0.0 | 0.6 | 0.6 | 0.6 | 0.61 |
Median Sharpe 0.61, range 0.61–0.61; 100% of settings above 0.3. An even grid means the idea doesn't depend on one lucky setting.
Walk-forward test
Each year, pick the best settings using only the past, then trade the next year blind.
- Out-of-sample Sharpe
- 0.26
- CAGR
- 2.0%
- Max drawdown
- -16.1%
- Period
- 2012–2021
Settings chosen: 2012: 1.05/False, 2013: 1.05/False, 2014: 1.05/True, 2015: 1.05/False, 2016: 1.05/False, 2017: 1.05/False, 2018: 1.05/False, 2019: 1.1/False, 2020: 1.1/True, 2021: 1.1/True
Trading costs
| Costs | CAGR | Sharpe |
|---|---|---|
| 0x costs | 3.7% | 0.42 |
| 1x costs | 3.4% | 0.38 |
| 2x costs | 3.1% | 0.34 |
| 3x costs | 2.8% | 0.30 |
Turnover 11.8× a year. Validation period.
Crises
| Episode | Strategy | SPY | 60/40 |
|---|---|---|---|
| 2008 financial crisis | 3.9% | -46.4% | -28.2% |
| 2011 euro crisis / US downgrade | 14.9% | -17.8% | -7.4% |
| 2013 taper tantrum | -2.2% | -5.4% | -5.1% |
| 2015–16 China / oil crash | 3.0% | -11.1% | -4.4% |
| Q4 2018 selloff | 5.8% | -18.9% | -10.3% |
| COVID crash | 9.0% | -33.4% | -18.6% |
Correlation to baselines: BuyHold -0.43, SixtyForty -0.25, TrendFollowing 0.46, TrendFollowing 0.40, AssetRotation 0.08, RiskParity 0.27, MacroRegime -0.35. Rolling one-year Sharpe: median 0.38, worst -2.02, negative 35% of the time.
Skeptic's verdict Rejected
Key evidence. Train Sharpe: constant 0.6 duration basket 0.61 vs timing 0.33–0.59 (6/6 below). The P term costs 0.11–0.12 Sharpe at every claims setting. Walk-forward OOS Sharpe 0.26, max drawdown −16.1%. Validation Sharpe 0.38 at 1x costs, 0.30 at 3x. Turnover 11.8x/yr. Correlation to SPY −0.43, to trend +0.46.
The hypothesis set its own test: the timing has to beat a constant basket of 0.6 duration (IEF/TLT) plus 0.4 SHY. It fails that test. On train, the constant basket scores Sharpe 0.61. All 6 timing variants score lower (0.33–0.59), and the default scores 0.33. The policy-path term P, the part meant to capture Cieslak-style underreaction, lowers Sharpe at every claims threshold, by 0.11–0.12. Walk-forward out of sample (2012–2021) gives Sharpe 0.26, with a −16% drawdown that bottoms in March 2021 and 722 days underwater. Validation Sharpe is 0.38 and drops to 0.30 at 3x costs. Turnover of about 12x a year comes from flipping the weight between 0 and 1.25. The crisis protection is real (+3.9% in 2008, +14.9% in 2011, +9.0% in COVID, −0.43 correlation to SPY). But that protection comes from owning duration in a bond bull market, and the constant basket gets it too. Correlation to long-only trend is 0.46. Code bug: _releases drops a monthly UNRATE print when it equals the previous one. That happens often, so the '3-month' and '12-month' windows quietly cover longer spans. It does not leak future data, but the signal is not the one specified. Results by year depend on very few labor turns: +21% in 2008, −16.6% in 2009, −7.6% in 2021. 2021 is the failure mode the strategy itself listed: a stale 52-week minimum keeps L=1 while bonds sell off.
Code
The exact strategy file the Engineer wrote, after passing the sandbox and the look-ahead detector.
labor_turn_duration.py
from amberlab.strategies.base import Strategy, month_ends, rebalance_on, trailing_return, realized_vol
import numpy as np, pandas as pd
def _releases(s):
"""Release-aligned points of a forward-filled macro series: keep the first day each new value appears."""
s = s.dropna()
return s[s.ne(s.shift())]
class LaborTurnDuration(Strategy):
family = "rates_macro"
description = ("Weekly duration timing. Labor signal L=1 if ICSA 4-week mean > claims_mult x its 52-week min "
"(release-aligned, 6d lag) or UNRATE 3-month mean minus its 12-month min >= 0.30pp (38d lag). "
"Policy signal P=1 if DGS2-DTB3 fell over 63 trading days. Hike state H=1 if ICSA 4wk mean < "
"1.03 x 52wk min and DGS2-DTB3 rose over 63 days. Duration basket (50% IEF/50% TLT) weight: 0 if H, "
"1.25 if L&P, 0.75 if one of L/P, 0.35 otherwise; remainder (if positive) in SHY. Rebalance on each "
"new claims release day. use_policy=False sets P=0 everywhere (trial 3: labor only).")
hypothesis = ("Cieslak (2018): markets and forecasters underestimate how far the Fed cuts once the labor market "
"turns, so duration earns excess returns after claims/unemployment rise off cycle lows. Counterparty: "
"investors anchored on Fed guidance and late-cycle duration sellers. Fails in inflation shocks where "
"the Fed hikes into labor weakness, at the ZLB (P uninformative), and in 2020-21 when stale 52-week "
"minima keep L=1 during a bond selloff.")
macro_ids = ("ICSA", "UNRATE", "DGS2", "DTB3")
@classmethod
def default_params(cls):
return {"claims_mult": 1.10, "unrate_gap": 0.30, "hike_mult": 1.03, "policy_days": 63,
"use_policy": True, "w_both": 1.25, "w_one": 0.75, "w_none": 0.35}
def target_weights(self, data):
p = self.params
idx = data.dates
icsa = _releases(data.macro("ICSA"))
c4 = icsa.rolling(4, min_periods=4).mean()
cmin = icsa.rolling(52, min_periods=52).min()
cratio = (c4 / cmin).reindex(idx).ffill()
ur = _releases(data.macro("UNRATE"))
u3 = ur.rolling(3, min_periods=3).mean()
ugap = (u3 - u3.rolling(12, min_periods=12).min()).reindex(idx).ffill()
spread = (data.macro("DGS2") - data.macro("DTB3")).reindex(idx)
dspread = spread - spread.shift(p["policy_days"])
valid = cratio.notna() & ugap.notna() & dspread.notna()
L = ((cratio > p["claims_mult"]) | (ugap >= p["unrate_gap"] - 1e-9)).astype(float)
P = (dspread < 0).astype(float)
if not p["use_policy"]:
P = P * 0.0
H = ((cratio < p["hike_mult"]) & (dspread > 0)).astype(float)
n = L + P
b = pd.Series(np.where(n >= 2, p["w_both"], np.where(n == 1, p["w_one"], p["w_none"])), index=idx)
b[H >= 1] = 0.0
w = pd.DataFrame({"IEF": b * 0.5, "TLT": b * 0.5, "SHY": (1.0 - b).clip(lower=0.0)}, index=idx)
w[~valid] = np.nan
rel_days = icsa.index.intersection(idx)
return rebalance_on(w, rel_days).reindex(idx)