DLI 综合评分的构建方式:CISS 式二次聚合、净流动性流量主干、融资压力叠加,以及对照 NFCI/STLFSI 的完整验证记录——包括所有被否决的替代方案。
Date: 2026-05-05 Status: Implemented in lib/regime/engine.ts, weights in lib/indicators/config.ts. Replaces: Linear weighted average + Group-A momentum kicker (the "hybrid transform" model documented in 2026-05-DLI-validation.md and 2026-05-DLI-percentile.md).
2026-05-17 addendum: Group A no longer scores Net Liquidity = Fed BS − TGA − RRP as a single linear input. The public DLI remains one headline score, but the Policy/Reserves tier now scores Fed balance sheet, TGA, and ON RRP directly. ON RRP is a conditional depletion-transition signal: a recent large drawdown toward a low-buffer state can tighten DLI, but a low balance that has already been stable becomes dormant and does not keep penalizing the headline. Net Liquidity remains a contextual chart, not a direct scoring input.
2026-05-17 addendum 2 — policy-tier transform (level → change1y):
Symptom. The headline read 89–91 / risk-off ("tightest in 5y") while two independent coincident benchmarks said the opposite: Chicago Fed NFCI = −0.52 and St. Louis Fed STLFSI = −0.76 (both = looser-than-average / below-normal stress), and the DLI's own funding/credit/risk tiers were calm (SOFR-IORB neutral, HY easing, VIX neutral).
Diagnosis (backtest, lag-0 — coincident, not forward-return). Not the perf sweep (engine math bit-identical) and not PR #5-vs-#3 (the Net-Liquidity counterfactual read 88–89/risk-off too). Root cause is the trailing-5y percentile of the raw level on the policy stock variables (Fed BS, TGA): the 5y window straddles the 2021 QE→QT structural break, so post-QT levels are pinned at an extreme and the direction flip reads that as "tightest in 5y." Quantified vs NFCI: the level transform is anti-correlated in 2024-26 (Spearman −0.29) — the opposite of what a coincident gauge should do — and ≈ 0 over the full decade (0.04).
Fix. fed-balance-sheet and tga switched to tightnessTransform: 'change1y' (percentile of the trailing-365d change — a coincident flow, invariant to the QE level bubble). ON RRP keeps its depletion-transition rule; the other 9 indicators keep level (byte-identical). Rubric: lag-0 Spearman vs NFCI and STLFSI by sub-period + undisputed-episode gates. Reproduce via scripts/backtest/.
| transform | NFCI FULL | NFCI 2024-26 | 2021 (want loose) | 2022 QT (want tight) | now |
|---|---|---|---|---|---|
| level (old) | 0.04 | −0.29 | 17 | 42 | 91 / risk-off |
| change1y (chosen) | 0.42 | +0.57 | 1 ✓ | 71 ✓ | 45 / neutral |
| detrend | 0.11 | −0.18 | 0 | 28 ✗ (loose in peak QT) | 86 |
Honest caveat. vs STLFSI in 2024-26, level (+0.25) beats change1y (−0.23) — a single-benchmark conflict. Resolved by change1y's full-period dominance on both benchmarks, being the only variant to pass both episode gates, and NFCI being the apter financial-conditions benchmark. Limitations: sample 2016+ (no GFC); crisis dates show stance-vs-stress divergence by design; calibrated on 2016-26 — re-run scripts/backtest/ if a future structural regime breaks it.
Net effect. Headline 2026-05: 90/risk-off → 45/neutral, consistent with NFCI/STLFSI. The DLI still correctly reads 2021 as loosest and 2022 QT as tightening.
Robustness — the "fix the window length instead" hypothesis, tested and rejected. A natural objection: maybe level is fine and only its 5y baseline is wrong — just widen/narrow the window. Tested as switchable engine modes (level_4y, level_10y, level_long = expanding/full-history), same rubric:
| transform | NFCI full | NFCI 2024-26 | 2021-09 (loose) | 2022-10 QT (tight) | now | flood 20-21 mean |
|---|---|---|---|---|---|---|
| level (5y) | 0.04 | −0.29 | 1 ✓ | 42 ✗ | 91 ✗ | 15 ✓ |
| level_4y | 0.06 | −0.21 | 1 ✓ | 39 ✗ | 87 ✗ | 14 ✓ |
| level_10y | 0.04 | −0.14 | 1 ✓ | 45 ✗ | 79 ✗ | 15 ✓ |
| level_long | 0.04 | −0.14 | 1 ✓ | 45 ✗ | 79 ✗ | 15 ✓ |
| change1y | 0.42 | +0.57 | 7 ✓ | 71 ✓ | 46 ✓ | 17 ✓ |
No level window fixes it: the Fed balance sheet peaked ~mid-2022, so any fixed trailing-level window in 2026 (4y/5y/10y/full) still straddles the one-time QE→QT regime shift and pins the current level at an extreme; all level variants also under-read 2022's aggressive QT (~39-45). It is a transform-family problem (stock vs flow), not a window-length problem. Also tested: the sustained-flood gate (2020-06→2021-12 must read loose throughout, not just on point dates) — change1y passes (mean 17, 99% of days loose), so the "flow transform is stock-blind for the COVID flood" concern does not bite on the actual record (the flood ran sustained positive flow ~2y). The conceptual limitation (pure flow is blind to a hypothetical zero-flow abundant plateau) remains documented; the level_* modes are kept as auditable switchable options for re-checking if such a regime appears.
2026-05-23 addendum 3 — trading-day sub-index (tested, shipped):
Hypothesis. srf is calendar-day-filled (0 on non-operation days), so weekends leak into buildSubIndexHistory as forward-filled, near-zero-change rows. A 60-entry rolling-correlation window then runs ~28% on weekend noise. Trading-day filtering of the sub-index index — drop Sat/Sun before computing Σ — should give a genuine 60-trading-day correlation matrix and a less-diluted CISS. Three mechanical predictions: (1) Σ less biased toward identity, so CISS reads structurally higher; (2) the EWMA-span=10 stops being trading-time-compressed to ~7 trading days; (3) DLI matches its underlying trading-day semantics (NFCI/STLFSI/ANFCI/KCFSI are all weekly).
Diagnosis (backtest, lag-0 — coincident, not forward-return). Same rubric as addendum 2 expanded to four independent stress benchmarks plus regime-conditional separation. Snapshot 2026-05-18, 10y of data per series. Reproduce via scripts/backtest/weekday-index-validate.ts.
Statistical noise floor: 95% CI on Spearman ρ ≈ ±2/√(n−2). For n=517 (FULL) → ±0.09; n≈124 (2024-26) → ±0.18; n=28 (KCFSI 2024-26) → ±0.40. Treat |Δρ| within ±2 SE as noise.
| variant | NFCI FULL | NFCI 24-26 | STLFSI FULL | STLFSI 24-26 | ANFCI FULL | ANFCI 24-26 | KCFSI FULL | KCFSI 24-26 |
|---|---|---|---|---|---|---|---|---|
| calendar-day | 0.398 | 0.570 | 0.118 | −0.209 | 0.359 | 0.407 | 0.182 | 0.359 |
| trading-day (chosen) | 0.397 | 0.612 | 0.106 | −0.232 | 0.353 | 0.438 | 0.189 | 0.418 |
| Δρ | −0.001 | ▲+0.042 | −0.011 | −0.024 | −0.006 | ▲+0.032 | ▲+0.007 | ▲+0.059 |
Regime-conditional separation (gap = mean(bench | risk-off) − mean(bench | risk-on); larger = better discriminator):
| benchmark | calendar-day | trading-day | Δgap |
|---|---|---|---|
| NFCI | 0.138 | 0.142 | +0.004 |
| STLFSI4 | 0.206 | 0.204 | −0.002 |
| ANFCI | 0.125 | 0.130 | +0.005 |
| KCFSI | 0.218 | 0.202 | −0.016 |
Episode gates (mean P across the window):
| calendar-day | trading-day | |
|---|---|---|
| 2020-06..2021-12 (want loose) | P20.1 | P20.4 |
| 2022-06..2023-01 (want tight) | P67.1 | P65.0 |
| now (2026-05-18) | P40 | P44 |
Decision: ship trading-day as default. Read the table as direction-consistency, not dominance:
The case is not as strong as addendum 2's change1y (NFCI FULL +0.38) — but it does not need to be. change1y was a conceptual shift in the transform family (stock vs flow); the bar there was about adopting a new model. Trading-day is a mechanical cleanup of an implementation detail (srf's calendar-day fill leaking into the index); the bar is about whether the cleanup is net positive on independent benchmarks. With 3/4 recent-window benchmarks directionally improved, full-period tracking preserved, regime gap preserved, and the mechanical argument self-consistent with the observed headline direction, the cleanup ships.
What changed in the engine. buildSubIndexHistory and calculateDLIHistory now default weekdayOnly: true. dli-history's in-process cache key is bumped v2 → v3. The legacy calendar-day index is still reachable via weekdayOnly: false for backtest reproduction (scripts/backtest/weekday-index-validate.ts exercises both).
2026-06-14 addendum 4 — Group-A base-effect regression (change1y → change_med1y), SRF zero-floor, NOW-consistency guardrail:
Symptom (user-reported). Headline read P64 / neutral ("leaning tight") while a loyal user pointed out the opposite was happening in the world: TGA was falling sharply (week of 2026-06, $904B → $799B), Net Liquidity was rebounding, funding was easy. Independent benchmarks agreed it was loose: NFCI at its 5y p25, STLFSI4 at p4, ANFCI/KCFSI loose.
Diagnosis (lag-0, reproduced exactly: P64, CISS 0.4264). The driver decomposition showed TGA at tightness p0.94, "tightening", contributing ~3× the next driver and dominating the 0.65-weight Group A. Root cause: the change1y transform percentiles the trailing-365d change as a single point-to-point difference, so its anchor is whatever the level was exactly one year ago. One year before the reading, TGA sat at its 2025 debt-ceiling trough (~$277B); the YoY change was therefore a distorted +$500B (p0.93) even though over the trailing 3 months TGA was flat-to-down (tightness ≈ 0.49). A pure base effect — TGA was not tightening. This is why the May addendum-2 validation passed and June broke: lag-0 Spearman and episode gates measure co-movement and historical-episode levels; they are blind to an endpoint level distortion. The change1y still scored NFCI FULL ρ ≈ 0.40 even with the endpoint at p0.94.
Rejected fix — reserve adequacy. The intuitive structural fix (re-anchor Group A on normalized reserves: Reserves/GDP or /bank-assets, distance-to-LCLoR) was tested and rejected: as a level signal it anti-tracks the benchmarks in 2024-26 (Reserves/GDP vs NFCI ρ = −0.745). In an ample-reserves regime, reserves fell through QT while financial conditions loosened (reserves stayed above scarcity). The level of reserves is not informative about conditions when reserves are ample; the flow is. So the fix keeps a flow-based Group A — it just makes the flow robust to a single-point anchor.
Fix. tga and fed-balance-sheet → tightnessTransform: 'change_med1y' = percentile of (level − trailing-1y median level). The median ignores a ≤~2-month transient (a debt-ceiling trough is ~2/12 of the window) while preserving the slow-cycle flow signal change1y was chosen for. Backtest 2026-06-11 (snapshot merged to fresh tails), same rubric as addendum 2 + a new NOW-consistency check:
| Group-A transform | NOW | NFCI FULL | NFCI 24-26 | ANFCI FULL | KCFSI FULL | gates loose/tight |
|---|---|---|---|---|---|---|
| change1y (old) | P64 ✗ | 0.402 | 0.606 | 0.348 | 0.183 | 19.8 / 65.0 |
| change_blend (Δ3/6/12m avg) | P38 | 0.282 | 0.461 | 0.263 | 0.079 | 20.1 / 59.9 |
| change_90d | P28 | 0.326 | 0.355 | 0.285 | 0.111 | 18.3 / 62.9 |
| change_180d | P17 | 0.339 | 0.463 | 0.338 | 0.074 | 12.8 / 61.2 |
| change_med1y (chosen) | P19 ✓ | 0.377 | 0.407 | 0.347 | 0.134 | 16.6 / 67.2 |
change_med1y is the only variant that fixes the NOW reading (P64→P19, consistent with NFCI p25 / STLFSI p4) and preserves full-period Spearman within 1 SE on all four benchmarks and improves both episode gates (better loose/tight separation). The short single horizons get NOW right but degrade 24-26 regime gaps (STLFSI/KCFSI go slightly negative); change_blend dilutes the signal too much. It is not overfit: median-baseline is a general robustness construction, and the improvement is on out-of-sample episode gates, not just "now". change_blend is retained in the engine as an auditable switchable option.
SRF zero-floor (secondary). SRF is a backstop facility, 0 on ~93% of days; operationally-zero usage rounds to ~$0.001B, which a <=/mid-rank percentile ranked above the exact-0 block → SRF read p0.78 "tight" with no usage. Added IndicatorConfig.zeroFloorEpsilon (set to 0.05 = $50M for SRF only): sub-epsilon readings snap to the 0 floor, and a backstop at its floor scores at its loosest (genuine usage still ranks high). Byte-identical for every other indicator (default eps 0). Combined headline → P16 / risk-on, all benchmark ρ preserved-or-improved, gates 17.9 / 68.0.
Process fix — the missing guardrail. scripts/backtest/dli-benchmark-validate.ts re-runs the full rubric and adds a NOW-consistency assertion: the headline's current 5y percentile must be within 30 pts of the benchmark median (each benchmark's own 5y percentile). The old change1y config fails it (DLI p64 vs median p25, gap +39); the new config passes (p16 vs p25, gap −9). Run after any transform/weight change. This is the check that would have caught the regression at ship time.
Net effect. Headline P64/neutral → P16/risk-on, TGA driver p0.94 "tightening" → p0.40 "neutral", consistent with the user's observation and all four independent benchmarks. The DLI still reads 2021 loosest and 2022 QT tightening.
Documented follow-ups (not in this change). (1) bank-cash-buffer (weekly H.8, ~9d publication lag) drops at the 14-day staleness gate when the snapshot is itself a few days old, thinning Group C to one indicator — consider a per-indicator staleness tolerance. (2) real-yield-10y enters Group D as a raw level percentile (currently p0.95); high real yields are policy-restrictiveness more than dollar-liquidity scarcity — worth revisiting whether the level or a change belongs in a liquidity gauge. (3) Group A's 0.65 weight remains a single-tier concentration risk; change_med1y makes it robust, but down-weighting A is an available further lever if a future Group-A artifact appears.
The prior linear weighted-average aggregator
DLI_level_t = 0.85·s_A_t + 0.05·s_B_t + 0.05·s_C_t + 0.05·s_D_t
DLI_tight_t = DLI_level_t + α · (s_A_t − s_A_{t−60})
has a structural defect: a 95th-percentile reading on a single stress indicator (VIX, HY OAS, FRA-OIS, SOFR-IORB) shifts DLI_level by ~3.5% of the neutral→tight P50→P80 span. Stress events that historically coincided with -8% to -15% SPX 60-day drawdowns were absorbed as background noise. The flaw is not the weights; it is the assumption that signals are independent and their information is linear in z. Stress indicators are fat-tailed and co-move during regime transitions.
Following Hollo, Kremer, and Lo Duca (2012, ECB CISS):
lower_worse indicators (Fed BS, reserves), the value-percentile is flipped so 1 = "tightest reading observed in the window". ON RRP is the exception: it is mapped through a conditional depletion-transition rule before aggregation.ON RRP rule:
s_g_t = mean of tightness percentiles of indicators in group g. All four groups are bounded in [0, 1].Σ_t is the Pearson correlation of the first differences of (s_A, s_B, s_C, s_D) over the trailing 60 days. Falls back to identity for the warm-up window.CISS_t = √( s_t' · (W ∘ Σ_t) · s_t )
where W = w · w' is the outer product of base group weights, ∘ is the Hadamard product, and s_t = (s_A_t, s_B_t, s_C_t, s_D_t). Missing groups are excluded with weight renormalization on the available subset.
tightnessScore is the percentile rank of CISS_t within the visible window; status uses P20 / P80 cuts on the rolling 5-year CISS history (≤P20 = risk-on / Loose, ≥P80 = risk-off / Tight, else neutral).Σ_t off-diagonals → +1), the quadratic form amplifies. When they offset (Σ_t near 0 or negative), components dampen. This captures the mechanism the linear average ignored.CISS_t ∈ [0, 1] for any percentile-bounded inputs.√(Σ_g w_g² · s_g²) — close to but slightly less than the linear weighted average. So the model continues to behave reasonably when the correlation window has insufficient data.| Group | Old (linear) | New (CISS) | Rationale |
|---|---|---|---|
| A — Policy / Reserves | 0.85 | 0.65 | Still dominant for level direction; CISS's quadratic structure means weight isn't the whole story |
| B — Funding | 0.05 | 0.10 | Real-time funding stress (FRA-OIS, SRF) deserves more pull |
| C — Credit | 0.05 | 0.05 | HY OAS + cash buffers — kept at 0.05 since signals are slower-moving |
| D — Risk / Price | 0.05 | 0.20 | VIX / HY / real yields — CISS amplifies these via co-movement, not flat weight |
The Group-A momentum kicker (DLI_A_MOMENTUM_COEF) is retired. CISS's correlation structure replaces the manual momentum hack — fast-moving groups gain influence when they co-move with others, dynamically and without a tuned coefficient.
| Date | Event | CISS | tightnessScore | status | s_A | s_B |
|---|---|---|---|---|---|---|
| 2018-12-24 | December 2018 selloff | 0.813 | 86 | risk-off ✓ | 0.93 | 0.99 |
| 2020-03-16 | COVID crash | 0.905 | 98 | risk-off ✓ | 0.97 | 1.00 |
| 2020-03-23 | COVID trough (Fed acts) | 0.847 | 92 | risk-off ✓ | 0.91 | 0.92 |
| 2022-06-15 | Mid-2022 rate-hike vol | 0.667 | 65 | neutral | 0.74 | 0.88 |
| 2022-10-14 | Cycle bottom | 0.539 | 40 | neutral | 0.61 | 0.87 |
| 2023-03-13 | SVB collapse | 0.144 | 5 | risk-on | 0.10 | 0.85 |
| 2024-08-05 | Aug 2024 carry unwind | 0.612 | 53 | neutral | 0.66 | 0.78 |
Notable: 2023-03-13 reads "Loose" because the BTFP emergency facility had already injected massive liquidity (s_A = 0.10, very loose). Funding stress (s_B = 0.85) was offset by the policy response. Empirically SPX bottomed within days of this date and rallied +7% over the following 60 days, consistent with the Loose classification's expected behavior.
| Metric | Value | Target | OK? |
|---|---|---|---|
Days in risk-on | 725 (20.0%) | ~20% (by P20 cut design) | ✓ |
Days in neutral | 2170 (59.9%) | ~60% | ✓ |
Days in risk-off | 726 (20.0%) | ~20% | ✓ |
| Regime flips per year | 12.0 | 6–15 | ✓ |
| Quantile | Value |
|---|---|
| min | 0.110 |
| p10 | 0.182 |
| p20 (Loose cut) | 0.293 |
| p50 | 0.603 |
| p80 (Tight cut) | 0.777 |
| p90 | 0.837 |
| p99 | 0.928 |
| max | 0.977 |
Healthy spread across [0, 1]; no clustering at the tails.
A first-pass 20-day / 60-day forward-return backtest produced internally inconsistent results (SPX/QQQ "Tight" regimes showed higher forward returns than "Loose" regimes — a mean-reversion artifact from catching market bottoms). The fix is not to re-tune; it is to recognize that forward-return validation is methodologically wrong for this kind of indicator:
Replaces the forward-return tables with Pearson(ΔDLI[t-h, t], log(asset_t / asset_{t-h})) at horizons h ∈ {1, 5, 10, 20} sliding across the full overlap. Expected pattern for a valid state indicator: |ρ| largest near h=1 and decay toward zero by h≈20 as the regime label drifts away from the asset window's endpoint.
Observed (full 10y overlap):
| Asset | h=1d | h=5d | h=10d | h=20d | n |
|---|---|---|---|---|---|
| SPX | −0.040 | −0.045 | −0.049 | −0.034 | 1394 |
| QQQ | −0.052 | −0.046 | −0.077 | −0.056 | 1394 |
| BTC | +0.001 | −0.019 | −0.029 | −0.043 | 3601 |
| GOLD | −0.008 | −0.035 | −0.046 | −0.109 | 1391 |
Honest reading:
A potential future diagnostic worth adding: regime-conditional ρ — correlation computed only over Tight regime windows vs only over Loose regime windows. We expect the absolute value to be markedly larger inside stress regimes than over the full sample (i.e., DLI is informative when it matters).
DLI under CISS aggregation is a point-in-time state indicator, period. It tells investors: "today, dollar liquidity is loose / neutral / tight." The historical chart shows how risk assets coincided with similar past regimes; the coincident drawdown card quantifies "during DLI Tight, BTC averaged this drawdown." There is no forward-return prediction layer, deliberately.
The asset comparison page's value lives in the regime band visualization (qualitative, contemporaneous) and the coincident drawdown / correlation diagnostic (quantitative, contemporaneous). Forward-return tables were removed.
lib/regime/engine.ts — full rewrite around tightnessPercentile01, computeGroupPercentile, rollingCorrelation, cissAggregate, calculateDLIHistory, calculateDLI.lib/indicators/config.ts — group weights 0.65/0.10/0.05/0.20, all groups now transform: 'percentile', DLI_PERCENTILE_WINDOW_DAYS = 1825 (5y), new DLI_CISS_CORRELATION_WINDOW_DAYS = 60.lib/data/asset-overlay.ts — cache key bumped v7-raw → v8-ciss.The DLIState, DLIHistoryPoint, and RegimeState API shapes are unchanged. compositeScore is now (0.5 − CISS) × 2, mapping CISS [0, 1] to a sign-aligned [+1, −1] composite for backward compatibility with iOS thresholds.
The CISS aggregation got the math of co-movement right but was solving the wrong problem. Empirically the CISS headline correlated ~+0.28 with NFCI (a financial-conditions index) but only ~−0.10 with the net-liquidity 3-month flow that users mean by "dollar liquidity." It was, in effect, a weak financial-conditions index wearing a dollar-liquidity name. Every Group-A patch (level → change1y → change_med1y → the addendum-4 base-effect fix) was treating a symptom of that structural mismatch; the recurring user complaint ("liquidity is clearly loosening, why does the score say tight?") was the mismatch surfacing. The 2026-06 decision (user: "直接重构为净流动性评分") was to make the spine of the headline the net-liquidity flow itself.
lib/regime/netliq.ts)netLiquidity = WALCL − TGA − ON RRP ($T, components ffill onto a business-day grid)
nlSmooth = EMA₁₀(netLiquidity)
flow6mEquiv = ⅔·Δ₁₈₂(nlSmooth) + ⅓·2·Δ₉₁(nlSmooth) ($T 6-month-equivalent flow)
impulse = clamp(0.5 − flow6mEquiv / 1.5, 0, 1) (0 = max loosening … 1 = max drain)
fundingStress = EMA₁₀( max( (SOFR-IORB−2)/13 , SRF≥0.05?SRF/20:0 ) ) (acute plumbing override, [0,1])
tight = EMA₁₀( 1 − (1−impulse)·(1−fundingStress) ) (noisy-OR, smoothed)
headline = round(tight · 100) (ABSOLUTE 0-100, not a percentile)
status : tight < 0.33 risk-on (loose) / > 0.67 risk-off (tight)
Constants re-derived from 2016-2026 data in scripts/backtest/netliq-calibrate.ts (the prior session's offline calibration was never persisted, so it was re-derived from scratch — and two of its claimed numbers did not reproduce; see below). FLOW_SCALE = 1.5 ≈ 2× the 90th-percentile |6m-equiv flow|.
impulse tracks the NL 6m flow at ρ ≈ −0.90; the full headline at −0.55 (the gap is the funding override, by design).This is now a dollar-liquidity gauge, not a financial-conditions index. The headline is driven only by the Fed/Treasury liquidity flow (Policy/A) and funding plumbing (Funding/B). VIX, HY spread, the dollar, real yields (Credit/C, Risk/D) are no longer in the headline — they remain as context panels (LiquidityMap) and on the indicator pages. Consequence: an equity-stress event with calm plumbing and a rising balance sheet (SVB-March-2023: net-liq read ~38) reads neutral-to-loose here, which is correct for a liquidity index. The drivers (DriverWaterfall) now decompose the net-liquidity flow into Fed BS / TGA / ON RRP component flows + the funding override, using existing indicator IDs.
DLIState / DLIHistoryPoint / RegimeState shapes and field directions are unchanged — every consumer keeps working. What changed: percentile5y / tightnessScore are now an absolute 0-100 (round(tight·100)), status uses fixed 0.33/0.67 thresholds, subScores A = impulse / B = funding / C,D = the existing percentile panels, contributions/drivers = the net-liquidity flow decomposition. dliTight remains a [0,1] tightness; compositeScore remains (0.5 − dliTight) × 2.
The legacy CISS implementation (buildSubIndexHistory, rollingCorrelation, cissAggregate, ewmaCissSeries, rolling5yPercentileStatus) was deleted from engine.ts; it is recoverable from git history (the commit prior to this addendum) if a rollback or A/B is ever needed. computeGroupPercentile / tightnessPercentile01 are retained for the C/D context panels.
lib/regime/netliq.ts — the model (new, self-contained, unit-tested in netliq.test.ts).lib/regime/engine.ts — calculateDLI / calculateDLIHistory / calculateRegime now delegate the headline to netliq; C/D via computeGroupPercentile.scripts/backtest/netliq-calibrate.ts — constant derivation + full validation harness.Approved sprint (user): upgrade the DLI score, state, AND today-highlights into a deployable, backtest-verifiable feature. Four changes, all validated against the real engine (scripts/backtest/dli-benchmark-validate.ts, NOW-consistency gate ≤30) and locked by unit tests (lib/regime/netliq.test.ts, 20 cases). Live reading unchanged at 40 / neutral — every change is dormant today by design and arms for the next drain-into-a-thin-buffer rather than re-pricing the present.
The problem it fixes. The impulse divided the 6m-equiv net-liquidity flow by a fixed $T scale (NL_FLOW_SCALE_T = 1.5). So a $200B 6-month drain read the same whether reserves were abundant (2021, cushion ~26%) or thin (now, ~12%). But the reserve-demand curve is convex: near scarcity a drain spikes funding rates; when abundant it is absorbed. The fixed scale is blind to that.
Why not a reserve LEVEL term (the obvious fix, already rejected). Adding reserves/GDP or a buffer level directly anti-tracks conditions in an ample-reserve regime (Reserves/GDP vs NFCI ρ = −0.745, 2024-26; see addendum 4): reserves fell through QT while conditions loosened. The level is not informative when reserves are ample — the flow is.
The fix. A gain on the flow, not a level term, and asymmetric (drains only):
g(buffer) = 1 + STRENGTH · smoothstep((FLOOR2 − buffer)/(FLOOR2 − FLOOR1)) ∈ [1, 1+STRENGTH]
flowGained = flow < 0 ? flow · g(buffer) : flow # amplify DRAINS only; injections unchanged
impulse = clamp(0.5 − flowGained / NL_FLOW_SCALE_T, 0, 1)
with FLOOR1 = 8 (the 2019 repo-crisis buffer floor), FLOOR2 = 14 (the classifyBuffer "thin" cut), STRENGTH = 1.5 — all empirical, not fitted. The buffer is the existing reserve-buffer series ((reserves + ON RRP) ÷ commercial-bank assets, %).
Why the asymmetry is load-bearing. In 2024-26 the buffer thinned toward 12-14% BUT net liquidity was rebounding (QT ending → flow > 0). A symmetric gain would have amplified that loosening or, worse, re-created the level-trap false-tighten. Drains-only means the gain is dormant whenever flow ≥ 0 — which is most of 2024-26 and today — so it cannot reproduce the ρ = −0.745 failure. It bites only when a drain resumes into a thin buffer: the early-warning window the funding override misses (mid-2019 had override ≈ 0 for months while the buffer sat at 8-9% and drained).
Backtest (real engine, drains-only gain 8/14/1.5):
| gate | baseline | with gain | note |
|---|---|---|---|
| NOW | 40 / neutral | 40 / neutral | flow injecting → gain dormant → NOW-consistency gap +4 PASS |
| 2019 repo (Sep-Oct) | 72.9 | 78.8 | thin buffer + draining → correctly amplified |
| 2021 QE (loose) | 21.2 | 21.2 | buffer ample → gain = 1, unchanged |
| 2022 QT (tight) | 77.4 | 77.4 | buffer ample → unchanged |
| SVB 2023 | 37.6 | 37.6 | unchanged |
| worst single-day move | 3.60 | 3.60 | no whipsaw introduced |
Since-2024 footprint: the gain moves the headline ≥1pt on ~22% of days, max +9pt, and every lift coincides with a real late-2025 scarcity episode (Nov-2025 DLI 93, SOFR-IORB +32bps) — no false positives in the danger window. Constants: NL_RESERVE_GAIN_FLOOR1_PCT, NL_RESERVE_GAIN_FLOOR2_PCT, NL_RESERVE_GAIN_STRENGTH.
The funding override's SOFR leg used only the volume-weighted median SOFR-IORB. But the 99th-percentile SOFR (the tail of the transaction distribution) blows out at quarter-/year-end settlement crunches before the median moves — e.g. 2024-09-19 printed a +44bps tail while the median was −8bps; today the tail is +8bps while the median is −2bps. Added a third max() leg to the override: (SOFR99−IORB − 10bps)/(50−10), floored at 10bps (the tail's ~p90, so routine quarter-end noise is ignored) and capped at 50bps (near the 2021-26 observed max of 55). Constants NL_SOFR99_FLOOR_BPS, NL_SOFR99_CAP_BPS.
No new data dependency: the NY Fed SOFR API already returns percentPercentile99 in the same payload sofr-iorb already fetches. New computed series sofr99-iorb (in lib/data/series.ts, from the same NY-Fed-SOFR + FRED-IORB calls). Gate-neutral (all episode gates and NOW unchanged); it produces small, transient, correctly-timed bumps on the ~8 quarter-end dates the median leg was blind to, decaying via the existing 15-day floor. When the series is absent (pre-first-refresh) the leg is simply dormant — graceful degradation, verified.
The headline score answers "which way is dollar liquidity moving" (the FLOW). It does not answer "how much cushion is left if a drain hits" (the STOCK of slack). Those are different questions, and conflating them was the root of the recurring "score says neutral but the system is clearly fragile" complaint. The state now carries a second, orthogonal vulnerability axis (RegimeVulnerability = 'low' | 'elevated' | 'high'):
The high conjunction is what keeps this from re-creating the reserve-level trap: 2022-24 read thin on some measures but the plumbing was dead calm and the buffer was ≥14%, so it stays low/elevated, never high. Validated trajectory: low throughout 2024-06→2025-08 (even when funding twitched, because the cushion was adequate) → elevated 2025-09 (buffer crossed 14% — early warning) → high 2025-10→2026-03 (the real crisis) → elevated since 2026-04 (funding subsided, buffer still thin). The axis leads the score at turns and cleanly separates fragile from stressed. Constants NL_BUFFER_THIN_PCT, NL_VULN_FUNDING_TWITCH.
Surfaced in three places: the RegimeCard secondary metrics (a "Structural State: Fragile (11.9%)" chip), the FlowVsBufferCard synthesizing verdict (vulnerability-aware copy across all four locales), and the home "today's takeaway" (a fragility clause when the axis diverges from the score). Exposed on /api/snapshot (regime.vulnerability, regime.buffer).
The AssetOverlayChart "How long does Net Liquidity lead BTC?" panel contradicted the project's validated coincident-not-leading stance (a multi-month "lead" is weak and unstable out-of-sample; the DLI's relationship with assets is regime-level and contemporaneous). Downgraded: collapsed by default (was default-expanded "centerpiece"), copy reframed across all four locales from a predictive-lead claim to a coincident co-movement profile ("descriptive, not predictive"), and the emphasized table cell moved from the spurious "best lag" to the lag-0 (contemporaneous) column, where a coincident gauge should peak. The data stays available one click away; the leading narrative is gone.
lib/regime/netliq.ts — asymmetric reserveGainAt, the SOFR99 tail leg in the funding override, and the buffer / vulnerability series (+ Vulnerability type). Constants NL_RESERVE_GAIN_*, NL_SOFR99_*, NL_BUFFER_THIN_PCT, NL_VULN_FUNDING_TWITCH.lib/regime/engine.ts — vulnerability / buffer threaded through calculateDLI / calculateDLIHistory / calculateRegime.lib/regime/types.ts — RegimeVulnerability; vulnerability/buffer on DLIState / RegimeState / DLIHistoryPoint.lib/data/series.ts + lib/data/nyfed.ts — sofr99-iorb fetcher (NY Fed percentPercentile99 − FRED IORB, bps).components/regime/FlowVsBufferCard.tsx, components/regime/RegimeCard.tsx, app/[locale]/page.tsx — 2D-state UI + today-highlights.components/history/AssetOverlayChart.tsx — lead-lag panel downgrade.app/api/snapshot/route.ts — regime.vulnerability / regime.buffer in the API.scripts/backtest/dli-benchmark-validate.ts (NOW-consistency gate, PASS gap +4), lib/regime/netliq.test.ts (20 cases).