Владимиров Владимир
Владимиров Владимир личный блог
12 августа 2023, 17:18

Создание на Lua своего индикатора в графике Quik: основы, нюансы, пример. Индикаторы: прогнозных High и Low следующего интервала; ценовых уровней объема.

   Кратко расскажу принципы и некоторые нюансы работы с графиком в 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

 

 

53 Комментария
  • bascomo
    12 августа 2023, 17:18
    Ничего себе кратко
      • bascomo
        12 августа 2023, 17:27
        Владимиров Владимир, неожиданно. Мне надо закончить уже этот весёлый проект, я никак не могу себя заставить, и заниматься тут писаниной — это, на самом деле, такой мой способ избегания.


          • bascomo
            12 августа 2023, 17:39
            Владимиров Владимир, да я думал, какой-то критический взгляд получу, сам переосмыслю, но нет. Ушат говна в лучшем случае. Есть там хотя бы люди, которые просто интересуются, любопытствуют, что да как. Но их меньшинство, да и это не та обратная связь, которой я ожидал, честно сказать. А большинство — оно везде большинство :)

            з.ы. вот у меня и дозревает)
              • bascomo
                12 августа 2023, 18:09
                Владимиров Владимир, да, это мешает. А всех мудаков по рекомендации Тимофея не заблокируешь.
  • Vkt
    12 августа 2023, 19:47
    Никогда не встречался такой косяк Квика, когда строишь график спреда между двумя инструментами с помощью getCandlesByIndex. Ну т.е. индикатор нормальный рабочий, график построился, а при новом включении Квика графика в окне нет — пусто. И нужно вручную его обновить чтобы он прорисовался. Жутко бесит, когда таких графиков много и все обновлять.
    Может есть какой способ чтобы это исправить?
      • Vkt
        12 августа 2023, 20:41
        Владимиров Владимир, Или это про режим редактирования графика «график-обновить»?

        Именно!

        Я тоже dbgview пользую.
        А что там можно локализовывать? Квик ошибок не выдает.
        Торги еще не начались. Окно с графиком пустое. после начала торгов индикатор начинает отрисовывать спред за сегодняшний день. Спред за прошлые дни только после ручного обновления появляется.
        Данные да из двух таблиц. Делал и из 3х, 4х. Все едино.
        Мало того. Иногда при новом запуске Квика спред может и сам прорисоваться!




        • bascomo
          12 августа 2023, 20:45
          Vkt, Такое у меня было связано с ошибкой деления на ноль в коде. Когда квик запускается в первый раз, данных ещё нет, и возникает такая ошибка. Терминал о ней не сообщает, а просто не отрисовывает индикатор. После догрузки данных всё начинает работать. Ну или не ноль, а null. То же самое будет
          • Vkt
            12 августа 2023, 21:05
            bascomo, вот это похоже на правду. Как-то можно решить эту проблему?
            • bascomo
              12 августа 2023, 21:08
              Vkt, Проверять делитель на нуль или пустое значение. Если в коде индикатора возникла ошибка, он выключается и не выполняется до принудительного обновления. Если ошибки не допускать, то он обновится на следующей новой свече. Можно писать значения в лог при первых 10 вызовах тела функции индикатора, чтобы проверить, что в них такое
              • Vkt
                12 августа 2023, 21:22
                bascomo, а если нет деления?
            • bascomo
              12 августа 2023, 21:11
              Владимиров Владимир, если null, то не выдаёт. Но без кода можно долго фантазировать. 
          • Vkt
            12 августа 2023, 21:19
            Владимиров Владимир, да какой там большая. Получили данные и вычли одно из другого.

            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
            • bascomo
              12 августа 2023, 21:25
              Vkt, так у вас index при первом вызове 0, вы -1 элемент получаете) я тоже так делал))
              добавьте вначале if index == 0 then return nil
              • Vkt
                12 августа 2023, 21:33
                bascomo, точно, в более поздних версиях этого индикатора есть if index == 0 then return nil
                Но проблемы это не решает. Тут какие-то более хитрые проверки нужны, раз он то появляется, то нет.
              • Vkt
                12 августа 2023, 21:34
                Владимиров Владимир, это индикатор спреда между двумя разными графиками разных инструментов с разными идентификаторами. Почему бы ему не работать?
                  • Vkt
                    12 августа 2023, 22:18
                    Владимиров Владимир, 0 это ноль: first_candle – индекс первой свечки. Первая (самая левая) свечка имеет индекс 0,
                      • Vkt
                        12 августа 2023, 22:45
                        Владимиров Владимир, Один из инструментов следует подписать на графике спреда, оставить дополнительный график для подписки на второй инструмент, а вывод значений первого инструмента и спреда разнести по разным осям (правая/левая)
                        Пользуюсь Квиком более  15 лет, но из этой фразы понял только про разнесение по разным осям )
                        Я обычно оба инструмента строю в одном окне друг под другом, и ниже в том же окне график спреда.

                          • Vkt
                            13 августа 2023, 15:24
                            Владимиров Владимир, окно графика имеет три окна по вертикали, в двух выводятся данные по инструментам, а в третьем окне рисуется спред. Все верно?

                            Да. Изредка бывает, что в одном окне 2 графика инструментов, а ниже спред.
                            А у вас, насколько я понял, в этом окне только расчетный спред. Т.е. «с точки зрения» квика в этом окне цена не меняется, соответственно нет причин для инициализации OnCalculate.
                            Это не так. Индикатор всегда привязан к какому либо графику инструмента. И в свойствах графика Квик это явно выводит. К тому же эта версия опровергается тем, что я писал выше. Бывает, что при загрузке Квика график спреда иногда появляется. Но даже если не появился, то после начала торгов он начинает отображаться, но только за текущий день. За предыдущие дни только после принудительного обновления. Значит OnCalculate таки срабатывает.
                            Промежуточная печать это хорошо, но что и в каком месте тут печатать не понятно.
                              • Vkt
                                13 августа 2023, 18:39
                                Владимиров Владимир, спасибо за столь активное участие в решении данной проблемы! Да, это явно косяк Квика. Потому и спросил в самом начале, вдруг кто решил данную проблему. Но видать и вправду нерешаемая.
                                По поводу кода согласования, было бы интересно ознакомится.
                                Но снова вопрос терминологии. Что есть свечки без сделок? Неужели у Квика есть такие? Я всегда думал, что нет сделок — нет свечки. Если сделки за период свечки все по одной цене, то она вырождается в точку.
                                Или имелось ввиду просто пропуски свечек у одного из инструментов?
                                Тогда нужно понимание и соответствующий алгоритм того, что мы будем рисовать на месте этих пропусков.

                                  • Vkt
                                    13 августа 2023, 19:22
                                    Владимиров Владимир, сегодня включил основной комп, где лежат рабочие скрипты индикаторов. Там  после получения от getCandlesByIndex значений я как раз всегда проверяю их на nil. Если хоть одно значение nil, то и return nil. Результат меня устраивает. Дополнительных проверок на соответствие даты и времени не делал.
                                    Видать не думали разрабы Квика, что мы еще мы спреды рисовать будем.
                                    Вот и косяки такие с прорисовкой.
                                    Кстати, в Метатрейдере еще веселее. Если в МТ4 спреды строятся хоть и не быстро, но  надежно, то в МТ5 построенный спред может в течении торгов просто пропасть, хотя утром был. Или улететь далеко вверх или вниз, нарисовать горизонтальную линию. Короче полный бардак. Хотя я в MQL5 еще хуже понимаю, чем в QLua. Впрочем они сделали возможность создания спреда в виде нового инструмента. Но работать с этим очень не удобно. Таки и мучаемся )

                                      • Vkt
                                        13 августа 2023, 21:19
                                        Владимиров Владимир, спасибо, попробую
              • bascomo
                12 августа 2023, 21:36
                Владимиров Владимир, По у сторонние силы)) я последний раз писал код на луа в 2020 году
  • Михаил К.
    12 августа 2023, 20:42
    А что у вас в индикаторе за значение iValue? Мне кажется, оно не определено.
  • Beach Bunny
    12 августа 2023, 21:56
    Код для индикатора SMA в статье плохой, так писать плохо, при более сложных расчетах будет тормозить.
    В примерах для Квика такой стиль был нуууу очень давно.
    Новую версию индикаторов на Qlua скачай у них с форума, там все нормально и более быстрый вариант расчета.
    А идентификатор графику зачем присваиваешь, ты его потом в роботе что ли используешь? — Если так то это плохой — тупой подход хоть и популярный у многих.
  • Фейтомия
    13 августа 2023, 15:38
    Для примера: Стандартный индикатор скользящей SMA....

    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

    Мог бы быть и поаккуратней написан, раз это «пример». Если idx >= per и Settings.Show_Ind не равно 1, то функция ничего явно не вернет и, видимо, вернет nil по умолчанию в lua? Не ясно почему вторая ветка возвращает явно nil, а первая нет. И похоже, что нет смысла высчитывать ma_value если Show_Ind не 1. Лишняя работа.

    Можно так переписать, вдобавок уменьшив количество вложений:

    function OnCalculate(idx)
      local per = Settings.period
      local mode = Settings.mode
      local lValue = iValue

      if idx < per or Settings.Show_Ind ~= 1 then
        return nil
      end

      local ma_value = 0
      for j = (idx — per) + 1, idx do
        ma_value = ma_value + dValue(j, mode)
      end

      return ma_value / per
    end



      • Фейтомия
        14 августа 2023, 09:56
        Владимиров Владимир, 
         Суть статьи абсолютно не в данном коде. 

        Эх, а в заголовке было написано:

        Создание на Lua своего индикатора в графике Quik: основы, нюансы, пример.
  • Сбергамот
    13 февраля 2024, 12:24
    Теперь в QUIK индикаторы не в отдельной папке, а зашиты в каком файле? 
    И как теперь добавить скажем lua-индикатор в QUIK?

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн