Пример простого торгового движка "Simple Engine" QLua(Lua)

Автор записи: Дмитрий (Admin)

Блоки кода
Данный движок не предоставляет полный функционал для разработки скриптов на QLua(Lua), но показывает на сколько проще и эффективней становится разработка при таком подходе.

В примере движка реализован следующий функционал:

  1. Движок предоставляет одну функцию для выставления лимитированной заявки
    1
    2
    3
    4
    5
    6
    7
    8
    
    SE_SetLimitOrder(
       account,    -- Код счета
       class_code, -- Код класса
       sec_code,   -- Код инструмента
       operation,  -- Операция ('B' - buy, 'S' - sell)
       price,      -- Цена
       qty         -- Количество
    )
  2. Движок реализует собственные функции обратного вызова, которые можно использовать в скрипте после подключения движка. Чтобы не путаться, все функции и переменные движка имеют префикс "SE_" (от Simple Engine). Вы можете использовать в скрипте только те функции, которые Вам нужны, движок просто не будет вызывать функцию, если ее нет в Вашем скрипте, ошибки это не вызовет.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    -- Функция выполняется при каждой итерации цикла while в функции main
    SE_OnMainLoop()
    -- Вызывается движком при ОШИБКЕ отправки ТРАНЗАКЦИИ
    SE_OnTransSendError(trans)
    -- Вызывается движком при ОШИБКЕ выполнения ТРАНЗАКЦИИ
    SE_OnTransExecutionError(trans)
    -- Вызывается движком при успешном ВЫПОЛНЕНИИ ТРАНЗАКЦИИ
    SE_OnTransOK(trans)
    -- Вызывается движком при появлении НОВОЙ ЗАЯВКИ
    SE_OnNewOrder(order)
    -- Вызывается движком при полном, или частичном ИСПОЛНЕНИИ ЗАЯВКИ
    SE_OnExecutionOrder(order)
    -- Вызывается движком при появлении НОВОЙ СДЕЛКИ (у таблицы trade есть поле trans_id, в которое передается ID транзакции)
    SE_OnNewTrade(trade)
    -- Вызывается движком при остановке скрипта
    SE_OnStop()
  3. Движок записывает в соответствующие массивы ответы по транзакциям, заявки и сделки, но не все, а только те, которые появились благодаря транзакциям, отправленным движком, анализирует их и вызывает соответствующие функции обратного вызова (если они определены в скрипте) при наступлении определенного события. Движок удаляет из массивов старые (обработанные) данные, таким образом не засоряя память.
Код движка
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
-- SimpleEngine.lua
-- ПРОСТОЙ ТОРГОВЫЙ ДВИЖОК "Simple Engine" ДЛЯ QLua
 
SE_Run            = true            -- Флаг работы цикла while в функции main
 
SE_trans_id       = os.time()       -- Текущие дата и время в секундах хорошо подходят для уникальных номеров транзакций
 
SE_TransReplies   = {}              -- Массив для хранения ответов по транзакциям
SE_LastTransID    = 0               -- Последний ID транзакции, ответ по которой был обработан и удален из массива
SE_Orders         = {}              -- Массив для хранения информации о заявках
SE_LastOrderNum   = 0               -- Последний номер заявки, которая была обработана и удалена из массива
SE_Trades         = {}              -- Массив для хранения информации о сделках
SE_LastTradeNum   = 0               -- Последний номер сделки, которая была обработана и удалена из массива
 
 
-- =========================================================
-- ===  МЕХАНИЗМ ДВИЖКА  ===================================
-- =========================================================
 
