Отправка стакана из QUIK (QLua) в приложение C#

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

Qlua-csharp-connector-dll
При отправке из терминала QUIK таких часто изменяющихся данных, как "СТАКАН", необходимо использовать обратную связь от C# о получении данных. Так же, на стороне QLua необходим буфер (стек), в который будут заноситься и из которого, в последствии, будут отправляться новые данные, по мере получения их приложением C#. На практике этот процесс происходит очень быстро, так что данные не успевают задерживаться в стеке в ожидании своей очереди. Благодаря чему, приложение C# всегда своевременно получает актуальные изменения. А благодаря стеку, ни одно изменение не останется упущенным.
Примеры кода:

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
require("QluaCSharpConnector")
 
IsStop = false;         -- Флаг остановки скрипта
 
Stack = {};             -- Массив для стека
Stack.idx_for_add = 1;  -- Индекс для добавления следующей, или первой записи
Stack.idx_for_get = 1;  -- Индекс для изъятия следующей, или первой записи
Stack.count = 0;        -- Количество находящихся в стеке записей
Stack.max = 1000;       -- Максимально возможное количество записей в стеке (при переполнении старые записи будут замещаться новыми)
 
CLASS_CODE   = "SPBFUT";   -- Класс бумаги
SEC_CODE     = "RIH5";	   -- Код бумаги
 
function main()
   local FirstQuote = true;
   local Quote = "";
   -- ОСНОВНОЙ ЦИКЛ
   while not IsStop do
 
      -- если робот получил СТАКАН, или это первый СТАКАН
      if QluaCSharpConnector.CheckGotQuote() or FirstQuote then
         -- берет стакан из стека
         Quote = GetFromStack();
         -- если стек не пустой
         if Quote ~= nil then
            if FirstQuote then FirstQuote = false; end;
            -- отправляет стакан роботу
            QluaCSharpConnector.SendQuote(Quote);
         end;
      end
 
      sleep(1);
   end;
end
 
-- Добавляет запись в стек
function AddToStack(NewEntry)
   -- Добавляет запись в стек
   Stack[Stack.idx_for_add] = NewEntry;
   -- Корректирует счетчик находящихся в стеке записей
   if Stack.count < Stack.max then Stack.count = Stack.count + 1; end;
   -- Увеличивает индекс для добавления следующей записи
   Stack.idx_for_add = Stack.idx_for_add + 1;
   -- Если индекс больше максимально допустимого, то следующая запись будет добавляться в начало стека
   if Stack.idx_for_add > Stack.max then Stack.idx_for_add = 1; end;
   -- Если изъятие записей отстало от записи (новая запись переписала старую), то увеличивает индекс для изъятия следующей записи
   if Stack.idx_for_add - Stack.idx_for_get == 1 and Stack.count > 1 -- смещение внутри стека
      then Stack.idx_for_get = Stack.idx_for_get + 1;
   -- Добавил в конец, когда индекс для изъятия тоже был в конце и количество не равно 0
   elseif Stack.idx_for_get - Stack.idx_for_add == Stack.max - 1 and Stack.count > 1
      then Stack.idx_for_get = 1;
   end;
end;
-- Извлекает запись из стека
function GetFromStack()
   local OldInxForGet = Stack.idx_for_get;
   if Stack.count == 0 then return nil; end;
   -- Уменьшает количество записей на 1
   Stack.count = Stack.count - 1;
   -- Корректирует, если это была единственная запись
   if Stack.count == -1 then
      Stack.count = 0;
      Stack.idx_for_get = Stack.idx_for_add; -- Выравнивает индексы
   else -- Если еще есть записи
      -- Сдвигает индекс изъятия на 1 вправо
      Stack.idx_for_get = Stack.idx_for_get + 1;
      -- Корректирует, если достигнут конец
      if Stack.idx_for_get > Stack.max then Stack.idx_for_get = 1; end;
   end;
   return Stack[OldInxForGet];
end;
 
--- Функция вызывается терминалом QUIK при получении изменения стакана котировок
function OnQuote(class, sec )
   if class == CLASS_CODE and sec == SEC_CODE then
      -- Получает стакан по нужному инструменту
      ql2 = getQuoteLevel2(class, sec);
      -- Представляет снимок СТАКАНА в виде СТРОКИ
      QuoteStr = "";
      for i = tonumber(ql2.bid_count), 1, -1 do
         if ql2.bid[i].quantity ~= nil then
            QuoteStr = QuoteStr..tostring(tonumber(ql2.bid[i].quantity))..";"..tostring(tonumber(ql2.bid[i].price))..";";
         else
            QuoteStr = QuoteStr.."0;"..tostring(tonumber(ql2.bid[i].price))..";";
         end;
      end;
      for i = 1, tonumber(ql2.offer_count), 1 do
         if ql2.offer[i].quantity ~= nil then
            if i < tonumber(ql2.offer_count) then
               QuoteStr = QuoteStr..tostring(tonumber(ql2.offer[i].quantity))..";"..tostring(tonumber(ql2.offer[i].price))..";";
            else
               QuoteStr = QuoteStr..tostring(tonumber(ql2.offer[i].quantity))..";"..tostring(tonumber(ql2.offer[i].price));
            end;
         else
            if i < tonumber(ql2.offer_count) then
               QuoteStr = QuoteStr.."0;"..tostring(tonumber(ql2.offer[i].price))..";";
            else
               QuoteStr = QuoteStr.."0;"..tostring(tonumber(ql2.offer[i].price));
            end;
         end;
      end;
      -- Добавляет СТАКАН-строку в стек
      AddToStack(QuoteStr);
   end;
end;
 
--- Функция вызывается терминалом QUIK при завершении пользователем скрипта
function OnStop(s)
   IsStop = true;
