Skip to content
ChartDeck ChartScript docs Account Open the terminal

Known differences

Behavior that differs between Pine and ChartScript and can change your numbers: na, division, colors, drawings, requests, time and data.

Most of ChartScript behaves exactly like Pine version 6. This page collects the places where it does not, or where an older Pine version behaved differently, and that can change what your script draws. Each section shows a small Pine example and the ChartScript way to write it. The importer already handles many of these for you; the import report tells you which ones it touched.

The syntax is different too (ChartScript is written with braces, const / let, state, && and options objects), but that never changes a value: Pine to ChartScript names maps it line by line. This page is about behavior.

#Comparing with na

In Pine, x == na compiles but is always false, so the branch never runs. In ChartScript it is an error, and the editor offers na(x) as a quick fix.

Pine
//@version=6
indicator("Gap check")
if close[1] == na
    label.new(bar_index, high, "first bar")
ChartScriptError: CS2203
//@chartscript 1
indicator("Gap check", { overlay: true })
if (close[1] == na) {
    label.new(bar_index, high, "first bar")
}

Write it with na instead:

ChartScriptOpen in editor
//@chartscript 1
indicator("Gap check", { overlay: true })
if (na(close[1])) {
    label.new(bar_index, high, "first bar")
}

#A bool is never na

As in Pine version 6, a bool is always true or false. Version 4 and 5 scripts that let a bool be na need false instead (the importer does this and lists it under Meaning changed).

Pine
//@version=5
indicator("Flag")
bool armed = na
if ta.crossover(close, ta.sma(close, 20))
    armed := true
plot(armed ? 1 : 0)
ChartScriptOpen in editor
//@chartscript 1
indicator("Flag")
let armed: bool = false
if (ta.crossover(close, ta.sma(close, 20))) {
    armed = true
}
plot(armed ? 1 : 0)

The same rule makes a number used as a condition an error (CS2204): write !na(x) && x != 0 for the version 4 / 5 meaning, or better, the comparison you actually mean. A JavaScript habit does not help here either: if (x) with a number is an error, not a test for zero.

#Whole-number division

ChartScript follows the version 6 rule: dividing two whole numbers keeps the fraction when there is one. 5 / 2 is 2.5. In version 5 and older, 5 / 2 was 2. If your logic needs the whole number, say so with int().

Pine
//@version=5
indicator("Middle bar")
len = input.int(5, "Length")
mid = len / 2          // 2 in version 5
plot(close[mid])
ChartScriptOpen in editor
//@chartscript 1
indicator("Middle bar")
const len = input.int(5, "Length")
const mid = int(len / 2)  // 2: int() drops the fraction
plot(close[mid])

Where an int is required (an index, a history offset, an int variable), the importer adds int(…) for you. Elsewhere it keeps the fraction and lists the division under Meaning changed so you can decide.

#&& and || skip their right side

