Скрипт для алгоритмической торговли стоп на lua как писать: Очень подробно разжёвано для чайников по LUA часть1! – Пример скрипта QLua (Lua), выполняющего торговые операции в терминале QUIK

Пример скрипта QLua (Lua), выполняющего торговые операции в терминале QUIK

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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
-- Скрипт позволяет открывать и закрывать позиции следующего типа:
-- покупается 2, выбранных при помощи кнопок, колл-опциона на фьючерс на индекс РТС,
-- продается 1, выбранный при помощи кнопок, фьючерс на индекс РТС.
-- Когда есть открытая данным скриптом позиция, в таблице отображается информация о ней (профит, баланс, ср.цена, дата открытия).
-- Профит вычисляется по формуле: "Текущая цена, по которой можно закрыть позицию" - "Цена открытия позиции" - "Комиссия 8 р. на каждый лот"
-- Так же, выполняется запись в лог-файл ("Log.txt") выполняемых скриптом операций
-- и в файле состояния ("State.txt") хранится информация о текущих выбранных инструментах и о текущей, открытой скриптом позиции
-- ВАЖНО!!! При выборе опциона, кнопками «/\»,»\/» меняется страйк опциона с шагом 500, убедитесь в том, что такой опцион существует!!!
-- (c) QuikLuaCSharp.ru
 
-- Флаг поддержания работы скрипта
IsRun = true;
 
COMMISSION = 4;   -- Комиссия (примерно) (биржа + брокер) за 1-у торговую операцию по 1-му лоту опциона, или фьючерса
 
 
TRADE_ACC               = "SPBFUT00136";     -- Торговый счет
CLIENT_CODE             = "SPBFUT00136";     -- Код клиента
CLASS_CODE_OPT          = "SPBOPT";          -- Класс ОПЦИОНОВ
CLASS_CODE_FUT          = "SPBFUT";          -- Класс ФЬЮЧЕРСОВ
 
SEC_CODE_FUT_FOR_OPEN   = "RIM5";            -- Код ФЬЮЧЕРСА для открытия
SEC_CODE_OPT_FOR_OPEN   = "RI110000BF5";     -- Код ОПЦИОНА для открытия
SEC_CODE_OPT_IN_POS     = "";                -- Код ОПЦИОНА в позиции
OPEN_BALANCE_OPT        = 0;                 -- Баланс позиции по ОПЦИОНАМ
OPEN_PRICE_OPT          = 0;                 -- Средняя цена открытия позиции по ОПЦИОНАМ
OPEN_DATE_OPT           = "";                -- Дата открытия позиции по ОПЦИОНАМ
SEC_CODE_FUT_IN_POS     = "";                -- Код ФЬЮЧЕРСА в позиции
OPEN_BALANCE_FUT        = 0;                 -- Баланс позиции по ФЬЮЧЕРСАМ
OPEN_PRICE_FUT          = 0;                 -- Средняя цена открытия позиции по ФЬЮЧЕРСАМ
OPEN_DATE_FUT           = "";                -- Дата открытия позиции по ФЬЮЧЕРСАМ
 
-- Флаг для поддержания работы функции main
IsRun = true;
 
trans_id_OPT = nil;	-- ID заявки на опционы
OrderNum_OPT = nil; 	-- Номер заявки на опционы в торговой системе
trans_id_FUT = nil; 	-- ID заявки на фьючерс
OrderNum_FUT = nil; 	-- Номер заявки на фьючерс в торговой системе
 
-- Константы для строк таблицы
BUTTON_UP_FUTCODE_LINE     = 1;  -- Строка кнопки увеличения даты фьючерса
SEC_CODE_FUT_LINE          = 2;  -- Строка кода фьючерса
BUTTON_DOWN_FUTCODE_LINE   = 3;  -- Строка кнопки уменьшения даты фьючерса
                          -- 4;  -- Пустая строка
BUTTON_UP_STRIKE_LINE      = 5;  -- Строка кнопки увеличения значений страйка опциона
SEC_CODE_OPT_LINE          = 6;  -- Строка кода опциона
BUTTON_DOWN_STRIKE_LINE    = 7;  -- Строка кнопки уменьшения значений страйка опциона
                          -- 8;  -- Пустая строка
BUTTON_OPEN_LINE           = 9;  -- Строка с кнопкой "Открыть"
                          -- 10; -- Пустая строка
                          -- 11; -- Пустая строка
INFO_TITLE_LINE            = 12; -- Строка заголовков информации по позиции
OPT_PROFIT_LINE            = 13; -- Строка профита по опционам
FUT_PROFIT_LINE            = 14; -- Строка профита по фьючерсу
ALL_PROFIT_LINE            = 15; -- Строка суммарного профита
                          -- 16; -- Пустая строка
BUTTON_CLOSE_LINE          = 17; -- Строка с кнопкой "Закрыть" 
 
-- Флаги возможности открытия/закрытия позиции
MayOpen = true;
MayClose = false;
 
STATE = nil;               -- Флаг текущего процесса ("OPEN"/"CLOSE")
 
trans_id_OPT = nil;        -- ID транзакции на покупку ОПЦИОНОВ
trans_id_FUT = nil;        -- ID транзакции на продажу ФЬЮЧЕРСА    
 
NotBuy_OPT = true;         -- Флаг, что ОПЦИОНЫ еще не куплены
NotSell_OPT = true;        -- Флаг, что ОПЦИОНЫ еще не проданы
NotBuy_FUT = true;         -- Флаг, что ФЬЮЧЕРСЫ еще не куплены
NotSell_FUT = true;        -- Флаг, что ФЬЮЧЕРСЫ еще не проданы
OpenPosProcessed = false;  -- Флаг, что открытие позиций обработано
ClosePosProcessed = false; -- Флаг, что закрытие позиций обработано
 
OrderNum_OPT = nil;        -- Номер заявки на ОПЦИОНЫ в торговой системе
OrderNum_FUT = nil;        -- Номер заявки на ФЬЮЧЕРС в торговой системе
 
function OnInit()
   -- Пытается открыть лог-файл в режиме "чтения/записи"
   Log = io.open(getScriptPath().."//Log.txt","r+");
   -- Если файл не существует
   if Log == nil then 
      -- Создает файл в режиме "записи"
      Log = io.open(getScriptPath().."//Log.txt","w"); 
      -- Закрывает файл
      Log:close();
      -- Открывает уже существующий файл в режиме "чтения/записи"
      Log = io.open(getScriptPath().."//Log.txt","r+");
   end; 
   -- Встает в конец файла
   Log:seek("end",0);
   -- Добавляет пустую строку-разрыв
   Log:write("\n");Log:flush();
 
   -- Пытается открыть файл состояния в режиме "чтения/записи"
   local f = io.open(getScriptPath().."//State.txt","r+");
   -- Если файл существует
   if f ~= nil then 
      -- Перебирает строки файла, считывает содержимое в соответствующие переменные
      local Count = 0; -- Счетчик строк
      for line in f:lines() do
         Count = Count + 1;
         if Count == 1 then SEC_CODE_FUT_FOR_OPEN = line;         -- Код ФЬЮЧЕРСА для открытия
         elseif Count == 2 then SEC_CODE_OPT_FOR_OPEN = line;     -- Код ОПЦИОНА для открытия
         elseif Count == 3 then SEC_CODE_OPT_IN_POS = line;       -- Код ОПЦИОНА в позиции
            -- ЕСЛИ есть открытая позиция, ТО разрешается закрывать позицию, ИНАЧЕ, разрешается открывать позицию
            if SEC_CODE_OPT_IN_POS ~= "" then 
               MayOpen = false;
               MayClose = true; 
               NotBuy_OPT = true;
               NotSell_FUT = true;
            else MayOpen = true; end;
         elseif Count == 4 then OPEN_BALANCE_OPT = tonumber(line);   -- Количество лотов в позиции по ОПЦИОНАМ
         elseif Count == 5 then OPEN_PRICE_OPT = tonumber(line);  -- Средняя цена открытия позиции по ОПЦИОНАМ
         elseif Count == 6 then OPEN_DATE_OPT = line;             -- Дата открытия позиции по ОПЦИОНАМ
         elseif Count == 7 then SEC_CODE_FUT_IN_POS = line;       -- Код ФЬЮЧЕРСА в позиции
         elseif Count == 8 then OPEN_BALANCE_FUT = tonumber(line);   -- Количество лотов в позиции по ФЬЮЧЕРСАМ
         elseif Count == 9 then OPEN_PRICE_FUT = tonumber(line);  -- Средняя цена открытия позиции по ФЬЮЧЕРСАМ
         elseif Count == 10 then OPEN_DATE_FUT = line;            -- Дата открытия позиции по ФЬЮЧЕРСАМ
         end;
      end;   
      -- Закрывает файл
      f:close();
   end;   
 
   -- Создает таблицу
   CreateTable();
   -- Назначает таблице t_id функцию обработки событий event_callback_message
   SetTableNotificationCallback(t_id, event_callback_message);  
 
   -- Инициализирует генератор
   math.randomseed(os.date("*t",os.time()).sec); -- Инициализирует генератор псевдослучайных чисел параметром, каждый параметр порождает соответствующую (но одну и ту же) последовательность псевдослучайных чисел.    
end;             
 
function main()
   -- Цикл будет выполнятся, пока IsRun == true
   while IsRun do
 
   -- ЕСЛИ текущий процесс ОТКРЫТИЕ позиции и ОПЦИОНЫ куплены и ФЬЮЧЕРС продан и открытие позиций еще не обработано, ТО
   if STATE == "OPEN" and not NotBuy_OPT and not NotSell_FUT and not OpenPosProcessed then
      -- Функция обработки открытия позиций
      OpenPosProcess();
   end;
 
   -- ЕСЛИ текущий процесс ЗАКРЫТИЕ позиции и ОПЦИОНЫ проданы и ФЬЮЧЕРС куплен и закрытие позиций еще не обработано, ТО
   if STATE == "CLOSE" and not NotSell_OPT and not NotBuy_FUT and not ClosePosProcessed then
      -- Функция обработки закрытия позиций
      ClosePosProcess();
   end;
 
      sleep(100);
   end;   
end;
 
-- Функция для записи в лог действий скрипта
function ToLog(str)
   local datetime = os.date("*t",os.time()); -- Текущие дата/время
   local sec_mcs_str = tostring(os.clock()); -- Секунды с микросекундами 
   local mcs_str = string.sub(sec_mcs_str, sec_mcs_str:find("%.") + 1);   -- Микросекунды
   -- Записывает в лог-файл переданную строку, добавляя в ее начало время с точностью до микросекунд
   Log:write(tostring(datetime.day).."-"
            ..tostring(datetime.month).."-"
            ..tostring(datetime.year).." "
            ..tostring(datetime.hour)..":"
            ..tostring(datetime.min)..":"
            ..tostring(datetime.sec).."."
            ..mcs_str.." "
            ..str.."\n");  -- Записывает в лог-файл
   Log:flush();   -- Сохраняет изменения в лог-файле
end;
 
-- Функция ОТКРЫВАЕТ позицию
function OpenPosition()   
   ToLog("OpenPosition"); -- Записывает в лог-файл
   -- Устанавливает флаг текущего процесса в ОТКРЫТИЕ позиции
   STATE = "OPEN";
   -- Сбрасывает флаг обработки открытия позиций
   OpenPosProcessed = false;
 
   trans_id_OPT = math.random(1,9999);
   ToLog("trans_id_OPT = "..tostring(trans_id_OPT)); -- Записывает в лог-файл
   -- Заполняет структуру для отправки транзакции на покупку 2-х ОПЦИОНОВ
   local Transaction={
      ["TRANS_ID"]   = tostring(trans_id_OPT),
      ["ACTION"]     = "NEW_ORDER",
      ["CLASSCODE"]  = CLASS_CODE_OPT,
      ["SECCODE"]    = SEC_CODE_OPT_FOR_OPEN,
      ["OPERATION"]  = "B", -- покупка (BUY)
      ["TYPE"]       = "M", -- по рынку (MARKET)
      ["QUANTITY"]   = "2", -- количество
      ["ACCOUNT"]    = TRADE_ACC,
      ["PRICE"]      = tostring(getParamEx(CLASS_CODE_OPT, SEC_CODE_OPT_FOR_OPEN, "offer").param_value + 10*getParamEx(CLASS_CODE_OPT, SEC_CODE_OPT_FOR_OPEN, "SEC_PRICE_STEP").param_value), -- по цене, завышенной на 10 мин. шагов цены
      ["COMMENT"]    = "Покупка опционов скриптом"
   }
   -- Флаг, что ОПЦИОНЫ еще не куплены
   NotBuy_OPT = true;
   local Result = sendTransaction(Transaction);
   ToLog("Заявка на покупку ОПЦИОНОВ отправлена"); -- Записывает в лог-файл
   ToLog("Result = "..Result); -- Записывает в лог-файл
   -- ЕСЛИ функция вернула строку диагностики ошибки, ТО значит транзакция не прошла
   if Result ~= "" then
      -- Выводит сообщение с ошибкой
      message("Покупка ОПЦИОНОВ не удалась!\nОШИБКА: "..Result);
      -- Завершает выполнение функции
      return(0);
   end;   
 
   -- Генерирует случайный ID транзакции на продажу ФЬЮЧЕРСОВ
   trans_id_FUT = math.random(1,9999);
   ToLog("trans_id_FUT = "..tostring(trans_id_FUT)); -- Записывает в лог-файл
   -- Заполняет структуру для отправки транзакции на продажу ФЬЮЧЕРСОВ
   local Transaction={
      ["TRANS_ID"]   = tostring(trans_id_FUT),
      ["ACTION"]     = "NEW_ORDER",
      ["CLASSCODE"]  = CLASS_CODE_FUT,
      ["SECCODE"]    = SEC_CODE_FUT_FOR_OPEN,
      ["OPERATION"]  = "S", -- продажа (SELL)
      ["TYPE"]       = "M", -- по рынку (MARKET)
      ["QUANTITY"]   = "1", -- количество
      ["ACCOUNT"]    = TRADE_ACC,
      ["PRICE"]      = tostring(getParamEx(CLASS_CODE_FUT, SEC_CODE_FUT_FOR_OPEN, "bid").param_value - 10*getParamEx(CLASS_CODE_FUT, SEC_CODE_FUT_FOR_OPEN, "SEC_PRICE_STEP").param_value), -- по цене, заниженной на 10 мин. шагов цены
      ["COMMENT"]    = "Продажа фьючерсов скриптом"
   }
   -- Флаг, что ФЬЮЧЕРСЫ еще не проданы
   NotSell_FUT = true;
   local Result = sendTransaction(Transaction);
   ToLog("Заявка на продажу ФЬЮЧЕРСОВ отправлена"); -- Записывает в лог-файл
   ToLog("Result = "..Result); -- Записывает в лог-файл
   -- ЕСЛИ функция вернула строку диагностики ошибки, ТО значит транзакция не прошла
   if Result ~= "" then
      -- Выводит сообщение с ошибкой
      message("Продажа ФЬЮЧЕРСОВ не удалась!\nОШИБКА: "..Result);
      -- Завершает выполнение функции
      return(0);
   end;   
