# XFlow Script

The indicator language of XFlow Charts. One document, complete enough to write a working indicator from, on candles, the footprint, the tape, quotes, depth of book, individual orders, options gamma and what the chart's own indicators draw.

Updated 2026-10-12. This document is complete on its own. Everything an indicator can call is listed here.

## What XFlow Script is

XFlow Script is the small, deterministic language that draws custom indicators on an XFlow Charts chart. It is series first and PineScript flavoured. It runs inside your browser and the desktop and phone apps, on the bars your chart is already showing, and it draws through the chart renderer instead of drawing itself.

A script can read every kind of market data the chart holds: the candles, the per price footprint ladder, the individual trades of the tape, the best bid and offer, the depth of book, the life of each individual resting order and every add, change, fill and cancel, the options gamma levels, the markers the Big Trades indicator drew, the zones Effort Zones and Range Absorption drew, and the values of an open built in indicator it extends. It can draw lines, bands, zones, boxes, markers, dots, text, vertical lines, straight segments, recoloured candles, custom volume profiles with a look of their own, filled rectangles with gradients, polygons, footprint style cells, a histogram on the price axis edge, a table in a chart corner, a readout panel and a whole sub panel, and it can raise alerts with a sound and a popup.

A script is plain text. You write it, or an AI writes it, and you paste it into the Custom Indicator Builder in the Indicators window. The builder compiles it as you type and reports one status line. When it reads Compiled, the indicator appears on the chart and any settings the script declares appear underneath the editor.

### The five facts that decide whether a script compiles

- Line 1 must be exactly //@xfscript 1. Nothing else identifies the text as XFlow Script.
- Every statement lives on one line. A call may never be split across lines.
- Loops exist and are bounded: for and while run at most 100,000 passes per bar. There are no arrays, no objects, no imports and no property access, and whole bar answers such as tape.largest_size or cell.poc_price are one call, so reach for a loop only when no builtin answers the question.
- Every name you call must be in the builtin tables on this page. Anything else is rejected before the script runs.
- Types are checked on every bar. An int series that resolves to nan stops the run.

The smallest complete indicator:

```xfs
//@xfscript 1
indicator("Simple Average")

let length: int = input.int(20, "length", "Length", 2, 400, 1)
plot.line(ta.sma(close, length), "#22C55E", 2, "solid")
```

Paste that into the builder and the chart draws a 20 period average with a Length control under the editor.

## Rules that decide whether your text compiles

### One statement, one line

A newline ends a statement. That is true inside an argument list too, so a call split over several lines is a parse error. Write the call on one line however long it becomes.

Rejected, then accepted:

```xfs
// Rejected: "Expected expression, found"
plot.line(
  ta.sma(close, 20),
  "#22C55E"
)

// Accepted
plot.line(ta.sma(close, 20), "#22C55E", 2, "solid")
```

### Braces open on the same line

The opening brace of an if branch or a function body must sit on the same line as the condition or the signature. A closing brace may be followed by else on the same line or on the next line. Chained else if is supported. When only a value depends on the condition, the conditional expression condition ? a : b is shorter than a branch.

```xfs
if close > open {
  plot.marker(true, high, "#22C55E", "triangle", "above", 8, 2)
} else if close < open {
  plot.marker(true, low, "#EF4444", "triangle", "below", 8, 2)
} else {
  plot.marker(true, close, "#9CA3AF", "diamond", "above", 8, 2)
}
```

### Declarations live at the top level

A top level let or var declares a series, and a series must be declared at the top level of the script, never inside an if branch. Inside an if branch you may assign to an existing var with := or a compound operator such as +=, and you may call plot functions. Inside a function body or a loop body, let declares a local instead: a plain value for the rest of that block, which may be reassigned.

```xfs
// Rejected: "Series declarations must be top level"
if close > open {
  let body: float = close - open
}

// Accepted
var up_bars: float = 0
up_bars := nz(up_bars[1], 0)
if close > open {
  up_bars += 1
}
```

### Other lexical rules

- Comments start with // and run to the end of the line. There is no block comment.
- Strings use double quotes, cannot span lines, and support the escapes \n, \t, \" and \\.
- Numbers may be decimal or scientific, for example 12, 0.25, .5 and 1e3.
- Identifiers use letters, digits and underscore, and cannot start with a digit. let, var, if, else, fn, return, true, false, while and for are reserved words. to, by, break and continue only mean something where a loop uses them.
- math.pi, math.e and the colour names color.aqua, color.black, color.blue, color.fuchsia, color.gray, color.green, color.lime, color.maroon, color.navy, color.olive, color.orange, color.purple, color.red, color.silver, color.teal, color.white and color.yellow are constants, written without parentheses.
- Builtin names such as ta.sma and plot.line are single names. The dot is part of the name, not property access.
- A semicolon also separates statements, so two short statements may share a line.
- The source is UTF-8 and is limited to 65,536 bytes.

## Grammar

This is the whole grammar. Anything outside it is a parse error. History indexing with square brackets is valid only directly on a series name, so write (high + low) into a series first if you need its history.

EBNF:

```xfs
program          = separators, { statement, separators }, EOF ;
separators       = { NEWLINE | ";" } ;

statement        = variable-declaration
                 | assignment
                 | if-statement
                 | for-statement
                 | while-statement
                 | "break" | "continue"
                 | function-declaration
                 | return-statement
                 | call-expression ;

variable-declaration
                 = ("let" | "var"), identifier, [":", type], "=", expression ;
assignment       = identifier, (":=" | "+=" | "-=" | "*=" | "/="), expression ;
if-statement     = "if", expression, block,
                   [separators, "else", (block | if-statement)] ;
for-statement    = "for", identifier, "=", expression, "to", expression,
                   ["by", expression], block ;
while-statement  = "while", expression, block ;
function-declaration
                 = "fn", identifier, "(", [parameters], ")",
                   ["->", type], block ;
parameters       = parameter, {",", parameter} ;
parameter        = identifier, ":", type ;
return-statement = "return", [expression] ;
block            = "{", separators, {statement, separators}, "}" ;

expression       = conditional ;
conditional      = logical-or, ["?", expression, ":", expression] ;
logical-or       = logical-and, {"||", logical-and} ;
logical-and      = equality, {"&&", equality} ;
equality         = comparison, {("==" | "!="), comparison} ;
comparison       = additive, {("<" | "<=" | ">" | ">="), additive} ;
additive         = multiplicative, {("+" | "-"), multiplicative} ;
multiplicative   = unary, {("*" | "/" | "%"), unary} ;
unary            = ["+" | "-" | "!"], unary | postfix ;
postfix          = primary, ["[", expression, "]"] ;
primary          = number | string | "true" | "false"
                 | identifier | constant
                 | call-expression
                 | "(", expression, ")" ;
constant         = "math.pi" | "math.e" | "color.red" | "color.green" | ... ;
call-expression  = dotted-identifier, "(", [arguments], ")" ;
arguments        = expression, {",", expression} ;

type             = "float" | "int" | "bool" | "string" | "color" | "side" | "void" ;
```

### Operator precedence

From tightest to loosest: unary + - !, then * / %, then + -, then < <= > >=, then == !=, then &&, then ||, and last the conditional ? :. All binary operators associate to the left. The conditional associates to the right, so a ? b : c ? d : e reads as a ? b : (c ? d : e).

- Arithmetic operands must be numbers. The + operator never joins strings; use str.concat.
- Division or remainder by zero returns nan rather than infinity.
- && and || require booleans and short circuit. ! requires a boolean. An if condition must be a boolean too.
- An ordered comparison against nan is false. Both == and != against nan are false, even when both sides are nan. Test with is_nan or replace with nz.
- Equality is strict. Numbers, booleans and strings are never coerced.
- condition ? a : b needs a bool condition and evaluates only the branch it takes, exactly like an if. Both branches should have one type; int and float mix freely.
- x += e, x -= e, x *= e and x /= e are short for x := x + e and so on, on a var or a local.
- A ta.* call inside one arm of ? : runs only on the bars that take that arm, exactly as inside an if branch. Compute it into a series first and pick between the results.

```xfs
let tone: color = close >= open ? color.green : color.red
let average_volume: float = ta.sma(volume, 20)
let size_class: string = volume > average_volume * 2 ? "HEAVY" : volume > average_volume ? "ABOVE" : "QUIET"
plot.candle_color(true, tone, 0.6)
plot.label(size_class, "#E5E7EB")
```

## Types and nan

| Type | Values | Notes |
| --- | --- | --- |
| float | any number, including nan | Prices, volumes, timestamps and most results. |
| int | a finite whole number | nan is not a valid int. Counts, lengths, bar indices. |
| bool | true or false | Never produced by coercion. |
| string | text up to 1,024 characters | Compared with == and !=. Joined with str.concat. |
| color | a "#RRGGBB" or "#RRGGBBAA" string | Six digits is fully opaque; the last pair is the opacity, 00 transparent to FF opaque. No CSS names are accepted, but the color.* constants and the color.rgb, color.new and color.from_gradient builders give you one. |
| side | "BUY", "SELL" or "UNDEFINED" | What the tape, order and marker side accessors return. Compare it with == "BUY". |
| void | no value | Only as the return type of a user function. |

Type annotations on a declaration are optional. When you write one it is enforced on every bar, which is the single most common reason a script that parses still stops. Annotate with float unless you are certain the value is a finite whole number.

The int trap:

```xfs
// Stops the run: "n requires int, received nan"
let n: int = tape.vwap()

// Safe
let n: float = tape.vwap()
let rounded: int = math.round(nz(tape.vwap(), close))
```

### nan is the absent value

- History before the first bar returns nan for every series, including a bool series and a string series. Annotating that read stops the run, so guard it: bar_index > 0 && flag[1] short circuits and stays a bool. The globals is_forming and last_bar answer false instead of nan.
- A moving average returns nan until its window is complete.
- -1 is the none index everywhere. Every accessor that takes a row index (tape, ladder, book, order, order event, strike, marker and zone) answers -1 or an out of range index with nan, a side accessor with UNDEFINED and a string accessor with an empty string. Builtins that find a row, such as tape.largest_index, cell.index_at, mbo.largest_index and effort_zones.at, answer -1 when there is none, so their answer can be passed straight on. An index below -1 or a fraction stops the run.
- A string accessor with nothing to report returns an empty string.
- nz(value) replaces a non finite value with 0, and nz(value, replacement) with your own.
- is_nan(value) is the only reliable equality test against nan.
- na is another name for nan and na(value) is is_nan(value), so Pine style code reads the same.

## Series, state and history

Every top level let and var is a series. The script executes once per bar, oldest to newest, and stores one value per bar. Function parameters and function local declarations are plain scalars for that call and have no history.

| Form | Meaning |
| --- | --- |
| let name: type = expression | Recomputed on every bar and immutable. |
| var name: type = expression | Initialised on the first bar, then carries its previous value forward. |
| name := expression | Updates a top level var, or a local inside a function or loop body. |
| name += expression | Short for name := name + expression. -=, *= and /= work the same way. |
| name[offset] | The value offset bars back. 0 is the current bar, 1 the previous one. |

The history offset must be a whole number from 0 through 200,000. It may be computed. Reading before the first bar gives nan.

```xfs
var cumulative: float = 0
cumulative := nz(cumulative[1], 0) + tape.delta()

let previous_close: float = close[1]
let gap: float = open - close[1]
```

### Chart globals

| Name | Type | Meaning |
| --- | --- | --- |
| open | float | Bar open. |
| high | float | Bar high. |
| low | float | Bar low. |
| close | float | Bar close, and the current price on the forming bar. |
| volume | float | Bar volume. |
| time | float | Bar start, epoch milliseconds. |
| time_end | float | Bar end, epoch milliseconds. |
| bar_index | int | Position of the bar in the loaded window, starting at 0. |
| bar_count | int | How many bars this run received. |
| last_bar | bool | True only on the newest bar in the window. This is how a drawing is pinned to the right edge. |
| is_forming | bool | True on the bar the chart is still building, which is the newest bar of the window. |
| nan | float | The absent numeric value. na is the same value. |
| hl2 | float | (high + low) / 2 |
| hlc3 | float | (high + low + close) / 3 |
| ohlc4 | float | (open + high + low + close) / 4 |
| hlcc4 | float | (high + low + close + close) / 4 |

Globals carry history too, so close[1], high[3] and hlc3[2] are valid. A script that declares its own series, function, parameter or local named hl2, hlc3, ohlc4, hlcc4 or na keeps its own meaning for that name everywhere in the script. They work on every bar type the chart offers, including range, volume, tick, delta and seconds bars, because a script receives bars rather than a chart type.

## User functions

Functions are declared at the top level and are pure. They may call builtins, including plot functions, and other user functions. They may not declare or assign series state, and recursion is rejected before the script runs.

```xfs
fn midpoint(a: float, b: float) -> float {
  return (a + b) / 2
}

fn stronger(a: float, b: float) -> bool {
  return a > b
}

let middle: float = midpoint(high, low)
plot.line(middle, "#22C55E", 1, "dotted")
```

- Every parameter needs a type. At most 16 parameters.
- An omitted return type means void.
- A non void function must return a matching value on every path it can take.
- Function local declarations use let, are scalar, have no history, and are only visible after the line that declares them inside the same block. A local may be reassigned with := or a compound operator; a parameter may not. var is not allowed inside a function.
- A function may contain loops, and its locals cannot reuse the name of a series, a global or another visible local.
- Each call site keeps its own series state, so ta.ema(close, 9) and ta.ema(open, 9) never share an accumulator.

## Loops

for and while repeat a block on the current bar. They are for walking rows: the price rows of the footprint ladder, the prints of the tape, the levels of the book, the order events of a bar, or a fixed set of offsets into history. Every loop is bounded, so a mistake stops the script with one line instead of freezing the chart.

```xfs
var heavy_rows: float = 0
heavy_rows := 0
for i = 0 to cell.count() - 1 by 1 {
  if nz(cell.volume(i), 0) < cell.max_volume() * 0.6 {
    continue
  }
  heavy_rows += 1
  plot.box(true, cell.price(i) + chart.tick_size(), cell.price(i), color.teal, 0.35, 0, 0)
}
plot.label(str.concat("HEAVY ROWS ", format.compact(heavy_rows)), "#E5E7EB")
```

| Form | Meaning |
| --- | --- |
| for i = a to b { } | Runs with i = a, then each whole number up to and including b. When a is above b it counts down instead. |
| for i = a to b by s { } | The same with step s, which may be fractional or negative. A step of 0 stops the script. |
| while condition { } | Repeats while the bool condition is true, checked before every pass. |
| break | Leaves the innermost loop. |
| continue | Skips to the next pass of the innermost loop. |

- a, b and the step are computed once, before the first pass, and must be numbers. i is read only inside the loop; copy it into a local to change it.
- Write by 1 when the end can fall below the start. for i = 0 to cell.count() - 1 counts DOWN on a bar with no rows and runs with i = 0 and i = -1; with by 1 it runs zero times. Every row accessor answers nan for -1, so the first form draws nothing wrong, but the second says what you mean. A script that has such a loop keeps running exactly as before, and the settings under the editor point it out with its line: A "for … to n - 1" loop without "by 1" counts down (0, then -1) when n is 0; add "by 1".
- A bound or step that is nan, such as a count from data the chart does not have, runs the loop zero times.
- let inside a loop body declares a local for that pass. A top level var may be assigned inside a loop.
- break and continue must be written inside the loop itself, not in a function called from it.
- A loop at the top level runs once per bar, on every bar. History reads with the loop variable, such as close[k], are allowed.

### What a loop body may call

Anything that keeps one value per bar, or declares something once, is refused inside a loop body, including inside a function called from one: ta.*, mtf.*, session.* except session.in, input.*, indicator, and the per bar drawings plot.line, plot.histogram, plot.band, plot.hline, plot.label, plot.candle_color, plot.panel_line, plot.panel_area, plot.panel_hline, plot.panel_range and the short form of plot.zone. Compute those once per bar outside the loop and use the result inside.

Drawings that make one object each time they run belong in loops: plot.marker, plot.text, plot.box, plot.dot, plot.segment, plot.panel_segment, plot.vline, plot.position, the event form of plot.zone, plot.rect, plot.polygon, plot.cell, plot.hbar, plot.table, profile.fold and profile.row_color.

### Budgets

- At most 100,000 passes of all loops together on one bar.
- At most 25,000,000 steps in one run, a step being one statement or expression inside a loop body, once per pass.
- A script with a loop may draw at most 250,000 markers, texts, boxes, dots, segments, event zones and vertical lines in one run. A script without a loop draws at most one per call site per bar and has no such cap.
- Statements inside a loop body do not count toward the 10,000 statements per bar.

A fixed look back: how many of the last ten bars closed up:

```xfs
var ups: float = 0
ups := 0
for k = 0 to 9 {
  if close[k] > open[k] {
    ups += 1
  }
}
plot.histogram(ups - 5, "#22C55E", "#EF4444")
```

## Coming from Pine Script

XFlow Script reads like Pine Script on purpose, and most indicators port line by line. These are the differences that matter.

| Pine Script | XFlow Script |
| --- | --- |
| //@version=6 and indicator("Name", overlay=true) | //@xfscript 1 and indicator("Name"). Price drawings go on the chart, and panel drawings open the sub panel by themselves. |
| Indented blocks | Braces, the opening brace on the same line: if close > open { } |
| A call wrapped over several lines | Not allowed: every call on one line |
| x = 1 and x := 2 | let x: float = 1 for a value recomputed on every bar; var x: float = 1 and x := 2 for one that carries forward |
| cond ? a : b, for i = 0 to n, while, break, continue, += | The same |
| na, na(x), nz(x), hl2, hlc3, ohlc4, hlcc4 | The same |
| input.int(14, "Length") | input.int(14, "length", "Length", 1, 500, 1): the key the setting is saved under, the title, then optional bounds and step |
| ta.sma, ta.ema, ta.wma, ta.hma, ta.vwma, ta.alma, ta.rsi, ta.atr, ta.tr, ta.stoch, ta.cci, ta.mfi, ta.cmo, ta.mom, ta.roc, ta.cum, ta.highest, ta.lowest, ta.change, ta.crossover, ta.crossunder, ta.cross, ta.barssince, ta.valuewhen, ta.pivothigh, ta.pivotlow, ta.linreg, ta.correlation, ta.percentrank, ta.median, ta.range, ta.sar, ta.tsi | The same names and arguments, except ta.sar, which is ta.psar(start, increment, maximum), and ta.obv, which is the call ta.obv() |
| [line, signal, hist] = ta.macd(close, 12, 26, 9) | ta.macd_line(close, 12, 26), ta.macd_signal(close, 12, 26, 9), ta.macd_hist(close, 12, 26, 9) |
| [middle, upper, lower] = ta.bb(close, 20, 2) | ta.sma(close, 20), ta.bb_upper(close, 20, 2), ta.bb_lower(close, 20, 2) |
| [middle, upper, lower] = ta.kc(close, 20, 2) | ta.ema(close, 20), ta.kc_upper(close, 20, 2), ta.kc_lower(close, 20, 2) |
| [trend, direction] = ta.supertrend(3, 10) | ta.supertrend(3, 10) and ta.supertrend_dir(3, 10). The direction is -1 in an up trend, as in Pine |
| [plus, minus, adx] = ta.dmi(14, 14) | ta.plus_di(14), ta.minus_di(14), ta.adx(14, 14) |
| request.security(syminfo.tickerid, "60", close) | mtf.close(60) for the hour this bar belongs to so far, mtf.prev_close(60) for the last finished hour; the chart instrument only |
| dayofmonth, dayofweek, hour, minute, month, year | time.day(time, "America/New_York") and its siblings, with the zone stated; Pine reads the exchange clock, which for CME futures is "America/Chicago". dayofweek is time.weekday(time, zone) + 1. To read yesterday, store it first: let day: float = time.day(time, "America/Chicago"), then day[1] |
| plot, plotshape, barcolor, fill, hline | plot.line, plot.marker, plot.candle_color, plot.band, plot.hline or plot.panel_hline |
| label.new, line.new, box.new, table.cell | plot.text, plot.segment, plot.box or plot.rect, plot.table |
| color.red, color.new(c, 50), color.rgb(r, g, b, t), color.from_gradient(v, lo, hi, c1, c2) | The same |
| str.tostring, str.length, str.contains, str.upper, str.substring | The same; join text with str.concat, never with + |
| Arrays, matrices, maps, types, methods, libraries, request.*, strategy.* | Not part of the language |

- A window with nan anywhere in it answers nan. Pine skips na inside some windows; XFlow Script does not, so wrap a source that can be absent in nz.
- ta.supertrend and ta.supertrend_dir answer nan while the ATR warms up, where Pine starts with a value.
- ta.rma on its own starts from the first value, where Pine starts from a simple average. ta.rsi, ta.atr and the directional indicators start the Pine way.
- ta.tsi answers a ratio from -1 to 1, as Pine does. Multiply by 100 for a percent scale.
- dayofweek counts Sunday as 1; time.weekday counts Sunday as 0.
- mtf.* buckets are counted from midnight UTC, so mtf.close(1440) is the UTC calendar day, not the futures session. Use session.* for sessions.
- Every typed declaration is checked on every bar. Annotate with float unless the value is always a whole number.
- An unknown Pine name is refused with a line naming the XFlow Script equivalent, for example time.day for dayofmonth.

### A port, line by line

Pine Script:

```xfs
//@version=6
indicator("Daily Typical Price", overlay = true)
mult = input.float(1.0, "Band multiplier")
newDay = dayofmonth != dayofmonth[1]
var float total = 0.0
var float count = 0.0
total := newDay ? hlc3 : total + hlc3
count := newDay ? 1 : count + 1
average = total / count
deviation = ta.atr(14) * mult
plot(average, color = close > average ? color.green : color.red, linewidth = 2)
plot(average + deviation, color = color.new(color.gray, 50))
plot(average - deviation, color = color.new(color.gray, 50))
```

XFlow Script:

```xfs
//@xfscript 1
indicator("Daily Typical Price")

let mult: float = input.float(1, "mult", "Band multiplier", 0.1, 5, 0.1)

let ny_day: float = time.day(time, "America/New_York")
let new_day: bool = ny_day != ny_day[1]

var total: float = 0
var count: float = 0
total := new_day ? hlc3 : total + hlc3
count := new_day ? 1 : count + 1

let average: float = total / count
let deviation: float = ta.atr(14) * mult

plot.line(average, close > average ? color.green : color.red, 2, "solid")
plot.line(average + deviation, color.new(color.gray, 50), 1, "solid")
plot.line(average - deviation, color.new(color.gray, 50), 1, "solid")
```

dayofmonth became a named series in a stated zone, so day[1] has something to read. The rest is the same line in the same order.

## Settings the user can change

An input call declares one control in the indicator settings and returns its current value on every bar. The call must be the direct initialiser of a top level declaration and every argument must be a literal; a negative number such as -300 counts as a literal. Use let rather than var for inputs.

| Signature | Returns |
| --- | --- |
| input.int(default: int, key: string, title: string [, min: int, max: int, step: int]) -> int | Whole number control |
| input.float(default: float, key: string, title: string [, min: float, max: float, step: float]) -> float | Decimal control |
| input.bool(default: bool, key: string, title: string) -> bool | Checkbox |
| input.color(default: color, key: string, title: string) -> color | Colour picker with an opacity slider. The saved value is "#RRGGBB" at full opacity and "#RRGGBBAA" below it. |

- key must match [A-Za-z][A-Za-z0-9_]{0,63} and must be unique inside the script. It is what the saved setting is stored under, so renaming it resets the control.
- title holds 1 to 80 characters and is what the user reads.
- Defaults and bounds must be finite, min must not exceed max, and a step must be positive. For input.int the default, bounds and step must all be whole numbers.
- A saved value outside the declared bounds is clamped before the script sees it.
- An input inside an if branch, a function, or an arithmetic expression is rejected.
- input.color accepts "#RRGGBB" or "#RRGGBBAA". The last pair is the opacity, 00 fully transparent to FF fully opaque, and a six digit default means fully opaque.
- Where a drawing call also takes its own opacity argument, such as plot.zone, plot.box, plot.candle_color, plot.panel_area and profile.fold, the two multiply: a "#RRGGBB80" fill at opacity 0.5 draws at roughly a quarter.

```xfs
let length: int = input.int(20, "length", "Length", 2, 400, 1)
let threshold: float = input.float(10, "deltaPercent", "Delta percent", 0, 100, 0.5)
let show_text: bool = input.bool(true, "showText", "Show text")
let up_color: color = input.color("#22C55E", "upColor", "Up")
let zone_fill: color = input.color("#22C55E40", "zoneFill", "Zone fill")
```

## Every kind of market data a script can read

A script never connects anywhere and never sees a credential. It reads the data of the chart it sits on, and when it reads depth of book, individual orders or options gamma, the chart loads that data for it by itself, through the same access checks the chart applies to its own indicators. You do not need to add a heatmap, an order indicator or a gamma indicator first. Each kind of data below is reached through its own family of builtins, aligned to the bar your script is on.

| Data | Builtins | When it is there |
| --- | --- | --- |
| Candles: open, high, low, close, volume, time | the globals | Always: every chart, every bar type, every datafeed, live and in replay up to the playhead. |
| Aggressor totals: buy volume, sell volume, delta | tape.volume, tape.buy_volume, tape.sell_volume, tape.delta | Always, wherever the chart has aggressor totals, which is the normal case on futures. |
| Individual trades, the time and sales | tape.* | Always. Bars built from the tape (range, volume, tick, delta, seconds) carry every print across the window. On time bars older history arrives as finished candles, so guard with tape.prints_available(). |
| Footprint ladder: bid and ask volume per price | cell.*, profile.fold, chart.tick_size | Always, on every bar the chart holds a ladder for. Guard with cell.available(). |
| Sessions and time | time.*, session.* | Always. |
| Higher timeframe candles | mtf.* | Always, built from the bars the chart holds. |
| Level 1: best bid and offer | l1.* | Live charts only. Each bar keeps the last quote it saw, how many quotes it saw and the spread they showed, from the moment a script reading l1.* is on the chart. In replay the settings say Live Quotes Required. |
| Level 2: depth of book per price | l2.* | With Market Depth access. Live, on every bar since the script started running on the chart; in replay, the recorded book on every bar up to the playhead. |
| Level 3: individual orders and every add, change, fill and cancel, market by order | mbo.* | With Market Depth access. Live, and in replay the recorded orders up to the playhead. |
| Options gamma, GEX | gex.* | With your own GEXBot key, at your GEXBot tier. Live only: every publication since the script started running on the chart, never in replay. |
| Big Trades markers | bigtrades.* | What the Big Trades indicator draws, when it is visible on the same chart. |
| Zones, gaps, boxes, markers, levels, stats and candles other indicators draw | effort_zones.*, range_absorption.*, fvg.*, stop_run.*, iceberg.*, delta_spike.*, important_levels.*, the GEX level families, orderflow_stats.*, chart_stats.*, candle_stats.*, cot.*, net_gex.*, dealers_flow.*, speed_of_tape.*, bigtrades_mbo.*, iceberg_mbo.*, overlay_chart.*, hft_candles.* and the rest | What that indicator draws, when it is visible on the same chart. The present time families (the GEX lines on the newest bar, Orderflow Stats, Dealers Flow) follow the indicator itself, so most read nothing in replay. |
| Session levels | session_profile.*, session_tpo.*, trading_sessions.*, ivb.* | That indicator visible on the same chart, which supplies its own settings. No look-ahead. |
| An open built in indicator you extend | base.* | Always: declare it with indicator(name, base) and the builder computes it for your script. |

### Two questions, asked every time

Quotes, depth, orders and gamma are not on every chart, so each of those families answers two separate questions and a well written script asks both.

| Question | Means |
| --- | --- |
| <family>.supported() | Whether this chart carries that data at all. False when the data is not part of your access, when the datafeed does not carry it, or when the chart is in replay and the data has no recorded twin. |
| <family>.available() | Whether there is something for THIS bar. Supported data still answers nothing for a bar older than what the chart loaded. |

Write both checks and give the script something to draw either way. An indicator that goes blank on a chart without depth reads as broken; one that says what it can with the tape and adds the depth when it is there reads as finished.

When a script reads data the chart cannot give it, the script still runs, and the settings under the editor say what is missing in one line: Market Depth Required for depth and orders, GEX Data Required for gamma, Live Quotes Required for quotes, and the indicator's own name, such as Effort Zones Required or Session Volume Profile Required, for an indicator a script reads by name that is not on the chart.

### dxFeed and Rithmic charts

| Data | Live chart on dxFeed | Live chart on Rithmic | Replay |
| --- | --- | --- | --- |
| Candles, aggressor totals, sessions | Yes | Yes | Yes, up to the playhead |
| Individual trades and the footprint ladder | Yes | Yes | Yes, up to the playhead |
| Level 1 quotes | Yes | Yes | No |
| Level 2 depth | With Market Depth access | Yes | With Market Depth access, the recorded book |
| Level 3 individual orders | With Market Depth access | Yes | With Market Depth access, the recorded orders |
| Options gamma | With your GEXBot key | With your GEXBot key | No |
| Indicators read by name | What the indicator draws | What the indicator draws | What the indicator draws, up to the playhead |

Replay and backtesting always run on dxFeed history, whichever datafeed your live chart uses, so on Rithmic depth and orders are a live feature. The script itself is identical on every datafeed; only what the chart holds differs, and supported() and available() tell the script which case it is in.

### The window a script sees

- A script runs over every bar the chart has loaded, up to 200,000 bars, and bar_count states how many it got.
- A script that declares a very large number of series is given fewer bars, so that the series storage stays within 8,000,000 values: the window becomes 8,000,000 divided by the number of top level let and var declarations.
- A script that reads individual prints or ladder rows over a very long window is likewise given the newest bars whose prints and rows fit comfortably within the 1,000,000 market item ceiling.
- Whenever the window is trimmed, the settings under the editor say History Limited To N Bars, and the drawing still lands on the right candles.
- bar_index counts from the left edge of the window and bar_count states how long it is, so bar_index == bar_count - 1 and last_bar are the same question.
- The script runs on the same instrument, timeframe and session the chart is showing. There is no way to request another symbol or another timeframe from inside a script.
- There is no clock, no random source, no network, no storage and no locale. The same source over the same data always produces the same drawing.

## Using our studies

A script can build on the indicators XFlow Charts ships. It reads what those indicators draw, or the same data they are drawn from, and never how they work. Pick the row for the indicator you want to extend and use the builtins beside it.

| Indicator on the chart | Read with | What the script gets |
| --- | --- | --- |
| Footprint Chart, Bar POC, Stacked Imbalances | cell.*, chart.tick_size | The bid and ask volume of every price row, the point of control, the value area, diagonal imbalance counts and stacked runs. |
| Volume, Volume Bubbles | the volume global, tape.* | Every print of the bar with its price, size, side and time, and the bar's buy and sell totals. |
| Heat Map, Heatmap (GPU), Tick Heatmap, Side DOM | l2.* | The resting size at every price level of the book, per bar, and the biggest level on each side. |
| GEX Profile | gex.* | Every strike with its gamma by volume and open interest, plus the zero gamma and major levels, on the bar the publication landed in. |
| CVD, Session Vwap, EMA, RSI, MACD, ATR | base.* | The indicator's own values on every bar, after declaring it with indicator(name, base). |
| Session Volume Profile, Session TPO | session_profile.*, session_tpo.* | Point of control, value area high and low of this session so far and of the five before it. |
| Composite Profile | profile.fold, cell.* | Your own profile over any range, drawn the way the chart draws profiles. |
| Big Trades | bigtrades.* | Every marker it drew on all loaded bars: price, size, side and time. |
| Big Trades MBO, Iceberg MBO | bigtrades_mbo.*, iceberg_mbo.* | Every square or point it drew: price, size, side and time. |
| Speed of Tape | speed_of_tape.* | The candle the panel draws on each bar: body, wick, side and buyers' share. |
| Effort Zones, Range Absorption | effort_zones.*, range_absorption.* | Every zone it drew: top, bottom, first bar and side, plus lookups by price. |
| FVG | fvg.* | Every gap it drew: top, bottom, first bar and direction, plus lookups by price. |
| Stop Run, Iceberg, Delta Spike | stop_run.*, iceberg.*, delta_spike.* | Every marker it drew, on its own bar: the market price and the side. |
| Important Levels | important_levels.* | Every line by its printed label, such as pD H or cW POC, and its price. |
| GEX Levels, GEX Classic, GEX State | gex_levels.*, gex_classic.*, gex_state.* | Every line of the current snapshot by its label and price. |
| Net GEX | net_gex.* | The Net GEX value on each bar. |
| Orderflow Stats | orderflow_stats.* | Every number the card prints, by row and column. |
| Dealers Flow | dealers_flow.* | Convexity, GEX and DEX as drawn on each bar, with the price each is drawn at. |
| COT | cot.* | Each COT panel's weekly value and change, from each report's release. |
| Chart Stats, On Candle Stats | chart_stats.*, candle_stats.* | Every row the indicator shows on each bar, by name. |
| Trading Sessions | trading_sessions.* | The high and low of every named session, so far or final, with its start and end. |
| IVB | ivb.* | The opening range high, low and middle once the range window has closed. |
| Overlay Chart | overlay_chart.* | The second instrument's last five finished candles, newest first. |
| HFT candles | hft_candles.* | The higher timeframe candle this bar belongs to, so far, and the finished ones before it. |

Every chart indicator appears in one row. The first rows read the data an indicator is drawn from and need nothing added: declare the base, or read the data family, and the chart supplies it. From Big Trades down, the script reads what that indicator draws, so the indicator must be visible on the chart.

## Builtins: declaration, absent values, text

| Signature | Returns |
| --- | --- |
| indicator(name: string [, base: string]) -> void | Sets the name the chart shows, which is also the title of its alert popup. At most one per script. The optional base names an open built in indicator to extend; see Extending a built in indicator. |
| nz(value: float [, replacement: float]) -> float | value when finite, otherwise replacement, default 0. |
| is_nan(value: float) -> bool | True only for nan. |
| na(value: float) -> bool | The same as is_nan, for Pine style code. |
| str.concat(part: string, ... ) -> string | Joins 1 to 24 strings. Fails above 1,024 characters. |
| format.compact(value: float) -> string | The chart number ladder, with K, M, B and T tiers. Empty string for nan. |
| format.fixed(value: float, decimals: int) -> string | Fixed point text, 0 through 6 decimals. Empty string for nan. |
| str.tostring(value [, decimals: int]) -> string | A number with up to 8 decimals and trailing zeros trimmed, or exactly decimals (0 through 8) decimals; true or false for a bool; NaN for nan. |
| str.length(text: string) -> int | Number of characters |
| str.contains(text: string, part: string) -> bool | Whether part appears in text |
| str.startswith(text: string, part: string) -> bool | Whether text starts with part |
| str.endswith(text: string, part: string) -> bool | Whether text ends with part |
| str.upper(text: string) -> string | Upper case, for the letters A to Z |
| str.lower(text: string) -> string | Lower case, for the letters A to Z |
| str.substring(text: string, begin: int [, end: int]) -> string | The characters from begin up to, not including, end |

### The chart

| Signature | Returns |
| --- | --- |
| chart.is_last_bar() -> bool | True on the newest bar of the window, the same as last_bar |
| chart.last_bar_index() -> int | bar_index of the newest bar, the same as bar_count - 1 |
| chart.timeframe_seconds() -> float | The usual spacing of the bars in seconds, 0 when it cannot be told, for example on range bars |
| chart.tick_size() -> float | The instrument tick, or nan when the chart did not state one |

Numbers never join strings on their own. Format them first.

```xfs
indicator("Delta Readout")
let caption: string = str.concat("DELTA ", format.compact(tape.delta()), " / ", format.fixed(tape.delta() / tape.volume() * 100, 1), "%")
plot.label(caption, "#E5E7EB")
```

## Builtins: math

| Signature | Returns |
| --- | --- |
| math.abs(x: float) -> float | Absolute value |
| math.min(a: float, b: float) -> float | Smaller of exactly two values |
| math.max(a: float, b: float) -> float | Larger of exactly two values |
| math.round(x: float [, digits: int]) -> float | Nearest whole number, or rounded to digits decimals, 0 through 15 |
| math.round_to_tick(x: float) -> float | Rounded to the instrument tick, nan when the chart states no tick |
| math.floor(x: float) -> float | Round down |
| math.ceil(x: float) -> float | Round up |
| math.sqrt(x: float) -> float | Square root |
| math.pow(base: float, exponent: float) -> float | Power |
| math.log(x: float) -> float | Natural logarithm |
| math.exp(x: float) -> float | Natural exponential |
| math.sign(x: float) -> float | Minus one, zero or one |
| math.clamp(x: float, low: float, high: float) -> float | x held inside your own bounds |
| math.scale(x: float, in_min: float, in_max: float, out_min: float, out_max: float) -> float | x remapped from one range to another, clamped at both ends |
| math.avg(a: float, b: float, ... ) -> float | Mean of 2 to 24 values |
| math.log10(x: float) -> float | Base 10 logarithm |
| math.sin, math.cos, math.tan(x: float) -> float | Trigonometry in radians |
| math.asin, math.acos, math.atan(x: float) -> float | The inverse functions, in radians |
| math.todegrees(x: float) -> float, math.toradians(x: float) -> float | Angle conversion |
| math.pi, math.e | The constants, written without parentheses |

math.min and math.max take exactly two arguments; nest them for more. An undefined domain, such as the square root of a negative number, returns nan.

math.log, math.log10, math.exp, math.pow and the trigonometry give the same result, to the last digit, on every device, so a script draws the same thing in the browser, in the desktop app and on the phone.

math.scale maps a size to a radius. It is clamped at both ends on purpose, so a print far above the top of your range keeps the largest radius instead of drawing a circle wider than the chart. It answers nan for a nan x, and when in_max is not above in_min it answers out_max at or above in_min and out_min below it.

```xfs
let size: float = tape.largest_size()
let radius: float = math.scale(nz(size, 0), 25, 400, 5, 22)
```

## Builtins: series maths

Every length must be a positive whole number. Window functions return nan until a complete window of finite values exists, which is normal at the left edge of the chart.

| Signature | Returns |
| --- | --- |
| ta.sma(source: float, length: int) -> float | Mean of a complete window |
| ta.ema(source: float, length: int) -> float | Exponential average, alpha 2 / (length + 1), seeded from the first finite value |
| ta.rma(source: float, length: int) -> float | Wilder average, alpha 1 / length |
| ta.wma(source: float, length: int) -> float | Linearly weighted mean, newest weighted length |
| ta.sum(source: float, length: int) -> float | Sum of a complete window |
| ta.variance(source: float, length: int) -> float | Population variance |
| ta.stdev(source: float, length: int) -> float | Square root of the population variance |
| ta.dev(source: float, length: int) -> float | Mean absolute deviation from the window mean |
| ta.highest(source: float, length: int) -> float | Maximum of a complete window |
| ta.lowest(source: float, length: int) -> float | Minimum of a complete window |
| ta.change(source: float [, length: int]) -> float | source minus source length bars ago, default 1 |
| ta.crossover(a: float, b: float) -> bool | a was at or below b and is now above |
| ta.crossunder(a: float, b: float) -> bool | a was at or above b and is now below |
| ta.cross(a: float, b: float) -> bool | Either of the two |

### Indicators

The usual indicators, with the same names and arguments as Pine Script where Pine has them. len is a positive whole number of bars.

| Signature | Returns |
| --- | --- |
| ta.tr([handle_na: bool]) -> float | True range. On the first bar it is high minus low, or nan with false |
| ta.atr(len) -> float | Average true range, Wilder smoothed |
| ta.rsi(source, len) -> float | Relative strength index, 0 to 100 |
| ta.macd_line(source, fast, slow) -> float | Fast EMA minus slow EMA |
| ta.macd_signal(source, fast, slow, signal) -> float | EMA of the MACD line |
| ta.macd_hist(source, fast, slow, signal) -> float | Line minus signal |
| ta.bb_upper(source, len, mult) -> float, ta.bb_lower(...) | Bollinger bands: the SMA plus or minus mult standard deviations |
| ta.bb_width(source, len, mult) -> float | Band width as a percent of the SMA |
| ta.kc_upper(source, len, mult) -> float, ta.kc_lower(...) | Keltner channels: the EMA plus or minus mult times the EMA of the true range |
| ta.stoch(source, high, low, len) -> float | Stochastic, 0 to 100 |
| ta.cci(source, len) -> float | Commodity channel index |
| ta.mfi(source, len) -> float | Money flow index, 0 to 100 |
| ta.wpr(len) -> float | Williams %R, -100 to 0 |
| ta.cmo(source, len) -> float | Chande momentum oscillator, -100 to 100 |
| ta.tsi(source, short, long) -> float | True strength index, -1 to 1 |
| ta.plus_di(len) -> float, ta.minus_di(len) -> float | Directional indicators |
| ta.adx(di_len, adx_len) -> float | Average directional index |
| ta.supertrend(factor, atr_len) -> float | The SuperTrend line |
| ta.supertrend_dir(factor, atr_len) -> float | -1 in an up trend, 1 in a down trend |
| ta.psar(start, increment, maximum) -> float | Parabolic SAR |
| ta.obv() -> float | On balance volume |
| ta.cum(source) -> float | Running total since the first bar |
| ta.mom(source, len) -> float, ta.roc(source, len) -> float | Change over len bars, and the same as a percent |
| ta.rising(source, len) -> bool, ta.falling(source, len) -> bool | Above, or below, every one of the previous len values |
| ta.hma, ta.vwma, ta.dema, ta.tema(source, len) -> float | Hull, volume weighted, double and triple exponential averages |
| ta.alma(source, len, offset, sigma) -> float | Arnaud Legoux average |
| ta.swma(source) -> float | Symmetric weighted average of the last four values |
| ta.linreg(source, len, offset) -> float | Least squares line value, offset bars back from the newest |
| ta.correlation(a, b, len) -> float | Pearson correlation |
| ta.percentrank(source, len) -> float | Percent of the previous len values at or below this one |
| ta.median(source, len) -> float | Median of the window |
| ta.percentile_nearest(source, len, percent) -> float, ta.percentile_linear(...) | Percentile by nearest rank, or interpolated |
| ta.range(source, len) -> float | Highest minus lowest |
| ta.pivothigh([source,] left, right) -> float, ta.pivotlow(...) | The pivot value, on the bar right bars after it, otherwise nan |
| ta.barssince(condition) -> float | Bars since the condition was last true, 0 on that bar, nan before it ever was |
| ta.highestbars(source, len) -> float, ta.lowestbars(...) | Offset of the extreme, 0 or negative |
| ta.valuewhen(condition, source, occurrence) -> float | source on the bar of an earlier true condition, 0 being the latest |
| ta.avg_when(condition, source, n) -> float, ta.sum_when(...) | Average or sum of source over the last n bars where the condition was true, n from 1 to 500 |
| ta.vwap() -> float | The session VWAP the chart draws, reset at 18:00 New York |
| ta.vwap_band(k: int) -> float | That VWAP plus k standard deviations, k from -3 to 3 |

- They follow Pine Script: ta.rsi, ta.atr and the directional indicators use Wilder smoothing seeded with a simple average, and ta.ema is seeded with the first value. ta.rma on its own is seeded with the first value, as it always was.
- A nan anywhere in a window gives nan for that window, where Pine skips na in some functions.
- ta.pivothigh and ta.pivotlow confirm late: the pivot is reported right bars after the bar it happened on, and equal neighbours are not a pivot.
- ta.supertrend_dir is -1 in an up trend, as in Pine. Both SuperTrend calls are nan while the ATR warms up.
- ta.median and the percentiles take windows up to 1,000 bars.
- ta.valuewhen, ta.avg_when and ta.sum_when must be called on every bar with the condition, not from inside an if.

These read the value you pass on each bar at that call site. Put the call where it runs on every bar, at the top level, so its window is continuous. A call inside an if branch, or inside one arm of ? :, only samples the bars where it runs. They cannot be called inside a loop.

```xfs
let fast: float = ta.ema(close, 9)
let slow: float = ta.ema(close, 21)
let crossed_up: bool = ta.crossover(fast, slow)
let spread_of_closes: float = ta.dev(close, 20)
```

## Builtins: time and sessions

Time functions read UTC unless you name a zone. Called without an argument they use the current bar start. An absent timestamp returns nan.

| Signature | Returns |
| --- | --- |
| time.year([timestamp: float, zone]) -> int | Four digit year |
| time.month([timestamp: float, zone]) -> int | Month, 1 through 12 |
| time.day([timestamp: float, zone]) -> int | Day of month, 1 through 31 |
| time.hour([timestamp: float, zone]) -> int | Hour, 0 through 23 |
| time.minute([timestamp: float, zone]) -> int | Minute, 0 through 59 |
| time.second([timestamp: float, zone]) -> int | Second, 0 through 59 |
| time.weekday([timestamp: float, zone]) -> int | Weekday, Sunday 0 through Saturday 6 |
| session.in(window: string [, zone]) -> bool | Whether the bar starts inside the window |
| session.open(window: string, zone) -> float | The opening price of the current session, nan outside it |
| session.adr(length: int, window: string, zone) -> float | Average high minus low of the last length complete sessions, nan outside the session |

- A window is written "HHMM-HHMM" with hours 00 to 23 and minutes 00 to 59, and excludes its closing instant.
- A zone is a whole number of minutes east of UTC, so New York in winter is -300, or one of the strings "UTC", "America/New_York" and "Europe/Prague". Any other name is rejected, so daylight saving is never guessed. New York applies the US rules in force since 2007 and Prague the EU rules in force since 1996; a bar older than that reads as outside the session, and a time.* call answers nan there, rather than being answered with the wrong rule.
- The time.* functions also read the exchange clocks: "America/Chicago" for CME, "Europe/London", "Asia/Tokyo", "Asia/Hong_Kong" and "Australia/Sydney", each with its own daylight saving rules. Chicago follows the US rules since 2007, London the EU rules since 1996 and Sydney the New South Wales rules since 2008; Tokyo and Hong Kong have no daylight saving. session.in, session.open and session.adr keep the three zones above.
- time.hour(time, "America/New_York") is the New York hour of the bar. To read a time part of an earlier bar, store it in a series first, such as let ny_day: float = time.day(time, "America/New_York"), and read ny_day[1].
- A Chicago session is the New York session one hour later all year, because both follow the US daylight saving dates: the CME day session 08:30 to 15:00 Chicago is session.in("0930-1600", "America/New_York"). A window on another exchange clock is built from that clock's hour and minute, as the example below does for London.
- session.in accepts an overnight window that wraps midnight. session.open and session.adr require a daytime window that starts before it ends.
- session.adr takes a length from 1 through 252, skips the still forming session, and skips a session that started late or had a gap. It stays nan until enough complete sessions exist.
- Call session.open and session.adr at the top level so they observe every bar.
- For futures, "America/New_York" is almost always what you want: it follows daylight saving on its own.

The cash session, with daylight saving handled:

```xfs
let window: string = "0930-1600"
let cash: bool = session.in(window, "America/New_York")
let anchor: float = session.open(window, "America/New_York")
let adr: float = session.adr(5, window, "America/New_York")
plot.line(anchor + adr, "#22C55E", 2, "solid")
plot.line(anchor - adr, "#EF4444", 2, "solid")
```

The London session on the London clock, and the CME trading day:

```xfs
let lon_h: float = time.hour(time, "Europe/London")
let lon_min: float = lon_h * 60 + time.minute(time, "Europe/London")
let in_london: bool = lon_min >= 480 && lon_min < 990
plot.candle_color(in_london, "#3B82F6", 0.35)
let cme_day: float = time.day(time, "America/Chicago")
let cme_hour: float = time.hour(time, "America/Chicago")
plot.vline(cme_hour == 17 && cme_hour[1] != 17, "#6B7280", 1, "dotted")
plot.label(str.concat("CME day ", format.fixed(cme_day, 0)), "#E5E7EB")
```

A fixed number of minutes is the other way to say it and is constant all year. Expose it as an input when you want the trader to point the tool at their own session.

The same window on a user settable shift:

```xfs
let shift: int = input.int(-300, "shiftMinutes", "Minutes from UTC", -840, 840, 15)
let inside: bool = session.in("0930-1600", shift)
plot.candle_color(inside, "#1F2937", 0.5)
```

## Builtins: higher timeframe candles

The candle of a longer timeframe that this bar belongs to, built from the bars the chart holds. The developing candle only ever contains this bar and the ones before it in the same bucket.