In ChartScript, as in Pine version 6, a && b (Pine's a and b) does not evaluate b when a is false, and a || b does not evaluate b when a is true. The result is always a bool. Versions 4 and 5 evaluated both sides on every bar. That matters when the right side is a stateful call such as ta.crossover: if it is skipped on some bars, it does not see those bars.

Pine
//@version=5
indicator("Filtered cross")
trendUp = close > ta.sma(close, 50)
signal = trendUp and ta.crossover(close, ta.ema(close, 10))
plotshape(signal, "Signal", shape.triangleup, location.belowbar)

Run the call on every bar by giving it its own line. The importer does exactly this for version 4 and 5 scripts:

ChartScriptOpen in editor
//@chartscript 1
indicator("Filtered cross", { overlay: true })
const trendUp = close > ta.sma(close, 50)
const crossed = ta.crossover(close, ta.ema(close, 10))
const signal = trendUp && crossed
plotshape(signal, "Signal", shape.triangleup, location.belowbar)

#Color constants

The named colors have the version 6 shades. Three of them were different before version 6:

NameChartScript (and version 6)Version 5 and older
color.red#F23645#FF5252
color.teal#089981#00897B
color.yellow#FDD835#FFEB3B

When it imports a version 4 or 5 script, the importer writes the old hex value in place of these three names, so the script looks the way its author saw it. If you prefer the newer shades, put the names back.

Pine
//@version=5
indicator("Bands", overlay = true)
plot(ta.highest(high, 20), "Top", color.red)
ChartScriptOpen in editor
//@chartscript 1
indicator("Bands", { overlay: true })
plot(ta.highest(high, 20), "Top", #FF5252)  // the version 5 shade of red

ChartScript also has theme colors that follow the chart's dark or light look: color.up, color.down, color.accent, color.muted, color.text and color.grid. Pine has no equivalent; they are worth using in new code.

#Drawing defaults

Labels, lines, boxes and tables use the version 6 defaults:

DrawingDefault colors
label.newblue bubble (color.blue), white text (version 5: black text)
line.newblue
box.newblue border and background, black text
table.cellblack text

A transparent color, or na, draws nothing: a label with { color: #00000000 } shows only its text. The importer adds { textcolor: color.black } to version 4 and 5 labels that did not set a text color.

Pine
//@version=5
indicator("Tags", overlay = true)
if ta.crossover(close, ta.sma(close, 20))
    label.new(bar_index, low, "up", style = label.style_label_up)
ChartScriptOpen in editor
//@chartscript 1
indicator("Tags", { overlay: true })
if (ta.crossover(close, ta.sma(close, 20))) {
    label.new(bar_index, low, "up", { style: label.style_label_up, textcolor: color.black })
}

#Display settings

display works as in version 6, including arithmetic such as display.all - display.status_line. A plot with { display: display.price_scale } appears only as a label on the price scale, not as a line in the pane: if a converted script seems to be missing level lines, check their display.

Pine
//@version=6
indicator("Level tags", overlay = true)
plot(ta.highest(high, 50), "High 50", display = display.price_scale)
plot(close, "Close", display = display.all - display.status_line)
ChartScriptOpen in editor
//@chartscript 1
indicator("Level tags", { overlay: true })
plot(ta.highest(high, 50), "High 50", { display: display.price_scale })
plot(close, "Close", { display: display.all - display.status_line })

#Other timeframes: no repainting by default

request.security returns, by default, the higher-timeframe bar as it develops on the live bar but the closed bar on history, so history and live disagree. ChartScript's request.tf asks you to choose, and its default never repaints:

atWhat each chart bar sees
request.close (default)the last higher bar that had closed, the same on history and live
request.startthe value at the start of the current higher bar; only for expressions known at that moment, which the compiler checks (CS3110)
request.developingthe higher bar so far, moving until it closes, on history and live

A lookahead that reads values not yet known on past bars cannot be written in ChartScript at all.

Pine
//@version=6
indicator("Daily close")
d = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)
plot(d)
ChartScriptOpen in editor
//@chartscript 1
indicator("Daily close", { overlay: true })
const d = request.tf("1D", close)  // the last closed day: never repaints
const live = request.tf("1D", close, { at: request.developing })
plot(d, "Closed day", color.gray)
plot(live, "Day so far", color.orange)

Another market uses request.market with a VENUE:SYMBOL id, and lower timeframes use request.lower_tf. More in Other timeframes and markets and Repainting.

#How many bars a script runs on

An imported script runs on the newest 5,000 bars unless its indicator(...) sets calc_bars_count (up to your plan's bars per run). Its requests load the span of that run plus their warm-up, not a history as deep as the plan. calc_bars_count on request.security and request.security_lower_tf is kept by the import, and in ChartScript it sets that context's depth (N bars of its timeframe, up to 20,000) where the original only limits it. Give it to a request whose result depends on how far back it started, such as zones built bar by bar:

Pine
//@version=6
indicator("Up 5m bars")
n = request.security(syminfo.tickerid, "5", ta.cum(close > open ? 1 : 0), calc_bars_count = 10000)
plot(n)
ChartScriptOpen in editor
//@chartscript 1
indicator("Up 5m bars")
const n = request.tf("5", ta.cum(close > open ? 1 : 0), { calc_bars_count: 10000 })
plot(n)

The value must be a constant: an input there is CS2103.

#Seconds timeframes

A script can request "1S", "5S", "10S", "15S", "30S" and "45S". What differs:

  • seconds history is short (the last 6 hours of a market ChartDeck records; na before that, said once in the legend row);
  • the sizes are the ones your plan can chart; another size in your script gives na with a note in the legend row;
  • a protected script and a script alert run on ChartDeck's servers, which serve seconds bars for a limited number of markets at once (past that the request gives na);
  • any other size ("20S") compiles with warning CS3103 and gives na.

Seconds timeframes has the details.

Pine
//@version=6
indicator("Fast close")
s = request.security(syminfo.tickerid, "30S", close)
plot(s)
ChartScriptOpen in editor
//@chartscript 1
indicator("Fast close")
const s = request.tf("30S", close)
plot(s)

A size that does not exist:

ChartScriptWarning: CS3103Open in editor
//@chartscript 1
indicator("Fast close")
const s = request.tf("20S", close)
plot(s)

#Time zones

Every time value in ChartScript is in UTC: time, hour, dayofweek, timestamp, str.format_time and session strings all use UTC unless you pass a time zone. Crypto markets trade around the clock, and ChartDeck's markets report UTC as their time zone (market.timezone). If your original script relied on another time zone (an exchange's local time, for example), pass it explicitly.

Pine
//@version=6
indicator("New York hour")
plot(hour(time, "America/New_York"))
ChartScriptOpen in editor
//@chartscript 1
indicator("New York hour")
plot(hour(time, "America/New_York"), "Hour in New York")
plot(hour, "Hour in UTC", color.gray)

#Sessions on 24/7 markets

Session strings work as in Pine ("HHMM-HHMM", optionally :1234567 for the days), and they are read in UTC. On a market that trades around the clock, the regular session is the whole UTC day: session.ismarket is always true, and session.isfirstbar marks the first bar of each UTC day. To mark a trading window such as the US cash hours, use a session string with time(); see Sessions.

Pine
//@version=6
indicator("US hours", overlay = true)
inUs = not na(time(timeframe.period, "1330-2000"))
bgcolor(inUs ? color.new(color.blue, 90) : na)
ChartScriptOpen in editor
//@chartscript 1
indicator("US hours", { overlay: true })
const inUs = !na(time(timeframe.period, "1330-2000"))  // 13:30-20:00 UTC
bgcolor(inUs ? color.new(color.blue, 90) : na)

#Timeframe strings

ChartScript names timeframes as version 6 does: "1D", "1W", "1M", and minutes as numbers ("60", "240"). timeframe.period on a daily chart is "1D", never "D". The importer rewrites version 4 / 5 comparisons such as timeframe.period == "D".

Pine
//@version=5
indicator("Daily only")
plot(timeframe.period == "D" ? close : na)
ChartScriptOpen in editor
//@chartscript 1
indicator("Daily only")
plot(timeframe.period == "1D" ? close : na)

#Market kind

syminfo.type becomes market.kind, which is "perp", "spot" or "other". It is never "crypto": a test for crypto becomes a test for perp or spot.

Pine
//@version=6
indicator("Kind")
plot(syminfo.type == "crypto" ? 1 : 0)
ChartScriptOpen in editor
//@chartscript 1
indicator("Kind")
const isCoin = market.kind == "perp" || market.kind == "spot"
plot(isCoin ? 1 : 0)

#Volume

volume is in base units, the coins traded (BTC on a BTCUSDT chart). A fixed volume threshold copied from a script tuned on another data source may need a new value. Comparing volume with its own average avoids the problem:

ChartScriptOpen in editor
//@chartscript 1
indicator("Volume spike")
const avgVol = ta.sma(volume, 20)
const spike = volume > 2 * avgVol
plot(volume, "Volume", spike ? color.orange : color.muted, { style: plot.style_columns })

#EMA and RMA start differently

ta.ema and ta.rma start from the simple average of the first values, the same as ChartDeck's built-in studies and alerts. Pine's documented EMA starts from the first value, so the first bars of an imported script's averages can differ a little (the importer says so in the report). After a few lengths they agree. For an EMA you can ask for the first-value start:

Pine
//@version=6
indicator("EMA")
plot(ta.ema(close, 21))
ChartScriptOpen in editor
//@chartscript 1
indicator("EMA", { overlay: true })
plot(ta.ema(close, 21), "EMA (average start)")
plot(ta.ema(close, 21, { seed: "first" }), "EMA (first-value start)", color.gray)

#Random numbers

math.random uses a fixed seed per script when you give none, so the chart and a server alert see the same numbers. Pine picks a different seed each time. Pass your own seed if you want a different sequence.

Pine
//@version=6
indicator("Noise")
plot(math.random())
ChartScriptOpen in editor
//@chartscript 1
indicator("Noise")
plot(math.random(0, 1, 7))

#Renamed but the same

These differ only in name; the importer renames them. See Pine to ChartScript names.

Pine
//@version=6
indicator("Ticks")
varip int ticks = 0
ticks += 1
alertcondition(ta.cross(close, open), "Cross", "Close crossed open")
plot(ticks)
ChartScriptOpen in editor
//@chartscript 1
indicator("Ticks")
live state ticks: int = 0
ticks += 1
alert.on(ta.cross(close, open), "Cross", "Close crossed open")
plot(ticks)

#Not available yet

  • Strategies: strategy.*, orders, positions and the strategy tester are not available yet. Imported strategies become indicators.
  • Fundamental and economic data: request.financial, request.economic, request.earnings, request.dividends, request.splits, request.quandl, request.seed and request.currency_rate give na.
  • Bid and ask: bid and ask give na.
  • Renko, Kagi, Line Break and Point & Figure markets (ticker.renko and the like) are not available yet.
  • The visible range: chart.left_visible_bar_time and chart.right_visible_bar_time are not available yet.

The ChartScript documentation is in English for now; translations will follow.