end;
-- Функция ЗАКРЫВАЕТ позицию
function ClosePosition()
   ToLog("ClosePosition"); -- Записывает в лог-файл
   -- Устанавливает флаг текущего процесса  в ЗАКРЫТИЕ позиции
   STATE = "CLOSE";
   -- Сбрасывает флаг обработки закрытия позиций
   ClosePosProcessed = false;
 
   trans_id_OPT = math.random(1,9999);
   ToLog("trans_id_OPT = "..tostring(trans_id_OPT)); -- Записывает в лог-файл
   -- Заполняет структуру для отправки транзакции на продажу 2-х ОПЦИОНОВ
   local Transaction={
      ["TRANS_ID"]   = tostring(trans_id_OPT),
      ["ACTION"]     = "NEW_ORDER",
      ["CLASSCODE"]  = CLASS_CODE_OPT,
      ["SECCODE"]    = SEC_CODE_OPT_IN_POS,
      ["OPERATION"]  = "S", -- продажа (SELL)
      ["TYPE"]       = "M", -- по рынку (MARKET)
      ["QUANTITY"]   = "2", -- количество
      ["ACCOUNT"]    = TRADE_ACC,
      ["PRICE"]      = tostring(getParamEx(CLASS_CODE_OPT, SEC_CODE_OPT_FOR_OPEN, "bid").param_value - 10*getParamEx(CLASS_CODE_OPT, SEC_CODE_OPT_FOR_OPEN, "SEC_PRICE_STEP").param_value), -- по цене, заниженной на 10 мин. шагов цены
      ["COMMENT"]    = "Продажа опционов скриптом"
   }
   -- Флаг, что ОПЦИОНЫ еще не проданы
   NotSell_OPT = true;
   local Result = sendTransaction(Transaction);
   ToLog("Заявка на продажу ОПЦИОНОВ отправлена"); -- Записывает в лог-файл
   ToLog("Result = "..Result); -- Записывает в лог-файл
   -- ЕСЛИ функция вернула строку диагностики ошибки, ТО значит транзакция не прошла
   if Result ~= "" then
      -- Выводит сообщение с ошибкой
      message("Продажа ОПЦИОНОВ не удалась!\nОШИБКА: "..Result);
      -- Завершает выполнение функции
      return(0);
   end;   
 
   -- Генерирует случайный ID транзакции на покупку ФЬЮЧЕРСОВ
   trans_id_FUT = math.random(1,9999);
   ToLog("trans_id_FUT = "..tostring(trans_id_FUT)); -- Записывает в лог-файл
   -- Заполняет структуру для отправки транзакции на покупку ФЬЮЧЕРСОВ
   local Transaction={
      ["TRANS_ID"]   = tostring(trans_id_FUT),
      ["ACTION"]     = "NEW_ORDER",
      ["CLASSCODE"]  = CLASS_CODE_FUT,
      ["SECCODE"]    = SEC_CODE_FUT_IN_POS,
      ["OPERATION"]  = "B", -- покупка (BUY)
      ["TYPE"]       = "M", -- по рынку (MARKET)
      ["QUANTITY"]   = "1", -- количество
      ["ACCOUNT"]    = TRADE_ACC,
      ["PRICE"]      = tostring(getParamEx(CLASS_CODE_FUT, SEC_CODE_FUT_FOR_OPEN, "offer").param_value + 10*getParamEx(CLASS_CODE_FUT, SEC_CODE_FUT_FOR_OPEN, "SEC_PRICE_STEP").param_value), -- по цене, завышенной на 10 мин. шагов цены
      ["COMMENT"]    = "Покупка фьючерсов скриптом"
   }
   -- Флаг, что ФЬЮЧЕРСЫ еще не куплены
   NotBuy_FUT = true;
   local Result = sendTransaction(Transaction);
   ToLog("Заявка на покупку ФЬЮЧЕРСОВ отправлена"); -- Записывает в лог-файл
   ToLog("Result = "..Result); -- Записывает в лог-файл
   -- ЕСЛИ функция вернула строку диагностики ошибки, ТО значит транзакция не прошла
   if Result ~= "" then
      -- Выводит сообщение с ошибкой
      message("Покупка ФЬЮЧЕРСОВ не удалась!\nОШИБКА: "..Result);
      -- Завершает выполнение функции
      return(0);
   end;
end;
-- Функция обработки открытия позиций
function OpenPosProcess()
   OPEN_BALANCE_OPT = 2;   -- Запоминает баланс позиции ОПЦИОНОВ
   OPEN_BALANCE_FUT = -1;  -- Запоминает баланс позиции ФЬЮЧЕРСОВ
 
   -- Получает среднюю цену позиции по ОПЦИОНАМ
   local LotSumOPT = 0; -- Общее количество лотов ОПЦИОНОВ в сделке(ах) по данной заявке
   local LotSumFUT = 0; -- Общее количество лотов ФЬЮЧЕРСОВ в сделке(ах) по данной заявке
   -- Бесконечный цикл, на случай, если информация о сделке придет позже
   while true do
      -- Обнуляет перед каждым новым проходом по таблице сделок
      OPEN_PRICE_OPT = 0;
      OPEN_PRICE_FUT = 0;
      LotSumOPT = 0;
      LotSumFUT = 0;
      -- Перебирает все строки в таблице сделок
      for i = 0,getNumberOf("TRADES") - 1 do
         -- ЕСЛИ сделка по ОПЦИОННОЙ заявке, ТО
         if getItem("TRADES",i).order_num == OrderNum_OPT then
            -- Считает сумму цен в сделках
            OPEN_PRICE_OPT = OPEN_PRICE_OPT + getItem("TRADES",i).price * getItem("TRADES",i).qty;
            -- Считает количество лотов в сделках
            LotSumOPT = LotSumOPT + getItem("TRADES",i).qty;
            -- Запоминает дату открытия позиции по ОПЦИОНАМ
            OPEN_DATE_OPT = tostring(getItem("TRADES",i).datetime.day).."-"..tostring(getItem("TRADES",i).datetime.month).."-"..tostring(getItem("TRADES",i).datetime.year);
         elseif getItem("TRADES",i).order_num == OrderNum_FUT then -- ЕСЛИ сделка по ФЬЮЧЕРСНОЙ заявке, ТО
            -- Считает сумму цен в сделках
            OPEN_PRICE_FUT = OPEN_PRICE_FUT + getItem("TRADES",i).price * getItem("TRADES",i).qty;
            -- Считает количество лотов в сделках
            LotSumFUT = LotSumFUT + getItem("TRADES",i).qty;
            -- Запоминает дату открытия позиции по ФЬЮЧЕРСАМ
            OPEN_DATE_FUT = tostring(getItem("TRADES",i).datetime.day).."-"..tostring(getItem("TRADES",i).datetime.month).."-"..tostring(getItem("TRADES",i).datetime.year);
         end;
      end;
      -- ЕСЛИ все лоты найдены в сделках по ОПЦИОННОЙ и ФЬЮЧЕРСНОЙ заявкам, ТО
      if LotSumOPT == math.abs(OPEN_BALANCE_OPT) and LotSumFUT == math.abs(OPEN_BALANCE_FUT) then
         ToLog("LotSumOPT = "..tostring(LotSumOPT).." LotSumFUT = "..tostring(LotSumFUT));
         -- Запоминает код ОПЦИОНА в позиции
         SEC_CODE_OPT_IN_POS = SEC_CODE_OPT_FOR_OPEN;
         -- Запоминает код ФЬЮЧЕРСА в позиции
         SEC_CODE_FUT_IN_POS = SEC_CODE_FUT_FOR_OPEN;
 
         -- Вычисляет среднюю цену по ОПЦИОНАМ
         OPEN_PRICE_OPT = OPEN_PRICE_OPT/math.abs(OPEN_BALANCE_OPT);            
         -- Вычисляет среднюю цену по ФЬЮЧЕРСАМ
         OPEN_PRICE_FUT = OPEN_PRICE_FUT/math.abs(OPEN_BALANCE_FUT);
 
         -- Меняет флаги возможности открытия/закрытия позиции
         MayOpen = false;
         MayClose = true;
         -- Событие открытия позиций обработано
         OpenPosProcessed = true;
         -- Текущий процесс отсутствует
         STATE = nil;
 
         -- Выводит в таблицу новые данные
         SetCell(t_id, OPT_PROFIT_LINE, 2, tostring(OPEN_BALANCE_OPT));   SetCell(t_id, OPT_PROFIT_LINE, 3, tostring(OPEN_PRICE_OPT));   SetCell(t_id, OPT_PROFIT_LINE, 4, tostring(OPEN_DATE_OPT));
         SetCell(t_id, FUT_PROFIT_LINE, 2, tostring(OPEN_BALANCE_FUT));   SetCell(t_id, FUT_PROFIT_LINE, 3, tostring(OPEN_PRICE_FUT));   SetCell(t_id, FUT_PROFIT_LINE, 4, tostring(OPEN_DATE_FUT));
         -- Меняет подсветку кнопок
         Gray(BUTTON_OPEN_LINE, 1);
         Green(BUTTON_CLOSE_LINE, 1);
 
         break;   -- Завершает бесконечный цикл
      end;
      sleep(100);
   end;
end;
-- Функция обработки закрытия позиций
function ClosePosProcess()
   -- Обнуляет все флаги
   SEC_CODE_OPT_IN_POS     = "";                -- Код ОПЦИОНА в позиции
   OPEN_BALANCE_OPT        = 0;                 -- Баланс позиции по ОПЦИОНАМ
   OPEN_PRICE_OPT          = 0;                 -- Средняя цена открытия позиции по ОПЦИОНАМ
   OPEN_DATE_OPT           = "";                -- Дата открытия позиции по ОПЦИОНАМ
   SEC_CODE_FUT_IN_POS     = "";                -- Код ФЬЮЧЕРСА в позиции
   OPEN_BALANCE_FUT        = 0;                 -- Баланс позиции по ФЬЮЧЕРСАМ
   OPEN_PRICE_FUT          = 0;                 -- Средняя цена открытия позиции по ФЬЮЧЕРСАМ
   OPEN_DATE_FUT           = "";                -- Дата открытия позиции по ФЬЮЧЕРСАМ
 
   -- Меняет флаги возможности открытия/закрытия позиции
   MayOpen = true;
   MayClose = false;
 
   -- Событие открытия позиций обработано
   ClosePosProcessed = true;
   -- Текущий процесс отсутствует
   STATE = nil;
 
   -- Выводит в таблицу новые данные
   SetCell(t_id, OPT_PROFIT_LINE, 1, tostring(0)); SetCell(t_id, OPT_PROFIT_LINE, 2, tostring(OPEN_BALANCE_OPT)); SetCell(t_id, OPT_PROFIT_LINE, 3, tostring(OPEN_PRICE_OPT));   SetCell(t_id, OPT_PROFIT_LINE, 4, tostring(OPEN_DATE_OPT));
   SetCell(t_id, FUT_PROFIT_LINE, 1, tostring(0)); SetCell(t_id, FUT_PROFIT_LINE, 2, tostring(OPEN_BALANCE_FUT)); SetCell(t_id, FUT_PROFIT_LINE, 3, tostring(OPEN_PRICE_FUT));   SetCell(t_id, FUT_PROFIT_LINE, 4, tostring(OPEN_DATE_FUT));
   SetCell(t_id, ALL_PROFIT_LINE, 1, tostring(0));
   -- Меняет подсветку кнопок
   Green(BUTTON_OPEN_LINE, 1);
   Gray(BUTTON_CLOSE_LINE, 1);
end;
 
