При отправке из терминала 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(); } |
Здравствуйте. Пытаюсь получить только цену покупки из стакана, ничего более. Не могу понять - почему у меня в C# попадают одно и то же значение. Как будто # запомнил 1 элемент который был в MMF и выводит постоянно именно его ?
https://cdn1.savepice.ru/uploads/2018/2/2/cb6cae0349ff5121c430b13ea42e51f9-full.png
Хотя при банальном выводе в файл (текстовый документ) через DLL в файле значения актуальные. Значит дело точно не в скрипте. С++ код :
static int forLua_StartDataGet(lua_State *L)
{
if (hFileMapMyMemory)
{
PBYTE pb = (PBYTE)(MapViewOfFile(hFileMapMyMemory, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 256));
if (pb != NULL)
{
while (Run)
{
double value = lua_tonumber(L, -1);
char buff[256];
sprintf(buff, "%f", value);
memcpy(pb, buff, strlen(buff));
Sleep(1000);
}
UnmapViewOfFile(pb);
CloseHandle(hFileMapMyMemory);
}
}
return 0;
}
Выводит ерунду как на скрине. А вот так нормальные значения :
float value = lua_tonumber(L, 1);
ofstream file;
file.open("C:\\BCS_Work\\Teach_QUIK\\QluaTEST.txt", ios::app);
file << value;
file << endl;
file.close();
Sleep(1000);
В чем тут дело, не подскажете ?
Здравствуйте! А почему у Вас здесь минус единица:
double value = lua_tonumber(L, -1);
а здесь просто единица:
float value = lua_tonumber(L, 1);
?
пробовал разные варианты. В стеке только 1 элемент поэтому не думаю что ошибка в этом участке. Что с конца один что с начала один.
Там в примере, если Вы видели, есть закомментированная строка:
Воспользуйтесь ею, чтобы исключить ошибку в DLL, если увидите, что в DLL все хорошо, то ищите проблему в C#
В робот данные попадают, обратно в скрипт нет.
Значит попробуйте сделать все в точности как в примере
Подгрузил в датагрид стакан по вашему коду. Если все брать в точности то работает корректно конечно. Еще одно подтверждение тому, что проблема не в коде, а в том кто его пишет )
Просто тот код уже продебажен ранее 🙂
Подскажите как бы оптимальнее задать значение "dwMaximumSizeLow" при создании файла в памяти ? Как я понимаю это минимальный размер файла, верно ? (https://msdn.microsoft.com/en-us/library/windows/desktop/aa366537(v=vs.85).aspx)
Я собираюсь OpenInterest выгружать из квика по нескольким инструментам (возможно одновременно) и возможно по нескольким таймфреймам.
В целом интересно как этот параметр рассчитывали Вы ? и будет ли этот файл динамическим или же с фиксированным размером ? (не разу так данными не обменивался между приложениями)
Здравствуйте! Я, как-то, не озадачивался особо этим вопросом, просто делал с запасом, вообще, примерно, 1 символ это 1 байт. На сколько я знаю, файл сам не расширяется.
Получается что если к примеру меньше размер сделать, то можно обрезать цену (первые 2 символа допустим войдут, а остальные нет я верно понимаю ?)
И еще кое что, при загрузке данных графиков через Lua ограничение квика на 3000 свечей так же сохраняется ? Или же можно грузить хоть от истока времен ?
По первому вопросу Вы правильно понимаете.
По второму, сейчас уже в несколько раз больше свечей в терминале хранится, но, по прежнему, в QLua можно получить только те, которые есть в терминале.
Благодарю
Всегда пожалуйста
Дмитрий, добрый день! Надеюсь сможете уделить немного времени и помочь разобраться со стеком 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 работают, и данные в С# передаются в обоих направлениях.
Задача решена. решение было здесь 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); // освобождает стек для следующей итерации
}
Доброго времени суток.
Реализовал передачу стакана. но дело в том, что при обработке его в программе, распределяется покупка/продажа, только если стакан полный, то есть состоит из 40 записей. Подскажите как можно раскидать стакан на покупку/продажу, если он не полный, то есть к примеру на продажу имеется 5 записей, а на покупку 11?
Сам отвечу на свой вопрос.
В стакане стандартно может находиться не более 40 записей, мы можем посмотреть количество записей по Спросу и Предложению находится в стакане по отдельности, таким образом я могу доставить нулевые записи куда мне нужно и корректно поделить стакан на Спрос и Предложение.
Немного измененный скрипт (под мои нужды, может кому понадобится)
Мой способ обработки полученного стакана в программе
Функция, которая заполняет DataGridView на форме и красит его:
Скрин, каким получается стакан в моем случае:

Изображение:
Если что то коряво и/или не правильно, то прошу сильно не ругать и тапками не кидаться =)
Прошу прощения, почему то посъедались все спец символы =(
Хорошо, когда люди сами отвечают на свои вопросы 🙂 Спецсимволы подправил, это не Ваша вина, это такая дурацкая защита в движке сайта.
спасибо за исправления спец символов))
надеюсь кому нибудь пригодится код))
Скорее всего!)
Привет! В стакане стандартно может находиться не более 100 записей, а не 40.
Вы так-то время засекали, сколько у вас процесс передачи стакана идет из OnQuote?
Что за мания везде тыркать глобальные переменные без нужды? Медленнее доступ к ним, чем к локальным.
Так же как вот такая конструкция: ql2.offer[i].quantity неравно nil - нафига вам эта проверка?
А если ql2.offer равно nil или тупо ql2 равно nil, тогда что? Загнется програмуля?
Есть такая функция не документированная getQuoteLevel2Ex, в которой и qty и price имеют тип "number".
Ах да, совсем забыл. Что за манечка делать расчеты на С#? Луа стакан посчитает быстрее, чем дойдет до стека та строка, которую вы героически склеиваете и туда пытаетесь всунуть, и время не нужно тратить на всякую ересь типа квик + С#.
Оба языка написаны на С, да и арка что-то выбрала Луа, не C#, а там ваще не глупые люди работают.
Хотя дело ваше...
Добрый день. Всё отлично работает через MemoryMappedFile, но как выполнить ту же задачу, но через обычный файл расположенный на жестком диске, не используя MemoryMappedFile?
Я попробовал, но ничего не вышло, хотя решение практически идентичное должно быть.
Добрый день! Что именно не вышло? Не пишет, или не читает?
Не работает в принципе, выдаёт ошибку. Я пробовал в качестве источника MemoryMappedFile использовать локальный файл, но вероятно нужно просто файл использовать, не загружая в RAM. Я просто не понимаю что нужно исправить в коде.
Вы протестируйте сначала обмен данными через файл между 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 использовать обычный файл никогда, по этому не могу примером поделиться, не знаю что там и как конкретно нужно сделать, но предполагаю, что все не сложно.
Ок, попробую.
Здравствуйте. Меня интересует такой вопрос: можно ли MapViewOfFile вызывать не в функциях, а один раз - сразу после CreateFileMapping? Код в этом случае упростится, но не приведет ли это к возможным ошибкам в процессе выполнения?
Здравствуйте! Я, честно говоря, не пробовал, но, теоритически, не должно быть ошибки, мы же таким образом просто получаем ссылку на область памяти. Попробуйте!
Спасибо. Посмотрю этот вариант.
Добрый вечер. А какое время используете для сохранения стакана(текущее время локальное)? Или время сервера? Как проблему с мили секундами решаете?
Добрый вечер! Время сервера, а с миллисекундами никак по стакану, только по всем сделкам. По стакану только секунды по getInfoParam("SERVERTIME"), да и то не факт, что они совпадают со сделками.
А как было бы правильно хранить полученные из квика данные? Скажем получаем мы стакан каждый раз при срабатывании OnQuote. При этом происходит обновление одного или нескольких значений, остальные остаются без изменений. Хранить только изменения в стакане(что более компактно) или же сохранять всё со значительной избыточностью?Что-то мне кажется второй вариант потянет на десятки гигабайт сохранённой истории за месяц. За год даже не представляю сколько это получится ...Может быть есть другой разумный вариант?
Думаю, второй, предложенный Вами вариант, самый оптимальный. Т.е. создавать на каждый день новый файл, предположим, вначале записывать весь стакан, а потом блоки только изменившихся значений со временем, таким образом можно будет для каждого дня восстановить историю стакана. Так же можно программно архивировать файл каждого дня по завершению, а когда нужно, программно разархивировать и прочитать. Думаю это не так много места займет на диске.
Добрый день!
Исходя из примеров видно, что при взаимодействии С# и dll (C++) обмен идет через определенные именованные участки памяти и понятно, что какие данные ожидать.
А вот между Lua и dll (C++) получается весь обмен идет через стек и потребуется именовать каждую строку и добавлять на стек, а далее разбирать этого стек, например, при получении в C#. (1)
Или можно между Lua и одной dll (C++) оперировать несколькими стеками(2)? Ну и третий вариант это запускать несколько dll (C++) и скриптов Lua, которые будут работать в разных потоках (3).
По скорости предполагаю третий вариант (3) будет наиболее быстрый и понятный. А какой вариант был бы по Вашему мнению более правильный?
Добрый день, Виктор! По моему мнению более правильный вариант, это использовать одну DLL и передавать в нее данные из скрипта, вызывая ее функции, передавая данные в параметрах, например, в скрипте Вы передаете в DLL разные данные так:
А в DLL Вы получаете их соответственно в функциях:
Потому что для каждого отдельного вызова функции используется отдельный стек!
Т.е. если Вы даже так напишите:
"Данные 1" и "Данные 2" не окажутся в одном стеке, а это будут 2 вызова функции и 2 разных стека, соответственно!
Вашу статью https://quikluacsharp.ru/qlua-c-cpp-csharp/vzaimodejstvie-lua-i-biblioteki-dll-napisannoj-na-c-c/ изучил, т.е. получается второй вариант (2) отпадает, тогда какой из вариантов (1) или (2) кажется Вам более целесообразным.
Спасибо! попробую отработать данный вариант.
Т.е. в общем виде схема такая: Данные отправляются из Lua в качестве параметра функции в DLL, там они берутся из переданного стека и записываются в именованную память, после чего эта память читается в C#.