Markov-HMM Regime v5 classifies each bar as Bull, Bear or Sideways using a hidden Markov model whose parameters were trained offline in Python and pasted into the script.
1Copy the scriptPress Copy script above, or the copy button on the code below.
2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.
Source Code
630 lines of Pine Script v5 from indicator/markov_hmm_v5_auto.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.
markov_hmm_v5_auto.pine
//@version=5
// ═══════════════════════════════════════════════════════════════════
// Markov-HMM Regime Detector v5 (Auto-Timeframe)
// ────────────────────────────────────────────────
// A Hidden Markov Model regime classifier for NQ futures.
//
// Architecture:
// Python (Baum-Welch / EM) trains the model offline → parameters
// are hardcoded here → PineScript runs the Forward algorithm in
// real time to decode the most probable regime each bar.
//
// States: Bull (1) · Bear (2) · Sideways (0)
// Features: z-scored return · volatility ratio · volume ratio
//
// The indicator auto-detects the chart timeframe and loads the
// matching parameter set (5m / 15m / 1h). Retrain periodically
// with: python3 trainer/nq_hmm_trainer.py
//
// Author: Flavio De Salvo
// Source: https://github.com/[your-username]/nq-hmm-regime-detector
// ═══════════════════════════════════════════════════════════════════
indicator("Markov-HMM Regime v5", overlay=true, max_labels_count=500, max_bars_back=5000)
// ─── Inputs ─────────────────────────────────────────────────────
grp_logic = "Regime logic"
grp_advanced = "Advanced"
grp_display = "Display"
grp_position = "Table positions"
fast_lookback = input.int(4, "Fast lookback", minval=2, maxval=60, group=grp_logic)
tf_minutes = timeframe.in_seconds() / 60
auto_fast_th = tf_minutes >= 1440 ? 2.5 : tf_minutes >= 240 ? 0.75 : tf_minutes >= 60 ? 0.3 : tf_minutes >= 15 ? 0.12 : tf_minutes >= 5 ? 0.07 : 0.05
fast_threshold_pct = input.float(0.12, "Fast threshold (%)", minval=0.01, maxval=50.0, step=0.01, group=grp_logic)
use_auto_fast = input.bool(true, "Auto-detect fast threshold", group=grp_logic)
stationary_power = input.int(50, "Stationary power", minval=10, maxval=200, group=grp_logic)
min_samples = input.int(8, "Min samples", minval=3, maxval=50, group=grp_logic)
decay_factor = input.float(0.997, "Transition decay", minval=0.95, maxval=0.9999, step=0.001, group=grp_advanced)
vol_confirm_mult = input.float(1.0, "Volume confirm mult", minval=0.5, maxval=3.0, step=0.1, group=grp_advanced)
vol_sma_len = input.int(20, "Volume SMA for confirm", minval=5, maxval=100, group=grp_advanced)
show_ribbon = input.bool(true, "Regime ribbon", group=grp_display)
show_banner = input.bool(true, "Banner", group=grp_display)
show_macro = input.bool(true, "Macro matrix + stats", group=grp_display)
show_fast = input.bool(true, "Fast matrix", group=grp_display)
show_stationary = input.bool(true, "Stationary dist", group=grp_display)
show_duration = input.bool(true, "Duration memory", group=grp_display)
show_2nd_order = input.bool(true, "2nd-order memory", group=grp_display)
show_candle = input.bool(true, "Candle streak memory", group=grp_display)
show_labels = input.bool(true, "Labels", group=grp_display)
txt_sz = input.string("normal", "Text size", options=["small","normal","large","huge"], group=grp_display)
min_hold = input.int(2, "Min hold for label", minval=1, maxval=50, group=grp_display)
pos_banner = input.string("top_center", "Banner", options=["top_left","top_center","top_right","middle_left","middle_center","middle_right","bottom_left","bottom_center","bottom_right"], group=grp_position)
pos_macro = input.string("top_right", "Macro + stats", options=["top_left","top_center","top_right","middle_left","middle_center","middle_right","bottom_left","bottom_center","bottom_right"], group=grp_position)
pos_fast = input.string("middle_right", "Fast", options=["top_left","top_center","top_right","middle_left","middle_center","middle_right","bottom_left","bottom_center","bottom_right"], group=grp_position)
pos_stat = input.string("bottom_center", "Stationary", options=["top_left","top_center","top_right","middle_left","middle_center","middle_right","bottom_left","bottom_center","bottom_right"], group=grp_position)
pos_dur = input.string("bottom_left", "Duration", options=["top_left","top_center","top_right","middle_left","middle_center","middle_right","bottom_left","bottom_center","bottom_right"], group=grp_position)
pos_2nd = input.string("middle_left", "2nd-order", options=["top_left","top_center","top_right","middle_left","middle_center","middle_right","bottom_left","bottom_center","bottom_right"], group=grp_position)
pos_candle = input.string("bottom_right", "Candle memory", options=["top_left","top_center","top_right","middle_left","middle_center","middle_right","bottom_left","bottom_center","bottom_right"], group=grp_position)
// ─── Helpers ────────────────────────────────────────────────────
pos_f(s) =>
switch s
"top_left" => position.top_left
"top_center" => position.top_center
"top_right" => position.top_right
"middle_left" => position.middle_left
"middle_center" => position.middle_center
"middle_right" => position.middle_right
"bottom_left" => position.bottom_left
"bottom_center" => position.bottom_center
"bottom_right" => position.bottom_right
=> position.top_right
sz_f(s) =>
switch s
"small" => size.small
"normal" => size.normal
"large" => size.large
"huge" => size.huge
=> size.normal
notch(s) =>
switch s
"huge" => "large"
"large" => "normal"
"normal" => "small"
=> "small"
val_size = sz_f(txt_sz)
hdr_size = sz_f(notch(txt_sz))
active_fast_th = use_auto_fast ? auto_fast_th : fast_threshold_pct
// Colour palette
c_bull=color.rgb(132,187,161,30), c_bear=color.rgb(197,127,134,30), c_side=color.rgb(164,171,183,30)
c_bg=color.new(color.black,30), c_txt=color.white
c_card=color.new(#0B0F0D,8), c_frame=color.new(#3FDE7E,78), c_acc=#3FDE7E
c_dbg=color.new(#3FDE7E,80), c_dtxt=#6BF0A6
c_off=color.new(color.white,55), c_hdr=color.new(color.white,35), c_foot=color.new(color.white,60)
c_algn=color.new(#FFD700,30), c_algnt=#FFD700
c_warn=#FF6B6B, c_strong=#4DEEAA, c_green=#26A69A, c_red=#EF5350
rsol(r) => r==1?c_bull:r==2?c_bear:c_side
rnam(r) => r==1?"Bull":r==2?"Bear":"Sideways"
rabr(r) => r==1?"BUL":r==2?"BER":"SID"
fpct(p) => str.tostring(math.round(p*100))+"%"
colp(p) => p>=0?(p>=0.80?c_strong:p<0.60?c_warn:c_off):c_foot
// Basis-point formatter with 1 decimal precision
// e.g. 0.000003 → "0.3bp" instead of "0bp"
fbps(p) =>
val = math.round(p * 100000) / 10.0
str.tostring(val) + "bp"
// ═══════════════════════════════════════════════════════════════
// HMM PARAMETERS — Auto-loaded by chart timeframe
//
// States: 0 = Sideways, 1 = Bull, 2 = Bear
// Layout: row-major 3×3 flattened into 9-element arrays
//
// Transition matrix:
// [SID→SID, SID→BUL, SID→BER,
// BUL→SID, BUL→BUL, BUL→BER,
// BER→SID, BER→BUL, BER→BER]
//
// Emission means & variances (Gaussian, diagonal covariance):
// [SID_zret, SID_vr, SID_ur,
// BUL_zret, BUL_vr, BUL_ur,
// BER_zret, BER_vr, BER_ur]
//
// Retrain: python3 trainer/nq_hmm_trainer.py
// ═══════════════════════════════════════════════════════════════
tf_sec = timeframe.in_seconds()
var trans = array.new_float(9, 0.0)
var means = array.new_float(9, 0.0)
var vars = array.new_float(9, 0.0)
var string tf_label = ""
if barstate.isfirst
if tf_sec <= 300
// ── 5-MINUTE parameters (13217 bars, Sharpe 0.78) ──
trans := array.from(0.943962, 0.000000, 0.056038, 0.031706, 0.914668, 0.053626, 0.110024, 0.210058, 0.679918)
means := array.from(0.035288, 0.732710, 0.848629, 0.037343, 1.220802, 0.826263, -0.038083, 1.088494, 2.240876)
vars := array.from(0.803964, 0.029437, 0.152834, 0.584471, 0.034105, 0.123098, 2.993140, 0.079303, 0.994303)
tf_label := "5m"
else if tf_sec <= 900
// ── 15-MINUTE parameters (4385 bars, Sharpe 0.03) ──
trans := array.from(0.806295, 0.172045, 0.021660, 0.048030, 0.914230, 0.037740, 0.036078, 0.001975, 0.961947)
means := array.from(0.084324, 1.177627, 2.583429, 0.058740, 1.245920, 0.719822, 0.026857, 0.621834, 0.837466)
vars := array.from(2.805351, 0.065580, 2.245801, 0.457494, 0.049356, 0.133854, 0.917522, 0.031809, 0.272145)
tf_label := "15m"
else if tf_sec <= 3600
// ── 1-HOUR parameters (13634 bars, Sharpe 0.60) ──
trans := array.from(0.871258, 0.001575, 0.127166, 0.000000, 0.845431, 0.154569, 0.176314, 0.231883, 0.591803)
means := array.from(0.038400, 0.929981, 2.778223, 0.045697, 1.002387, 0.144128, -0.009978, 0.945287, 0.396098)
vars := array.from(2.275991, 0.076837, 1.880630, 0.176232, 0.078041, 0.004992, 0.803508, 0.070128, 0.041729)
tf_label := "1h"
else
// ── 1-DAY parameters (6435 bars, 25.7 years, Sharpe 0.49, DD reduction 50%) ──
trans := array.from(0.927545, 0.035307, 0.037148, 0.044334, 0.954892, 0.000774, 0.041405, 0.001427, 0.957168)
means := array.from(0.032079, 0.958639, 1.004624, 0.126549, 0.704361, 1.034901, 0.015084, 1.235448, 1.008822)
vars := array.from(0.967460, 0.005203, 0.126411, 1.072657, 0.015507, 0.135864, 1.043655, 0.017889, 0.140397)
tf_label := "1D"
// ═══════════════════════════════════════════════════════════════
// FEATURE COMPUTATION + HMM FORWARD ALGORITHM
//
// The forward algorithm computes:
// α_t(s) ∝ [ Σ_s' α_{t-1}(s') · A(s'→s) ] · B(s, x_t)
//
// where A is the transition matrix and B is the Gaussian emission
// density. We work in log-space with a max-shift trick for
// numerical stability.
// ═══════════════════════════════════════════════════════════════
// Feature computation (matching Python trainer exactly)
bar_log_ret = math.log(close/close[1])
hmm_rvol = ta.stdev(bar_log_ret, 20) // fast vol (20-bar)
hmm_lvol = ta.stdev(bar_log_ret, 60) // slow vol (60-bar)
hmm_vsma = ta.sma(volume, 20) // volume MA (20-bar)
z_return = not na(hmm_rvol) and hmm_rvol>0 ? bar_log_ret/hmm_rvol : 0.0
vol_ratio = not na(hmm_rvol) and not na(hmm_lvol) and hmm_lvol>0 ? hmm_rvol/hmm_lvol : 1.0
volume_ratio = not na(hmm_vsma) and hmm_vsma>0 ? volume/hmm_vsma : 1.0
// Clip to match training bounds
zr = math.max(-5.0, math.min(5.0, z_return))
vr = math.max(0.0, math.min(5.0, vol_ratio))
ur = math.max(0.0, math.min(15.0, volume_ratio))
// Gaussian log-density (single feature, diagonal covariance)
log_g(x, mu, v) => -0.5*((x-mu)*(x-mu)/v + math.log(2.0*math.pi*v))
// Total log-emission for a state (sum of 3 independent Gaussians)
le_state(state_idx) =>
base = state_idx * 3
log_g(zr, array.get(means, base), array.get(vars, base)) + log_g(vr, array.get(means, base+1), array.get(vars, base+1)) + log_g(ur, array.get(means, base+2), array.get(vars, base+2))
// Forward variables (posterior probabilities)
var float a_sid=1.0/3.0, var float a_bul=1.0/3.0, var float a_ber=1.0/3.0
var int macro_regime = 0
if barstate.isconfirmed and not na(zr) and not na(vr) and not na(ur)
// Prediction step: α_pred(s) = Σ_{s'} α_{t-1}(s') · A(s'→s)
ps = a_sid*array.get(trans,0) + a_bul*array.get(trans,3) + a_ber*array.get(trans,6)
pb = a_sid*array.get(trans,1) + a_bul*array.get(trans,4) + a_ber*array.get(trans,7)
pr = a_sid*array.get(trans,2) + a_bul*array.get(trans,5) + a_ber*array.get(trans,8)
// Update step: α_t(s) ∝ α_pred(s) · B(s, x_t)
ls = le_state(0), lb = le_state(1), lr = le_state(2)
mx = math.max(ls, math.max(lb, lr))
es = math.exp(ls-mx), eb = math.exp(lb-mx), er = math.exp(lr-mx)
raw_s=ps*es, raw_b=pb*eb, raw_r=pr*er
tot=raw_s+raw_b+raw_r
if tot>0
a_sid:=raw_s/tot, a_bul:=raw_b/tot, a_ber:=raw_r/tot
else
a_sid:=1.0/3.0, a_bul:=1.0/3.0, a_ber:=1.0/3.0
macro_regime := a_bul>=a_sid and a_bul>=a_ber ? 1 : a_ber>=a_sid and a_ber>=a_bul ? 2 : 0
confidence = macro_regime==1?a_bul:macro_regime==2?a_ber:a_sid
// ═══════════════════════════════════════════════════════════════
// FAST Z-SCORE REGIME + ALIGNMENT
//
// A short-lookback z-score regime provides a "trigger" signal.
// When both HMM (macro) and fast agree on direction → aligned.
// Alignment = high-conviction entry signal.
// ═══════════════════════════════════════════════════════════════
log_ret_fast = math.log(close/close[fast_lookback])
rvol_f = ta.stdev(bar_log_ret, math.max(fast_lookback*2, 10))
float z_fast = na
if not na(rvol_f) and rvol_f>0
z_fast := log_ret_fast/(rvol_f*math.sqrt(fast_lookback))
int fast_regime = na
if not na(log_ret_fast)
if not na(z_fast)
fast_regime := z_fast>0.4?1:z_fast<-0.4?2:0
else
fast_regime := log_ret_fast>active_fast_th/100.0?1:log_ret_fast<-active_fast_th/100.0?2:0
aligned = not na(macro_regime) and not na(fast_regime) and macro_regime==fast_regime and macro_regime!=0
vol_sma_c = ta.sma(volume, vol_sma_len)
vol_confirmed = not na(vol_sma_c) and volume>=vol_sma_c*vol_confirm_mult
bar_return = close/close[1]-1.0
// Background ribbon
rib = fast_regime==1?color.rgb(132,187,161,90):fast_regime==2?color.rgb(197,127,134,90):color.rgb(164,171,183,90)
arib = aligned and fast_regime==1?color.rgb(132,187,161,75):aligned and fast_regime==2?color.rgb(197,127,134,75):rib
bgcolor(show_ribbon?arib:na, title="Regime ribbon")
// ═══════════════════════════════════════════════════════════════
// LIVE STATISTICS — EMA-decayed transition counts, 2nd-order
// memory, duration persistence, candle streaks, regime P&L
// ═══════════════════════════════════════════════════════════════
var mc=array.new_int(9,0), var fc=array.new_int(9,0)
var em=array.new_float(9,0.0), var ef_a=array.new_float(9,0.0)
prev_m=macro_regime[1], prev_f=fast_regime[1]
if barstate.isconfirmed
for i=0 to 8
array.set(em,i,array.get(em,i)*decay_factor)
array.set(ef_a,i,array.get(ef_a,i)*decay_factor)
if not na(prev_m) and not na(macro_regime)
mi=prev_m*3+macro_regime
array.set(mc,mi,array.get(mc,mi)+1)
array.set(em,mi,array.get(em,mi)+1.0)
if not na(prev_f) and not na(fast_regime)
fi=prev_f*3+fast_regime
array.set(fc,fi,array.get(fc,fi)+1)
array.set(ef_a,fi,array.get(ef_a,fi)+1.0)
// 2nd-order transitions (trigram memory)
var so=array.new_int(81,0)
ppm=macro_regime[2]
if barstate.isconfirmed and not na(ppm) and not na(prev_m) and not na(macro_regime)
fp=ppm*3+prev_m, tp=prev_m*3+macro_regime
array.set(so,fp*9+tp,array.get(so,fp*9+tp)+1)
// Duration tracking
var int mac_dur=1, var int prev_dur=1
if barstate.isconfirmed and not na(macro_regime)
prev_dur:=mac_dur
if not na(prev_m) and macro_regime==prev_m
mac_dur+=1
else
mac_dur:=1
var ds=array.new_int(9,0), var dm_a=array.new_int(9,0), var dl_a=array.new_int(9,0)
if barstate.isconfirmed and not na(prev_m) and not na(macro_regime)
di=prev_m*3+macro_regime
if prev_dur<=3
array.set(ds,di,array.get(ds,di)+1)
else if prev_dur<=8
array.set(dm_a,di,array.get(dm_a,di)+1)
else
array.set(dl_a,di,array.get(dl_a,di)+1)
// Per-regime return statistics (for E[r] and Kelly)
var float srb=0.0,var float sr2b=0.0,var int nb=0
var float srr=0.0,var float sr2r=0.0,var int nr=0
var float srs=0.0,var float sr2s=0.0,var int ns=0
if barstate.isconfirmed and not na(macro_regime) and not na(bar_return)
if macro_regime==1
srb+=bar_return, sr2b+=bar_return*bar_return, nb+=1
else if macro_regime==2
srr+=bar_return, sr2r+=bar_return*bar_return, nr+=1
else
srs+=bar_return, sr2s+=bar_return*bar_return, ns+=1
// Alignment hit rate
var int sig_ok=0, var int sig_n=0
if barstate.isconfirmed and not na(aligned[1]) and aligned[1]
sig_n+=1
if fast_regime[1]==1 and close>close[1]
sig_ok+=1
else if fast_regime[1]==2 and close<close[1]
sig_ok+=1
// Candle streak memory
var int gstrk=0, var int rstrk=0
var gc_a=array.new_int(6,0), var rc_a=array.new_int(6,0)
if barstate.isconfirmed
pgs=gstrk, prs=rstrk
cg=close>open
if cg
gstrk+=1, rstrk:=0
else
rstrk+=1, gstrk:=0
if pgs>=1
bk=pgs>=3?4:pgs==2?2:0
if cg
array.set(gc_a,bk,array.get(gc_a,bk)+1)
else
array.set(gc_a,bk+1,array.get(gc_a,bk+1)+1)
if prs>=1
bk=prs>=3?4:prs==2?2:0
if cg
array.set(rc_a,bk,array.get(rc_a,bk)+1)
else
array.set(rc_a,bk+1,array.get(rc_a,bk+1)+1)
// Model fit (average log-likelihood)
cur_ll = le_state(0)*a_sid + le_state(1)*a_bul + le_state(2)*a_ber
avg_ll = ta.sma(cur_ll, 100)
// ═══════════════════════════════════════════════════════════════
// TRANSITION LABELS (on-chart)
// ═══════════════════════════════════════════════════════════════
var int last_lbl=na
lh=not na(fast_regime)
for k=0 to min_hold-1
lh:=lh and not na(fast_regime[k]) and fast_regime[k]==fast_regime
if barstate.isconfirmed and not na(fast_regime) and lh and fast_regime!=last_lbl
if show_labels and not na(last_lbl)
fo=min_hold-1
label.new(bar_index-fo,high[fo],rabr(last_lbl)+"->"+rabr(fast_regime),yloc=yloc.abovebar,style=label.style_label_down,color=rsol(fast_regime),textcolor=color.white,size=size.small)
last_lbl:=fast_regime
// ═══════════════════════════════════════════════════════════════
// MATRIX HELPERS
// ═══════════════════════════════════════════════════════════════
matmul(A,B) =>
a00=array.get(A,0),a01=array.get(A,1),a02=array.get(A,2),a10=array.get(A,3),a11=array.get(A,4),a12=array.get(A,5),a20=array.get(A,6),a21=array.get(A,7),a22=array.get(A,8)
b00=array.get(B,0),b01=array.get(B,1),b02=array.get(B,2),b10=array.get(B,3),b11=array.get(B,4),b12=array.get(B,5),b20=array.get(B,6),b21=array.get(B,7),b22=array.get(B,8)
C=array.new_float(9,0.0)
array.set(C,0,a00*b00+a01*b10+a02*b20),array.set(C,1,a00*b01+a01*b11+a02*b21),array.set(C,2,a00*b02+a01*b12+a02*b22)
array.set(C,3,a10*b00+a11*b10+a12*b20),array.set(C,4,a10*b01+a11*b11+a12*b21),array.set(C,5,a10*b02+a11*b12+a12*b22)
array.set(C,6,a20*b00+a21*b10+a22*b20),array.set(C,7,a20*b01+a21*b11+a22*b21),array.set(C,8,a20*b02+a21*b12+a22*b22)
C
bld_m(rc) =>
P=array.new_float(9,0.0)
for r=0 to 2
rsum=array.get(rc,r*3)+array.get(rc,r*3+1)+array.get(rc,r*3+2)
for c=0 to 2
array.set(P,r*3+c,rsum>0?array.get(rc,r*3+c)/rsum:1.0/3.0)
P
bld_f(rc) =>
P=array.new_float(9,0.0)
for r=0 to 2
rsum=array.get(rc,r*3)+array.get(rc,r*3+1)+array.get(rc,r*3+2)
for c=0 to 2
array.set(P,r*3+c,rsum>0?array.get(rc,r*3+c)/rsum:1.0/3.0)
P
row_n(ca,si) => array.get(ca,si*3)+array.get(ca,si*3+1)+array.get(ca,si*3+2)
safe_p(ca,si,mn) =>
rs=row_n(ca,si)
rs>=mn?array.get(ca,si*3+si)/rs:-1.0
draw_fast(tbl, P, cnt, tt, ft) =>
table.cell(tbl,0,0,tt,text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(tbl,1,0,"",bgcolor=c_card),table.cell(tbl,2,0,"",bgcolor=c_card),table.cell(tbl,3,0,"",bgcolor=c_card)
table.cell(tbl,4,0,"3x3",text_color=c_foot,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_right)
table.cell(tbl,0,1,"",bgcolor=c_card)
table.cell(tbl,1,1,"BUL",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tbl,2,1,"BER",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tbl,3,1,"SID",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tbl,4,1,"n",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
for r=0 to 2
rn=r==0?"BUL":r==1?"BER":"SID"
rnt=row_n(cnt,r)
table.cell(tbl,0,r+2,rn,text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
for c=0 to 2
p=array.get(P,r*3+c)
id=(r==c)
table.cell(tbl,c+1,r+2,fpct(p),text_color=id?c_dtxt:c_off,bgcolor=id?c_dbg:c_card,text_size=id?val_size:hdr_size,text_halign=text.align_center)
table.cell(tbl,4,r+2,str.tostring(rnt),text_color=c_foot,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tbl,0,5,ft,text_color=c_foot,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(tbl,1,5,"",bgcolor=c_card),table.cell(tbl,2,5,"",bgcolor=c_card),table.cell(tbl,3,5,"",bgcolor=c_card)
table.cell(tbl,4,5,"live",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_right)
// ═══════════════════════════════════════════════════════════════
// DASHBOARD TABLES
// ═══════════════════════════════════════════════════════════════
var table tb = table.new(pos_f(pos_banner), 1, 7, bgcolor=c_bg, border_width=0)
var table tm = table.new(pos_f(pos_macro), 5, 8, bgcolor=c_card, border_width=1, border_color=c_card, frame_color=c_frame, frame_width=1)
var table tf_t = table.new(pos_f(pos_fast), 5, 6, bgcolor=c_card, border_width=1, border_color=c_card, frame_color=c_frame, frame_width=1)
var table ts = table.new(pos_f(pos_stat), 3, 4, bgcolor=c_card, border_width=1, border_color=c_card, frame_color=c_frame, frame_width=1)
var table td = table.new(pos_f(pos_dur), 4, 5, bgcolor=c_card, border_width=1, border_color=c_card, frame_color=c_frame, frame_width=1)
var table t2 = table.new(pos_f(pos_2nd), 3, 5, bgcolor=c_card, border_width=1, border_color=c_card, frame_color=c_frame, frame_width=1)
var table tc = table.new(pos_f(pos_candle), 4, 8, bgcolor=c_card, border_width=1, border_color=c_card, frame_color=c_frame, frame_width=1)
if barstate.islast
Pm = bld_m(mc)
Pf = bld_m(fc)
Pe = bld_f(em)
M = array.copy(Pm)
for _i=1 to stationary_power-1
M:=matmul(M,Pm)
// ─── BANNER ───
if show_banner
cs=" ("+str.tostring(math.round(confidence*100))+"%)"
table.cell(tb,0,0,"HMM "+tf_label+": "+rnam(macro_regime)+cs,text_color=c_txt,bgcolor=rsol(macro_regime),text_size=val_size,text_halign=text.align_center)
zfs=not na(z_fast)?" z:"+str.tostring(math.round(z_fast*10)/10.0):""
table.cell(tb,0,1,"FAST: "+rnam(fast_regime)+zfs,text_color=c_txt,bgcolor=rsol(fast_regime),text_size=hdr_size,text_halign=text.align_center)
if aligned
table.cell(tb,0,2,fast_regime==1?"ALIGNED LONG":"ALIGNED SHORT",text_color=c_algnt,bgcolor=c_algn,text_size=val_size,text_halign=text.align_center)
else
table.cell(tb,0,2,"NO ALIGNMENT",text_color=c_foot,bgcolor=c_bg,text_size=hdr_size,text_halign=text.align_center)
db=mac_dur<=3?"early":mac_dur<=8?"mid":"mature"
vt=vol_confirmed?" | VOL+":""
table.cell(tb,0,3,str.tostring(mac_dur)+" bars ("+db+")"+vt,text_color=c_foot,bgcolor=c_bg,text_size=size.small,text_halign=text.align_center)
// Edge & Kelly criterion
float te=na, float kf=0.0
if not na(macro_regime)
if macro_regime==1 and nb>min_samples
mu=srb/nb, va=math.max(sr2b/nb-mu*mu,0.000001)
te:=mu, kf:=aligned?math.max(0.0,math.min(mu/va,1.0)):0.0
else if macro_regime==2 and nr>min_samples
mu=srr/nr, va=math.max(sr2r/nr-mu*mu,0.000001)
te:=-mu, kf:=aligned?math.max(0.0,math.min(-mu/va,1.0)):0.0
else if macro_regime==0 and ns>min_samples
te:=srs/ns, kf:=0.0
et=aligned and not na(te)?"E[r]: "+fbps(te)+" | Kelly: "+fpct(kf):not na(te)?"E[r]: "+fbps(te)+" (no trade)":"Gathering..."
ec=not na(te)?(te>0 and aligned?c_strong:te>0?c_off:c_warn):c_foot
table.cell(tb,0,4,et,text_color=ec,bgcolor=c_bg,text_size=size.small,text_halign=text.align_center)
// P(stay) + recent trend
string rt=""
color rtc=c_foot
if not na(macro_regime)
ph=array.get(Pm,macro_regime*3+macro_regime)
prr=array.get(Pe,macro_regime*3+macro_regime)
dlt=prr-ph
ew=math.round(1.0/(1.0-decay_factor))
ar=math.abs(dlt)>=0.03?(dlt>0?" +":"")+str.tostring(math.round(dlt*100))+"%":""
rt:="P(stay): "+fpct(ph)+" | rcnt("+str.tostring(ew)+"b): "+fpct(prr)+ar
rtc:=math.abs(dlt)>=0.05?(dlt>0?c_strong:c_warn):c_foot
table.cell(tb,0,5,rt,text_color=rtc,bgcolor=c_bg,text_size=size.small,text_halign=text.align_center)
// Hit rate + log-likelihood
hp=sig_n>=20?sig_ok*1.0/sig_n:na
ht=not na(hp)?"Hit: "+fpct(hp)+" (n="+str.tostring(sig_n)+")":"Hit: gathering..."
hc=not na(hp)?(hp>=0.55?c_strong:hp<0.45?c_warn:c_off):c_foot
llt=not na(avg_ll)?" | LL: "+str.tostring(math.round(avg_ll*10)/10.0):""
table.cell(tb,0,6,ht+llt,text_color=hc,bgcolor=c_bg,text_size=size.small,text_halign=text.align_center)
// ─── MACRO + STATS ───
if show_macro
table.cell(tm,0,0,"MACRO (HMM "+tf_label+")",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(tm,1,0,"",bgcolor=c_card),table.cell(tm,2,0,"",bgcolor=c_card),table.cell(tm,3,0,"",bgcolor=c_card)
table.cell(tm,4,0,"3x3",text_color=c_foot,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_right)
table.cell(tm,0,1,"",bgcolor=c_card)
table.cell(tm,1,1,"BUL",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,2,1,"BER",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,3,1,"SID",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,4,1,"n",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
for r=0 to 2
rn=r==0?"BUL":r==1?"BER":"SID"
rnt=row_n(mc,r)
table.cell(tm,0,r+2,rn,text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
for c=0 to 2
p=array.get(Pm,r*3+c)
id=(r==c)
table.cell(tm,c+1,r+2,fpct(p),text_color=id?c_dtxt:c_off,bgcolor=id?c_dbg:c_card,text_size=id?val_size:hdr_size,text_halign=text.align_center)
table.cell(tm,4,r+2,str.tostring(rnt),text_color=c_foot,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
// E[r] row
mub=nb>5?srb/nb:na, mur=nr>5?srr/nr:na, mus=ns>5?srs/ns:na
table.cell(tm,0,5,"E[r]",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,1,5,not na(mub)?fbps(mub):"—",text_color=not na(mub) and mub>0?c_strong:c_warn,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,2,5,not na(mur)?fbps(mur):"—",text_color=not na(mur) and mur>0?c_strong:c_warn,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,3,5,not na(mus)?fbps(mus):"—",text_color=not na(mus) and mus>0?c_strong:c_warn,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,4,5,"",bgcolor=c_card)
// Vol row
vb=nb>5?math.sqrt(math.max(sr2b/nb-math.pow(mub,2),0)):na
vrr2=nr>5?math.sqrt(math.max(sr2r/nr-math.pow(mur,2),0)):na
vs=ns>5?math.sqrt(math.max(sr2s/ns-math.pow(mus,2),0)):na
table.cell(tm,0,6,"vol",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,1,6,not na(vb)?fbps(vb):"—",text_color=c_off,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,2,6,not na(vrr2)?fbps(vrr2):"—",text_color=c_off,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,3,6,not na(vs)?fbps(vs):"—",text_color=c_off,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tm,4,6,"",bgcolor=c_card)
table.cell(tm,0,7,"trend P",text_color=c_foot,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(tm,1,7,"",bgcolor=c_card),table.cell(tm,2,7,"",bgcolor=c_card),table.cell(tm,3,7,"",bgcolor=c_card)
table.cell(tm,4,7,"live",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_right)
// ─── FAST ───
if show_fast
draw_fast(tf_t, Pf, fc, "FAST", "trade P")
// ─── STATIONARY ───
if show_stationary
table.cell(ts,0,0,"LONG-RUN",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(ts,1,0,"",bgcolor=c_card),table.cell(ts,2,0,"",bgcolor=c_card)
table.cell(ts,0,1,"BUL",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(ts,1,1,"BER",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(ts,2,1,"SID",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(ts,0,2,fpct(array.get(M,0)),text_color=c_dtxt,bgcolor=c_dbg,text_size=val_size,text_halign=text.align_center)
table.cell(ts,1,2,fpct(array.get(M,1)),text_color=c_dtxt,bgcolor=c_dbg,text_size=val_size,text_halign=text.align_center)
table.cell(ts,2,2,fpct(array.get(M,2)),text_color=c_dtxt,bgcolor=c_dbg,text_size=val_size,text_halign=text.align_center)
table.cell(ts,0,3,"steady state",text_color=c_foot,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(ts,1,3,"",bgcolor=c_card)
table.cell(ts,2,3,"live",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_right)
// ─── DURATION ───
if show_duration
table.cell(td,0,0,"DURATION",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(td,1,0,"",bgcolor=c_card),table.cell(td,2,0,"",bgcolor=c_card),table.cell(td,3,0,"",bgcolor=c_card)
table.cell(td,0,1,"",bgcolor=c_card)
table.cell(td,1,1,"1-3",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(td,2,1,"4-8",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(td,3,1,"9+",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
for s=0 to 2
sn=s==0?"BUL":s==1?"BER":"SID"
pss=safe_p(ds,s,min_samples),pmm=safe_p(dm_a,s,min_samples),pll=safe_p(dl_a,s,min_samples)
nns=row_n(ds,s),nnm=row_n(dm_a,s),nnl=row_n(dl_a,s)
ic=not na(macro_regime) and s==macro_regime
table.cell(td,0,s+2,sn,text_color=ic?c_dtxt:c_hdr,bgcolor=ic?c_dbg:c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(td,1,s+2,pss>=0?fpct(pss)+"("+str.tostring(math.round(nns))+")":"—",text_color=colp(pss),bgcolor=ic and mac_dur<=3?c_dbg:c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(td,2,s+2,pmm>=0?fpct(pmm)+"("+str.tostring(math.round(nnm))+")":"—",text_color=colp(pmm),bgcolor=ic and mac_dur>=4 and mac_dur<=8?c_dbg:c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(td,3,s+2,pll>=0?fpct(pll)+"("+str.tostring(math.round(nnl))+")":"—",text_color=colp(pll),bgcolor=ic and mac_dur>=9?c_dbg:c_card,text_size=hdr_size,text_halign=text.align_center)
// ─── 2ND ORDER ───
if show_2nd_order
table.cell(t2,0,0,"2ND ORDER",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(t2,1,0,"",bgcolor=c_card),table.cell(t2,2,0,"",bgcolor=c_card)
table.cell(t2,0,1,"",bgcolor=c_card)
table.cell(t2,1,1,"SUST",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(t2,2,1,"FRESH",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
for s=0 to 2
sn=s==0?"BUL":s==1?"BER":"SID"
sf=s*3+s, srs2=0
for c2=0 to 8
srs2+=array.get(so,sf*9+c2)
sp=srs2>=min_samples?array.get(so,sf*9+sf)/srs2:-1.0
ft2=0,fs2=0
for f=0 to 2
if f!=s
ff=f*3+s, frs=0
for c2=0 to 8
frs+=array.get(so,ff*9+c2)
ft2+=frs, fs2+=array.get(so,ff*9+sf)
fp2=ft2>=min_samples?fs2/ft2:-1.0
ic=not na(macro_regime) and s==macro_regime
table.cell(t2,0,s+2,sn,text_color=ic?c_dtxt:c_hdr,bgcolor=ic?c_dbg:c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(t2,1,s+2,sp>=0?fpct(sp)+"("+str.tostring(math.round(srs2))+")":"—",text_color=colp(sp),bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(t2,2,s+2,fp2>=0?fpct(fp2)+"("+str.tostring(math.round(ft2))+")":"—",text_color=colp(fp2),bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
// ─── CANDLE MEMORY ───
if show_candle
table.cell(tc,0,0,"CANDLE MEMORY",text_color=c_acc,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_left)
table.cell(tc,1,0,"",bgcolor=c_card),table.cell(tc,2,0,"",bgcolor=c_card),table.cell(tc,3,0,"",bgcolor=c_card)
table.cell(tc,0,1,"AFTER",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tc,1,1,"P(GRN)",text_color=c_green,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tc,2,1,"P(RED)",text_color=c_red,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
table.cell(tc,3,1,"(n)",text_color=c_hdr,bgcolor=c_card,text_size=hdr_size,text_halign=text.align_center)
gl=array.from("1 grn","2 grn","3+ grn")
for i=0 to 2
ix=i*2, ng=array.get(gc_a,ix), nrr=array.get(gc_a,ix+1), tot=ng+nrr
pg=tot>=min_samples?ng*1.0/tot:-1.0, prr=tot>=min_samples?nrr*1.0/tot:-1.0
ic=(gstrk==i+1 and i<2) or (gstrk>=3 and i==2)
rb=ic?c_dbg:c_card
table.cell(tc,0,i+2,array.get(gl,i),text_color=ic?c_green:c_hdr,bgcolor=rb,text_size=hdr_size,text_halign=text.align_center)
table.cell(tc,1,i+2,pg>=0?fpct(pg):"—",text_color=pg>=0?(pg>=0.55?c_strong:pg<0.45?c_warn:c_off):c_foot,bgcolor=rb,text_size=val_size,text_halign=text.align_center)
table.cell(tc,2,i+2,prr>=0?fpct(prr):"—",text_color=prr>=0?(prr>=0.55?c_warn:prr<0.45?c_strong:c_off):c_foot,bgcolor=rb,text_size=val_size,text_halign=text.align_center)
table.cell(tc,3,i+2,str.tostring(math.round(tot)),text_color=c_foot,bgcolor=rb,text_size=hdr_size,text_halign=text.align_center)
rl=array.from("1 red","2 red","3+ red")
for i=0 to 2
ix=i*2, ng=array.get(rc_a,ix), nrr=array.get(rc_a,ix+1), tot=ng+nrr
pg=tot>=min_samples?ng*1.0/tot:-1.0, prr=tot>=min_samples?nrr*1.0/tot:-1.0
ic=(rstrk==i+1 and i<2) or (rstrk>=3 and i==2)
rb=ic?c_dbg:c_card
table.cell(tc,0,i+5,array.get(rl,i),text_color=ic?c_red:c_hdr,bgcolor=rb,text_size=hdr_size,text_halign=text.align_center)
table.cell(tc,1,i+5,pg>=0?fpct(pg):"—",text_color=pg>=0?(pg>=0.55?c_strong:pg<0.45?c_warn:c_off):c_foot,bgcolor=rb,text_size=val_size,text_halign=text.align_center)
table.cell(tc,2,i+5,prr>=0?fpct(prr):"—",text_color=prr>=0?(prr>=0.55?c_warn:prr<0.45?c_strong:c_off):c_foot,bgcolor=rb,text_size=val_size,text_halign=text.align_center)
table.cell(tc,3,i+5,str.tostring(math.round(tot)),text_color=c_foot,bgcolor=rb,text_size=hdr_size,text_halign=text.align_center)
// ═══════════════════════════════════════════════════════════════
// ALERTS
// ═══════════════════════════════════════════════════════════════
alertcondition(macro_regime!=macro_regime[1] and not na(macro_regime[1]), title="HMM Regime Change", message="HMM regime changed")
alertcondition(aligned and not aligned[1], title="New Alignment", message="HMM+Fast alignment established")
alertcondition(not aligned and aligned[1], title="Lost Alignment", message="HMM+Fast alignment lost")
alertcondition(confidence<0.4, title="Low Confidence", message="HMM confidence below 40%")
plot(macro_regime, title="HMM Regime", display=display.none)
plot(confidence, title="Confidence", display=display.none)
Overview
Markov-HMM Regime v5 runs the forward algorithm on price and volume features and loads a parameter set matching the chart timeframe. A regime ribbon, banner, labels and several tables show transition, stationary, duration and candle-streak statistics. It is built for Nasdaq futures and draws on the price chart.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Fast lookback
int
4
fast_lookback
Fast threshold (%)
float
0.12
fast_threshold_pct
Auto-detect fast threshold
bool
true
use_auto_fast
Stationary power
int
50
stationary_power
Min samples
int
8
min_samples
Transition decay
float
0.997
decay_factor
Volume confirm mult
float
1.0
vol_confirm_mult
Volume SMA for confirm
int
20
vol_sma_len
Regime ribbon
bool
true
show_ribbon
Banner
bool
true
show_banner
Macro matrix + stats
bool
true
show_macro
Fast matrix
bool
true
show_fast
Show all 26 settings
Setting
Type
Default
Variable
Stationary dist
bool
true
show_stationary
Duration memory
bool
true
show_duration
2nd-order memory
bool
true
show_2nd_order
Candle streak memory
bool
true
show_candle
Labels
bool
true
show_labels
Text size
string
normal
txt_sz
Min hold for label
int
2
min_hold
Banner
string
top_center
pos_banner
Macro + stats
string
top_right
pos_macro
Fast
string
middle_right
pos_fast
Stationary
string
bottom_center
pos_stat
Duration
string
bottom_left
pos_dur
2nd-order
string
middle_left
pos_2nd
Candle memory
string
bottom_right
pos_candle
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
This indicator uses a Hidden Markov Model (HMM) framework combined with an adaptive dual moving average engine and Directional Movement Index (DMI) features to identify market regimes (bullish, bearish, or ranging/chop). It calculates regime probabilities on e
This overlay indicator plots a SuperTrend-style band on the price chart using ATR length, a selectable source, and a base factor. Optional settings enable an adaptive factor driven by rate of change, a volatility filter, K-means clustering, and an LSTM layer w
This overlay indicator builds many SuperTrend trailing stops across a user-set ATR length, factor range, and step. Each factor is scored with an exponential performance memory. K-means clustering groups those results, and the user chooses the best, average, or
Markov State Engine classifies market conditions into six distinct regimes, including strong trends, weak trends, compression, and expansion. It evaluates historical state transitions using an exit-conditional Markov model with Laplace smoothing to estimate re
This indicator classifies each bar as bull, bear, or sideways from a rolling log return compared with adjustable percentage thresholds. It paints a regime ribbon on the price chart, shows a current-regime banner, and labels state changes after a minimum hold.
This indicator uses a Lorentzian distance k-nearest neighbors classification algorithm to predict market direction based on up to five technical features, such as RSI, WaveTrend, CCI, and ADX. It compares current feature values against historical bar patterns