auto_stories InfoLib
Script Topic: indicator-buildouts

Price Forecast Model Script: Volatility Projection Bands (SD1–SD3)

Projects statistically likely future price ranges using the standard deviation of log returns, scaling uncertainty over time and plotting forward volatility bands (SD1–SD3) with an optional drift term.


Credits
Created by https://x.com/SargonDinkha1
Links: GitHub · YouTube · Trading View


Price Prediction Forecast Model

This indicator projects future price ranges based on recent market volatility.
It does not predict exact prices — instead, it shows where price is statistically likely to move over the next X bars.

How It Works

Price moves up and down by different amounts each bar. This indicator measures how large those moves have been recently (volatility) using the standard deviation of log returns.

That volatility is then:

  • Projected forward in time
  • Scaled as time increases (uncertainty grows)
  • Converted into future price ranges

The further into the future you project, the wider the expected range becomes.

Volatility Bands (Standard Deviation–Based)

The indicator plots up to three projected volatility bands using standard deviation multipliers:

  1. SD1 (1.0×) → Typical expected price movement
  2. SD2 (1.25×) → Elevated volatility range
  3. SD3 (1.5×) → High-volatility / stress range

These bands are based on standard deviation of volatility, not fixed probability guarantees.

Optional Drift

An optional drift term can be enabled to introduce a long-term directional bias (up or down).
This is useful for markets with persistent trends.

//@version=5
indicator("Price Prediction Forecast Model]", overlay = true, max_lines_count = 500, max_labels_count = 100)

//────────────────────────────────────────────────────────────
// USER INPUTS
//────────────────────────────────────────────────────────────
projection_bars = input.int(80, "Projected Bars")

use_drift = input.bool(false, "Apply Drift Term")
annual_drift = input.float(0.0, "Annual Drift (%)") / 100

// Sigma bands
enable_sd1 = input.bool(true, "", inline = "sd1")
sd1_mult = input.float(1.0, "SD1", inline = "sd1")
sd1_color = input.color(#9cff87, "", inline = "sd1")

enable_sd2 = input.bool(true, "", inline = "sd2")
sd2_mult = input.float(1.25, "SD2", inline = "sd2")
sd2_color = input.color(#DEFFD6, "", inline = "sd2")

enable_sd3 = input.bool(true, "", inline = "sd3")
sd3_mult = input.float(1.5, "SD3", inline = "sd3")
sd3_color = input.color(#ffffff, "", inline = "sd3")

//────────────────────────────────────────────────────────────
// TIME MODE + BARS PER YEAR (must come BEFORE drift())
//────────────────────────────────────────────────────────────
// Time mode input
time_mode = input.string(
"Auto",
"Time Mode",
options = ["Auto", "Intraday", "Daily", "Weekly", "Monthly"])

// Time info
tf_minutes = timeframe.in_seconds() / 60.0
minutes_per_day = 390.0
bars_per_day = minutes_per_day / tf_minutes

// Bars per year (for drift)
bars_per_year =
time_mode == "Intraday" ? 252.0 * bars_per_day :
time_mode == "Daily" ? 252.0 :
time_mode == "Weekly" ? 52.0 :
time_mode == "Monthly" ? 12.0 :
// Auto
timeframe.isintraday ? 252.0 * bars_per_day :
timeframe.isweekly ? 52.0 :
timeframe.ismonthly ? 12.0 :
252.0

// Mode-dependent rolling window (no rolling_window input used)
rolling_window_mode =
time_mode == "Intraday" ? 200 :
time_mode == "Daily" ? 50 :
time_mode == "Weekly" ? 20 :
time_mode == "Monthly" ? 12 :
// Auto: pick something reasonable for whatever's on chart
timeframe.isintraday ? 200 :
timeframe.isweekly ? 20 :
timeframe.ismonthly ? 12 :
50

///────────────────────────────────────────────────────────────
// VOLATILITY ENGINE (LOG RETURNS)
//────────────────────────────────────────────────────────────
returns = math.log(close / close[1])
sigma = ta.stdev(returns, rolling_window_mode)

// Drift
drift(k) =>
use_drift ? annual_drift * (k / bars_per_year) : 0.0

//────────────────────────────────────────────────────────────
// ARRAYS FOR UPPER/LOWER BOUNDS (SHARED SHAPE)
//────────────────────────────────────────────────────────────
var float[] upper = array.new_float()
var float[] lower = array.new_float()

if barstate.isfirst
for k = 0 to projection_bars - 1
array.push(upper, na)
array.push(lower, na)

//────────────────────────────────────────────────────────────
// FLAT ARRAYS OF LINES – ONE SET PER BAND
//────────────────────────────────────────────────────────────
var line[] upper1 = array.new_line()
var line[] lower1 = array.new_line()

var line[] upper2 = array.new_line()
var line[] lower2 = array.new_line()

var line[] upper3 = array.new_line()
var line[] lower3 = array.new_line()

if barstate.isfirst
// SD1
if enable_sd1
for k = 0 to projection_bars - 1
array.push(upper1, line.new(na, na, na, na))
array.push(lower1, line.new(na, na, na, na))
// SD2
if enable_sd2
for k = 0 to projection_bars - 1
array.push(upper2, line.new(na, na, na, na))
array.push(lower2, line.new(na, na, na, na))
// SD3
if enable_sd3
for k = 0 to projection_bars - 1
array.push(upper3, line.new(na, na, na, na))
array.push(lower3, line.new(na, na, na, na))

//───────────────────────────────────────────────────────────
// CORE ROUTINE TO DRAW ONE BAND
//───────────────────────────────────────────────────────────

draw_band(lines_up, lines_dn, mult, col) =>
// first point anchored at current close
array.set(upper, 0, close)
array.set(lower, 0, close)

for k \= 1 to projection\_bars \- 1  
    drift\_term      \= drift(k)  
    expected\_center \= close \* math.exp(drift\_term)  
    expected\_move   \= expected\_center \* sigma \* math.sqrt(k) \* mult

    array.set(upper, k, expected\_center \+ expected\_move)  
    array.set(lower, k, expected\_center \- expected\_move)

    ul \= array.get(lines\_up, k)  
    ll \= array.get(lines\_dn, k)

    line.set\_xy1(ul, bar\_index \+ k \- 1, array.get(upper, k \- 1))  
    line.set\_xy2(ul, bar\_index \+ k,     array.get(upper, k))  
    line.set\_color(ul, col)

    line.set\_xy1(ll, bar\_index \+ k \- 1, array.get(lower, k \- 1))  
    line.set\_xy2(ll, bar\_index \+ k,     array.get(lower, k))  
    line.set\_color(ll, col)

// Label at the far edge  
label.new(x \= bar\_index \+ projection\_bars, y \= array.get(upper, projection\_bars \- 1), text \= str.tostring(mult), style \= label.style\_label\_left, color \= col, textcolor \= color.black)

//────────────────────────────────────────────────────────────
// MAIN PROJECTION EXECUTION
//────────────────────────────────────────────────────────────
if barstate.islastconfirmedhistory
if enable_sd1
draw_band(upper1, lower1, sd1_mult, sd1_color)
if enable_sd2
draw_band(upper2, lower2, sd2_mult, sd2_color)
if enable_sd3
draw_band(upper3, lower3, sd3_mult, sd3_color)