-- Основная функция скрипта, пока работает эта функция, работает скрипт
function main()
   -- Пока работает данный цикл, работает функция main, следовательно, работает скрипт
   while SE_Run do
      -- Выполняет предопределенную функцию итераций, если она объявлена
      if SE_OnMainLoop ~= nil then SE_OnMainLoop() end
   -- МОНИТОРИТ ИЗМЕНЕНИЯ
      -- Перебирает ОТВЕТЫ ПО ТРАНЗАКЦИЯМ
      for i,TransReplie in ipairs(SE_TransReplies) do
         -- Если ответ еще не был учтен
         if SE_TransReplies[i].checked == nil then
            -- Проверяет на наличие ошибок по транзакции
            if SE_TransReplies[i].status > 1 and SE_TransReplies[i].status ~= 3 then
               -- Вызывает функцию обратного вызова (если она объявлена)
               if SE_OnTransExecutionError ~= nil then SE_OnTransExecutionError(SE_TransReplies[i]) end
               -- Запоминает, что ответ был учтен
               SE_TransReplies[i].checked = true
            -- Если транзакция выполнена
            elseif SE_TransReplies[i].status == 3 then
               -- Вызывает функцию обратного вызова (если она объявлена)
               if SE_OnTransOK ~= nil then SE_OnTransOK(SE_TransReplies[i]) end
               -- Запоминает, что ответ был учтен
               SE_TransReplies[i].checked = true
            end
         end
      end
      -- Перебирает ЗАЯВКИ
      for i,Order in ipairs(SE_Orders) do
         -- Если выставление заявки еще не было учтено
         if SE_Orders[i].checked == nil then
            -- Вызывает функцию обратного вызова (если она объявлена)
            if SE_OnNewOrder ~= nil then SE_OnNewOrder(SE_Orders[i]) end
            SE_Orders[i].checked = true
         end
         -- Проверяет какое количество в заявке исполнено
         local ExecutionCount = SE_Orders[i].qty - SE_Orders[i].balance
         -- Если это первая проверка, или с предыдущей проверки количество изменилось и заявка частично, или полностью исполнена
         if (SE_Orders[i].last_execution_count == nil or SE_Orders[i].last_execution_count ~= ExecutionCount) and ExecutionCount > 0 then
            -- Вызывает функцию обратного вызова (если она объявлена)
            if SE_OnExecutionOrder ~= nil then
               SE_OnExecutionOrder(SE_Orders[i])
               -- Запоминает исполненное количество для последующего сравнения
               SE_Orders[i].last_execution_count = ExecutionCount
            end
         end
      end
      -- Перебирает СДЕЛКИ
      for i,Trade in ipairs(SE_Trades) do
         -- Если в сделке уже появилось поле с номером заявки, по которой она была совершена
         if SE_Trades[i].order_num ~= nil then
            -- Перебирает заявки
            for j,Order in ipairs(SE_Orders) do
               -- Если найдена заявка, по которой совершена сделка
               if SE_Trades[i].order_num == SE_Orders[j].order_num then
                  -- Добавляет таблице сделки номер транзакции, которая инициировала данную сделку
                  SE_Trades[i].trans_id = SE_Orders[j].trans_id
                  -- Вызывает функцию обратного вызова (если она объявлена)
                  if SE_OnNewTrade ~= nil then SE_OnNewTrade(SE_Trades[i]) end
                  -- Запоминает номер последней обработанной сделки
                  SE_LastTradeNum = SE_Trades[i].trade_num
                  -- Удаляет сделку из массива, чтобы больше ее не обрабатывать
                  table.sremove(SE_Trades, i)
                  -- Если заявка сделки полностью исполнена и обработана
                  if SE_Orders[j].last_execution_count ~= nil and SE_Orders[j].last_execution_count == SE_Orders[j].qty then
                     -- Запоминает номер последней обработанной транзакции
                     SE_LastTransID = SE_Orders[j].trans_id
                     -- Удаляет ответ по транзакции из массива, чтобы больше ее не обрабатывать
                     for k,TransReplie in ipairs(SE_TransReplies) do
                        if TransReplie.trans_id == SE_Orders[j].trans_id then
                           table.sremove(SE_TransReplies, k)
                           break
                        end
                     end
                     -- Запоминает номер последней обработанной заявки
                     SE_LastOrderNum = SE_Orders[j].order_num
                     -- Удаляет заявку из массива, чтобы больше ее не обрабатывать
                     table.sremove(SE_Orders, j)
                     -- Прерывает цикл по заявкам
                     break
                  end
               end
            end
         end
      end
 
      sleep(1)
   end
end
-- Срабатывает при остановке скрипта
function OnStop() SE_Run = false if SE_OnStop ~= nil then SE_OnStop() end end
 
-- ========================================
-- ===  Функции, собирающие информацию  ===
 
-- Функция вызывается терминалом когда с сервера приходит ответ по транзакции
function OnTransReply(trans_reply)
   -- Если не относится к движку, выходит из функции
   if trans_reply.brokerref:find('SE_'..SEC_CODE) == nil then return end
   -- Перебирает массив ответов по транзакциям
   for i,TransReplie in ipairs(SE_TransReplies) do
      -- Если если ответ по данной транзакции уже занесен в массив
      if SE_TransReplies[i].trans_id == trans_reply.trans_id then
         -- Если появление ответа уже было учтено, сохраняет эту информацию
         if SE_TransReplies[i].checked ~= nil then trans_reply.checked = true end
         -- Заменяет его в массиве
         table.sremove(SE_TransReplies, i)
         table.sinsert(SE_TransReplies, trans_reply)
         -- Выходит из функции
         return
      end
   end
   -- Ответ еще не был добавлен в массив, добавляет
   if SE_LastTransID < trans_reply.trans_id then table.sinsert(SE_TransReplies, trans_reply) end
