0_dte
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © voided

//@version=4
study("0_dte", overlay = false)

// USAGE
//
//  This script guages the probability of an underlying moving a certain amount 
//  on expiration day, to aid the popular "0 dte" strategy. The script counts
//  how many next-day moves exceeded a given magnitude in the past, under 
//  similar conditions. The inputs are:
//
//      - mark_mode: 
//          + "open": measures the magnitude as "open to close"--a true 0 dte.
//          + "previous close": for lazy people who don't want to wake up
//            early. measures magnitude from the previous day's close.
//      - move_mode:
//          + "percent": measures moves that exceed a given percentage.
//          + "absolute": measures moves that exceed a point value.
//      - move-dir: measure only up moves, down moves, or both.
//      - vol_model: the model for realized volatility. (may add more later).
//      - min_vol: only measure moves when realized vol is above this value.
//      - max_vol: only measure moves when realized vol is below this value.
//      - precision: number of digits printed in the output table.
//
//  EXAMPLE:
//      mark_mode:  "previous close"
//      move_mode:  "percent"
//      move_dir:   "up"
//      move_mag:   0.07
//      vol_model:  hv30
//      min_vol:    0.2
//      max_vol:    0.5
//
//      These settings will count the number of trading days that closed 7%
//      higher than the previous day's close, when the previous day's realized
//      volatility (annualized) was between 20% and 50%.
//
//      OUTPUTS:
//      
//      current vol: green plot. Today's realized vol. Shown for convenience.
//      max and min vol: red plots. Also shown for convenience.
//      count: the number of days that exceeded the chosen magnitude, when
//      the previous day's realized volatility was within the chosen bounds.
//      total: the total number of days where realized volatility was within
//      the chosen bounds
//      probability: count / total. the percentage of days that exceeded the
//      move when volatility was within the bounds.
//      move: plotted as a purple line. purple "X" labels are plotted above
//      bars where the move exceeded the magnitude threshold and volatility
//      was in-bounds. a "hit".
//
//  This script is based on the idea that realized volatility has some bearing
//  on future volatility. By seeing what happened in the past when volatility
//  was close to its current value, we may be able to assess the probability
//  that our short put will be in the money, tomorrow, and our account
//  devastated.

var count = 0
mark_mode = input(title = "mark mode", options = [ "open", "previous close" ], defval = "open")
move_mode = input(title = "move mode", options = [ "percent", "absolute" ], defval = "percent")
move_dir = input(title = "move direction", options = [ "up", "down", "any" ], defval = "any")
move_mag = input(title = "move magnitude", type = input.float, defval = -1)
vol_model = input(title = "vol model", options = [ "hv30" ], defval = "hv30")
min_vol = input(title = "min vol", type = input.float, defval = -1.0)
max_vol = input(title = "max vol", type = input.float, defval = -1.0)
precision = input(title = "precision", type = input.integer, defval = 2)

var float v = 0.

if vol_model == "hv30"
    v := sqrt(ema(pow(log(close / close[1]), 2), 30) * 252)
// else if ...
    // define other volatility models here

var int total = 0
var float move = na

move := if move_mode == "percent"
    if mark_mode == "open"
        (close / open - 1)
    else
        (close / close[1] - 1)
else
    if mark_mode == "open"
        close - open
    else
        close - close[1]

move := if move_dir == "up"
    move
else if move_dir == "down"
    -move
else if move_dir == "any"
    abs(move)

in_range = v[1] >= min_vol and v[1] <= max_vol

if move >= move_mag and in_range
    label.new(x = bar_index, y = move, text = "X", style = label.style_none, textcolor = color.fuchsia)
    count := count + 1

if in_range
    total := total + 1

if barstate.islast
    fmt = "#."
    
    for i = 0 to precision
        fmt := fmt + "#"
    
    var ot = table.new(position.top_right, 2, 6)
    
    lbl_clr = color.new(color.blue, 60)
    stat_clr = color.new(color.blue, 80)
    
    table.cell(ot, 0, 0, "current vol", bgcolor = lbl_clr)
    table.cell(ot, 0, 1, "max vol", bgcolor = lbl_clr)
    table.cell(ot, 0, 2, "min vol", bgcolor = lbl_clr)
    table.cell(ot, 0, 3, "count", bgcolor = lbl_clr)
    table.cell(ot, 0, 4, "total", bgcolor = lbl_clr)
    table.cell(ot, 0, 5, "probability", bgcolor = lbl_clr)
    
    table.cell(ot, 1, 0, tostring(v, fmt), bgcolor = stat_clr)
    table.cell(ot, 1, 1, tostring(max_vol), bgcolor = stat_clr)
    table.cell(ot, 1, 2, tostring(min_vol), bgcolor = stat_clr)
    table.cell(ot, 1, 3, tostring(count), bgcolor = stat_clr)
    table.cell(ot, 1, 4, tostring(total, fmt), bgcolor = stat_clr)
    table.cell(ot, 1, 5, tostring(count / total, fmt), bgcolor = stat_clr)
    
plot(move, "move", color = color.fuchsia)
plot(v[1], "realized vol", color = color.green)
plot(move_mag, "magnitude", color = color.blue)
plot(min_vol, "min vol", color = color.red)
plot(max_vol, "max_vol", color = color.red)