Prior-day and prior-week highs and lows plus the running day open, drawn as horizontal levels that are calendar-correct on any chart interval. The kScript (legacy) recipe and the Indicator it became, side by side.
Yesterday's high and low, last week's high and low, and today's open are the levels the market keeps reacting to, and half the annotations on any trading chart are someone maintaining them by hand. This recipe maintains them for you: five levels computed from calendar-anchored sessions, drawn as thin lines that are exactly the same on a 1-minute chart and a 4-hour chart, and that never repaint: a prior day high is always the prior completed UTC day.
The kScript (legacy) recipe
//@version=2
define(title="Key Levels", position="onchart", axis=false, maxBarsBack=300);
var dayCol = input(name="dayCol", type="color", defaultValue="#f5a623", label="Daily color");
var weekCol = input(name="weekCol", type="color", defaultValue="#4a90d9", label="Weekly color");
var showOpen = input(name="showOpen", type="boolean", defaultValue=true, label="Show day open");
var labelOffset = input(name="labelOffset", type="slider", defaultValue=12, label="Label offset (bars)", constraints={min: 4, max: 40, step: 1});
timeseries data = ohlcv(symbol=currentSymbol, exchange=currentExchange);
timeseries d1 = htf(data, "1D");
timeseries w1 = htf(data, "1W");
var dayOpen = htf(data, "1D", {mode: "developing"}).open;
var pdh = d1.high;
var pdl = d1.low;
var pwh = w1.high;
var pwl = w1.low;
persist firstT = na;
if (isna(firstT)) { firstT = data.time; }
func lvl(x1, price, text, col) {
if (isnum(price)) {
var interval = isnum(data.time[1]) ? data.time - data.time[1] : 0;
var rightX = data.time + labelOffset * interval;
var ax1 = (isnum(firstT) && x1 < firstT) ? firstT : x1;
line.new(ax1, price, rightX, price, { color: col, width: 1, lineStyle: "solid", stickyRight: true });
label.new(rightX, price, text + " " + tostring(price, "0.00"), { color: col, backgroundColor: "#1e222d", size: 11, stickyRight: true });
}
}
if (isLastBar) {
var DAY = 86400000;
var curDay = timestamp(year(), month(), dayOfMonth());
var dow = dayOfWeek();
var dowOff = dow == "monday" ? 0 : (dow == "tuesday" ? 1 : (dow == "wednesday" ? 2 : (dow == "thursday" ? 3 : (dow == "friday" ? 4 : (dow == "saturday" ? 5 : 6)))));
var curWeek = curDay - dowOff * DAY;
var prevDay = curDay - DAY;
var prevWeek = curWeek - 7 * DAY;
lvl(prevDay, pdh, "PDH", dayCol);
lvl(prevDay, pdl, "PDL", dayCol);
lvl(prevWeek, pwh, "PWH", weekCol);
lvl(prevWeek, pwl, "PWL", weekCol);
if (showOpen) { lvl(curDay, dayOpen, "Open", dayCol); }
}The Indicator
import { input, none, ohlcv, output, overlay, param, segment, time } from "./sdk/declare";
import { in_bar_t, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_day_open, out_open_visible, out_pdh, out_pdl, out_pwh, out_pwl } from "./gen/outputs";
import { p_show_open } from "./gen/params";
param("show_open", 1, { min: 0, max: 1, description: "1 draws the running day open, 0 hides it" });
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("bar_t", time.bar_open_sec);
const pdh = output("pdh", none, overlay, { description: "Prior UTC day high" });
const pdl = output("pdl", none, overlay, { description: "Prior UTC day low" });
const pwh = output("pwh", none, overlay, { description: "Prior UTC week high" });
const pwl = output("pwl", none, overlay, { description: "Prior UTC week low" });
const dayOpen = output("day_open", none, overlay, { description: "Open of the running UTC day" });
const openVisible = output("open_visible", none);
// Each bar draws its level from itself to the next bar; consecutive bars chain into one line that
// steps to the new level on the first bar of each period.
segment("pdh_line", { yFrom: pdh, yTo: pdh, from: 0, to: 1, color: "#f5a623", width: 1 });
segment("pdl_line", { yFrom: pdl, yTo: pdl, from: 0, to: 1, color: "#f5a623", width: 1 });
segment("pwh_line", { yFrom: pwh, yTo: pwh, from: 0, to: 1, color: "#4a90d9", width: 1 });
segment("pwl_line", { yFrom: pwl, yTo: pwl, from: 0, to: 1, color: "#4a90d9", width: 1 });
segment("open_line", { yFrom: dayOpen, yTo: dayOpen, from: 0, to: 1, when: openVisible, color: "#f5a623", width: 1, lineStyle: "dashed" });
let showOpen: f64 = 1.0;
let dayIdx: f64 = NaN;
let dayHigh: f64 = NaN;
let dayLow: f64 = NaN;
let runningOpen: f64 = NaN;
let priorDayHigh: f64 = NaN;
let priorDayLow: f64 = NaN;
let weekIdx: f64 = NaN;
let weekHigh: f64 = NaN;
let weekLow: f64 = NaN;
let priorWeekHigh: f64 = NaN;
let priorWeekLow: f64 = NaN;
export function init(): void {
showOpen = p_show_open();
}
export function state(): i32 {
const open = in_open();
const high = in_high();
const low = in_low();
const day = Math.floor(in_bar_t() / 86400.0);
const week = Math.floor((day + 3.0) / 7.0);
if (day != dayIdx) {
// The day that just closed becomes the prior day. A day already running when the history
// starts is incomplete and never becomes a level.
if (!isNaN(dayIdx)) {
priorDayHigh = dayHigh;
priorDayLow = dayLow;
}
dayIdx = day;
dayHigh = high;
dayLow = low;
runningOpen = open;
} else {
if (high > dayHigh) dayHigh = high;
if (low < dayLow) dayLow = low;
}
if (week != weekIdx) {
if (!isNaN(weekIdx)) {
priorWeekHigh = weekHigh;
priorWeekLow = weekLow;
}
weekIdx = week;
weekHigh = high;
weekLow = low;
} else {
if (high > weekHigh) weekHigh = high;
if (low < weekLow) weekLow = low;
}
return 1;
}
export function finalize(): void {
out_pdh(priorDayHigh);
out_pdl(priorDayLow);
out_pwh(priorWeekHigh);
out_pwl(priorWeekLow);
out_day_open(runningOpen);
out_open_visible(showOpen != 0.0 ? 1.0 : 0.0);
emitRow();
}
export function reset(): void {
dayIdx = NaN;
dayHigh = NaN;
dayLow = NaN;
runningOpen = NaN;
priorDayHigh = NaN;
priorDayLow = NaN;
weekIdx = NaN;
weekHigh = NaN;
weekLow = NaN;
priorWeekHigh = NaN;
priorWeekLow = NaN;
}How it works
The levels come from completed sessions. The module keeps the running high and low of the current UTC day and week from the time source's day index. On the first bar of a new day, the day that just closed becomes the prior day: pdh and pdl step to its high and low, and hold there for the whole day. Weeks work the same way. A day or week already running when the loaded history starts is incomplete and never becomes a level, the same clamp the kScript's persist firstT provided. This is the same no-repaint contract as the Regime filter: a level is only ever built from bars that have closed.
The day open is the one level that should move. It is the open of the first bar of the running day, refreshed once at the boundary and held. The kScript read it through {mode: "developing"}; here it is a variable assigned on the boundary bar.
Drawing is a segment per bar. Each level is a segment from this bar to the next (from: 0, to: 1) with both ends on the same output. The pieces chain into one horizontal line that steps to the new level on the first bar of each period. Every level output is none: the numbers are computed and readable, but the segments are what draws, so the levels never appear twice. The open_line segment is gated by open_visible, which is the show_open param turned into an output: a setting becomes a gate.
Nothing waits for the last bar. The kScript drew once, under isLastBar, and extended the lines to the right. An Indicator has no last-bar special case: the level exists on every bar it applies to, which is also what makes each level a metric with a value on every bar.
What changed in the port
line.new(...)onisLastBarbecame per-bar segments. There is no single object drawn once and extended to the right; offsets clamp to the loaded range, so the line ends at the newest bar rather thanlabelOffsetbars past it.- The
label.new(...)price tags are not in this port. A tag per level is arender.labelover a string slot (Drawing objects), which switches the file to the second runtime contract; the levels themselves are readable in the legend and as metrics. - The boolean input became a numeric param (
0or1) and the color inputs becamecoloroptions on the segments. maxBarsBackhas nothing to declare: state lives in a few module-level variables, however much history is loaded.
Customize it
- Add monthly levels. Compute a month index from the day index and keep a third running high and low; two more outputs and two more segments (7 of the 16 allowed).
- Prior close and midpoint. Keep the last close of each day for a
pdclevel;(pdh + pdl) / 2is the day's midpoint, a favorite mean-reversion magnet. - Style per period.
lineStyle: "dotted"on the weekly segments keeps them quieter than the daily ones;width: 2makes a level louder. - Read the levels elsewhere. Publish and install, and every level is a metric:
om metric getreturns it, and a watch can compare the chart's close against it through aCompareof two metric references:
om metric get --metric wrun/@you/key-levels/pdh --symbol BTCUSDT --exchange BINANCE_FUTURES
om metric series --metric wrun/@you/key-levels/pwh --symbol BTCUSDT --exchange BINANCE_FUTURES --interval 4h --bars 42Concepts used
- Time and sessions for the day and week indexes and why the prior day is always a completed UTC day
- Drawing objects for segments with
from: 0, to: 1as horizontal levels - Typed inputs for the boolean-as-param that becomes a
whengate - Core variables for the running highs and lows that
reset()restores