Кратко расскажу принципы и некоторые нюансы работы с графиком в Qiuk в плане создания своего индикатора (здесь и далее – подразумевается использование языка программирования Lua). В конце текста изначально хотел прикрепить видео с демонстрацией и краткими пояснениями работы моих индикаторов, но решил сделать это во второй части статьи, чтобы совместить просто иллюстрацию с небольшим анализом фьючерсов и акций.
На полноту изложения вопроса по работе с индикаторами на графике Quik не претендую. Информация будет полезна интересующимся данной темой, не рассчитана на профессионалов (которые и так все знают, умеют и реализовали – свято в это верю), но все же предполагает наличие определенного уровня знаний Lua.
Зачем мучиться со своими индикаторами? Конечно, в этом нет смысла, если вас устраивают стандартные индикаторы или отсутствуют самостоятельные подходы (методы) торговли, либо визуализация вам в принципе не требуется (не интересна).
В моем случае мне банально захотелось сделать визуализацию своего метода прогнозирования экстремумов цены следующего интервала. Алго этого метода уже давно было реализовано. Предполагал, что сварганю все красиво максимум за день, ведь работающий код уже есть, оставалась только связка с графиком в Quik и наведение красоты. Но Quik внес свои коррективы в этот план: чтобы окончательно все сделать, разобраться в незадокументированных нюансах работы с графиком в Quik потребовалось больше времени. Про некоторые «грабли» расскажу, а также изложу основные азы для тех, кто заинтересуется вопросом в прикладном плане.
Основы создания индикаторов в Quik изложены в соответствующих руководствах разработчиков (полнота и доступность изложения в них — на совести разработчиков), переписывать все нет смысла, но основные моменты изложу для полноты содержания и восприятия.
Структура кода любого индикатора должна содержать три составляющих: глобальную таблицу с именем Settings, функции Init() и OnCalculate(), а файл с кодом размещается в папке LuaIndicators (создается вами если отсутствует), которая в свою очередь размещается внутри папки, где установлен сам Quik. Если отсутствует одна из вышеуказанных частей, либо в коде допущена ошибка, то в списке добавляемых на график индикаторов (в режиме редактирования графика – редактировать – добавить индикатор) вы свой индикатор не увидите. Кратко про каждую составляющую:
1). Settings =
{
Name = " Имя индикатора ",
line = {
{Name = «Название линии»,
Type = (Тип линии),
Width = (Толщина линии),
Color = (Цвет линии)
}
}
Таблица Settings задает:
— имя индикатора, под которым вы найдете его в списке всех индикаторов (избегайте уже испольованных названий). В этой же области (между элементами Name и line) вы можете определить дополнительно свои параметры (не просто используемые в расчете, а которые вы предполагаете изменять)
— тип линии можно задавать следующим:
Type = TYPE_LINE --- линии
Type = TYPE_HISTOGRAM --- гистограммы
Type = TYPE_POINT --- точки
Type = TYPE_DASHDOT --- точка-тире
Type = TYPE_DASH --- тире
Type = TYPE_TRIANGLE_UP --- треугольники вверх
Type = TYPE_TRIANGLE_DOWN --- треугольники вниз
— толщина линии (число)
— цвет линии – в формате модели RGB
Толщину, тип и цвет линий удобно выбирать в режиме редактирования настроек индикатора (после его добавления) и потом внести выбранные значения в код.
Поле line может содержать несколько элементов (по числу линий, используемых в индикаторе), структура которых аналогична приведенной выше.
2). function Init()
interval_1 = getDataSourceInfo().interval — не обязательная строка (см. текст ниже)
return #Settings.line
end
Эта функция инициализирует скрипт индикатора и срабатывает в момент его добавления. Возвращается число линий индикатора, перечисленных в Settings.Line и управление передается функции function OnCalculate(index).
Здесь есть первый нюанс. Когда вы меняете на графике инструмент (график «заякорен») или интервал — вновь срабатывает Init() и график автоматически подписывается на новые данные. Но если ваша расчетная часть кода использует параметры, существенно зависимые от верности указания кода актива (к примеру, шаг и точность цены, стоимость шага цены) вам надо считать класс и код инструмента, интервал. Это позволяет сделать стандартная функция getDataSourceInfo(), возвращающая эти параметры: interval; class_code и sec_code. Но считать изменение этих параметров в скрипте индикатора сразу же невозможно, предположу – что из-за паузы подгрузки данных. По факту, getDataSourceInfo в Init() возвращает только интервал (причем не стабильно верно).
Чем это чревато – когда расчетная часть кода использует параметры, зависимые от верности указания кода актива, можно много чего интересного насчитать с неверными interval, class_code и sec_code. Для проверки такого «нежданчика» советую ставить вторую строчку (с interval_1, и объявить ее глобальной), которая будет вторично проверяться на верность в OnCalculate(). Считывать в Init() кода и класса актива бессмысленно, поскольку они в Init() вообще не определяются, это надо делать в OnCalculate().
3). function OnCalculate(index)
В функции происходит основная магия рисования индикатора. В ней перебираются данные TOHLCV (начиная с наиболее поздних) и по умолчанию отображается график цены. Чтобы на графике отобразился еще и ваш индикатор, необходимо чтобы OnCalculate() вернула значения индикатора, причем в порядке их перечисления в Settings.Line (если выводимых на график значений несколько). Естественно, до этого значения индикатора должны быть рассчитаны.
Городить существенные расчеты внутри функции разработчики не рекомендуют. Впрочем, все равно более-менее сложные расчеты реализовываются в разрезе функций (или методов).
Из нюансов.
Считывание из getDataSourceInfo информации о интервале (interval), коде класса (class_code) и коде инструмента (sec_code) в OnCalculate при index = 1 нестабильно, я считываю их при index = 2 и проверяю верность считанного в Init интервала. Зачем так заморачиваться? Потому что в расчетной части используются такие параметры инструмента, как точность и шаг цены, которые определяются только при известных классах кода и инструмента. Если вам не требуются специфические параметры инструмента, то нет смысла делать эту проверку.
В руководстве сказано, что функции со сложными вычислениями следует размещать в отдельном файле, который размещен в ином каталоге, чем папка индикатора. Разработчикам виднее, в моем случае более 600 строк кода индикатора с более 30 функциями и итерационными расчетами прекрасно работают, будучи размещенными в одном файле с вышеперечисленными функциями.
Работа с данными TOHLCV имеет особенности: вне OnCalculate у Т() нет даты, только время, хотя Т(index) непосредственно в OnCalculate() является нормальной полной таблицей. Еще один нюанс — вне OnCalculate() получение значений OHLCV нестабильно при смене инструмента или интервала. Причину нестабильности не понял, пришлось с данными работать через таблицу данных (getCandlesByIndex), а не напрямую с источником данных (OHLCV). Этот вариант немного более извращенный в плане кода, но основное его неудобство не в этом – надо помнить о необходимости присвоения графику идентификатора, который служит названием таблицы, содержащей данные TOHLCV (график – редактировать — дополнительно). Связанное с этим неудобство заключается еще в том, что разные графики не могут иметь один идентификатор, а значит для добавления на другой график индикатора, работающего с таблицей источника данных, необходимо менять название идентификатора как в коде, так в самом графике. Поясню на примере: в Quik созданы два графика, работающих со срочным и фондовым рынками (каждый график «заякорен» к одному классу активов). Чтобы добавить один и тот же индикатор на оба графика, приходится сделать два файла с кодом индикатора, отличающиеся только названиями индикатора и идентификатора, и соответственно добавлять на графики разные по названию индикаторы. Этого геморроя не было бы, если бы можно было работать стабильно напрямую с TOHLCV.
Небольшие советы:
1) задавать в Settings параметр, который будет разрешать/запрещать вывод вашего индикатора на график. Например, для этого у меня задается параметр Settings.Show_Ind, который управляет выводом значений индикатора (=1 – есть вывод, =0 индикатор не выводится). Управление выводом в этом случае простое: OnCalculate() при Settings.Show_Ind=1 возвращает значения индикатора, а при Settings.Show_Ind=0 – нет; Такое управление позволяет простым изменением параметра Settings.Show_Ind при редактировании его значения (график – редактирование) убрать индикатор с графика не удаляя сам индикатор, когда вывод его на график не требуется (см. пример кода ниже);
2) задавать в Settings параметр, который будет задавать дату, с которой рассчитывать и (или) показывать на графике индикатор (под датой здесь понимается дата+время, т.е. таблица). Для этого у меня задается параметр Settings.From_Date (например, он равен «20.07.2023» (в Lua нет типа данных «дата»). А в коде вы переводите данное значение в формат POSIX (Unix-время, т.е. число секунд, прошедших с полуночи 1 января 1970 г.). Такое представление времени упрощает сравнение времени разных дат до банального сравнения двух чисел (к примеру, сравнение разных дат в формате, который я видел в советах на сайте – приведение даты 10.07.2023 к виду 10072023 гораздо сложнее, как минимум год здесь надо будет обрабатывать отдельно). Делается перевод даты и времени несложно, в приведенном выше примере это будет так:
Cur_Date = {} — не забываем объявить массив
Cur_Date.day = tonumber(string.sub(Settings.From_Date, 1, 2 )) вырезали день из From_Date
Cur_Date.month = tonumber(string.sub(Settings.From_Date, 4, 5 )) вырезали месяц из From_Date
Cur_Date.year = tonumber(string.sub(Settings.From_Date, 7, 10)) )) вырезали год из From_Date
Start_Date = os.time(Cur_Date) число (количество секунд) = дата и время в формате POSIX
(Если есть необходимость уточнять дату до часов и минут, то аналогично формируются элементы массива с часом и минутой).
Небольшое уточнение: данные OHLCV с МБ – естественно по Московскому времени, а перевод в POSIX делается по локальному (местному времени, т.е. времени вашего компьютера). Чтобы все было совсем верно, следует сдвинуть значение Start_Date на разницу часовых поясов вашего региона с Москвой. У меня это +4 часа или 4*60*60 секунд.
Управление выводом (и расчетами) в этом случае аналогичное: если os.time(T(index)) > Start_Date (т.е. дата индекса позже заданной) то рассчитываем значения индикатора и возвращаем их значения, иначе – не возвращаем (не считаем). В моем случае расчет всех свечек, имеющихся в источнике данных (как известно, это минимум 3000 свечек (на коротких интервалах больше) + сохраненные данные предыдущих торговых сессий, доходило до более 40 тыс. свечек) при итерационных расчетах приводил к заметной (несколько секунд) задержке при выводе индикатора на график (смена интервала, инструмента). Поэтому при сложных расчетах такое управление с начальной датой существенно снизит «залипание» графика при смене инструмента или интервала, и собственно — самого Quik.
3) задавайте в Settings все те параметры, которые вы собираетесь «качать». Это позволит простым их редактированием в свойствах графика увидеть результат изменений в индикаторе. Поскольку изменение таких параметров в самом коде приведет к изменению на графике только после удаления индикатора и добавления его вновь (сам код считывается только один раз – при запуске Quik если индикатор уже добавлен на график, или при добавлении индикатора).
Для примера: Стандартный индикатор скользящей SMA с возможностью изменения: периода, источника данных (O, H, L, C, V и их сочетания) для средней и управлением вывода значений индикатора (авторство – разработчиков Quik, мною исправлена пара «очепяток» и добавлено управление выводом). Желающие могут скопировать код и вставить его в размещенный в соответствующей папке (см. выше) файл с расширением «lua», в списке индикаторов вы увидите этот индикатор под именем «Test_SMA».
Settings = {
Name = «Test_SMA»,
mode = «C»,
period = 5,
Show_Ind = 1,
line = {
{Name = «Test_SMA»,
Color = RGB(255, 0, 0),
Type = TYPE_LINE,
Width = 2 }
}
}
function Init()
#Settings.line
end
function OnCalculate(idx)
local per = Settings.period
local mode = Settings.mode
local lValue = iValue
if idx >= per then
local ma_value=0
for j = (idx-per)+1, idx do
ma_value = ma_value+dValue(j, mode)
end
if Settings.Show_Ind == 1 then
return ma_value/per
end
else
return nil
end
end
function dValue(i,param)
local v = param or «C»
if v == «O» then
return O(i)
elseif v == «H» then
return H(i)
elseif v == «L» then
return L(i)
elseif v == «C» then
return C(i)
elseif v == «V» then
return V(i)
elseif v == «M» then
return (H(i) + L(i))/2
elseif v == «T» then
return (H(i) + L(i)+C(i))/3
elseif v == «W» then
return (H(i) + L(i)+2*C(i))/4
else
return C(i)
end
end
А ты, я смотрю, что-то разразился целой пачкой статей. Интересные мысли — не совсем согласен по сути и в терминологии… Но здорово — вот таких постов тут дефицит. Можно сказать, что я прервал молчание как минимум частично из за твоего цикла — заразное это дело )))
з.ы. вот у меня и дозревает)
Может есть какой способ чтобы это исправить?
С такой ситуацией не встречался. Т.е. индикатор добавлен на график и работает без проблем, а после выхода из квика и нового его запуска — индикатор уже добавлен на графике но не выводится? Рассуждаем логически: новое включение квика отличается от уже запущенного квика только считыванием сохраненных настроек. По идее такого глюка не должно быть. А что понимается под словами «нужно обновить график»? Не понял это, график же обновляется автоматически при изменении цены… Или это про режим редактирования графика «график-обновить»?
Возможны нюансы в плане кода. Надо делать печать промежуточных результатов для локализации ошибки (я использую для этого dbgview). Из вопроса не понятно — в коде через getCandlesByIndex берутся данные из двух таблиц по двум инструментам или берется таблица готового спреда? В первом случае много нюансов, смотря как все реализовано.
Именно!
Я тоже dbgview пользую.
А что там можно локализовывать? Квик ошибок не выдает.
Торги еще не начались. Окно с графиком пустое. после начала торгов индикатор начинает отрисовывать спред за сегодняшний день. Спред за прошлые дни только после ручного обновления появляется.
Данные да из двух таблиц. Делал и из 3х, 4х. Все едино.
Мало того. Иногда при новом запуске Квика спред может и сам прорисоваться!
Насчет локализации ошибок. Если расчетная часть большая, то нужно для начала отследить по мере выполнения кода верность используемых данных и возвращаемых значений индикатора. Если вылезет место, где они «ломаются» — тогда проще будет думать о причине. На словах трудно что-то советовать, код надо смотреть-анализировать…
Settings =
{
Name = «Spread»,
Symbol1 = «s1»,
K1 = 3,
Symbol2 = «s2»,
K2 = 4,
line=
{
{Name = «Spread», Color = RGB(0, 0, 255), Type = 1,Width = 1}
}
}
function Init()
return 1
end
function OnCalculate(index)
local indx1 = (getCandlesByIndex(Settings.Symbol1, 0, index-1, 1)[0].close or 0) * Settings.K1
local indx2 = (getCandlesByIndex(Settings.Symbol2, 0, index-1, 1)[0].close or 0) * Settings.K2
return indx1-indx2
end
добавьте вначале if index == 0 then return nil
Но проблемы это не решает. Тут какие-то более хитрые проверки нужны, раз он то появляется, то нет.
Запись
getCandlesByIndex(Settings.Symbol1, 0, index-1, 1)[0].close
что-то недопонял )). Close = getCandlesByIndex(Settings.Symbol1, 0, index-1, 1).close — это значение С, а [0] — что такое? Ноль или О?
И потом — обновление графика задает OnCalculate при изменении цены. В данном случае — цены какого инструмента? Если подписка на ИД сделана на двух дополнительных графиках, то на графике со спредом то изменения цены нет! Вот он и не активируется. Один из инструментов следует подписать на графике спреда, оставить дополнительный график для подписки на второй инструмент, а вывод значений первого инструмента и спреда разнести по разным осям (правая/левая). Думаю, тогда не надо будет обновлять график )))
Я обычно оба инструмента строю в одном окне друг под другом, и ниже в том же окне график спреда.
Для продолжения обсуждения — напишите конкретно как у вас реализован график. Из последнего вашего сообщения я понял так: окно графика имеет три окна по вертикали, в двух выводятся данные по инструментам, а в третьем окне рисуется спред. Все верно?
Теперь — зачем такие подробности: Когда вы добавляете индикатор, то срабатывает Init, которая в свою очередь инициализирует OnCalculate. Такая же последовательность происходит при изменениях интервала или инструмента, а также при обновлении графика. Эта логика верна для каждого окна графика отдельно. А в процессе работы квика инициализация OnCalculate происходит при изменении цены основного источника данных текущего окна графика. А у вас, насколько я понял, в этом окне только расчетный спред. Т.е. «с точки зрения» квика в этом окне цена не меняется, соответственно нет причин для инициализации OnCalculate. Вот суть моего предположения. Это «отлавливается» промежуточной печатью — проверкой потока данных. Из этих соображений и я предложил линию спреда разместить в окне с одним из инструментов. Тогда основной ИД будет меняться и автоматически запускать перерисовку графика.
Про код комментировать не буду, как я понял вы привели не окончательный вариант, либо его часть.
Да. Изредка бывает, что в одном окне 2 графика инструментов, а ниже спред.
Это не так. Индикатор всегда привязан к какому либо графику инструмента. И в свойствах графика Квик это явно выводит. К тому же эта версия опровергается тем, что я писал выше. Бывает, что при загрузке Квика график спреда иногда появляется. Но даже если не появился, то после начала торгов он начинает отображаться, но только за текущий день. За предыдущие дни только после принудительного обновления. Значит OnCalculate таки срабатывает.
Промежуточная печать это хорошо, но что и в каком месте тут печатать не понятно.
Сделал сегодня подобный индикатор спреда и на практике понял о чем был вопрос, собственно. Со слов я проблему недооценил и неверно понял. У меня эта проблема выражается в том, что при смене интервала спред не прорисовывается верно. Но после обновления графика или переустановки индикатора- все отображается правильно. Причину выявил — не обновляется ИД второго инструмента. Перемещение индикатора и инструментов по окнам ничего не дает. В рамках getCandlesByIndex вроде испробовал все хитрости, какие пришли в голову, не помогло. Как принудительно заставить провести обновление ИД второго инструмента — способа не нашел. Погуглил — не мы одни с этой проблемой столкнулись. Похоже на еще один «косяк» квика. Все же видимо надо в таком случае подписываться на ИД второго инструмента самостоятельно.
На заметку: в общем случае (когда у какого-то инструмента есть свечки без сделок) — требуется более сложный код для отсечения таких моментов и согласования по времени значений, формирующих спред. Если будет желание — могу сбросить.
По поводу кода согласования, было бы интересно ознакомится.
Но снова вопрос терминологии. Что есть свечки без сделок? Неужели у Квика есть такие? Я всегда думал, что нет сделок — нет свечки. Если сделки за период свечки все по одной цене, то она вырождается в точку.
Или имелось ввиду просто пропуски свечек у одного из инструментов?
Тогда нужно понимание и соответствующий алгоритм того, что мы будем рисовать на месте этих пропусков.
В ИД действительно нет свечек без сделок. Но когда используется индикатор, то индекс в OnCalculate — это просто порядковый номер интервала. К примеру, спред между фьючерсами Сбера по 9 и 12 контрактам — там часто по 12-му не было сделок, а по 9-му были. На графике индекс изменяется по основному ИД — это 9-ый контракт. И получается, что у таких свечкек есть на одном индексе только данные одного контракта. Если брать данные просто по индексу, получается несоответствие. В этих местах разумно оставлять предыдущее значение спреда. А соответствие данных обеспечивается поиском свечки второго инструмента с одинаковым временем, соответствующим времени свечки первого инструмента. Это в общем случае. Но код лучше сразу писать с учетом таких неожиданностей. Ну и поскольку это квик — полезно проверять значения на nil, чтобы потом голову не чесать — откуда белиберда получается.
Видать не думали разрабы Квика, что мы еще мы спреды рисовать будем.
Вот и косяки такие с прорисовкой.
Кстати, в Метатрейдере еще веселее. Если в МТ4 спреды строятся хоть и не быстро, но надежно, то в МТ5 построенный спред может в течении торгов просто пропасть, хотя утром был. Или улететь далеко вверх или вниз, нарисовать горизонтальную линию. Короче полный бардак. Хотя я в MQL5 еще хуже понимаю, чем в QLua. Впрочем они сделали возможность создания спреда в виде нового инструмента. Но работать с этим очень не удобно. Таки и мучаемся )
В примерах для Квика такой стиль был нуууу очень давно.
Новую версию индикаторов на Qlua скачай у них с форума, там все нормально и более быстрый вариант расчета.
А идентификатор графику зачем присваиваешь, ты его потом в роботе что ли используешь? — Если так то это плохой — тупой подход хоть и популярный у многих.
Спасибо за совет про «скачать новую версию индикаторов». Я стандартными индикаторами не пользуюсь. Совсем.
Про идентификатор в тексте тоже написано — зачем и почему.
Мог бы быть и поаккуратней написан, раз это «пример». Если idx >= per и Settings.Show_Ind не равно 1, то функция ничего явно не вернет и, видимо, вернет nil по умолчанию в lua? Не ясно почему вторая ветка возвращает явно nil, а первая нет. И похоже, что нет смысла высчитывать ma_value если Show_Ind не 1. Лишняя работа.
Можно так переписать, вдобавок уменьшив количество вложений:
Мог(ла) бы и поаккуратней прочитать: Авторство кода в примере — разработчиков квика, об этом написано, я лишь поправил опечатки в нем и добавил для примера управление выводом. Суть статьи абсолютно не в данном коде. Он приведен именно для примера — чтобы желающие попробовали и поняли, что делать подобные индикаторы не страшно и не сложно. Но если их делать, то понятно — не из стандартного набора, смысла повторять их нет. Расчетную часть каждый должен создать сам по своим хотелкам.
Эх, а в заголовке было написано:
И как теперь добавить скажем lua-индикатор в QUIK?