Блог им. morefinances
Сегодня начинаем уже писать полноценные скрипты для терминала, а не отдельные блоки кода на lua.
Пройдем:
Структура скрипта
В торговом терминале можно запускать небольшие примеры на lua, как мы это делали ранее, но если говорить о постоянно работающем алгоритме, а не о компактной программе, которая должна выполнить только несколько коротких действий, то минимальная структура скрипта для квика будет содержать следующие функции:
function OnInit – инициализирует глобальные переменные и константы (например, торгуемые бумаги, размеры тейка и стопа, торговый счет и пр.), имена таблиц, необходимых файлов.
function OnStop – функция остановки скрипта, активируется при нажатии клавиши «Остановить» в панели скриптов терминала.
function main – основная функция, создает отдельный поток для выполнения скрипта. Обычно внутри main создается цикл для непрерывной работы, т.к. без него функция выполнит один раз весь код, который в ней прописан и скрипт остановится.
Например:
function OnInit() progname="example_one: " end function main() message(progname.." старт") end
Данный код только выдаст сообщение и остановит скрипт.
Поэтому создаем переменную, которая пока будет true, цикл в main будет работать:
function OnInit() progname="example_one: " do_it = true end function OnStop() do_it = false message(progname.." завершение работы") end function main() message(progname.." старт") while do_it do sleep(1000) end end
Не забываем ставить паузу sleep внутри цикла, чтобы не перегружать процессор.
Теперь скрипт не выключается, а при нажатии «Остановить» он выдаст сообщение о завершении работы и приостановит работу.
Внутри функции main до цикла while можно поставить ту часть кода, которую необходимо запустить всего 1 раз, а весь основной код нашего алгоритма уже размещать внутри цикла. Дополнительно мы можем прописывать собственные функции, которые будем задействовать до и внутри while, в этом случае их необходимо разместить выше функции main.
Напишем скрипт по которому сравним данные от os.sysdate и getInfoParam о которых говорили в прошлый раз:
function OnInit() progname="example_two: " do_it = true end function OnStop() do_it = false message(progname.." завершение работы") end function main() message(progname.." старт") for i=1, 20 do time1 = os.sysdate() time2 = getInfoParam('SERVERTIME') time_txt=time1.hour..":"..time1.min..":"..time1.sec message("Время через os.sysdate: "..time_txt.." время SERVERTIME:"..time2) sleep(250) end while do_it do sleep(1000) end end
Цикл for, в котором запрашиваем время, ставим до while, чтобы программа выполнила его всего 1 раз, иначе он будет постоянно перезапускаться.
Если мы запустим скрипт, то увидим 20 выводов с описанием времени, полученных разным способом. Как правило, в отдельных строках мы будем наблюдать расхождение о которых говорили ранее.
Если бы мы хотели, чтобы вывод был только по тем строкам, где время расходится, то можно добавить небольшое условие, по секундам:
sec1 = time1.sec -- секунды, полученные от sysdate sec2 = tonumber(string.sub(time2, #time2-1, #time2)) -- вырезаем секунды (последние 2 символа строки), полученные от SERVERTIME, переводим в число
А вывод поставим в условие:
if sec1~=sec2 then … end
Если мы захотим, чтобы после вывода строк скрипт сам остановился, то после цикла for достаточно приравнять переменную do_it = false. Итого получаем:
function OnInit() progname="example_three: " do_it = true end function OnStop() do_it = false message(progname.." завершение работы") end function main() message(progname.." старт") for i=1, 20 do time1 = os.sysdate() time2 = getInfoParam('SERVERTIME') sec1 = time1.sec sec2 = tonumber(string.sub(time2, #time2-1, #time2)) if sec1~=sec2 then time_txt=time1.hour..":"..time1.min..":"..time1.sec message("Время через os.sysdate: "..time_txt.." время SERVERTIME:"..time2) end sleep(250) end do_it = false while do_it do sleep(1000) end end
Теперь скрипт выводит только те строки, где были выявлено расхождение.
При этом после завершения скрипта информация об этом не вывелась, т.к. мы не нажимали кнопку «Остановить». Однако, если бы мы вместо do_it = false написали бы OnStop(), то и скрипт бы остановился, и надпись появилась, т.к. вызвалась в этом случае соответствующая функция.
Рассмотрим еще небольшой пример, в котором коснемся работы сервера.
Иногда связь с сервером брокера может разрываться и важно, чтобы скрипт правильно обрабатывал это событие, и самое главное: нормально возвращался к работе после автоматического восстановления работы.
С помощью функции isConnected() можно получить текущий статус соединения:
1 – если связь установлена и 0 – если связи нет.
function OnInit() do_it = true progname="example_four: " end function OnStop() do_it = false message(progname.." завершение работы") end function main() message(progname.." старт") connect = isConnected() if connect == 1 then message(progname..'связь с сервером установлена') else message(progname..'связи с сервером нет') end while do_it do sleep(1000) end end
Теперь при запуске скрипта в терминале выйдет запись есть ли связь сервером или нет. Но это только на старте.
Чтобы обрабатывать само событие обрыва соединения нужно воспользоваться функцией обратного вызова OnDisconnected(), которая вызывается терминалом при данном событии. Для этого разместим эту функцию до main и опишем сообщение о том, что терминал отключен от сервера. Здесь же в скрипте можно добавить переменную status, которая будет равна 1, если связь есть и -1, если нет. Тогда в этой функции OnDisconnected() мы также поменяем ей знак, а уже в цикле main`а обработаем это событие:
function OnInit() do_it = true progname="example_five: " status = 0 mes_ind = 0 end function OnStop() do_it = false message(progname.."завершение работы") end function OnDisconnected() status = - 1 message(progname..'терминал отключен от сервера') end function main() message(progname.." старт") connect = isConnected() if connect == 1 then message(progname..' связь с сервером установлена') status = 1 else message(progname..'связи с сервером нет') status = - 1 end while do_it do connect2 = isConnected() if status == -1 and connect2 == 1 then message(progname..'соединение с сервером восстановлено') status = 1 end sleep(1000) end end
В функциях обратного вызова лучше не включать большое количество кода, т.к. это может сильно подтормаживать терминал, в отдельных моментах приводить к его зависанию.
Теперь можно запустить скрипт при установленном соединении, а после выключить и повторно включить. Скрипт просигналит про каждое изменение.
Аналогично можно запустить скрипт при выключенной связи и после старта подключиться. Скрипт укажет и на отсутствие связи на старте и на восстановление соединения после.
Работа с файлами
Запись данных в файл
Запись осуществляется следующим образом:
filename = "save_one" -- имя файла DirectionSaveFile=tostring("C:\\files\\"..filename..".csv") -- директория сохранения и формат my_csv=io.open(DirectionSaveFile,"w") -- создание файла abc = "Проба записи в файл" -- переменная для записи my_csv:write(abc..";\n") -- запись в файл, в конце указываем разделитель и перенос строки my_csv:flush() my_csv:close() -- закрытие файла
Чтобы скрипт корректно отработал необходимо на C: создать папку files. Как видно в переменную DirectionSaveFile путь указывается с помощью двойного обратного слэша \\, т.к. в строке он переводится в одинарный. Это можно увидеть, если добавить message(DirectionSaveFile) в конце скрипта, например.
Перезапись
Если в io.open(DirectionSaveFile,«w») поставить вместо «w» параметр «a+», то файл будет создан, если его нет, или открыт если уже есть для дополнения, т.е. сохранятся предыдущие записи в файле. Это удобно для записи логов, например.
filename = "save_one" DirectionSaveFile=tostring("C:\\files\\"..filename..".csv") my_csv=io.open(DirectionSaveFile,"a+") for i = 1, 10 do my_csv:write(i..";\n") end my_csv:flush() my_csv:close()
Если у вас выскакивает ошибка, то, возможно, вы ранее открыли созданный файл save_one.csv в Excel и не закрыли перед повторным запуском. Терминал не сможет сохранить в этом случае данные в открытый в программе файл и будет сообщать об этом в панели скриптов.
Чтение
В случае чтения файла используется параметр «r» в io.open:
DirectionFile=tostring("C:\\files\\save_one.csv") f = io.open(DirectionFile, "r"); t = f:read("*all") f:close()
В этом случае все данные файла будут записаны в одну строку t.
Чтобы разбить её в массив по разделителю (в нашем варианте ";") необходимо добавить цикл:
s = {} -- создаем массив a = 1 -- счетчик индекса массива for i in string.gmatch(t, "[^;]+") do -- разбиваем строку t по ";" и итерируем s[a] = tonumber(i) message(tostring(s[a])) -- выводим на экран полученный элемент массива a = a + 1 end
Последнему значению будет присвоено nil – т.к. это конец файла. При этом размер полученного массива message(""..#s) покажет правильный размер массива без nil.
Если вы сохраняли данные в файл в одну строку без разделителей, то итерировать не нужно, просто делаются необходимые срезы строки через string.sub с переводом подстроки в число (tonumber) для корректной работы в дальнейшем.
Альтернативный код построчного чтения файла через io.lines:
DirectionFile=tostring("C:\\files\\save_one.csv") t = {} for line in io.lines(DirectionFile) do t[#t+1]=line message(t[#t]) end
Работа с файлами нам позже пригодится и для записи логов, и для взаимодействия с другими скриптами, в т.ч. на любых других языках.
Полезные функции при работе с файлами:
getScriptPath выдаст нам путь, по которому находится запускаемый скрипт.
getWorkingFolder вернёт путь нахождения терминала (файл info.exe). Особенно полезно, когда используется несколько квиков от разных брокеров.
scriptpath=getScriptPath() workfold = getWorkingFolder() message(scriptpath) message(workfold)
И в том, и в другом случае функции возвращают адрес без слеша в конце строки (т.е. в дальнейшем нужно будет прибавить… "\\").
Все файлы сегодняшних примеров размещены для удобства на github:
https://github.com/morefinances/qlua
Упражнения для закрепления:
1. Сохраните в файл текущее время и дату в формате: ГГГГММЧЧММСС (для 12:05 14.07.23 это 202307141205). Помните, что нужно добавлять нули, чтобы был корректный формат (подробнее об этом говорили в прошлой статье).
2. Сгенерируйте циклом последовательность Фибоначчи (до 100-го значения) и сохраните его в файл в одну строку и в другой файл с разбивкой по 1 элементу в строке.
3. В прошлый раз в качестве одного из упражнения было необходимо сделать массив параметров getInfoParam и пройти по данной функции циклом с выводом результатов каждого запроса. Попробуйте теперь эти результаты сохранить в файл и другим скриптом прочитать их.
В предыдущих сериях:
Qlua: введение
https://smart-lab.ru/blog/917696.php
Настраиваем торговый терминал и редактор кода
https://smart-lab.ru/blog/918869.php
Основы qlua, часть 1:
message, конкатенация
фильтрация по сообщениям в терминале
PrintDbgStr, комментарии
типы данных, type
операции с числами
операции со строками
операции с таблицами
условные операторы
https://smart-lab.ru/blog/920031.php
Основы qlua, часть 2:
Циклы for … do … end, while do … end, repeat … until.
sleep, break, goto.
как пройти весь массив циклом, как пройти таблицу по ключам
локальные и глобальные переменные, функции
получение даты и времени
получение данных через getInfoParam
https://smart-lab.ru/blog/921366.php
Теги: qlua для начинающих, кружок авиамоделизма.
Когда разработчики Квика встраивали Луа в Квик, это был (Луа 5.1 х32) популярный язык с огромным количеством всяких разных библиотек, компилятором (LuaJIT), своим менеджером пакетов и большим комьюнити. Потом прошло некоторое количество времени, Луа в Квике изменился на Луа 5.4 х64 и все достоинства его превратились в тыкву. Оказалось, что это практически умирающий ( утопающий) язык, поддержка которого — дело рук самих утопающих. Кто-то успел ухватить тренд и быстренько переписал все на Python и остался конкурентноспособен ( хороший пример Torch -> PyTorch ), кто-то все проспал и утонул (ну или еще пока барахтается).
В связи с выше сказанным, очень хотелось бы выяснить, так сказать на берегу, стоит ли свеч QLua, или можно построить такого же стабильного и быстрого робота на другом языке.
Стоит ли код, управляющий деньгами, отдалять от биржи на еще одну прокладку, чтобы быть модным и в тренде?
Что экзотического может быть в минималистическом скрипте с паскалеподобным синтаксисом, спецификация которого осваивается за 1 вечер?
P.S. кружок авиамоделизма в виде перепевания Иерусалимски и QLua.chm не одобряю, смысла в этой деятельности не просматривается. Если бы в статье с этим заголовком, например, обсуждалось что делать с коллбэками DataSource после реконнекта — пользы в ней было бы больше, для начинающих.
В том-то и дело, что я начал писать на Луа, когда он только появился в Квике, и сегодня я не уверен, что знание Луа — это не лишний, небесполезный груз.
Компактный скрипт можно быстро написать и на Питоне. Если Вы его знаете. Чуть посложнее, типа такого: на чистом Луа скорее всего Вы не напишите никогда. Без «сторонних» библиотек вообще нельзя написать ничего серьезного. А задача-то стоит не столько написать, сколько заработать денег на алготорговле. Простые алгоритмы, несколько роботов… это поиграть и забыть. Вот с этим согласен. Но в таких случаях обычно и совсем без Квика как-то обходятся.
PS. За Excel и VBA стоит одна из крупнейших корпораций, десятки лет и сотни миллионов пользователей. Но я про это вообще не в курсе. Как-то R обхожусь.
PPS. Луа — простой язык. Но эта простота из серии «хуже воровства». Наделать невидимых простым глазом ошибок проще простого.
Да, выбор большой)