Most trend indicators answer the question “is the market trending?” with a binary or a smoothed signal. The Markov Chain Trend Probability indicator, by Henrique Centieiro (inspired by the work of Russian mathematician Andrey Markov), takes a different angle: it answers “given the recent history of regimes, what is the probability the market stays in its current state?” — and then quantifies how well its own probability estimates have been calibrated using a metric borrowed from weather forecasting: the Brier Score.
The result is an oscillator panel that displays three time series: the stationary probability of being in an uptrend, the stationary probability of being in a downtrend, and the running Brier Score of the model’s recent predictions. It is, in spirit, a self-aware trend filter — it tells you both what it thinks and how trustworthy that thinking has been lately.
A Markov chain is a memoryless stochastic process: the next state depends only on the current state, not on the full history. For a two-state market regime (uptrend / downtrend), the chain is fully described by four transition probabilities arranged in a 2×2 matrix:
to=Up to=Down
from=Up [ P↑↑ P↑↓ ]
from=Down [ P↓↑ P↓↓ ]
Each row sums to 1. The diagonal entries (P↑↑, P↓↓) measure regime persistence: high values mean the market tends to stay where it is; low values mean it flips often. The stationary distribution (the long-run probability of being in each state) is the eigenvector of the transition matrix associated with eigenvalue 1.
In this indicator, both the transition probabilities and the stationary probabilities are estimated empirically by counting frequencies over the last historyLength bars.
Each bar is labelled UPTREND or DOWNTREND based on a single normalised statistic:
priceChange = close − close[lookbackPeriod]
atrNormChange = priceChange / ATR(atrLength)
The decision rule has a deliberate dead zone:
atrNormChange > +atrThreshold → UPTRENDatrNormChange < −atrThreshold → DOWNTRENDThe hysteresis (carry-over) is what gives the model its memory. Without it, the state would flip on every minor wiggle inside the dead zone and the transition probabilities would converge to noise.
The indicator keeps the last historyLength states in a FIFO buffer. From this buffer it computes:
P(Up) = #(Up states) / historyLength, P(Down) = 1 − P(Up)Up is followed by Up and how often Down is followed by Down. Divide by the number of times each origin state appeared.The displayed P(Up→Up) reads as: “of the bars where the previous state was UPTREND, what fraction transitioned to UPTREND on the next bar?” — pure first-order regime persistence.
A probability that the market will be UP is an honest forecast only if, when you say “70% chance of UP”, you are actually right about 70% of the time. The Brier Score measures exactly this:
Brier = average over N predictions of (prediction − outcome)²
where outcome is 1 if the realised state was UPTREND and 0 otherwise. The indicator stores the previous bar’s P(Up) as the prediction, and pairs it with the actual state observed on the current bar. The running mean squared error over the last brierLB pairs is the Brier Score.
Reference thresholds:
| Brier | Interpretation |
|-------|----------------|
| 0.00 | Perfect forecasts |
| 0.10 | Exceptional |
| 0.15 | Excellent |
| 0.25 | Random baseline (50/50 forecasts) |
| > 0.30 | Poorly calibrated |
The indicator displays Brier × 100 alongside the probability lines so the visual scale matches.
| Feature | Output |
|---------|--------|
| Stationary probabilities | `P(Up) %`, `P(Down) %` (0..100) |
| Brier Score | Calibration metric × 100 (lower = better) |
| State classification | UPTREND / DOWNTREND with ATR-normalised dead zone |
| Memory window | `historyLength = 33` bars (configurable) |
| Calibration window | `brierLB = 20` predictions (configurable) |
| Reference levels | 50% (neutral), 25% (random baseline), 15% (excellent threshold) |
P(Down) = 100 − P(Up) by construction). Plotted explicitly because it makes regime balance visually obvious.The most useful read is the gap between green and red combined with Brier below 15: a wide gap (e.g. P(Up) = 80%, P(Down) = 20%) with low Brier means the regime is both clear and the model has been tracking it accurately.
P(Up) > 60% and Brier < 20%. The Brier gate keeps you out during transitional periods where the probability is high but unreliable.P(Up) ≈ P(Down) ≈ 50% (no regime) — the indicator can be used as a not-trending filter.P(Up→Up) and P(Down→Down) (computed internally) are both high, the market is in a strongly autocorrelated regime — favourable for trend-following. If they are both low, the market is fast-flipping — favourable for short-term mean reversion.| Parameter | Default | Description |
|-----------|---------|-------------|
| `lookbackPeriod` | 14 | Window in bars for price change |
| `atrThreshold` | 0.5 | Multiplier of ATR for the dead zone; higher = stickier regime |
| `historyLength` | 33 | Number of recent states kept for probability estimation |
| `atrLength` | 14 | ATR period |
| `brierLB` | 20 | Window of (prediction, outcome) pairs for Brier Score |
A higher atrThreshold produces fewer state changes, more regime persistence, and a higher P(Up→Up) / P(Down→Down). A larger historyLength makes the probabilities slower to adapt but more statistically reliable.
Translating this indicator to ProBuilder required several design choices worth documenting, as the Pine version makes heavy use of TradingView’s dynamic arrays and visual primitives that have no direct ProBuilder counterpart.
1. Dynamic arrays as fixed-slot FIFOs with shift. Pine’s array.unshift / array.pop pair (insert at the front, drop from the back) is replaced by a fixed-size ProBuilder array with a manual shift on each bar:
for k = historyLength - 1 downto 1 do
$stateHist[k] = $stateHist[k - 1]
next
$stateHist[0] = currentState
The loop runs historyLength - 1 operations per bar. With historyLength = 33 this is trivial (~30 ops). A parallel stateCount counter tracks how many slots are valid during the warm-up period.
2. State machine with carry-over. ProBuilder’s IF/ELSIF chains do not implicitly retain a variable’s previous value. The “keep previous state” branch must be written explicitly:
if barindex < warmup then
currentState = UPTREND
elsif atrNormChange > atrThreshold then
currentState = UPTREND
elsif atrNormChange < 0 - atrThreshold then
currentState = DOWNTREND
else
currentState = currentState[1]
endif
Without the final else, the state would silently become undefined whenever the dead zone is hit, and the whole state machine would collapse.
3. Brier predictions stored one bar late to avoid data leakage. The prediction stored at bar t is probUp[1] — the value computed at the close of bar t-1, before the outcome of bar t was known. Pairing it with currentState of bar t gives an honest prediction/outcome pair.
$predHist[0] = probUp[1]
if currentState = UPTREND then
$actHist[0] = 1
else
$actHist[0] = 0
endif
4. Transition direction. The transition counter iterates pairs (slot[i+1], slot[i]) and interprets the older slot as the “from” state and the newer slot as the “to” state. This matches the standard chronological reading of a Markov transition: “given the state at time t, what is the probability of state at time t+1?”. The original Pine code computed the reverse direction; the ProBuilder port restores the chronological order so P(Up→Up) measures the canonical first-order transition probability.
5. What was lost from the original. Five elements of the Pine version have no clean ProBuilder equivalent and were dropped:
bgcolor(). The panel uses three coloured series instead.style() (the line type and width cannot be variables). Colours are hardcoded for a sober, theme-neutral look. Edit the coloured(...) arguments directly if you want to recolour.//----------------------------------------------
//PRC_Markov Chain Trend Probability
//version = 0
//22.05.26
//Iván González @ www.prorealcode.com
//Traducido de "Markov Chain Trend Probability" (PineScript v5)
//Autor original: Henrique Centieiro
//Sharing ProRealTime knowledge
//----------------------------------------------
// === Inputs ===
lookbackPeriod = 14 // n: ventana de cambio de precio
atrThreshold = 0.5 // X: umbral en multiplos de ATR
historyLength = 33 // historico de estados para probabilidades
atrLength = 14 // periodo del ATR
brierLB = 20 // ventana para Brier Score
// Constantes de estado
UPTREND = 1
DOWNTREND = -1
//----------------------------------------------
// === ATR y cambio normalizado ===
//----------------------------------------------
atrValue = averagetruerange[atrLength]
priceChange = close - close[lookbackPeriod]
if atrValue > 0 then
atrNormChange = priceChange / atrValue
else
atrNormChange = 0
endif
//----------------------------------------------
// === State machine UPTREND / DOWNTREND con carry-over ===
//----------------------------------------------
warmup = max(lookbackPeriod, atrLength)
if barindex < warmup then
currentState = UPTREND
elsif atrNormChange > atrThreshold then
currentState = UPTREND
elsif atrNormChange < 0 - atrThreshold then
currentState = DOWNTREND
else
currentState = currentState[1]
endif
//----------------------------------------------
// === Buffer FIFO de estados (slot[0] = mas reciente) ===
//----------------------------------------------
once stateCount = 0
for k = historyLength - 1 downto 1 do
$stateHist[k] = $stateHist[k - 1]
next
$stateHist[0] = currentState
if stateCount < historyLength then
stateCount = stateCount + 1
endif
//----------------------------------------------
// === Probabilidades de transicion ===
// (cronologicas: slot[i+1] = anterior, slot[i] = siguiente)
//----------------------------------------------
upToUp = 0
upTotal = 0
downToDown = 0
downTotal = 0
if stateCount >= 2 then
for i = 0 to stateCount - 2 do
fromState = $stateHist[i + 1]
toState = $stateHist[i]
if fromState = UPTREND then
upTotal = upTotal + 1
if toState = UPTREND then
upToUp = upToUp + 1
endif
elsif fromState = DOWNTREND then
downTotal = downTotal + 1
if toState = DOWNTREND then
downToDown = downToDown + 1
endif
endif
next
endif
if upTotal > 0 then
pUpToUp = upToUp / upTotal
else
pUpToUp = 0
endif
if downTotal > 0 then
pDownToDown = downToDown / downTotal
else
pDownToDown = 0
endif
//----------------------------------------------
// === Probabilidades estacionarias ===
//----------------------------------------------
upCount = 0
if stateCount > 0 then
for i = 0 to stateCount - 1 do
if $stateHist[i] = UPTREND then
upCount = upCount + 1
endif
next
probUp = upCount / stateCount
probDown = 1 - probUp
else
probUp = 0
probDown = 0
endif
//----------------------------------------------
// === Brier Score ===
// Prediccion en t-1 vs outcome observado en t
//----------------------------------------------
once brierCount = 0
if stateCount >= 2 then
for k = brierLB - 1 downto 1 do
$predHist[k] = $predHist[k - 1]
$actHist[k] = $actHist[k - 1]
next
$predHist[0] = probUp[1]
if currentState = UPTREND then
$actHist[0] = 1
else
$actHist[0] = 0
endif
if brierCount < brierLB then
brierCount = brierCount + 1
endif
endif
if brierCount > 0 then
sumSq = 0
for i = 0 to brierCount - 1 do
diff = $predHist[i] - $actHist[i]
sumSq = sumSq + diff * diff
next
brierPct = sumSq / brierCount * 100
else
brierPct = undefined
endif
//----------------------------------------------
// === Salidas (escala 0..100) ===
//----------------------------------------------
pUpPct = probUp * 100
pDnPct = probDown * 100
// Lineas horizontales de referencia
linMid = 50
linBaseline = 25
linExcel = 15
return pUpPct coloured(76, 175, 80, 255) as "P(Up) %", pDnPct coloured(244, 67, 54, 255) as "P(Down) %", brierPct coloured(255, 152, 0, 255) style(dottedline, 2) as "Brier Score %", linMid coloured(150, 150, 150, 150) style(dottedline2, 1) as "Neutral 50%", linBaseline coloured(255, 235, 59, 200) style(dottedline2, 1) as "Random Baseline 25%", linExcel coloured(76, 175, 80, 200) style(dottedline, 1) as "Excellent 15%"