| Signature | Returns |
| --- | --- |
| mtf.open(minutes: int) -> float | Open of the developing higher timeframe candle |
| mtf.high(minutes: int) -> float | Its high so far |
| mtf.low(minutes: int) -> float | Its low so far |
| mtf.close(minutes: int) -> float | Its close so far, which is this bar's close |
| mtf.volume(minutes: int) -> float | Its volume so far |
| mtf.prev_open(minutes: int) -> float | Open of the last finished candle of that timeframe |
| mtf.prev_high(minutes: int) -> float | Its high |
| mtf.prev_low(minutes: int) -> float | Its low |
| mtf.prev_close(minutes: int) -> float | Its close |
| mtf.prev_volume(minutes: int) -> float | Its volume |

- minutes is a whole number from 1 to 1440. Candles are counted from midnight UTC, so 60 is the clock hour and 1440 the UTC calendar day. For the futures session use session.open and the session builtins.
- The first candle of the loaded window answers nan unless the window starts exactly on its boundary, because bars before the window are not there to complete it.
- A script may read at most 8 different timeframes. Call them at the top level, never inside a loop.
- Use the chart's own instrument only; there is no other symbol.

```xfs
let hour_high: float = mtf.prev_high(60)
let hour_low: float = mtf.prev_low(60)
plot.line(hour_high, "#22C55E", 1, "dashed")
plot.line(hour_low, "#EF4444", 1, "dashed")
plot.candle_color(close > mtf.open(240), "#22C55E", 0.3)
```

## Builtins: colour

| Signature | Returns |
| --- | --- |
| color.rgb(red: float, green: float, blue: float [, transparency: float]) -> color | A colour from 0 to 255 channels. transparency runs from 0, opaque, to 100, invisible, as in Pine |
| color.new(base: color, transparency: float) -> color | The same colour with its transparency replaced, 0 to 100 |
| color.from_gradient(value: float, low: float, high: float, low_color: color, high_color: color) -> color | A colour between low_color and high_color by where value sits between low and high, held at the ends |
| color.aqua, color.black, color.blue, color.fuchsia, color.gray, color.green, color.lime, color.maroon, color.navy, color.olive, color.orange, color.purple, color.red, color.silver, color.teal, color.white, color.yellow | Named colours, written without parentheses |

- Each builder answers a "#RRGGBB" colour when it is opaque and "#RRGGBBAA" otherwise, so its result works everywhere a colour does.
- A nan argument answers fully transparent, "#00000000", so an absent value draws nothing rather than drawing in a wrong colour.
- color.from_gradient blends all four channels, opacity included, so a gradient from "#22C55E00" to "#22C55E" fades in.
- Build colours once per bar where you can. Every colour built counts toward the strings a bar may build.

```xfs
let rsi: float = ta.rsi(close, 14)
let heat: color = color.from_gradient(rsi, 30, 70, color.red, color.green)
plot.candle_color(true, color.new(heat, 40), 1)
plot.dot(true, close, color.rgb(250, 204, 21, 20), 3)
```

## Builtins: candles and the trade tape

The tape builtins read the trades that printed inside the current bar. They split into two families, and the difference matters because only one of them is available everywhere.

### Bar totals, available on every bar

| Signature | Returns |
| --- | --- |
| tape.available() -> bool | Whether this bar carries totals or prints |
| tape.volume() -> float | Buy plus sell volume |
| tape.buy_volume() -> float | Volume that lifted the offer |
| tape.sell_volume() -> float | Volume that hit the bid |
| tape.delta() -> float | Buy volume minus sell volume |

### Individual prints

| Signature | Returns |
| --- | --- |
| tape.prints_available() -> bool | Whether this bar carries individual prints |
| tape.count() -> int | How many prints this bar carries |
| tape.price(index: int) -> float | Price of print index, nan when out of range |
| tape.size(index: int) -> float | Size of print index, nan when out of range |
| tape.side(index: int) -> side | BUY when it lifted the offer, SELL when it hit the bid, UNDEFINED when out of range |
| tape.time(index: int) -> float | Epoch milliseconds, nan when absent |
| tape.vwap() -> float | Size weighted price of the prints on this bar |
| tape.high() -> float | Highest print price on this bar |
| tape.low() -> float | Lowest print price on this bar |

### Finding the big prints in one call

These walk the prints for you with your rule as the argument, and are faster than a loop over tape.price and tape.size. Your own big trade rule is a size threshold, a side and a radius that follows size.

| Signature | Returns |
| --- | --- |
| tape.largest_index() -> int | Index of the largest print on the bar, or -1 |
| tape.largest_size() -> float | Size of that print, nan when there is none |
| tape.largest_price() -> float | Price of that print |
| tape.largest_side() -> side | Side of that print |
| tape.nth_largest_index(rank: int) -> int | Index of the print at that size rank, 0 being the largest, or -1. Equal sizes keep tape order |
| tape.count_above(min_size: float) -> int | How many prints on the bar are at least min_size |
| tape.volume_above(min_size: float) -> float | Their total volume |
| tape.buy_volume_above(min_size: float) -> float | The part of it that lifted the offer |
| tape.sell_volume_above(min_size: float) -> float | The part that hit the bid |

- Print indexes start at 0 and run to tape.count() minus 1, in the order the trades printed.
- tape.largest_index and tape.nth_largest_index answer -1 when there is no print, and tape.price, tape.size, tape.side and tape.time answer nan or UNDEFINED for -1, so the pair needs no guard.
- A whole bar answer is one call. Reach for tape.price and tape.size only when you want one specific print.
- Calling any print accessor tells the chart your indicator wants the per print tape for the window, which is a heavier load than totals alone. Use the totals when they answer the question.
- Bars built from the tape carry prints for the whole loaded window. Time based history is served as completed candles, so on a time chart tape.prints_available() is commonly false on older bars while the totals still answer. Guard print work with tape.prints_available().

```xfs
let has_prints: bool = tape.prints_available()
let mean_size: float = tape.volume() / math.max(tape.count(), 1)
let first_side: side = tape.side(0)
let sweep: bool = has_prints && tape.size(0) > 100 && first_side == "BUY"
let biggest: int = tape.largest_index()
let biggest_price: float = tape.price(biggest)
```

## Builtins: the footprint ladder

A footprint bar is a ladder of price rows, each carrying the volume that hit the bid and the volume that lifted the offer. These accessors read the same ladder the footprint draws. Rows are ordered from the lowest price up, and a row price is the BOTTOM of that row, so a price p belongs to the row r with r.price at or below p and p below r.price plus one tick.

| Signature | Returns |
| --- | --- |
| cell.available() -> bool | Whether this bar carries a ladder |
| cell.count() -> int | How many rows |
| cell.price(index: int) -> float | Price at the bottom of the row |
| cell.bid_volume(index: int) -> float | Volume that hit the bid on that row |
| cell.ask_volume(index: int) -> float | Volume that lifted the offer on that row |
| cell.volume(index: int) -> float | Both sides added |
| cell.delta(index: int) -> float | Ask volume minus bid volume on that row |
| cell.imbalance(index: int) -> float | Row delta divided by row volume, from -1 to 1 |
| cell.index_at(price: float) -> int | Index of the row that contains this price, or -1 |
| cell.volume_at(price: float) -> float | Row volume at a price |
| cell.bid_volume_at(price: float) -> float | Bid volume at a price |
| cell.ask_volume_at(price: float) -> float | Ask volume at a price |
| cell.delta_at(price: float) -> float | Row delta at a price |
| cell.poc_index() -> int | Index of the highest volume row, or -1 |
| cell.poc_price() -> float | Price of the highest volume row |
| cell.max_volume() -> float | Volume on that row |
| cell.max_delta() -> float | Largest absolute row delta on the bar |
| cell.high_price() -> float | Price of the top row |
| cell.low_price() -> float | Price of the bottom row |
| cell.va_high([percent: float]) -> float | Top of the value area, default 70 percent |
| cell.va_low([percent: float]) -> float | Bottom of the value area |
| cell.bid_imbalances(ratio: float) -> int | Rows whose bid beat the ask one row above by at least ratio |
| cell.ask_imbalances(ratio: float) -> int | Rows whose ask beat the bid one row below by at least ratio |
| cell.stacked_bid(ratio: float, min_count: int) -> bool | A run of at least min_count consecutive bid imbalances |
| cell.stacked_ask(ratio: float, min_count: int) -> bool | The same for ask imbalances |
| chart.tick_size() -> float | The instrument tick, or nan when the chart did not state one |

- Absent is not zero. A bar with no ladder answers cell.available() false, cell.count() zero, cell.index_at() and cell.poc_index() minus one, the two stacked tests false, and nan everywhere else. A bar that HAS a ladder answers zero for a price nothing traded at, because that is an observed zero.
- cell.index_at() and cell.poc_index() answer -1 when there is no row, and the row accessors answer nan for -1, so cell.price(cell.index_at(p)) needs no guard.
- The imbalance accessors compare a row with its diagonal neighbour: a bid against the ask one row above, an ask against the bid one row below.
- They need to know what one row apart means, so they use the instrument tick. Where the chart states no tick they answer nan, or false for the two stacked tests, rather than calling rows two ticks apart neighbours. The price lookups answer nan and -1 the same way.
- The heavier answers, the point of control, the value area and the imbalance counts, are computed once per bar however many times you call them.
- Calling any cell accessor tells the chart your indicator wants the ladder, the same way the print level tape accessors do.

```xfs
let has_ladder: bool = cell.available()
let poc: float = cell.poc_price()
let value_high: float = cell.va_high(70)
let value_low: float = cell.va_low(70)
let stacked: bool = has_ladder && cell.stacked_ask(3, 3)
plot.hline(poc, "#F59E0B", 1, "dotted", "POC")
plot.marker(stacked, high, "#22C55E", "square", "above", 8, 2)
```

## Builtins: level 1 quotes

The best bid and offer. On a live chart the newest bar carries the current quote, and every bar that formed while a script reading l1.* was on the chart keeps the last quote it saw, how many quotes arrived during it and the spread they showed. Bars from before that carry none and answer l1.available() false. In replay there is no recorded quote at all, so l1.supported() is false there. For a book with a recorded history, use the depth builtins.

| Signature | Returns |
| --- | --- |
| l1.supported() -> bool | Whether quotes are wired on this chart |
| l1.available() -> bool | Whether a quote is attached to this bar |
| l1.bid_price() -> float | Best bid, nan without a quote |
| l1.ask_price() -> float | Best offer |
| l1.bid_size() -> float | Size at the best bid |
| l1.ask_size() -> float | Size at the best offer |
| l1.bid_exchange() -> string | Bid exchange code, empty when not stated |
| l1.ask_exchange() -> string | Offer exchange code, empty when not stated |
| l1.quote_count() -> int | How many quotes arrived while this bar formed, 0 without any |
| l1.spread_avg() -> float | Average offer minus bid over those quotes, nan without any |
| l1.spread_max() -> float | Widest spread seen during the bar |
| l1.spread_min() -> float | Narrowest spread seen during the bar |

- On past bars the quote is the one standing when the bar closed. On the newest bar it is the quote right now.
- The chart keeps up to the newest 20,000 bars of quotes per instrument and timeframe. Reopening the chart starts the history again.
- The spread statistics count only quotes with both a bid and an offer above zero.

```xfs
let quoted: bool = l1.supported() && l1.available()
let spread: float = l1.ask_price() - l1.bid_price()
let jumpy: bool = quoted && l1.spread_max() > l1.spread_min() * 3
plot.marker(jumpy, high, "#F59E0B", "x", "above", 8, 2)
let book_lean: float = (l1.bid_size() - l1.ask_size()) / (l1.bid_size() + l1.ask_size())
if quoted {
  plot.label(str.concat("SPREAD ", format.fixed(spread, 2), "  BID ", format.compact(l1.bid_size()), " x ASK ", format.compact(l1.ask_size())), "#E5E7EB")
}
```

## Builtins: level 2 depth of book

The aggregated book, the same book a heatmap draws, as one snapshot per bar: the newest book the chart recorded inside that bar. Levels are best first: bids descending from the top of the book, offers ascending, so index 0 is always the inside market. Levels with nothing resting are left out.

| Signature | Returns |
| --- | --- |
| l2.supported() -> bool | Whether depth is wired on this chart |
| l2.available() -> bool | Whether a book snapshot landed inside this bar |
| l2.time() -> float | Snapshot timestamp, epoch milliseconds |
| l2.bid_count() -> int | Bid levels in the snapshot, 0 without one |
| l2.ask_count() -> int | Offer levels in the snapshot |
| l2.bid_price(index: int) -> float | Price of a bid level, index 0 the best bid |
| l2.bid_size(index: int) -> float | Resting size on it |
| l2.ask_price(index: int) -> float | Price of an offer level, index 0 the best offer |
| l2.ask_size(index: int) -> float | Resting size on it |
| l2.bid_total([depth: int]) -> float | Sum of the best depth bid sizes, every level when depth is omitted or 0 |
| l2.ask_total([depth: int]) -> float | The same on the offer side |
| l2.imbalance([depth: int]) -> float | Bid total minus ask total over their sum, from -1 to 1 |
| l2.size_at(price: float) -> float | Resting size at an exact price, zero when the book has none there |
| l2.spread() -> float | Best offer minus best bid |
| l2.mid() -> float | Midpoint of the top of book |
| l2.max_bid_price() -> float | Price of the bid level with the most resting size |
| l2.max_bid_size() -> float | Its size |
| l2.max_ask_price() -> float | Price of the offer level with the most resting size |
| l2.max_ask_size() -> float | Its size |

- Live, the chart records the book on each bar from the moment the script starts running on it, up to the newest 5,000 bars, and the newest bar always carries the current book. Bars from before that answer l2.available() false. Reopening the chart starts the history again.
- A bar with no snapshot answers zero counts and nan for every price and size. An index of -1 answers nan.
- The max builtins find the biggest resting level on each side of the snapshot, the wall, in one call. Of two equal levels the one nearer the inside market wins. Without a snapshot they answer nan.
- Totals only walk the levels you ask for, so l2.imbalance(10) is cheap even on a deep book.
- In replay the book comes from the recorded depth at the playhead and never from after it.

```xfs
let has_depth: bool = l2.supported() && l2.available()
let lean: float = l2.imbalance(10)
let resting_bid: float = l2.bid_total(5)
let resting_ask: float = l2.ask_total(5)
if has_depth {
  plot.histogram(lean, "#22C55E", "#EF4444")
  plot.label(str.concat("BID WALL ", format.fixed(l2.max_bid_price(), 2), " x ", format.compact(l2.max_bid_size()), "  ASK WALL ", format.fixed(l2.max_ask_price(), 2), " x ", format.compact(l2.max_ask_size())), "#E5E7EB")
}
plot.panel_range(-1, 1)
```

## Builtins: level 3 individual orders

Market by order: every resting order whose life overlapped the bar, each one seen exactly as it stood at the end of that bar. Nothing the order did later leaks into an earlier bar, so a bar at 09:31 never knows what the order became at 15:59.

| Signature | Returns |
| --- | --- |
| mbo.supported() -> bool | Whether order level data is wired on this chart |
| mbo.available() -> bool | Whether this bar carries orders |
| mbo.count() -> int | How many orders overlapped the bar, 0 without data |
| mbo.order_id(index: int) -> string | The exchange order id, empty when out of range |
| mbo.side(index: int) -> side | BUY for a resting bid, SELL for a resting offer |
| mbo.price(index: int) -> float | Its resting price at the end of the bar |
| mbo.size(index: int) -> float | Its displayed size at the end of the bar |
| mbo.peak_size(index: int) -> float | The largest size it had shown so far |
| mbo.filled(index: int) -> float | How much of it had traded so far |
| mbo.refills(index: int) -> float | How many times its size grew back at the same price so far |
| mbo.first_time(index: int) -> float | When it was first seen, epoch milliseconds |
| mbo.last_time(index: int) -> float | Its last change seen so far |
| mbo.status(index: int) -> string | "active" while it rests, "executed" or "cancelled" once it ended inside the bar |
| mbo.largest_index() -> int | Index of the largest displayed order on the bar, or -1 |
| mbo.largest_size() -> float | Its size |
| mbo.bid_size_total() -> float | Total displayed size of the resting bids |
| mbo.ask_size_total() -> float | Total displayed size of the resting offers |
| mbo.iceberg_count(min_refills: float) -> int | How many orders on the bar were refilled at least min_refills times |

### Order events

Every change to an order stamped inside the bar, oldest first: when it was placed, when its price or size changed, each fill and its cancel. This is the stream to read when the question is what happened during the bar, such as how much size was pulled from the bid.

| Signature | Returns |
| --- | --- |
| mbo.event_count() -> int | How many events happened inside the bar, 0 without data |
| mbo.event_type(index: int) -> string | "add", "modify", "fill" or "cancel", empty when out of range |
| mbo.event_price(index: int) -> float | The resting price, or for a fill the execution price |
| mbo.event_size(index: int) -> float | The new resting size, the contracts filled, or the size removed by a cancel |
| mbo.event_side(index: int) -> side | The side of the resting order: BUY for a bid, SELL for an offer |
| mbo.event_time(index: int) -> float | When it happened, epoch milliseconds |
| mbo.event_order_id(index: int) -> string | The order it belongs to |
| mbo.added_size(side: string) -> float | Total size placed on that side during the bar; side is "BUY" or "SELL" |
| mbo.cancelled_size(side: string) -> float | Total size pulled from that side |
| mbo.filled_size(side: string) -> float | Total size filled against resting orders on that side |

- An order that was already resting when the chart started watching has no add event; its later changes are there.
- Without data the three totals answer nan, the count 0 and the accessors their empty values.
- Order events are a lot of rows on a busy bar. Filter inside the loop and draw only what matters.

```xfs
let has_orders: bool = mbo.supported() && mbo.available()
let pulled_bid: float = mbo.cancelled_size("BUY")
let pulled_ask: float = mbo.cancelled_size("SELL")
for i = 0 to (has_orders ? mbo.event_count() : 0) - 1 by 1 {
  if mbo.event_type(i) == "cancel" && mbo.event_size(i) >= 50 {
    plot.dot(true, mbo.event_price(i), mbo.event_side(i) == "BUY" ? "#F97316" : "#38BDF8", 5)
  }
}
plot.label(str.concat("PULLED BID ", format.compact(nz(pulled_bid, 0)), "  ASK ", format.compact(nz(pulled_ask, 0))), "#E5E7EB")
```

- Order indexes run from 0 to mbo.count() minus 1, oldest order first. The whole bar answers (largest, totals, iceberg count) are one call; loop over the indexes when you need every order.
- mbo.largest_index() answers -1 on a bar with no orders, and the per order accessors answer the empty values for -1, so mbo.side(mbo.largest_index()) needs no guard.
- Order data reaches back one trading day by default. With the Iceberg MBO indicator on the chart, its Days to load setting decides.
- A bar with no data answers zero counts, -1, UNDEFINED, empty strings and nan.

```xfs
let has_orders: bool = mbo.supported() && mbo.available()
let big: int = mbo.largest_index()
let big_side: side = mbo.side(big)
let refilled: float = mbo.refills(big)
let icebergs: float = mbo.iceberg_count(3)
plot.marker(has_orders && icebergs > 0 && big_side == "BUY", low, "#22C55E", "circle", "below", 8, 2)
plot.marker(has_orders && icebergs > 0 && big_side == "SELL", high, "#EF4444", "circle", "above", 8, 2)
```

## Builtins: options gamma

The gamma publication that landed inside the bar, with every price already on the chart price axis. A live chart keeps every publication it received since the script started running, each on the bar it landed in, up to the newest 2,000. A bar no publication landed in answers gex.available() false; nothing is carried forward for you, so keep the numbers in a var to draw them across the chart.

| Signature | Returns |
| --- | --- |
| gex.supported() -> bool | Whether gamma is wired on this chart |
| gex.available() -> bool | Whether a publication landed inside this bar |
| gex.time() -> float | Publication timestamp, epoch milliseconds |
| gex.spot() -> float | Spot at publication, as a chart price |
| gex.zero_gamma() -> float | The zero gamma level |
| gex.major_pos_vol() -> float | Largest positive gamma level by volume |
| gex.major_neg_vol() -> float | Largest negative gamma level by volume |
| gex.major_pos_oi() -> float | Largest positive gamma level by open interest |
| gex.major_neg_oi() -> float | Largest negative gamma level by open interest |
| gex.net_vol() -> float | Net gamma by volume |
| gex.net_oi() -> float | Net gamma by open interest |
| gex.strike_count() -> int | How many strikes the publication carries, 0 without one |
| gex.strike_price(index: int) -> float | Price of a strike, ascending |
| gex.strike_vol(index: int) -> float | Its gamma by volume |
| gex.strike_oi(index: int) -> float | Its gamma by open interest |
| gex.strike_at(price: float) -> float | Price of the strike that owns this price |
| gex.vol_at(price: float) -> float | Its gamma by volume |
| gex.oi_at(price: float) -> float | Its gamma by open interest |

- A strike owns the half open range from itself to the next strike, so nothing is rounded to a nearest strike.
- Gamma comes from your own GEXBot key and what your GEXBot tier includes. The script opens it by itself; no gamma indicator needs to be on the chart.
- Gamma is present time data with no recorded history, so gex.supported() is false in replay.

```xfs
var zero_level: float = nan
zero_level := nz(zero_level[1], nan)
if gex.supported() && gex.available() {
  zero_level := gex.zero_gamma()
}
plot.hline(zero_level, "#F59E0B", 1, "dashed", "Zero gamma")
```

## Builtins: the Big Trades markers

The markers the chart's own Big Trades indicator drew on this bar, with that indicator's current settings: each one's price, size, side and time. Use them to filter, count, colour or alert on what Big Trades already shows instead of re-detecting large prints from the tape.

| Signature | Returns |
| --- | --- |
| bigtrades.available() -> bool | Whether a visible Big Trades indicator is on this chart |
| bigtrades.count() -> int | How many markers it drew on this bar |
| bigtrades.price(index: int) -> float | The price the marker is drawn at |
| bigtrades.size(index: int) -> float | Its size in contracts |
| bigtrades.side(index: int) -> side | BUY or SELL |
| bigtrades.time(index: int) -> float | Its time, epoch milliseconds |

- The markers cover all loaded bars, not only the bars on screen, so a script gives the same answer at every zoom, on every screen size and on every device.
- Marker indexes run from 0 to bigtrades.count() minus 1, oldest first. An index of -1, or one past the last marker, answers nan or UNDEFINED.
- Absent is not empty: without a visible Big Trades indicator every bar answers available() false; with one, a bar it marked nothing on answers count() 0.

```xfs
let marked: bool = bigtrades.available() && bigtrades.count() > 0
let first_size: float = bigtrades.size(0)
let first_side: side = bigtrades.side(0)
plot.dot(marked && first_size >= 200 && first_side == "BUY", bigtrades.price(0), "#22C55E", 10)
plot.dot(marked && first_size >= 200 && first_side == "SELL", bigtrades.price(0), "#EF4444", 10)
```

## Builtins: Effort Zones and Range Absorption zones

The zones the chart's own Effort Zones and Range Absorption indicators drew, read by name. A script gets exactly the boxes you see on the chart, with that indicator's own settings: the top, the bottom, the first bar and the side of each zone. The two families have the same builtins; write range_absorption where the examples say effort_zones to read the other one. Use them to alert when price enters a zone, count big trades inside a zone, recolour candles that close through one, or state how far away the nearest one is.

| Signature | Returns |
| --- | --- |
| effort_zones.available() -> bool | Whether a visible Effort Zones indicator is on this chart |
| effort_zones.count() -> int | How many of its zones cover this bar, 0 without the indicator |
| effort_zones.started() -> int | How many of those zones begin on this bar |
| effort_zones.high(index: int) -> float | Top of a zone covering this bar |
| effort_zones.low(index: int) -> float | Bottom of that zone |
| effort_zones.start(index: int) -> float | Start of the zone's first bar, epoch milliseconds |
| effort_zones.side(index: int) -> side | BUY, SELL, or UNDEFINED when the zone has no side |
| effort_zones.at(price: float) -> int | Index of the newest zone whose band holds the price, edges included, or -1 |
| effort_zones.above(price: float) -> int | Index of the zone entirely above the price with the lowest bottom, or -1 |
| effort_zones.below(price: float) -> int | Index of the zone entirely below the price with the highest top, or -1 |
| range_absorption.available() -> bool | Whether a visible Range Absorption indicator is on this chart |
| range_absorption.count() -> int | How many of its zones cover this bar, 0 without the indicator |
| range_absorption.started() -> int | How many of those zones begin on this bar |
| range_absorption.high(index: int) -> float | Top of a zone covering this bar |
| range_absorption.low(index: int) -> float | Bottom of that zone |
| range_absorption.start(index: int) -> float | Start of the zone's first bar, epoch milliseconds |
| range_absorption.side(index: int) -> side | BUY, SELL, or UNDEFINED when the zone has no side |
| range_absorption.at(price: float) -> int | Index of the newest zone whose band holds the price, edges included, or -1 |
| range_absorption.above(price: float) -> int | Index of the zone entirely above the price with the lowest bottom, or -1 |
| range_absorption.below(price: float) -> int | Index of the zone entirely below the price with the highest top, or -1 |

