Your chart already shows you where the session high and low are. It does not show you how long they have stood — and a high that has held since 10:05 is not the same object as one printed ninety seconds ago. This puts a clock on both of them.
Each session's high and low are drawn as a ray from the bar that made them, carrying a running clock. The ray starts faint and thin and gains colour and weight as the extreme ages, so the two are never confusable at a glance. When a new extreme prints, the clock resets and the ray goes pale again.
There is no probability on this chart, no target, no bias and no signal. The clock is a lookup on something the chart already knows — it reports elapsed time and nothing else.
An extreme is stamped at the bar that made it and the age is measured to that bar's own time on history, to the live clock only on the last bar. Scroll back and the chart reads exactly as it did at the time.
// This work is licensed under the Mozilla Public License 2.0
// https://mozilla.org/MPL/2.0/
// AXIS Desk
//@version=6
indicator("AXIS Extremes Clock", "AXIS EXTREMES", overlay = true, max_lines_count = 500, max_labels_count = 500)
// =============================================================================
// AXIS EXTREMES CLOCK
//
// The chart already tells you WHERE the session high and low are.
// It does not tell you HOW LONG they have stood.
//
// This draws each session's high and low as a ray from the bar that made it,
// and puts a running clock on it. The ray strengthens as the extreme ages, so
// a high that has held all morning does not look like one printed a minute ago.
//
// That is the whole study. It forecasts nothing and it signals nothing.
// It is a clock on something the chart already knows.
// =============================================================================
// ---- inputs -----------------------------------------------------------------
g_s = "Session"
i_sess = input.session("0930-1600", "Session", group = g_s, tooltip = "The window whose high and low are tracked. Regular US cash hours by default. Use 1800-1700 for a full Globex day.")
i_tz = input.string("America/New_York", "Timezone", options = ["America/New_York", "America/Chicago", "America/Los_Angeles", "Europe/London", "Europe/Berlin", "Asia/Tokyo", "Asia/Shanghai", "Australia/Sydney", "UTC"], group = g_s)
g_d = "Display"
i_hist = input.int(5, "Sessions kept", minval = 1, maxval = 60, group = g_d, tooltip = "How many past sessions stay drawn.")
i_lbl = input.bool(true, "Clock labels on the rays", group = g_d)
i_table = input.bool(true, "Corner readout", group = g_d)
i_pos = input.string("Top right", "Readout corner", options = ["Top right", "Top left", "Bottom right", "Bottom left"], group = g_d)
i_size = input.string("Small", "Text size", options = ["Tiny", "Small", "Normal", "Large"], group = g_d)
g_a = "Age ramp"
i_full = input.int(180, "Full intensity at (minutes)", minval = 5, maxval = 1440, group = g_a, tooltip = "A fresh extreme draws faint and thin. It reaches full colour and full width once it has stood this long.")
i_wmin = input.int(1, "Width when fresh", minval = 1, maxval = 4, group = g_a)
i_wmax = input.int(3, "Width when settled", minval = 1, maxval = 4, group = g_a)
g_c = "Colour"
i_cHi = input.color(color.new(#2FA98A, 0), "High", group = g_c, inline = "c1")
i_cLo = input.color(color.new(#C9705F, 0), "Low", group = g_c, inline = "c1")
i_fade = input.int(72, "Fade when fresh (%)", minval = 0, maxval = 95, group = g_c)
// ---- session state ----------------------------------------------------------
inSess = not na(time(timeframe.period, i_sess, i_tz))
newSess = inSess and not inSess[1]
var float hiPx = na
var float loPx = na
var int hiMs = na
var int loMs = na
var int hiBar = na
var int loBar = na
var line hiLine = na
var line loLine = na
var label hiLab = na
var label loLab = na
// ---- helpers ----------------------------------------------------------------
// Elapsed milliseconds rendered as "4h 07m" / "38m". Nothing below a minute:
// a sub-minute extreme is noise and a ticking seconds field reads as precision
// this study does not have.
f_dur(int ms) =>
int mins = na(ms) or ms < 0 ? 0 : int(math.floor(ms / 60000))
int h = int(math.floor(mins / 60))
int m = mins % 60
h > 0 ? str.tostring(h) + "h " + str.format("{0,number,00}", m) + "m" : str.tostring(m) + "m"
// 0.0 the instant it prints, 1.0 once it has stood i_full minutes.
f_ramp(int ms) =>
na(ms) ? 0.0 : math.min(1.0, math.max(0.0, ms / (i_full * 60000.0)))
f_size(string s) =>
switch s
"Tiny" => size.tiny
"Small" => size.small
"Normal" => size.normal
=> size.large
txtSize = f_size(i_size)
// ---- track the extremes -----------------------------------------------------
if newSess
hiPx := high
loPx := low
hiMs := time
loMs := time
hiBar := bar_index
loBar := bar_index
hiLine := line.new(bar_index, high, bar_index, high, xloc = xloc.bar_index, color = i_cHi, width = i_wmin)
loLine := line.new(bar_index, low, bar_index, low, xloc = xloc.bar_index, color = i_cLo, width = i_wmin)
if i_lbl
hiLab := label.new(bar_index, high, "", xloc = xloc.bar_index, yloc = yloc.price, style = label.style_label_left, color = #00000000, textcolor = i_cHi, size = txtSize)
loLab := label.new(bar_index, low, "", xloc = xloc.bar_index, yloc = yloc.price, style = label.style_label_left, color = #00000000, textcolor = i_cLo, size = txtSize)
// Retire anything older than the kept window.
if array.size(line.all) > 0
while array.size(line.all) > i_hist * 2
line.delete(array.shift(line.all))
if array.size(label.all) > 0
while array.size(label.all) > i_hist * 2
label.delete(array.shift(label.all))
else if inSess and not na(hiPx)
if high > hiPx
hiPx := high
hiMs := time
hiBar := bar_index
if low < loPx
loPx := low
loMs := time
loBar := bar_index
// ---- draw -------------------------------------------------------------------
// Age is measured to the live clock on the last bar and to the bar's own time
// everywhere else, so replayed history reads exactly as it did at the time.
nowMs = barstate.islast ? timenow : time
if inSess and not na(hiLine)
hiAge = nowMs - hiMs
loAge = nowMs - loMs
hiR = f_ramp(hiAge)
loR = f_ramp(loAge)
line.set_xy1(hiLine, hiBar, hiPx)
line.set_xy2(hiLine, bar_index, hiPx)
line.set_color(hiLine, color.new(i_cHi, int(i_fade * (1 - hiR))))
line.set_width(hiLine, int(math.round(i_wmin + (i_wmax - i_wmin) * hiR)))
line.set_xy1(loLine, loBar, loPx)
line.set_xy2(loLine, bar_index, loPx)
line.set_color(loLine, color.new(i_cLo, int(i_fade * (1 - loR))))
line.set_width(loLine, int(math.round(i_wmin + (i_wmax - i_wmin) * loR)))
if i_lbl and not na(hiLab)
label.set_xy(hiLab, bar_index, hiPx)
label.set_text(hiLab, " " + f_dur(hiAge))
label.set_textcolor(hiLab, color.new(i_cHi, int(i_fade * (1 - hiR))))
label.set_xy(loLab, bar_index, loPx)
label.set_text(loLab, " " + f_dur(loAge))
label.set_textcolor(loLab, color.new(i_cLo, int(i_fade * (1 - loR))))
// ---- corner readout ---------------------------------------------------------
tPos = switch i_pos
"Top right" => position.top_right
"Top left" => position.top_left
"Bottom right" => position.bottom_right
=> position.bottom_left
var table t = na
if i_table and barstate.islast
if na(t)
t := table.new(tPos, 2, 3, border_width = 1)
hiAge = nowMs - hiMs
loAge = nowMs - loMs
table.cell(t, 0, 0, "STOOD", text_color = color.new(color.gray, 0), text_size = txtSize, text_halign = text.align_left)
table.cell(t, 1, 0, "", text_size = txtSize)
table.cell(t, 0, 1, "high", text_color = i_cHi, text_size = txtSize, text_halign = text.align_left)
table.cell(t, 1, 1, inSess ? f_dur(hiAge) : "--", text_color = i_cHi, text_size = txtSize, text_halign = text.align_right)
table.cell(t, 0, 2, "low", text_color = i_cLo, text_size = txtSize, text_halign = text.align_left)
table.cell(t, 1, 2, inSess ? f_dur(loAge) : "--", text_color = i_cLo, text_size = txtSize, text_halign = text.align_right)
// ---- plots for alerts / data window -----------------------------------------
// Exposed as minutes so an alert or another study can read the clock directly.
plot(inSess ? (nowMs - hiMs) / 60000.0 : na, "High stood (min)", display = display.data_window)
plot(inSess ? (nowMs - loMs) / 60000.0 : na, "Low stood (min)", display = display.data_window)
plot(inSess ? hiPx : na, "Session high", display = display.data_window)
plot(inSess ? loPx : na, "Session low", display = display.data_window)
Futures traders: set the session to
1800-1700 for a full Globex day, or leave it at cash hours to clock the
RTH extremes only. The two behave very differently and it is worth looking at both.