end
 
-- Функция вызывается терминалом когда с сервера приходит информация по заявке
function OnOrder(order)
   -- Если не относится к движку, выходит из функции
   if order.brokerref:find('SE_'..SEC_CODE) == nil then return end
   -- Перебирает массив заявок
   for i,Order in ipairs(SE_Orders) do
      -- Если заявка уже занесена в массив
      if SE_Orders[i].trans_id == order.trans_id then
         -- Если появление заявки уже было учтено, сохраняет эту информацию
         if SE_Orders[i].checked ~= nil then order.checked = true end
         -- Если исполненное количество уже учитывалось, сохраняет эту информацию
         if SE_Orders[i].last_execution_count ~= nil then order.last_execution_count = SE_Orders[i].last_execution_count end
         -- Заменяет ее
         table.sremove(SE_Orders, i)
         table.sinsert(SE_Orders, order)
         -- Выходит из функции
         return
      end
   end
   -- Заявка еще не была добавлена в массив, добавляет
   if SE_LastOrderNum < order.order_num then table.sinsert(SE_Orders, order) end
end
 
-- Функция вызывается терминалом когда с сервера приходит информация по сделке
function OnTrade(trade)
   -- Если не относится к движку, выходит из функции
   if trade.brokerref:find('SE_'..SEC_CODE) == nil then return end
   -- Перебирает массив сделок
   for i,Trade in ipairs(SE_Trades) do
      -- Если данная сделка уже занесена в массив
      if SE_Trades[i].trade_num == trade.trade_num then
         -- Если появление сделки уже было учтено, сохраняет эту информацию
         if SE_Trades[i].checked ~= nil then trade.checked = true end
         -- Заменяет ее
         table.sremove(SE_Trades, i)
         table.sinsert(SE_Trades, trade)
         -- Выходит из функции
         return
      end
   end
   -- Сделка еще не была добавлена в массив, добавляет
   if SE_LastTradeNum < trade.trade_num then table.sinsert(SE_Trades, trade) end
end
-- ========================================
-- =========================================================
 
 
 
-- =========================================================
-- ===  ФУНКЦИИ ДВИЖКА  ====================================
-- =========================================================
 
-- Выставляет лимитированную заявку
function SE_SetLimitOrder(
   account,    -- Код счета
   class_code, -- Код класса
   sec_code,   -- Код инструмента
   operation,  -- Операция ('B' - buy, 'S' - sell)
   price,      -- Цена
   qty         -- Количество
)
   -- Выставляет лимитированную заявку
   -- Получает ID для следующей транзакции
   SE_trans_id = SE_trans_id + 1
   -- Заполняет структуру для отправки транзакции
   local Transaction={
      ['TRANS_ID']   = tostring(SE_trans_id),-- Номер транзакции
      ['ACCOUNT']    = account,              -- Код счета
      ['CLASSCODE']  = class_code,           -- Код класса
      ['SECCODE']    = sec_code,             -- Код инструмента
      ['ACTION']     = 'NEW_ORDER',          -- Тип транзакции ('NEW_ORDER' - новая заявка)
      ['TYPE']       = 'L',                  -- Тип ('L' - лимитированная, 'M' - рыночная)
      ['OPERATION']  = operation,            -- Операция ('B' - buy, или 'S' - sell)
      ['PRICE']      = tostring(price),      -- Цена
      ['QUANTITY']   = tostring(qty),        -- Количество
      ['CLIENT_CODE']= 'SE_'..sec_code       -- Комментарий к транзакции, который будет виден в транзакциях, заявках и сделках в поле brokerref
   }
   -- Отправляет транзакцию
   local Res = sendTransaction(Transaction)
   -- Если при отправке транзакции возникла ошибка
   if Res ~= '' then
      -- Вызывает функцию обратного вызова (если она объявлена)
      if SE_OnTransSendError ~= nil then
         local trans = {}
         trans.trans_id = SE_trans_id
         trans.transaction = Transaction
         trans.result_msg = Res
         SE_OnTransSendError(trans)
      end
      -- Возвращает номер транзакции и сообщение об ошибке
      return SE_trans_id, Res
   end
   -- Если транзакция отправлена, возвращает ее номер
   return SE_trans_id
end
-- =========================================================
В примере ниже специально задействованы все функции обратного вызова реализуемые движком для того, чтобы показать как их использовать. Хотя, для данного примера, хватило бы 4-х функций:
SE_OnMainLoop()
SE_OnTransSendError(trans)
SE_OnTransExecutionError(trans)
SE_OnNewTrade(trade)

