Синтаксис и типы
1Синтаксис и типы
Как устроен LootScript: выполнение по барам, объявление скрипта, пять форм присваивания, типы и квалификаторы, na, история x[n], операторы, условия, switch, циклы, функции и свои типы.
Эта страница — краткое описание языка: как выполняется скрипт, как объявлять переменные, какие есть типы, что такое na, как читать прошлые бары, как писать условия, циклы и функции. Все примеры — целые рабочие скрипты: их можно вставить в редактор и добавить на график.
Как выполняется скрипт
Всё, что написано на верхнем уровне файла, выполняется один раз на каждый бар — от самого старого загруженного к текущему, строка за строкой. Поэтому переменная в LootScript хранит не одно число, а значение на каждом баре, и к прошлым значениям можно обратиться.
indicator("Тело свечи")
body = close - open // считается заново на каждом баре
plot(body, "Тело", style = plot.style_columns)На текущем, ещё не закрытом баре скрипт пересчитывается с каждым новым тиком, начиная с состояния, которое было на закрытии предыдущего бара. Закрытые бары при этом не пересчитываются.
Объявление
Первая инструкция скрипта — ровно одно объявление: indicator(...), strategy(...) или library(...). Выше него могут стоять только комментарии и необязательная строка версии #version 1. Без объявления скрипт не скомпилируется с ошибкой «Нет объявления скрипта — начните с indicator(“Название”)».
#version 1
// Комментарии и строка версии могут стоять выше объявления.
indicator("Размах бара", shortTitle = "Размах", overlay = false)
plot(high - low, "Размах", style = plot.style_histogram)Главные параметры индикатора: title — название в списке скриптов, shortTitle — короткое имя для легенды, overlay — рисовать поверх цены (true) или в отдельной панели (false, по умолчанию). Параметры стратегии описаны на странице Стратегии и тестер.
Переменные и присваивание
| Форма | Что делает |
|---|---|
x = выражение |
Объявляет переменную. На каждом баре она считается заново. |
x := выражение |
Присваивает новое значение уже объявленной переменной. Тип должен совпадать. |
var x = выражение |
Объявляет переменную один раз, на первом баре. Значение переходит на следующие бары. |
varip x = выражение |
Как var, но значение сохраняется и между тиками текущего бара. |
[a, b, c] = функция(...) |
Раскладывает несколько результатов функции по переменным. |
Для изменения уже объявленной переменной есть и короткие формы: +=, -=, *=, /=, %=. Тип можно указать явно: float level = 0. Имена встроенных рядов и пространств (close, volume, ta и других) заняты — назвать так свою переменную нельзя.
Главное отличие от привычных языков — обычная переменная не накапливает значение между барами. Для счётчиков и сумм нужен var:
indicator("Счётчик зелёных свечей")
var greens = 0 // объявляется один раз и переживает переход к следующему бару
if close > open {
greens += 1
}
today = 0 // а эта переменная равна 0 на каждом новом баре
today := today + 1 // поэтому здесь всегда 1
plot(greens, "Зелёных с начала истории")varip нужен редко — например, чтобы считать тики внутри бара. На истории тиков нет, поэтому такое значение на прошлых барах будет другим, чем было вживую. Если значение varip попадает на график, редактор предупредит о перерисовке.
Типы
| Тип | Что хранит | Пример |
|---|---|---|
int |
целое число | 14, 1_000_000, 0xFF |
float |
дробное число | 2.5, .5, 1e-9 |
bool |
истина или ложь | true, false |
string |
текст | "BTC", 'BTC' |
color |
цвет с прозрачностью | #FF9800, #FF980080, color.teal |
Есть и составные типы: массивы array<float>, словари map<string, float>, матрицы matrix<float>, а также ваши собственные типы и перечисления (см. ниже) и графические объекты line, box, label, table.
Правила, о которые спотыкаются чаще всего:
- Тип фиксируется при объявлении. После
x = 1присвоитьx := 2.5нельзя:x— целое. Пишитеx = 1.0илиfloat x = 1. - Целое превращается в дробное само, обратно — только явно:
math.round,math.floor,math.ceil,math.trunc. - Деление целых — целое:
7 / 2равно3. Нужна дробь — сделайте одно из чисел дробным:7.0 / 2. - Возведение в степень
^всегда даёт дробное число:2 ^ 3равно8.0. - Строку нельзя сложить с числом.
"Цена: " + closeне скомпилируется — пишите"Цена: " + str.tostring(close). - Условие — только
bool.if volume { … }— ошибка; пишитеif volume > 0 { … }.
Квалификаторы: насколько значение постоянно
Кроме типа у каждого значения есть квалификатор — насколько оно постоянно во время работы скрипта. От слабого к сильному:
| Квалификатор | Когда известно значение | Пример |
|---|---|---|
const |
уже при компиляции | 14, "EMA" |
input |
после чтения настроек, дальше не меняется | input.int(14, "Период") |
simple |
при запуске, дальше не меняется | шаг цены инструмента |
series |
может меняться на каждом баре | close, bar_index |
На практике это важно в одном месте: периоды функций ta.* должны быть постоянными — литералом или настройкой. Иначе функции пришлось бы пересобирать своё состояние на каждом баре.
indicator("Период из настроек")
len = input.int(14, "Период", minval = 2)
plot(ta.rsi(close, len), "RSI")
// ta.rsi(close, bar_index) не скомпилируется:
// «length» в «ta.rsi» требует значение не слабее input, а передано seriesna — нет значения
na означает «значения нет»: не хватает истории для расчёта, биржа не отдаёт показатель, в данных дыра. na есть в любом типе.
- Арифметика с
naдаётna:na + 1— этоna. - Любое сравнение с
naложно, поэтомуif x > 0приx = naне выполнится. - Проверяйте отсутствие только через
na(x). Записьx == naкомпилятор отклонит с ошибкой «Сравнение с na всегда ложно — используйте na(x)». nz(x)подставляет 0 вместоna,nz(x, v)— значениеv. То же короче:x ?? v.naна графике — разрыв линии, а не ноль.
indicator("Средняя без пустого начала", overlay = true)
avg = ta.sma(close, 50) // на первых 49 барах — na
line1 = na(avg) ? close : avg // проверка через na(x)
line2 = avg ?? close // то же короче
plot(line2, "Средняя или цена", color = color.orange)История: x[n]
x[n] — значение x на баре, который был n баров назад; x[0] — текущее. Работает с любым рядом, в том числе с вашими переменными.
indicator("Наклон средней")
ma = ta.ema(close, 20)
slope = ma - ma[1] // изменение за один бар
plot(slope, "Наклон", style = plot.style_histogram,
color = slope >= 0 ? color.teal : color.red)- Если
nбольше числа баров до начала истории, результат —na, а не ошибка. nможет меняться от бара к бару:close[len]при переменномlenдопустимо.- Отрицательный индекс — ошибка: заглядывать в будущие бары нельзя.
- Настраивать глубину истории не нужно: скрипту доступна вся история, загруженная на график.
Операторы
От высшего приоритета к низшему:
| Приоритет | Операторы |
|---|---|
| 1 | x[n], вызов f(…), доступ через точку a.b |
| 2 | ^ — степень, вычисляется справа налево |
| 3 | унарные -, +, not |
| 4 | *, /, % |
| 5 | +, - |
| 6 | <, >, <=, >= |
| 7 | ==, != |
| 8 | and |
| 9 | or |
| 10 | ?? — замена na |
| 11 | ? : — условное выражение |
- Сравнения не сцепляются: вместо
a < b < cпишитеa < b and b < c. andиorне вычисляют правую часть, если ответ уже ясен по левой.- Степень старше унарного минуса:
-2 ^ 2равно-4. Нужно4— пишите(-2) ^ 2. - Дробные числа сравнивайте с допуском:
math.approx(a, b), потому что0.1 + 0.2 == 0.3ложно.
Условия
Блоки всегда в фигурных скобках. if бывает инструкцией и выражением; для простого выбора из двух есть ? :.
indicator("Направление бара")
dir = 0
if close > open {
dir := 1
} else if close < open {
dir := -1
}
// if как выражение: обе ветки обязательны
bodySize = if close >= open { close - open } else { open - close }
// условный оператор для простых случаев
barColor = dir > 0 ? color.teal : color.red
plot(bodySize, "Тело", style = plot.style_columns, color = barColor)switch
switch выбирает первую подходящую ветку. Форма с объектом сравнивает его со значениями веток, форма без объекта проверяет условия по порядку. Ветка => без условия — ветка по умолчанию. Проваливания в следующую ветку нет. Когда switch возвращает значение, ветка по умолчанию обязательна.
indicator("Средняя на выбор", overlay = true)
kind = input.string("EMA", "Тип средней", options = ["EMA", "SMA", "WMA"])
ma = switch kind {
"EMA" => ta.ema(close, 20)
"SMA" => ta.sma(close, 20)
=> ta.wma(close, 20)
}
zone = switch {
close > ma * 1.02 => 1
close < ma * 0.98 => -1
=> 0
}
plot(ma, "Средняя", color = zone == 1 ? color.teal : zone == -1 ? color.red : color.gray)Циклы
for i in a .. b перебирает числа от a включительно до b не включительно. Шаг задаётся словом by, в том числе отрицательный. Переменная цикла существует только внутри цикла и не меняется в теле.
indicator("Зелёные свечи за N баров")
n = input.int(10, "Сколько баров", minval = 1, maxval = 200)
greens = 0
for i in 0 .. n { // i = 0, 1, …, n − 1
if close[i] > open[i] {
greens += 1
}
}
plot(greens, "Зелёных", style = plot.style_columns)indicator("Виды циклов")
total = 0
for i in 0 .. 10 by 2 { // 0, 2, 4, 6, 8
total += i
}
for i in 10 .. 0 by -1 { // 10, 9, …, 1
if i % 3 == 0 {
continue // сразу к следующему шагу
}
total += i
}
var closes = array.new<float>(0)
closes.push(close)
if closes.size() > 20 {
closes.shift()
}
sum = 0.0
for v in closes { // по элементам массива; for [i, v] in … — с индексом
sum += v
}
k = 0
while k < 100 {
k += 7
if k > 50 {
break // выход из цикла
}
}
plot(sum / closes.size(), "Средняя из массива")Бесконечный цикл терминал не повесит: у скрипта есть бюджет операций на бар и лимит времени. Превысив их, скрипт остановится с сообщением «Скрипт исчерпал бюджет операций на бар — упростите цикл» или «Скрипт превысил лимит времени на проход».
Функции
Функция объявляется словом func. Короткая форма — выражение после =>, длинная — тело в скобках с return. Типы параметров можно не указывать — они выводятся из вызовов, а у параметров могут быть значения по умолчанию. Аргументы передаются по порядку или по имени.
indicator("Z-оценка объёма")
func barRange(h, l) => h - l
func zscore(series float src, input int len) => float {
m = ta.sma(src, len)
s = ta.stdev(src, len)
return s == 0 ? na : (src - m) / s
}
func scaled(x, k = 2.0) => x * k
z = zscore(volume, 50)
plot(z, "Z объёма", style = plot.style_columns, color = z > 2 ? color.orange : color.gray)
plot(scaled(barRange(high, low), k = 0.5), "Полразмаха")- Рекурсии нет. Функция не может вызывать саму себя, ни напрямую, ни через другую функцию. Повторение пишется циклом.
- Состояние — у каждого вызова своё. Если внутри функции есть
ta.*илиvar, два вызова одной функции ведут независимые расчёты:
indicator("Два вызова одной функции", overlay = true)
func smooth(src, len) => ta.ema(src, len)
fast = smooth(close, 10) // своя средняя
slow = smooth(close, 30) // другая, независимая средняя
plot(fast, "Быстрая", color = color.teal)
plot(slow, "Медленная", color = color.orange)Функции, нужные в нескольких скриптах, выносят в библиотеку: скрипт с объявлением library("Имя", 1), перед функциями — слово export, в другом скрипте — import Имя/1 as имя. Библиотека берётся из вашей папки скриптов.
Несколько результатов
Некоторые функции возвращают сразу несколько рядов: ta.bb, ta.macd, ta.kc, ta.dmi, ta.supertrend и другие. Их результат раскладывается по переменным в квадратных скобках; число имён должно совпадать с числом результатов.
indicator("Полосы Боллинджера", overlay = true)
[basis, upper, lower] = ta.bb(close, 20, 2.0)
plot(basis, "Середина", color = color.gray)
plot(upper, "Верх", color = color.teal)
plot(lower, "Низ", color = color.teal)Свои типы и перечисления
type собирает несколько полей в одну структуру. Объект создаётся через Имя.new(...), поля читаются и меняются через точку.
indicator("Последний локальный максимум", overlay = true)
type Swing {
int barIndex
float price
}
var Swing last = Swing.new(barIndex = 0, price = high)
ph = ta.pivothigh(high, 5, 5)
if not na(ph) {
last.barIndex := bar_index - 5
last.price := ph
}
plot(last.price, "Максимум", style = plot.style_stepline, color = color.orange)enum — закрытый список вариантов с подписями. Удобнее строк: опечатка в имени варианта — ошибка компиляции, а не тихо не сработавшее условие. Вместе с input.enum перечисление превращается в выпадающий список в настройках — см. Настройки и отрисовка.
indicator("Режим рынка")
enum Regime {
trend = "Тренд"
flat = "Флэт"
}
regime = math.abs(close - close[20]) > ta.atr(14) * 3 ? Regime.trend : Regime.flat
plot(regime == Regime.trend ? 1 : 0, "Тренд", style = plot.style_stepline)Функции ta.* внутри условий
Функции вроде ta.ema, ta.rsi или ta.atr помнят своё состояние с прошлого бара. Если бы такой вызов выполнялся только на части баров — внутри if или одной ветки ? :, — его расчёт пропускал бы бары и тихо давал неверные числа.
LootScript этого не допускает: компилятор сам выносит каждый вызов ta.* из условия и выполняет его на каждом баре, а условие лишь выбирает, использовать ли результат. Поэтому такой код считает RSI правильно:
indicator("RSI только на зелёных барах")
r = close > open ? ta.rsi(close, 14) : na
plot(r, "RSI", style = plot.style_circles)Вынести вызов нельзя, только если его аргумент сам рождается внутри ветки. Тогда компилятор не угадывает, а сообщает: «Аргумент «len» нельзя вычислить вне ветки — вынесите вызов ta.* на верхний уровень». Решение — посчитать аргумент до условия:
indicator("Аргумент до условия")
// Так нельзя — len объявлена внутри ветки:
// if close > open {
// len = 10
// e = ta.ema(close, len)
// }
len = 10
e = ta.ema(close, len)
plot(close > open ? e : na, "EMA на зелёных барах", style = plot.style_circles)Комментарии и перенос строк
//— комментарий до конца строки,/* … */— блочный, блоки можно вкладывать друг в друга.- Инструкция заканчивается с концом строки, точка с запятой не нужна.
- Длинное выражение продолжается на следующей строке, если строка оборвана внутри скобок, заканчивается оператором или следующая строка начинается с оператора,
?или:.
indicator("Перенос строк")
typical = high
+ low
+ close
signal = close > open
? typical / 3
: na
plot(signal, "Типичная цена на зелёных барах", style = plot.style_circles)Дальше
https://docs.lootx.trade/es/loot/syntax