alfacentavra
alfacentavra личный блог
09 августа 2023, 15:57

Qlua: получение данных биржевых свечей с сервера брокера, обработка данных, пишем скрипт выгрузки котировок

Функция CreateDataSource
Получение количества свечек данных
Пауза для подгрузки данных
Получение по инструменту OPEN, HIGH, LOW, CLOSE, VOLUME
Обработка времени и даты
Закрытие источника данных
Примеры: получение данных последних 10 свечей, выгрузка новой минутной свечки после её закрытия, текущее значение простой средней SMA10 по минуткам
Простой скрипт выгрузки котировок

Сегодня рассмотрим функцию, с помощью которой можно получать данные биржевых свечек. Это можно делать и с графиков (чуть позже рассмотрим), но в этом случае нужно, чтобы сам график как источник данных был открытым, что не очень удобно, особенно если скрипт использует несколько таймфреймов – необходимо аналогичным образом держать открытыми и соответствующее количество графиков.

Более практичным вариантом является получение данных через функцию CreateDataSource, запрос осуществляется следующим образом:

ds, err = CreateDataSource(код класса, тикер инструмента, интервал)

Код класса: для акций «TQBR», для срочного рынка «SPBFUT».

Код класса и тикер указываются в кавычках, интервал без – это одна из следующих констант:

INTERVAL_TICK тиковые данные
INTERVAL_M1   1 минута 
INTERVAL_M2   2 минуты 
INTERVAL_M3   3 минуты 
INTERVAL_M4   4 минуты
INTERVAL_M5   5 минут
INTERVAL_M6   6 минут
INTERVAL_M10 10 минут
INTERVAL_M15 15 минут
INTERVAL_M20 20 минут
INTERVAL_M30 30 минут
INTERVAL_H1    1 час
INTERVAL_H2    2 часа
INTERVAL_H4    4 часа
INTERVAL_D1    1 день
INTERVAL_W1   1 неделя
INTERVAL_MN1  1 месяц

Если запрос обрабатывается нормально (все параметры указаны корректно), то ds – выдает таблицу данных, с которой и будем работать. Если нет, то err будет содержать описание ошибки (неверный код класса, код инструмента, интервал или параметр).

Особенность работы CreateDataSource: в отличие от прошлых примеров c небольшими кусками кода на lua, которые мы могли сразу запускать в терминале, данная функция будет работать только в полноценной структуре скрипта qlua. Поэтому во всех следующих примерах кода будем размещать в main(), иначе терминал не сможет запустить скрипт.


Количество свечек данных

Количество свечей можно получить через:

number_of_candles = ds:Size()

Нумерация идёт с самой ранней свечи.

 

Пауза для подгрузки данных

Если мы первый раз обращаемся к источнику данных по инструменту, то необходимо сделать паузу для подгрузки данных после вызова функции.

Это можно сделать, например, поставив паузу в 500-1000 мс:

function main()
  ds, err = CreateDataSource("TQBR", "SBER", INTERVAL_M1)
  sleep(500) -- пауза для подгрузки данных
  number_of_candles = ds:Size()
  message("Количество свечей в источнике данных: "..number_of_candles)
end

Более профессиональный подход – сделать это через цикл:

function main()
  ds, err = CreateDataSource("TQBR", "SBER", INTERVAL_M1)
  indexload = 0
 
  repeat
    sleep(100)
    indexload = indexload + 1
  until(ds:Size()~=0 or indexload==10)

  number_of_candles = ds:Size()
  message("Количество свечей в источнике данных: "..number_of_candles)
end

Здесь цикл может прерваться ранее 1000 мс, если получены ненулевые данные по количеству свечей по инструменту. Обычно хватает 100 мс, т.е. в реальности получаем гораздо более быстродейственный вариант.


Получение
данных   OPEN, HIGH, LOW, CLOSE, VOLUME свечи

Получив количество свечей можно обращаться уже за основной информацией.

Следующий код выдаст данные по последней (текущей) свечке:

function main()

  ds, error_discr = CreateDataSource("TQBR", "SBER", INTERVAL_M1)
  sleep(500)

  openprice           = ds:O(index)
  highprice             = ds:H(index)
  lowprice              = ds:L(index)
  closeprice           = ds:C(index)
  volume                = ds:V(index)
 
  message("Цена открытия="..openprice.." максимум="..highprice.." минимум="..lowprice.." последняя цена="..closeprice.." объем="..volume)

end

Если нужна не текущая свечка, а предыдущая (последняя закрытая), то нужно здесь указать не index, а index-1. Аналогичным образом обращаемся к более ранним свечкам, вплоть до 1й.


Обработка времени

Дата и время, как это уже мы встречали ранее, даётся в табличном виде.

ds:T() при нормальной работе CreateDataSource выдаст таблицу вида: {year, month, day, week_day, hour, min, sec, ms, count}

c которой уже можно работать с помощью ключей.


Для удобства воспользуемся функциями, которые мы использовали ранее для перевода даты и времени в нужный формат:

function hhmmss(date_time)

  local Hour = date_time.hour
  if Hour<10 then Hour = "0"..Hour end

  local Min = date_time.min
  if Min<10 then Min = "0"..Min end

  local Sec=date_time.sec
  if Sec<10 then Sec="0"..Sec end

  return Hour..Min..Sec

end

 

function DDMMYYYY(date_time)

  local DD = date_time.day
  if DD<10 then DD = "0"..DD end

  local MM = date_time.month
  if MM<10 then MM = "0"..MM end

  local YYYY = date_time.year
  return DD..MM..YYYY

end

Закрытие источника данных

После того как мы получили и обработали данные и больше не планируем к ним обращаться следует закрыть источник данных.
Это делается через ds:Close()

 

Пример получения последних 10 свечей по Сбербанку:

function main()

ds, error_discr = CreateDataSource("TQBR", "ABRD", INTERVAL_M1)
repeat
  sleep(100)
  indexload = indexload + 1
until(ds:Size()~=0 or indexload>=10)

number_of_candles = ds:Size()

message("Количество свечей в источнике данных: "..number_of_candles)

for index = number_of_candles-9, number_of_candles do

  openprice           = ds:O(index)
  highprice             = ds:H(index)
  lowprice              = ds:L(index)
  closeprice           = ds:C(index)
  volume                 = ds:V(index)
  localtime             = hhmmss(ds:T(index))
  localdata             = DDMMYYYY(ds:T(index))

  message("Свеча "..index.." data:"..localdata.." time:"..localtime.." open="..openprice.." high="..highprice.." low="..lowprice.." close="..closeprice.." volume="..volume)

end

ds:Close() -- закрыть источник данных

end


Полный код:
https://github.com/morefinances/qlua/blob/main/CreateDataSource_10candles.lua.

Получим следующий вывод:

Qlua: получение данных биржевых свечей с сервера брокера, обработка данных, пишем скрипт выгрузки котировок
 

Здесь 4619 – текущая свеча (еще не закрылась) и более ранние минутные данные.

Раньше можно было еще через функцию обратного вызова SetUpdateCallback подписаться на данные по свечке, чтобы получать коллбэк при её изменении, но у разработчиков при очередном обновлении терминала в конце 2021 года «что-то пошло не так» и функция стала недоступной в новых версиях квика.

На форуме арка еще в начале 2022 года писала, что «Проблема изучается, будет устранена в одной из очередных версий ПО», но, видимо, еще не время.

Поэтому хоть в мануале QLUA есть даже пример, но он на текущий момент не рабочий, пока не останавливаемся на этом, хотя было удобно получать таким образом изменения по свечке.


Пример выгрузки новой свечки при закрытии минуты.

В качестве альтернативы в части появления новой свечки можно в цикле while с необходимой периодичностью запрашивать данные по инструменту и сравнивать ds:Size, когда пришла новая свеча, тогда и обрабатываем её:

if ds:Size()~=number_of_candles then …


тогда получим скрипт, который будет выгружать новую минутную свечку после её закрытия:

function OnInit()
indexload = 0
end

function hhmmss(date_time)
local Hour = date_time.hour
if Hour<10 then Hour = "0"..Hour end
local Min = date_time.min
if Min<10 then Min = "0"..Min end
local Sec=date_time.sec
if Sec<10 then Sec="0"..Sec end
return Hour..Min..Sec
end

function DDMMYYYY(date_time)
local DD = date_time.day
if DD<10 then DD = "0"..DD end
local MM = date_time.month
if MM<10 then MM = "0"..MM end
local YYYY = date_time.year
return DD..MM..YYYY
end

function OnStop()
do_it = false 
ds:Close()
end

function main()
do_it = true
ds, error_discr = CreateDataSource("TQBR", "SBER", INTERVAL_M1)
repeat
sleep(100)
indexload = indexload + 1
until(ds:Size()~=0 or indexload>=10)
number_of_candles = ds:Size()
message("Количество свечей в источнике данных: "..number_of_candles)

while do_it do
ds, error_discr = CreateDataSource("TQBR", "SBER", INTERVAL_M1)
if ds:Size()~=number_of_candles and do_it then

  if ds:Size() == number_of_candles + 1 then

    number_of_candles = ds:Size()

      openprice= ds:O(number_of_candles)
      highprice = ds:H(number_of_candles)
      lowprice = ds:L(number_of_candles)
      closeprice = ds:C(number_of_candles)
      volume = ds:V(number_of_candles)
      localtime = hhmmss(ds:T(number_of_candles))
      localdata = DDMMYYYY(ds:T(number_of_candles))
      message(number_of_candles.." "..localdata.." "..localtime.." OPEN="..openprice.." HIGH="..highprice.." LOW="..lowprice.." CLOSE="..closeprice.." VOLUME="..volume)
   end

end

sleep(1000)

end

end

Файл: https://github.com/morefinances/qlua/blob/main/new_candle_example.lua 

Получаем:
Qlua: получение данных биржевых свечей с сервера брокера, обработка данных, пишем скрипт выгрузки котировок 

Видим, что данные по свечке иногда приходят не сразу. Обычно это 1-2 секунды задержки, но может доходить и до 3-5 секунд и больше (например в 15:08 данные получены лишь на 49й секунде). По этой причине скальперских роботов, ориентируясь на получение данных через свечки, лучше не писать. Для этого используют другие, более быстродейственные варианты получения данных по инструментам, один из них – это работа с таблицей текущих торгов, которую мы проходили ранее, если для алгоритма достаточно информации из неё, другой вариант – это работа с лентой всех сделок, которую мы рассмотрим позднее. Однако получение данных по большим таймфреймам для дополнительных расчетов, фильтров, построения средних вполне нормальный вариант.


Пример: код простая средняя
SMA10 по минуткам.

function main()

ds, error_discr = CreateDataSource("TQBR", "SBER", INTERVAL_M1)
sleep(500)

number_of_candles = ds:Size()
message("Количество свечей в источнике данных: "..number_of_candles)

N = 10
summ = 0

if number_of_candles < N then
  message("Недостаточно данных для расчета")
  message(number_of_candles.." < "..N)
else
  for index = number_of_candles-(N-1), number_of_candles do
    summ = summ + ds:C(index)
  end
  message("SMA("..N..")="..summ/N)
end

-- закрыть источники данных
ds:Close()

end 

Результат:

Qlua: получение данных биржевых свечей с сервера брокера, обработка данных, пишем скрипт выгрузки котировок
Файл: https://github.com/morefinances/qlua/blob/main/SMA10_from_CreateDataSource.lua

Можно настроить график средней (Moving Average) на минутках, поставить параметры построения по закрытию, простая скользящая (Simple), по 10 свечкам и убедиться, что цифры будут биться.


Скрипт выгрузки котировок.

За рубежом маркетдата платная. Московская Биржа тоже предлагает за достаточно большие деньги получать большой объем биржевой информации по рынку, но есть несколько ресурсов, где можно скачать котировки бесплатно. Если мы говорим про наш рынок, то сейчас это сайт брокера FINAM и сайт MFD. Раньше еще была возможность скачать котировки с сайта Алора, но с новым дизайном несколько лет назад они убрали эту опцию. 

Сегодня мы разгрузим немного серверы Финама и MFD, т.к. напишем скрипт, который поможет нам самостоятельно выгружать нужную информацию с терминала в файл.

Возьмем скрипт в котором мы проходились по 10 свечкам и просто заменим с небольшими изменениями вывод через message на вывод в файл, а сам цикл будет проходить по всем данным, а не по 10 последним значениям.

Вначале main создаем файл для записи:

-- наименование файла: тикер _ дата и время текущий свечи _ дата и время первой свечи
filename = tiker.."_"..YYYYDDMM(ds:T(number_of_candles))..hhmmss(number_of_candles).."_"..YYYYDDMM(ds:T(1))..hhmmss(ds:T(1))

--размещение файла на C:\files
DirectionSaveFile=tostring("C:\\files\\"..filename..".csv")

--создаем файл для записи
my_csv=io.open(DirectionSaveFile,"a+")


До самого цикла for создадим шапку таблицы:

my_csv:write("<DATA>;<TIME>;<OPEN>;<HIGH>;<LOW>;<CLOSE>;<VOLUME>;\n")

Внутри цикла после присвоения переменных заменяем message на:
my_csv:write(localdata..";"..localtime..";"..openprice..";"..highprice..";"..lowprice..";"..closeprice..";"..volume..";\n")

После прохождения всего цикла сохраняем и закрываем файл:

my_csv:flush()
my_csv:close()


Добавим таблицу для контроля выгрузки, в которой отразим первые и последние 10 строк данных и наш скрипт готов:

if table_result==nil then 
table_result = AllocTable()
AddColumn(table_result, 1, "<DATA>", true, QTABLE_DATE_TYPE, 12)
AddColumn(table_result, 2, "<TIME>", true, QTABLE_TIME_TYPE, 10) -- QTABLE_STRING_TYPE
AddColumn(table_result, 3, "<OPEN>", true, QTABLE_DOUBLE_TYPE, 8)
AddColumn(table_result, 4, "<HIGH>", true, QTABLE_DOUBLE_TYPE, 8)
AddColumn(table_result, 5, "<LOW>", true, QTABLE_DOUBLE_TYPE, 8)
AddColumn(table_result, 6, "<CLOSE>", true, QTABLE_DOUBLE_TYPE, 9)
AddColumn(table_result, 7, "<VOL>", true, QTABLE_INT_TYPE, 15)
CreateWindow(table_result)
SetWindowPos(table_result,0,440,700,420)
SetWindowCaption(table_result, "Выгрузка котировок : "..tiker.." таймфрейм : "..timeframe)
  for u = 1, size_table do
    InsertRow(table_result,-1)         
  end
end

Заполним таблицу данными:

for index = 1, number_of_candles  do
openprice           = ds:O(index)
highprice             = ds:H(index)
lowprice              = ds:L(index)
closeprice           = ds:C(index)
volume               = ds:V(index)
localtime             = hhmmss(ds:T(index))
localdata             = DDMMYYYY(ds:T(index))
if index >= number_of_candles-9 and  index <= (number_of_candles) then
local linetable = 1 + number_of_candles - index
SetCell(table_result, linetable, 1, tostring(localdata))
SetCell(table_result, linetable, 2, tostring(localtime))
SetCell(table_result, linetable, 3, tostring(openprice))
SetCell(table_result, linetable, 4, tostring(highprice))
SetCell(table_result, linetable, 5, tostring(lowprice))
SetCell(table_result, linetable, 6, tostring(closeprice))
SetCell(table_result, linetable, 7, tostring(math.floor(volume)))
end

for i = 1, 7 do
SetCell(table_result, 11, i, "...")
end

if index >= 1 and  index <= 10 then
local linetable = 22 - index
SetCell(table_result, linetable, 1, tostring(localdata))
SetCell(table_result, linetable, 2, tostring(localtime))
SetCell(table_result, linetable, 3, tostring(openprice))
SetCell(table_result, linetable, 4, tostring(highprice))
SetCell(table_result, linetable, 5, tostring(lowprice))
SetCell(table_result, linetable, 6, tostring(closeprice))
SetCell(table_result, linetable, 7, tostring(math.floor(volume)))
end

end

Подправим дату на более привычный для выгрузки формат и получим:

Qlua: получение данных биржевых свечей с сервера брокера, обработка данных, пишем скрипт выгрузки котировок

И видим аналогичные данные в хронологическом порядке в файле:

Qlua: получение данных биржевых свечей с сервера брокера, обработка данных, пишем скрипт выгрузки котировок

Итоговый файл скрипта:
https://github.com/morefinances/qlua/blob/main/downloader.lua

Удобство этого алгоритма в том, что можно самому определять какие данные грузить и в какой форме. Например: если для стратегии не нужны вечерние/утренние сессии, то их здесь же в коде можно по условию не сохранять в файл.

С минимальными правками кода можно в одном файле сохранять котировки базового актива и фьючерса на него или одновременно грузить несколько инструментов, чтобы отслеживать корреляцию по ним.

В дальнейшем мы еще будем возвращаться к этому скрипту и делать его апгрейд. Например, когда пройдем тему реализации в терминале диалоговых окон, появится возможность выбирать инструмент и таймфрейм уже после запуска скрипта, а не править это постоянно в коде.


Упражнения

1. Поменяйте порядок выдачи строк в таблице скрипта так, чтобы они соответствовали прямому хронологическому порядку (как в файле).

2. Попробуйте написать советника, который будет давать сигналы по выбранным свечным формациям (например разворотным).

 

Теги: qlua для начинающих, кружок авиамоделизма.

Ранее:

Qlua: введение
Доля клиентов, использующих алгоритмическую торговлю
«Кружок авиамоделизма»
Разные торговые терминалы, почему Quik
Основной функционал qlua

Настраиваем торговый терминал и редактор кода
Установка торгового терминала
Подготовка терминала
Вкладки в терминале
Сохранение и загрузка настроек
Таблица системных сообщений
Отключение окна сообщений
Редактор Notepad++
Настройка русского языка в редакторе
Выбор синтаксиса языка и кодировки
Цветовые настройки
Дублирующий просмотр
Запуск первого скрипта

Основы qlua, часть 1:
message, конкатенация
фильтрация по сообщениям в терминале
PrintDbgStr, комментарии
типы данных, type
операции с числами
операции со строками
операции с таблицами
условные операторы

Основы qlua, часть 2:
Циклы for … do … end, while do … end, repeat … until.
sleep, break, goto.
как пройти весь массив циклом, как пройти таблицу по ключам
локальные и глобальные переменные, функции
получение даты и времени
получение данных через getInfoParam

Qlua: структура скрипта.
Структура типового скрипта qlua с примерами.
Обработка скриптом «обрыва связи» с сервером и возобновления работы.
Работа с файлами: запись, перезапись и чтение файла.
getScriptPath, getWorkingFolder

Qlua: получение данных из таблицы текущих торгов, создание таблиц в торговом терминале.
Получение биржевых данных через функцию getParamEx
Выгрузка списка параметров функции getParamEx через DDE из торгового терминала
Создание пользовательских таблиц в торговом терминале

Qlua: работа с таблицами (продолжение). Пишем своего советника (начало).
Интегрируем таблицы в структуру скрипта qlua.
Удаляем таблицы через DestroyTable.
Останавливаем скрипт через IsWindowClosed.
Обработка события закрытия таблицы через коллбэк.
Работа с цветом SetColor, Highlight, SetSelectedRow.
Пишем простого советника.

Qlua: дополняем скрипт советника таймингом:
Устанавливаем время старта работы скрипта,
Ставим тайминг на получение сигналов на вход,
Устанавливаем таймер на приостановку скрипта.

Qlua: завершаем апгрейд советника:
Пропуск «поздних» сигналов на старте.
Обработка советником обрыва связи.
Сохранение сигналов и логов в файл.

Qlua: пишем скринер акций Московской биржи
Версия лайт.

 

24 Комментария
  • nicknh
    09 августа 2023, 16:11

    Я бы настоятельно не рекомендовал создавать блокирующий цикл ожидания получения данных с сервера. Вы не знаете сколько будет идти ответ 100 млс. или 10 секунд и больше.

    Обычно такие вещи делаются через неблокирующие ожидания. В Lua, например, это можно сделать через корутины, очереди задач, последовательно опрашивая о результате. Ведь задача может быть более сложной — заказать одномоментно данные по 100-1000 инструментам. И если последовательно ждать на каждом, то это будет долго, очень долго. Поэтому сначала все заказывается. А потом проверяем каждый заказ. Не дошли данные, просто переходим к следующему. И так пока все не придет. При этом вполне могут быть ситуации, когда данные не придут т.к. их нет и надо сбросить ожидание с ошибкой по достижению некого большого периода ожидания.

    • Максим
      10 августа 2023, 12:38
      nicknh, кто будет искать — «корутины» на Луа называются «замыканиями» в официальной книге Иерусалимски
      или здесь но помните что Lua это не QLua 
      fingercomp.gitlab.io/lua-coroutines/
      • nicknh
        10 августа 2023, 12:39
        Максим, Нет, в русском переводе этой книги они называются «сопрограммы» (coroutine www.lua.org/pil/9.1.html). Замыкания (Closure www.lua.org/pil/6.1.html) — это другая концепция.
  • NoobSaibotGAZPSBERLKOH
    09 августа 2023, 16:45
    Ну вот как по заказу контент подъехал наконец-то! Начну прямо с введения.
  • astic
    09 августа 2023, 18:56
    Может тебе проще книгу про программирование на Луа написать и издать как Тимофей Валерьевич в свое время сделал а не тут жемчуг метать :) Я в купил для домашней коллекции.
      • astic
        09 августа 2023, 18:54
        alfacentavra, тут важна логика и интересные идеи. Ну вот к примеру такие понятия как «блокирующие/неблокирующие ожидания». А работает или нет на какой то конкретной версии квика это уже не важно.
    • Socol
      09 августа 2023, 18:10
      astic, вы хотите сказать что тут все кроме вас — свиньи?
      • astic
        09 августа 2023, 18:48
        Socol, да нет я хочу сказать что такие вещи слишком сложны для такого форума а вот если б были напечатаны где то то вполне б можно было б купить и на досуге почитать логику
      • astic
        09 августа 2023, 18:56
        Socol, если это слово обидное то исправлю на жемчуг :)
  • Finman
    09 августа 2023, 21:16
    Автору огромная благодарность за труд! 
  • Hired
    09 августа 2023, 21:53
    спасибо за труды! добро обязательно вернётся

    вопрос по теме: чем CreateDataSource лучше считывания данных через Идентификатор (задающийся в квик настройках графика)? не считая того, что нужно будет сделать нужное количество графиков в отдельную вкладку квика. минусы как раз описаны в первом комменте. если правильно понимаю, то CreateDataSource при заказе данных влияет на пинг (квиковские данные начинают опаздывать, важно для скоростной торговли) или ошибаюсь?

    p.s. пару раз видел темы, где писали что CreateDataSource возвращает неправильные данные, отличные от данных в терминале. не знаю насколько это правда
  • Socol
    10 августа 2023, 11:28
    Автор, большое Вам спасибо!
  • Илья Нечаев
    10 августа 2023, 13:35
    Есть тут lua ниндзя у которого робота можно заказать?
  • Сергей Иванов
    12 августа 2023, 11:33
    alfacentavra, большое спасибо!
    Хотелось бы побыстрее дойти до заявок )))))) перейти от теории к практике ))
  • Сергей Иванов
    12 августа 2023, 11:41
     Пара вопросов:
    1.  «стоит на всякий перезаказать данные в терминале» — поясните как это сделать
    2. Таблица которую мы создаем m_t=AllocTable() немного отличается от стандартных «квиковских», нет возможности «скопировать ячейку» или «скопировать всю таблицу» нет ли других вариантов создания собственых таблиц?
    3.лента всех сделок — это как?
     
  • Олег
    22 апреля 2024, 09:07
    Здравствуйте. Каким образом программно можно получить результат того, что файл для чтения и записи пуст? Если все стереть с файла, то все равно он не распознается ни как — nil, ни как — 0. Если его прочесть (пустой файл), то также дает, что что-то имеется, но не соответствует ни nil, ни " ", ни 0, хотя имеет размер = 1.
    Хотел бы задать условие, если файл пустой, то что-то выполнить, но почему-то никак не распознается, что пуст.

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

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