- Zone indexes run from 0 to count() minus 1, oldest zone first, and cover only the zones on THIS bar. A zone covers every bar from its first bar to its last, and a zone still running covers the newest bar.
- -1 is none: high, low and start answer nan and side answers UNDEFINED. at, above and below answer -1 when no zone fits, so their answer can be passed straight into high, low, start or side. A nan price answers -1, and on a tie in above or below the newest zone wins.
- Absent is not empty. Without the indicator visible on the chart every bar answers available() false and count() 0, and the settings under the editor say Effort Zones Required or Range Absorption Required. With it, a bar no zone covers answers count() 0.
- Zones appear as the chart draws them. A script sees exactly the boxes on the chart, live and at the replay playhead alike. The indicator's own settings decide which zones exist; a script reads them and cannot change them.
- effort_zones.* and range_absorption.* return the zones exactly as the chart draws them. On past bars a zone shows from its first bar, which can be a few bars before the chart first drew it.

Effort Zones is read on the bar it describes. Draw at a zone, tint candles inside one, alert when price enters one, or state how far the nearest zones are. Output that would have to be stored or carried across bars is not accepted, and the builder says so in one line.

```xfs
let zone: int = effort_zones.at(close)
let in_zone: bool = zone >= 0
let entered: bool = bar_index > 0 && in_zone && !in_zone[1]
let zone_side: side = effort_zones.side(zone)
plot.marker(entered && zone_side == "BUY", low, "#22C55E", "triangle", "below", 10, 3)
plot.marker(entered && zone_side == "SELL", high, "#EF4444", "triangle", "above", 10, 3)
let next_up: int = effort_zones.above(close)
plot.label(str.concat("NEXT ZONE ABOVE ", format.fixed(effort_zones.low(next_up), 2)), "#E5E7EB")
```

## Builtins: session profiles, trading sessions and the opening range

Four chart indicators give a script session levels by name: the Session Volume Profile and the Session TPO give each session's point of control and value area, Trading Sessions gives the high and low of each named session, and IVB gives the opening range. Each needs its indicator visible on the chart, and each uses that indicator's own settings.

### Session Volume Profile and Session TPO

| Signature | Returns |
| --- | --- |
| session_profile.available() -> bool | Whether a visible Session Volume Profile is on this chart |
| session_profile.count() -> int | How many sessions this bar can read: its own and up to five before it |
| session_profile.poc(index: int) -> float | Point of control of that session |
| session_profile.vah(index: int) -> float | Value area high |
| session_profile.val(index: int) -> float | Value area low |
| session_profile.start(index: int) -> float | Start of the session's first bar, epoch milliseconds |
| session_profile.end(index: int) -> float | Start of its last bar, epoch milliseconds, nan while it is still developing |
| session_tpo.available() -> bool | Whether a visible Session TPO is on this chart |
| session_tpo.count() -> int | How many sessions this bar can read: its own and up to five before it |
| session_tpo.poc(index: int) -> float | Point of control of that session |
| session_tpo.vah(index: int) -> float | Value area high |
| session_tpo.val(index: int) -> float | Value area low |
| session_tpo.start(index: int) -> float | Start of the session's first bar, epoch milliseconds |
| session_tpo.end(index: int) -> float | Start of its last bar, epoch milliseconds, nan while it is still developing |

- A session is the futures trading day, 18:00 to 17:00 New York time.
- Index 0 is this bar's own session, built from its bars up to and including this one, never a later bar. 1 to 5 are the completed sessions before it, newest first. -1 or an index past count() minus 1 answers nan.
- A finished session's final levels appear only on bars after it ended. On its own bars a script sees the levels as they developed.
- session_profile uses the value area percent set on the chart's Session Volume Profile; session_tpo uses the bracket length, value area percent and row size set on the Session TPO. The levels are the same at every zoom and on every device.
- session_profile needs the footprint ladder: where a bar of the session has none, that session reads nan from that bar on.
- Without the indicator visible, available() is false and the settings under the editor say Session Volume Profile Required or Session TPO Required.

```xfs
var prior_poc: float = nan
prior_poc := nz(prior_poc[1], nan)
if session_profile.available() && session_profile.count() > 1 {
  prior_poc := session_profile.poc(1)
}
plot.hline(prior_poc, "#F59E0B", 1, "dashed", "Prior POC")
plot.line(session_profile.poc(0), "#E5E7EB", 1, "dotted")
```

### Trading Sessions

| Signature | Returns |
| --- | --- |
| trading_sessions.available() -> bool | Whether a visible Trading Sessions indicator is on this chart |
| trading_sessions.count() -> int | How many session rows it has enabled |
| trading_sessions.find(name: string) -> int | Index of the session with exactly that name, or -1 |
| trading_sessions.name(index: int) -> string | The session's name as set in the indicator |
| trading_sessions.high(index: int) -> float | Its high: so far while inside it, final once it ended |
| trading_sessions.low(index: int) -> float | Its low, the same way |
| trading_sessions.start(index: int) -> float | Start of the session's first bar, epoch milliseconds |
| trading_sessions.end(index: int) -> float | Start of its last bar, epoch milliseconds, nan while it is still running |

- Every enabled session row of the indicator is one record on every bar, in the indicator's order, so find("London") gives the index to read London by. The name must match the row's label exactly.
- Inside a session its high and low are the high and low so far and end is nan. Once it ends the record holds its final high and low and its last bar, until that session starts again.
- Every session on the loaded bars is covered, not only the ones drawn on screen. Before a session's first appearance on the loaded bars, or for one that began before the first loaded bar, the values are nan.
- Without the indicator visible the settings say Trading Sessions Required.

```xfs
let london: int = trading_sessions.find("London")
let london_done: bool = !is_nan(trading_sessions.end(london))
plot.line(trading_sessions.high(london), "#22C55E", 1, "dashed")
plot.line(trading_sessions.low(london), "#EF4444", 1, "dashed")
plot.marker(london_done && close > trading_sessions.high(london) && close[1] <= trading_sessions.high(london), high, "#22C55E", "triangle", "above", 9, 3)
```

### IVB, the opening range

| Signature | Returns |
| --- | --- |
| ivb.available() -> bool | Whether a visible IVB indicator is on this chart |
| ivb.count() -> int | How many opening ranges cover this bar |
| ivb.high(index: int) -> float | Top of the range |
| ivb.low(index: int) -> float | Bottom of the range |
| ivb.mid(index: int) -> float | Middle of the range |

- The range appears only from the first bar that starts at or after the range window has closed, never on the bars inside the window, and it lasts as far as the indicator draws its lines.
- With the indicator's levels switched off there is no range. Without the indicator visible the settings say IVB Required.

```xfs
let has_range: bool = ivb.available() && ivb.count() > 0
plot.marker(has_range && close > ivb.high(0) && close[1] <= ivb.high(0), high, "#22C55E", "triangle", "above", 9, 3)
plot.marker(has_range && close < ivb.low(0) && close[1] >= ivb.low(0), low, "#EF4444", "triangle", "below", 9, 3)
```

## Builtins: FVG gaps and the Stop Run, Iceberg and Delta Spike markers

Four more chart indicators are read by what they draw. FVG draws gaps, which a script reads exactly like zones. Stop Run, Iceberg and Delta Spike draw markers, which a script reads one marker at a time. Each needs its indicator visible on the chart and uses that indicator's own settings.

### FVG

| Signature | Returns |
| --- | --- |
| fvg.available() -> bool | Whether a visible FVG indicator is on this chart |
| fvg.count() -> int | How many of its gaps cover this bar |
| fvg.started() -> int | How many of those gaps begin on this bar |
| fvg.high(index: int) -> float | Top of a gap covering this bar |
| fvg.low(index: int) -> float | Bottom of that gap |
| fvg.side(index: int) -> side | BUY for a bullish gap, SELL for a bearish gap |
| fvg.start(index: int) -> float | Start of the gap's first bar, epoch milliseconds |
| fvg.at(price: float) -> int | Index of the newest gap whose band holds the price, edges included, or -1 |
| fvg.above(price: float) -> int | Index of the gap entirely above the price with the lowest bottom, or -1 |
| fvg.below(price: float) -> int | Index of the gap entirely below the price with the highest top, or -1 |

- A gap covers the bars from the bar that completed it through the bar its box ends on. Indexes run oldest gap first, and -1 is none, exactly as for the zones above.
- side is the gap's own direction, whatever colour the chart paints it.
- Without the indicator visible the settings say FVG Required.

### Stop Run, Iceberg and Delta Spike

| Signature | Returns |
| --- | --- |
| stop_run.available() -> bool | Whether a visible Stop Run indicator is on this chart |
| stop_run.count() -> int | How many of its markers sit on this bar |
| stop_run.price(index: int) -> float | The market price the marker stands for |
| stop_run.side(index: int) -> side | BUY for a buy side marker, SELL for a sell side marker |
| iceberg.available() -> bool | Whether a visible Iceberg indicator is on this chart |
| iceberg.count() -> int | How many of its markers sit on this bar |
| iceberg.price(index: int) -> float | The market price the marker stands for |
| iceberg.side(index: int) -> side | BUY for a buy side marker, SELL for a sell side marker |
| delta_spike.available() -> bool | Whether a visible Delta Spike indicator is on this chart |
| delta_spike.count() -> int | How many of its markers sit on this bar |
| delta_spike.price(index: int) -> float | The market price the marker stands for |
| delta_spike.side(index: int) -> side | BUY for a buy side marker, SELL for a sell side marker |

- Each marker sits on its own bar only. A bar's markers are indexed from its lowest price up, and -1 or an index past the last marker answers nan or UNDEFINED.
- price is the market price the marker stands for, the same at every zoom, even though the chart draws the marker glyph slightly offset from it.
- side is BUY for the indicator's buy side marker and SELL for its sell side marker, the same split the indicator shows with its two colours.
- iceberg.* reads the Iceberg indicator. For individual resting orders, including Iceberg MBO's, use mbo.*.
- Delta Spike may be on the chart more than once; a script reads the first visible one.
- Without the indicator visible the settings say Stop Run Required, Iceberg Required or Delta Spike Required.

```xfs
let gap: int = fvg.at(close)
let run_price: float = stop_run.price(0)
let run_in_gap: bool = stop_run.count() > 0 && fvg.at(run_price) >= 0
plot.marker(run_in_gap && stop_run.side(0) == "BUY", run_price, "#22C55E", "circle", "below", 10, 2)
plot.marker(run_in_gap && stop_run.side(0) == "SELL", run_price, "#EF4444", "circle", "above", 10, 2)
plot.candle_color(gap >= 0 && fvg.side(gap) == "BUY", "#22C55E", 0.35)
```

## Builtins: levels, stats, flows and candles from the rest of the catalog

Every other chart indicator that draws something a script can use is readable by name, grouped here by the shape of what it draws. Each family needs its indicator visible on the chart, reads that indicator's own settings, and answers exactly what the chart shows. Every family has available() and count(); -1 or an index past count() minus 1 is none, answering nan, an empty string or UNDEFINED; and when the indicator is missing the settings under the editor say its name followed by Required, for example Important Levels Required or Net GEX Required.

### Level lines: Important Levels, GEX Levels, GEX Classic, GEX State

| Signature | Returns |
| --- | --- |
| important_levels.available() -> bool | Whether a visible Important Levels indicator is on this chart |
| important_levels.count() -> int | How many lines this bar carries |
| important_levels.find(name: string) -> int | Index of the line with exactly that label, or -1 |
| important_levels.name(index: int) -> string | The line's label as the chart prints it |
| important_levels.price(index: int) -> float | The line's price |
| important_levels.at(price: float) -> int | Index of a line exactly at that price, or -1 |
| important_levels.above(price: float) -> int | Index of the nearest line above the price, or -1 |
| important_levels.below(price: float) -> int | Index of the nearest line below the price, or -1 |
| gex_levels.available() -> bool | Whether a visible GEX Levels indicator is on this chart |
| gex_levels.count() -> int | How many lines this bar carries |
| gex_levels.find(name: string) -> int | Index of the line with exactly that label, or -1 |
| gex_levels.name(index: int) -> string | The line's label as the chart prints it |
| gex_levels.price(index: int) -> float | The line's price |
| gex_levels.at(price: float) -> int | Index of a line exactly at that price, or -1 |
| gex_levels.above(price: float) -> int | Index of the nearest line above the price, or -1 |
| gex_levels.below(price: float) -> int | Index of the nearest line below the price, or -1 |
| gex_classic.available() -> bool | Whether a visible GEX Classic indicator is on this chart |
| gex_classic.count() -> int | How many lines this bar carries |
| gex_classic.find(name: string) -> int | Index of the line with exactly that label, or -1 |
| gex_classic.name(index: int) -> string | The line's label as the chart prints it |
| gex_classic.price(index: int) -> float | The line's price |
| gex_classic.at(price: float) -> int | Index of a line exactly at that price, or -1 |
| gex_classic.above(price: float) -> int | Index of the nearest line above the price, or -1 |
| gex_classic.below(price: float) -> int | Index of the nearest line below the price, or -1 |
| gex_state.available() -> bool | Whether a visible GEX State indicator is on this chart |
| gex_state.count() -> int | How many lines this bar carries |
| gex_state.find(name: string) -> int | Index of the line with exactly that label, or -1 |
| gex_state.name(index: int) -> string | The line's label as the chart prints it |
| gex_state.price(index: int) -> float | The line's price |
| gex_state.at(price: float) -> int | Index of a line exactly at that price, or -1 |
| gex_state.above(price: float) -> int | Index of the nearest line above the price, or -1 |
| gex_state.below(price: float) -> int | Index of the nearest line below the price, or -1 |

- important_levels: one record per line, named as the chart prints it: a period and a level, such as pD H, pD L, pW POC, cD L or cM VWAP, or a custom window's name and level, such as Asia H. The periods are pD and cD for the previous and current day, pW and cW for the week, pM and cM for the month; the levels are H, L, O, C, POC, VAH, VAL and VWAP.
- important_levels never looks ahead: each bar reads the lines the chart would draw at that bar's start from the bars up to it, so a previous day's high appears only once that day has ended and a running day's high is its high so far. The newest bar reads exactly the lines on the chart.
- The GEX level families read the lines drawn from the snapshot the chart shows now, live or at the replay playhead, and carry them on the newest bar only. For a gamma history bar by bar, use gex.*.
- Their names are the chart's labels, such as ZG, GEX+, GEX- and MaxΔ on GEX Levels; ZG, P-Vol, N-Vol, P-OI, N-OI and the minute lines on GEX Classic; MLG, MSG, LG, SG, P-DEL and N-DEL on GEX State. List the rest with name(i).
- The GEX families need your GEXBot key connected.

```xfs
let pdh: int = important_levels.find("pD H")
let pdl: int = important_levels.find("pD L")
let broke_pdh: bool = pdh >= 0 && close > important_levels.price(pdh) && open <= important_levels.price(pdh)
plot.marker(broke_pdh, high, "#22C55E", "triangle", "above", 10, 3)
let zg: int = gex_levels.find("ZG")
plot.label(str.concat("PREV DAY LOW ", format.fixed(important_levels.price(pdl), 2), "  ZERO GAMMA ", format.fixed(gex_levels.price(zg), 2)), "#E5E7EB")
```

### Named values: Orderflow Stats, Chart Stats, On Candle Stats, Dealers Flow, COT, Net GEX

| Signature | Returns |
| --- | --- |
| orderflow_stats.available() -> bool | Whether a visible Orderflow Stats indicator is on this chart |
| orderflow_stats.count() -> int | How many named values this bar carries |
| orderflow_stats.find(name: string) -> int | Index of the value with exactly that name, or -1 |
| orderflow_stats.name(index: int) -> string | Its name |
| orderflow_stats.value(index: int) -> float | Its number |
| chart_stats.available() -> bool | Whether a visible Chart Stats indicator is on this chart |
| chart_stats.count() -> int | How many named values this bar carries |
| chart_stats.find(name: string) -> int | Index of the value with exactly that name, or -1 |
| chart_stats.name(index: int) -> string | Its name |
| chart_stats.value(index: int) -> float | Its number |
| candle_stats.available() -> bool | Whether a visible On Candle Stats indicator is on this chart |
| candle_stats.count() -> int | How many named values this bar carries |
| candle_stats.find(name: string) -> int | Index of the value with exactly that name, or -1 |
| candle_stats.name(index: int) -> string | Its name |
| candle_stats.value(index: int) -> float | Its number |
| dealers_flow.available() -> bool | Whether a visible Dealers Flow indicator is on this chart |
| dealers_flow.count() -> int | How many named values this bar carries |
| dealers_flow.find(name: string) -> int | Index of the value with exactly that name, or -1 |
| dealers_flow.name(index: int) -> string | Its name |
| dealers_flow.value(index: int) -> float | Its number |
| dealers_flow.spot(index: int) -> float | The price the line is drawn at |
| cot.available() -> bool | Whether a visible COT indicator is on this chart |
| cot.count() -> int | How many named values this bar carries |
| cot.find(name: string) -> int | Index of the value with exactly that name, or -1 |
| cot.name(index: int) -> string | Its name |
| cot.value(index: int) -> float | Its number |
| cot.change(index: int) -> float | Its change from the previous week |
| cot.released() -> int | How many reports became active on this bar |
| net_gex.available() -> bool | Whether a visible Net GEX indicator is on this chart |
| net_gex.count() -> int | 1 when this bar has a value, else 0 |
| net_gex.value(index: int) -> float | The Net GEX value, index 0 |

- orderflow_stats: one value per number the Orderflow Stats card prints, named row then column. The rows are GR, Vanna, Charm, DEX Aggregate, DEX Aggregate Call, DEX Aggregate Put, DEX Net, DEX Net Call, DEX Net Put and CVR with the columns 0DTE and 1DTE, and DEX flow, GEX flow and CVR flow with the columns Today and 1DTE, for example "DEX Net 0DTE" or "GEX flow Today". A hidden section or a number printed as a dash has no value. The card keeps no history: a snapshot sits from the bar its time falls in through the newest bar, earlier bars read none, and in replay it is empty.
- chart_stats reads Chart Stats and candle_stats reads On Candle Stats: one value per row the indicator shows, on each bar, named Volume, Delta, Delta %, Cumulative Delta, Bid Volume, Ask Volume, Max Delta, Min Delta, Trades, Delta Trades, Volume per Second or Candle Time. Only the rows switched on in the indicator exist. The values are the same at every zoom.
- dealers_flow: one value per line the chart draws on the bar, named Convexity, GEX or DEX, with spot the price the line is drawn at. Each bar carries only its own data. Not in replay, and it needs your GEXBot key.
- cot: one value per visible COT indicator, named by that indicator's label, such as COT Commercial or COT Index (Retail), with change its change from the week before. A report is readable only from its release, never from its report date, and released() is 1 on the bar a new report becomes active.
- net_gex: the Net GEX line as a step: each bar reads the newest point from before the next bar starts, the forming bar the newest so far. A bar before the first point reads none. It needs your GEXBot key.

```xfs
let delta_row: int = chart_stats.find("Delta %")
let strong_bar: bool = delta_row >= 0 && math.abs(chart_stats.value(delta_row)) >= 30
plot.candle_color(strong_bar, "#F59E0B", 0.6)
let net: int = orderflow_stats.find("DEX Net 0DTE")
plot.label(str.concat("DEX NET 0DTE ", format.compact(orderflow_stats.value(net)), "  NET GEX ", format.compact(net_gex.value(0))), "#E5E7EB")
```

### Candles: Overlay Chart and HFT candles

| Signature | Returns |
| --- | --- |
| overlay_chart.available() -> bool | Whether a visible Overlay Chart indicator is on this chart |
| overlay_chart.count() -> int | How many candles this bar can read |
| overlay_chart.open(index: int) -> float | Open of that candle, index 0 the newest |
| overlay_chart.high(index: int) -> float | Its high |
| overlay_chart.low(index: int) -> float | Its low |
| overlay_chart.close(index: int) -> float | Its close |
| overlay_chart.side(index: int) -> side | BUY for a candle that closed at or above its open, else SELL |
| hft_candles.available() -> bool | Whether a visible HFT candles indicator is on this chart |
| hft_candles.count() -> int | How many candles this bar can read |
| hft_candles.open(index: int) -> float | Open of that candle, index 0 the newest |
| hft_candles.high(index: int) -> float | Its high |
| hft_candles.low(index: int) -> float | Its low |
| hft_candles.close(index: int) -> float | Its close |
| hft_candles.side(index: int) -> side | BUY for a candle that closed at or above its open, else SELL |

- Both list their candles newest first.
- overlay_chart: the second instrument's candles, up to five. Index 0 is the most recent overlay candle whose span has ended at or before this bar, 1 the one before it; a candle appears only once its span ended. On the newest bar the candle in progress is read as the chart draws it. The first visible Overlay Chart is read.
- hft_candles: index 0 is the higher timeframe candle this bar belongs to, built from its bars up to and including this one; then the finished candles before it, as many as the indicator shows. A finished candle's final values are never on a bar inside it but its last.

```xfs
let htf_up: bool = hft_candles.available() && hft_candles.side(0) == "BUY"
let prev_high: float = hft_candles.high(1)
plot.hline(prev_high, "#6B7280", 1, "dashed", "Prior HTF high")
plot.candle_color(htf_up, "#22C55E", 0.25)
plot.marker(overlay_chart.count() > 1 && overlay_chart.close(0) > overlay_chart.high(1), high, "#3B82F6", "diamond", "above", 8, 4)
```

### Orderflow marks: Big Trades MBO, Iceberg MBO, Speed of Tape

| Signature | Returns |
| --- | --- |
| bigtrades_mbo.available() -> bool | Whether a visible Big Trades MBO indicator is on this chart |
| bigtrades_mbo.count() -> int | How many squares sit on this bar |
| bigtrades_mbo.price(index: int) -> float | The price it is drawn at |
| bigtrades_mbo.size(index: int) -> float | Its size in contracts |
| bigtrades_mbo.side(index: int) -> side | BUY or SELL |
| bigtrades_mbo.time(index: int) -> float | Its time, epoch milliseconds |
| iceberg_mbo.available() -> bool | Whether a visible Iceberg MBO indicator is on this chart |
| iceberg_mbo.count() -> int | How many points sit on this bar |
| iceberg_mbo.price(index: int) -> float | The price it is drawn at |
| iceberg_mbo.size(index: int) -> float | Its size in contracts |
| iceberg_mbo.side(index: int) -> side | BUY or SELL |
| iceberg_mbo.time(index: int) -> float | Its time, epoch milliseconds |
| speed_of_tape.available() -> bool | Whether a visible Speed of Tape indicator is on this chart |
| speed_of_tape.count() -> int | 1 when the panel draws a candle on this bar, else 0 |
| speed_of_tape.value(index: int) -> float | The candle's body, index 0 |
| speed_of_tape.peak(index: int) -> float | Its wick |
| speed_of_tape.side(index: int) -> side | The side the candle is coloured as |
| speed_of_tape.buy_share(index: int) -> float | The buyers' share the colour shows, from 0 to 1 |

- bigtrades_mbo: one record per square Big Trades MBO draws, over all loaded bars, on the bar of the order's last print, where its size is final: price where the square is drawn, size the contracts the order took, side the aggressor side, time the instant the square is drawn at. In replay an order not finished at the playhead is not there yet.
- iceberg_mbo: one record per point Iceberg MBO marks, a bar and a price: price, size the contracts filled there, side the order's side, time its first fill there. A point appears only from the bar by which its order had become an iceberg, even though the chart then draws its arrows back on earlier bars.
- speed_of_tape: one record on each bar where the Speed of Tape panel draws a candle; a bar its filter hides has none. Each bar reads only its own tape; the forming bar develops.
- For the raw data behind them, read mbo.* and tape.* directly.

```xfs
let big: bool = bigtrades_mbo.count() > 0 && bigtrades_mbo.size(0) >= 100
plot.dot(big && bigtrades_mbo.side(0) == "BUY", bigtrades_mbo.price(0), "#22C55E", 8)
plot.dot(big && bigtrades_mbo.side(0) == "SELL", bigtrades_mbo.price(0), "#EF4444", 8)
plot.marker(iceberg_mbo.count() > 0, iceberg_mbo.price(0), "#F59E0B", "square", "below", 7, 2)
plot.histogram(speed_of_tape.value(0), "#22C55E", "#EF4444")
```

## Extending a built in indicator

A script can start from an open built in indicator and draw on top of it. Name the base as the second argument of indicator. The builder then draws that indicator, with its own default look, as part of your script, and hands your script its values on every bar. The indicator already on your chart is never touched: a fork is a second, independent indicator.

```xfs
indicator("CVD with signal", "cvd")
```

| Signature | Returns |
| --- | --- |
| base.available() -> bool | Whether the base is attached to this run |
| base.value() -> float | The base's main value on this bar, its first series |
| base.series(name: string) -> float | One named series of the base on this bar. The name must be a literal from the table below |

| Base | Series | Settings (input key, default, bounds) | Draws in |
| --- | --- | --- | --- |
| "cvd" | value, open, high, low | candles true, resetDaily true | sub panel |
| "vwap" | value, upper1, lower1, upper2, lower2, upper3, lower3 | bands 3, 0 to 3 | price chart |
| "ema" | value | period 20, 1 to 500 | price chart |
| "rsi" | value | period 14, 1 to 500; overbought 70, 50 to 100; oversold 30, 0 to 50 | sub panel |
| "atr" | value | period 14, 1 to 500 | sub panel |
| "macd" | value, signal, histogram | fastPeriod 12, slowPeriod 26, signalPeriod 9, each 1 to 500 | sub panel |

- To expose a base setting, declare an input whose key is the setting name, for example let period: int = input.int(20, "period", "Period", 1, 500, 1). The base then follows that control. Undeclared settings keep their defaults.
- base.* without a base in indicator(), a base that is not in the table, or a series name the base does not publish is rejected when the script compiles, with one line listing what is accepted.
- Protected XFlow Charts indicators cannot be a base.
- Your drawing sits on top of the base's, in the same pane, so a panel base puts your panel lines in its panel.

A fork of VWAP that marks closes outside the second band:

```xfs
//@xfscript 1
indicator("VWAP Stretch", "vwap")

let bands: int = input.int(2, "bands", "Bands", 0, 3, 1)
let upper: float = base.series("upper2")
let lower: float = base.series("lower2")
let stretched_up: bool = base.available() && close > upper
let stretched_down: bool = base.available() && close < lower
plot.marker(stretched_up, high, "#EF4444", "triangle", "above", 9, 3)
plot.marker(stretched_down, low, "#22C55E", "triangle", "below", 9, 3)
plot.label(str.concat("VWAP ", format.fixed(base.value(), 2)), "#E5E7EB")
```

## Drawing on the chart

A script never draws directly. Each plot call records what you want, and the chart renderer paints it in the correct place with the correct projection, so drawings stay on their candle as you pan and zoom. Colours are always an explicit "#RRGGBB" or "#RRGGBBAA" string.

| Call | Where it draws |
| --- | --- |
| plot.line | A line over price, on the main chart |
| plot.band | A filled cloud between two price series, on the main chart |
| plot.zone | A filled rectangle over price, behind the candles |
| plot.box | An outlined rectangle over price, with an optional label |
| plot.hline | A horizontal level across the whole chart, with an optional label |
| plot.vline | A vertical line through one bar |
| plot.segment | A straight line between two points on the price chart |
| plot.marker | A small shape pinned to a price on one bar |
| plot.dot | A plain dot pinned to a price on one bar |
| plot.text | A text block pinned to a price on one bar |
| plot.candle_color | The candle itself, recoloured |
| plot.label | A line in the readout at the top left of the chart |
| profile.fold | A volume profile folded from the footprint ladder, in the chart look or your own |
| profile.row_color | One row of the newest folded profile, recoloured |
| plot.rect | A filled rectangle, optionally with a border and a gradient |
| plot.polygon | A filled shape of 3 to 10 points |
| plot.cell | Price rows of the current candle, filled like a footprint cell, with optional text |
| plot.hbar | A histogram row growing from the price axis edge |
| plot.table | A cell of a table in a chart corner |
| plot.position | A trade drawn from entry to stop and target |
| plot.histogram | A histogram in the sub panel below the chart |
| plot.panel_line | A line in the sub panel |
| plot.panel_area | A filled area in the sub panel |
| plot.panel_hline | A reference level across the sub panel |
| plot.panel_segment | A straight line between two points in the sub panel |
| plot.panel_range | A fixed vertical scale for the sub panel |

Any of the panel calls opens the sub panel below the chart. Everything else draws on the main chart. A script may do both. Price chart calls expect prices; put an oscillator, a ratio, a count or a delta in the sub panel, not on the price axis.

### plot.line

```xfs
plot.line(value: float, color: color [, width: float, style: string]) -> void
```

- Defaults: width 2, style "solid". Styles are "solid", "dashed" and "dotted". Width is held between 1 and 5.
- A nan value leaves a gap at that bar.
- The colour may change from bar to bar: pass a color var and the line changes colour on the bar where it changes, as one continuous stroke. Width and style are taken from the first bar on which that call site runs.

```xfs
let average: float = ta.ema(close, 20)
var trend_color: color = "#22C55E"
trend_color := "#22C55E"
if close < average {
  trend_color := "#EF4444"
}
plot.line(average, trend_color, 2, "solid")
```

### plot.band

```xfs
plot.band(upper: float, lower: float, up_color: color, down_color: color) -> void
```

Exactly four arguments. Each call site is its own cloud, filled at a soft opacity so price stays readable. A nan bound leaves a gap.

```xfs
let basis: float = ta.sma(close, 20)
let sigma: float = ta.stdev(close, 20)
plot.band(basis + sigma * 2, basis - sigma * 2, "#22C55E", "#EF4444")
```

### plot.zone

```xfs
plot.zone(high: float, low: float, color: color [, opacity: float]) -> void

plot.zone(when: bool, high: float, low: float, color: color, opacity: float, start_offset: int, bars_right: int [, fill_mode: string]) -> void
```

- Only 3, 4, 7 or 8 arguments are accepted. Opacity defaults to 0.14 and is held between 0 and 1.
- The short form is a running rectangle. Consecutive bars with the same finite high and low collapse into one box, and a change or a gap closes it.
- The long form creates a separate box every time when is true, so boxes from one call site can overlap. start_offset is a whole number from -200000 through 0 and moves the left edge back, which is how a pattern detected on the third bar starts on the first. bars_right is a whole number from 0 through 200000, measured from that left edge.
- fill_mode defaults to "none". "bullish" ends the box on the first later bar that trades through the low, "bearish" on the first later bar that trades through the high, and a wick counts. "bullish_close" and "bearish_close" end it only when a later bar closes beyond the boundary.
- A box that has not been filled may extend past the last candle, so a projection to the right is drawn as asked.
- A zone whose high equals its low is drawn as a one pixel line across its bars, which is how a single price level is marked for a stretch of bars.

```xfs
let gap_up: bool = bar_index >= 2 && low > high[2]
plot.zone(gap_up, low, high[2], "#22C55E", 0.14, -1, 30, "bullish")
```

### plot.box

```xfs
plot.box(when: bool, high: float, low: float, color: color [, opacity: float, start_offset: int, bars_right: int, border_width: float, border_style: string, label: string]) -> void
```

- An outlined rectangle. Where plot.zone is a filled area, this is a border, so it marks a level without hiding the candles inside it.
- Defaults: opacity 1, start_offset 0, bars_right 0, border width 1 (held between 1 and 5), style "solid", no label.
- start_offset is a whole number from -200000 through 0 and moves the left edge back. bars_right is a whole number from 0 through 200000, measured from that left edge. Both edges are inclusive, the same convention plot.zone uses, so a box and a zone drawn for the same rectangle land on the same pixels.
- A zero height box is the way to draw one horizontal segment across a bar range rather than the whole chart.

```xfs
let inside: bool = bar_index >= 1 && high < high[1] && low > low[1]
plot.box(inside, high[1], low[1], "#6B7280", 1, -1, 12, 1, "dashed", "Inside")
```

### plot.hline

```xfs
plot.hline(price: float, color: color [, width: float, style: string, label: string]) -> void
```

Defaults: width 1, style "solid", no label. One level per call site, and the last finite price that call site produced is the level that is drawn. That is how a level held in a var becomes a single line rather than one line per bar. The optional label travels with the level.

```xfs
var session_high: float = nan
session_high := math.max(nz(session_high[1], high), high)
plot.hline(session_high, "#22C55E", 1, "dashed", "Session high")
```

### plot.vline

```xfs
plot.vline(when: bool, color: color [, width: float, style: string]) -> void
```

A vertical line through the current bar, top to bottom of the chart, on every bar where when is true. Defaults: width 1, held between 1 and 5, style "solid".

```xfs
let opened: bool = session.in("0930-1600", "America/New_York")
let first_cash_bar: bool = opened && !(bar_index > 0 && opened[1])
plot.vline(first_cash_bar, "#6B7280", 1, "dotted")
```

### plot.segment

```xfs
plot.segment(when: bool, bar_a: int, value_a: float, bar_b: int, value_b: float, color: color [, width: float, style: string]) -> void
```

- A straight line from (bar_a, value_a) to (bar_b, value_b) on the price chart, drawn on every bar where when is true. This is how a trendline, a swing to swing connector or a divergence line is drawn.
- bar_a and bar_b are bar_index values, not offsets. Keep the bar_index of a pivot in a var when you find it and pass it here later, and the line is drawn back to where it belongs.
- They must be whole numbers from 0 through the current bar_index plus 200000. An end past the newest bar is cut at the newest bar with its slope kept, so a projection shows its direction.
- A nan in any of the four coordinates draws nothing on that bar. Defaults: width 2, held between 1 and 5, style "solid".
- The colour may differ from one segment to the next. Segments of one call site and one colour that overlap in time are kept up to eight deep, the newest winning.

```xfs
var pivot_bar: float = nan
var pivot_price: float = nan
pivot_bar := nz(pivot_bar[1], nan)
pivot_price := nz(pivot_price[1], nan)
let new_high: bool = bar_index >= 20 && high >= ta.highest(high, 20)
plot.segment(new_high && !is_nan(pivot_bar), pivot_bar, pivot_price, bar_index, high, "#F59E0B", 2, "solid")
if new_high {
  pivot_bar := bar_index
  pivot_price := high
}
```

### plot.marker

```xfs
plot.marker(when: bool, price: float, color: color [, shape: string, anchor: string, size: float, offset_pixels: float]) -> void
```

- Defaults: shape "diamond", anchor "above", size 10, offset 2.
- Shapes are "x", "circle", "triangle", "square" and "diamond". Anchors are "above" and "below".
- Size is held between 3 and 24 pixels, the offset between 0 and 20 pixels.
- Nothing is drawn when when is false or the price is nan.
- A new marker is one of the two things an alert fires on.

```xfs
let signal: bool = ta.crossover(close, ta.sma(close, 20))
plot.marker(signal, high, "#22C55E", "triangle", "above", 9, 3)
```

### plot.dot

```xfs
plot.dot(when: bool, price: float, color: color [, size: float]) -> void
```

A plain filled dot at a price on one bar. Size defaults to 4 and is held between 1 and 20. Use it where a marker shape would be too loud, and pair it with math.scale when the size should follow the size of a trade.

```xfs
let biggest: float = tape.largest_size()
let radius: float = math.scale(nz(biggest, 0), 25, 400, 4, 18)
plot.dot(nz(biggest, 0) >= 75, nz(tape.largest_price(), close), "#F59E0B", radius)
```

### plot.text

```xfs
plot.text(when: bool, price: float, text: string, color: color [, anchor: string, font_size: float, offset_pixels: float, min_bar_slot_pixels: float]) -> void
```

- Defaults: anchor "above", font size 9, offset 4, minimum bar slot 5.
- Plain centred text with no shape, plate or background. Use \n for up to 8 stacked lines, up to 1,024 characters.
- Font size is held between 6 and 24, the offset between 0 and 40, the slot threshold between 0 and 200.
- The block hides itself when one candle is narrower on screen than min_bar_slot_pixels, so zooming out never produces a wall of text. Raise it for wide blocks.
- An empty string draws nothing. The text must be a string; format numbers first.

```xfs
let body: string = str.concat("VOL ", format.compact(tape.volume()))
plot.text(tape.available(), high, body, "#22C55E", "above", 9, 4, 60)
```

### plot.candle_color

```xfs
plot.candle_color(when: bool, color: color [, opacity: float]) -> void
```

Recolours the candle itself on the bars where when is true. One colour per bar: if two call sites claim the same candle the last one executed wins. Opacity is held between 0 and 1 and defaults to a solid fill.

```xfs
let squeeze: bool = high - low < ta.sma(high - low, 20) * 0.5
plot.candle_color(squeeze, "#F59E0B", 0.7)
```

### plot.position

```xfs
plot.position(when: bool, side: string, entry: float, stop: float, target: float [, bars: int]) -> void
```

- Draws a trade the way the chart draws its position tool: a red box from entry to stop, a green box from entry to target, both starting at the close of the signal bar.
- side is "BUY" or "SELL". A buy needs stop below entry below target, a sell the reverse; anything else draws nothing.
- The boxes end on the first later bar whose wick reaches the stop or the target, or after bars bars, from 1 to 500, default 100. A bar that touches both ends the drawing without claiming which came first.
- It is a drawing, not a backtest.

```xfs
let long_signal: bool = ta.crossover(close, ta.ema(close, 20))
plot.position(long_signal, "BUY", close, low, close + 2 * (close - low), 100)
```

### plot.label

```xfs
plot.label(text: string, color: color) -> void
```

Writes one line into the readout at the top left of the chart. This is the information panel. One line per call site, carrying its newest value, so a call site that runs on every bar leaves one line stating what is true now rather than ten thousand lines. Several call sites give several lines, in the order they first ran. An empty string draws nothing.

```xfs
plot.label(str.concat("VOL ", format.compact(tape.volume())), "#E5E7EB")
plot.label(str.concat("DELTA ", format.compact(tape.delta())), "#22C55E")
```

### profile.fold

```xfs
profile.fold(when: bool, bars_back: int [, value_area_percent: float, color: color, opacity: float, position: string, width_percent: float, coloring: string, poc_color: color, va_color: color, outside_va_color: color]) -> void
profile.row_color(price: float, color: color) -> void
```

- Adds the footprint ladders of the last bars_back bars, ending on this bar, into one volume profile and draws it the way every other profile on the chart is drawn. This is a custom volume profile over any range you can describe.
- bars_back is a whole number from 1 through 200000. value_area_percent defaults to 70, must be finite, and is held between 0 and 100.
- With no color and no opacity the profile wears your own Session Profile appearance, key for key. The script says what to fold; the chart says what a profile looks like.
- color paints the volume histogram and opacity, 0 through 1, is the profile master opacity. The bull and bear pair is not touched, so a delta column keeps the palette the rest of the chart uses. Fold twice with two colors to draw two profiles two ways.
- The point of control and the value area agree with the chart's own Session Profile over the same bars.
- A range where no bar carried a ladder produces no profile rather than an empty one.
- At most 512 profiles and 200,000 profile rows per run. Fold on a condition, at the end of a session or every N bars, rather than on every bar.
- The rest of the look is optional too, and "auto" for a colour, position or coloring, or nan for opacity or width_percent, keeps the chart look for that one argument. position is "left", "right" or "center" within the folded bars. width_percent, above 0 and at most 100, is how much of that span the profile takes. coloring "single" paints one volume column in color and "delta" paints each row by the sign of its own delta. poc_color, va_color and outside_va_color paint the point of control, the value area rows and the rows outside it.
- profile.row_color recolours the row holding price in the newest profile folded so far, and wins over the chart marks on that row. At most 5,000 recoloured rows per run.

```xfs
let session_bars: int = input.int(60, "sessionBars", "Bars in the profile", 2, 2000, 1)
let anchor: bool = bar_index % session_bars == session_bars - 1
profile.fold(anchor, session_bars, 70)
profile.fold(last_bar, session_bars, 70, "auto", nan, "right", 30, "delta", "#FFFFFF", "#3B82F6", "auto")
if last_bar {
  profile.row_color(close, "#F59E0B")
}
```

### Custom graphics

plot.rect, plot.polygon and plot.cell draw filled shapes on the price chart, painted by the chart itself so they stay on their bars as you pan and zoom. plot.hbar and plot.table draw on the chart frame instead: the price axis edge and a corner.

Horizontal positions in plot.rect and plot.polygon are bar offsets from the centre of the current candle, fractions allowed: 0 is the centre, -0.5 and 0.5 are the edges of its slot, -10 is ten bars back and 20 is twenty bars into the future. They run from -200,000 to 500. Vertical positions are prices. A nan anywhere draws nothing on that bar.

```xfs
plot.rect(x1: float, x2: float, top: float, bottom: float, fill: color [, border_color: color, border_width: float, gradient_color: color, gradient_direction: string]) -> void
plot.polygon(fill: color, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float [, ... x10: float, y10: float]) -> void
plot.cell(low: float, high: float, fill: color [, text: string, text_color: color]) -> void
plot.hbar(low: float, high: float, value: float, max_value: float, fill: color [, side: string, width_pixels: float]) -> void
plot.table(row: int, column: int, text: string [, text_color: color, background: color, position: string]) -> void
```

- plot.rect: a rectangle with no width or no height draws nothing. border_width is held between 0 and 5 pixels. gradient_direction is "vertical", fill at the top fading to gradient_color at the bottom, the default, or "horizontal", left to right. Use the opacity pair of a "#RRGGBBAA" colour for glows and fades.
- plot.polygon: 3 to 10 points, listed in order around the shape, so 7, 9, 11 and so on up to 21 arguments. Concave shapes are fine; edges that cross stop the script with one line.
- plot.cell: fills the body column of the current candle between two price rows, both included, so plot.cell(p, p, ...) is exactly the footprint row that holds p. The text is drawn centred when the cell is tall enough to hold it.
- plot.hbar: one row of a histogram, the rows of price from low to high, value divided by max_value of width_pixels long, growing from the "right" edge, the default, or the "left" one. width_pixels is held between 10 and 600 and defaults to 120. A value of 0 or less draws nothing.
- plot.table: one cell of a grid, row 0 to 19 and column 0 to 7, in the "top_right" corner, the default, or "top_left", "bottom_right" or "bottom_left". background paints the cell.
- plot.hbar and plot.table keep only what their call site drew on the LAST bar that ran it: each new bar that runs the call replaces the earlier set. So an unconditional plot.hbar in a loop over the ladder draws the newest bar's ladder, and a plot.table call states the newest values. Inside an if, a call that stops running keeps its last set on screen; pick colours and text with ? : instead of putting these calls in branches.
- Budgets per run: 240,000 shape points, a rectangle costing 6 and a bordered one 30, the newest kept; 5,000 histogram rows; 640 table cells; 5,000 cell texts, the newest kept. Reaching one says so in the settings under the editor; nothing is cut silently.

The newest candle's ladder on the price axis edge, coloured by delta:

```xfs
let top_volume: float = cell.max_volume()
let top_delta: float = math.max(nz(cell.max_delta(), 0), 1)
for i = 0 to cell.count() - 1 by 1 {
  let p: float = cell.price(i)
  let shade: color = color.from_gradient(cell.delta(i), -top_delta, top_delta, "#EF4444", "#22C55E")
  plot.hbar(p, p, cell.volume(i), top_volume, shade, "right", 160)
}
plot.table(0, 0, "Delta", "#9CA3AF")
plot.table(0, 1, format.compact(tape.delta()), tape.delta() >= 0 ? "#22C55E" : "#EF4444")
```

A rectangle that fades to the right, and an arrow under the bar:

```xfs
let unit: float = nz(ta.atr(14), high - low)
let prior_high: float = ta.highest(high, 20)
let breakout: bool = bar_index > 20 && close > prior_high[1]
if breakout {
  plot.rect(-0.5, 15, high, low, "#3B82F655", "#3B82F6", 1, "#3B82F600", "horizontal")
  plot.polygon("#22C55E", 0, low - unit * 0.2, 0.4, low - unit * 0.8, -0.4, low - unit * 0.8)
}
```

### plot.histogram

```xfs
plot.histogram(value: float, up_color: color, down_color: color) -> void
```

Exactly three arguments. There is one histogram per script, so write one call site. A nan value leaves that bar empty. Bars at or above zero take the up colour, bars below take the down colour.

```xfs
plot.histogram(ta.ema(tape.delta(), 10), "#22C55E", "#EF4444")
```

### The rest of the sub panel

```xfs
plot.panel_line(value: float, color: color [, width: float, style: string]) -> void
plot.panel_area(value: float, color: color [, opacity: float]) -> void
plot.panel_hline(value: float, color: color [, style: string, label: string]) -> void
plot.panel_segment(when: bool, bar_a: int, value_a: float, bar_b: int, value_b: float, color: color [, width: float, style: string]) -> void
plot.panel_range(min: float, max: float) -> void
```

- plot.panel_line is plot.line inside the panel: one series per call site, width held between 1 and 5, a nan value leaving a gap.
- plot.panel_area fills from the panel baseline to the value. Opacity defaults to 0.2.
- plot.panel_hline is a reference level across the panel, one per call site, carrying its newest finite value, with an optional label.
- plot.panel_segment is plot.segment inside the panel, with the same rules.
- plot.panel_range fixes the vertical scale instead of letting the panel fit its data. The last pair executed wins, so state it once with literals when the reading has a fixed scale such as 0 to 100.

An oscillator with a signal line and a fixed scale:

```xfs
let raw: float = tape.delta() / tape.volume() * 100
let smooth: float = ta.ema(nz(raw, 0), 14)
let signal: float = ta.ema(smooth, 9)
plot.panel_range(-100, 100)
plot.panel_area(smooth, "#6B7280", 0.18)
plot.panel_line(smooth, "#22C55E", 2, "solid")
plot.panel_line(signal, "#EF4444", 1, "dashed")
plot.panel_hline(0, "#6B7280", "dotted", "Flat")
```

### Conditional drawing

A plot call writes only on the bars where it actually runs, so an if branch is how you create gaps and discrete markers. For a single line that changes colour, use one call site with a colour picked by ? :, or a color var as shown under plot.line; two call sites in two branches give two separate lines with gaps between them. plot.hbar and plot.table are the exception to reach for branches: they keep their last set when the branch stops running.

Draw only while a condition holds:

```xfs
let average: float = ta.sma(close, 20)
if close >= average {
  plot.line(average, "#22C55E", 2, "solid")
}
```

## Live and replay behave the same way

- In replay the script receives exactly the bars revealed up to the playhead, and the newest bar it receives is the one at the playhead. Nothing after the playhead is visible: not a bar, not a print, not a book snapshot, not an order change.
- On a live chart and in replay the script is recomputed over the loaded window as new data lands, at most about once a second, so the newest bar updates and completed bars stay stable.
- Because a run always starts from bar 0 of the window, a var accumulator does not double count when a run repeats.
- The newest bar in the window is the one still developing, and its values change until it closes. last_bar and is_forming are both true on it, live and in replay.
- Depth and individual orders have recorded twins, and everything a script reads from an indicator by name is what that indicator shows at the playhead, so a script reading them behaves the same in replay as live. Quotes and gamma have no recorded twin: in replay l1.supported() and gex.supported() are false, the Orderflow Stats family is empty, and Dealers Flow is not on the chart, so the script should draw what it can without them.
- On a live chart, quotes, depth and gamma are recorded bar by bar as they arrive, so their history starts when the script started running on the chart and starts again when the chart is reopened. In replay, depth and orders come from the recording on every bar up to the playhead.

## Alerts and popups

An indicator written in XFlow Script can raise an alert, and you do not write a single line of alert code for it. Open the indicator settings, go to the Alert tab, and turn it on. Choose a sound, and leave the popup on to get a dismissable popup in the middle of the screen; a short notification is shown either way. The popup is titled with the name your script gives itself in indicator().

- It fires on a NEW marker from plot.marker, or a NEW zone from plot.zone, or both. Those are the two channels the alert watches, so the way to make a condition alertable is to give it its own plot.marker or plot.zone call site.
- A marker alerts when its bar is new. A zone alerts once the bar that created it has closed, so it states the zone it ends up with rather than one still moving.
- Nothing that was already on the chart when you turned the alert on fires, and history loaded later does not fire either. Each marker or zone fires once.
- A zone whose high equals its low is drawn as a line and does not raise an alert. Give it some height, or add a marker.
- Lines, dots, text, boxes, labels and profiles never raise alerts.

