Просто наблюдение. Пока не могу подтвердить реальным тестом.
В последних версиях терминала, по ощущениям начиная с 12-ой, наличие колбека в скрипте стало ощутимо "тормозить" терминал. Естественно и раннее колбек использовался только для передачи в очередь. Но теперь даже это приводит к периодическим замираниям терминала. Особенно если у скрипта есть и окно со своим колбеком на обновление.
Наблюдаю у разных брокеров. Есть те, у кого это, можно сказать, постоянно. А есть те, у кого это только в периоды повышенной ликвидности.
Сильнее это проявляется если запустить несколько скриптов и даже в спокойное время. При этом на демо Quik Junior, где поток данных очевидно низкий, это малозаметно.
Хотелось бы понять - это у меня что-то вылезло и надо глубоко копать или такое есть и других.
Примечательно, что есть скрипт из одной строки запускающий dll реализующий jRPC сервер из Квика на плюсах. И если подписаться в нём на OnQuote, то происходит то же самое. Казалось бы - библиотека на ++, нет окон - а терминал не даёт переключить закладки, думает.
Пользователь
Сообщений: Регистрация: 27.01.2017
20.07.2026 16:50:17
Ок. Сделал нагрузочный тест Версия терминала 13.02.20. Реальный брокер.
Создаётся простая таблица. По списку (всего 7) инструментов выводятся данные.
Чтобы была какая-то нагрузка, то берем два колбека OnParam и OnQuote. Данные в таблицу выводятся только из main. В колбеке OnParam вызывается getParamEx несколько раз (нагрузка же).
OnQuote только для записи флага. Чтение DOM тоже в main.
Чтобы хоть как-то смоделировать реальный скрипт, созданы библиотеки заглушки с большим число функций. В main вызывается по одной функции из них.
В итоге скрипт занимает примерно 1 мб. Что не так и много.
Запускаю на реальном брокере и терминал "умирает". Не совсем, но уже ощутимо видно как таблица текущих торгов, да и весь интерфейс, перестаёт работать "плавно". В таблице данные выводятся с задержками, приходится "пинать окно" (вспоминаем тему )
Также на этом примере можно увидеть (по крайней мере у меня), что если не подключать большие библиотеки, то уже всё заметно живее. Т.е. банальный размер скрипта влияет.
При этом в более старых версиях терминала не замечалось особого влияния размера скрипта на работу.
Пользователь
Сообщений: Регистрация: 30.01.2015
21.07.2026 05:21:52
можете сделать вывод времени входа и выхода для колбеков и для main c помощью точного счетчик Windows?
Пользователь
Сообщений: Регистрация: 30.01.2015
Роботорговец
21.07.2026 14:54:06
Давно эта тема обсуждалась, ещё в 9-й версии. Оценить оверхэд от колбэков можно на следующих скриптах:
Код
local run = true
function OnStop()
run = nil
end
function main()
local n = getNumberOf("all_trades")
local m = n
message("Num all trades: " .. n)
repeat
sleep(1)
m = n; n = getNumberOf("all_trades")
if run == nil then return end
until n < m
local t = os.clock()
message("Num all trades: " .. m)
repeat
sleep(1)
n = getNumberOf("all_trades")
if run == nil then return end
until n >= m -- ожидание пока прокачаются все сделки
t = os.clock() - t
message(string.format("%.1f s; num: %u; avg: %.3f ms", t, n, t / n * 1000))
end
Код
local run = true
function OnStop()
run = nil
end
function main()
local n = getNumberOf("all_trades")
local m = n
message("Num all trades: " .. n)
repeat
sleep(1)
m = n; n = getNumberOf("all_trades")
if run == nil then return end
until n < m
local t = os.clock()
message("Num all trades: " .. m)
repeat
sleep(1)
n = getNumberOf("all_trades")
if run == nil then return end
until n >= m -- ожидание пока прокачаются все сделки
t = os.clock() - t
message(string.format("with OnAllTrade: %.1f s; num: %u; avg: %.3f ms", t, n, t / n * 1000))
end
function OnAllTrade(alltrade) end
Перезаказываете обезличенные сделки и замерете время прокачки всех сделок в обоих случаях. Важно: в поле «Выбрать классы инструментов» указать все инструменты без фильтров для более точных измерений! У меня результаты такие:
Код
40.2 s; num: 5719275; avg: 0.007 ms
with OnAllTrade: 85.7 s; num: 5731311; avg: 0.015 ms
Скрытый текст
Видно, что в первом случае упор идёт на пропускную способность канала, т.е. гипотетически сделки могли загрузиться быстрее при более широком канале. Во втором случае скрипт не успевает обрабатывать колбэки с той скоростью, с которой поступают сделки. А мы помним, что колбэки работают в том же потоке, что и GUI. Если у вас разницы нет, что с колбэком, что без него, значит узким местом является не процессор, а например, пропускная способность сети (подключение по Wi-Fi) или сервера. И тогда таким образом оценить оверхэд не получится.
Цитата
Nikolay написал: При этом на демо Quik Junior, где поток данных очевидно низкий, это малозаметно.
Поэтому у разработчиков всё отлично и "не воспроизводится".
Надо делать так, как надо. А как не надо - делать не надо.
Пользователь
Сообщений: Регистрация: 30.01.2015
Роботорговец
21.07.2026 15:21:10
Цитата
Старатель написал: Оценить оверхэд от колбэков можно на следующих скриптах:
Как использовать:
Настройка получения данных выполняется через пункт меню Система / Настройки / Основные настройки..., раздел «Программа» / «Получение данных» / «Обезличенные сделки» В поле «Выбрать классы инструментов» выбрать все без фильтров! Это важно для корректных измерений. Если у вас уже выбраны все классы без фильтров этот шаг можно пропустить. Если нет, - то нажать кнопку «Перезаказать данные».
Подождать пока прокачаются все сделки и запустить первый скрипт.
Снова перезаказать обезличенные сделки и дождаться, когда скрипт выведет итоговый результат и остановится.
Повторить 3-й пункт со вторым скриптом.
Надо делать так, как надо. А как не надо - делать не надо.
Пользователь
Сообщений: Регистрация: 27.01.2017
21.07.2026 16:24:42
Цитата
nikolz написал: можете сделать вывод времени входа и выхода для колбеков и для main c помощью точного счетчик Windows?
Если без излишеств, просто через os.clock, то сами колбеки работаю микросекунды. main уже зависит о того, что делает, но там тоже не что-то существенное.
Я пока наблюдаю странное - подключаешь несколько библиотек на 1000 функций - и привет. Собственно это в тесте и сделано, чтобы проверить влияние этого. Я, конечно, понимаю, что Lua - не тот язык в котором обычно пишутся модули на тысячи строк, но как-то я не встречал упоминаний о влиянии этого на производительность. Хотя если вспомнить World of Warcraft, то ему не мешает.
Пришлось разбираться, т.к. в последних версиях терминала стало всё очень медленно если запущены "большие" скрипты, банально вкладки переключаются со скрипом. Пальцем в небо - что-то сделали с GC. У меня функции использую локальные переменные, а функций много. В итоге GC надо работать. Как он работает в Квике не знаю, но надеюсь, что независимо от основного потока.
Впрочем, приведённый нагрузочный тест не делает ничего такого, чтобы сильно потреблять память, а падение производительности есть.
Пользователь
Сообщений: Регистрация: 12.05.2020
21.07.2026 22:41:32
Цитата
Nikolay написал: Я пока наблюдаю странное - подключаешь несколько библиотек на 1000 функций - и привет.
Странно. Я ваш тест проверял в QUIK 13.0.1.2 брокер ВТБ. Никаких особенностей не заметил (с подключением пакетов и без). В вашем тесте мной был выполнен замер Время выполнения всех коллбеков / Общее время выполнения скрипта: ~0,0004. Вы выполняли ваш тест на другом ПК?
Пользователь
Сообщений: Регистрация: 30.01.2015
Роботорговец
22.07.2026 00:03:34
Цитата
TGB написал: Никаких особенностей не заметил (с подключением пакетов и без).
Сколько у вас колбэков в секунду?
Надо делать так, как надо. А как не надо - делать не надо.
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 09:17:48
Думаю, что всё зависит от времени, потока данных, да и брокера. На ВТБ позже проверю. Я проверяю на одном небольшом брокере, у которого сервера не особо "широкие".
Вот в 9 утра на всплеске ликвидности запустил. Плюс, что важно, запущены и другие скрипты, но можно и этот дублировать и запускать, в реальности у нас же не один скрипт работает.
Вот участок , всего 125 млс, 9 колбеков
Enter main loop Wed Jul 22 09:03:18 2026 Enter OnQuote SPBFUT RIU6 Exit OnQuote 0.0 SPBFUT RIU6 --------------------------------------------------------------------------- Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 90318 2089.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT RIU6 Exit OnParam 0.0 SPBFUT RIU6 90318 82730.0 --------------------------------------------------------------------------- Enter OnQuote SPBFUT MMU6 Exit OnQuote 0.0 SPBFUT MMU6 --------------------------------------------------------------------------- Enter OnParam TQBR SBER Exit OnParam 0.0 TQBR SBER 90318 260.58 --------------------------------------------------------------------------- Enter OnParam TQBR GAZP Exit OnParam 0.0 TQBR GAZP 90318 85.22 --------------------------------------------------------------------------- Enter OnQuote TQBR GAZP Exit OnQuote 0.0 TQBR GAZP --------------------------------------------------------------------------- Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 90318 2089.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT RIU6 Exit OnParam 0.0010000000002037 SPBFUT RIU6 90318 82740.0 --------------------------------------------------------------------------- Exit main loop 0.125
А вот участок уже 1.36 секунды, но уже 222 колбека.
Enter main loop Wed Jul 22 09:03:19 2026
Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 90319 2089.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT USDRUBF Exit OnParam 0.0 SPBFUT USDRUBF 90319 78.6 --------------------------------------------------------------------------- Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 90319 2089.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT SiU6 Exit OnParam 0.00099999999974898 SPBFUT SiU6 90319 79637.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT RIU6 Exit OnParam 0.0 SPBFUT RIU6 90319 82730.0 --------------------------------------------------------------------------- Enter OnQuote SPBFUT SiU6 Exit OnQuote 0.0 SPBFUT SiU6 --------------------------------------------------------------------------- Enter OnQuote SPBFUT USDRUBF Exit OnQuote 0.0 SPBFUT USDRUBF --------------------------------------------------------------------------- Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 90319 2089.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT RIU6 Exit OnParam 0.0 SPBFUT RIU6 90319 82730.0 --------------------------------------------------------------------------- ............................................................................................................... чтобы не делать сообщение очень длинным убрал часть ............................................................................................................... --------------------------------------------------------------------------- Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 90320 2088.85 --------------------------------------------------------------------------- Enter OnParam SPBFUT SiU6 Exit OnParam 0.0 SPBFUT SiU6 90320 79634.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT USDRUBF Exit OnParam 0.0 SPBFUT USDRUBF 90320 78.6 --------------------------------------------------------------------------- Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 90320 2088.85 --------------------------------------------------------------------------- Exit main loop 1.3679999999999
Пользователь
Сообщений: Регистрация: 12.05.2020
22.07.2026 10:20:59
Цитата
Старатель написал: Сколько у вас колбэков в секунду?
На тесте Nikolay: 1) OnQuote всего (в сек.) = ~68 2) OnParam всего (в сек.) = ~482 3) OnParam обработано отфильтрованных (в сек.) = ~16 В моих скриптах эти колбэки не отрабатываются.
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 10:21:11
Ок. Запустил на VTB в 10 утра. И это просто финиш. В основном была обработка main за 0-0.5 секунды.
А вот этот кусок за 593.92. Записал даже видео работы, где видно что происходит если запустить скрипт. Редактирование поля в таблице обновляется только после пинка. Цена не соответствует данным в ТТТ. Как только скрипт остановлен, то ТТТ сразу оживает.
Видео
Enter main loop Wed Jul 22 10:01:25 2026 Enter OnParam SPBFUT USDRUBF Exit OnParam 0.0 SPBFUT USDRUBF 100102 78.52 --------------------------------------------------------------------------- Enter OnParam SPBFUT CNYRUBF Exit OnParam 0.0 SPBFUT CNYRUBF 100102 11.587 --------------------------------------------------------------------------- Enter OnParam SPBFUT SiU6 Exit OnParam 0.0 SPBFUT SiU6 100102 79532.0 --------------------------------------------------------------------------- Enter OnParam SPBFUT MMU6 Exit OnParam 0.0 SPBFUT MMU6 100102 2095.95 ---------------------------------------------------------------------------
local path = _G.getScriptPath()
package.path = path.."/?.lua;"
local bl1 = require('big_lib')
local bl2 = require('big_lib2')
local bl3 = require('big_lib3')
local bl4 = require('big_lib4')
local logFile
local function log_tostring(...)
local n = select('#', ...)
if n == 1 then
return tostring(select(1, ...))
end
local t = {}
for i = 1, n do
t[#t + 1] = tostring((select(i, ...)))
end
return table.concat(t, " ")
end
local function log(...)
if logFile==nil then return end
logFile:write(log_tostring(...).."\n");
logFile:flush();
end
local sec_list = {
{'SPBFUT', 'MMU6'},
{'SPBFUT', 'SiU6'},
{'SPBFUT', 'RIU6'},
{'SPBFUT', 'USDRUBF'},
{'SPBFUT', 'CNYRUBF'},
{'TQBR', 'SBER'},
{'TQBR', 'GAZP'}
}
local rev_lis = {}
for i = 1, #sec_list do
rev_lis[sec_list[i][2]] = i
end
local sleep = _G.sleep
local isRun = true
local t_id = nil
-- local SeaGreen = 12713921 -- RGB(193, 255, 193) нежно-зеленый
-- local RosyBrown = 12698111 -- RGB(255, 193, 193) нежно-розовый
-- local GetCell = _G.GetCell
-- local Highlight = _G.Highlight
local SetCell = _G.SetCell
local getParamEx = _G.getParamEx
local getInfoParam = _G.getInfoParam
local getQuoteLevel2 = _G.getQuoteLevel2
local IsWindowClosed = _G.IsWindowClosed
local message = _G.message
local table_sremove = _G.table.sremove
local queue = {}
local get_dom = {}
function _G.OnParam(class_code, sec_code)
local s = os.clock()
if t_id and rev_lis[sec_code] then
log('Enter OnParam', class_code, sec_code)
local last_price = tonumber((getParamEx(class_code, sec_code, 'LAST') or {}).param_value) or nil
local res = {}
res.last_time = tonumber((getParamEx(class_code, sec_code, 'TIME') or {}).param_value) or 0
if res.last_time == 0 then return end
res.last_price = last_price
if not res.last_price then return end
local bid_depth = tonumber((getParamEx(class_code, sec_code, 'BIDDEPTHT') or {}).param_value) or 0
res.best_bid = bid_depth > 0 and tonumber((getParamEx(class_code, sec_code, 'BID') or {}).param_value) or nil
local ask_depth = tonumber((getParamEx(class_code, sec_code, 'OFFERDEPTHT') or {}).param_value) or 0
res.best_ask = ask_depth > 0 and tonumber((getParamEx(class_code, sec_code, 'OFFER') or {}).param_value) or nil
res.last_rec = tonumber((tostring(getInfoParam('LASTRECORDTIME') or ''):gsub(':', ''))) or 0
res.local_time = (tonumber((tostring(getInfoParam('LOCALTIME') or ''):gsub(':', ''))) or 0)
res.time_diff = math.abs(res.local_time - res.last_rec) + 10
queue[#queue+1] = {class_code, sec_code, last_price}
log('Exit OnParam', os.clock() - s, class_code, sec_code, res.last_rec, last_price)
log('---------------------------------------------------------------------------')
end
end
function _G.OnQuote(class_code, sec_code)
local s = os.clock()
if t_id and rev_lis[sec_code] and not get_dom[sec_code] then
log('Enter OnQuote', class_code, sec_code)
get_dom[sec_code] = {class_code, sec_code}
log('Exit OnQuote', os.clock() - s, class_code, sec_code)
log('---------------------------------------------------------------------------')
end
end
local function CreateTable()
t_id = _G.AllocTable()
_G.AddColumn(t_id, 0, "sec", true, _G.QTABLE_STRING_TYPE, 15)
_G.AddColumn(t_id, 1, "price", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.AddColumn(t_id, 2, "bid", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.AddColumn(t_id, 3, "ask", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.AddColumn(t_id, 4, "edit", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.CreateWindow(t_id)
_G.SetWindowPos(t_id, 90, 120, 470, 300)
for i = 1, #sec_list do
_G.InsertRow(t_id, i)
SetCell(t_id, i, 0, sec_list[i][2])
end
end
local function event_callback(_, msg, par1, par2)
log('Enter TableNotificationCallback')
if (msg == _G.QTABLE_CLOSE) then
isRun = false
end
if msg == _G.QTABLE_CHAR then
SetCell(t_id, par1, 4, tostring(par2), par2)
end
log('Exit TableNotificationCallback')
log('---------------------------------------------------------------------------')
end
local function process_queue()
if #queue == 0 then return end
while #queue > 0 do
local last_price = queue[1][3]
-- local lp = GetCell(t_id, 1, 0).value or last_price
-- if lp < last_price then
-- Highlight(t_id, 1, 0, SeaGreen, 0, 500)
-- elseif lp > last_price then
-- Highlight(t_id, 1, 0, RosyBrown, 0, 500)
-- end
SetCell(t_id, rev_lis[queue[1][2]], 1, tostring(last_price), last_price)
table_sremove(queue, 1)
end
end
local function GetQuotes(class_code, sec_code)
local q = getQuoteLevel2(class_code, sec_code)
if not q then return end
local qbid = q["bid"]
local qask = q["offer"]
if not qbid and not qask then
return
end
local best_bid = tonumber(qbid[#qbid]["price"])
local best_ask = tonumber(qask[1]["price"])
SetCell(t_id, rev_lis[sec_code], 2, tostring(best_bid), best_bid)
SetCell(t_id, rev_lis[sec_code], 3, tostring(best_ask), best_ask)
get_dom[sec_code] = nil
end
function _G.OnInit()
CreateTable()
_G.SetTableNotificationCallback(t_id, event_callback)
end
function _G.main()
for i = 1, #sec_list do
if not _G.Subscribe_Level_II_Quotes(sec_list[i][1], sec_list[i][2]) then
return message('Не удалось подписаться на получение данных стакана по инструменту '..tostring(sec_list[i][1])..'|'..tostring(sec_list[i][2]))
end
end
logFile = io.open(path.."\\load_test.txt", "w")
local lt = os.time()
while isRun do
local s = os.clock()
log('Enter main loop', os.date())
IsWindowClosed(t_id)
local ct = os.time()
local u
-- if ct-lt >= 2 then
-- for i = 1, #sec_list do
-- queue[#queue+1] = {sec_list[i][1], sec_list[i][2], tonumber(getParamEx(sec_list[i][1], sec_list[i][2], 'LAST').param_value)}
-- get_dom[sec_list[i][2]] = {sec_list[i][1], sec_list[i][2]}
-- u = true
-- end
-- lt = ct
-- end
process_queue()
for i = 1, #sec_list do
if get_dom[sec_list[i][2]] then
GetQuotes(sec_list[i][1], sec_list[i][2])
end
end
bl1.func1()
bl2.func100()
bl3.func200()
bl4.func300()
log('Exit main loop', os.clock()-s, u and 'update' or '')
log('---------------------------------------------------------------------------')
sleep(60)
end
end
function _G.OnStop()
isRun = false
if t_id and not IsWindowClosed(t_id) then
_G.DestroyTable(t_id)
end
end
Пользователь
Сообщений: Регистрация: 12.05.2020
22.07.2026 10:45:20
Цитата
Nikolay написал: Запустил на VTB в 10 утра. И это просто финиш.
А если запустить только ваш тест без остальных ваших скритов?
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 10:46:15
Правда здесь уже, видимо, проявилась нагрузка на запись лога внутри колбеков, т.к. запись в файл внутри колбека - это очень плохая идея. Поэтому лучше заменить логирование на PrintDbgStr.
Код
local function log(...)
PrintDbgStr(log_tostring(...))
end
В первой версии теста лога не было. Но, с другой стороны, это тоже такой тест, явно показывающий как main просто перестает вызываться.
Цитата
TGB написал: В моих скриптах эти колбэки не отрабатываются.
Тоже, особенно OnQuote. Но бывают задачи где это необходимо и я стал замечать проблемы. Пришлось смотреть, что происходит.
Вы так ничего не измерите. Обработка кода внутри колбэка идёт на ура. Оверхэд возникает из-за самого вызова колбэка, даже пустого. Смотрите мой пример .[QUOTE]
Надо делать так, как надо. А как не надо - делать не надо.
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 11:00:36
Цитата
Старатель написал: Вы так ничего не измерите. Обработка кода внутри колбэка идёт на ура. Оверхэд возникает из-за самого вызова колбэка, даже пустого. Смотрите мой пример
Возможно, но моя задача понять что вызывает проблемы. Пока я натолкнулся на влияние на работу main при странном сочетание наличия таблицы и обработки её реакций, библиотек и колбеков. Сами колбеки, судя по всему, работают быстро.
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 11:16:43
Цитата
TGB написал: А если запустить только ваш тест без остальных ваших скритов?
Выключил все скрипты, только этот. Лог в PrintDbgStr. Вот участок с странным временем. Для начала, есть OnParam с 244 млс. Общее время 1.193 сек для 55 отфильтрованных колбеков.
if t_id and rev_lis[sec_code] then
log('Enter OnParam', class_code, sec_code)
local last_price = tonumber((getParamEx(class_code, sec_code, 'LAST') or {}).param_value) or nil
local res = {}
res.last_time = tonumber((getParamEx(class_code, sec_code, 'TIME') or {}).param_value) or 0
if res.last_time == 0 then return end
res.last_price = last_price
if not res.last_price then return end
local bid_depth = tonumber((getParamEx(class_code, sec_code, 'BIDDEPTHT') or {}).param_value) or 0
res.best_bid = bid_depth > 0 and tonumber((getParamEx(class_code, sec_code, 'BID') or {}).param_value) or nil
local ask_depth = tonumber((getParamEx(class_code, sec_code, 'OFFERDEPTHT') or {}).param_value) or 0
res.best_ask = ask_depth > 0 and tonumber((getParamEx(class_code, sec_code, 'OFFER') or {}).param_value) or nil
res.last_rec = tonumber((tostring(getInfoParam('LASTRECORDTIME') or ''):gsub(':', ''))) or 0
res.local_time = (tonumber((tostring(getInfoParam('LOCALTIME') or ''):gsub(':', ''))) or 0)
res.time_diff = math.abs(res.local_time - res.last_rec) + 10
queue[#queue+1] = {class_code, sec_code, last_price}
log('Exit OnParam', os.clock() - s, class_code, sec_code, res.last_rec, last_price)
Из выше приведенного, где может быть блокировка или зацикливание, "темный" фрагмент ?:
Код
res.last_rec = tonumber((tostring(getInfoParam('LASTRECORDTIME') or ''):gsub(':', ''))) or 0
res.local_time = (tonumber((tostring(getInfoParam('LOCALTIME') or ''):gsub(':', ''))) or 0)
Формально, почему бы нет, но функция getInfoParam в высокочастотном OnParam подозрительна.
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 13:00:50
Цитата
TGB написал: функция getInfoParam в высокочастотном OnParam подозрительна
Ничего особенного по ней в описании нет. Впрочем, документация очень шаткая на данный момент.
Проблема OnParam в том, что он очень убог. Этот колбек должен быть реализован в виде:
где chng_params - это таблица с изменившимися ключами и их значениями (и еще лучше добавить время пакета в котором это приехало). А подписка на такой колбек должна быть адресной - указать инструмент и набор полей для отслеживания.
В текущей реализации приходится DDOS-ить API чтобы получить данные. Сделки могут быть раз в час, цена посл. сделки может не изменятся, а будет постоянно менять лучший спрос, предложение (есть такие инструменты где роботы постоянно меняют стакан) - постоянный холостой вызов колбека.
Впрочем, этот тест просто для нагрузки, так что можно и запрашивать данные. Тот же LASTRECORDTIME важен, т.к. иногда приходят опоздавшие данные и их лучше пропускать.
Цитата
TGB написал: А если отключить запись в пользовательскую таблицу?
Тогда это будет скрипт без таблицы, по факту. Хотелось бы решить вопрос при наличии таблицы. Я специально добавил постоянный опрос IsWindowClosed.
Пользователь
Сообщений: Регистрация: 30.01.2015
Роботорговец
22.07.2026 13:15:52
Цитата
Nikolay написал: Тот же LASTRECORDTIME важен, т.к. иногда приходят опоздавшие данные и их лучше пропускать.
В LASTRECORDTIME проставляется время сервера или клиента?
Надо делать так, как надо. А как не надо - делать не надо.
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 13:38:20
Цитата
Старатель написал: В LASTRECORDTIME проставляется время сервера или клиента?
По моим наблюдениям - сервера.
Пользователь
Сообщений: Регистрация: 12.05.2020
22.07.2026 14:36:22
Цитата
Nikolay написал: Ничего особенного по ней в описании нет.
Все таки предлагается протестировать измененный вариант:
Код
res.last_rec = 0 --- tonumber((tostring(getInfoParam('LASTRECORDTIME') or ''):gsub(':', ''))) or 0
res.local_time = 0 --- (tonumber((tostring(getInfoParam('LOCALTIME') or ''):gsub(':', ''))) or 0)
Пользователь
Сообщений: Регистрация: 12.05.2020
22.07.2026 14:47:19
Цитата
Nikolay написал: Ничего особенного по ней в описании нет.
Может быть протестировать измененный вариант:
Код
res.last_rec = 0 --- tonumber((tostring(getInfoParam('LASTRECORDTIME') or ''):gsub(':', ''))) or 0
res.local_time = 0 --- (tonumber((tostring(getInfoParam('LOCALTIME') or ''):gsub(':', ''))) or 0)
написал: Оценить оверхэд от колбэков можно на следующих скриптах:
Как использовать: Настройка получения данных выполняется через пункт меню Система / Настройки / Основные настройки..., раздел «Программа» / «Получение данных» / «Обезличенные сделки» В поле «Выбрать классы инструментов» выбрать все без фильтров! Это важно для корректных измерений. Если у вас уже выбраны все классы без фильтров этот шаг можно пропустить. Если нет, - то нажать кнопку «Перезаказать данные». Подождать пока прокачаются все сделки и запустить первый скрипт. Снова перезаказать обезличенные сделки и дождаться, когда скрипт выведет итоговый результат и остановится. Повторить 3-й пункт со вторым скриптом.
122.6 s; num: 7088692; avg: 0.017 ms with OnAllTrade: 378.9 s; num: 7125638; avg: 0.053 ms
13.0.1.2
Пользователь
Сообщений: Регистрация: 27.01.2017
22.07.2026 17:36:05
Цитата
TGB написал: Может быть протестировать измененный вариант:
Вот убрал запрос getInfoParam, запустил. Всего 86 колбеков. Все они в 1 мкс. Но main выполнялся 1.37 сек. Как-то не верится, что банальный разбор такой небольшой очереди будет столько, хоть и через очень неэффективную table.sremove. Возможна и она проблема. Позже сделаю тест с другой двойной очередью без table.sremove.
Станислав написал: 122.6 s; num: 7088692; avg: 0.017 ms with OnAllTrade: 378.9 s; num: 7125638; avg: 0.053 ms
Оверхэд на колбэк не менее 0.036 мс, а то и больше. Не знаю, будут ли эти значения такими же для OnParam, OnQuote и др. Но если да, то умножте эту величину на количество всех колбэков в секунду, которые получает рабочий скрипт, и получите время, затрачиваемое только на вызов колбэков в течение 1 секунды. И это без их фактической обработки. Особенно это актуально в активные часы торгов, когда событий больше
Надо делать так, как надо. А как не надо - делать не надо.
Пользователь
Сообщений: Регистрация: 12.05.2020
22.07.2026 19:25:55
Цитата
Nikolay написал: Как-то не верится, что банальный разбор такой небольшой очереди будет столько, хоть и через очень неэффективную table.sremove. Возможна и она проблема. Позже сделаю тест с другой двойной очередью без table.sremove.
Очередь, реализованная таким образом, что в ней невозможны блокировки, если она используется только между двумя потоками (main и потоком коллбеков). Варианты реализации такой очереди на форуме выкладывались не один раз (в том числе Старателем), но так как реализация короткая, то я повторюсь: Код:
Код
---- Функции работы с очередями --
--- При использовании между потоком колбеков и потоком main очереди потокобезопасные. ---
--- Создать очередь
function QueueNew()
return {first = 1, last = 0 }
end
--- Записать в очередь --
function QueuePush(self, v)
if v == nil then error('!!! Ошибка. Параметр v функции QueuePush nil') end
local last = self.last + 1
self[last] = v
self.last = last
return last - self.first + 1 -- количество элементов в очереди ---
end
--- Читать очередь --
function QueuePop(self)
local first = self.first
if first > self.last then return nil end
local v, v1 = self[first], self[first + 1]
self.first = first + 1
self[first] = nil
v1 = type (v1) == 'string' and #v1 or 0
return v, v1 -- v1 -длина следующего элемента (если строка) ---
end
-- Текущий размер очереди ---
function QueueSize(self)
return self.last - self.first + 1
end
Пользователь
Сообщений: Регистрация: 27.01.2017
23.07.2026 13:16:02
Ок. Переделал на очередь (по факту - не особо лучше чем с банальной table.sremove)
Итог. Худшие результаты:
Exit main loop 4.3549999999996 in queue 164 queue time 3.875 dom time 0.19399999999951
Exit main loop 2.5430000000015 in queue 116 queue time 2.4950000000008 dom time 0.04700000000048
Exit main loop 2.3759999999984 in queue 97 queue time 2.357 dom time 0.018999999998414
Exit main loop 1.018 in queue 57 queue time 0.97600000000057 dom time 0.040999999999258
Exit main loop 0.9429999999993 in queue 43 queue time 0.94100000000071 dom time 0.0019999999985885
Exit main loop 0.88700000000063 in queue 43 queue time 0.48700000000099 dom time 0.39899999999943
Exit main loop 0.85599999999977 in queue 78 queue time 0.76599999999962 dom time 0.090000000000146
Если убрать из колбеков PrintDbgStr, то немного быстрее, точнее меньше выбросов, и максимальный результат такой:
Exit main loop 1.0730000000003 in queue 54 queue time 1.0709999999999 dom time 0.0020000000004075
Но есть и такие выбросы по чтению стакана
Exit main loop 0.46200000000135 in queue 3 queue time 0.055000000000291 dom time 0.40600000000086
Exit main loop 0.47899999999936 in queue 10 queue time 0.16300000000047 dom time 0.31499999999869
Exit main loop 0.34699999999975 in queue 7 queue time 0.041999999999462 dom time 0.30400000000009
При этом число инструментов по которым читался стакан (а он читается один раз за цикл main, если были вызовы OnQuote), не особо влияет. Могут все 7 прочитаться за 50 млс, а могут всего 2 уже за 300.
Пока по ощущениям - это GC. Чем больше мусора, тем хуже main. Также прослеживается линейная зависимость от размера очереди - 100 элементов = 1 секунде работы main. Что, конечно, совершенно не сравнимо с чистым Lua где та же очередь из 10 тыс. элементов обрабатывается за 2 мкс.
При этом, если эту же очередь заполнить и разобрать внутри бесконечного цикла в main, то она и разберется за эти 2 мкс:
Код
local list = Queue.new()
for i = 1, 10000 do
list:push(i)
end
local st = os.clock()
while list:pop() do end
log('process 10000 for time', os.clock() - st)
Но та же очередь, заполняемая на колбеках, а разбираемая в main - уже на порядок медленней.
Также однозначно - в колбеках никаких операций ввода-вывода.
Но я так и не могу выявить что-то одно, что может так влиять на интерфейс терминала, кроме банально низкой скорости вообще.
Пользователь
Сообщений: Регистрация: 30.01.2015
Роботорговец
23.07.2026 14:40:28
На открытии: duration: 00:00:10 (10.7 s) (10:00:44 - 10:00:55) SetCell: calls: 2899; total: 0.6 s ( 5.7%); avg: 0.21 ms; min: 0 ms; max: 19 ms
Но если подержать подольше, то может быть и так: duration: 00:23:28 (1408.5 s) (11:21:15 - 11:44:44) SetCell: calls: 174682; total: 101.4 s (7.2%); avg: 0.58 ms; min: 0 ms; max: 446 ms
Надо делать так, как надо. А как не надо - делать не надо.
Пользователь
Сообщений: Регистрация: 27.01.2017
23.07.2026 14:55:03
Я уже давно перестал дергать таблицу, т.к. после 7-ой версии терминала она стала такой хрупкой. В реальных скриптах вывод данных только по необходимости, в лучшем случае раз в минуту.
Пользователь
Сообщений: Регистрация: 27.01.2017
23.07.2026 15:06:32
Иногда в логах работы скриптов видно, что банальная установка данных в таблицу может занимать 3 секунды.
Что-то типа такого:
fork,columninipairs(columns) do ifvalues[column]~=nilthen t:SetValue(line, k, values[column] andtostring(values[column]) or'', tonumber(values[column]) or0) end end
Банальный итератор на 20 элементов (в таблице скрипта редко больше), а время столько
написал: можете сделать вывод времени входа и выхода для колбеков и для main c помощью точного счетчик Windows?
Если без излишеств, просто через os.clock, то сами колбеки работаю микросекунды. main уже зависит о того, что делает, но там тоже не что-то существенное.
Я пока наблюдаю странное - подключаешь несколько библиотек на 1000 функций - и привет. Собственно это в тесте и сделано, чтобы проверить влияние этого. Я, конечно, понимаю, что Lua - не тот язык в котором обычно пишутся модули на тысячи строк, но как-то я не встречал упоминаний о влиянии этого на производительность. Хотя если вспомнить World of Warcraft, то ему не мешает.
Пришлось разбираться, т.к. в последних версиях терминала стало всё очень медленно если запущены "большие" скрипты, банально вкладки переключаются со скрипом. Пальцем в небо - что-то сделали с GC. У меня функции использую локальные переменные, а функций много. В итоге GC надо работать. Как он работает в Квике не знаю, но надеюсь, что независимо от основного потока.
Впрочем, приведённый нагрузочный тест не делает ничего такого, чтобы сильно потреблять память, а падение производительности есть.
Наблюдал такой прикол ставишь индикатора регрессии на большой истории и привет ждешь старта квик очень долго. На демо все быстро потому что там история маленькая.
Пользователь
Сообщений: Регистрация: 30.01.2015
23.07.2026 17:15:05
Для себя сделал вывод. Разработчики еще 25 лет назад честно написали QUIK - это для подачи заявок брокеру вместо телефона. Они честно сказали и честно это соблюдают. ------------------------------- QUIK - не для написания роботв а для подачи заявок и получения информации с биржи ------------------------------- Роботов надо писать в отдельных приложениях или хотя бы в отдельных потоках с одноразовым вызовом колбеков и по возможности без графиков
Пользователь
Сообщений: Регистрация: 27.01.2017
23.07.2026 17:35:47
Цитата
nikolz написал: Наблюдал такой прикол ставишь индикатора регрессии на большой истории и привет ждешь старта квик очень долго. На демо все быстро потому что там история маленькая.
Это объяснимо. При старте, график инициализирует расчёт на всё истории дважды минимум (зачем? ответа так и нет). Если индикатор сделан так, что считает каждый бар с начала, что для регрессии очень редко нужно, и баров "до дури", ясно что будет так. У регрессии сложность же O квадрат от периода, а если не линейная, то ещё хуже. И так каждый бар...
Пользователь
Сообщений: Регистрация: 27.01.2017
24.07.2026 11:58:11
Ок. Чтобы показать влияние на работу интерфейса терминала еще сделал такой тест.
Терминал свежий, установлен не более двух недель назад. Брокер ВТБ. Остановил все скрипты, даже автологин.
В тестируемом скрипте добавил запоминание последних значений, чтобы выводились в окно только измененные данные. Лог через PrintDbgStr. В колбеках вывода в лог нет, только в main.
Запускаем три экземпляра скрипта и смотрим на реакцию терминала.
Один скрипт - почти нет влияния. Два - уже ТТТ "подвисла" и данные выводятся не так быстро. Три - всё встало, данные появляются только если "пнуть" таблицу
В архиве скрипты, лог с таймингом работы main, запись видео работы терминала.
Код тестового скрипта - по факту, он ничего не делает, кроме обработки колбеков, запроса данных через методы QLUA, и вывода данных в таблицу. Никаких расчётов, даже файловые операции убраны.
Код
local path = _G.getScriptPath()
package.path = path.."/?.lua;"
local bl1 = require('big_lib')
local bl2 = require('big_lib2')
local bl3 = require('big_lib3')
local bl4 = require('big_lib4')
local id = 'script 1:'
local logFile
local function log_tostring(...)
local n = select('#', ...)
if n == 1 then
return tostring(select(1, ...))
end
local t = {}
t[#t + 1] = id
for i = 1, n do
t[#t + 1] = tostring((select(i, ...)))
end
return table.concat(t, " ")
end
local function log(...)
-- Пишем в отладочные сообщения, вместо лога
PrintDbgStr(log_tostring(...))
-- Пишем в отладочные сообщения, вместо лога
-- if logFile==nil then return end
-- logFile:write(log_tostring(...).."\n");
-- logFile:flush();
end
local sec_list = {
{'SPBFUT', 'MMU6'},
{'SPBFUT', 'SiU6'},
{'SPBFUT', 'RIU6'},
{'SPBFUT', 'USDRUBF'},
{'SPBFUT', 'CNYRUBF'},
{'TQBR', 'SBER'},
{'TQBR', 'GAZP'}
}
local last_data = {}
local rev_lis = {}
for i = 1, #sec_list do
rev_lis[sec_list[i][2]] = i
last_data[sec_list[i][2]] = {}
end
local sleep = _G.sleep
local isRun = true
local t_id = nil
-- local SeaGreen = 12713921 -- RGB(193, 255, 193) нежно-зеленый
-- local RosyBrown = 12698111 -- RGB(255, 193, 193) нежно-розовый
-- local GetCell = _G.GetCell
-- local Highlight = _G.Highlight
local SetCell = _G.SetCell
local getParamEx = _G.getParamEx
local getInfoParam = _G.getInfoParam
local getQuoteLevel2 = _G.getQuoteLevel2
local IsWindowClosed = _G.IsWindowClosed
local DestroyTable = _G.DestroyTable
local message = _G.message
-- local table_sremove = _G.table.sremove
local Queue = {}
Queue.__index = Queue
function Queue.new()
return setmetatable({first = 1, last = 0 }, Queue)
end
--- Записать в очередь --
function Queue:push(v)
if v == nil then error('!!! Ошибка. Параметр v функции QueuePush nil') end
local last = self.last + 1
self[last] = v
self.last = last
return last - self.first + 1 -- количество элементов в очереди ---
end
--- Читать очередь --
function Queue:pop()
local first = self.first
if first > self.last then return nil end
local v, v1 = self[first], self[first + 1]
self.first = first + 1
self[first] = nil
v1 = type (v1) == 'string' and #v1 or 0
return v, v1 -- v1 -длина следующего элемента (если строка) ---
end
-- Текущий размер очереди ---
function Queue:size()
return self.last - self.first + 1
end
local queue = Queue.new()
local get_dom = {}
function _G.OnParam(class_code, sec_code)
local s = os.clock()
if t_id and rev_lis[sec_code] then
-- log('Enter OnParam', class_code, sec_code)
local last_price = tonumber((getParamEx(class_code, sec_code, 'LAST') or {}).param_value) or nil
-- Записываем в очередь только если цена изменилась
if last_data[sec_code][1] == last_price then return end
local res = {}
res.last_time = tonumber((getParamEx(class_code, sec_code, 'TIME') or {}).param_value) or 0
if res.last_time == 0 then return end
res.last_price = last_price
if not res.last_price then return end
local bid_depth = tonumber((getParamEx(class_code, sec_code, 'BIDDEPTHT') or {}).param_value) or 0
res.best_bid = bid_depth > 0 and tonumber((getParamEx(class_code, sec_code, 'BID') or {}).param_value) or nil
local ask_depth = tonumber((getParamEx(class_code, sec_code, 'OFFERDEPTHT') or {}).param_value) or 0
res.best_ask = ask_depth > 0 and tonumber((getParamEx(class_code, sec_code, 'OFFER') or {}).param_value) or nil
res.last_rec = tonumber((tostring(getInfoParam('LASTRECORDTIME') or ''):gsub(':', ''))) or 0
res.local_time = (tonumber((tostring(getInfoParam('LOCALTIME') or ''):gsub(':', ''))) or 0)
res.time_diff = math.abs(res.local_time - res.last_rec) + 10
last_data[sec_code][1] = last_price
queue:push{class_code, sec_code, last_price}
-- log('Exit OnParam', os.clock() - s, class_code, sec_code, res.last_rec, last_price)
-- log('---------------------------------------------------------------------------')
end
end
function _G.OnQuote(class_code, sec_code)
local s = os.clock()
if t_id and rev_lis[sec_code] and not get_dom[sec_code] then
-- log('Enter OnQuote', class_code, sec_code)
get_dom[sec_code] = {class_code, sec_code}
-- log('Exit OnQuote', os.clock() - s, class_code, sec_code)
-- log('---------------------------------------------------------------------------')
end
end
local function event_callback(_, msg, par1, par2)
-- log('Enter TableNotificationCallback', msg, par1, par2)
if (msg == _G.QTABLE_CLOSE) then
isRun = false
end
if msg == _G.QTABLE_CHAR then
SetCell(t_id, par1, 4, tostring(par2), par2)
end
-- log('Exit TableNotificationCallback', msg, par1, par2)
-- log('---------------------------------------------------------------------------')
end
local function CreateTable()
t_id = _G.AllocTable()
_G.AddColumn(t_id, 0, "sec", true, _G.QTABLE_STRING_TYPE, 15)
_G.AddColumn(t_id, 1, "price", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.AddColumn(t_id, 2, "bid", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.AddColumn(t_id, 3, "ask", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.AddColumn(t_id, 4, "edit", true, _G.QTABLE_DOUBLE_TYPE, 15)
_G.CreateWindow(t_id)
_G.SetWindowPos(t_id, 90, 120, 470, 300)
for i = 1, #sec_list do
_G.InsertRow(t_id, i)
SetCell(t_id, i, 0, sec_list[i][2])
end
_G.SetTableNotificationCallback(t_id, event_callback)
end
local function process_queue()
local n = 0
local res = queue:pop()
while res do
n = n + 1
local last_price = res[3]
-- local lp = GetCell(t_id, 1, 0).value or last_price
-- if lp < last_price then
-- Highlight(t_id, 1, 0, SeaGreen, 0, 500)
-- elseif lp > last_price then
-- Highlight(t_id, 1, 0, RosyBrown, 0, 500)
-- end
SetCell(t_id, rev_lis[res[2]], 1, tostring(last_price), last_price)
-- log(' --- set pop value', n, rev_lis[res[2]], last_price)
res = queue:pop()
end
return n
end
local function GetQuotes(class_code, sec_code)
local q = getQuoteLevel2(class_code, sec_code)
if not q then return end
local qbid = q["bid"]
local qask = q["offer"]
if not qbid and not qask then
return
end
local best_bid = tonumber(qbid[#qbid]["price"])
local best_ask = tonumber(qask[1]["price"])
if last_data[sec_code][2] ~= best_bid then
SetCell(t_id, rev_lis[sec_code], 2, tostring(best_bid), best_bid)
last_data[sec_code][2] = best_bid
end
if last_data[sec_code][3] ~= best_ask then
SetCell(t_id, rev_lis[sec_code], 3, tostring(best_ask), best_ask)
last_data[sec_code][3] = best_ask
end
get_dom[sec_code] = nil
end
function _G.main()
for i = 1, #sec_list do
if not _G.Subscribe_Level_II_Quotes(sec_list[i][1], sec_list[i][2]) then
return message('Не удалось подписаться на получение данных стакана по инструменту '..tostring(sec_list[i][1])..'|'..tostring(sec_list[i][2]))
end
end
CreateTable()
-- logFile = io.open(path.."\\load_test.txt", "w")
local lt = os.time()
local wnd_kill_interval = 120
while isRun do
local cwt = os.time()
if cwt - lt > wnd_kill_interval then
lt = cwt
DestroyTable(t_id)
CreateTable()
end
-- тест скорости локальной очереди
-- local list = Queue.new()
-- for i = 1, 10000 do
-- list:push(i)
-- end
-- local st = os.clock()
-- while list:pop() do end
-- log('process 10000 for time', os.clock() - st)
-- тест скорости локальной очереди
local s = os.clock()
log('Enter main loop', os.date())
IsWindowClosed(t_id)
local ct = os.clock()
local n = process_queue()
local qt = os.clock() - ct
ct = os.clock()
local qn = 0
for i = 1, #sec_list do
if get_dom[sec_list[i][2]] then
qn = qn + 1
GetQuotes(sec_list[i][1], sec_list[i][2])
end
end
local dt = os.clock() - ct
bl1.func1()
bl2.func100()
bl3.func200()
bl4.func300()
log('Exit main loop', os.clock()-s, 'in queue', n, 'queue time', qt, 'dom time', dt, 'for', qn)
log('---------------------------------------------------------------------------')
sleep(60)
end
end
function _G.OnStop()
isRun = false
if t_id and not IsWindowClosed(t_id) then
_G.DestroyTable(t_id)
end
end
Пользователь
Сообщений: Регистрация: 30.01.2015
24.07.2026 12:10:04
что-то тест уж больно большой. Трудно проверить что же он все таки проверяет
Пользователь
Сообщений: Регистрация: 27.01.2017
24.07.2026 12:27:21
Э... что же тогда большое.
У нас есть колбеки - настоятельно рекомендуемые к использованию. Есть окно скрипта - единственный способ вывести данные. Банальная задача - вывести данные, полученные через getParamEx. Также, для какой-то нагрузки - читаем 7 стаканов, при этом даже не разбирая их, а банально беря верхние значения, т.е. просто запрос данных.
И выводим это в таблицу. Всё. Ни транзакций, ни расчетов, ни запросов данных графиков и потока данных - только, по факту, запрос данных ТТТ из терминала.
Проверяем как это терминал переваривает, если таких скриптов несколько. Такое на 7-ой версии терминала даже не вызывало вопросов.
У нас есть колбеки - настоятельно рекомендуемые к использованию. Есть окно скрипта - единственный способ вывести данные. Банальная задача - вывести данные, полученные через getParamEx. Также, для какой-то нагрузки - читаем 7 стаканов, при этом даже не разбирая их, а банально беря верхние значения, т.е. просто запрос данных.
И выводим это в таблицу. Всё. Ни транзакций, ни расчетов, ни запросов данных графиков и потока данных - только, по факту, запрос данных ТТТ из терминала.
Проверяем как это терминал переваривает, если таких скриптов несколько. Такое на 7-ой версии терминала даже не вызывало вопросов.
Да стаканы читать тяжело. Но выводить все в таблицы если это робот то это избыточно. --------------------- Относительно индикатора регрессии считаю в скользящем окне. Этот индикатор лучше чем мувинги.
Пользователь
Сообщений: Регистрация: 15.06.2023
24.07.2026 15:49:18
Nikolay, Если правильно понял то проблемы уже заметны на 7 инструментах? Так? ВТБ у меня очень не охотно отдает данные по отклику сервера, экспериментировать не хочу, как брокер настойки предложил, так и оставил. Торговля не активная робот упрощённый вариант, перевел его со Сбера там практически терминал встал. На Сбере скачал новый и оставил облигационный портфель и несколько самописных индикаторов с настройками как есть боюсь руками трогать все опять развалится. Робот событийный (оставил 3 инструмента для арбитража, 2 стратегии), в таком виде все крутится с неплохо скоростью и нормальное исполнение стоит на фьючерсе на серебро. Архитектура не большие модули и ОПП колбэки. В расчеты наворочены есть MDP, но это больше сложная математика чем вычисления. Основные задержки это конечно тоже связаны c API QUIK. Все метрики технологические наблюдаемы в живую вывожу в заголовок окна робота для контроля пульса. Окно не сложное. В таком варианте работает нормально. Накручивать инструменты побаиваюсь все обрушить, задача стоит в начале в таком виде довести до промышленного или близкого к нему уровню. Ниже колбэки которые обрабатываю + история для нескольких индикаторов.
Код
-- ====================================================================
-- ИСПРАВЛЕНО (ИЗЛОМ 2): ЧИСТЫЙ НАЛИВ ИЗ МОДУЛЯ БЕЗ ДУБЛИРОВАНИЯ ПОЛЕЙ
-- Все флаги, заголовки и REQ_PARAMS_REGISTRY уже запечатаны в MEO_Config.lua!
-- ====================================================================
_G.MEO_Config = require("MEO_Config").init()
local c = _G.MEO_Config
-- Прописываем только динамический маркер старта
c.PLATFORM_START_TIMESTAMP = os.time()
-- ====================================================================
-- ГЛOБAЛЬНЫE КOЛЛБЭКИ ТEРМИНAЛA QUIK API С МЬЮТЕКС-ЭКРАНИРОВАНИЕМ
-- ====================================================================
function OnAllTrade(trade)
if not Robot or not c.is_run or not c.IS_WARMED_UP or Robot.Is_Inside_HotPath then return end
if type(Robot.ProcessAllTradeEvent) == "function" then Robot:ProcessAllTradeEvent(trade) end
end
function OnQuote(class_code, sec_code)
if not Robot or not c.is_run or not c.IS_WARMED_UP or Robot.Is_Inside_HotPath then return end
if Robot.BookBridge and type(Robot.BookBridge.ProcessQuoteEvent) == "function" then
Robot.BookBridge:ProcessQuoteEvent(class_code, sec_code)
end
end
function OnParam(class_code, sec_code)
if not Robot or not c.is_run or not c.IS_WARMED_UP or Robot.Is_Inside_HotPath then return end
if Robot.ProcessParamEvent and type(Robot.ProcessParamEvent) == "function" then
Robot:ProcessParamEvent(class_code, sec_code)
end
end
function OnFuturesClientHolding(fut_pos)
if not Robot or not c.is_run or not c.IS_WARMED_UP or Robot.Is_Inside_HotPath then return end
if Robot.Ledger and type(Robot.Ledger.IncorporateExchangeHolding) == "function" then
Robot.Ledger:IncorporateExchangeHolding(fut_pos)
end
end
function OnFuturesLimitChange(fut_limit)
if not Robot or not c.is_run or not c.IS_WARMED_UP or Robot.Is_Inside_HotPath then return end
if Robot.ProcessFuturesLimitChangeEvent and type(Robot.ProcessFuturesLimitChangeEvent) == "function" then
Robot:ProcessFuturesLimitChangeEvent(fut_limit)
end
end
function OnTrade(trade)
if not Robot or not c.is_run or not c.IS_WARMED_UP or Robot.Is_Inside_HotPath then return end
if Robot.Tracker and type(Robot.Tracker.ProcessTradeEvent) == "function" then
Robot.Tracker:ProcessTradeEvent(trade)
end
end
function OnTransReply(reply)
if not Robot or not c.is_run or not c.IS_WARMED_UP then return end
if Robot.Tracker and type(Robot.Tracker.RegisterTransReply) == "function" then
Robot.Tracker:RegisterTransReply(reply)
end
end
function OnOrder(order)
if not Robot or not c.is_run or not c.IS_WARMED_UP then return end
if Robot.Tracker and type(Robot.Tracker.RegisterOrderEvent) == "function" then
Robot.Tracker:RegisterOrderEvent(order)
end
end
function OnStop()
c.is_run = false
if Robot and type(Robot.DestroyAndUnsubscribe) == "function" then Robot:DestroyAndUnsubscribe() end
message("MEO OS: Core supervisor engine terminated securely. All local subscriptions cleared.", 1)
return 1000
end
-- ====================================================================
-- ГЛАВНАЯ ТОЧКА ВХОДА ИЗОХРОННОГО РАНТАЙМА (КОНТУР А)
-- ====================================================================
function main()
if not c.ACCOUNT or c.ACCOUNT == "" or not c.CLIENT_CODE or c.CLIENT_CODE == "" then
c.is_run = false
message("MEO SUPERVISOR FATAL: Ключи счета не обнаружены в ОЗУ таблицы MEO_Config.lua!", 3)
return
end
package.loaded["MEO_PreFlight_Validator"] = nil
local PreFlightLauncher = require("MEO_PreFlight_Validator")
-- Аппаратный Handshake поднимает, прогревает и возвращает готовый объект Robot
_G.Robot = PreFlightLauncher.ExecuteFullHandshake(c)
if not _G.Robot then
message("MEO SUPERVISOR FATAL: Предполётная подготовка прервана отказом шлюзов!", 3)
return
end
local last_known_size = c.BAR_CACHE_IDX
local TT = GetMilliseconds()
while c.is_run do
local cur_size = _G.Robot.ds:Size()
if cur_size > 0 then
if cur_size > last_known_size then
last_known_size = cur_size
c.BAR_CACHE_IDX = cur_size
_G.Robot:ExecuteQuantumClosure(_G.Robot.ds, cur_size)
end
end
if _G.Robot.ProcessLearningQueue then _G.Robot:ProcessLearningQueue() end
if _G.Robot.TriggerUIRender then _G.Robot:TriggerUIRender() end
local current_work_time = GetMilliseconds() - TT
c.LAST_EXECUTION_LAG = current_work_time
local calculated_sleep = 200 - current_work_time
if calculated_sleep <= 0 then calculated_sleep = 1 end
sleep(calculated_sleep)
TT = GetMilliseconds()
end
end
Пользователь
Сообщений: Регистрация: 15.06.2023
24.07.2026 16:14:22
Цитата
nikolz написал: Относительно индикатора регрессии считаю в скользящем окне. Этот индикатор лучше чем мувинги.
Так ведь и задачи стоят передними разные. 1. Лучшее описание данных 2. Элементарное усреднение и сглаживание данных. От сюда и применение разное. Их нельзя просто сравнивать, кто лучше кто хуже. Вопрос стоит под какую задачу сто применяется.
Пользователь
Сообщений: Регистрация: 27.01.2017
24.07.2026 16:32:17
Цитата
VPM написал: Если правильно понял то проблемы уже заметны на 7 инструментах
Иногда они заметны и на меньшем числе. Здесь основной вопрос - это деградация производительности всего терминала при достаточно простых, но частых действиях. Таблицы уже давно стали влиять на терминал, кажется с 8-ой версии. Но сейчас это уже выходит за рамки нормального. Вполне ожидаемо было бы квотирование процессорного времени скриптам, но сам терминал должен работать без влияния. Здесь же банальная задача - и терминал лежит.
Так что хотелось бы увидеть реакцию разработчиков. Правда сразу можно сказать - на демо Quik Junior такое не наблюдается, точнее, видимо, необходимо ещё больше приложить нагрузку. А вот на реальных брокерских терминалах, особенно если ещё есть поток данных по опционам - то уже даже один скрипт может "положить" терминал.
Поэтому я и не использую колбеки. Но и без них есть проблемы, просто с ними - это видно очень отчетливо.
Пользователь
Сообщений: Регистрация: 15.06.2023
24.07.2026 16:50:31
Сильное заметное наблюдаемое влияние было после перехода на * брокер Сбер. Пришлось бросить терминал. Интересно было бы сравнить терминал до перехода на * и последние навороченные версии?
Но в любом случае метрики: время выполнения, нагрузку, а сейчас добавил еще и лаг лучше держать перед глазами, кроме логирования.
Пользователь
Сообщений: Регистрация: 27.01.2017
24.07.2026 17:14:37
У меня 5 брокеров - 4-е терминала постоянно открыто. Сбер не самый "плохой" из них. Я бы даже сказал вполне нормально. Самый тормозной - Кит. Самый отзывчивый Альфа. Прямая корреляция с числом клиентов и мощностью серверов. Есть стойкое ощущение, что обмен данными сервер-клиенты - занимает всё время терминала.
Пользователь
Сообщений: Регистрация: 30.05.2016
24.07.2026 21:43:09
Сервера брокеров еще и включают шейпинг трафика, например я вижу на графике скорости передачи ограничение в 10 мегабит. Брокер ВТБ.
Пользователь
Сообщений: Регистрация: 20.03.2023
25.07.2026 01:35:18
Nikolay, PrintDbgStr является синхронизирующим логгером, поэтому все три скрипта синхронизируются по нему (ЕМНИП, даже тогда, когда нет читателя в виде DebugView). Аналогично все SetCell синхронизированы с главным потоком квика (гуи), поэтому все три скрипта борются за какой-нибудь lock, поэтому сама ТТТ стала тупить. Пробовали заменить SetCell на вывод в файл, расположенный на рамдиске, или иную заглушку? Полезно также сравнить с версией, когда SetCell вызывается прямо в колбэке, т.к. по идее тогда можно избежать синхронизации, ведь квик знает, что вызов идёт из гуёвого потока.
getQuoteLevel2 вроде как может тупить даже в гордом одиночестве - возможно потому, что она почему-то ждёт, когда бэкенд доформирует таблицу полностью, вместо того, чтобы отдать предыдущую сразу же.
Роль big lib не ясна, и она не измеряется тестом.
os.clock() кстати совсем не бесплатная функция, советую убрать из колбэков, если они очень часто вызываются
Я согласен, что тест слишком большой для выделения бага. Если Вам так нравится использовать таблицы qlua вместо сторонних гуи либ, я бы предложил искать баг без колбэков вообще, чисто на SetCell из нескольких скриптов с заданной частотой. Если не воспроизведётся, добавлять колбэки по одному, а в них сначала только писать в очередь, делегируя всю обработку в main, и если и так не воспроизведётся, то переносить частично обработку из main назад в колбэки, пока не станет понятно, что тупит именно из-за содержания колбэков.
Пользователь
Сообщений: Регистрация: 27.01.2017
25.07.2026 09:07:08
Пока была задача воспроизвести "зависание" терминала. В реальности данные в таблицу не выводятся с такой частотой, да и в последнем тесте убрал дублирующие установки, что сильно снизило частоту вызовов. Колбеки используются только служебные OnConnect, OnClenUp, OnTransReply, но идут частые запросы к данным через getParamEx, getItem. Здесь просто модельный скрипт с набором действий, которые часто можно встретить в скриптах.
Я бы и рад их использовать, если писал только себе. Себе мне проще выдать данные по любому обмену, а всё написать вне Квика.
А чтобы найти надо всё отсекать и постепенно подключать, смотреть на результат. Проблема только в том, что работу скрипта ещё можно как-то измерить, а вот работу терминала только визуально. Ты вроде как SetCell вызвал, а данные в таблице появляются только если "пнуть" таблицу.
Пользователь
Сообщений: Регистрация: 30.01.2015
26.07.2026 05:37:17
Цитата
Nikolay написал: Ок. Чтобы показать влияние на работу интерфейса терминала еще сделал такой тест.
Терминал свежий, установлен не более двух недель назад. Брокер ВТБ. Остановил все скрипты, даже автологин.
В тестируемом скрипте добавил запоминание последних значений, чтобы выводились в окно только измененные данные. Лог через PrintDbgStr. В колбеках вывода в лог нет, только в main.
Запускаем три экземпляра скрипта и смотрим на реакцию терминала.
Один скрипт - почти нет влияния. Два - уже ТТТ "подвисла" и данные выводятся не так быстро. Три - всё встало, данные появляются только если "пнуть" таблицу
В архиве скрипты, лог с таймингом работы main, запись видео работы терминала.
Код тестового скрипта - по факту, он ничего не делает, кроме обработки колбеков, запроса данных через методы QLUA, и вывода данных в таблицу. Никаких расчётов, даже файловые операции убраны.
Код
Взял скрипты На демо версии тесты не запускаются что не так?
Пользователь
Сообщений: Регистрация: 27.01.2017
26.07.2026 10:01:04
Текст скрипта выше, проблем с запуском на 4-ех терминалов нет. Библиотеки в архиве тоже есть.