Продолжаем изучать основы qlua. Улучшаем советника, которого писали ранее и уже дополняли в разных вариантах работой со временем.
Сегодня рассмотрим:
Дополним сигналами на закрытие позиции.
Создадим дополнительную таблицу для вывода данных.
Научим скрипт делать расчет финансового результата.
Сигналы на закрытие позиции.
Логика выходов не менее важная часть любой торговой стратегии и должна тестироваться также скрупулезно как и логика сигналов на вход и разные варианты фильтров для лонга и шорта. Также может быть отдельная логика управлением позицией, например часть позы может частично докупаться если движение идет в сторону прибыли, частично резаться если в сторону убытков, могут по-разному управляться стопы: вся позиция или часть закрываться по уровням, вся или часть двигаться трейлингом в разной логике, например, какая-то часть или вся позиция закрываться по времени (перед закрытием основной сессии или через определенное количество часов после входа, если нет сильного движения и цена ни стоп, ни тейк не достигла).
Это всё тестируется и чаще берется какой-то один вариант в работу. Реже более сложная модель, совмещающая в разные сочетания несколько видов выходов.
Но здесь в качестве учебного варианта мы рассмотрим, что выход из сигнала происходит просто когда перестают работать условия самих сигналов. Напомню логику, которую взяли изначально для работы советника:
Сигнал LONG: в случае, если количество лотов на покупку (суммарный спрос) в 2 раза больше аналогичного объема на продажу (суммарное предложение), при этом текущая цена выше цены закрытия вчерашнего дня (основной торговой сессии).
Сигнал SHORT: если суммарные продажи в 2 раза больше суммарного спроса и текущая цена ниже цены закрытия вчерашнего дня (основной сессии).
В нашем коде достаточно разместить в условиях отработки сигналов следующий код после else:
--отработка выхода из сигналов
if signal[i] ~= 0 then
if signal[i] == 1 then
message(progname.." закрытие позиции LONG по "..tikers[i].." по цене "..tLast.param_image)
end
if signal[i] == -1 then
message(progname.." закрытие позиции SHORT по "..tikers[i].." по цене "..tLast.param_image)
end
signal[i] = 0
end
И если ранее соблюдались условия сигнала (signal[i] не равен 0), а теперь нет, то будет выдано необходимое сообщение и сигналу по инструменту присвоен ноль.
При этом, также как скользящие средние в боковике будут давать множество сигналов на открытие и закрытие позиции, так же и в нашем случае если нет сильного движения можно получить в коротком интервале множество входов и выходов по одному и тому же инструменту. Например только за первые 17 минут открытия торгов (скрипт начинает генерировать сигналы с 10:01) можно получить таким образом аж 22 сигнала на вход:
В случае нашего советника обратный сигнал чаще будет возникать, когда соотношение продаж и покупок будет меняться, и реже, когда будет «прыгать» цена вокруг цены вчерашнего закрытия, но на открытии торгов цены обычно оказываются близко к закрытию вчерашнего дня, поэтому и такая череда сигналов на старте. После первых 1-2 часов торгов советник будет давать гораздо меньше сигналов.
Отдельная таблица для вывода сигналов, логов и результатов работы скрипта.
Бывает удобным, чтобы не смотреть постоянно в таблицу системных сообщений вывести дополнительно необходимые сообщения в отдельную таблицу. Это может пригодится, когда, например, одновременно работают несколько скриптов и по одному нужно более пристально контролировать работу.
Такую таблицу можно сделать в фиксированном формате, например в 10 строк и когда они будут заполнятся, то счетчик индекса строки будет обновляться до единицы. При желании, для наглядности строку с последними данными можно подсвечивать цветом.

Либо сделать «резиновой», когда она будет просто увеличиваться на одну строку, если добавляется новая запись. В этом случае можно последнюю строку не подсвечивать, т.к. последняя запись всегда будет в самом низу страницы:

Добавим сперва саму таблицу в main:
size_table = 10 -- количество строк таблицы
table_result_ind = 1 -- индекс строки
if table_result==nil then
table_result = AllocTable()
AddColumn(table_result, 1, "Сигналы и результаты:", true, QTABLE_STRING_TYPE, 70)
CreateWindow(table_result)
SetWindowPos(table_result,0,430,500,300)
SetWindowCaption(table_result, progname.." сообщения скрипта")
for u = 1, size_table do
InsertRow(table_result,-1)
end
endДля вывода в таблицу удобно будет сделать функцию, которую будем вызывать из условий сигнала.
В первом случае функция будет выглядеть так:
function table_print(table_text, table_result_ind)
SetCell(table_result, table_result_ind, 1, table_text)
if table_result_ind == 1 then
SetColor(table_result, 10, QTABLE_NO_INDEX, mWhite, mBlack, mWhite, mBlack)
SetColor(table_result, table_result_ind, QTABLE_NO_INDEX, mBlue, mBlack, mBlue, mBlack)
else
SetColor(table_result, table_result_ind - 1, QTABLE_NO_INDEX, mWhite, mBlack, mWhite, mBlack)
SetColor(table_result, table_result_ind, QTABLE_NO_INDEX, mBlue, mBlack, mBlue, mBlack)
end
endВо втором случае:
function table_print(table_text, table_result_ind) if table_result_ind <=10 then SetCell(table_result, table_result_ind, 1, table_text) else InsertRow(table_result,-1) SetCell(table_result, table_result_ind, 1, table_text) end end
Добавим в условиях сигнала соответствующий вывод в таблицу и обновление счетчика строки:

И мы получим необходимый результат.
Ссылка на github первого варианта.
Ссылка на github второго варианта.
Чтобы в нашей таблице отражалось еще и время сигнала достаточно, например, добавить в строку для отображения time3, которое мы отслеживаем в каждом случае if (помним, что отражение будет числом).
В эту же таблицу далее мы сможем размещать информацию, например, про разрывы связи или о приостановке получения сигналов на вход. Также эту таблицу, в отличие от основной, мы можем не закрывать при остановке скрипта, чтобы можно было лишний раз посмотреть как отработал алгоритм, какие были сигналы, их результат и пр.
Вывод информации по финансовому результату позиции.
Удобно, чтобы скрипт мог считать финансовый результат. На боевых счетах это может быть точное вычисление с учетом всех особенностей тарифа брокера, плечей, FIFO или даже с учетом налогов. Но сейчас, в первичном исполнении советника, мы упростим и будем находить в качестве финансового результата простой валовый доход по трейду: разницу между покупкой или продажей для лонгов и продажей и дальнейшим откупом для шортов. Когда мы подойдем к работе с реальными счетами расчеты можно будет сделаем уже с учетом всех затрат. Пока простая арифметика для понимания насколько в целом успешна или убыточна взятая в качестве стратегии для советника гипотеза по инструментам.
Предположим, что в каждую сделку мы входим минимальной суммой. Т.е. нам нужно знать лотность по каждому инструменту советника. Плюс нам нужны будут цены входа и выхода по каждой сделки.
Добавим 3 массива:
lots = {} -- лотность по инструменту
buypos = {} -- будем фиксировать цену покупки по каждой бумаги
sellpos = {} -- аналогично фиксируем цену продажиЛоты можно запросить, например, в цикле, где получали цену закрытия предыдущего дня. Там же поставить нулевые значения массивам цен:
for x = 1, #tikers do
closeprice[x] = getParamEx("TQBR", tikers[x], "PREVLEGALCLOSEPR") -- цена закрытия предыдущего дня
signal[x] = 0 -- статус сигнала по инструменту
lots[x] = getParamEx("TQBR", tikers[x], "LOTSIZE")
buypos[x] = 0 -- добавляем нулевые значения в массив цен покупок
sellpos[x] = 0 -- и в массив цен продаж
endВ условиях сигналов добавляем фиксацию цены входа (для шорта это цена продажи sellpos, для лонга это цена покупки buypos):

Нужно сказать, что данный пример входа в позицию – это очень большая условность, т.к. цена при срабатывании сигнала будет отличатся (в отдельные моменты значительно) от цены реального входа (более правильно – в случае советника брать ближайший BID/OFFER со стакана, работу с которым мы будем рассматривать в скором времени, а в случае роботов цену фиксируем по конкретной сделке, т.к. даже ориентируясь на стакан иногда приходится предусматривать повторное выставление заявки, если рынок в моменте резко сдвинулся в одну или другую сторону).
Теперь добавим в условиях сигналов if в части отработки выхода цену закрытия (обратная цене входа, т.е. для лонговых позиций это sellpos, для шортовых buypos), необходимые расчеты результата и вывод в нашу дополнительную таблицу:
Округление цифр.
Как видим, результаты по трейду скрипт считает, но в силу особенности арифметики с плавающей запятой получаем всякие небольшие неточности.
Кто впервые с этим столкнулся удивится, что привычные 0.1 для компьютера на самом деле 0.1000000000000000055511151231257827021181583404541015625, и данную погрешность порой
можно встретить даже в Excel просто чаще её нам Excel не показывает.
Например простой скрипт message(tostring(0.1+0.2 == 0.3)) вернет нам false, а не true, что было бы логичным. Подробнее об этом можно почитать в сети загуглив «погрешности при операциях с плавающей запятой», например.
К сожалению, просто отсечь эту погрешность до второго знака нельзя, как мы это делали ранее с нулями, т.к. иначе 0.9999999999998 превратится в 0,99, а не в 1. Нам нужна функция, которая сделает округления по принятым математическим правилам.
И проще всего это реализовать через строковое форматирование данного числа. Сделаем функцию:
function mprint(numb)
return string.format("%.2f", numb)
endЧерез которую будем выводить итоговый результат. Она округлит по всем правилам число до второго знака.
Кстати, если применить вывод с помощью форматирования к сумме 0,1+0,2 и к числу 0,3 с точностью до 30 знака
message(string.format("%.30f",0.1+0.2))
message(string.format("%.30f",0.3))
то увидим, почему эти числа для компьютера не равны:
0.300000000000000044408920985006
0.299999999999999988897769753748
Теперь в скрипте выдачу информации finres будем проводить через данную функцию mprint с точностью до 2 знака, в итоге получим уже нормальное отражение цифр:

Код варианта советника с учетом расчетов результата:
https://github.com/morefinances/qlua/blob/main/simple%20advisor%20v1_2_finres.lua
В качестве самостоятельных упражнений:
1. Сделайте расчет финансового результата с учетом комиссий брокера (например 0,05% от суммы за сделку).
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: дополняем скрипт советника таймингом
Устанавливаем время старта работы скрипта,
Ставим тайминг на получение сигналов на вход,
Устанавливаем таймер на приостановку скрипта.
Данная публикация является личным мнением автора. Мнение владельца сайта может не совпадать с мнением автора.
я больше по облигациям — ISIN и уровни покупки/продажи хранятся в Excel файле
Даже если не хочется работать с форматом csv, а нужен именно xls файл с формулами и форматированием, то можно сделать небольшой макрос, который нужные данные сохранит по кнопке (или при активации нужной ячейки, например) в csv, а скрипт на qlua уже все данные возьмет с csv. Зато работать это всё будет а) быстрее и б) более устойчиво, т.к. делается полностью штатными средствами.
1/ luacom у меня не взлетел, excel = luacom.GetObject(«Excel.Application») не видит работающий экземпляр Excel2019/Win10
2/ перехожу к плану «Файл обмена», подскажете нет ли рабочих примеров чтения/записи в файл. Пока нашел вот такой пример записи (https://quikluacsharp.ru/qlua-osnovy/iz-qlua-lua-v-excel-csv/)
Не подскажите пример чтения?
Работу с файлами, в т.ч. чтение рассматривал здесь:
smart-lab.ru/blog/922044.php
Про запись в файл рассматривал еще в сегодняшней статье:
https://smart-lab.ru/blog/927748.php
wdho.ru/f4704dc, разместите в директории терминала.
Вышеприведенный пример по первой ссылке работает (выдает данные с ячеек A1 и B1). Только его нужно поместить в main.