end
DLL-функции(C/C++) CheckGotQuote() и SendQuote()
(Если Вы не знаете как создавать библиотеки DLL, которые можно использовать в скриптах QLua(Lua), ознакомьтесь, пожалуйста, с данной статьей).

// Имя для выделенной памяти
TCHAR Name[] = TEXT("TerminalQuote");
// Создаст, или подключится к уже созданной памяти с таким именем
HANDLE hFileMapTerminalQuote = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 1400, Name);
 
//Проверяет получил-ли робот последний СТАКАН
static int forLua_CheckGotQuote(lua_State *L)
{
   //Если указатель на память получен
   if (hFileMapTerminalQuote)
   {
      //Получает доступ к байтам памяти
      PBYTE pb = (PBYTE)(MapViewOfFile(hFileMapTerminalQuote, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 1400));
 
      //Если доступ к байтам памяти получен
      if (pb != NULL)
      {
         //проверяет на пустую запись (сигнал, что можно отправлять стакан)
         if (pb[0] == 0)
         {
            lua_pushboolean(L, true);
         }
         else
         {
            lua_pushboolean(L, false);
         }
         //закрывает представление
         UnmapViewOfFile(pb);
      }
      else lua_pushboolean(L, false);
      }
   else
   {
      lua_pushboolean(L, false);
   }
 
   return(1);
}
 
//Отправляет новые изменения стакана
static int forLua_SendQuote(lua_State *L)
{
   //Если указатель на память получен
   if (hFileMapTerminalQuote)
   {
      //Получает доступ к байтам памяти
      PBYTE pb = (PBYTE)(MapViewOfFile(hFileMapTerminalQuote, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 1400));
 
      //Если доступ к байтам памяти получен
      if (pb != NULL)
      {
         //Получает из Lua-стека переданное значение
         const char *Quote = lua_tostring(L, 1);
         int Size = 0;
         //считает количество символов в строке
         for (int i = 0; i < 1400; i++)
         {
            if (Quote[i] == 0)break;
            Size++;
         }
         //записывает стакан в память
         memcpy(pb, Quote, Size);
            //lua_pushstring(L, (char*)pb);//возвращает то, что записалось (если раскомментировать) (может пригодиться при отладке)
         //закрывает представление
         UnmapViewOfFile(pb);
      }
      else lua_pushstring(L, "");
   }
   else lua_pushstring(L, "");
 
   return(1);
}
Код C#

// Создаст, или подключится к уже созданной памяти с таким именем
public MemoryMappedFile MemoryTerminalQuote;
// Создает поток для чтения
StreamReader SR_TerminalQuote;
// Создает поток для записи
StreamWriter SW_TerminalQuote;
 
//Флаг работы приложения (аналогично QLua)
//Ключевое слово "volatile" гарантирует, что в переменной в любое время и при обращении из любого потока будет актуальное значение
public volatile bool Run = true;

//выделяет именованную память под получение СТАКАНА от QUIK, создает потоки чтения/записи
MemoryTerminalQuote = MemoryMappedFile.CreateOrOpen("TerminalQuote", 1400, MemoryMappedFileAccess.ReadWrite);
SR_TerminalQuote = new StreamReader(MemoryTerminalQuote.CreateViewStream(), System.Text.Encoding.Default);
SW_TerminalQuote = new StreamWriter(MemoryTerminalQuote.CreateViewStream(), System.Text.Encoding.Default);

//Считывает стакан из памяти
private string GetTerminalQuoteData()
{
   SR_TerminalQuote.BaseStream.Seek(0, SeekOrigin.Begin);
   return SR_TerminalQuote.ReadToEnd().Trim('\0', '\r', '\n');
}
 
//Очищает память, сообщая тем самым терминалу, что стакан получен
private void SetTerminalQuoteData(string Data = "")
{
   SW_TerminalQuote.BaseStream.Seek(0, SeekOrigin.Begin);
   for (int i = 0; i < 1400; i++) SW_TerminalQuote.Write("\0");
   if (Data != "")
   {
      SW_TerminalQuote.BaseStream.Seek(0, SeekOrigin.Begin);
      SW_TerminalQuote.Write(Data);
   }
   SW_TerminalQuote.Flush();
}

//Получает снимок СТАКАНА из QUIK
private void GettingQuoteData()
{
   //Запускает функцию получения стакана в отдельном потоке, чтобы приложение откликалось на действия пользователя
   //Чтобы остановить выполнение данного потока, нужно переменной "Run" присвоить значение "false"
   //Сделать это можно, либо в функции по событию закрытия приложения, либо при нажатии на кнопке и т.д.
   new Thread(() =>
   {
      string QuoteStr = "";
      string[] QuoteStrParts;
 
      //Постоянный цикл в отдельном потоке
      while (Run)
      {
         //Получает стакан из памяти
         QuoteStr = GetTerminalQuoteData();
 
         //Если СТАКАН получен, стирает запись, подтверждая это
         if (QuoteStr != "00" && QuoteStr != "" && QuoteStr != "0" && QuoteStr != "-1")
         {
            //Стирает запись, подтверждая что стакан получен и будет обработан
            SetTerminalQuoteData();
 
            //Разделяет снимок СТАКАНА на составляющие
            QuoteStrParts = QuoteStr.Split(';');
 
            //Что-то делает с полученным стаканом
            // ...
 
            //Удаляет массив
            QuoteStrParts = null;
         }
         //Чтоб процесс не "забивал" одно из ядер процессора на 100% нужна пауза в 1 миллисекунду
         Thread.Sleep(1);
      }
   }).Start();
}
Если у Вас появились какие-то вопросы, задайте их в комментариях под статьей !!!