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

Автор записи: Дмитрий (Admin)
1 звезда2 звезды3 звезды4 звезды5 звезд (Голосов 7, среднее: 5,00 из 5)
Загрузка...

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

QLua-скрипт
DLL-функции(C/C++) CheckGotQuote() и SendQuote()
Код C#
Если у Вас появились какие-то вопросы, задайте их в комментариях под статьей !!!

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

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

  1. Дмитрий, добрый день! Надеюсь сможете уделить немного времени и помочь разобраться со стеком Lua. Пытаюсь, основываясь на материалах сайта, реализовать OnQuote в C++ dll.
    Все отлично работает, вот только таблицы bid и offer не получается из стека вытащить. Делаю так:
    static int forLua_OnQuote(lua_State *L)
    {
    // здесь все работает, получаю class_code и sec_code
    class_code = lua_tostring(L, 1);
    sec_code = lua_tostring(L, 2);
    // если class_code и sec_code соответствуют критериям отбора выполняем "getQuoteLevel2"

    lua_settop(L, 0); // Очищаем стек Lua

    lua_getglobal(L, "getQuoteLevel2");
    lua_pushstring(L, class_code.c_str());
    lua_pushstring(L, sec_code.c_str());
    lua_pcall(L, 2, 1, 0);

    if (lua_istable(L, 1))
    {
    // Получаю bid_count и offer_count - количество записей (строк ) в таблицах bid и
    // offer. Здесь тоже пока все работает

    int bid_count;
    lua_pushstring(L, "bid_count");
    lua_gettable(L, -2);
    bid_count = lua_tointeger(L, -1);
    lua_pop(L, 1);

    int offer_count;
    lua_pushstring(L, "offer_count");
    lua_gettable(L, -2);
    offer_count = lua_tointeger(L, -1);
    lua_pop(L, 1);

    // Проблема в том, что я не знаю как дальше получить из стека строки самих таблиц bid и offer. Т.е. по аналогии с тем как у Вас реализовано в скрипте, хочется сделать
    for (int i = 0; i < bid_count; i++)
    {
    //здесь надо как то вытащить из стека i-ю строку таблицы bid и получить из нее
    значения price и quantity
    }
    // и аналогично для таблицы offer.

    Со всем остальным вроде бы разобрался. И функции обратного вызова в C++ dll работают, и данные в С# передаются в обоих направлениях.

    1. Задача решена. решение было здесь http://www.cyberforum.ru/lua/thread1543618-page2.html

      lua_getglobal(L, "getQuoteLevel2");
      lua_pushstring(L, class_code.c_str());
      lua_pushstring(L, sec_code.c_str());
      lua_pcall(L, 2, 1, 0);

      if (!lua_istable(L, 1)) // Проверяет, является-ли первый элемент стека таблицей Lua
      {
      lua_pushstring(L, "Ошибка передачи котировок в C++ dll");
      return lua_error(L); ;
      }

      //******получаем количество строк в таблицах bid и offer **************************************

      lua_pushstring(L, "bid_count"); /* поместить ключ на стек */
      lua_gettable(L, -2); //получить значение
      bid_count = lua_tointeger(L, -1);
      lua_pop(L, 1);

      lua_pushstring(L, "offer_count"); /* поместить ключ на стек */
      lua_gettable(L, -2); //получить значение
      offer_count = lua_tointeger(L, -1);
      lua_pop(L, 1);

      //**Получаем таблицы bid и offer ***********************************************************************************

      lua_getfield(L, -1, "bid");

      lua_pushnil(L); //стек: -1=ключ равный нулю, -2=ссылка на таблицу 'bid'
      while (lua_next(L, -2) != 0) // идем по строкам таблицы bid
      {
      lua_pushnil(L); //стек: -1=ключ равный нулю, -2 ссылка на таблицу bid[i] (i-ю строку таблицы bid)
      while (lua_next(L, -2) != 0) //идем по колонкам i-ой строки таблицы bid
      {
      if (lua_tostring(L, -2) == "price")
      {
      // что то делаем со значение price, которое лежит в lua_tostring(L, -1)
      }
      if (lua_tostring(L, -2) == "quantity")
      {
      // что то делаем со значение quantity, которое лежит в lua_tostring(L, -1)
      }

      lua_pop(L, 1); // освобождает стек для следующей итерации
      }
      lua_pop(L, 1); // освобождает стек для следующей итерации
      }

  2. Доброго времени суток.
    Реализовал передачу стакана. но дело в том, что при обработке его в программе, распределяется покупка/продажа, только если стакан полный, то есть состоит из 40 записей. Подскажите как можно раскидать стакан на покупку/продажу, если он не полный, то есть к примеру на продажу имеется 5 записей, а на покупку 11?

    1. Сам отвечу на свой вопрос.
      В стакане стандартно может находиться не более 40 записей, мы можем посмотреть количество записей по Спросу и Предложению находится в стакане по отдельности, таким образом я могу доставить нулевые записи куда мне нужно и корректно поделить стакан на Спрос и Предложение.

      Немного измененный скрипт (под мои нужды, может кому понадобится)

      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
      
      function OnQuote(class, sec)
      	ql2 = getQuoteLevel2(ClassName, SecName);
      	QuoteStr = "";
      	-- Доставляем нулевые записи перед записями Предложения
      	if (tonumber(ql2.bid_count) ~= 20) then
      		for i = 1, 20 - tonumber(ql2.bid_count), 1 do
      			QuoteStr = QuoteStr.."0;".."0"..";";
      		end;
      	end;
      	-- Ставим записи Предложения
      	for i = 1, tonumber(ql2.bid_count), 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 &lt; 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 &lt; 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;
      	-- Доставляем нулевые записи после записей Спроса
      	if (tonumber(ql2.offer_count) ~= 20) then
      		for i = 1, 20 - tonumber(ql2.offer_count), 1 do
      			QuoteStr = QuoteStr.."0;".."0"..";";
      		end;
      	end;
      	getQuotes.SendQuote(QuoteStr);
      end;

      Мой способ обработки полученного стакана в программе

      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
      
      string QuoteStr = "";
                  string[] QuoteStrParts;
       
                  while (tradeTableRun)
                  {
                      SR_TerminalQuote.BaseStream.Seek(0, SeekOrigin.Begin);
                      QuoteStr = SR_TerminalQuote.ReadToEnd().Trim('\0', '\r', '\n');
                      SW_TerminalQuote.BaseStream.Seek(0, SeekOrigin.Begin);
                      for (int i = 0; i < 1400; i++)
                          SW_TerminalQuote.Write("\0");
                      SW_TerminalQuote.BaseStream.Seek(0, SeekOrigin.Begin);
                      SW_TerminalQuote.Write("");
                      SW_TerminalQuote.Flush();
       
                      if (fsQoute == true)
                      {
                          if (QuoteStr != "00" && QuoteStr != "" && QuoteStr != "0" && QuoteStr != "-1")
                          {
       
                              QuoteStrParts = QuoteStr.Split(';');
       
                              for (int i = 0; i <= QuoteStrParts.Length - 1; i++)
                              {
                                  if (i % 2 == 0)
                                      quantity.Add(QuoteStrParts[i]);
                                  if (i % 2 == 1)
                                      price.Add(QuoteStrParts[i]);
                              }
                              //Отправка таблицы на форму в DataGridView с проверкой количества записей
                              if (quantity.Count == 40 && price.Count == 40)
                              {
                                  if (tf != null && !tf.IsDisposed)
                                      tf.GettingQuoteData(price, quantity);
                              }
                              //проверяем количество записей
                              if (quantity.Count == 40 && price.Count == 40)
                              {
                                  // получаем цены для рыночной покупки/продажи
                                  buyMarketPrice = Convert.ToDouble(price[19].Replace('.', ','));
                                  sellMarketPrice = Convert.ToDouble(price[20].Replace('.', ','));
                              }
                              quantity.Clear();
                              price.Clear();
                              buyMarketPrice = 0;
                              sellMarketPrice = 0;
                              QuoteStrParts = null;
                          }
                      }
                      else
                          fsQoute = true;
                      Thread.Sleep(1);
                  }

      Функция, которая заполняет DataGridView на форме и красит его:

      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
      
      public void GettingQuoteData(List price, List quantity)
              {
                  if (quantity.Count == 40 && price.Count == 40)
                  {
                      int row = 0;
                      for (int i = quantity.Count - 1; i != -1; i--)
                      {
                          quoteGrid[0, row].Value = "";
                          quoteGrid[1, row].Value = "";
                          quoteGrid[2, row].Value = "";
       
                          if (row <= 19)
                          {
                              quoteGrid[1, row].Value = price[i];
                              quoteGrid[2, row].Value = quantity[i];
                              quoteGrid.Rows[row].Cells[1].Style.BackColor = ColorTranslator.FromHtml("#ff8080");
                              quoteGrid.Rows[row].Cells[2].Style.BackColor = ColorTranslator.FromHtml("#ff8080");
                              quoteGrid.Rows[row].Cells[0].Style.BackColor = Color.Gray;
                          }
                          else
                          {
                              quoteGrid[0, row].Value = quantity[i];
                              quoteGrid[1, row].Value = price[i];
                              quoteGrid.Rows[row].Cells[0].Style.BackColor = ColorTranslator.FromHtml("#70db70");
                              quoteGrid.Rows[row].Cells[1].Style.BackColor = ColorTranslator.FromHtml("#70db70");
                              quoteGrid.Rows[row].Cells[2].Style.BackColor = Color.Gray;
                          }
                          row++;
                      }
                  }
       
                  buyMarketPriceBox.Text = quoteGrid[1, 19].Value.ToString();
                  sellMarketPriceBox.Text = quoteGrid[1, 20].Value.ToString();
                  Thread.Sleep(1);
              }

      Скрин, каким получается стакан в моем случае:
      Изображение:

      Если что то коряво и/или не правильно, то прошу сильно не ругать и тапками не кидаться =)

      1. Привет! В стакане стандартно может находиться не более 100 записей, а не 40.
        Вы так-то время засекали, сколько у вас процесс передачи стакана идет из OnQuote?

        Что за мания везде тыркать глобальные переменные без нужды? Медленнее доступ к ним, чем к локальным.
        Так же как вот такая конструкция: ql2.offer[i].quantity неравно nil - нафига вам эта проверка?
        А если ql2.offer равно nil или тупо ql2 равно nil, тогда что? Загнется програмуля?

        Есть такая функция не документированная getQuoteLevel2Ex, в которой и qty и price имеют тип "number".

        Ах да, совсем забыл. Что за манечка делать расчеты на С#? Луа стакан посчитает быстрее, чем дойдет до стека та строка, которую вы героически склеиваете и туда пытаетесь всунуть, и время не нужно тратить на всякую ересь типа квик + С#.
        Оба языка написаны на С, да и арка что-то выбрала Луа, не C#, а там ваще не глупые люди работают.
        Хотя дело ваше...

  3. Добрый день. Всё отлично работает через MemoryMappedFile, но как выполнить ту же задачу, но через обычный файл расположенный на жестком диске, не используя MemoryMappedFile?
    Я попробовал, но ничего не вышло, хотя решение практически идентичное должно быть.

      1. Не работает в принципе, выдаёт ошибку. Я пробовал в качестве источника MemoryMappedFile использовать локальный файл, но вероятно нужно просто файл использовать, не загружая в RAM. Я просто не понимаю что нужно исправить в коде.

        1. Вы протестируйте сначала обмен данными через файл между C++ и C# без DLL, а просто создайте на C++ консольное приложение, тогда можно будет отладку использовать и видеть по шагам что происходит и что не так, как схему обкатаете нужную Вам, перенесете ее в DLL и будет Вам счастье 🙂
          Погуглите "работа с файлами C++", вот здесь есть самый простейший пример работы с файлом: https://quikluacsharp.ru/c-c-osnovy/odin-iz-prostejshih-sposobov-otladki-dll-c-c-rabotayushhej-s-imenovannoj-pamyatyu/
          Я не пытался вместо MMF использовать обычный файл никогда, по этому не могу примером поделиться, не знаю что там и как конкретно нужно сделать, но предполагаю, что все не сложно.

  4. Здравствуйте. Меня интересует такой вопрос: можно ли MapViewOfFile вызывать не в функциях, а один раз - сразу после CreateFileMapping? Код в этом случае упростится, но не приведет ли это к возможным ошибкам в процессе выполнения?

  5. Добрый вечер. А какое время используете для сохранения стакана(текущее время локальное)? Или время сервера? Как проблему с мили секундами решаете?

    1. Добрый вечер! Время сервера, а с миллисекундами никак по стакану, только по всем сделкам. По стакану только секунды по getInfoParam("SERVERTIME"), да и то не факт, что они совпадают со сделками.

  6. А как было бы правильно хранить полученные из квика данные? Скажем получаем мы стакан каждый раз при срабатывании OnQuote. При этом происходит обновление одного или нескольких значений, остальные остаются без изменений. Хранить только изменения в стакане(что более компактно) или же сохранять всё со значительной избыточностью?Что-то мне кажется второй вариант потянет на десятки гигабайт сохранённой истории за месяц. За год даже не представляю сколько это получится ...Может быть есть другой разумный вариант?

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

  7. Добрый день!
    Исходя из примеров видно, что при взаимодействии С# и dll (C++) обмен идет через определенные именованные участки памяти и понятно, что какие данные ожидать.
    А вот между Lua и dll (C++) получается весь обмен идет через стек и потребуется именовать каждую строку и добавлять на стек, а далее разбирать этого стек, например, при получении в C#. (1)
    Или можно между Lua и одной dll (C++) оперировать несколькими стеками(2)? Ну и третий вариант это запускать несколько dll (C++) и скриптов Lua, которые будут работать в разных потоках (3).
    По скорости предполагаю третий вариант (3) будет наиболее быстрый и понятный. А какой вариант был бы по Вашему мнению более правильный?

    1. Добрый день, Виктор! По моему мнению более правильный вариант, это использовать одну DLL и передавать в нее данные из скрипта, вызывая ее функции, передавая данные в параметрах, например, в скрипте Вы передаете в DLL разные данные так:

      QluaCSharpConnector.SendData1("Данные 1");
      QluaCSharpConnector.SendData2("Данные 2");

      А в DLL Вы получаете их соответственно в функциях:

      static int forLua_SendData1(lua_State *L)
          //Получает из Lua-стека переданное значение
          const char *Data = lua_tostring(L, 1); // В Data будет строка "Данные 1"
      end;
       
      static int forLua_SendData2(lua_State *L)
          //Получает из Lua-стека переданное значение
          const char *Data = lua_tostring(L, 1); // В Data будет строка "Данные 2"
      end;

      Потому что для каждого отдельного вызова функции используется отдельный стек!
      Т.е. если Вы даже так напишите:

      QluaCSharpConnector.SendData1("Данные 1");
      QluaCSharpConnector.SendData1("Данные 2");

      "Данные 1" и "Данные 2" не окажутся в одном стеке, а это будут 2 вызова функции и 2 разных стека, соответственно!

        1. Т.е. в общем виде схема такая: Данные отправляются из Lua в качестве параметра функции в DLL, там они берутся из переданного стека и записываются в именованную память, после чего эта память читается в C#.