Skip to content
ChartDeck ChartScript docs Account Open the terminal

Style guide

How to name, declare, lay out, comment and color ChartScript code so it reads well, works in every chart theme and is ready to share.

ChartScript does not force a style on you: the compiler only cares that the code is valid. But a script is read far more often than it is written. You come back to it weeks later, other members read it when you publish it as open source, and a tidy script is much easier to debug. This page collects the conventions the official ChartDeck scripts follow, with the reasons behind them.

#The top of the file

Every script starts with the version header on line 1. Nothing may come before it, not even a blank line or a comment:

ChartScriptOpen in editor
//@chartscript 1
indicator("Header only", { overlay: true })
plot(close, "Close")

Right under the header, put a license line and a short description. The official scripts use an SPDX license identifier, a copyright line and a few lines that say what the script draws and how it calculates it:

ChartScriptOpen in editor
//@chartscript 1
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Your Name
//
// Range position: where the close sits inside the high-low range of the last N bars,
// from 0 (at the low) to 100 (at the high).
indicator("Range position", { shorttitle: "RngPos" })
const len = input.int(50, "Length", { minval: 2, maxval: 1000 })
const hi = ta.highest(high, len)
const lo = ta.lowest(low, len)
const pos = hi > lo ? (close - lo) / (hi - lo) * 100 : na
plot(pos, "Position", color.accent, 2)
hline(50, "Middle", color.muted, hline.style_dotted)

Why bother:

  • The SPDX line is machine-readable. It says in one line which license the code is under. When you publish an open-source script, the license you pick in the publish dialog is the one that counts, and keeping the same identifier in the file means a copy made from it carries the right terms. See Publishing and licenses.
  • The description is for people. Say what the script shows and any choice a reader would not guess (how an average is seeded, which side of a cross counts). Two or three lines are enough.
  • If your script started as a copy of someone else's open-source script, keep their header and the // Copied from ... line that "Make a copy" added. Removing them breaks the license.

#Naming

WhatStyleExamples
Variables and function parameterscamelCase, a nounfastLen, atrValue, upperBand
Booleansa question or stateisBull, hasVolume, crossUp
FunctionscamelCase, a verb or what it returnsbodyShare(), bandWidth()
Fixed values (a const with a literal value)UPPER_SNAKE_CASEMAX_LEN, GRP_STYLE
User types and enumsPascalCaseLevel, TrendState

A few rules come from the language itself:

  • Names are case-sensitive and can be up to 64 characters long. They start with a letter or _.
  • You cannot name a variable after a built-in namespace such as ta, math or color: that is error CS2003. Some namespace names are ordinary English words (basis is one), so if a plain name is refused, this is usually why.
  • Keywords such as const, let, function or if, and reserved words (var, strategy, class, self, undefined and a few more), cannot be used as names. Words that are keywords only in one position, such as state, type or method, still work as names, but a different name is clearer.

Prefer names that say what a value is over names that say how it was made: trendLine reads better than emaOfHl2. Short names like i or n are fine for loop counters and nothing else.

#Declarations: const first

Declare every variable with const unless you change it later. Use let only for a value you reassign on the same bar (a running result inside a loop, a value an if sets), and state only for a value that must carry over from one bar to the next. Then the keyword tells the reader at a glance how a name behaves:

  • const — computed on this bar, never changed;
  • let — computed on this bar, changed further down;
  • state — kept between bars.

Assigning to a const is CS2404, so the compiler also catches a value you changed by accident. The editor's quick fix turns the const into let when you did mean it.

#Layout and indentation

Blocks are made by braces { … }; indentation is only for the reader. Indent each level by 4 spaces, the way the editor and the formatter do, and put the opening brace at the end of the line that starts the block:

ChartScriptFragmentOpen in editor
let trend = 0
if (close > open) {
    trend = 1
} else if (close < open) {
    trend = -1
}
plot(trend)

Always write the braces, even for a single statement: if (c) x = 1 is valid, but a second line added later would not be part of the if.

Semicolons are optional. A line break ends a statement, so the official scripts write one statement per line and no semicolons. Use ; only to put two short statements on one line.