-- Функция обработки событий в таблице
function event_callback_message(t_id, msg, par1, par2)
   -- ЕСЛИ нажата кнопка "/\" для ФЬЮЧЕРСА, ТО
   if msg == QTABLE_LBUTTONUP and par1 == BUTTON_UP_FUTCODE_LINE and par2 == 1 then
      -- Получает символьную часть ФЬЮЧЕРСА (RIH->RIM->RIU->RIZ)
      local FutSecCode_STR = string.sub(SEC_CODE_FUT_FOR_OPEN, 1, 3);
      -- Получает числовую часть (0-9)
      local FutSecCode_NUM = tonumber(string.sub(SEC_CODE_FUT_FOR_OPEN, 4, 4));
      -- Увеличивает значение 
      if FutSecCode_STR == "RIH" then FutSecCode_STR = "RIM";
      elseif FutSecCode_STR == "RIM" then FutSecCode_STR = "RIU";
      elseif FutSecCode_STR == "RIU" then FutSecCode_STR = "RIZ"; 
      elseif FutSecCode_STR == "RIZ" then 
         FutSecCode_STR = "RIH";
         FutSecCode_NUM = FutSecCode_NUM + 1;
         if FutSecCode_NUM == 10 then FutSecCode_NUM = 0; end;
      end;
 
      -- Запоминает новое значение ФЬЮЧЕРСА для открытия
      SEC_CODE_FUT_FOR_OPEN = FutSecCode_STR..tostring(FutSecCode_NUM);
      -- Выводит новое значение ФЬЮЧЕРСА в таблицу
      SetCell(t_id, SEC_CODE_FUT_LINE, 1, SEC_CODE_FUT_FOR_OPEN.."      ");
 
      -- Подстраивает значение ОПЦИОНА под выбранный ФЬЮЧЕРС и запоминает его
      SEC_CODE_OPT_FOR_OPEN = string.sub(SEC_CODE_OPT_FOR_OPEN, 1, #SEC_CODE_OPT_FOR_OPEN - 1)..tostring(FutSecCode_NUM);
      -- Выводит новое значение ОПЦИОНА в таблицу
      SetCell(t_id, SEC_CODE_OPT_LINE, 1, SEC_CODE_OPT_FOR_OPEN);
   end;
 
   -- ЕСЛИ нажата кнопка "\/" для ФЬЮЧЕРСА, ТО
   if msg == QTABLE_LBUTTONUP and par1 == BUTTON_DOWN_FUTCODE_LINE and par2 == 1 then
      -- Получает текущее значение
      local FutSecCode = GetCell(t_id, SEC_CODE_FUT_LINE, 1);
      -- Получает символьную часть (RIH->RIM->RIU->RIZ)
      local FutSecCode_STR = string.sub(FutSecCode.image, 1, 3);
      -- Получает числовую часть (0-9)
      local FutSecCode_NUM = tonumber(string.sub(FutSecCode.image, 4, 4));
      -- Уменьшает значение 
      if FutSecCode_STR == "RIZ" then FutSecCode_STR = "RIU";
      elseif FutSecCode_STR == "RIU" then FutSecCode_STR = "RIM";
      elseif FutSecCode_STR == "RIM" then FutSecCode_STR = "RIH"; 
      elseif FutSecCode_STR == "RIH" then 
         FutSecCode_STR = "RIZ";
         FutSecCode_NUM = FutSecCode_NUM - 1;
         if FutSecCode_NUM == -1 then FutSecCode_NUM = 9; end;
      end;
 
      -- Запоминает новое значение ФЬЮЧЕРСА для открытия
      SEC_CODE_FUT_FOR_OPEN = FutSecCode_STR..tostring(FutSecCode_NUM);
      -- Выводит новое значение ФЬЮЧЕРСА в таблицу
      SetCell(t_id, SEC_CODE_FUT_LINE, 1, SEC_CODE_FUT_FOR_OPEN.."      ");
 
      -- Подстраивает значение ОПЦИОНА под выбранный ФЬЮЧЕРС и запоминает его
      SEC_CODE_OPT_FOR_OPEN = string.sub(SEC_CODE_OPT_FOR_OPEN, 1, #SEC_CODE_OPT_FOR_OPEN - 1)..tostring(FutSecCode_NUM);
      -- Выводит новое значение ОПЦИОНА в таблицу
      SetCell(t_id, SEC_CODE_OPT_LINE, 1, SEC_CODE_OPT_FOR_OPEN);
   end;
 
   -- ЕСЛИ нажата кнопка "/\" для ОПЦИОНА, ТО
   if msg == QTABLE_LBUTTONUP and par1 == BUTTON_UP_STRIKE_LINE and par2 == 1 then
      -- Получает цифры СТРАЙКА из кода ОПЦИОНА
      local OptSecCode_STRIKE = tonumber(string.sub(SEC_CODE_OPT_FOR_OPEN, 3, #SEC_CODE_OPT_FOR_OPEN - 3));
      -- Увеличивает СТРАЙК (здесь задан шаг 500, смотрите, чтобы такой опцион существовал!!!)
      OptSecCode_STRIKE = OptSecCode_STRIKE + 500;
 
      -- Подстраивает значение ОПЦИОНА, запоминает
      SEC_CODE_OPT_FOR_OPEN = string.sub(SEC_CODE_OPT_FOR_OPEN, 1, 2)..tostring(OptSecCode_STRIKE)..string.sub(SEC_CODE_OPT_FOR_OPEN, #SEC_CODE_OPT_FOR_OPEN - 2, #SEC_CODE_OPT_FOR_OPEN);
      -- Выводит новое значение ОПЦИОНА в таблицу
      SetCell(t_id, SEC_CODE_OPT_LINE, 1, SEC_CODE_OPT_FOR_OPEN);
   end;
 
   -- ЕСЛИ нажата кнопка "\/" для ОПЦИОНА, ТО
   if msg == QTABLE_LBUTTONUP and par1 == BUTTON_DOWN_STRIKE_LINE and par2 == 1 then
      -- Получает цифры СТРАЙКА из кода ОПЦИОНА
      local OptSecCode_STRIKE = tonumber(string.sub(SEC_CODE_OPT_FOR_OPEN, 3, #SEC_CODE_OPT_FOR_OPEN - 3));
      -- Уменьшает СТРАЙК (здесь задан шаг 500, смотрите, чтобы такой опцион существовал!!!)
      OptSecCode_STRIKE = OptSecCode_STRIKE - 500;
 
      -- Подстраивает значение ОПЦИОНА, запоминает
      SEC_CODE_OPT_FOR_OPEN = string.sub(SEC_CODE_OPT_FOR_OPEN, 1, 2)..tostring(OptSecCode_STRIKE)..string.sub(SEC_CODE_OPT_FOR_OPEN, #SEC_CODE_OPT_FOR_OPEN - 2, #SEC_CODE_OPT_FOR_OPEN);
      -- Выводит новое значение ОПЦИОНА в таблицу
      SetCell(t_id, SEC_CODE_OPT_LINE, 1, SEC_CODE_OPT_FOR_OPEN);
   end;
 
   -- ЕСЛИ нажата кнопка "Открыть", ТО
   if msg == QTABLE_LBUTTONUP and par1 == BUTTON_OPEN_LINE and par2 == 1 then
      -- ЕСЛИ нет возможности открыть позицию (уже открыта), ТО
      if not MayOpen then
         -- Выводит сообщение
         message("ЗАПРЕЩЕНО! Позиция уже открыта!!!", 3);
      else -- ИНАЧЕ
         -- Открывает позицию
         OpenPosition();
      end;
   end;
 
   -- ЕСЛИ нажата кнопка "Закрыть"
   if msg == QTABLE_LBUTTONUP and par1 == BUTTON_CLOSE_LINE and par2 == 1 then
      -- ЕСЛИ нет возможности закрыть позицию (еще не открыта), ТО
      if not MayClose then
         -- Выводит сообщение
         message("НЕВОЗМОЖНО! Позиция еще не открыта!!!", 3);
      else -- ИНАЧЕ
         -- Закрывает позицию
         ClosePosition();
      end;
   end;
end;
 
--- Функция создает таблицу
function CreateTable()
   -- Получает доступный id для создания
   t_id = AllocTable();	
   -- Добавляет 5 колонок
   AddColumn(t_id, 0, "", true, QTABLE_STRING_TYPE, 24);
   AddColumn(t_id, 1, "", true, QTABLE_INT_TYPE, 15);
   AddColumn(t_id, 2, "", true, QTABLE_STRING_TYPE, 15);
   AddColumn(t_id, 3, "", true, QTABLE_STRING_TYPE, 15);
   AddColumn(t_id, 4, "", true, QTABLE_STRING_TYPE, 15);
   -- Создает таблицу
   t = CreateWindow(t_id);
   -- Устанавливает заголовок	
   SetWindowCaption(t_id, "Открыть/закрыть +2 ОПЦИОНА -1 ФЬЮЧЕРС");
   -- Задает положение и размеры окна таблицы
   SetWindowPos(t_id, 100, 100, 472, 325);
   -- Добавляет строки
   InsertRow(t_id, BUTTON_UP_FUTCODE_LINE);  -- Строка кнопки увеличения даты фьючерса
   InsertRow(t_id, SEC_CODE_FUT_LINE);       -- Строка кода фьючерса
   InsertRow(t_id, BUTTON_DOWN_FUTCODE_LINE);-- Строка кнопки уменьшения даты фьючерса
   InsertRow(t_id, 4);                       -- Пустая строка
   InsertRow(t_id, BUTTON_UP_STRIKE_LINE);   -- Строка кнопки увеличения значений страйка опциона
   InsertRow(t_id, SEC_CODE_OPT_LINE);       -- Строка кода опциона
   InsertRow(t_id, BUTTON_DOWN_STRIKE_LINE); -- Строка кнопки уменьшения значений страйка опциона
   InsertRow(t_id, 8);                       -- Пустая строка
   InsertRow(t_id, BUTTON_OPEN_LINE);        -- Строка с кнопкой "Открыть" 
   InsertRow(t_id, 10);                      -- Пустая строка
   InsertRow(t_id, 11);                      -- Пустая строка 
   InsertRow(t_id, INFO_TITLE_LINE);         -- Строка заголовков информации по позиции
   InsertRow(t_id, OPT_PROFIT_LINE);         -- Строка профита по опционам
   InsertRow(t_id, FUT_PROFIT_LINE);         -- Строка профита по фьючерсу
   InsertRow(t_id, ALL_PROFIT_LINE);         -- Строка суммарного профита
   InsertRow(t_id, 16);                      -- Пустая строка
   InsertRow(t_id, BUTTON_CLOSE_LINE);       -- Строка с кнопкой "Закрыть"   
   -- Добавляет значения в ячейки, окрашивает кнопки
                                                         SetCell(t_id, BUTTON_UP_FUTCODE_LINE, 1, "/\\         ");Green(BUTTON_UP_FUTCODE_LINE, 1);
   SetCell(t_id, SEC_CODE_FUT_LINE, 0, "Код ФЬЮЧЕРСА");  SetCell(t_id, SEC_CODE_FUT_LINE, 1, SEC_CODE_FUT_FOR_OPEN.."      ");
                                                         SetCell(t_id, BUTTON_DOWN_FUTCODE_LINE, 1, "\\/         ");Green(BUTTON_DOWN_FUTCODE_LINE, 1);
 
                                                         SetCell(t_id, BUTTON_UP_STRIKE_LINE, 1, "/\\         ");Green(BUTTON_UP_STRIKE_LINE, 1);
   SetCell(t_id, SEC_CODE_OPT_LINE, 0, "Код ОПЦИОНА");   SetCell(t_id, SEC_CODE_OPT_LINE, 1, SEC_CODE_OPT_FOR_OPEN);
                                                         SetCell(t_id, BUTTON_DOWN_STRIKE_LINE, 1, "\\/         ");Green(BUTTON_DOWN_STRIKE_LINE, 1);
 
                                                         SetCell(t_id, BUTTON_OPEN_LINE, 1, "ОТКРЫТЬ   ");  
                                                         if MayOpen then Green(BUTTON_OPEN_LINE, 1); 
                                                         else Gray(BUTTON_OPEN_LINE, 1); 
                                                         end;
 
                                                         SetCell(t_id, INFO_TITLE_LINE, 1, "Профит");    SetCell(t_id, INFO_TITLE_LINE, 2, "Кол-во откр.");            SetCell(t_id, INFO_TITLE_LINE, 3, "Ср.цена поз.");             SetCell(t_id, INFO_TITLE_LINE, 4, "Дата откр.");
   SetCell(t_id, OPT_PROFIT_LINE, 0, "ОПЦИОНЫ");         SetCell(t_id, OPT_PROFIT_LINE, 1, tostring(0)); SetCell(t_id, OPT_PROFIT_LINE, 2, tostring(OPEN_BALANCE_OPT));   SetCell(t_id, OPT_PROFIT_LINE, 3, tostring(OPEN_PRICE_OPT));   SetCell(t_id, OPT_PROFIT_LINE, 4, tostring(OPEN_DATE_OPT));
   SetCell(t_id, FUT_PROFIT_LINE, 0, "ФЬЮЧЕРСЫ");        SetCell(t_id, FUT_PROFIT_LINE, 1, tostring(0)); SetCell(t_id, FUT_PROFIT_LINE, 2, tostring(OPEN_BALANCE_FUT));   SetCell(t_id, FUT_PROFIT_LINE, 3, tostring(OPEN_PRICE_FUT));   SetCell(t_id, FUT_PROFIT_LINE, 4, tostring(OPEN_DATE_FUT));
   SetCell(t_id, ALL_PROFIT_LINE, 0, "ИТОГО")

ПОСТАНОВКА И СНЯТИЕ STOP-ОРДЕРА В QLUA(LUA)

Когда передо мной встала задача удаления поставленного стоп-ордера, наткнулся в интернете на скудность информации по данной тематике.

Самая распространенная ошибка начинающего программиста отправка в SendTransaction в STOP_ORDER_KEY  trans_id стоп-ордера

Робот выставляет стоп-заявку на покупку по определенной цене, затем через 2 секунды снимает её.

Также в коде имеются следующие фишки:

  • Запись удобочитаемого лог-файла.  Записи с интервалом <=1 сек. группируются в пул. Между пулами — пустая строка. После остановки скрипта в файл добавляется двойная линия.
  • Функция преобразования числа в строку с удалением точки и нулей правее нее для отправки этой строки в SendTransaction
  • Функция, возвращающая Entry или Exit в зависимости от trans_id принадлежности транзакций к входу или выходу
[Начало:LUA]
--/*НАСТРАИВАЕМЫЕ ПАРАМЕТРЫ*/
NAME_OF_STRATEGY = 'Str1_' -- НАЗВАНИЕ СТРАТЕГИИ (не более 9 символов!)
CLASS_CODE = "QJSIM" -- Код класса SPBFUT
SEC_CODE = "SBER" -- Код бумаги SiZ6 SiH7, SiM7, SiU7
ACCOUNT = "NL0011100043" -- Идентификатор счета SPBFUT00355
CLIENT_CODE = NAME_OF_STRATEGY..SEC_CODE -- "Код клиента"
QTY_LOTS = "1" -- Кол-во торгуемых лотов
FILE_LOG_NAME = "C:\\TRADING\\QUIK Junior\\Scripts\\Log.txt" -- ИМЯ ЛОГ-ФАЙЛА
 
--/*РАБОЧИЕ ПЕРЕМЕННЫЕ РОБОТА (менять не нужно)*/
g_price_step = 0 -- ШАГ ЦЕНЫ ИНСТРУМЕНТА
g_trans_id_entry = 110001 -- Задает начальный номер ID транзакций на вход
g_trans_id_exit = 220001 -- Задает начальный номер ID транзакций на выход
g_arrTransId_entry = {} -- массив ID транзакций на вход
g_arrTransId_exit = {} -- массив ID транзакций на выход
g_transId_del_order = "1234" -- ID ордера на удаление заявки (не меняется)
g_transId_del_stopOrder = "6789" -- ID ордера на удаление стоп заявки (не меняется)
g_currentPosition = 0 -- в позиции? сколько лотов и какое направление
g_IsTrallingStop = false -- выставлен ли трейлинг стоп на сервере
g_stopOrderEntry_num= "" -- номер стоп-заявки на вход в системе, по которому её можно снять
g_stopOrderExit_num = "" -- номер стоп-заявки на выход в системе, по которому её можно снять
g_order_num = "" -- номер заявки в системе, по которому её можно снять
g_oldTrade_num = "" -- номер предыдущей обработанной сделки
g_previous_time = os.time() -- помещение в переменную времени сервера в формате HHMMSS 
isRun = true -- Флаг поддержания работы бесконечного цикла в main

function OnInit()
   -- Получает ШАГ ЦЕНЫ ИНСТРУМЕНТА
    g_price_step = getParamEx(CLASS_CODE, SEC_CODE, "SEC_PRICE_STEP").param_value
 
    f = io.open(FILE_LOG_NAME, "a+") -- открывает файл 
    myLog("Initialization finished")
end
function main()
   g_trans_id_exit = g_trans_id_exit + 1
   g_arrTransId_exit[#g_arrTransId_exit+1] = g_trans_id_exit
 
   SendStopOrder("131.9", QTY_LOTS, "B", g_trans_id_exit) -- Отправить стоп ордер
   sleep(2000)                                            -- через 2 секунды
   DeleteStopOrder(g_stopOrderExit_num)                   -- удалить стоп-ордер
 
   while isRun do
      sleep(5000) -- обрабатываем цикл с задержкой 5сек. 
   end
end

-- функция вызывается терминалом ТЕРМИНАЛОМ QUIK при остановке скрипта
function OnStop()
   myLog("Script Stoped") 
   f:close() -- Закрывает файл
   isRun = false
end

function SendStopOrder(stopPrice, quantity, operation, trans_id)
   local offset=50 -- отступ для гарантированного исполнения ордера по рынку (в кол-ве шагов цены)
   local price
   local direction
 
   if operation=="B" then
      price = stopPrice + g_price_step*offset 
      direction = "5" -- Направленность стоп-цены. «5» - больше или равно
   else 
      price = stopPrice - g_price_step*offset 
      direction = "4" -- Направленность стоп-цены. «4» - меньше или равно
   end
   --message("stopPrice"..stopPrice)
   --Пошлем стоп заявку
   local Transaction = {
                       ['ACTION'] = "NEW_STOP_ORDER", 
                       ['PRICE'] = tostring(price), 
                       ['EXPIRY_DATE'] = "TODAY",--"GTC", -- на учебном серве только стоп-заявки с истечением сегодня, потом поменять на GTC
                       ['STOPPRICE'] = tostring(stopPrice),
                       ['STOP_ORDER_KIND'] = "SIMPLE_STOP_ORDER",
                       ['TRANS_ID'] = removeZero(tostring(trans_id)),
                       ['CLASSCODE'] = CLASS_CODE,
                       ['SECCODE'] = SEC_CODE,
                       ['ACCOUNT'] = ACCOUNT,
                       ['CLIENT_CODE'] = CLIENT_CODE, -- Комментарий к транзакции, который будет виден в транзакциях, заявках и сделках 
                       ['TYPE'] = "L",
                       ['OPERATION'] = tostring(operation),
                       ['CONDITION'] = direction, -- Направленность стоп-цены. Возможные значения: «4» - меньше или равно, «5» – больше или равно
                       ['QUANTITY'] = tostring(math.abs(quantity))
                       }
   local res = sendTransaction(Transaction)
   if string.len(res) ~= 0 then
      message('Error: '..res, 3)
      myLog("SendStopOrder(): Error "..res)
   else
      myLog("SendStopOrder(): "..EntryOrExit(trans_id).."; trans_id="..trans_id)
   end
end

function DeleteStopOrder(stopOrder_num)
   local Transaction = {
                       ['ACTION'] = "KILL_STOP_ORDER", 
                       ['CLASSCODE'] = CLASS_CODE,
                       ['SECCODE'] = SEC_CODE,
                       ['ACCOUNT'] = ACCOUNT,
                       ['CLIENT_CODE'] = CLIENT_CODE,
                       ['TYPE'] = "L",
                       ['STOP_ORDER_KIND'] = "SIMPLE_STOP_ORDER",
                       ['TRANS_ID'] = g_transId_del_order, -- ID УДАЛЯЮЩЕЙ транзакции
                       ['STOP_ORDER_KEY'] = tostring(stopOrder_num)
                       }
 
   local res = sendTransaction(Transaction)
   if string.len(res) ~= 0 then
      message('Error: '..res, 3)
      myLog("DeleteStopOrder(): fail "..res)
   else
      myLog("DeleteStopOrder(): "..stopOrder_num.." success ")
   end 
end
-- создан/изменен/сработал стоп-ордер 
function OnStopOrder(stopOrder)
   -- Если не относится к роботу, выходит из функции
   if stopOrder.brokerref:find(CLIENT_CODE) == nil then return end

   local string state="_" -- состояние заявки
   --бит 0 (0x1) Заявка активна, иначе не активна
   if bit.band(stopOrder.flags,0x1)==0x1 then
      state="стоп-заявка создана"
      if EntryOrExit(stopOrder.trans_id) == "Entry" then
         g_stopOrderEntry_num = stopOrder.order_num 
      end
      if EntryOrExit(stopOrder.trans_id) == "Exit" then
         g_stopOrderExit_num = stopOrder.order_num
      end 
   end
   if bit.band(stopOrder.flags,0x2)==0x1 or stopOrder.flags==26 then
      state="стоп-заявка снята"
   end
   if bit.band(stopOrder.flags,0x2)==0x0 and bit.band(stopOrder.flags,0x1)==0x0 then
      state="стоп-ордер исполнен"
   end
   if bit.band(stopOrder.flags,0x400)==0x1 then
      state="стоп-заявка сработала, но была отвергнута торговой системой"
   end
   if bit.band(stopOrder.flags,0x800)==0x1 then
      state="стоп-заявка сработала, но не прошла контроль лимитов"
   end
   if state=="_" then
      state="Набор битовых флагов="..tostring(stopOrder.flags)
   end
   myLog("OnStopOrder(): sec_code="..stopOrder.sec_code.."; "..EntryOrExit(stopOrder.trans_id)..";\t"..state..
         "; condition_price="..stopOrder.condition_price.."; transID="..stopOrder.trans_id.."; order_num="..stopOrder.order_num ) 
end
------------------------- Сервисные функции--------------------
-- функция записывает в лог строчку с временем и датой 
function myLog(str)
   if f==nil then return end

   local current_time=os.time()--tonumber(timeformat(getInfoParam("SERVERTIME"))) -- помещене в переменную времени сервера в формате HHMMSS 
   if (current_time-g_previous_time)>1 then -- если текущая запись произошла позже 1 секунды, чем предыдущая
      f:write("\n") -- добавляем пустую строку для удобства чтения
   end
   g_previous_time = current_time 
 
   f:write(os.date().."; ".. str .. ";\n")
 
   if str:find("Script Stoped") ~= nil then 
      f:write("======================================================================================================================\n\n")
      f:write("======================================================================================================================\n")
   end
   f:flush() -- Сохраняет изменения в файле
end
-- удаление точки и нулей после нее
function removeZero(str)
   while (string.sub(str,-1) == "0" and str ~= "0") do
      str = string.sub(str,1,-2)
   end
   if (string.sub(str,-1) == ".") then 
      str = string.sub(str,1,-2)
   end 
   return str
end
-- Возвращает Entry или Exit в зависимости от trans_id
function EntryOrExit(trans_id)
   if isArrayContain(g_arrTransId_entry, trans_id) then 
      return "Entry"
   elseif isArrayContain(g_arrTransId_exit, trans_id) then 
      return "Exit"
   elseif trans_id==g_transId_del_order then
      return "DeleteOrder"
   elseif trans_id==g_transId_del_stopOrder then
      return "DeleteStopOrder"
   else
      return "NoId"
   end
end

function isArrayContain(array, value)
   if #array < 1 then return end
      for i=1, #array do
         if array[i]==value then
         return true
      end
   end
   return false
end
[Конец:LUA]

В итоге получается текстовый  лог-файл, удобный для чтения человеком, с которого можно считать данные программой (например на  C#) и подбить статистику:

01/17/17 19:34:56; Initialization finished;
01/17/17 19:34:56; SendStopOrder(): Exit; trans_id=220002;
01/17/17 19:34:56; OnStopOrder(): sec_code=SBER; Exit; стоп-заявка создана; condition_price=131.9; transID=220002; order_num=7902164;

01/17/17 19:34:58; DeleteStopOrder(): 7902164 success ;
01/17/17 19:34:58; OnStopOrder(): sec_code=SBER; Exit; стоп-заявка снята; condition_price=131.9; transID=220002; order_num=7902164;

01/17/17 19:35:01; Script Stoped;
======================================================================================================================

======================================================================================================================
[Конец: Log.txt]

 

Простой MA-робот QLua с выставлением тэйк-профит и стоп-лимит

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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
--[[ Простой MA-робот (c)QuikLuaCSharp.ru
!!! ДЛЯ ИСПОЛЬЗОВАНИЯ ТОЛЬКО В ОБРАЗОВАТЕЛЬНЫХ ЦЕЛЯХ НА ДЕМО-СЧЕТЕ !!!
 
Робот торгует по 2-м простым(simple) скользящим средним (MA).
1.Если нет открытых роботом позиций выполняется следующий алгоритм:
   1.Если быстрая скользящая пересекает медленную снизу вверх и на текущей свече еще не открывались позиции, совершается покупка.
   2.Если быстрая скользящая пересекает медленную сверху вниз и на текущей свече еще не открывались позиции, совершается продажа.
2.Если открылась позиция, выставляется "Тэйк-профит и Стоп-лимит"
3.Робот ждет, пока закроется позичия по Стоп-лоссу, либо Тэйк-профиту, после чего переходит к пункту №1
 
Особенности:
1.Робот выводит в текстовых сообщениях информацию о ключевых моментах алгоритма, у всех его сообщений префикс "Простой MA-робот:".
2.Робот всегда находится в 1-м из 2-х состояний(ROBOT_STATE):'В ПРОЦЕССЕ СДЕЛКИ', либо 'В ПОИСКЕ ТОЧКИ ВХОДА'.
3.Если при выставлении заявки на продажу робот узнает, что операции шорт запрещены по данному инструменту, он больше не будет открывать шорт, только лонг.
4.Когда робот получает сигнал на открытие сделки, он совершает 10 попыток с прмежутками в 100 мс открыть позицию, если этого не удается, останавливает скрипт.
5.После открытия позиции робот совершает 10 попыток с прмежутками в 100 мс выставить Тэйк-профит и Стоп-лимит, а затем дождаться закрытия позиции,
  если этого не удается, останавливает скрипт.
6.Если стоп-заявка сработала, но позиция не закрылась в течении 10 секунд, пытается за 10 попыток принудительно закрыть позицию встречной сделкой.
  Если позицию удалось закрыть (даже принудительно), продолжает работать, иначе скрипт останавливается.
]]
 
--/*НАСТРАИВАЕМЫЕ ПАРАМЕТРЫ*/
--ACCOUNT           = 'NL0011100043';       -- Идентификатор счета
ACCOUNT           = 'SPBFUT00506';        -- Идентификатор счета
--CLASS_CODE        = 'QJSIM';              -- Код класса
CLASS_CODE        = 'SPBFUT';             -- Код класса
--SEC_CODE          = 'SBER';               -- Код бумаги
SEC_CODE          = 'RIZ5';               -- Код бумаги
INTERVAL          = INTERVAL_M1;          -- Таймфрейм графика (для построения скользящих)
SLOW_MA_PERIOD    = 12;                   -- ПЕРИОД МЕДЛЕННОЙ скользящей
SLOW_MA_SOURCE    = 'C';                  -- ИСТОЧНИК МЕДЛЕННОЙ скользящей [O - open, C - close, H - hi, L - low]
FAST_MA_PERIOD    = 4;                    -- ПЕРИОД БЫСТРОЙ скользящей
FAST_MA_SOURCE    = 'C';                  -- ИСТОЧНИК БЫСТРОЙ скользящей [O - open, C - close, H - hi, L - low]
STOP_LOSS         = 10;                   -- Размер СТОП-ЛОССА (в шагах цены)
TAKE_PROFIT       = 30;                   -- Размер ТЭЙК-ПРОФИТА (в шагах цены)
 
--/*РАБОЧИЕ ПЕРЕМЕННЫЕ РОБОТА (менять не нужно)*/
SEC_PRICE_STEP    = 0;                    -- ШАГ ЦЕНЫ ИНСТРУМЕНТА
SEC_NO_SHORT      = false;                -- Флаг, что по данному инструменту запрещены операции шорт
DS                = nil;                  -- Источник данных графика (DataSource)
ROBOT_STATE       ='В ПОИСКЕ ТОЧКИ ВХОДА';-- СОСТОЯНИЕ робота ['В ПРОЦЕССЕ СДЕЛКИ', либо 'В ПОИСКЕ ТОЧКИ ВХОДА']
trans_id          = os.time();            -- Задает начальный номер ID транзакций
trans_Status      = nil;                  -- Статус текущей транзакции из функции OnTransPeply
trans_result_msg  = '';                   -- Сообщение по текущей транзакции из функции OnTransPeply
CurrentDirect     = 'BUY';                -- Текущее НАПРАВЛЕНИЕ ['BUY', или 'SELL']
LastOpenBarIndex  =  0;                   -- Индекс свечи, на которой была открыта последняя позиция (нужен для того, чтобы после закрытия по стопу тут же не открыть еще одну позицию)
 
Run               = true;                 -- Флаг поддержания работы бесконечного цикла в main
 
-- Функция первичной инициализации скрипта (ВЫЗЫВАЕТСЯ ТЕРМИНАЛОМ QUIK в самом начале)
function OnInit()
   -- Получает доступ к свечам графика
   local Error = '';
   DS,Error = CreateDataSource(CLASS_CODE, SEC_CODE, INTERVAL);
   -- Если график, к которому нужно подключиться не открыт в терминале, то данные заказываются с сервера, на их получение нужно время,
   -- по этому, рекомендуется добавлять вот такое ожидание, прежде, чем обращаться к DS:
   -- Ждет, пока данные будут получены с сервера (на случай, если такой график не открыт)
   while (Error == "" or Error == nil) and DS:Size() == 0 do sleep(1) end
   if Error ~= "" and Error ~= nil then message("Ошибка подключения к графику: "..Error) return end
 
   -- Подписывается на обновления графика
   DS:SetEmptyCallback()
 
   -- Получает ШАГ ЦЕНЫ ИНСТРУМЕНТА
   SEC_PRICE_STEP = getParamEx(CLASS_CODE, SEC_CODE, "SEC_PRICE_STEP").param_value;
end;
 
function main()
   -- Выводит сообщение
   message('Простой MA-робот: '..ROBOT_STATE);
   -- "Бесконечный" цикл
   while Run do 
      --Если СОСТОЯНИЕ робота "В ПРОЦЕССЕ СДЕЛКИ"
      if ROBOT_STATE == 'В ПРОЦЕССЕ СДЕЛКИ' then
         -- Выводит сообщение
         message('Простой MA-робот: В ПРОЦЕССЕ СДЕЛКИ');
         -- Делает 10 попыток открыть сделку
         local Price = false; -- Переменная для получения результата открытия позиции (цена, либо ошибка(false))
         for i=1,10 do
            if not Run then return; end; -- Если скрипт останавливается, не затягивает процесс
            -- Если первый раз пытается открыть SELL, а операции шорт по данному инструменту запрещены
            if CurrentDirect == "SELL" and SEC_NO_SHORT then
               -- Прерывает цикл FOR
               break; 
            end;
            -- Совершает СДЕЛКУ указанного типа ["BUY", или "SELL"] по рыночной(текущей) цене размером в 1 лот,
            --- возвращает цену открытой сделки, либо FALSE, если невозможно открыть сделку
            Price = Trade(CurrentDirect);
            -- Если сделка открылась
            if Price ~= false then
               -- Прерывает цикл FOR
               break;
            end;
            sleep(100); -- Пауза в 100 мс между попытками открыть сделку
         end;
         if not Run then return; end; -- Если скрипт останавливается, не затягивает процесс
         -- Если сделка открыта
         if Price ~= false then
            -- Запоминает индекс свечи, на которой была открыта последняя позиция (нужен для того, чтобы после закрытия по стопу тут же не открыть еще одну позицию)
            LastOpenBarIndex = DS:Size();
            -- Выводит сообщение
            message('Простой MA-робот: Открыта сделка '..CurrentDirect..' по цене '..Price);
            -- Делает 10 попыток выставить СТОП-ЛОСС и ТЭЙК-ПРОФИТ, и дождаться закрытия сделки
            message('Простой MA-робот: Делает 10 попыток выставить СТОП-ЛОСС и ТЭЙК-ПРОФИТ, и дождаться закрытия сделки');
            local Result = nil; -- Переменная для получения результата выставления и срабатывания СТОП-ЛОСС и ТЭЙК-ПРОФИТ
            for i=1,10 do
               if not Run then return; end; -- Если скрипт останавливается, не затягивает процесс
               -- Выставляет СТОП-ЛОСС и ТЭЙК-ПРОФИТ, ЖДЕТ пока он сработает, принимает ЦЕНУ и ТИП ["BUY", или "SELL"] открытой сделки,
               --- возвращает FALSE, если не удалось выставить СТОП-ЛОСС и ТЭЙК-ПРОФИТ, или TRUE, если сделка закрылась,
               --- либо NIL, если при ошибке за 10 попыток не удалось принудительно закрыть позицию
               Result = SL_TP(Price, CurrentDirect);
               -- Если сделка закрылась
               if Result == true then
                  -- Прерывает цикл FOR
                  break;
               end;
            end;
            -- Если за 10 попыток не удалось закрыть позицию
            if Result == nil or Result == false then
               -- Выводит сообщение
               message('Простой MA-робот: После 10-и попыток не удалось закрыть сделку!!! Завершение скрипта!!!');
               -- Завершает выполнение скрипта
               Run = false;
               -- Прерывает основной цикл WHILE
               break;
            else -- УДАЛОСЬ ЗАКРЫТЬ СДЕЛКУ               
               -- Выводит сообщение
               message('Простой MA-робот: Сделка закрыта по СТОП-ЛОССУ, либо ТЭЙК-ПРОФИТУ');
               --Меняет СОСТОЯНИЕ робота на "В ПОИСКЕ ТОЧКИ ВХОДА"
               ROBOT_STATE = 'В ПОИСКЕ ТОЧКИ ВХОДА';
               -- Выводит сообщение
               message('Простой MA-робот: В ПОИСКЕ ТОЧКИ ВХОДА');
            end;
         else -- Сделку не удалось открыть
            -- Если первый раз пытался открыть SELL, а операции шорт по данному инструменту запрещены
            if CurrentDirect == "SELL" and SEC_NO_SHORT then
               -- Выводит сообщение
               message('Простой MA-робот: Была первая попытка совершить запрещенную операцию шорт! Больше этого не повторится:)');
               --Меняет СОСТОЯНИЕ робота на "В ПОИСКЕ ТОЧКИ ВХОДА"
               ROBOT_STATE = 'В ПОИСКЕ ТОЧКИ ВХОДА';
               -- Выводит сообщение
               message('Простой MA-робот: В ПОИСКЕ ТОЧКИ ВХОДА');
            else
               -- Выводит сообщение
               message('Простой MA-робот: После 10-и попыток не удалось открыть сделку!!! Завершение скрипта!!!');
               -- Завершает выполнение скрипта
               Run = false;
            end;
         end;
      else         
         -- СОСТОЯНИЕ робота 'В ПОИСКЕ ТОЧКИ ВХОДА'
         -- Если на этой свече еще не было открыто позиций
         if DS:Size() > LastOpenBarIndex then 
            -- Если быстрая пересекла медленную СНИЗУ ВВЕРХ
            if FastMA(DS:Size()-1) <= SlowMA(DS:Size()-1) and FastMA() > SlowMA() then
               -- Задает направление НА ПОКУПКУ
               CurrentDirect = 'BUY';
               message('CurrentDirect = "BUY"');
               -- Меняет СОСТОЯНИЕ робота на "В ПРОЦЕССЕ СДЕЛКИ"
               ROBOT_STATE = 'В ПРОЦЕССЕ СДЕЛКИ';
            -- Если быстрая пересекла медленную СВЕРХУ ВНИЗ
            elseif FastMA(DS:Size()-1) >= SlowMA(DS:Size()-1) and FastMA() < SlowMA() then
               -- Если по данному инструменту не запрещены операции шорт
               if not SEC_NO_SHORT then
                  -- Задает направление НА ПРОДАЖУ
                  CurrentDirect = 'SELL';
                  message('CurrentDirect = "SELL"');
                  -- Меняет СОСТОЯНИЕ робота на "В ПРОЦЕССЕ СДЕЛКИ"
                  ROBOT_STATE = 'В ПРОЦЕССЕ СДЕЛКИ';
               end;
            end;
         end;
      end;
      sleep(10);--Пауза 10 мс, для того, чтобы не перегружать процессор компьютера
   end;
end;
 
-- Функция вызывается терминалом QUIK при получении ответа на транзакцию пользователя
function OnTransReply(trans_reply)
   -- Если поступила информация по текущей транзакции
   if trans_reply.trans_id == trans_id then
      -- Передает статус в глобальную переменную
      trans_Status = trans_reply.status;
      -- Передает сообщение в глобальную переменную
      trans_result_msg  = trans_reply.result_msg;
   end;
end;
 
-- Функция ВЫЗЫВАЕТСЯ ТЕРМИНАЛОМ QUIK при остановке скрипта
function OnStop()
   Run = false;
end;
 
-----------------------------
-- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ --
-----------------------------
 
-- Совершает СДЕЛКУ указанного типа (Type) ["BUY", или "SELL"] по рыночной(текущей) цене размером в 1 лот,
--- возвращает цену открытой сделки, либо FALSE, если невозможно открыть сделку
function Trade(Type)
   --Получает ID транзакции
   trans_id = trans_id + 1;
 
   local Price = 0;
   local Operation = '';
   --Устанавливает цену и операцию, в зависимости от типа сделки и от класса инструмента
   if Type == 'BUY' then
      if CLASS_CODE ~= 'QJSIM' and CLASS_CODE ~= 'TQBR' then Price = getParamEx(CLASS_CODE, SEC_CODE, 'offer').param_value + 10*SEC_PRICE_STEP;end; -- по цене, завышенной на 10 мин. шагов цены
      Operation = 'B';
   else
      if CLASS_CODE ~= 'QJSIM' and CLASS_CODE ~= 'TQBR' then Price = getParamEx(CLASS_CODE, SEC_CODE, 'bid').param_value - 10*SEC_PRICE_STEP;end; -- по цене, заниженной на 10 мин. шагов цены
      Operation = 'S';
   end;
   -- Заполняет структуру для отправки транзакции
   local Transaction={
      ['TRANS_ID']   = tostring(trans_id),
      ['ACTION']     = 'NEW_ORDER',
      ['CLASSCODE']  = CLASS_CODE,
      ['SECCODE']    = SEC_CODE,
      ['OPERATION']  = Operation, -- операция ("B" - buy, или "S" - sell)
      ['TYPE']       = 'M', -- по рынку (MARKET)
      ['QUANTITY']   = '1', -- количество
      ['ACCOUNT']    = ACCOUNT,
      ['PRICE']      = tostring(Price),
      ['CLIENT_CODE']= 'Простой MA-робот'
   }
   -- Отправляет транзакцию
   sendTransaction(Transaction);
   -- Ждет, пока получит статус текущей транзакции (переменные "trans_Status" и "trans_result_msg" заполняются в функции OnTransReply())
   while Run and trans_Status == nil do sleep(1); end;
   -- Запоминает значение
   local Status = trans_Status;
   -- Очищает глобальную переменную
   trans_Status = nil;
   -- Если транзакция не выполнена по какой-то причине
   if Status ~= 3 then
      -- Если данный инструмент запрещен для операции шорт
      if Status == 6 then
         -- Выводит сообщение
         message('Простой MA-робот: Данный инструмент запрещен для операции шорт!');
         SEC_NO_SHORT = true;
      else
         -- Выводит сообщение с ошибкой
         message('Простой MA-робот: Транзакция не прошла!\nОШИБКА: '..trans_result_msg);
      end;
      -- Возвращает FALSE
      return false;
   else --Транзакция отправлена
      local OrderNum = nil;
      --ЖДЕТ пока ЗАЯВКА на ОТКРЫТИЕ сделки будет ИСПОЛНЕНА полностью
      --Запоминает время начала в секундах
      local BeginTime = os.time();
      while Run and OrderNum == nil do
         --Перебирает ТАБЛИЦУ ЗАЯВОК
         for i=0,getNumberOf('orders')-1 do
            local order = getItem('orders', i);
            --Если заявка по отправленной транзакции ИСПОЛНЕНА ПОЛНОСТЬЮ
            if order.trans_id == trans_id and order.balance == 0 then
               --Запоминает номер заявки
               OrderNum  = order.order_num;
               --Прерывает цикл FOR
               break;
            end;
         end;
         --Если прошло 10 секунд, а заявка не исполнена, значит произошла ошибка
         if os.time() - BeginTime > 9 then
            -- Выводит сообщение с ошибкой
            message('Простой MA-робот: Прошло 10 секунд, а заявка не исполнена, значит произошла ошибка');
            -- Возвращает FALSE
            return false;
         end;
         sleep(10); -- Пауза 10 мс, чтобы не перегружать процессор компьютера
      end;
 
      --ЖДЕТ пока СДЕЛКА ОТКРЫТИЯ позиции будет СОВЕРШЕНА
      --Запоминает время начала в секундах
      BeginTime = os.time();
      while Run do
         --Перебирает ТАБЛИЦУ СДЕЛОК
         for i=0,getNumberOf('trades')-1 do
            local trade = getItem('trades', i);
            --Если сделка по текущей заявке
            if trade.order_num == OrderNum then
               --Возвращает фАКТИЧЕСКУЮ ЦЕНУ открытой сделки
               return trade.price;
            end;
         end;
         --Если прошло 10 секунд, а сделка не совершена, значит на демо-счете произошла ошибка
         if os.time() - BeginTime > 9 then
            -- Выводит сообщение с ошибкой
            message('Простой MA-робот: Прошло 10 секунд, а сделка не совершена, значит на демо-счете произошла ошибка');
            -- Возвращает FALSE
            return false;
         end;
         sleep(10); -- Пауза 10 мс, чтобы не перегружать процессор компьютера
      end;
   end;
end;
 
-- ПРИНУДИТЕЛЬНО ЗАКРЫВАЕТ ОТКРЫТУЮ ПОЗИЦИЮ переданного типа (Type) ["BUY", или "SELL"]
function KillPos(Type)
   -- Дается 10 попыток
   local Count = 0; -- Счетчик попыток
   if Type == 'BUY' then
      -- Пока скрипт не остановлен и позиция не закрыта
      while Run and not Trade('SELL') do -- Открывает SELL, тем самым закрывая BUY, если Trade('SELL') вернет TRUE, цикл прекратится
         Count = Count + 1; -- Увеличивает счетчик
         -- Если за 10 попыток не удалось закрыть позицию
         if Count == 10 then
            -- Возвращает NIL
            return nil;
         end;
         sleep(100); -- Пауза 100 мс, чтобы изменилась ситуация на сервере
      end;
   else
      -- Пока скрипт не остановлен и позиция не закрыта
      while Run and not Trade('BUY') do -- Открывает BUY, тем самым закрывая SELL, если Trade('BUY') вернет TRUE, цикл прекратится
         Count = Count + 1; -- Увеличивает счетчик
         -- Если за 10 попыток не удалось закрыть позицию
         if Count == 10 then
            -- Возвращает NIL
            return nil;
         end;
         sleep(100); -- Пауза 100 мс, чтобы изменилась ситуация на сервере
      end;
   end;
   -- Возвращает TRUE, если удалось принудительно закрыть позицию
   return true;
end;
 
-- Выставляет СТОП-ЛОСС и ТЭЙК-ПРОФИТ, ЖДЕТ пока он сработает, принимает ЦЕНУ (Price) и ТИП (Type) ["BUY", или "SELL"] открытой сделки,
--- возвращает FALSE, если не удалось выставить СТОП-ЛОСС и ТЭЙК-ПРОФИТ, либо TRUE, если сделка закрылась,
--- либо NIL, если при ошибке за 10 попыток не удалось принудительно закрыть позицию
function SL_TP(Price, Type)
   -- ID транзакции
   trans_id = trans_id + 1;
 
	-- Находит направление для заявки
	local operation = "";
	local price = "0"; -- Цена, по которой выставится заявка при срабатывании Стоп-Лосса (для рыночной заявки по акциям должна быть 0)
	local stopprice = ""; -- Цена Тэйк-Профита
	local stopprice2 = ""; -- Цена Стоп-Лосса
   local market = "YES"; -- После срабатывания Тэйка, или Стопа, заявка сработает по рыночной цене
	-- Если открыт BUY, то направление стоп-лосса и тэйк-профита SELL, иначе направление стоп-лосса и тэйк-профита BUY
	if Type == 'BUY' then
		operation = "S"; -- Тэйк-профит и Стоп-лосс на продажу(чтобы закрыть BUY, нужно открыть SELL)
      -- Если не акции
      if CLASS_CODE ~= 'QJSIM' and CLASS_CODE ~= 'TQBR' then
         price = tostring(math.floor(getParamEx(CLASS_CODE, SEC_CODE, 'PRICEMIN').param_value)); -- Цена выставляемой заявки после страбатывания Стопа минимально возможная, чтобы не проскользнуло
         market = "NO";  -- После срабатывания Тэйка, или Стопа, заявка сработает НЕ по рыночной цене
      end;
		stopprice	= tostring(Price + TAKE_PROFIT*SEC_PRICE_STEP); -- Уровень цены, когда активируется Тэйк-профит
		stopprice2	= tostring(Price - STOP_LOSS*SEC_PRICE_STEP); -- Уровень цены, когда активируется Стоп-лосс
	else -- открыт SELL
		operation = "B"; -- Тэйк-профит и Стоп-лосс на покупку(чтобы закрыть SELL, нужно открыть BUY)
      -- Если не акции
		if CLASS_CODE ~= 'QJSIM' and CLASS_CODE ~= 'TQBR' then
         price = tostring(math.floor(getParamEx(CLASS_CODE, SEC_CODE, 'PRICEMAX').param_value)); -- Цена выставляемой заявки после страбатывания Стопа максимально возможная, чтобы не проскользнуло
         market = "NO";  -- После срабатывания Тэйка, или Стопа, заявка сработает НЕ по рыночной цене
      end;
		stopprice	= tostring(Price - TAKE_PROFIT*SEC_PRICE_STEP); -- Уровень цены, когда активируется Тэйк-профит
		stopprice2	= tostring(Price + STOP_LOSS*SEC_PRICE_STEP); -- Уровень цены, когда активируется Стоп-лосс
	end;
	-- Заполняет структуру для отправки транзакции на Стоп-лосс и Тэйк-профит
	local Transaction = {
		["ACTION"]              = "NEW_STOP_ORDER", -- Тип заявки
		["TRANS_ID"]            = tostring(trans_id),
		["CLASSCODE"]           = CLASS_CODE,
		["SECCODE"]             = SEC_CODE,
		["ACCOUNT"]             = ACCOUNT,
		["OPERATION"]           = operation, -- Операция ("B" - покупка(BUY), "S" - продажа(SELL))
		["QUANTITY"]            = "1", -- Количество в лотах
		["PRICE"]               = price, -- Цена, по которой выставится заявка при срабатывании Стоп-Лосса (для рыночной заявки по акциям должна быть 0)
		["STOPPRICE"]           = stopprice, -- Цена Тэйк-Профита
		["STOP_ORDER_KIND"]     = "TAKE_PROFIT_AND_STOP_LIMIT_ORDER", -- Тип стоп-заявки
		["EXPIRY_DATE"]         = "TODAY", -- Срок действия стоп-заявки ("GTC" – до отмены,"TODAY" - до окончания текущей торговой сессии, Дата в формате "ГГММДД")
      -- "OFFSET" - (ОТСТУП)Если цена достигла Тэйк-профита и идет дальше в прибыль,
      -- то Тэйк-профит сработает только когда цена вернется минимум на 2 шага цены назад,
      -- это может потенциально увеличить прибыль
		["OFFSET"]              = tostring(2*SEC_PRICE_STEP),
		["OFFSET_UNITS"]        = "PRICE_UNITS", -- Единицы измерения отступа ("PRICE_UNITS" - шаг цены, или "PERCENTS" - проценты)
      -- "SPREAD" - Когда сработает Тэйк-профит, выставится заявка по цене хуже текущей на 100 шагов цены,
      -- которая АВТОМАТИЧЕСКИ УДОВЛЕТВОРИТСЯ ПО ТЕКУЩЕЙ ЛУЧШЕЙ ЦЕНЕ,
      -- но то, что цена значительно хуже, спасет от проскальзывания,
      -- иначе, сделка может просто не закрыться (заявка на закрытие будет выставлена, но цена к тому времени ее уже проскочит)
		["SPREAD"]              = tostring(100*SEC_PRICE_STEP),
		["SPREAD_UNITS"]        = "PRICE_UNITS", -- Единицы измерения защитного спрэда ("PRICE_UNITS" - шаг цены, или "PERCENTS" - проценты)
      -- "MARKET_TAKE_PROFIT" = ("YES", или "NO") должна ли выставится заявка по рыночной цене при срабатывании Тэйк-Профита.
      -- Для рынка FORTS рыночные заявки, как правило, запрещены,
      -- для лимитированной заявки на FORTS нужно указывать заведомо худшую цену, чтобы она сработала сразу же, как рыночная
		["MARKET_TAKE_PROFIT"]  = market,
		["STOPPRICE2"]          = stopprice2, -- Цена Стоп-Лосса
		["IS_ACTIVE_IN_TIME"]   = "NO",
      -- "MARKET_TAKE_PROFIT" = ("YES", или "NO") должна ли выставится заявка по рыночной цене при срабатывании Стоп-Лосса.
      -- Для рынка FORTS рыночные заявки, как правило, запрещены,
      -- для лимитированной заявки на FORTS нужно указывать заведомо худшую цену, чтобы она сработала сразу же, как рыночная
		["MARKET_STOP_LIMIT"]   = market,
		["CLIENT_CODE"]         = "Простой MA-робот ТЭЙК-ПРОФИТ и СТОП-ЛОСС"
	}
   -- Отправляет транзакцию на установку ТЭЙК-ПРОФИТ и СТОП-ЛОСС
	sendTransaction(Transaction);   
   -- Ждет, пока не получит статус текущей транзакции (переменные "trans_Status" и "trans_result_msg" заполняются в функции OnTransReply())
   while Run and trans_Status == nil do sleep(10); end;
   -- Запоминает значение
   local Status = trans_Status;
   -- Очищает глобальную переменную
   trans_Status = nil;
   -- Если транзакция не выполнена по какой-то причине
   if Status ~= 3 then
      -- Выводит сообщение с ошибкой
      message('Простой MA-робот: Установка ТЭЙК-ПРОФИТ и СТОП-ЛОСС не удалась!\nОШИБКА: '..trans_result_msg);
      -- Возвращает FALSE
      return false;
   else
      -- Выводит сообщение
      message('Простой MA-робот: ВЫСТАВЛЕНА заявка ТЭЙК-ПРОФИТ и СТОП-ЛОСС: '..trans_id);
      local OrderNum_CLOSE = nil;
      -- ЖДЕТ пока СТОП-ЗАЯВКА на СТОП-ЛОСС и ТЭЙК-ПРОФИТ будет ИСПОЛНЕНА полностью
      while Run and OrderNum_CLOSE == nil do
         -- Перебирает ТАБЛИЦУ СТОП-ЗАЯВОК
         for i=0,getNumberOf("stop_orders")-1 do
            local stop_order = getItem("stop_orders", i);
            -- Если заявка по текущей транзакции СТОП-ЛОСС и ТЭЙК-ПРОФИТ
            if stop_order.trans_id == trans_id then
               -- Если заявка по отправленной СТОП-ЛОСС и ТЭЙК-ПРОФИТ транзакции ИСПОЛНЕНА ПОЛНОСТЬЮ
               if stop_order.balance == 0 then
                  -- Если по наступлению стоп-цены выставлена заявка
                  if stop_order.linkedorder > 0 then
                     -- Запоминает номер заявки, которая была создана при срабатывании СТОП-ЛОСС, или ТЭЙК-ПРОФИТ
                     OrderNum_CLOSE  = stop_order.linkedorder;
                     -- Прерывает цикл FOR
                     break;
                  -- Стоп-заявка сработала, но была отвергнута торговой системой
                  elseif CheckBit(stop_order.flags, 10) == 1 then
                     -- ПРИНУДИТЕЛЬНО ЗАКРЫВАЕТ ОТКРЫТУЮ ПОЗИЦИЮ и выходит из функции
                     return KillPos(Type);
                  end;
               end;
            end;
         end;
         sleep(10); -- Пауза 10 мс, чтобы не перегружать процессор компьютера
      end;
      --ЖДЕТ пока СДЕЛКА ЗАКРЫТИЯ позиции будет СОВЕРШЕНА
      --Запоминает время начала в секундах
      BeginTime = os.time();
      while Run do
         --Перебирает ТАБЛИЦУ СДЕЛОК
         for i=0,getNumberOf("trades")-1 do
            local trade = getItem("trades", i);
            --Если сделка по текущей заявке на СТОП-ЛОСС и ТЭЙК-ПРОФИТ
            if trade.order_num == OrderNum_CLOSE then
               -- Возвращает TRUE
               return true;
            end;
         end;
         --Если прошло 10 секунд, а сделка не совершена, значит на демо-счете произошла ошибка сервера "Обработка кросс-заявок блокирована"
         if os.time() - BeginTime > 9 then
            -- ПРИНУДИТЕЛЬНО ЗАКРЫВАЕТ ОТКРЫТУЮ ПОЗИЦИЮ и выходит из функции
            return KillPos(Type);
         end;
         sleep(1);
      end;
   end;
end;
 
-- Возвращает ЗНАЧЕНИЕ МЕДЛЕННОЙ скользящей по индексу свечи (по умолчанию: последняя)
function SlowMA(Index)
   -- Если индекс свечи не указан, то устанавливает индекс последней свечи
   if Index == nil then Index = DS:Size(); end;
   -- Сумма значений SLOW_MA_SOURCE на SLOW_MA_PERIOD свечах
   local Sum = 0;
   -- Перебирает последние SLOW_MA_PERIOD свечей
   for i=Index, Index - (SLOW_MA_PERIOD - 1), -1 do      
      -- Считает сумму, исходя из выбранного источника для медленной скользящей
      if SLOW_MA_SOURCE == 'O' then
         Sum = Sum + DS:O(i);
      elseif SLOW_MA_SOURCE == 'C' then
         Sum = Sum + DS:C(i);
      elseif SLOW_MA_SOURCE == 'H' then
         Sum = Sum + DS:H(i);
      elseif SLOW_MA_SOURCE == 'L' then
         Sum = Sum + DS:L(i);
      else
         message('Простой MA-робот:ОШИБКА! Не верно указан источник для медленной скользящей!');
         -- Останавливает скрипт
         OnStop();
      end;
   end;
   -- Возвращает значение
   return Sum/SLOW_MA_PERIOD;
end;
-- Возвращает ЗНАЧЕНИЕ БЫСТРОЙ скользящей по индексу свечи (по умолчанию: последняя)
function FastMA(Index)
   -- Если индекс свечи не указан, то устанавливает индекс последней свечи
   if Index == nil then Index = DS:Size(); end;
   -- Сумма значений FAST_MA_SOURCE на FAST_MA_PERIOD свечах
   local Sum = 0;
   -- Перебирает последние FAST_MA_PERIOD свечей
   for i=Index, Index - (FAST_MA_PERIOD - 1), -1 do
      -- Считает сумму, исходя из выбранного источника для быстрой скользящей
      if FAST_MA_SOURCE == 'O' then
         Sum = Sum + DS:O(i);
      elseif FAST_MA_SOURCE == 'C' then
         Sum = Sum + DS:C(i);
      elseif FAST_MA_SOURCE == 'H' then
         Sum = Sum + DS:H(i);
      elseif FAST_MA_SOURCE == 'L' then
         Sum = Sum + DS:L(i);
      else
         message('Простой MA-робот:ОШИБКА! Не верно указан источник для быстрой скользящей!');
         -- Останавливает скрипт
         OnStop();
      end;
   end;
   -- Возвращает значение
   return Sum/FAST_MA_PERIOD;
end;
 
-- Функция возвращает значение бита (число 0, или 1) под номером bit (начинаются с 0) в числе flags, если такого бита нет, возвращает nil
function CheckBit(flags, bit)
   -- Проверяет, что переданные аргументы являются числами
   if type(flags) ~= "number" then error("Предупреждение!!! Checkbit: 1-й аргумент не число!"); end;
   if type(bit) ~= "number" then error("Предупреждение!!! Checkbit: 2-й аргумент не число!"); end;
   local RevBitsStr  = ""; -- Перевернутое (задом наперед) строковое представление двоичного представления переданного десятичного числа (flags)
   local Fmod = 0; -- Остаток от деления
   local Go = true; -- Флаг работы цикла
   while Go do
      Fmod = math.fmod(flags, 2); -- Остаток от деления
      flags = math.floor(flags/2); -- Оставляет для следующей итерации цикла только целую часть от деления
      RevBitsStr = RevBitsStr ..tostring(Fmod); -- Добавляет справа остаток от деления
      if flags == 0 then Go = false; end; -- Если был последний бит, завершает цикл
   end;
   -- Возвращает значение бита
   local Result = RevBitsStr :sub(bit+1,bit+1);
   if Result == "0" then return 0;
   elseif Result == "1" then return 1;
   else return nil;
   end;
end;

Гайд Написание Lua скриптов для MoonLoader | BLASTHACK


Данная тема посвящена нюансам и особенностям разработки Lua скриптов для MoonLoader.

Список вопросов:
Начало разработки.
Взаимодействие с игроками.
Работа с переменными.
Работа с командами.



Начало разработки.
Каждый Lua скрипт при запуске создает поток «main» который и является основным местом работы со скриптом.
Сразу обговорим — скрипт «жив» пока «жив» поток main(), поэтому если мы хотим продолжать работать со скриптом после запуска — будем держать поток активным:
function main() -- Объявляем поток main()
    wait(-1) -- Устанавливаем бесконечное ожидание
end -- закрываем функцию
Запускаем скрипт — работает!
Что ж, поставим себе цель — при запуске выводить сообщение «Привет мир» в чат.
Что нам для этого нужно?
Заходим на WIKI (Раздел «Функции» moonloader`a) и ищем функции чата — Поиск -> «Chat». Видим функцию sampAddChatMessage(message, color) — то что надо.
Вставляем:
function main()
    sampAddChatMessage("Привет мир!", 0xFFFFFFFF) -- Выводим сообщение в чат
    wait(-1) -- Устанавливаем бесконечное ожидание
end
Запускаем скрипт — ошибка?
(error) test.lua: opcode '0AF8' call caused an unhandled exception
Бежим на WIKI (Опять раздел «Функции» moonloader`a) и в поиск вводим наш опкод «0AF8».
Находим нашу функцию sampAddChatMessage()
Что это значит? Дело в том что нельзя вызывать функции для работы с SA:MP пока сам SA:MP не готов. Что делать? Добавим перед выводом сообщения такой код:
if not isSampfuncsLoaded() or not isSampLoaded() then -- Если SF или SA:MP не загружены
    return -- Завершаем работу скрипта
end
while not isSampAvailable() do -- Ждём пока функция isSampAvailable() вернет true
    wait(0) -- Устанавливаем минимальное ожидание, что бы наша игра не зависла
    -- значение 0 говорит что мы ждём следующий кадр (Frame)
end
Теперь наш скрипт будет ждать пока самп полностью не загрузится и продолжит выполнение кода.
Что такое wait() и с чем его едят?
Wait позволяет ставить поток в режим ожидания на указанное количество мили-секунд (ms)
Wait нельзя в CallBack-функциях, это приведет к ошибке, а в последствии и смерти, скрипта.
Почему мы ставим 0? Потому-что это минимальная задержка.

Подведем краткий итог кода:

function main()
    if not isSampfuncsLoaded() or not isSampLoaded() then
        return
    end
    while not isSampAvailable() do
        wait(0)
    end
    sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
    wait(-1)
end
Теперь при входе в игру / перезапуске скрипта в чат будет выводиться сообщение «Привет мир».
К списку вопросов

Взаимодействие с игроками
Перейдем к примерам посложнее — попробуем получить ник, ид и счет игрока в которого мы прицелились.
Цель поставили, какие нам нужны средства?
Опять бежим к WIKI, всё в тот же раздел функций и ищем функции связанные с целями, Поиск -> Target.
Смотрим в списке есть getCharPlayerIsTargeting(ped).
Она возвращает 2 значения — result, ped.
Первое значение — возвращает результат проверки на целится ли указанный Ped в кого-либо.
Второе — если указанный Ped целится — возвращает саму цель.
Как нам это использовать? Всё просто, уберем бесконечное ожидание и будем проверять значение result.
function main()
    if not isSampfuncsLoaded() or not isSampLoaded() then
        return
    end
    while not isSampAvailable() do
        wait(0)
    end
    sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
    while true do -- Создаем бесконечный цикл, вместо бесконечного ожидания
        wait(0) -- Опять таки чтобы наша игра не зависла, ждем след кадр
        local result, ped = getCharPlayerIsTargeting(PLAYER_HANDLE) -- Каждый кадр получаем данные функции, PLAYER_HANDLE - возвращает ваш Handle
        if result then -- Если result вернет true
            print("True!") -- Выведем сообщение в лог / консоль
        end
    end
end
Что мы можем с этим делать? Теперь если мы целимся в кого-либо, мы увидим сообщения в moonloader.log об этом. Мы получили Ped игрока в которого мы целимся. Давайте используем эту информацию.
function main()
    if not isSampfuncsLoaded() or not isSampLoaded() then
        return
    end
    while not isSampAvailable() do
        wait(0)
    end
    sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
    while true do
        wait(0)
        local result, ped = getCharPlayerIsTargeting(playerPed)
        if result then
            local result2, id = sampGetPlayerIdByCharHandle(ped) -- Попробуем получить ID игрока по его Ped
            if result2 then -- Если получилось
                local nickname = sampGetPlayerNickname(id) -- Запишем его ник
                local score = sampGetPlayerScore(id) -- и счет
                sampAddChatMessage(string.format("%s[%d] имеет счет %d", nickname, id, score), -1) -- И выведем это в чат
            end
        end
    end
end
К списку вопросов

Работа с переменными
Что у нас получилось? Мы будем получать кучу сообщений в чат о игрока по пока целимся в кого-либо, но как убрать этот флуд?
Давайте создадим переменную перед main(), например targeting, и присвоим ей значение false.
И задействуем её в коде
local targeting = false -- Создаем булевую переменную
function main()
    if not isSampfuncsLoaded() or not isSampLoaded() then
        return
    end
    while not isSampAvailable() do
        wait(0)
    end
    sampAddChatMessage("Привет мир!", 0xFFFFFFFF)
    while true do
        wait(0)
        local result, ped = getCharPlayerIsTargeting(playerPed)
        if result and not targeting then -- Если мы целимся, но ранее ни в кого не целились
            local result2, id = sampGetPlayerIdByCharHandle(ped)
            if result2 then
                local nickname = sampGetPlayerNickname(id)
                local score = sampGetPlayerScore(id)
                sampAddChatMessage(string.format("%s[%d] имеет счет %d", nickname, id, score), -1)
                targeting = true -- запишем что мы целимся в кого либо и наше условие выше станет ложным
            end
        elseif not result and targeting then -- Если условие обратное - скажем скрипту что мы не целимся
            targeting = false
        end
    end
end
Отлично, теперь если мы целимся в игрока — получаем одно сообщение, меняем цель или заново целимся в того же игрока — снова получим о нём информацию.
К списку вопросов

Работа с командами.
Уже лучше, давайте теперь работаем с командами.
Уберем лишний код и оставим следующее:
function main()
    if not isSampfuncsLoaded() or not isSampLoaded() then
        return
    end
    while not isSampAvailable() do
        wait(0)
    end
    while true do
        wait(0)
    end
end
Как добавить команду? Давайте опять порыщем на WIKI, Поиск -> Command
Нашли sampRegisterChatCommand(command, callback)? Отлично, можно уже приступать.
Зарегистрируем нашу команду, например «toster»:
function main()
    if not isSampfuncsLoaded() or not isSampLoaded() then
        return
    end
    while not isSampAvailable() do
        wait(0)
    end

    sampRegisterChatCommand("toster", tosterCallBack) -- Регистрируем ДО бесконечного цикла.
    -- Первый параметр - команда, без слэша.
    -- Второй - функция-callback которая будет вызваться если будет введена команда.

    wait(-1) -- Уберем бесконечный цикл, он тут вовсе не нужен, но нам же нужно чтобы скрипт работал
end

function tosterCallBack(params)
    sampAddChatMessage("Дзынь!", -1)
end
Теперь при вводе команды «/toster» мы увидим в чате сообщение. Успех? Но как например вывести свой текст в чат? Легко!
sampAddChatMessage(params, -1)
Теперь мы выводим весь текст после команды «/toster» в чат. Например: /toster Привет жалкий мирок!

Помните я говорил что нельзя использовать wait() внутри callback-функций, это всё еще так, но есть тут одна хитрость!

function tosterCallBack()
    sampAddChatMessage("Дзынь!", -1)
    lua_thread.create(function() -- Создаем новый поток
        wait(5000) -- Ждём 5 секунд
        sampAddChatMessage("Тоже дзынь, но на 5 секунд позже!", -1) -- Выводим текст в чат
    end) -- Тут наш поток умирает :(
end
К списку вопросов
На сегодня всё, теперь вы чертовы волшебники, поняв как всё работает, можно приступать к изучению примеров Lua скриптов!

Тема будет дополняться по мере изучения популярных вопросов по Lua скриптингу.

Спойлер: Хейтерам

Я понимаю что вы невъебенные мастера луа скриптинга, но это тема не для вас

Lua в квик: набор для начинающего

Язык луа предоставляет достаточно широкие возможности для создания инфраструктуры торговых алгоритмов. Большой плюс Луа его интегрированность непосредственно в торговый терминал квик, что снижает риск обрыва потока данных из квика в стороннюю оболочку, снижение времени на передачу данных, его обработку и передачу торговых сигналов обратно в квик. Кроме этого, луа позволяет создать собственный интерфейс внутри квика, отслеживать исполнение (полное и частичное) ордеров, возможность встраивать элементы ММ.
Документация по Луа, как им то невооборазимым образом разбросана по разным документам. Разумеется пользоваться скриптами будет небольшое кол-во пользователей, тем не менее, документацию по языку все же можно было собрать в одном месте.

Итак, синтаксис языка и его описание
Если с синтаксисом языка все понятно, двигаем дальше.

Проверим, готов ли наш квик к обработке скриптов на луа.
Работу QLua обеспечивает файл qlua.dll, который должен находиться в одной папке с файлами рабочего места QUIK, например, C:\Program Files\QUIK. Если файла нет, пробуем обновить квик или скачиваем dll с сайта quik.ru

Программирование
Скрипт пишется непосредственно в текстовом файле и сохраняется с расширением .lua
Возможно 3 подхода к написанию скриптов:
1. Скрипт пишется в текстовом файле и при его запуске выполняется 1 раз. Удобен для выполнения каких то разовых операций.
2. Можно написать в Lua-скрипте функцию с предопределенным именем и всю логику работы робота (или вычислительного скрипта) поместить в эту функцию. Функция main выполняется в отдельном потоке, т.е. она не мешает работе основного функционала терминала QUIK, наличие функции позволяет выполнять периодически приостанавливать скрипт и возобновлять его работу. Если зациклить main() и вставить sleep(), то получаем полную эмуляцию подхода, использующегося при программировании на встроенном QPILE: периодический расчет чего-либо через заданный интервал времени.
3. В QLUA доступна событийная модель программирования.
При выборе такого подхода получаем весьма гибкую среду выполнения пользовательских скриптов внутри QUIK, позволяющую мгновенно получать интересующие события от терминала, производя нужную нам обработку этих событий. Для того, чтобы обработать то или иное событие, необходимо просто прописать в своем скрипте функцию с предопределенным названием. Все доступные функции обработки событий есть в документации по QLUA. Поддерживаются самые различные события совершение на бирже очередной сделки, выставление новой заявки пользователем (или скриптом), изменение стакана котировок и т.д. Схематически принцип выполнения скриптов внутри терминала можно изобразить следующим образом:

Скрипт LUA в QUIK может содержать несколько функций с предопределенными названиями, являющимися обработчиками событий (таких как новая сделка, изменение лимитов, изменение котировок и т.д.).
Выполнение скрипта происходит после пожатия на кнопку «Запустить» в диалоге «Таблицы -> Lua -> Доступные скрипты», и всегда начинается с обработки тела скрипта вне каких-либо функций (на схеме обозначено [BODY]) и вызова обработчика с именем Init() (если он есть). После того, как функция Init() завершится, происходит создание отдельного потока приложения QUIK, и в этом потоке начинает выполняться функция main(), которая обязательно должна быть. Скрипт считается работающим до тех пор, пока выполняется функция main. Как только она завершится — прекращается и выполнение скрипта, т.е. вызов из него обработчиков событий. Обратите внимание, что все функции обработки событий, в отличие от функции main(), выполняются в рамках основного потока терминала QUIK, а значит время их работы должно быть сравнительно небольшим, иначе будут заметны «подвисания» в работе терминала.

Начать программирование можно с такого кода:

is_run = true

function main()

while is_run do

sleep(1000)   — приостановка (1000 = на 1 секунду)

robot()

end

end


function robot() —тело робота
— здесь ваш код

end —конец тела робота


function OnStop(stop_flag)

is_run=false

stop_flag=1

end


Описание функций находятся здесь в файле «Интерпетатор языка луа» или в веб-версии здесь.
К сожалению, описание функций не полное и когда вы дойдете то программирования отправки транзакций (функция «sendtransaction()» ) то обнаружите что параметры нужно искать в другом месте, а именно в «Руководстве пользователя квик» в разделе 6 «Работа с другми приложениями», правда удобно?)) Я потратил несколько минут на поиски.

В остальном язык сильно проще и приятнее QPILE.
Примеры работающего робота, торгующего в спреде здесь. Его принцип работы и описание в предыдущем посте.

текст частично взят отсюда

База скрипта в QLua (lua)

Функция main выполняется в отдельном потоке, в этом весь смысл, чтобы его понять лучше, запустите вот такой скрипт:
1
2
3
while true do
   sleep(10000)
end

Попробуйте что-то сделать в терминале после запуска такого скрипта. Ваш терминал будет «висеть» 10 секунд, потому что цикл while будет выполняться в основном потоке терминала, в том же где происходит все взаимодействие с пользователем, т.е. с Вами.
Потом запустите вот такой скрипт:

1
2
3
4
5
function main()
   while true do
      sleep(10000)
   end
end

Здесь тот же самый цикл, только уже внутри main, после запуска такого скрипта терминал будет работать как обычно, как будто никакого скрипта и не выполняется, потому что для функции main терминал создаст отдельный поток, параллельный основному. Чтобы понять что такое потоки, можете сравнить их с рабочими на фабрике, один рабочий — один поток. Представьте, что у Вас есть один рабочий, который занимается отрисовкой всего в терминале, он все отрисовывает, все работает, все хорошо. И тут Вы ему говорите: «Выполни мой скрипт», он говорит: «Хорошо», бросает отрисовку, начинает выполнять Ваш скрипт и видит там команду sleep(10000), для него это означает «ничего не делай 10 секунд», он говорит: «хорошо», и просто сидит на стуле 10 секунд 🙂 Естественно никто в это время ничего в терминале не перерисовывает и не реагирует на действия пользователя. Посидел работник 10 секунд и дальше продолжил заниматься отрисовкой, терминал «ожил». А вот если Вы ему «скажите» выполнить второй скрипт с main, то он, увидев main, подзывает другого работника, который ничего не делал и перекладывает эту задачу на него, а сам продолжает заниматься отрисовкой, т.к. наш первый работник у них главный, он может другим задачи распределять 🙂 Второй работник начинает выполнять все, что находится в функции main и никак не отвлекает «бригадира» от его работы.

Сразу могу провести аналогию с функциями обратного вызова. Если, например, мы запустили такой скрипт:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
run = true
 
function main()
   while run do
      sleep(10)
   end
   message('Пока')
end
 
function OnTrade()
   message('Появилась новая сделка')
end
 
function OnStop()
   run = false
end

«Бригадир» смотрит что в нем сверху вниз, видит run = true, записывает себе в журнал, что в таком-то скрипте есть переменная run и она равна true, смотрит дальше, есть функция main, значит нужно будет привлечь другого работника, смотрит дальше, и видит функцию обратного вызова OnTrade, а в должностной инструкции бригадира написано, что это его обязанность обрабатывать функции обратного вызова, он у себя помечает в журнале, что когда придет информация о сделке пользователя, он должен выполнить эту функцию в этом скрипте, смотрит дальше, и видит что есть, так же, функция обратного вызова OnStop, опять помечает у себя в журнале, что если пользователь нажмет кнопку «Остановить» выделив перед этим данный скрипт в окне «Доступные скрипты», то он должен выполнить эту функцию . После этого он, как обычно, подзывает работника, говорит ему выполнять main и продолжает заниматься своей работой по обслуживанию терминала. Работник начинает выполнять то, что в main, смотрит, а в ней цикл «ПОКА(while) переменная run равна true ДЕЛАТЬ(do) «, проверяет переменную run, а она действительно равна true, и начинает делать то, что внутри цикла написано, видит там паузу 10 миллисекунд, ничего не делает 10 мс, идет дальше, видит там end, значит конец цикла, но так как это цикл, то он возвращается обратно к while и снова проверяет значение переменной run, оно по прежнему true и он опять выполняет паузу, и будет продолжать так делать, пока(while) переменная run равна true.
А в это время в терминал приходит информация, что по выставленной пользователем ранее заявке совершилась сделка, бригадир, получив эту информацию, смотрит свой журнал и видит, что при таком событии он должен выполнить функцию OnTrade в нашем скрипте, что он и делает, видит, что он должен вывести сообщение с текстом «Появилась новая сделка», делает это и продолжает заниматься своей работой. Сделок может быть много и каждый раз он будет выводить сообщение.
Потом вдруг нам надоело получать эти сообщения о сделках и мы нажали кнопку «Остановить» для этого скрипта. Бригадир опять читает свой журнал и видит, что должен выполнить функцию OnStop прежде, чем завершить наш скрипт. Переходит к ее выполнению, смотрит, а там написано, что он должен переменной run присвоить значение false, что он и делает, а так же ставит себе таймер на 5 секунд, после срабатывания которого он должен работнику, который выполняет main дать приказ завершить все работы и исчезнуть куда-нибудь, если он сам не придет в течении этих 5 секунд и не скажет, что он все выполнил 🙂
А в это время работник, в очередной раз проверяя значение переменной run, обнаруживает, что она уже не равна больше true, а равна false, а значит не нужно больше выполнять цикл, он переходит к тому, что ниже цикла, видит, что должен вывести сообщение с текстом «Пока», делает это, смотрит дальше, а дальше конец функции main, а значит и конец его работы. Радостный он бежит к бригадиру, сообщает, что все сделал, бригадир отпускает его с работы домой, а у себя из журнала вычеркивает переменную run и функции OnTrade и OnStop, после чего продолжает заниматься своей работой, как будто этого скрипта никогда и не было 🙂

как перестать встраивать и начать жить / Habr

Lua: как перестать встраивать и начать жить

За Lua прочно закрепилась слава полуязыка — инструмента, который при случае можно встроить, чтобы заскриптовать приложение, написанное на компилируемом языке вроде С++. Тем не менее Lua является вполне самостоятельным языком, имеющим свой интерпретатор, возможность создания модулей, большое число библиотек, и при этом данный ЯП обладает минимальным размером среди аналогов. Проще говоря у нас есть все, чтобы создавать такие же приложения как на perl, python, и вообще любом другом распространенном языке программирования.

Я могу предложить вам следующие доводы в пользу Lua:

  • — приложения будут легко переносимы между Windows и Linux (не факт что код будет работать без изменений, но портирование правда пройдет безболезненно, если не были использованы платформоспецифичные библиотеки)
  • — малый оверхед создаваемых программ
  • — высокая скорость работы и загрузки приложений
  • — возможность оперативно «приклеить» к вашему приложению любую С-библиотеку — лучшего «клея» для библиотек вы не найдете
  • — приятный минималистичный синтаксис языка, с возможностью реализации на нем современных парадигм программирования
  • — программы на Lua очень легко развертывать
  • — малое потребление памяти

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

В качестве графического тулкита будем использовать iup — кроссплатформенную библиотеку, изначально созданную с расчетом использования из Lua.

Установка Lua SDK

В рамках идеи использования Lua как самостоятельного ЯП, была создана сборка Lua for Windows, которая содержит себе библиотеки, необходимые в повседневных задачах, возникающих при программировании под указанную ОС: работы с БД, GUI, парсинг XML и т.д. Пусть вас не смущает, что версия Lua в сборке 5.1, а не 5.2 — особой разницы в нашем случае между ними нет.

Скачайте и установите сборку.

Краткое описание концепции iup

Я долго думал, как же расписать процесс создания программы, не вдаваясь подробно в устройство iup. И решил коротко описать его основные принципы:
  • — iup.dialog является корневым элементом интерфейса программы — в этом контейнере размещаются все элементы
  • — позиционирование элементов в контейнере производится при помощи layout-ов: задания правил размещения элемента в контейнере. Iup сам расположит и отрисует элемент согласно правилам. Основные контейнеры — фрейм, вертикальный сайзер, горизонтальный сайзер.
  • — обработчики событий задаются в виде функций, прикрепленных к виджету
  • — после создания диалога запускается цикл обработки событий

Если вы ранее писали для GUI при помощи Tk, WxWidgets или WinAPI, то все это покажется знакомым. Если нет, то программа довольно подробно покрыта комментариями.
Код программы
-- подключение библиотек iup
require("iuplua" )
require("iupluacontrols")
require("iuplua_pplot")

-- библиотека для работы с Canvas, чтобы сохранять график в файл
require("cdlua")
require("iupluacd")


require("string")


-- глобальные переменные для виджетов и настроек программы

-- максимальное число графиков
plots_number = 5

-- виджеты вкладок, где будут размещаться виджеы ввода данных для каждого графика
tabs = {}

-- контейнеры для виджетов
vboxes = {}

-- чекбоксы для выбора того, какие графики строить
checkboxes = {}

-- здесь храним виджеты с текстом данных о точках
coords = {}

-- виджеты подписи для каждого графика
legends = {}

-- виджеты обозначения осей координат
global_legend = {}


-- к величайшему стыду, в Lua нет стандартной функции split
function string:split(sep)
        local sep, fields = sep or ":", {}
        local pattern = string.format("([^%s]+)", sep)
        self:gsub(pattern, function(c) fields[#fields+1] = c end)
        return fields
end


-- функция рисует на плоттере график по указаным точкам
function draw_plot(pwidget, pnum, data)
   x = data[1].value:split(",")
   y = data[2].value:split(",")

   if checkboxes[pnum].value == "OFF" then return end

   if not (#x == #y) or #x == 0 then
	  iup.Message("Ошибка", 
				  "Задано неверное число точек для графика " .. pnum)
      return
   end
   
   iup.PPlotBegin(pwidget, 0)
   iup.PPlotAdd(pwidget, 0, 0)
   for i = 1,#x do
      iup.PPlotAdd(pwidget, x[i], y[i])
   end
   iup.PPlotEnd(pwidget)
end


-- виджет отвечающий за кнопку построения графика
plot_btn = iup.button{ title = "Построить"}

-- колбэк для кнопки "построить график"
function plot_btn:action()

   -- создать виджет графопостроителя
   plot = iup.pplot
   {
      expand="YES",
      TITLE = "Simple Line",
      MARGINBOTTOM="65",
      MARGINLEFT="65",
      AXS_XLABEL = global_legend[1].value,
      AXS_YLABEL = global_legend[2].value,
      LEGENDSHOW="YES",
      LEGENDPOS="TOPLEFT",
      size = "400x300"
   }

   -- этот блок для обхода бага - без него подпись к первому графику отображаться не будет
   iup.PPlotBegin(plot, 0)
   iup.PPlotAdd(plot,0,0)
   plot.DS_LEGEND = ""
   iup.PPlotEnd(plot)

   -- обходим виджеты с данными
   for i = 1, plots_number do
      -- чтобы свеженарисованный графи отобразился с правильной подписью
	  print(legends[i].value)
      plot.DS_LEGEND = legends[i].value
      -- рисуем график
      draw_plot(plot, i, coords[i])
   end


   -- кнопка сохранения графика в картинку на диске
   save_btn = iup.button{ title = "Сохранить" }

   -- теперь создаем само окно, где будет отображаться график
   plot_dg = iup.dialog
   {
      iup.vbox -- это вертикальный сайзер, помести в него графопостроитель и кнопку
      {
	 plot,
	 save_btn
      },
   }

   -- обработчик кнопки сохранения графика
   function save_btn:action()

	  -- создаем диалог выбора имени файла ля сохранения
	  -- в связи с ограничениями библиотеки сохранять можно только в EMF
	  fs_dlg = iup.filedlg{DIALOGTYPE = "SAVE", FILTER = "*.emf" }
	  iup.Popup(fs_dlg)

	  -- если файл выбран
	  if tonumber(fs_dlg.STATUS) >= 0 then

		 -- дописать при необходимости нужное расширение
		 pic = fs_dlg.value
		 if not (string.sub(pic, string.len(pic)-3) == ".emf") then
			pic = pic .. ".emf"
		 end

		 -- создаем псевдо-холст, ассоциированный с файлом
		 tmp_cv = cd.CreateCanvas(cd.EMF, pic .. " 400x300")
		 -- выводим график на холст
		 iup.PPlotPaintTo(plot, tmp_cv)
		 -- сохраняем данные в файл
		 cd.KillCanvas(tmp_cv)
	  end
   end

   -- отображаем диалог с графиком
   plot_dg:showxy(iup.CENTER, iup.CENTER)

   -- запускаем петлю обработки событий для диалога
   if (iup.MainLoopLevel()==0) then
      iup.MainLoop()
   end

end


-- в цикле создаем вкладки, в которых мы будем размещать виджеты 
-- для сбора данных
for i=1,plots_number do

   -- создание текстовых виджетов, куда будут вводиться координаты точек
   coords[i] = {}
   for j = 1,2 do
      coords[i][j] = iup.text 
      {
		 expand="HORIZONTAL",
		 multiline = "YES",
		 VISIBLELINES = 5
      }
   end

   -- виджет для редактирования подписи к графику
   legends[i] = iup.text{ expand = "HORIZONTAL" }

   -- создаем контейнер вкладки и заполняем его элементами
   vboxes[i] = iup.vbox
   {
	  iup.hbox
	  {
		 iup.label { title = "Подпись графика:" },
		 legends[i]
	  },
	  iup.hbox 
	  {
		 iup.label
		 {
			title="X : ", 
		 },
		 coords[i][1]
	  },
	  iup.hbox 
	  {
		 iup.label
		 {
			title="Y : ", 
		 },
		 coords[i][2]
	  };
	  expand="YES",

   }

   -- меняем заголовк вкладки
   vboxes[i].tabtitle = "График " .. i

   -- создаем чекбокс, который будет указывать на то, нужно ли строить
   -- график по данным из указанной вкладки
   checkboxes[i] = iup.toggle{ title= "График" .. i, value = "ON" }
end

-- теперь из заполненных нами контейнеров создаем вкладки
tabs = iup.tabs{unpack(vboxes)}


-- создаем текстовые виджеты для редактирования подписей осей
global_legend[1] = iup.text{}
global_legend[2] = iup.text{}

-- создаем фрейм для общих настроек графика
frame = iup.frame
{
   iup.vbox
   {      
      iup.label{
		 title="Использовать данные:", 
		 expand="HORIZONTAL"
			   },
      iup.vbox
      {
		 unpack(checkboxes)
      },
      iup.label{}, -- пустую подпись можно использовать как распорку
      iup.label{title = "Подписи"},
      iup.hbox { iup.label{ title = "Ось X "}, global_legend[1] },
      iup.hbox { iup.label{ title = "Ось Y "}, global_legend[2] },
      iup.label{},
      plot_btn
   };
   expand = "VERTICAL",
}

-- создаем главное окно программы и наносим на него настройки и табы
dg = iup.dialog
{
   iup.hbox
   {
      frame, tabs
   },
   title="Строим график",
   size = "HALF"
}


-- показываем главное окно и запускаем обработку событий
dg:showxy(iup.CENTER, iup.CENTER)
if (iup.MainLoopLevel()==0) then
  iup.MainLoop()
end
Пара слов о развертывании

Скрипт можно запустить при помощи команды:
lua plotter.exe

В данном случае библиотеки будут подключаться из поддиректории clibs/, которая находится в директории, куда был установлен Lua for Windows. Чтобы максимально компактно упаковать скрипт и библиотеки для переноса на другую машину, достаточно скопировать в одну папку следущие файлы(указаны с относительными путями от директории установки Lua):

lua.exe
lib/lua5.1.dll
clibs/cd.dll
clibs/cdlua51.dll
clibs/iup.dll
clibs/iup_pplot.dll
clibs/iupcd.dll
clibs/iupcontrols.dll
clibs/iupgl.dll
clibs/iuplua51.dll
clibs/iuplua_pplot51.dll
clibs/iupluacd51.dll
clibs/iupluacontrols51.dll
clibs/freetype6.dll

Не забудьте поместить в эту папку и сам скрипт с программой. Теперь вы можете перенести эту папку на другую машину и запустить вашу программы командой, указанной выше. При этом никакие другие действия по установке библиотек и рантайма не нужны.

К сожалению файлы cd.dll, cdluad51.dll и iupcd.dll в данной версии Lua for Windows могут работать некорректно, поэтому рекомендую взять их из архива по ссылке ниже.

Итоги

Архив с рабочей версией тут, для удобства добавлена пускалка app.bat.

Скриншоты:

В результате получили, пусть и неказистую, утилиту, имеющую такой же функционал, как и если бы она была написана на «серьезном» языке программирования. При этом простую в развертывании и суммарным весом менее 2 мб. Потребление памяти — около 7 мб. Исходный код доступен для редактирования, сам Lua интерактивно понятен, что упрощает доработку подобного софта на местах.

На мой взгляд, это отличный выбор для написания учебного софта для школ и институтов, а также для внутреннего использования на предприятиях. Так как слабые машины до сих пор в изобилии присутствуют в подобных местах по всему СНГ, то использование Luа подобным образом целесообразно, особенно в свете постепнного прихода Linux на десктопы. К тому же тенденцию потери исходников самописного софта при жуткой его же забагованности можно приравнять к национальному бедствию.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *