SD book

3. SD v2

I'm new here · 3/6 I'm working on SD itself · 2/6 I want to change the indicator · 2/4 I want to change a trading strategy · 1/5

5 min read

SD v2 keeps the paper's squeeze clock and changes what feeds it, how it ages, and what the final number means. This chapter walks the pipeline from raw bars to the SD number, in the order the code runs:

 bars ─▶ primitives.rs ─▶ features.rs ─────────────▶ engine.rs ─▶ Sd2Reading
          (ATR, σ, RSI,    (compression depth q,        (lean × intensity
           MACD, linreg)    tiers, clock, energy,         → SD, expected
                            momentum panel, decay)        move, probability)
                                    │
                                    └─▶ model.rs (fit once, frozen in sd2_model.json)

#1. Multi-scale compression

Problem: a short, deep squeeze is blurred by the 21-bar bands (chapter 2).

Fix (features.rs, CompressionMode::MultiScale): measure the depth twice, q21 = 2σ₂₁/ATR₂₁ and q10 = 2σ₁₀/ATR₁₀. The 10-bar gauge may raise the tier, but only when the 21-bar gauge already sees some compression, or when the 10-bar gauge alone sees a tier-3 squeeze.

ChoiceWhy
A second gauge, not a shorter main gauge10-bar bands are jumpy; as the only gauge they invent squeezes in every quiet week
It can only raise the tierthe paper's "highest tier reached" logic is untouched, so short squeezes now fire from their true height
A gate: 21-bar must agree, unless the fast gauge sees tier 3one calm week in a volatile market is not a coil

#2. Stored energy

Idea: a squeeze is a spring. Depth × duration is how hard it has been wound.

 energy += (2 − q21) + ½·(2 − q10)    every bar the squeeze is compressed
 energy  = frozen                      after it fires
 energy  = 0                           when a new squeeze starts, or 3θ after the fire

Measured: move size rises steadily from the lowest energy quartile to the highest, in both eras.

#3. Blended momentum

Problem: AO alone, and one noisy bar ends a fire.

Fix (features.rs, the momentum panel): eight gauges, each divided by ATR (or its own natural scale) and squashed with tanh into (−1, 1) so no single wild bar dominates:

GaugeMeasures
AO5 vs 34 bar midpoint averages (v1's gauge)
TTM linregslope-adjusted end of a regression line through price vs its 20-bar center
MACD histogramfast trend pulling away from slow, and accelerating
ROC(10)ten-day change, in units of normal ten-day travel
RSI(14)share of recent movement that was up
SMF EMA-8v1's smoothed derivative of the 8 EMA
EMA-8 − EMA-21short trend vs medium trend
6-month trend126-day return in units of 126-day volatility

blend is their average; agree is the share pointing the same way.

Flip rule (FlipMode::BlendedPersistent): a fire ends on a momentum change only when the blend changes sign and stays changed for two bars.

Measured: none of this predicts direction better than a coin flip (chapter 2). The blend is kept because it is a steadier description of momentum than AO alone: fewer false flips, and a lean that people can read. It is not sold as an edge.

#4. Time decay

 strength(phase) = 1                              while phase ≤ 1.5
                 = 2^−(phase − 1.5)                after   (halves every θ bars)
 phase = τ / θ

engine.rs::time_strength uses that curve for an active fire. A squeeze that is still coiling gets 0.5: momentum hints at a direction while the spring winds, but nothing has happened yet. With no squeeze behind it, momentum gets 0.25, the weakest kind of evidence.

Why this shape: it is the measured profile of post-fire move size (chapter 2's table), not a guess. Change it, and re-run sd_backtest anatomy to see whether your curve fits the data better.

#5. Expected movement

model.rs holds a ridge regression (a straight-line fit with a penalty that keeps weights small unless the data insists). It predicts:

 expected |move over 21 bars|  ÷  (σ₂₀ · √21)

from eleven direction-free inputs (research.rs::MAGNITUDE_NAMES): two volatility ratios, energy, both depths, tier, squeeze duration, decay, bandwidth percentile, and the strength (not sign) of momentum. It is fitted once on bars before 2019 and frozen into src/sd2/sd2_model.json, so the live indicator, the backtests and ThinkScript all use the same numbers.

The result is scaled back to percent with a noise-guarded volatility: min(σ₂₀, σ_robust). Here σ_robust is the volatility of overlapping 5-day returns ÷ √5 over 60 bars. Chapter 4 tells the story of why: bad prints inflate one-day volatility, and the guard stopped SD from mistaking noise for movement.

#6. The SD number

 lean      = tanh(2 · blend) × strength                ∈ [−1, 1]
 intensity = rank of expected move (10th → 90th pct)   ∈ [ 0, 1]
 SD        = 0.5 + 0.5 · lean · intensity
SD readsMeans
0.50nothing to say: small expected move, or no lean
0.60 / 0.40a moderate move expected, momentum leaning up / down
0.85 / 0.15a large move expected, with a strong, fresh lean

Alongside the number, Sd2Reading carries the expected move in percent, the fire phase, the energy, the tier, and calibrated_up_probability: the share of past readings in the same SD decile that rose. That probability is about 58% everywhere, which is SD's honest admission that the side is mostly drift. Show it to people.

Measured on 2019–2026 (sd_backtest verify):

Classic SDSD v2
ρ(|SD − 0.5|, |21-day move|)+0.003+0.171
Direction hit rate50.9%50.4%
"Always up"56.9%56.9%

#Where to find it

PieceFile
Rolling toolssrc/sd2/primitives.rs
Squeeze clock (shared with v1)src/sd2/squeeze.rs
v1, every barsrc/sd2/classic.rs
Features: compression, energy, momentum, decaysrc/sd2/features.rs
Logistic and ridge modelssrc/sd2/model.rs
The SD numbersrc/sd2/engine.rs
Frozen modelsrc/sd2/sd2_model.json (regenerate: sd_backtest fit)
Command linesrc/bin/sd2.rs
ThinkScriptIndicator/SahastaDirectionalityV2STUDY.ts (constants copied from the JSON)

Next: the steady backtester, the machine that produced every number above.