Group the script into the same four parts the official scripts use, in this order, with a blank line between them:

  1. the header, license and description,
  2. indicator(...) and the inputs,
  3. the calculations,
  4. the outputs: plots, fills, levels, drawings and alerts.

Reading top to bottom then answers "what can I change?", "what does it compute?" and "what does it draw?" in that order.

#Long lines

A long statement can continue on the next line. The statement goes on when:

  • the line ends inside an open (, [ or options object { … } of a call;
  • the line ends with an operator, a ,, ., ?, : or = (the statement is not finished yet);
  • the next line starts with ., ?, :, &&, ||, ??, a comparison, *, / or %.

A line that starts with (, [, +, - or ! always starts a new statement. So break a long sum after the +, not before it (a line starting with + or - is CS1055), and keep a history read x[1] on the same line as x. Indent continuation lines by one level (4 spaces):

ChartScriptFragmentOpen in editor
const score = ta.rsi(close, 14) / 100 +
    ta.mfi(hlc3, 14) / 100 +
    (close > ta.sma(close, 50) ? 1 : 0)
const strongUp = close > open
    && volume > ta.sma(volume, 20)
plot(score)
plotshape(strongUp, "Strong", shape.circle, location.bottom, color.up)

Spaces around operators and after commas make long conditions much easier to scan:

ChartScriptFragmentOpen in editor
const fastMa = ta.ema(close, 21)
const slowMa = ta.ema(close, 55)
const volAvg = ta.sma(volume, 20)

const strongUp = close > open && fastMa > slowMa && volume > volAvg * 1.5

plot(fastMa, "Fast", color.up, 2)
plotshape(strongUp, "Strong up bar", shape.circle, location.bottom, color.up)

#Compute first, decide later

Put every ta.* call on its own line at the top level, then use the results in your conditions. A ta.* call written inside a condition (or on the right of && / ||, which stop early) only updates on the bars where that code runs, so its average or highest value silently skips bars. The compiler warns about it:

ChartScriptWarning: CS2503FragmentOpen in editor
const up = close > open && volume > ta.sma(volume, 20)
plotshape(up, "Up on volume", shape.circle, location.bottom, color.up)

The fix is the layout above: const volAvg = ta.sma(volume, 20) on its own line, then volume > volAvg in the condition.

#Comments

Use // comments, which run to the end of the line. Block comments /* … */ also work and may span lines; keep them for temporarily switching off a few lines while you test.

  • Say why, not what. // the histogram fades while it shrinks back to zero helps; // set colour does not.
  • Mark the sections with a short comment such as // ---- Inputs.
  • Document your functions with //@ annotations on the lines just above them. //@function, //@param, //@returns, //@type, //@field, //@enum and //@variable are read by the compiler and attached to the declaration, so they are ready for the editor and for library documentation.
ChartScriptFragmentOpen in editor
//@function The share of the bar's range taken by its body, from 0 to 1.
//@param barOpen The bar's open.
//@param barHigh The bar's high.
//@param barLow The bar's low.
//@param barClose The bar's close.
//@returns na when the bar has no range.
function bodyShare(barOpen: float, barHigh: float, barLow: float, barClose: float) {
    const rng = barHigh - barLow
    return rng > 0 ? math.abs(barClose - barOpen) / rng : na
}

plot(bodyShare(open, high, low, close), "Body share")

#Inputs

Inputs are the part of your script other people touch most, so they deserve the most care. See Inputs for every input kind and Designing your script's settings for sections, rows and color roles.

  • Give every input a title in plain words, with the unit when there is one: "ATR length", "Band width (×ATR)".
  • Set sensible bounds with minval / maxval (and step for floats). Bounds stop impossible values before they reach your code, so you need fewer checks later.
  • Group related inputs with { group: … }. Store each group title in a const (const GRP_CALC: string = "Calculation") so a typo cannot split one group into two.
  • Put two short inputs on one row with the same inline key (a length and its color, for example).
  • Pass settings by name in the options object, the last argument ({ minval: 1, group: GRP_CALC }), rather than as a long row of positional values: the names say what each number means.
  • Order them the way a person thinks: what to calculate first, then what to show, then the style.
ChartScriptFragmentOpen in editor
const GRP_CALC: string = "Calculation"
const GRP_SHOW: string = "Display"

const len = input.int(20, "Length", { minval: 1, maxval: 500, group: GRP_CALC })
const mult = input.float(2.0, "Width (×stdev)", { minval: 0.1, maxval: 10, step: 0.1, group: GRP_CALC })
const showBands = input.bool(true, "Show bands", { group: GRP_SHOW, inline: "bands" })
const bandColor = input.color(color.accent, "", { group: GRP_SHOW, inline: "bands" })

const mid = ta.sma(close, len)
const dev = ta.stdev(close, len) * mult
plot(mid, "Middle", color.muted)
plot(showBands ? mid + dev : na, "Upper", bandColor)
plot(showBands ? mid - dev : na, "Lower", bandColor)

#Colors that work in every theme

ChartDeck charts can be dark, light or high-contrast, and members can pick color-blind-friendly up/down colors. A color that looks good on a dark chart may vanish on a light one. Three habits keep your script readable everywhere:

  1. Prefer the theme colors. color.up, color.down, color.accent, color.muted, color.text and color.grid follow the chart look automatically. Use color.up / color.down for anything bullish / bearish, so a member's own up/down choice carries over to your script. color.muted is right for zero lines and guide levels.
  2. When you need a fixed color, give one per theme with color.theme: color.theme(dark, light, hc). The light and high-contrast colors are optional and fall back to the dark one.
  3. Fade with transparency, not with a darker shade. color.new(color.up, 80) is a faint version of the up color on any background, and theme colors stay theme colors when you do this.

Avoid color.white and color.black for lines: one of them is invisible in each theme. They are fine for text on a colored label background. See Colors for the full list.

#A well-styled script

Here is everything above in one script. It plots a fast and a slow average colored by trend, fills the gap between them and marks the crosses:

ChartScriptOpen in editor
//@chartscript 1
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Your Name
//
// Trend Pulse: a fast and a slow EMA, coloured by which side the fast one is on, with an
// optional marker where they cross. The cloud between them is faint so the candles stay readable.
indicator("Trend Pulse", { shorttitle: "TPulse", overlay: true })

// ---- Inputs
const GRP_MA: string = "Averages"
const GRP_SHOW: string = "Display"

const fastLen = input.int(21, "Fast length", { minval: 1, maxval: 500, group: GRP_MA })
const slowLen = input.int(55, "Slow length", { minval: 2, maxval: 500, group: GRP_MA })
const src = input.source(close, "Source", { group: GRP_MA })
const showCloud = input.bool(true, "Fill between the averages", { group: GRP_SHOW })
const showCross = input.bool(true, "Mark crosses", { group: GRP_SHOW })

// ---- Calculations
const fastMa = ta.ema(src, fastLen)
const slowMa = ta.ema(src, slowLen)
const isBull = fastMa > slowMa
const crossUp = ta.crossover(fastMa, slowMa)
const crossDown = ta.crossunder(fastMa, slowMa)

// ---- Outputs
const trendColor = isBull ? color.up : color.down
const fastPlot = plot(fastMa, "Fast EMA", trendColor, 2)
const slowPlot = plot(slowMa, "Slow EMA", color.muted, 1)
fill(fastPlot, slowPlot, showCloud ? color.new(trendColor, 85) : na, "Cloud")
plotshape(showCross && crossUp, "Cross up", shape.triangleup, location.belowbar, color.up, { size: size.small })
plotshape(showCross && crossDown, "Cross down", shape.triangledown, location.abovebar, color.down, { size: size.small })

Notice what it does not do: no fixed hex colors, no ta.* calls inside conditions, no unexplained numbers, and no let where nothing changes. Every number a member might want to change is an input with a title and bounds.

#See also

  • Debugging: finding out why a script does not do what you expect.
  • Performance: keeping a script fast.
  • The official scripts: open a built-in indicator's settings on your chart and click View source to see these conventions in real code.

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