A condition worth alerting on gets its own marker:

```xfs
let squeeze: float = ta.stdev(close, 20) / ta.sma(close, 20) * 100
let broke_out: bool = ta.crossover(squeeze, ta.sma(squeeze, 50) * 1.5)
plot.marker(broke_out, high, "#F59E0B", "triangle", "above", 12, 4)
```

## Sharing a script and protecting its source

A script runs in the browser of whoever has it on a chart, on that person's own market data. Protecting the source means other users run your indicator and change its settings without seeing your code.

- Save the script to your Builder library, then switch on Protect source on its row. Publish it to Shared as usual; people who install it, or open a workspace or template of yours that holds it, get the protected indicator.
- You keep the source. You edit and save it as before, and everyone who has it runs your latest saved version.
- What stays visible to them is what the chart shows anyway: the name from indicator(), the settings your input.* calls declare with their titles and defaults, and everything the script draws.
- Its settings show Source is protected by the author where the code would be.
- Turning Protect source off gives readers the source again.
- XFlow Lab files shared to the gallery cannot be protected; save the script as a Builder indicator to protect it.

## Limits

These ceilings are part of the language. Crossing one stops the run with a clear message.

| Resource | Ceiling |
| --- | --- |
| Source size | 65,536 bytes |
| Tokens | 20,000 |
| Syntax nodes | 10,000 |
| Bars per run | 200,000 |
| Stored series values, bars times top level declarations | 8,000,000 |
| History offset, start_offset, bars_right, bars_back | 200,000 |
| Statements executed on one bar, outside loop bodies | 10,000 |
| Loop passes on one bar | 100,000 |
| Loop steps in one run | 25,000,000 |
| Markers, texts, boxes, dots, segments, event zones and vertical lines drawn by a script with a loop | 250,000 per run |
| Shape points from plot.rect, plot.polygon and plot.cell | 240,000 per run, the newest kept |
| plot.hbar rows | 5,000 |
| plot.table cells | 640 |
| plot.cell texts | 5,000 per run, the newest kept |
| profile.row_color rows | 5,000 per run |
| Strings built on one bar | 200,000 |
| Characters of text built on one bar, and in one run | 16,000,000 and 512,000,000 |
| Different mtf timeframes in one script | 8 |
| Different session.open and session.adr windows in one run | 64 |
| Function call depth | 32 |
| Parameters per function | 16 |
| Arguments per call | 24 |
| Distinct plot call sites | 64 |
| Folded profiles per run | 512 |
| Folded profile rows per run | 200,000 |
| String length | 1,024 characters |
| Lines in one text block | 8 |
| Market items supplied to one run: prints, ladder rows, book levels, orders and strikes together | 1,000,000 |
| Items walked by whole bar answers on one bar | 1,000,000 |
| Time for one run | 8 seconds |

A script without a loop has no separate cap on markers, text blocks or zones: it draws at most one per call site per bar, the chart keeps the whole result and draws only what the current view intersects.

## Messages and what fixes them

The builder shows one line and marks the row in the editor gutter. These are the messages you will meet, with the change that clears each one. The script is compiled and then run over one test bar as you type, so most mistakes are reported before the script ever reaches your chart. Where a message below starts with name, it names the call or the variable at fault.

### Text and structure

| Message | Fix |
| --- | --- |
| Missing //@xfscript 1 header | Make //@xfscript 1 the first line of the script. |
| Empty script | The editor is empty. Paste the script. |
| Expected expression, found | A statement was split across lines, or an operand is missing. Put the whole call on one line. |
| Expected { | Move the opening brace onto the same line as the if condition or the function signature. |
| Expected ) after arguments | A closing parenthesis is missing, or a call was split across lines. |
| Expected variable name | A let or var needs a name, and a reserved word such as for, while or if cannot be one. |
| Expected = after variable name | A let or var needs = and an initial value. |
| Unterminated block | A closing brace is missing. |
| Unterminated string literal | A closing double quote is missing. |
| String literals cannot span lines | Use \n inside one quoted string instead of a real line break. |
| Unsupported escape | Only \n, \t, \" and \\ are supported. |
| Unexpected character | A character outside the language, such as a single quote, a backtick or a hash outside a string. |
| Unknown type name | Types are float, int, bool, string, color, side and void. |
| Expected to in for loop (for i = start to end) | Write for, the loop variable, =, the start, to, the end, and optionally by and a step. |
| break is only valid inside a loop | break and continue must sit inside the loop itself, not in a function called from it. |
| Expected : in conditional expression (condition ? a : b) | A ? needs its : and the second value. |
| Functions must be declared at top level | A fn cannot be declared inside another function. Move it to the top level. |
| Nested functions are not allowed | A fn cannot be declared inside an if branch. Move it to the top level. |
| Persistent var declarations must be at top level | var cannot be declared inside a function. Keep state in a top level var. |
| Series declarations must be top level | Move the let or var out of the if branch and assign inside it instead. |
| History access is only valid on a series identifier | Store the expression in a let first, then index that name. |
| return is only valid inside a function | Delete the return, or move it into a function body. |
| Only a function call can be used as a statement | A bare expression is not a statement. Assign it or call something. |

### Names

| Message | Fix |
| --- | --- |
| Unknown function name | That name is not on this page. Check the spelling, or use a builtin that exists. |
| Unknown identifier name | The name was never declared. Declare it with let or var before use. |
| Unknown series name | History was read from a name that is not a series. |
| Duplicate or reserved name | Rename it. A global or builtin name cannot be reused. |
| Duplicate or reserved parameter name | A parameter repeats another, or reuses a series or global name. |
| Duplicate or reserved local name | A function local repeats a name already in use. |
| let series name is immutable | Declare it with var if you want to assign it with :=. |
| Assignment target name is not declared | Declare the var above the assignment. |
| User functions are pure and cannot assign state | Return a value from the function and assign it outside. |
| Recursive function cycle includes name | Remove the recursion. Recursion is rejected before the run. |
| Function locals are scalar and have no history | History indexing works on top level series only. |
| Loop locals are scalar and have no history | The same for a let inside a loop body. |
| Loop variable name is read-only | Copy it into a local, let j: float = i, and change the copy. |
| name cannot be a loop variable name | A loop variable cannot take a global, builtin or series name. Rename it. |
| Unknown identifier name; XF Script has ... | A Pine name. The message names the XFlow Script equivalent, such as time.day for dayofmonth. |
| Effort Zones output can only be used on the bar it describes | Use the Effort Zones value on the bar where you read it. |
| A script may declare indicator() at most once | Keep one indicator() call. |

### Arguments and types

| Message | Fix |
| --- | --- |
| name expects N arguments, received M | Check the signature on this page. Optional arguments are shown in brackets. Argument counts of the builtins are checked before the script runs, including inside a branch that never executes. |
| name requires int, received nan | Annotate as float, or wrap with nz and math.round. |
| name requires type, received | The value does not match the annotation. Change the annotation or the value. |
| name expects a number, received | An argument is of the wrong type. Strings and numbers are never coerced. |
| name expects bool, received | A condition, an && or \|\| operand, or a when argument is not a boolean. |
| name expects string, received | Text arguments must be strings. Format numbers with format.compact or format.fixed. |
| Expected #RRGGBB or #RRGGBBAA color, received | Colours are six or eight hex digits with a leading hash, and nothing else. |
| name style must be one of solid, dashed, dotted | One sentence covers every choice argument, and it lists what is accepted. |
| name shape must be one of x, circle, triangle, square, diamond | The same sentence for a marker shape. |
| name anchor must be one of above, below | The same for an anchor. |
| name fill mode must be one of none, bullish, bearish, bullish_close, bearish_close | The same for a zone fill mode. |
| plot.zone expects 3, 4, 7, or 8 arguments | Use the short form or the full event form, nothing between. |
| plot.zone start_offset must be an integer from -200000 through 0 | Move the left edge back by a whole number of bars. |
| plot.zone bars_right must be an integer from 0 through 200000 | Project right by a whole number of bars. |
| plot.box start_offset must be an integer from -200000 through 0 | The same rule for a box. |
| plot.box bars_right must be an integer from 0 through 200000 | The same rule for a box. |
| plot.segment bar_a and bar_b must be integer bar_index values | Pass bar_index values you stored, as whole numbers, never an offset or a time. |
| plot.text expects string text | Build the text with str.concat and the format builtins. |
| plot.label expects string text | The same for a readout line. |
| profile.fold bars_back must be an integer from 1 through 200000 | Fold a whole number of bars. |
| profile.fold value_area_percent must be finite | Pass a number such as 70. |
| profile.fold opacity must be between 0 and 1 | Opacity is a fraction, not a percent. |
| name index must be a non-negative integer or -1 | A row index is a whole number from 0 up, or -1 for none. A literal index below -1 is refused as you type. |
| tape.nth_largest_index rank must be a non-negative integer | Rank 0 is the largest print. |
| name depth must be a non-negative integer | A depth argument is a whole number. Omit it or pass 0 for every level. |
| format.fixed decimals must be an integer from 0 through 6 | Pass a whole number of decimals. |
| name keeps one value per bar and cannot be called inside a loop | Call it once per bar outside the loop and use its result inside. |
| name draws one value per bar and cannot be called inside a loop | Draw objects inside a loop, such as plot.marker, plot.box or plot.hbar, or move the call out. |
| name declares a setting and cannot be called inside a loop | Declare inputs at the top level. |
| for step must not be 0 | Use a positive or negative step. |
| plot.polygon edges must not cross; list the points in order around the shape | Order the points around the outline. |
| name bar offsets must be from -200000 through 500 | Keep plot.rect and plot.polygon positions inside the range. |
| plot.table row must be an integer from 0 through 19 | Rows are 0 to 19, columns 0 to 7. |
| profile.fold width_percent must be greater than 0 and at most 100 | Pass a percent, or nan for the chart look. |
| name timezone must be UTC, America/New_York, America/Chicago, Europe/Prague, Europe/London, Asia/Tokyo, Asia/Hong_Kong, Australia/Sydney, or a fixed offset in minutes | Name one of those zones, or pass whole minutes. |
| fn expects N arguments | A user function was called with the wrong number of arguments. |
| fn must return type | Every path through a non void function needs a return of that type. |

### Inputs, sessions and bases

| Message | Fix |
| --- | --- |
| Inputs must be direct top-level variable initializers | Write let name = input.something(...) at the top level, on its own. |
| Input declarations require literal arguments | Every input argument must be a literal, not an expression. |
| Duplicate input key | Give every input its own key. |
| input.int key must match [A-Za-z][A-Za-z0-9_]{0,63} | Start the key with a letter and use letters, digits and underscore only. |
| input.int title must contain 1-80 characters | Shorten or fill in the title. |
| input.int min must not exceed max | Swap the bounds. |
| input.int step must be positive | Use a step above zero. |
| input.int values must be integers | Whole numbers only for input.int, including the step. |
| input.color default must be #RRGGBB or #RRGGBBAA | Write the default colour as a hex literal. |
| Session window must use HHMM-HHMM | Write the window as four digits, a hyphen and four digits, for example "0930-1600". |
| Unsupported session timezone | Use "UTC", "America/New_York", "Europe/Prague" or a whole number of minutes. |
| Session fixed offset must be an integer number of minutes | Pass whole minutes, such as -300. |
| Completed-session ranges require a daytime session window | session.open and session.adr need a window that starts before it ends. |
| indicator(name, base): the base must be a string literal | Write the base as a literal such as "cvd". |
| Unknown base study | Use one of the open bases listed on this page. |
| That study is protected and cannot be a base | Protected indicators cannot be extended. The message lists the open bases. |
| base.* reads the declared base study | Add the base to indicator("name", "base") before calling base.*. |
| base.series needs a literal series name | Write the series name as a literal. |
| base has no series | Use a series name the base publishes; the message lists them. |

### Limits

| Message | Fix |
| --- | --- |
| Source exceeds 65536 bytes | Shorten the script. |
| Token count exceeds 20000 | Shorten the script. |
| AST node count exceeds 10000 | Shorten the script, or move repeated logic into a function. |
| Calls accept at most 24 arguments | Split a long str.concat into two. |
| Functions accept at most 16 parameters | Pass fewer values. |
| History offset must be an integer from 0 to 200000 | Use a whole number offset inside the range. |
| Plot count exceeds 64 | Reduce the number of distinct plot call sites. |
| Profile count exceeds 512 | Fold on a condition rather than on every bar. |
| Profile rows exceed 200000 | Fold fewer or shorter ranges. |
| plot.text accepts at most 8 lines | Reduce the number of \n breaks. |
| String exceeds 1024 characters | Shorten the text. |
| Statement count exceeds 10000 on one bar | The script does too much per bar. Simplify it. |
| Loop exceeded 100000 iterations on bar N | A loop runs too often on one bar, or a while condition never turns false. |
| Loops exceeded 25000000 steps in one run (stopped on bar N) | The loops do too much work over the whole window. Loop over fewer rows or fewer bars, for example only near last_bar. |
| Drawn objects exceed 250000 in one run | A loop draws too many objects. Filter inside the loop, or draw only on recent bars. |
| Strings built on one bar exceed 200000 | Build text once per bar, not once per pass of a loop. |
| mtf.* reads at most 8 different timeframes in one script | Use fewer different minutes values. |
| Call depth exceeds 32 | User functions call each other too deeply. |
| Series storage exceeds 8000000 cells | Declare fewer top level series. |
| Input data exceeds 1000000 market items | The chart holds more data than one run may read. Load a shorter range. |
| Aggregate work exceeds 1000000 items on one bar | One bar asked for too much whole bar work. Ask fewer distinct thresholds. |
| Script stopped after running longer than 8 seconds | One run took too long. The script does not run again until you edit it or change one of its settings. Do less work per bar. |

### Protected indicators from other users

| Message | Fix |
| --- | --- |
| This indicator stopped on bar N (limit or runtime error) | The author's script stopped on the data of your chart. A protected script shows no more detail; try a shorter range, or ask the author. |
| Reload the page to run this indicator | A newer version is available. Reload the page. |
| This indicator is no longer available | The author deleted it, or it was removed from the gallery. |

## Patterns worth knowing

### Carry a value forward

```xfs
var level: float = nan
level := nz(level[1], nan)
if ta.crossover(close, ta.sma(close, 50)) {
  level := close
}
plot.hline(level, "#22C55E", 1, "dashed", "Trigger")
```

### Reset something once per session

History indexing works on a series name, never on a call, so store the session test in a series first and read its previous value from there.

```xfs
let in_cash: bool = session.in("0930-1600", "America/New_York")
let was_in_cash: bool = bar_index > 0 && in_cash[1]
let first_bar: bool = in_cash && !was_in_cash

var session_high: float = nan
session_high := nz(session_high[1], high)
if first_bar {
  session_high := high
}
if in_cash {
  session_high := math.max(session_high, high)
}
plot.hline(session_high, "#22C55E", 1, "dashed", "Session high")
```

### Values that can be absent

Divide anyway, because division by zero returns nan rather than infinity, and then decide what an absent value should read as with nz, or with ? : when the two cases need different values. Use an if branch when they need different work.

```xfs
let total: float = tape.volume()
let delta_percent: float = tape.delta() / total * 100
let readable: string = format.fixed(nz(delta_percent, 0), 1)
let known: bool = !is_nan(delta_percent)
plot.text(known, high, readable, "#9CA3AF", "above", 9, 4, 60)
```

### Use a data family when it is there, and never assume it

```xfs
let has_depth: bool = l2.supported() && l2.available()
let book_lean: float = l2.imbalance(10)
let tape_lean: float = tape.delta() / tape.volume()
var lean: float = 0
lean := nz(tape_lean, 0)
if has_depth {
  lean := nz(book_lean, 0)
}
plot.histogram(lean, "#22C55E", "#EF4444")
```

### Draw once at the right edge

```xfs
let projection: float = close + ta.stdev(close, 20) * 2
plot.text(last_bar, projection, format.fixed(projection, 2), "#E5E7EB", "above", 10, 12, 40)
plot.marker(last_bar, projection, "#22C55E", "diamond", "above", 8, 3)
plot.label(str.concat("BARS ", format.compact(bar_count)), "#6B7280")
```

### Count the bars since something happened

```xfs
let crossed: bool = ta.crossunder(close, ta.ema(close, 21))
plot.histogram(nz(ta.barssince(crossed), 0), "#6B7280", "#6B7280")
```

### Pick between several colours

```xfs
let strength: float = tape.delta() / tape.volume()
let bar_color: color = strength > 0.3 ? "#22C55E" : strength < -0.3 ? "#EF4444" : "#6B7280"
plot.candle_color(tape.available(), bar_color, 0.8)
```

### Colour on a scale

```xfs
let strength: float = tape.delta() / tape.volume()
let heat: color = color.from_gradient(nz(strength, 0), -0.5, 0.5, "#EF4444", "#22C55E")
let faded: color = color.new(heat, 60)
plot.candle_color(tape.available(), faded, 1)
```

## Complete indicators

Each of these compiles as written. Copy one into the Custom Indicator Builder and it draws. Every one that reads optional data still draws on a chart without it.

### Custom volume profile

A custom volume profile: fold the footprint ladder of the New York cash session into one profile, drawn the way every other profile on the chart is drawn, with the value area and the point of control marked on price.

```xfs
//@xfscript 1
indicator("Custom Volume Profile")

let window: string = "0930-1600"
let value_percent: float = input.float(70, "valuePercent", "Value area percent", 10, 100, 5)
let max_bars: int = input.int(2000, "maxBars", "Longest session", 10, 5000, 10)
let poc_color: color = input.color("#F59E0B", "pocColor", "Point of control")
let value_color: color = input.color("#6B7280", "valueColor", "Value area")

let in_session: bool = session.in(window, "America/New_York")
let was_in: bool = bar_index > 0 && in_session[1]
let first_bar: bool = in_session && !was_in
let session_end: bool = was_in && !in_session

var elapsed: float = 0
elapsed := nz(elapsed[1], 0)
if first_bar {
  elapsed := 1
}
if in_session && !first_bar {
  elapsed := elapsed + 1
}

let folded: int = math.round(math.clamp(elapsed, 1, max_bars))
profile.fold(session_end, folded, value_percent)

let poc: float = cell.poc_price()
let value_high: float = cell.va_high(value_percent)
let value_low: float = cell.va_low(value_percent)
let has_ladder: bool = cell.available()

if in_session && has_ladder {
  plot.dot(true, poc, poc_color, 3)
  plot.band(value_high, value_low, value_color, value_color)
}

plot.hline(poc, poc_color, 1, "dotted", "POC")
```

### Footprint imbalance and cell colouring

A footprint imbalance read: count the diagonal imbalances on each bar, mark the stacked runs that matter, recolour the candle when one side owned the bar, and show the balance of the two sides in the panel.

```xfs
//@xfscript 1
indicator("Footprint Imbalance")

let ratio: float = input.float(3, "ratio", "Imbalance ratio", 1.5, 10, 0.5)
let stack: int = input.int(3, "stack", "Stacked rows", 2, 10, 1)
let buy_color: color = input.color("#22C55E", "buyColor", "Buying")
let sell_color: color = input.color("#EF4444", "sellColor", "Selling")

let has_ladder: bool = cell.available()
let buy_rows: float = cell.ask_imbalances(ratio)
let sell_rows: float = cell.bid_imbalances(ratio)
let stacked_buy: bool = has_ladder && cell.stacked_ask(ratio, stack)
let stacked_sell: bool = has_ladder && cell.stacked_bid(ratio, stack)
let lean: float = buy_rows - sell_rows

plot.marker(stacked_buy, low, buy_color, "triangle", "below", 10, 3)
plot.marker(stacked_sell, high, sell_color, "triangle", "above", 10, 3)
plot.candle_color(stacked_buy, buy_color, 0.75)
plot.candle_color(stacked_sell, sell_color, 0.75)
plot.dot(has_ladder, cell.poc_price(), "#F59E0B", 3)
plot.histogram(nz(lean, 0), buy_color, sell_color)
plot.panel_hline(0, "#6B7280", "dotted", "Balanced")
```

### Custom big trade rule

A big trade rule of your own: mark every bar carrying a print above your size, draw the bubble at the price it printed, scale the bubble with the size, and count the big prints in the panel.

```xfs
//@xfscript 1
indicator("Custom Big Trades")

let min_size: float = input.float(75, "minSize", "Big trade size", 1, 100000, 1)
let top_size: float = input.float(400, "topSize", "Size for the largest bubble", 2, 100000, 1)
let min_radius: float = input.float(5, "minRadius", "Smallest bubble", 1, 20, 1)
let max_radius: float = input.float(18, "maxRadius", "Largest bubble", 1, 20, 1)
let buy_color: color = input.color("#22C55E", "buyColor", "Bought")
let sell_color: color = input.color("#EF4444", "sellColor", "Sold")
let label_size: float = input.float(9, "labelSize", "Label size", 6, 24, 1)

let prints: bool = tape.prints_available()
let biggest: float = nz(tape.largest_size(), 0)
let at_price: float = nz(tape.largest_price(), close)
let big_side: side = tape.largest_side()
let qualifies: bool = prints && biggest >= min_size
let radius: float = math.scale(biggest, min_size, top_size, min_radius, max_radius)

let bought: bool = qualifies && big_side == "BUY"
let sold: bool = qualifies && big_side == "SELL"

plot.dot(bought, at_price, buy_color, radius)
plot.dot(sold, at_price, sell_color, radius)
plot.text(qualifies, at_price, format.compact(biggest), "#E5E7EB", "above", label_size, 10, 60)

let cluster: float = tape.count_above(min_size)
plot.histogram(nz(cluster, 0), buy_color, sell_color)
plot.label(str.concat("BIG PRINTS ", format.compact(cluster)), "#E5E7EB")
```

### Iceberg and absorption detector on individual orders

Individual orders: mark bars where a resting order kept refilling at its price, and bars where a large resting order was hit hard and held. When the chart has no order level data it falls back to the tape, so it still draws.

```xfs
//@xfscript 1
indicator("Iceberg and Absorption")

let min_refills: float = input.float(3, "minRefills", "Refills for an iceberg", 1, 50, 1)
let min_fill: float = input.float(50, "minFill", "Filled size for absorption", 1, 100000, 1)
let tape_size: float = input.float(150, "tapeSize", "Tape fallback size", 1, 100000, 1)
let bid_color: color = input.color("#22C55E", "bidColor", "Buyers holding")
let ask_color: color = input.color("#EF4444", "askColor", "Sellers holding")

let has_orders: bool = mbo.supported() && mbo.available()
let icebergs: float = mbo.iceberg_count(min_refills)
let big: int = mbo.largest_index()
let big_side: side = mbo.side(big)
let big_price: float = mbo.price(big)
let big_filled: float = mbo.filled(big)
let big_refills: float = mbo.refills(big)
let big_active: bool = mbo.status(big) == "active"

let iceberg_bid: bool = has_orders && icebergs > 0 && big_side == "BUY" && big_refills >= min_refills
let iceberg_ask: bool = has_orders && icebergs > 0 && big_side == "SELL" && big_refills >= min_refills
let held_bid: bool = has_orders && big_active && big_side == "BUY" && big_filled >= min_fill && low >= big_price
let held_ask: bool = has_orders && big_active && big_side == "SELL" && big_filled >= min_fill && high <= big_price

plot.marker(iceberg_bid, big_price, bid_color, "diamond", "below", 10, 2)
plot.marker(iceberg_ask, big_price, ask_color, "diamond", "above", 10, 2)
plot.zone(held_bid, big_price, big_price - chart.tick_size() * 2, bid_color, 0.25, 0, 20, "bullish")
plot.zone(held_ask, big_price + chart.tick_size() * 2, big_price, ask_color, 0.25, 0, 20, "bearish")

let heavy_tape: bool = !has_orders && tape.available() && tape.volume_above(tape_size) > 0
let tape_absorb_bid: bool = heavy_tape && tape.sell_volume_above(tape_size) > tape.buy_volume_above(tape_size) && close >= open
let tape_absorb_ask: bool = heavy_tape && tape.buy_volume_above(tape_size) > tape.sell_volume_above(tape_size) && close <= open
plot.marker(tape_absorb_bid, low, bid_color, "circle", "below", 7, 2)
plot.marker(tape_absorb_ask, high, ask_color, "circle", "above", 7, 2)

plot.histogram(nz(icebergs, 0), bid_color, ask_color)
if has_orders {
  plot.label(str.concat("ICEBERGS ", format.compact(icebergs), "  BIGGEST ", format.compact(mbo.largest_size())), "#E5E7EB")
} else {
  plot.label("ORDER DATA OFF, TAPE ONLY", "#6B7280")
}
```

