//@version=5
indicator(title=”Option3 Dual Tracker — Trendiness + Volatility (3-mode each)”,
shorttitle=”DualTracker”,
overlay=false,
max_bars_back=500,
max_labels_count=100)
//=============================================================================
// INFORMATION
//=============================================================================
//
// Option 3: Score-Driven + Change-Point Hazard Dual Tracker
// Two parallel trackers (TrendTracker + VolTracker), each with 3 sub-modes
// (Up / Down / Null). Produces regime likelihood surface, NOT trade signals.
//
// Pine Script v5 | OHLCV-only | No repainting | No lookahead
// Blueprint: blueprint.md (100% faithful)
// Style ref: kortex.pine (helpers, guards, stability patterns)
//
// NAMING:
// Trend tracker prefix: Tr (mTrUp, sTrUp, wTrUp, etc.)
// Vol tracker prefix: Vo (mVoUp, sVoUp, wVoUp, etc.)
//=============================================================================
// CONSTANTS
//=============================================================================
float EPS = 1e-9
float EPS_SIG = 1e-8
float EPS_DEN = 1e-12
float IQR_DIVISOR = 1.349
float IQR_FALLBACK = 1e-4
float UNIFORM_W = 1.0 / 3.0
float LOG3 = math.log(3.0)
float INIT_VAR = 1e-4
float M_MIN = -5.0
float M_MAX = 5.0
// Regime label enum
int REG_NEUTRAL = 0
int REG_BREAKOUT = 1
int REG_TREND_LOWVOL = 2
int REG_CHOP = 3
int REG_WHIPSAW_RISK = 4
int REG_EVENT_STRESS = 5
//=============================================================================
// INPUTS (Parameter Dictionary — blueprint §9)
//=============================================================================
// — Trend Target —
string GROUP_TR_TGT = “Trend Target”
int erLenInput = input.int(20, “erLen”, minval=8, maxval=64, group=GROUP_TR_TGT, tooltip=”ER lookback (Kaufman)”)
// — Vol Target —
string GROUP_VO_TGT = “Vol Target”
int volAnchorInput = input.int(96, “volAnchorLen”, minval=24, maxval=240, group=GROUP_VO_TGT, tooltip=”ATR long anchor length”)
// — Scale —
string GROUP_SCALE = “Scale”
int iqrLenInput = input.int(96, “iqrLen”, minval=48, maxval=300, group=GROUP_SCALE, tooltip=”IQR window for robust scale”)
float clipKInput = input.float(4.0, “clipK”, minval=2.0, maxval=10.0, step=0.5, group=GROUP_SCALE, tooltip=”Clip multiple over IQR scale”)
float betaBaseInput = input.float(0.95, “betaBase”, minval=0.85, maxval=0.99, step=0.01, group=GROUP_SCALE, tooltip=”EWMA beta for base scale”)
// — Student-t —
string GROUP_STUT = “Student-t”
float nuBaseInput = input.float(5.0, “nuBase”, minval=2.1, maxval=30.0, step=0.1, group=GROUP_STUT, tooltip=”Degrees of freedom (heavy-tail)”)
// — Mean Update (Score-driven) —
string GROUP_MEAN = “Score Update”
float alphaMInput = input.float(0.05, “alphaM”, minval=0.01, maxval=0.30, step=0.01, group=GROUP_MEAN, tooltip=”Score step size for mean update”)
float gCapMultInput = input.float(3.0, “gCapMult”, minval=1.0, maxval=10.0, step=0.5, group=GROUP_MEAN, tooltip=”Scaled-score clamp (× sBase)”)
// — Var Update —
string GROUP_VAR = “Var Update”
float betaSInput = input.float(0.95, “betaS”, minval=0.85, maxval=0.99, step=0.01, group=GROUP_VAR, tooltip=”Variance EWMA beta”)
float eCapMultInput = input.float(6.0, “eCapMult”, minval=3.0, maxval=20.0, step=0.5, group=GROUP_VAR, tooltip=”Residual winsor cap (× sBase)”)
float kVarFloorInput = input.float(1.0, “kVarFloor”, minval=0.5, maxval=3.0, step=0.1, group=GROUP_VAR, tooltip=”Variance floor = k × sBase²”)
// — Markov —
string GROUP_MARKOV = “Markov”
float pStayBaseInput = input.float(0.90, “pStayBase”, minval=0.60, maxval=0.99, step=0.01, group=GROUP_MARKOV, tooltip=”Base sticky-prior self-transition”)
float pStayMinInput = input.float(0.60, “pStayMin”, minval=0.10, maxval=0.80, step=0.01, group=GROUP_MARKOV, tooltip=”pStay after hazard break”)
// — Hazard (CUSUM) —
string GROUP_HAZ = “Hazard”
float cusumKInput = input.float(0.5, “cusumK”, minval=0.1, maxval=1.0, step=0.1, group=GROUP_HAZ, tooltip=”CUSUM drift k (NIST)”)
float cusumHInput = input.float(6.0, “cusumH”, minval=3.0, maxval=15.0, step=0.5, group=GROUP_HAZ, tooltip=”CUSUM threshold h (NIST)”)
int cooldownInput = input.int(8, “cooldownBars”, minval=1, maxval=50, group=GROUP_HAZ, tooltip=”Post-break cooldown bars”)
// — Fusion —
string GROUP_FUSE = “Fusion”
float wDomInput = input.float(0.60, “wDom”, minval=0.45, maxval=0.80, step=0.01, group=GROUP_FUSE, tooltip=”Dominant weight threshold”)
float confMinInput = input.float(0.55, “confMin”, minval=0.30, maxval=0.80, step=0.01, group=GROUP_FUSE, tooltip=”Min confidence for regime label”)
// — Debug —
string GROUP_DBG = “Debug”
bool showDebugInput = input.bool(false, “Show debug plots”, group=GROUP_DBG)
//=============================================================================
// HELPER FUNCTIONS (kortex.pine style)
//=============================================================================
fSafeDiv(float num, float den, float eps) =>
float epsSafe = math.max(EPS_DEN, eps)
float denSafe = math.abs(den) > epsSafe ? den : (den >= 0.0 ? epsSafe : -epsSafe)
num / denSafe
fWinsor(float x, float capAbs) =>
float capEff = math.max(0.0, nz(capAbs, 0.0))
math.max(-capEff, math.min(capEff, x))
fClamp01(float x) =>
math.max(0.0, math.min(1.0, x))
fGuardSig(float x) =>
math.max(EPS_SIG, x)
fGuardDen(float x) =>
math.max(EPS_DEN, x)
// Robust IQR scale with stdev fallback (per kortex.pine fIqrScale).
fIqrScale(float x, int len) =>
float q25 = ta.percentile_linear_interpolation(x, len, 25)
float q75 = ta.percentile_linear_interpolation(x, len, 75)
float iqrScale = (q75 – q25) / IQR_DIVISOR
float fallback = nz(ta.stdev(x, len), IQR_FALLBACK)
float robust = (not na(iqrScale) and iqrScale > 0.0) ? iqrScale : fallback
fGuardSig(robust)
// Entropy-based confidence (blueprint §7.1): conf = 1 – H/log(3)
fEntropyConf(float w1, float w2, float w3) =>
float h = -(w1 * math.log(w1 + EPS) + w2 * math.log(w2 + EPS) + w3 * math.log(w3 + EPS))
fClamp01(1.0 – h / LOG3)
//=============================================================================
// CORE DATA (OHLCV-safe)
//=============================================================================
float closeSafe = fGuardSig(close)
float closePrev = fGuardSig(nz(close[1], close))
float openSafe = fGuardSig(open)
float highSafe = fGuardSig(high)
float lowSafe = fGuardSig(low)
float logClose = math.log(closeSafe)
//=============================================================================
// WARMUP
//=============================================================================
int warmupLen = math.max(erLenInput, math.max(volAnchorInput, iqrLenInput))
bool isWarmup = bar_index < warmupLen
//=============================================================================
// STEP 1 — DIMENSIONLESS TARGETS (blueprint §1)
//=============================================================================
// — 1.1 TrendTracker target: deltaER / sBase (SNR normalized) —
// Kaufman ER: |ln(C_t) – ln(C_{t-L})| / sum(|ln(C_i/C_{i-1})|, L)
float trNetChange = math.abs(logClose – nz(logClose[erLenInput], logClose))
float trLogRet = math.log(closeSafe / closePrev)
float trSumAbsRet = math.sum(math.abs(trLogRet), erLenInput)
float erRaw = fSafeDiv(trNetChange, trSumAbsRet, EPS)
float erVal = math.max(0.0, math.min(1.0, nz(erRaw, 0.0)))
float erPrev = nz(erVal[1], erVal)
float deltaER = erVal – erPrev
// yTrend will be SNR-normalized by sBaseTrend in Step 2 (after sBase is computed)
// — 1.2 VolTracker target: ln(TR / ATR_long) (dimensionless) —
// True Range
float trueRange = math.max(highSafe – lowSafe, math.max(math.abs(highSafe – closePrev), math.abs(lowSafe – closePrev)))
float atrLong = nz(ta.ema(trueRange, volAnchorInput), trueRange)
// yVol = ln(TR / (ATR_long + eps)) — blueprint §1.2 “çok pratik” variant
float yVolRaw = math.log(fGuardSig(trueRange) / fGuardSig(atrLong))
float yVol = nz(yVolRaw, 0.0)
//=============================================================================
// STEP 2 — ROBUST SCALE sBase (blueprint §2)
//=============================================================================
// — 2.1 sBase for TrendTracker (on deltaER first, then normalize) —
// We use deltaER directly for IQR/clip/EWMA, sBase normalizes it
float sIqrTr = fIqrScale(deltaER, iqrLenInput)
float deltaERClip = fWinsor(deltaER, clipKInput * sIqrTr)
var float vBaseTr = INIT_VAR
float vBaseTrNext = betaBaseInput * vBaseTr + (1.0 – betaBaseInput) * deltaERClip * deltaERClip
vBaseTrNext := math.max(EPS_SIG * EPS_SIG, vBaseTrNext)
float sBaseTr = math.sqrt(vBaseTrNext)
// SNR-normalized trend target (blueprint §1.1: yTrend = deltaER / (sBase + eps))
float yTrend = fSafeDiv(deltaER, sBaseTr, EPS)
// — 2.2 sBase for VolTracker (on yVol) —
float sIqrVo = fIqrScale(yVol, iqrLenInput)
float yVolClip = fWinsor(yVol, clipKInput * sIqrVo)
var float vBaseVo = INIT_VAR
float vBaseVoNext = betaBaseInput * vBaseVo + (1.0 – betaBaseInput) * yVolClip * yVolClip
vBaseVoNext := math.max(EPS_SIG * EPS_SIG, vBaseVoNext)
float sBaseVo = math.sqrt(vBaseVoNext)
//=============================================================================
// STEP 3 — SUB-MODE MODELS: Student-t + score-driven mean + EWMA var
// (blueprint §3)
//=============================================================================
// Persistent state: means, variances, weights for each tracker × 3 modes
// — TrendTracker state —
var float mTrUp = 0.0
var float mTrDown = 0.0
var float mTrNull = 0.0
var float sTrUp = INIT_VAR
var float sTrDown = INIT_VAR
var float sTrNull = INIT_VAR
var float wTrUpPrev = UNIFORM_W
var float wTrDownPrev = UNIFORM_W
var float wTrNullPrev = UNIFORM_W
// — VolTracker state —
var float mVoUp = 0.0
var float mVoDown = 0.0
var float mVoNull = 0.0
var float sVoUp = INIT_VAR
var float sVoDown = INIT_VAR
var float sVoNull = INIT_VAR
var float wVoUpPrev = UNIFORM_W
var float wVoDownPrev = UNIFORM_W
var float wVoNullPrev = UNIFORM_W
// — Hazard CUSUM state (per tracker) —
var float trSpos = 0.0
var float trSneg = 0.0
var int trCooldown = 0
var float voSpos = 0.0
var float voSneg = 0.0
var int voCooldown = 0
// — Previous bar mixture stats for hazard (per tracker) —
var float trMuMixPrev = 0.0
var float trVMixPrev = INIT_VAR
var float voMuMixPrev = 0.0
var float voVMixPrev = INIT_VAR
// Previous regime label for throttled plotchar
var int prevRegimeLabel = 0
// =====================================================================
// GENERIC TRACKER COMPUTATION (inline, no Pine UDFs with arrays)
// We compute TrendTracker then VolTracker sequentially.
// =====================================================================
float nu = math.max(2.1, math.min(30.0, nuBaseInput))
float nuP1 = nu + 1.0
// —————————————————————
// TREND TRACKER
// —————————————————————
// Step 3: Residuals (using previous bar’s means)
float eTrUp = yTrend – mTrUp
float eTrDown = yTrend – mTrDown
float eTrNull = yTrend – mTrNull
// Step 3.1: Student-t log-likelihood (blueprint §3.1, drop constants)
// loglik_k = -0.5*log(s_k) – 0.5*(nu+1)*log(1 + e_k²/(nu*s_k))
float sTrUpLik = fGuardDen(sTrUp)
float sTrDownLik = fGuardDen(sTrDown)
float sTrNullLik = fGuardDen(sTrNull)
float llTrUp = -0.5 * math.log(sTrUpLik) – 0.5 * nuP1 * math.log(1.0 + eTrUp * eTrUp / (nu * sTrUpLik))
float llTrDown = -0.5 * math.log(sTrDownLik) – 0.5 * nuP1 * math.log(1.0 + eTrDown * eTrDown / (nu * sTrDownLik))
float llTrNull = -0.5 * math.log(sTrNullLik) – 0.5 * nuP1 * math.log(1.0 + eTrNull * eTrNull / (nu * sTrNullLik))
// —————————————————————
// Step 6 (computed BEFORE posterior to get pStay for THIS bar)
// Hazard proxy — Seçenek A: residual z-score using PREVIOUS bar mixture
// —————————————————————
float trZSurp = fSafeDiv(math.abs(yTrend – trMuMixPrev), math.sqrt(fGuardDen(trVMixPrev)) + EPS, EPS)
float trSposNext = math.max(0.0, trSpos + (trZSurp – cusumKInput))
float trSnegNext = math.max(0.0, trSneg + (-trZSurp – cusumKInput))
bool trBreakRaw = (trSposNext > cusumHInput) or (trSnegNext > cusumHInput)
bool trInCool = trCooldown > 0
bool trBreakFlag = trBreakRaw and (not trInCool)
// Cooldown management
int trCdNext = trInCool ? (trCooldown – 1) : (trBreakFlag ? cooldownInput : 0)
// Reset CUSUM on break (blueprint §6.2 — after break, reset accumulators)
float trSposFinal = trBreakFlag ? 0.0 : trSposNext
float trSnegFinal = trBreakFlag ? 0.0 : trSnegNext
// pStay adaptation (blueprint §6.3)
float pStayTr = (trBreakFlag or trInCool) ? pStayMinInput : pStayBaseInput
// —————————————————————
// Step 4: Markov prior (blueprint §4)
// —————————————————————
float pSwTr = (1.0 – pStayTr) / 2.0
float wPrTrUp = wTrUpPrev * pStayTr + (wTrDownPrev + wTrNullPrev) * pSwTr
float wPrTrDown = wTrDownPrev * pStayTr + (wTrUpPrev + wTrNullPrev) * pSwTr
float wPrTrNull = wTrNullPrev * pStayTr + (wTrUpPrev + wTrDownPrev) * pSwTr
// —————————————————————
// Step 5: Posterior softmax (blueprint §5)
// —————————————————————
float uTrUp = math.log(fGuardDen(wPrTrUp)) + llTrUp
float uTrDown = math.log(fGuardDen(wPrTrDown)) + llTrDown
float uTrNull = math.log(fGuardDen(wPrTrNull)) + llTrNull
float maxUTr = math.max(uTrUp, math.max(uTrDown, uTrNull))
float expTrUp = math.exp(uTrUp – maxUTr)
float expTrDown = math.exp(uTrDown – maxUTr)
float expTrNull = math.exp(uTrNull – maxUTr)
float sumExpTr = expTrUp + expTrDown + expTrNull
float wTrUp_post = (na(sumExpTr) or sumExpTr <= 0.0) ? UNIFORM_W : expTrUp / sumExpTr
float wTrDown_post = (na(sumExpTr) or sumExpTr <= 0.0) ? UNIFORM_W : expTrDown / sumExpTr
float wTrNull_post = (na(sumExpTr) or sumExpTr <= 0.0) ? UNIFORM_W : expTrNull / sumExpTr
// —————————————————————
// Step 3.2: Score-driven mean update (blueprint §3.2)
// g_k = (nu+1)*e_k*s_k / (nu*s_k + e_k²)
// —————————————————————
float gCapTr = gCapMultInput * sBaseTr
// Up mode
float gTrUp = fSafeDiv(nuP1 * eTrUp * sTrUpLik, nu * sTrUpLik + eTrUp * eTrUp, EPS)
gTrUp := fWinsor(gTrUp, gCapTr)
float mTrUpNxt = math.max(0.0, math.max(M_MIN, math.min(M_MAX, mTrUp + alphaMInput * gTrUp)))
// Down mode
float gTrDown = fSafeDiv(nuP1 * eTrDown * sTrDownLik, nu * sTrDownLik + eTrDown * eTrDown, EPS)
gTrDown := fWinsor(gTrDown, gCapTr)
float mTrDownNxt = math.min(0.0, math.max(M_MIN, math.min(M_MAX, mTrDown + alphaMInput * gTrDown)))
// Null mode — anchored to 0 (blueprint §3.2)
float gTrNull = fSafeDiv(nuP1 * eTrNull * sTrNullLik, nu * sTrNullLik + eTrNull * eTrNull, EPS)
gTrNull := fWinsor(gTrNull, gCapTr)
float mTrNullNxt = 0.0
// —————————————————————
// Step 3.3: Variance update — EWMA on winsorized residual² (blueprint §3.3)
// —————————————————————
float eCapTr = eCapMultInput * sBaseTr
float eTrUpW = fWinsor(eTrUp, eCapTr)
float eTrDownW = fWinsor(eTrDown, eCapTr)
float eTrNullW = fWinsor(eTrNull, eCapTr)
float sFloorTr = kVarFloorInput * sBaseTr * sBaseTr
float sTrUpNxt = math.max(sFloorTr, betaSInput * sTrUp + (1.0 – betaSInput) * eTrUpW * eTrUpW)
float sTrDownNxt = math.max(sFloorTr, betaSInput * sTrDown + (1.0 – betaSInput) * eTrDownW * eTrDownW)
float sTrNullNxt = math.max(sFloorTr, betaSInput * sTrNull + (1.0 – betaSInput) * eTrNullW * eTrNullW)
// Step 7 (partial): Trend confidence — entropy-based (blueprint §7.1)
float confTrend = fEntropyConf(wTrUp_post, wTrDown_post, wTrNull_post)
// Mixture stats for NEXT bar’s hazard (Seçenek A)
float trMuMixNow = wTrUp_post * mTrUpNxt + wTrDown_post * mTrDownNxt + wTrNull_post * mTrNullNxt
float trDUp = mTrUpNxt – trMuMixNow
float trDDown = mTrDownNxt – trMuMixNow
float trDNull = mTrNullNxt – trMuMixNow
float trVMixNow = wTrUp_post * (sTrUpNxt + trDUp * trDUp) + wTrDown_post * (sTrDownNxt + trDDown * trDDown) + wTrNull_post * (sTrNullNxt + trDNull * trDNull)
// —————————————————————
// VOL TRACKER (identical pipeline, different target)
// —————————————————————
// Step 3: Residuals
float eVoUp = yVol – mVoUp
float eVoDown = yVol – mVoDown
float eVoNull = yVol – mVoNull
// Step 3.1: Student-t log-likelihood
float sVoUpLik = fGuardDen(sVoUp)
float sVoDownLik = fGuardDen(sVoDown)
float sVoNullLik = fGuardDen(sVoNull)
float llVoUp = -0.5 * math.log(sVoUpLik) – 0.5 * nuP1 * math.log(1.0 + eVoUp * eVoUp / (nu * sVoUpLik))
float llVoDown = -0.5 * math.log(sVoDownLik) – 0.5 * nuP1 * math.log(1.0 + eVoDown * eVoDown / (nu * sVoDownLik))
float llVoNull = -0.5 * math.log(sVoNullLik) – 0.5 * nuP1 * math.log(1.0 + eVoNull * eVoNull / (nu * sVoNullLik))
// Step 6: Vol hazard (Seçenek A)
float voZSurp = fSafeDiv(math.abs(yVol – voMuMixPrev), math.sqrt(fGuardDen(voVMixPrev)) + EPS, EPS)
float voSposNext = math.max(0.0, voSpos + (voZSurp – cusumKInput))
float voSnegNext = math.max(0.0, voSneg + (-voZSurp – cusumKInput))
bool voBreakRaw = (voSposNext > cusumHInput) or (voSnegNext > cusumHInput)
bool voInCool = voCooldown > 0
bool voBreakFlag = voBreakRaw and (not voInCool)
int voCdNext = voInCool ? (voCooldown – 1) : (voBreakFlag ? cooldownInput : 0)
float voSposFinal = voBreakFlag ? 0.0 : voSposNext
float voSnegFinal = voBreakFlag ? 0.0 : voSnegNext
float pStayVo = (voBreakFlag or voInCool) ? pStayMinInput : pStayBaseInput
// Step 4: Markov prior
float pSwVo = (1.0 – pStayVo) / 2.0
float wPrVoUp = wVoUpPrev * pStayVo + (wVoDownPrev + wVoNullPrev) * pSwVo
float wPrVoDown = wVoDownPrev * pStayVo + (wVoUpPrev + wVoNullPrev) * pSwVo
float wPrVoNull = wVoNullPrev * pStayVo + (wVoUpPrev + wVoDownPrev) * pSwVo
// Step 5: Posterior softmax
float uVoUp = math.log(fGuardDen(wPrVoUp)) + llVoUp
float uVoDown = math.log(fGuardDen(wPrVoDown)) + llVoDown
float uVoNull = math.log(fGuardDen(wPrVoNull)) + llVoNull
float maxUVo = math.max(uVoUp, math.max(uVoDown, uVoNull))
float expVoUp = math.exp(uVoUp – maxUVo)
float expVoDown = math.exp(uVoDown – maxUVo)
float expVoNull = math.exp(uVoNull – maxUVo)
float sumExpVo = expVoUp + expVoDown + expVoNull
float wVoUp_post = (na(sumExpVo) or sumExpVo <= 0.0) ? UNIFORM_W : expVoUp / sumExpVo
float wVoDown_post = (na(sumExpVo) or sumExpVo <= 0.0) ? UNIFORM_W : expVoDown / sumExpVo
float wVoNull_post = (na(sumExpVo) or sumExpVo <= 0.0) ? UNIFORM_W : expVoNull / sumExpVo
// Step 3.2: Score-driven mean update
float gCapVo = gCapMultInput * sBaseVo
float gVoUp = fSafeDiv(nuP1 * eVoUp * sVoUpLik, nu * sVoUpLik + eVoUp * eVoUp, EPS)
gVoUp := fWinsor(gVoUp, gCapVo)
float mVoUpNxt = math.max(0.0, math.max(M_MIN, math.min(M_MAX, mVoUp + alphaMInput * gVoUp)))
float gVoDown = fSafeDiv(nuP1 * eVoDown * sVoDownLik, nu * sVoDownLik + eVoDown * eVoDown, EPS)
gVoDown := fWinsor(gVoDown, gCapVo)
float mVoDownNxt = math.min(0.0, math.max(M_MIN, math.min(M_MAX, mVoDown + alphaMInput * gVoDown)))
float gVoNull = fSafeDiv(nuP1 * eVoNull * sVoNullLik, nu * sVoNullLik + eVoNull * eVoNull, EPS)
gVoNull := fWinsor(gVoNull, gCapVo)
float mVoNullNxt = 0.0
// Step 3.3: Variance update
float eCapVo = eCapMultInput * sBaseVo
float eVoUpW = fWinsor(eVoUp, eCapVo)
float eVoDownW = fWinsor(eVoDown, eCapVo)
float eVoNullW = fWinsor(eVoNull, eCapVo)
float sFloorVo = kVarFloorInput * sBaseVo * sBaseVo
float sVoUpNxt = math.max(sFloorVo, betaSInput * sVoUp + (1.0 – betaSInput) * eVoUpW * eVoUpW)
float sVoDownNxt = math.max(sFloorVo, betaSInput * sVoDown + (1.0 – betaSInput) * eVoDownW * eVoDownW)
float sVoNullNxt = math.max(sFloorVo, betaSInput * sVoNull + (1.0 – betaSInput) * eVoNullW * eVoNullW)
// Step 7 (partial): Vol confidence — entropy-based (blueprint §7.1)
float confVol = fEntropyConf(wVoUp_post, wVoDown_post, wVoNull_post)
// Mixture stats for next bar hazard
float voMuMixNow = wVoUp_post * mVoUpNxt + wVoDown_post * mVoDownNxt + wVoNull_post * mVoNullNxt
float voDUp = mVoUpNxt – voMuMixNow
float voDDown = mVoDownNxt – voMuMixNow
float voDNull = mVoNullNxt – voMuMixNow
float voVMixNow = wVoUp_post * (sVoUpNxt + voDUp * voDUp) + wVoDown_post * (sVoDownNxt + voDDown * voDDown) + wVoNull_post * (sVoNullNxt + voDNull * voDNull)
//=============================================================================
// STATE COMMIT (single write-point — blueprint §10, kortex.pine pattern)
//=============================================================================
// — TrendTracker —
mTrUp := mTrUpNxt
mTrDown := mTrDownNxt
mTrNull := mTrNullNxt
sTrUp := sTrUpNxt
sTrDown := sTrDownNxt
sTrNull := sTrNullNxt
wTrUpPrev := wTrUp_post
wTrDownPrev := wTrDown_post
wTrNullPrev := wTrNull_post
trSpos := trSposFinal
trSneg := trSnegFinal
trCooldown := trCdNext
trMuMixPrev := trMuMixNow
trVMixPrev := math.max(EPS_SIG, trVMixNow)
// — VolTracker —
mVoUp := mVoUpNxt
mVoDown := mVoDownNxt
mVoNull := mVoNullNxt
sVoUp := sVoUpNxt
sVoDown := sVoDownNxt
sVoNull := sVoNullNxt
wVoUpPrev := wVoUp_post
wVoDownPrev := wVoDown_post
wVoNullPrev := wVoNull_post
voSpos := voSposFinal
voSneg := voSnegFinal
voCooldown := voCdNext
voMuMixPrev := voMuMixNow
voVMixPrev := math.max(EPS_SIG, voVMixNow)
// — Scale accumulators —
vBaseTr := vBaseTrNext
vBaseVo := vBaseVoNext
//=============================================================================
// STEP 7 — OUTPUTS (blueprint §7)
//=============================================================================
// Trend weights
float wTrendUp = wTrUp_post
float wTrendDown = wTrDown_post
float wTrendNull = wTrNull_post
// Vol weights
float wVolUp = wVoUp_post
float wVolDown = wVoDown_post
float wVolNull = wVoNull_post
// Break flags
bool breakFlagTrend = trBreakFlag
bool breakFlagVol = voBreakFlag
//=============================================================================
// STEP 8 — POST-HOC FUSION: 2D REGIME GRID (blueprint §8)
//=============================================================================
// Dominant state detection (blueprint §8 thresholds)
// trendDir: 1=Up, -1=Down, 0=Neutral
int trendDir = 0
if wTrendUp > wDomInput and confTrend > confMinInput
trendDir := 1
else if wTrendDown > wDomInput and confTrend > confMinInput
trendDir := -1
// volDir: 1=Expand, -1=Contract, 0=Neutral
int volDir = 0
if wVolUp > wDomInput and confVol > confMinInput
volDir := 1
else if wVolDown > wDomInput and confVol > confMinInput
volDir := -1
// Combined breakFlag (for fusion logic)
bool anyBreak = breakFlagTrend or breakFlagVol
// Recent break: within cooldown window
bool recentBreak = trCooldown > 0 or voCooldown > 0
// 2D regime label (boolean tree — blueprint §8)
int regimeLabel = REG_NEUTRAL
bool hasTrend = trendDir != 0
if anyBreak or (volDir == 1 and confVol > 0.7 and confTrend < confMinInput)
// EVENT/STRESS: breakFlag == true OR (volDir==Expand AND confVol very high AND confTrend low)
regimeLabel := REG_EVENT_STRESS
else if hasTrend and volDir == 1
// BREAKOUT: trend ∈ {Up,Down} AND vol==Expand AND NOT breakFlag
regimeLabel := REG_BREAKOUT
else if hasTrend and volDir == -1
// TREND_LOWVOL: trend ∈ {Up,Down} AND vol==Contract
regimeLabel := REG_TREND_LOWVOL
else if trendDir == 0 and volDir == -1
// CHOP: trend==Neutral AND vol==Contract
regimeLabel := REG_CHOP
else if hasTrend and volDir == 0 and recentBreak
// WHIPSAW_RISK: trend ∈ {Up,Down} AND vol==Neutral AND breakFlag in recent past
regimeLabel := REG_WHIPSAW_RISK
// Suppress labels during warmup
regimeLabel := isWarmup ? REG_NEUTRAL : regimeLabel
//=============================================================================
// PLOTS (blueprint §7.2)
//=============================================================================
// — Trend weights —
plot(wTrendUp, “wTrendUp”, color=color.new(color.teal, 0))
plot(wTrendDown, “wTrendDown”, color=color.new(color.red, 0))
plot(wTrendNull, “wTrendNull”, color=color.new(color.gray, 40))
// — Vol weights —
plot(wVolUp, “wVolUp”, color=color.new(color.orange, 0))
plot(wVolDown, “wVolDown”, color=color.new(color.blue, 0))
plot(wVolNull, “wVolNull”, color=color.new(color.silver, 40))
// — Confidence —
plot(confTrend, “confTrend”, color=color.new(color.lime, 0))
plot(confVol, “confVol”, color=color.new(color.yellow, 0))
// — Break flags (as 0/1) —
plot(breakFlagTrend ? 1.0 : 0.0, “breakTrend”, color=color.new(color.fuchsia, 0), style=plot.style_columns, display=display.none)
plot(breakFlagVol ? 1.0 : 0.0, “breakVol”, color=color.new(color.purple, 0), style=plot.style_columns, display=display.none)
// — Regime label as numeric —
plot(float(regimeLabel), “regimeLabel2D”, color=color.new(color.white, 0), display=display.none)
// — Debug plots (toggled) —
dispDbg = showDebugInput ? display.all : display.none
plot(yTrend, “yTrend”, color=color.new(color.aqua, 50), display=dispDbg)
plot(yVol, “yVol”, color=color.new(color.olive, 50), display=dispDbg)
plot(sBaseTr, “sBaseTr”, color=color.new(color.maroon, 50), display=dispDbg)
plot(sBaseVo, “sBaseVo”, color=color.new(color.navy, 50), display=dispDbg)
plot(trZSurp, “trZSurp”, color=color.new(color.fuchsia,60), display=dispDbg)
plot(voZSurp, “voZSurp”, color=color.new(color.purple, 60), display=dispDbg)
plot(pStayTr, “pStayTr”, color=color.new(color.teal, 60), display=dispDbg)
plot(pStayVo, “pStayVo”, color=color.new(color.orange,60), display=dispDbg)
// — Regime plotchar markers (throttled: only on confirmed bars with regime change) —
bool regimeChanged = regimeLabel != prevRegimeLabel
prevRegimeLabel := regimeLabel
bool showMarker = barstate.isconfirmed and regimeChanged and not isWarmup
plotchar(showMarker and regimeLabel == REG_BREAKOUT ? 1.05 : na, “BREAKOUT”, “B”, location.absolute, color=color.lime, size=size.tiny)
plotchar(showMarker and regimeLabel == REG_TREND_LOWVOL ? 1.05 : na, “TREND_LOWVOL”, “T”, location.absolute, color=color.teal, size=size.tiny)
plotchar(showMarker and regimeLabel == REG_CHOP ? 1.05 : na, “CHOP”, “C”, location.absolute, color=color.gray, size=size.tiny)
plotchar(showMarker and regimeLabel == REG_WHIPSAW_RISK ? 1.05 : na, “WHIPSAW”, “W”, location.absolute, color=color.orange, size=size.tiny)
plotchar(showMarker and regimeLabel == REG_EVENT_STRESS ? 1.05 : na, “EVENT_STRESS”, “!”, location.absolute, color=color.red, size=size.tiny)
// — Background color hint for regime —
color regBg = regimeLabel == REG_BREAKOUT ? color.new(color.lime, 92) :
regimeLabel == REG_TREND_LOWVOL ? color.new(color.teal, 92) :
regimeLabel == REG_CHOP ? color.new(color.gray, 92) :
regimeLabel == REG_WHIPSAW_RISK ? color.new(color.orange, 92) :
regimeLabel == REG_EVENT_STRESS ? color.new(color.red, 92) :
na
bgcolor(regBg, title=”Regime BG”)