Strings and formatting
Text in ChartScript: string literals, template literals, joining text, the str.* functions, and formatting numbers and times.
Strings are the text your script shows: label text, table cells, log lines, alert messages. Most of the work is turning numbers into readable text, so this page spends most of its time on str.tostring and str.format.
#Writing strings
const a = "Double quotes"
const b = "Single quotes work the same"
const c = "Line one\nLine two" // \n starts a new line
const d = "Say \"hi\"" // escape the quote you opened with
const e = "Arrow \u2191" // a character by its Unicode number
plot(str.length(a + b + c + d + e))Escapes: \\ (backslash), \", \', \n (new line), \t (tab) and \uXXXX. A normal string must end on the line where it starts (CS1021).
For text over several lines, use a template literal between backticks (next section). It may span lines, and its value is exactly the text between the backticks, line breaks and spaces included:
const help = `Fast line: EMA of the close.
Slow line: SMA of the close.`
plot(str.length(help))Because the indentation of the following lines is part of the text, start them at the left edge.
A string can be at most 40,960 characters. A longer literal is refused when you compile (CS1023); a longer string built while the script runs stops it (CS5104).
All text a script outputs is shown as plain text. Markup such as HTML is displayed as typed, never interpreted.
#Template literals
A template literal is text between backticks, `…`. Inside it, ${expression} inserts the value of any expression as text:
const rsi = ta.rsi(close, 14)
const a = `RSI ${rsi}` // the same as "RSI " + str.tostring(rsi)
const b = `RSI ${str.tostring(rsi, "0.0")} on ${market.symbol}` // with a number format
const c = `Range ${high - low}, ${close > open ? "up" : "down"} bar`
log.info(b)
plot(rsi)${x}converts the value withstr.tostring(x)in its default format: up to 10 decimals for numbers,NaNforna. For another format, callstr.tostringinside the braces, as inbabove.- Any expression can go between the braces: arithmetic, a ternary, a function call.
- A template literal may span lines (see above). Escapes are the same as in other strings, plus
\`for a backtick and\$for a dollar sign that should not start${. - A template literal without
${…}is simply a string.
Like any text built from values, a template literal cannot be used where fixed text is required, such as a plot title (CS2103).
#Joining text
+ joins two strings, and += appends to a string variable:
let msg = "Close: "
msg += str.tostring(close)
msg += close > open ? " (up)" : " (down)"
plot(str.length(msg))+ only joins strings. A number has to be converted first (CS2008):
//@chartscript 1
indicator("Missing conversion")
const txt = "RSI is " + ta.rsi(close, 14)
plot(close)Write "RSI is " + str.tostring(ta.rsi(close, 14), "#.0"), or `RSI is ${str.tostring(ta.rsi(close, 14), "#.0")}`.
#Numbers to text: str.tostring
str.tostring(value) works for numbers, bools, strings and enum values. For numbers, a second argument controls the format:
| Format | 1234.5678 becomes | Notes |
|---|---|---|
| (none) | 1234.5678 | up to 10 decimals, no trailing zeros |
"#.##" | 1234.57 | # = a digit only if needed |
"0.00" | 1234.57 | 0 = always a digit: 42 gives 42.00 |
"#,###.00" | 1,234.57 | , adds thousands separators |
"#" | 1235 | rounds to a whole number |
format.mintick | rounded to the market's tick size | 102.456 on a 0.1-tick market gives 102.5 |
format.volume | 1.235K | short form with K, M, B |
format.percent | 1234.57% | adds a % sign; it does not multiply by 100 |
A few other cases: str.tostring(true) is "true", an int prints without decimals, and na prints as NaN. To show something friendlier for missing values, test first: na(x) ? "–" : str.tostring(x, "0.00").
const rsi = ta.rsi(close, 14)
const txt = na(rsi) ? "RSI: warming up" : "RSI: " + str.tostring(rsi, "0.0")
log.info(txt)
plot(rsi)#Placeholders: str.format and str.tpl
str.format fills numbered placeholders {0}, {1} … with the arguments that follow. A placeholder can carry a number format:
| Placeholder | Example output |
|---|---|
{0} | the value as str.tostring would write it |
{0,number,#.##} | 3.14 |
{0,number,0.000} | 0.123 |
{0,number,percent} | 0.12345 becomes 12% (here the value is multiplied by 100) |
{0,date,yyyy-MM-dd} | a time in milliseconds as a date, in UTC |
const line1 = str.format("{0}: {1,number,#.##} ({2,number,percent})", market.symbol, close, (close - open) / open)
log.info(line1)
plot(close)The same placeholder may appear more than once, and the arguments can be any type. str.tpl does the same job with the same {0} syntax. For short text a template literal is often easier to read; str.format is handy when each value needs its own format.
#Times to text: str.format_time
str.format_time(time, format, timezone) writes a timestamp as text. The default format is ISO style (2026-03-14T09:00:00+0000) in UTC.
| Pattern | Meaning |
|---|---|
yyyy | year |
MM | month, 01–12 |
dd | day of the month |
HH | hour, 00–23 |
mm | minute |
ss | second |
const stamp = str.format_time(time, "yyyy-MM-dd HH:mm")
log.info("Bar opened " + stamp + " UTC")
plot(close)#The str.* functions
| Function | Does | Example |
|---|---|---|
str.length(s) | number of characters | str.length("BTC") is 3 |
str.upper(s), str.lower(s) | change case | "BTC" / "btc" |
str.trim(s) | remove spaces at both ends | " x " gives "x" |
str.contains(s, part) | does s contain part? | true / false |
str.startswith, str.endswith | prefix / suffix test | str.endswith(market.symbol, "USDT") |
str.pos(s, part) | first position of part, from 0 | str.pos("hello", "l") is 2 |
str.substring(s, from, to) | part of a string; to is not included and may be left out | str.substring("hello", 1, 3) is "el" |
str.replace(s, old, new, n) | replace one occurrence (the nth, from 0) | |
str.replace_all(s, old, new) | replace every occurrence | "a-b-c" → "a+b+c" |
str.split(s, sep) | split into a string[] | str.split("a,b,c", ",") has 3 elements |
str.repeat(s, n, sep) | repeat with an optional separator | str.repeat("ab", 3, "|") is "ab|ab|ab" |
str.match(s, pattern) | the first match of a regular expression, or "" | str.match("abc123", "[0-9]+") is "123" |
str.tonumber(s) | text to a float, na if it is not a number | str.tonumber("3.5") |
To join an array of strings, use arr.join(", ").
#A complete example
This script puts a summary label on the newest bar: the symbol, the price rounded to the tick size and the change over a chosen number of bars.
//@chartscript 1
indicator("Price summary", { overlay: true })
const len = input.int(24, "Compare with N bars ago", { minval: 1 })
const chg = close - close[len]
const chgPct = chg / close[len] * 100
state info: label = na
if (barstate.islast) {
label.delete(info)
const sign = chg >= 0 ? "+" : ""
const txt = str.format(
"{0} {1}\n{2}{3,number,0.00}% over {4} bars",
market.symbol,
str.tostring(close, format.mintick),
sign,
chgPct,
len
)
info = label.new(
bar_index,
high,
txt,
{ style: label.style_label_down, color: chg >= 0 ? color.up : color.down, textcolor: color.white }
)
}
plot(close, "Close", color.muted)The text is built only on the last bar (barstate.islast), because it is only shown there. That keeps the script fast on long charts.
#Good to know
- Titles of plots, inputs and alert conditions are fixed text and cannot be built from values (CS2103). Changing text belongs in labels, tables, logs and
alert.sendmessages. - Building long strings on every bar of a long chart costs time. Build display text inside
if (barstate.islast)when only the last bar shows it. log.info("close {0}", close)takes the same{0}placeholders asstr.format, which is handy while debugging; the output appears in the editor's Console tab.