### Depth imbalance panel

Depth of book: a panel of how the resting size leans between the best bid levels and the best offer levels, smoothed, with the resting totals in the readout. Where the chart has no depth it shows the tape delta instead.

```xfs
//@xfscript 1
indicator("Depth Imbalance")

let levels: int = input.int(10, "levels", "Levels per side", 1, 50, 1)
let smooth: int = input.int(5, "smooth", "Smoothing", 1, 50, 1)
let bid_color: color = input.color("#22C55E", "bidColor", "Bids heavier")
let ask_color: color = input.color("#EF4444", "askColor", "Offers heavier")

let has_depth: bool = l2.supported() && l2.available()
let book: float = l2.imbalance(levels)
let tape_lean: float = tape.delta() / tape.volume()

var reading: float = 0
reading := nz(tape_lean, 0)
if has_depth {
  reading := nz(book, 0)
}
let smoothed: float = ta.ema(reading, smooth)

plot.panel_range(-1, 1)
plot.histogram(reading, bid_color, ask_color)
plot.panel_line(smoothed, "#E5E7EB", 2, "solid")
plot.panel_hline(0.3, bid_color, "dotted", "Bid heavy")
plot.panel_hline(-0.3, ask_color, "dotted", "Offer heavy")

if has_depth {
  plot.label(str.concat("BID ", format.compact(l2.bid_total(levels)), "  ASK ", format.compact(l2.ask_total(levels)), "  SPREAD ", format.fixed(l2.spread(), 2)), "#E5E7EB")
} else {
  plot.label("DEPTH OFF, SHOWING TAPE DELTA", "#6B7280")
}
```

### Spread and quote tracker

Level 1: the live spread and the size at the best bid and offer in the readout, a marker at the right edge when the spread widens, and a running record of the quote kept in a var. In replay, where there are no quotes, it reads the top of the recorded book instead.

```xfs
//@xfscript 1
indicator("Spread Tracker")

let wide_ticks: float = input.float(2, "wideTicks", "Wide spread in ticks", 1, 50, 1)
let calm_color: color = input.color("#E5E7EB", "calmColor", "Normal")
let wide_color: color = input.color("#F59E0B", "wideColor", "Wide")

let tick: float = nz(chart.tick_size(), 0.25)
let quoted: bool = l1.supported() && l1.available()
let booked: bool = l2.supported() && l2.available()

var spread: float = nan
spread := nz(spread[1], nan)
var bid_size: float = nan
var ask_size: float = nan
bid_size := nz(bid_size[1], nan)
ask_size := nz(ask_size[1], nan)
if booked {
  spread := l2.spread()
  bid_size := l2.bid_size(0)
  ask_size := l2.ask_size(0)
}
if quoted {
  spread := l1.ask_price() - l1.bid_price()
  bid_size := l1.bid_size()
  ask_size := l1.ask_size()
}

let spread_ticks: float = spread / tick
let is_wide: bool = spread_ticks >= wide_ticks
var text_color: color = "#E5E7EB"
text_color := calm_color
if is_wide {
  text_color := wide_color
}

plot.marker(last_bar && is_wide, close, wide_color, "x", "above", 10, 6)
if is_nan(spread) {
  plot.label("NO QUOTE ON THIS CHART", "#6B7280")
} else {
  plot.label(str.concat("SPREAD ", format.fixed(spread_ticks, 0), " TICKS  BID ", format.compact(bid_size), " x ASK ", format.compact(ask_size)), text_color)
}
```

### Gamma levels

Options gamma: the zero gamma level and the two major levels held as lines across the chart, and the gamma of the strike the price is in, in the readout. Nothing is invented where the chart has no gamma.

```xfs
//@xfscript 1
indicator("Gamma Levels")

let zero_color: color = input.color("#F59E0B", "zeroColor", "Zero gamma")
let pos_color: color = input.color("#22C55E", "posColor", "Positive")
let neg_color: color = input.color("#EF4444", "negColor", "Negative")

let has_gamma: bool = gex.supported() && gex.available()

var zero_level: float = nan
var call_wall: float = nan
var put_wall: float = nan
var net_gamma: float = nan
zero_level := nz(zero_level[1], nan)
call_wall := nz(call_wall[1], nan)
put_wall := nz(put_wall[1], nan)
net_gamma := nz(net_gamma[1], nan)
if has_gamma {
  zero_level := gex.zero_gamma()
  call_wall := gex.major_pos_vol()
  put_wall := gex.major_neg_vol()
  net_gamma := gex.net_vol()
}

plot.hline(zero_level, zero_color, 2, "dashed", "Zero gamma")
plot.hline(call_wall, pos_color, 1, "solid", "Major positive")
plot.hline(put_wall, neg_color, 1, "solid", "Major negative")
plot.marker(has_gamma, close, zero_color, "circle", "above", 6, 4)

if gex.supported() {
  plot.label(str.concat("NET GAMMA ", format.compact(net_gamma), "  AT PRICE ", format.compact(gex.vol_at(close))), "#E5E7EB")
} else {
  plot.label("GAMMA OFF ON THIS CHART", "#6B7280")
}
```

### Building on the Big Trades markers

The markers the Big Trades indicator already draws: keep only the largest one on each bar, ring the ones above your size, keep a running balance of big buying against big selling in the panel, and colour the candle that carried the biggest print of the session.

```xfs
//@xfscript 1
indicator("Big Trades Balance")

let ring_size: float = input.float(250, "ringSize", "Ring above", 1, 100000, 1)
let buy_color: color = input.color("#22C55E", "buyColor", "Buying")
let sell_color: color = input.color("#EF4444", "sellColor", "Selling")

let shown: bool = bigtrades.available()
let marks: float = bigtrades.count()
let size0: float = nz(bigtrades.size(0), 0)
let size1: float = nz(bigtrades.size(1), 0)
let pick_second: bool = size1 > size0
var top_size: float = 0
var top_price: float = nan
var top_side: side = "UNDEFINED"
top_size := size0
top_price := bigtrades.price(0)
top_side := bigtrades.side(0)
if pick_second {
  top_size := size1
  top_price := bigtrades.price(1)
  top_side := bigtrades.side(1)
}

let ringed: bool = shown && marks > 0 && top_size >= ring_size
plot.marker(ringed && top_side == "BUY", top_price, buy_color, "circle", "below", 16, 0)
plot.marker(ringed && top_side == "SELL", top_price, sell_color, "circle", "above", 16, 0)

var balance: float = 0
balance := nz(balance[1], 0)
if shown && marks > 0 && top_side == "BUY" {
  balance := balance + top_size
}
if shown && marks > 0 && top_side == "SELL" {
  balance := balance - top_size
}
plot.panel_area(balance, "#6B7280", 0.15)
plot.panel_line(balance, "#E5E7EB", 2, "solid")
plot.panel_hline(0, "#6B7280", "dotted", "Even")

if shown {
  plot.label(str.concat("BIG TRADE BALANCE ", format.compact(balance)), "#E5E7EB")
} else {
  plot.label("ADD BIG TRADES TO THIS CHART", "#6B7280")
}
```

### Alert when price enters an Effort Zone

Reading a chart indicator by name: a marker the moment a bar closes inside one of the zones Effort Zones drew, coloured by the zone's side, candles tinted while price sits in a zone, and a readout of how many ticks away the nearest zone above and below are. Turn on the Alert tab and every entry marker raises a sound and a popup. Needs Effort Zones on the chart; without it the readout says so.

```xfs
//@xfscript 1
indicator("Effort Zone Entry")

let buy_color: color = input.color("#22C55E", "buyColor", "Buy side zone")
let sell_color: color = input.color("#EF4444", "sellColor", "Sell side zone")
let tint: float = input.float(0.45, "tint", "Candle tint while inside", 0, 1, 0.05)

let has_zones: bool = effort_zones.available()
let zone: int = effort_zones.at(close)
let in_zone: bool = zone >= 0
let entered: bool = bar_index > 0 && has_zones && in_zone && !in_zone[1]
let zone_side: side = effort_zones.side(zone)

plot.marker(entered && zone_side == "BUY", low, buy_color, "triangle", "below", 12, 4)
plot.marker(entered && zone_side == "SELL", high, sell_color, "triangle", "above", 12, 4)
plot.marker(entered && zone_side == "UNDEFINED", close, "#9CA3AF", "diamond", "above", 10, 4)
plot.candle_color(in_zone && zone_side == "BUY", buy_color, tint)
plot.candle_color(in_zone && zone_side == "SELL", sell_color, tint)
plot.dot(effort_zones.started() > 0, high, "#F59E0B", 4)

let tick: float = nz(chart.tick_size(), 0.25)
let up: int = effort_zones.above(close)
let down: int = effort_zones.below(close)
let to_up: float = (effort_zones.low(up) - close) / tick
let to_down: float = (close - effort_zones.high(down)) / tick

if has_zones {
  plot.label(str.concat("ZONES ON THIS BAR ", format.compact(effort_zones.count()), "  UP ", format.fixed(nz(to_up, 0), 0), "  DOWN ", format.fixed(nz(to_down, 0), 0), " TICKS"), "#E5E7EB")
} else {
  plot.label("ADD EFFORT ZONES TO THIS CHART", "#6B7280")
}
```

### Big Trades inside Range Absorption zones

Two chart indicators together: of the markers Big Trades drew on each bar, count the ones that sit inside a Range Absorption zone, ring them on the chart, show the count in the panel, and keep a running total of their volume by side. A bar with two or more gets its own marker, so it can alert. Needs Big Trades and Range Absorption on the chart.

```xfs
//@xfscript 1
indicator("Big Trades In Zones")

let buy_color: color = input.color("#22C55E", "buyColor", "Bought")
let sell_color: color = input.color("#EF4444", "sellColor", "Sold")
let cluster: float = input.float(2, "cluster", "Markers for a cluster", 1, 10, 1)

let ready: bool = bigtrades.available() && range_absorption.available()
let in0: bool = ready && range_absorption.at(bigtrades.price(0)) >= 0
let in1: bool = ready && range_absorption.at(bigtrades.price(1)) >= 0
let in2: bool = ready && range_absorption.at(bigtrades.price(2)) >= 0
let in3: bool = ready && range_absorption.at(bigtrades.price(3)) >= 0

var hits: float = 0
var bought: float = 0
var sold: float = 0
hits := 0
bought := nz(bought[1], 0)
sold := nz(sold[1], 0)
if in0 {
  hits := hits + 1
}
if in1 {
  hits := hits + 1
}
if in2 {
  hits := hits + 1
}
if in3 {
  hits := hits + 1
}
if in0 && bigtrades.side(0) == "BUY" {
  bought := bought + bigtrades.size(0)
}
if in0 && bigtrades.side(0) == "SELL" {
  sold := sold + bigtrades.size(0)
}
if in1 && bigtrades.side(1) == "BUY" {
  bought := bought + bigtrades.size(1)
}
if in1 && bigtrades.side(1) == "SELL" {
  sold := sold + bigtrades.size(1)
}

plot.marker(in0, bigtrades.price(0), "#F59E0B", "circle", "above", 14, 0)
plot.marker(in1, bigtrades.price(1), "#F59E0B", "circle", "above", 14, 0)
plot.marker(hits >= cluster, high, "#F59E0B", "square", "above", 10, 12)
plot.histogram(hits, buy_color, sell_color)
plot.panel_hline(cluster, "#6B7280", "dotted", "Cluster")

if ready {
  plot.label(str.concat("IN ZONE BOUGHT ", format.compact(bought), "  SOLD ", format.compact(sold)), "#E5E7EB")
} else {
  plot.label("ADD BIG TRADES AND RANGE ABSORPTION TO THIS CHART", "#6B7280")
}
```

### Depth walls

Depth of book: the biggest resting bid and the biggest resting offer in the book held as labelled levels, a marker when a new wall at least twice the previous one appears, and the wall sizes in the readout. Without depth it says so and draws nothing false.

```xfs
//@xfscript 1
indicator("Depth Walls")

let jump: float = input.float(2, "jump", "New wall multiple", 1.1, 10, 0.1)
let bid_color: color = input.color("#22C55E", "bidColor", "Bid wall")
let ask_color: color = input.color("#EF4444", "askColor", "Ask wall")

let has_depth: bool = l2.supported() && l2.available()

var bid_wall: float = nan
var bid_wall_size: float = nan
var ask_wall: float = nan
var ask_wall_size: float = nan
bid_wall := nz(bid_wall[1], nan)
bid_wall_size := nz(bid_wall_size[1], nan)
ask_wall := nz(ask_wall[1], nan)
ask_wall_size := nz(ask_wall_size[1], nan)

let new_bid_wall: bool = has_depth && l2.max_bid_size() >= nz(bid_wall_size, 0) * jump && !is_nan(bid_wall_size)
let new_ask_wall: bool = has_depth && l2.max_ask_size() >= nz(ask_wall_size, 0) * jump && !is_nan(ask_wall_size)
plot.marker(new_bid_wall, l2.max_bid_price(), bid_color, "square", "below", 9, 2)
plot.marker(new_ask_wall, l2.max_ask_price(), ask_color, "square", "above", 9, 2)

if has_depth {
  bid_wall := l2.max_bid_price()
  bid_wall_size := l2.max_bid_size()
  ask_wall := l2.max_ask_price()
  ask_wall_size := l2.max_ask_size()
}

plot.hline(bid_wall, bid_color, 2, "solid", "Bid wall")
plot.hline(ask_wall, ask_color, 2, "solid", "Ask wall")
if l2.supported() {
  plot.label(str.concat("BID WALL ", format.compact(bid_wall_size), "  ASK WALL ", format.compact(ask_wall_size)), "#E5E7EB")
} else {
  plot.label("DEPTH OFF ON THIS CHART", "#6B7280")
}
```

### Session levels without look-ahead

Session levels read by name: the prior session's point of control and value area from the Session Volume Profile held as lines, the developing point of control traced through today, the London high and low from Trading Sessions, and a marker when price closes back inside the prior value area from outside it. Needs Session Volume Profile and Trading Sessions on the chart; each part says so when its indicator is missing.

```xfs
//@xfscript 1
indicator("Session Levels")

let value_color: color = input.color("#6B7280", "valueColor", "Prior value area")
let poc_color: color = input.color("#F59E0B", "pocColor", "Prior POC")
let london_color: color = input.color("#3B82F6", "londonColor", "London")

let has_profile: bool = session_profile.available() && session_profile.count() > 1
var prior_poc: float = nan
var prior_vah: float = nan
var prior_val: float = nan
prior_poc := nz(prior_poc[1], nan)
prior_vah := nz(prior_vah[1], nan)
prior_val := nz(prior_val[1], nan)
if has_profile {
  prior_poc := session_profile.poc(1)
  prior_vah := session_profile.vah(1)
  prior_val := session_profile.val(1)
}
plot.hline(prior_poc, poc_color, 2, "solid", "Prior POC")
plot.hline(prior_vah, value_color, 1, "dashed", "Prior VAH")
plot.hline(prior_val, value_color, 1, "dashed", "Prior VAL")
plot.line(session_profile.poc(0), "#E5E7EB", 1, "dotted")

let inside_value: bool = close <= prior_vah && close >= prior_val
let back_inside: bool = has_profile && inside_value && !(bar_index > 0 && inside_value[1])
plot.marker(back_inside, close, poc_color, "diamond", "above", 10, 4)

let london: int = trading_sessions.find("London")
plot.line(trading_sessions.high(london), london_color, 1, "dashed")
plot.line(trading_sessions.low(london), london_color, 1, "dashed")

if !session_profile.available() {
  plot.label("ADD SESSION VOLUME PROFILE TO THIS CHART", "#6B7280")
} else if !trading_sessions.available() {
  plot.label("ADD TRADING SESSIONS TO THIS CHART", "#6B7280")
} else {
  plot.label(str.concat("PRIOR POC ", format.fixed(prior_poc, 2), "  LONDON ", format.fixed(trading_sessions.low(london), 2), " TO ", format.fixed(trading_sessions.high(london), 2)), "#E5E7EB")
}
```

### Stop runs inside fair value gaps

Three chart indicators together: mark every Stop Run marker that prints inside a gap FVG drew, coloured by the marker's side, recolour the candle when the gap and the stop run agree on direction, and count the agreeing bars in the panel. Every marker is its own alert. Needs FVG and Stop Run on the chart.

```xfs
//@xfscript 1
indicator("Stop Runs In Gaps")

let buy_color: color = input.color("#22C55E", "buyColor", "Buy side")
let sell_color: color = input.color("#EF4444", "sellColor", "Sell side")

let ready: bool = fvg.available() && stop_run.available() && stop_run.count() > 0
let run_price: float = stop_run.price(0)
let run_side: side = stop_run.side(0)
let gap: int = fvg.at(run_price)
let in_gap: bool = ready && gap >= 0
let agrees: bool = in_gap && fvg.side(gap) == run_side

plot.marker(in_gap && run_side == "BUY", run_price, buy_color, "circle", "below", 11, 2)
plot.marker(in_gap && run_side == "SELL", run_price, sell_color, "circle", "above", 11, 2)
plot.candle_color(agrees && run_side == "BUY", buy_color, 0.6)
plot.candle_color(agrees && run_side == "SELL", sell_color, 0.6)

var agreeing: float = 0
agreeing := nz(agreeing[1], 0)
if agrees {
  agreeing := agreeing + 1
}
plot.panel_line(agreeing, "#E5E7EB", 2, "solid")

if fvg.available() && stop_run.available() {
  plot.label(str.concat("GAPS HERE ", format.compact(fvg.count()), "  AGREEING RUNS ", format.compact(agreeing)), "#E5E7EB")
} else {
  plot.label("ADD FVG AND STOP RUN TO THIS CHART", "#6B7280")
}
```

### Previous day levels confirmed by bar stats

Reading several chart indicators by name: an alert marker when a bar closes through the previous day high or low from Important Levels while Chart Stats shows a strong Delta % in the same direction, the nearest Important Levels line above and below in the readout, and the higher timeframe candle's direction from HFT candles. Needs Important Levels and Chart Stats on the chart; HFT candles is optional.

```xfs
//@xfscript 1
indicator("Levels With Stats")

let min_delta: float = input.float(25, "minDelta", "Delta % to confirm", 0, 100, 1)
let up_color: color = input.color("#22C55E", "upColor", "Break up")
let down_color: color = input.color("#EF4444", "downColor", "Break down")

let pdh: int = important_levels.find("pD H")
let pdl: int = important_levels.find("pD L")
let pdh_price: float = important_levels.price(pdh)
let pdl_price: float = important_levels.price(pdl)
let row: int = chart_stats.find("Delta %")
let delta_pct: float = chart_stats.value(row)

let broke_up: bool = pdh >= 0 && close > pdh_price && open <= pdh_price && delta_pct >= min_delta
let broke_down: bool = pdl >= 0 && close < pdl_price && open >= pdl_price && delta_pct <= 0 - min_delta
plot.marker(broke_up, high, up_color, "triangle", "above", 12, 4)
plot.marker(broke_down, low, down_color, "triangle", "below", 12, 4)
plot.hline(pdh_price, up_color, 1, "dashed", "pD H")
plot.hline(pdl_price, down_color, 1, "dashed", "pD L")

let up: int = important_levels.above(close)
let down: int = important_levels.below(close)
var trend_color: color = "#9CA3AF"
trend_color := "#9CA3AF"
if hft_candles.available() && hft_candles.side(0) == "BUY" {
  trend_color := up_color
} else if hft_candles.available() && hft_candles.side(0) == "SELL" {
  trend_color := down_color
}

if important_levels.available() && chart_stats.available() {
  plot.label(str.concat("ABOVE ", important_levels.name(up), " ", format.fixed(important_levels.price(up), 2), "  BELOW ", important_levels.name(down), " ", format.fixed(important_levels.price(down), 2)), trend_color)
} else {
  plot.label("ADD IMPORTANT LEVELS AND CHART STATS TO THIS CHART", "#6B7280")
}
```

### Extending a built in indicator

A fork of the built in cumulative delta: the base draws its own delta candles in the panel, and the script adds a signal line, marks the crossings on price, and states the reading in the readout.

```xfs
//@xfscript 1
indicator("CVD with Signal", "cvd")

let candles: bool = input.bool(true, "candles", "Delta candles")
let reset_daily: bool = input.bool(true, "resetDaily", "Reset each session")
let signal_length: int = input.int(20, "signalLength", "Signal length", 2, 200, 1)
let up_color: color = input.color("#22C55E", "upColor", "Above signal")
let down_color: color = input.color("#EF4444", "downColor", "Below signal")

let delta_line: float = base.value()
let signal: float = ta.ema(delta_line, signal_length)
let crossed_up: bool = base.available() && ta.crossover(delta_line, signal)
let crossed_down: bool = base.available() && ta.crossunder(delta_line, signal)

plot.panel_line(signal, "#F59E0B", 2, "solid")
plot.marker(crossed_up, low, up_color, "triangle", "below", 9, 3)
plot.marker(crossed_down, high, down_color, "triangle", "above", 9, 3)

if delta_line >= signal {
  plot.label(str.concat("CVD ", format.compact(delta_line), " ABOVE SIGNAL"), up_color)
} else {
  plot.label(str.concat("CVD ", format.compact(delta_line), " BELOW SIGNAL"), down_color)
}
```

### Swing lines and session markers

Straight lines between points: connect each new swing high to the previous one and each new swing low to the previous one, draw a vertical line at every cash session open, and project the latest swing line to the right edge.

```xfs
//@xfscript 1
indicator("Swing Lines")

let length: int = input.int(20, "length", "Swing length", 3, 200, 1)
let high_color: color = input.color("#EF4444", "highColor", "Swing highs")
let low_color: color = input.color("#22C55E", "lowColor", "Swing lows")

let ready: bool = bar_index >= length
let new_high: bool = ready && high >= ta.highest(high, length) && high > nz(high[1], high)
let new_low: bool = ready && low <= ta.lowest(low, length) && low < nz(low[1], low)

var high_bar: float = nan
var high_price: float = nan
var low_bar: float = nan
var low_price: float = nan
high_bar := nz(high_bar[1], nan)
high_price := nz(high_price[1], nan)
low_bar := nz(low_bar[1], nan)
low_price := nz(low_price[1], nan)

plot.segment(new_high && !is_nan(high_bar), high_bar, high_price, bar_index, high, high_color, 2, "solid")
plot.segment(new_low && !is_nan(low_bar), low_bar, low_price, bar_index, low, low_color, 2, "solid")
if new_high {
  high_bar := bar_index
  high_price := high
}
if new_low {
  low_bar := bar_index
  low_price := low
}

let in_cash: bool = session.in("0930-1600", "America/New_York")
let cash_open: bool = in_cash && !(bar_index > 0 && in_cash[1])
plot.vline(cash_open, "#6B7280", 1, "dotted")
plot.hline(high_price, high_color, 1, "dotted", "Last swing high")
plot.hline(low_price, low_color, 1, "dotted", "Last swing low")
```

### Chart markings

Chart markings: an outlined box on every three bar gap, a filled zone that ends the moment price fills it, and the last gap edge held as a level.

```xfs
//@xfscript 1
indicator("Gap Markings")

let bars_right: int = input.int(40, "barsRight", "Bars right", 1, 500, 1)
let outline: bool = input.bool(true, "outline", "Outline instead of fill")
let up_color: color = input.color("#22C55E", "upColor", "Bullish gap")
let down_color: color = input.color("#EF4444", "downColor", "Bearish gap")

let ready: bool = bar_index >= 2
let gap_up: bool = ready && low > high[2]
let gap_down: bool = ready && high < low[2]

plot.box(gap_up && outline, low, high[2], up_color, 1, -1, bars_right, 1, "solid", "Gap")
plot.box(gap_down && outline, low[2], high, down_color, 1, -1, bars_right, 1, "solid", "Gap")
plot.zone(gap_up && !outline, low, high[2], up_color, 0.16, -1, bars_right, "bullish")
plot.zone(gap_down && !outline, low[2], high, down_color, 0.16, -1, bars_right, "bearish")

var last_edge: float = nan
last_edge := nz(last_edge[1], nan)
if gap_up {
  last_edge := high[2]
}
if gap_down {
  last_edge := low[2]
}

plot.hline(last_edge, "#9CA3AF", 1, "dotted", "Last gap edge")
plot.marker(gap_up, low, up_color, "diamond", "below", 8, 3)
plot.marker(gap_down, high, down_color, "diamond", "above", 8, 3)
```