При использовании движка, не используйте в своих скриптах следующие стандартные функции QLua:
main()
OnTransReply()
OnOrder()
OnTrade()
OnStop()
потому что они уже используются в движке !

Код примера
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
-- SimpleEngineExample.lua
-- ПРИМЕР ИСПОЛЬЗОВАНИЯ ДВИЖКА
-- для примера отправляется транзакция на выставление лимитированной заявки на покупку по определенной цене
 
require('SimpleEngine')                -- Подключение движка
 
ACCOUNT              = "SPBFUT00k59"   -- Код счета
CLASS_CODE           = "SPBFUT"        -- Код класса
SEC_CODE             = "RIH6"          -- Код инструмента
 
MyTransactionSended  = false           -- Флаг, что транзакция отправлена
 
-- =========================================================
-- ===  ФУНКЦИИ ОБРАТНОГО ВЫЗОВА (вызываются движком)  =====
-- =========================================================
 
-- Функция выполняется при каждой итерации цикла while в функции main
function SE_OnMainLoop()
   -- Здесь можно выполнять какой-то свой код
   -- ...
   -- ПРИМЕР
   -- Если тестовая транзакция еще не отправлена
   if not MyTransactionSended then
      -- Выставляет лимитированную заявку
      local trans_id, Res = SE_SetLimitOrder(
         ACCOUNT,    -- Код счета
         CLASS_CODE, -- Код класса
         SEC_CODE,   -- Код инструмента
         'B',        -- Операция ('B' - buy, 'S' - sell)
         77000,      -- Цена
         1           -- Количество
      )
      -- Запоминает, что транзакция отправлена, чтобы отправлена ее при каждой итерации цикла в main
      MyTransactionSended = true
   end
end
 
-- Вызывается движком при ОШИБКЕ отправки ТРАНЗАКЦИИ
function SE_OnTransSendError(trans)
   -- Здесь Ваш код для действий при ошибке отправки транзакции (возможно повторная отправка транзакции)
   -- ...
   -- Выводит сообщение
   message('SE_OnTransSendError() ОШИБКА отправки транзакции №'..trans.trans_id..': '..trans.result_msg)
end
-- Вызывается движком при ОШИБКЕ выполнения ТРАНЗАКЦИИ
function SE_OnTransExecutionError(trans)
   -- Здесь Ваш код для действий при ошибке выполнения транзакции (возможно повторная отправка транзакции)
   -- ...
   -- Выводит сообщение
   message('SE_OnTransExecutionError() ОШИБКА выполнения транзакции №'..trans.trans_id..': '..trans.result_msg)
end
-- Вызывается движком при успешном ВЫПОЛНЕНИИ ТРАНЗАКЦИИ
function SE_OnTransOK(trans)
   -- Здесь Ваш код для действий при успешном выполнении транзакции
   -- ...
   -- Выводит сообщение
   message('SE_OnTransOK() Транзакция №'..trans.trans_id..' УСПЕШНО выполнена')
end
 
-- Вызывается движком при появлении НОВОЙ ЗАЯВКИ
function SE_OnNewOrder(order)
   -- Здесь Ваш код для действий при появлении новой заявки
   -- ...
   -- Выводит сообщение
   message('SE_OnNewOrder() Выставлена новая заявка №'..order.order_num..' по транзакции №'..order.trans_id..', инструменту '..order.sec_code..', цене '..order.price..', на объем '..order.qty)
end
-- Вызывается движком при полном, или частичном ИСПОЛНЕНИИ ЗАЯВКИ
function SE_OnExecutionOrder(order)
   -- Здесь Ваш код для действий при полном, или частичном исполнении заявки
   -- ...
   -- Выводит сообщение
   message('SE_OnExecutionOrder() БАЛАНС заявки №'..order.order_num..' изменился с '..(order.qty - (order.last_execution_count or 0))..' на '..order.balance)
end
 
-- Вызывается движком при появлении НОВОЙ СДЕЛКИ
function SE_OnNewTrade(trade)
   -- Здесь Ваш код для действий при появлении новой сделки
   -- ...
   -- Выводит сообщение
   message('SE_OnNewTrade() Новая СДЕЛКА №'..trade.trade_num..' по транзакции №'..trade.trans_id..' по цене '..trade.price..' объемом '..trade.qty)
end
 
-- Вызывается движком при остановке скрипта
function SE_OnStop()
 
end
-- =========================================================

Если у Вас появились какие-то вопросы, задайте их в комментариях под статьей !!!

Так же, напишите, пожалуйста, в комментариях свое мнению о таком подходе и нужен ли, по Вашему мнению, полноценный торговый движок для QLua.

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

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