### Information panel

An information panel: several lines in the readout at the top left of the chart, each stating what is true right now, including the book and the quote when the chart has them.

```xfs
//@xfscript 1
indicator("Info Panel")

let lookback: int = input.int(50, "lookback", "Baseline", 5, 500, 1)
let head_color: color = input.color("#E5E7EB", "headColor", "Heading")
let up_color: color = input.color("#22C55E", "upColor", "Positive")
let down_color: color = input.color("#EF4444", "downColor", "Negative")

let total: float = tape.volume()
let delta: float = tape.delta()
let delta_percent: float = delta / total * 100
let average_volume: float = ta.sma(total, lookback)
let range_points: float = high - low
let average_range: float = ta.sma(range_points, lookback)
let effort: float = total / average_volume

plot.label(str.concat("VOL ", format.compact(total), "  x", format.fixed(nz(effort, 0), 2)), head_color)
plot.label(str.concat("RANGE ", format.fixed(range_points, 2), " / ", format.fixed(nz(average_range, 0), 2)), head_color)

if delta >= 0 {
  plot.label(str.concat("DELTA +", format.compact(delta), "  ", format.fixed(nz(delta_percent, 0), 1), "%"), up_color)
} else {
  plot.label(str.concat("DELTA ", format.compact(delta), "  ", format.fixed(nz(delta_percent, 0), 1), "%"), down_color)
}

if cell.available() {
  plot.label(str.concat("POC ", format.fixed(cell.poc_price(), 2), "  ROWS ", format.compact(cell.count())), head_color)
}
if l2.supported() && l2.available() {
  plot.label(str.concat("BOOK 10 ", format.fixed(l2.imbalance(10) * 100, 0), "%"), head_color)
}
if l1.supported() && l1.available() {
  plot.label(str.concat("BID ", format.fixed(l1.bid_price(), 2), "  ASK ", format.fixed(l1.ask_price(), 2)), head_color)
}
```

### Alert on a condition

An alert: a loud marker and a zone the moment a breakout triggers, and a level left behind so you can see where it fired. Turn the alert itself on in the indicator settings, Alert tab, and it fires on the marker and the zone with a sound and a popup titled Breakout Alert.

```xfs
//@xfscript 1
indicator("Breakout Alert")

let lookback: int = input.int(20, "lookback", "Lookback", 2, 500, 1)
let require_body: bool = input.bool(true, "requireBody", "Body must close beyond")
let alert_color: color = input.color("#F59E0B", "alertColor", "Alert")

let highest_high: float = ta.highest(high, lookback)
let lowest_low: float = ta.lowest(low, lookback)
let prior_high: float = highest_high[1]
let prior_low: float = lowest_low[1]

let up_edge: bool = close > prior_high
let down_edge: bool = close < prior_low
let strong_up: bool = !require_body || close > open
let strong_down: bool = !require_body || close < open

let broke_up: bool = up_edge && strong_up
let broke_down: bool = down_edge && strong_down

var last_alert: float = nan
last_alert := nz(last_alert[1], nan)
if broke_up {
  last_alert := close
}
if broke_down {
  last_alert := close
}

plot.marker(broke_up, high, alert_color, "triangle", "above", 14, 4)
plot.marker(broke_down, low, alert_color, "triangle", "below", 14, 4)
plot.zone(broke_up, high, prior_high, alert_color, 0.2, 0, 10, "bullish")
plot.zone(broke_down, prior_low, low, alert_color, 0.2, 0, 10, "bearish")
plot.text(broke_up, high, "BREAKOUT UP", alert_color, "above", 10, 18, 60)
plot.text(broke_down, low, "BREAKOUT DOWN", alert_color, "below", 10, 18, 60)
plot.hline(last_alert, alert_color, 1, "dashed", "Last alert")
plot.label(str.concat("LAST ALERT ", format.fixed(last_alert, 2)), alert_color)
```

### Colour on a condition

Colour that follows a condition: the candles themselves recoloured while the trend holds, and one average line that switches colour with it as a single continuous stroke.

```xfs
//@xfscript 1
indicator("Trend Colour")

let length: int = input.int(34, "length", "Length", 2, 400, 1)
let paint_candles: bool = input.bool(true, "paintCandles", "Recolour candles")
let up_color: color = input.color("#22C55E", "upColor", "Up")
let down_color: color = input.color("#EF4444", "downColor", "Down")
let flat_color: color = input.color("#9CA3AF", "flatColor", "Flat")
let fade: float = input.float(0.65, "fade", "Candle opacity", 0.1, 1, 0.05)

let basis: float = ta.ema(close, length)
let slope: float = ta.change(basis, 1)
let rising: bool = slope > 0
let falling: bool = slope < 0
let width: float = ta.stdev(close, length)

var trend_color: color = "#9CA3AF"
trend_color := flat_color
if rising {
  trend_color := up_color
} else if falling {
  trend_color := down_color
}

plot.candle_color(paint_candles && (rising || falling), trend_color, fade)
plot.line(basis, trend_color, 3, "solid")
plot.band(basis + width, basis - width, up_color, down_color)
plot.marker(ta.crossover(close, basis), low, up_color, "circle", "below", 7, 2)
plot.marker(ta.crossunder(close, basis), high, down_color, "circle", "above", 7, 2)
```

### Panel oscillator

An oscillator in the panel below the chart: a smoothed aggression reading on a fixed scale, with its signal line, a filled body, a zero level and the crossings marked.

```xfs
//@xfscript 1
indicator("Aggression Oscillator")

let smooth: int = input.int(14, "smooth", "Smoothing", 1, 200, 1)
let signal_length: int = input.int(9, "signalLength", "Signal", 1, 200, 1)
let trigger: float = input.float(40, "trigger", "Trigger level", 5, 100, 5)
let up_color: color = input.color("#22C55E", "upColor", "Positive")
let down_color: color = input.color("#EF4444", "downColor", "Negative")
let signal_color: color = input.color("#E5E7EB", "signalColor", "Signal")

let raw: float = tape.delta() / tape.volume() * 100
let oscillator: float = ta.ema(nz(raw, 0), smooth)
let signal: float = ta.ema(oscillator, signal_length)
let histogram: float = oscillator - signal

plot.panel_range(-100, 100)
plot.panel_area(oscillator, up_color, 0.16)
plot.panel_line(oscillator, up_color, 2, "solid")
plot.panel_line(signal, signal_color, 1, "dashed")
plot.panel_hline(0, "#6B7280", "dotted", "Flat")
plot.panel_hline(trigger, down_color, "dotted", "High")
plot.histogram(histogram, up_color, down_color)

plot.marker(ta.crossover(oscillator, signal), low, up_color, "square", "below", 7, 2)
plot.marker(ta.crossunder(oscillator, signal), high, down_color, "square", "above", 7, 2)
```

### Session tool

A session tool: the opening price of the cash session, average daily range projections around it, and the session box.

```xfs
//@xfscript 1
indicator("Session Tool")

let window: string = "0930-1600"
let zone: string = "America/New_York"
let adr_length: int = input.int(5, "adrLength", "ADR sessions", 1, 252, 1)
let open_color: color = input.color("#E5E7EB", "openColor", "Session open")
let target_color: color = input.color("#6B7280", "targetColor", "ADR targets")

let in_session: bool = session.in(window, zone)
let was_in: bool = bar_index > 0 && in_session[1]
let first_bar: bool = in_session && !was_in
let session_end: bool = was_in && !in_session

let anchor: float = session.open(window, zone)
let adr: float = session.adr(adr_length, window, zone)

var box_high: float = nan
var box_low: float = nan
box_high := nz(box_high[1], nan)
box_low := nz(box_low[1], nan)
if first_bar {
  box_high := high
  box_low := low
}
if in_session {
  box_high := math.max(nz(box_high, high), high)
  box_low := math.min(nz(box_low, low), low)
}

if in_session {
  plot.line(anchor, open_color, 2, "solid")
  plot.line(anchor + adr / 2, target_color, 1, "dashed")
  plot.line(anchor - adr / 2, target_color, 1, "dashed")
  plot.zone(box_high, box_low, target_color, 0.08)
}

plot.box(session_end, box_high, box_low, open_color, 1, 0, 0, 1, "solid", "Session")
plot.marker(first_bar, low, open_color, "diamond", "below", 8, 4)
plot.label(str.concat("ADR ", format.fixed(adr, 2)), target_color)
```

### Text overlay on every bar

A text overlay: the numbers that matter printed above or below each candle, placed on the side that keeps price clear, and hidden when the chart is zoomed out.

```xfs
//@xfscript 1
indicator("Bar Stats")

let font: float = input.float(9, "font", "Font size", 6, 24, 1)
let slot: float = input.float(84, "slot", "Minimum bar width", 10, 200, 2)
let up_color: color = input.color("#22C55E", "upColor", "Positive")
let down_color: color = input.color("#EF4444", "downColor", "Negative")
let flat_color: color = input.color("#9CA3AF", "flatColor", "Neutral")

let total: float = tape.volume()
let delta: float = tape.delta()
let delta_percent: float = delta / total * 100
let has_tape: bool = tape.available() && total > 0

let body: string = str.concat(format.compact(delta), "\n", format.fixed(nz(delta_percent, 0), 1), "%\n", format.compact(total))

let bearish: bool = delta < 0

if bearish {
  plot.text(has_tape, high, body, down_color, "above", font, 4, slot)
} else if delta > 0 {
  plot.text(has_tape, low, body, up_color, "below", font, 4, slot)
} else {
  plot.text(has_tape, low, body, flat_color, "below", font, 4, slot)
}
```

### Custom volume profile look

A volume profile with a look of its own: each New York cash session folded on the right of its bars with delta coloured rows, a white point of control and a blue value area, the session open row marked on the developing profile, and the newest candle's ladder drawn on the price axis edge in a red to green gradient by delta.

```xfs
//@xfscript 1
indicator("Profile Look")

let window: string = "0930-1600"
let width: float = input.float(30, "width", "Profile width percent", 5, 100, 5)
let ladder_px: float = input.float(160, "ladderWidth", "Live ladder width in pixels", 40, 600, 10)
let sell_color: color = input.color("#EF4444", "sellColor", "Sellers")
let buy_color: color = input.color("#22C55E", "buyColor", "Buyers")
let poc_color: color = input.color("#FFFFFF", "pocColor", "Point of control")
let va_color: color = input.color("#3B82F6", "vaColor", "Value area")

let in_session: bool = session.in(window, "America/New_York")
let was_in: bool = bar_index > 0 && in_session[1]
let first_bar: bool = in_session && !was_in
let session_end: bool = was_in && !in_session

var elapsed: float = 0
elapsed := first_bar ? 1 : in_session ? elapsed + 1 : elapsed
let folded: int = math.round(math.clamp(elapsed, 1, 5000))

profile.fold(session_end, folded, 70, "auto", nan, "right", width, "delta", poc_color, va_color, "auto")
profile.fold(last_bar && in_session, folded, 70, "auto", nan, "right", width, "delta", poc_color, va_color, "auto")
let open_price: float = session.open(window, "America/New_York")
if last_bar && in_session {
  profile.row_color(open_price, color.yellow)
}

let top_volume: float = cell.max_volume()
let top_delta: float = math.max(nz(cell.max_delta(), 0), 1)
for i = 0 to cell.count() - 1 by 1 {
  let p: float = cell.price(i)
  let shade: color = color.from_gradient(cell.delta(i), -top_delta, top_delta, sell_color, buy_color)
  plot.hbar(p, p, cell.volume(i), top_volume, i == cell.poc_index() ? poc_color : shade, "right", ladder_px)
}

plot.table(0, 0, "Bar delta", "#9CA3AF")
plot.table(0, 1, format.compact(tape.delta()), tape.delta() >= 0 ? buy_color : sell_color)
```

### Custom footprint cells

Footprint style cells of your own: every price row of the recent candles painted on a gradient by its imbalance, bid by ask on the point of control row, and stacked imbalances marked.

```xfs
//@xfscript 1
indicator("Delta Cells")

let lookback: int = input.int(150, "lookback", "Bars to paint", 10, 2000, 10)
let strong: float = input.float(0.6, "strong", "Imbalance for full colour", 0.1, 1, 0.05)
let sell_color: color = input.color("#EF4444CC", "sellColor", "Sellers")
let buy_color: color = input.color("#22C55ECC", "buyColor", "Buyers")
let calm: color = input.color("#6B728033", "calmColor", "Balanced")

let recent: bool = bar_index >= bar_count - lookback
let rows: float = recent && cell.available() ? cell.count() : 0
let poc_row: int = cell.poc_index()

for i = 0 to rows - 1 by 1 {
  let p: float = cell.price(i)
  let lean: float = nz(cell.imbalance(i), 0)
  let shade: color = lean >= 0 ? color.from_gradient(lean, 0, strong, calm, buy_color) : color.from_gradient(lean, -strong, 0, sell_color, calm)
  let caption: string = i == poc_row ? str.concat(format.compact(cell.bid_volume(i)), " x ", format.compact(cell.ask_volume(i))) : ""
  plot.cell(p, p, shade, caption, "#FFFFFF")
}

let stacked_up: bool = recent && cell.stacked_ask(3, 3)
let stacked_down: bool = recent && cell.stacked_bid(3, 3)
plot.marker(stacked_up, low, "#22C55E", "triangle", "below", 9, 3)
plot.marker(stacked_down, high, "#EF4444", "triangle", "above", 9, 3)
```

### Opening range glow

The first half hour of the New York session as a gradient rectangle projected to the right, and a filled arrow on the first close that breaks out of it.

```xfs
//@xfscript 1
indicator("Opening Range Glow")

let range_window: string = "0930-1000"
let cash_window: string = "0930-1600"
let project: int = input.int(60, "project", "Bars to project", 5, 400, 5)
let glow: color = input.color("#3B82F666", "glow", "Range colour")
let up_color: color = input.color("#22C55E", "upColor", "Break up")
let down_color: color = input.color("#EF4444", "downColor", "Break down")

let in_range: bool = session.in(range_window, "America/New_York")
let in_cash: bool = session.in(cash_window, "America/New_York")
let range_started: bool = in_range && !(bar_index > 0 && in_range[1])
let range_ended: bool = !in_range && bar_index > 0 && in_range[1]
let unit: float = nz(ta.atr(14), high - low)

var range_high: float = nan
var range_low: float = nan
var range_bars: float = 0
var broken: bool = false
if range_started {
  range_high := high
  range_low := low
  range_bars := 0
  broken := false
}
if in_range {
  range_high := math.max(range_high, high)
  range_low := math.min(range_low, low)
  range_bars += 1
}

let fade: color = color.new(glow, 100)
if range_ended {
  plot.rect(-range_bars - 0.5, project, range_high, range_low, glow, glow, 1, fade, "vertical")
}

let break_up: bool = in_cash && !in_range && !broken && close > range_high
let break_down: bool = in_cash && !in_range && !broken && close < range_low
let below: float = low - unit * 0.2
let above: float = high + unit * 0.2
if break_up {
  plot.polygon(up_color, 0, below, 0.45, below - unit * 0.6, 0.15, below - unit * 0.6, 0.15, below - unit * 1.4, -0.15, below - unit * 1.4, -0.15, below - unit * 0.6, -0.45, below - unit * 0.6)
  broken := true
}
if break_down {
  plot.polygon(down_color, 0, above, 0.45, above + unit * 0.6, 0.15, above + unit * 0.6, 0.15, above + unit * 1.4, -0.15, above + unit * 1.4, -0.15, above + unit * 0.6, -0.45, above + unit * 0.6)
  broken := true
}
```

### Market readout table

A table in a chart corner with the newest bar's delta, delta percent, RSI, ATR and range, each value cell shaded from red to green by what it says.

```xfs
//@xfscript 1
indicator("Market Readout")

let corner_pick: int = input.int(0, "corner", "Corner, 0 to 3 clockwise from top right", 0, 3, 1)
let corner: string = corner_pick == 0 ? "top_right" : corner_pick == 1 ? "bottom_right" : corner_pick == 2 ? "bottom_left" : "top_left"
let quiet: color = "#00000000"
let head_bg: color = "#1F2937E6"
let label_color: color = "#9CA3AF"
let value_color: color = "#F9FAFB"

let rsi: float = ta.rsi(close, 14)
let atr: float = ta.atr(14)
let delta_percent: float = tape.delta() / tape.volume() * 100
let tick: float = nz(chart.tick_size(), 0.25)
let range_ticks: float = (high - low) / tick

fn shade(value: float, low_end: float, high_end: float) -> color {
  return is_nan(value) ? "#00000000" : color.from_gradient(value, low_end, high_end, "#EF444466", "#22C55E66")
}

fn shown(value: float, decimals: int) -> string {
  return is_nan(value) ? "n/a" : str.tostring(value, decimals)
}

plot.table(0, 0, "Readout", value_color, head_bg, corner)
plot.table(0, 1, str.concat(format.compact(bar_count), " bars"), label_color, head_bg, corner)
plot.table(1, 0, "Delta", label_color, quiet, corner)
plot.table(1, 1, format.compact(tape.delta()), value_color, shade(delta_percent, -40, 40), corner)
plot.table(2, 0, "Delta %", label_color, quiet, corner)
plot.table(2, 1, shown(delta_percent, 1), value_color, shade(delta_percent, -40, 40), corner)
plot.table(3, 0, "RSI 14", label_color, quiet, corner)
plot.table(3, 1, shown(rsi, 1), value_color, shade(rsi, 30, 70), corner)
plot.table(4, 0, "ATR 14", label_color, quiet, corner)
plot.table(4, 1, shown(atr, 2), value_color, quiet, corner)
plot.table(5, 0, "Range ticks", label_color, quiet, corner)
plot.table(5, 1, shown(range_ticks, 0), value_color, shade(range_ticks / nz(atr / tick, 1), 0.5, 1.5), corner)
```

### RSI with bands

An RSI in the sub panel with fixed overbought and oversold levels and Bollinger bands drawn on the RSI itself, candles coloured when the RSI is stretched beyond both, and markers when it turns back.

```xfs
//@xfscript 1
indicator("RSI Bands")

let length: int = input.int(14, "length", "RSI length", 2, 200, 1)
let band_length: int = input.int(20, "bandLength", "Band length", 2, 200, 1)
let band_mult: float = input.float(2, "bandMult", "Band width", 0.5, 4, 0.1)
let upper: float = input.float(70, "upper", "Overbought", 50, 100, 1)
let lower: float = input.float(30, "lower", "Oversold", 0, 50, 1)

let rsi: float = ta.rsi(close, length)
let band_top: float = ta.bb_upper(rsi, band_length, band_mult)
let band_bottom: float = ta.bb_lower(rsi, band_length, band_mult)

plot.panel_range(0, 100)
plot.panel_line(band_top, "#6B7280", 1, "dashed")
plot.panel_line(band_bottom, "#6B7280", 1, "dashed")
plot.panel_line(rsi, "#A78BFA", 2, "solid")
plot.panel_hline(upper, "#EF4444", "dotted", "Overbought")
plot.panel_hline(50, "#4B5563", "dotted", "")
plot.panel_hline(lower, "#22C55E", "dotted", "Oversold")

let stretched_up: bool = rsi > upper && rsi > band_top
let stretched_down: bool = rsi < lower && rsi < band_bottom
plot.candle_color(stretched_up || stretched_down, stretched_up ? "#EF4444" : "#22C55E", 0.8)
plot.marker(ta.crossunder(rsi, upper), high, "#EF4444", "triangle", "above", 9, 3)
plot.marker(ta.crossover(rsi, lower), low, "#22C55E", "triangle", "below", 9, 3)
```

### SuperTrend candles

SuperTrend as two lines that break at each flip, candles coloured by the trend, a marker on every flip, and a label with how long the trend has run.

```xfs
//@xfscript 1
indicator("SuperTrend Candles")

let factor: float = input.float(3, "factor", "Factor", 0.5, 10, 0.1)
let atr_length: int = input.int(10, "atrLength", "ATR length", 1, 200, 1)
let up_color: color = input.color("#22C55E", "upColor", "Up trend")
let down_color: color = input.color("#EF4444", "downColor", "Down trend")

let trail: float = ta.supertrend(factor, atr_length)
let direction: float = ta.supertrend_dir(factor, atr_length)
let ready: bool = !is_nan(direction)
let up: bool = direction == -1
let tone: color = up ? up_color : down_color
let flipped: bool = ready && bar_index > 0 && direction != direction[1] && !is_nan(direction[1])

plot.line(up ? trail : nan, up_color, 2, "solid")
plot.line(ready && !up ? trail : nan, down_color, 2, "solid")
plot.candle_color(ready, tone, 0.85)
plot.marker(flipped && up, low, up_color, "triangle", "below", 10, 3)
plot.marker(flipped && !up, high, down_color, "triangle", "above", 10, 3)

let since: float = ta.barssince(flipped)
plot.label(str.concat(up ? "UP TREND " : "DOWN TREND ", format.fixed(nz(since, 0), 0), " BARS"), tone)
```

### Pulled liquidity from individual orders

How the resting book changed during each bar, from individual order events: size added and pulled on each side as a lean from -1 to 1, with a dot at every large pulled order. Without order data it falls back to the depth of book, and without depth to the tape, and it says which one it used.

```xfs
//@xfscript 1
indicator("Pulled Liquidity")

let big: float = input.float(50, "big", "Pulled order size to mark", 1, 100000, 1)
let bid_color: color = input.color("#F97316", "bidColor", "Pulled bids")
let ask_color: color = input.color("#38BDF8", "askColor", "Pulled offers")

let has_orders: bool = mbo.supported() && mbo.available()
let has_depth: bool = l2.supported() && l2.available()

let added_bid: float = mbo.added_size("BUY")
let pulled_bid: float = mbo.cancelled_size("BUY")
let added_ask: float = mbo.added_size("SELL")
let pulled_ask: float = mbo.cancelled_size("SELL")
let order_lean: float = ((added_bid - pulled_bid) - (added_ask - pulled_ask)) / (added_bid + pulled_bid + added_ask + pulled_ask)
let tape_lean: float = tape.delta() / tape.volume()

let lean: float = has_orders ? order_lean : has_depth ? l2.imbalance(10) : tape_lean
plot.histogram(nz(lean, 0), "#22C55E", "#EF4444")
plot.panel_range(-1, 1)
plot.panel_hline(0, "#6B7280", "dotted", "")

for i = 0 to (has_orders ? mbo.event_count() : 0) - 1 by 1 {
  if mbo.event_type(i) == "cancel" && mbo.event_size(i) >= big {
    plot.dot(true, mbo.event_price(i), mbo.event_side(i) == "BUY" ? bid_color : ask_color, 5)
  }
}

plot.label(str.concat("LEAN FROM ", has_orders ? "ORDERS" : has_depth ? "BOOK" : "TAPE"), "#9CA3AF")
```

## Before you paste

- Line 1 is //@xfscript 1.
- There is at most one indicator("...") call and it names the indicator.
- Every statement is on one line and no call is split across lines.
- Every series let and var is at the top level, never inside an if branch; a let inside a function or loop body is a local.
- Every := or += target is a var or a local.
- Every loop whose end can fall below its start says by 1, and no ta.*, input.* or per bar drawing is called inside a loop.
- Every name called appears in the builtin tables on this page.
- Every colour is a "#RRGGBB" or "#RRGGBBAA" string.
- Every argument count matches a signature on this page.
- No annotation says int unless the value is always a finite whole number.
- Numbers are turned into text with format.compact or format.fixed before str.concat.
- Any use of tape.price, tape.size, tape.side or tape.time is guarded by tape.prints_available().
- Any use of cell.* is guarded by cell.available().
- Any use of l1.*, l2.*, mbo.* or gex.* is guarded by supported() and available(), and the script still draws when that data is absent.
- Any use of an indicator read by name, such as bigtrades, effort_zones, important_levels or chart_stats, checks available(), because it needs that indicator on the chart, and a find(name) answer of -1 is handled.
- Quotes, live depth and gamma exist only on bars the chart saw since the script started; values meant to span the chart are carried forward in a var.
- plot.hbar and plot.table calls are not inside an if; their colours and text are picked with ? :.
- Effort Zones output is used on the bar it describes.
- Oscillators, ratios, counts and deltas go in the sub panel; only prices go on the price chart.
- There is at least one plot call, otherwise the indicator draws nothing.
