Работа колбеков

Страницы: 1 2 След.
RSS
Работа колбеков
 
Просто наблюдение. Пока не могу подтвердить реальным тестом.

В последних версиях терминала, по ощущениям начиная с 12-ой, наличие колбека в скрипте стало ощутимо "тормозить" терминал.
Естественно и раннее колбек использовался только для передачи в очередь. Но теперь даже это приводит к периодическим замираниям терминала.
Особенно если у скрипта есть и окно со своим колбеком на обновление.

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

Сильнее это проявляется если запустить несколько скриптов и даже в спокойное время. При этом на демо Quik Junior, где поток данных очевидно низкий, это малозаметно.

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

Примечательно, что есть скрипт из одной строки запускающий dll реализующий jRPC сервер из Квика на плюсах. И если подписаться в нём на OnQuote, то происходит то же самое. Казалось бы - библиотека на ++, нет окон - а терминал не даёт переключить закладки, думает.
 
Ок. Сделал нагрузочный тест https://disk.360.yandex.ru/d/P7xB_poL0QlKfAВерсия терминала 13.02.20. Реальный брокер.

Создаётся простая таблица. По списку (всего 7) инструментов выводятся данные.

Чтобы была какая-то нагрузка, то берем два колбека OnParam и OnQuote. Данные в таблицу выводятся только из main. В колбеке OnParam вызывается getParamEx несколько раз (нагрузка же).

OnQuote только для записи флага. Чтение DOM тоже в main.

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

В итоге скрипт занимает примерно 1 мб. Что не так и много.

Запускаю на реальном брокере  и терминал "умирает". Не совсем, но уже ощутимо видно как таблица текущих торгов, да и весь интерфейс, перестаёт работать "плавно".
В таблице данные выводятся с задержками, приходится "пинать окно" (вспоминаем тему https://forum.quik.ru/messages/forum10/message49120/topic3777/#message49120)

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

При этом в более старых версиях терминала не замечалось особого влияния размера скрипта на работу.
 
можете сделать вывод времени входа и выхода для колбеков и для  main  c помощью точного счетчик Windows?
 
Давно эта тема обсуждалась, ещё в 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, где поток данных очевидно низкий, это малозаметно.
Поэтому у разработчиков всё отлично и "не воспроизводится".
Надо делать так, как надо. А как не надо - делать не надо.
 
Цитата
Старатель написал:
Оценить оверхэд от колбэков можно на следующих скриптах:
Как использовать:
  1. Настройка получения данных выполняется через пункт меню Система / Настройки / Основные настройки..., раздел «Программа» / «Получение данных» / «Обезличенные сделки»
    В поле «Выбрать классы инструментов» выбрать все без фильтров! Это важно для корректных измерений.
    Если у вас уже выбраны все классы без фильтров этот шаг можно пропустить. Если нет, - то нажать кнопку «Перезаказать данные».
     
  2. Подождать пока прокачаются все сделки и запустить первый скрипт.
     
  3. Снова перезаказать обезличенные сделки и дождаться, когда скрипт выведет итоговый результат и остановится.
     
  4. Повторить 3-й пункт со вторым скриптом.
Надо делать так, как надо. А как не надо - делать не надо.
 
Цитата
nikolz написал:
можете сделать вывод времени входа и выхода для колбеков и для  main  c помощью точного счетчик Windows?
Если без излишеств, просто через os.clock, то сами колбеки работаю микросекунды. main уже зависит о того, что делает, но там тоже не что-то существенное.

Я пока наблюдаю странное - подключаешь несколько библиотек на 1000 функций - и привет. Собственно это в тесте и сделано, чтобы проверить влияние этого.
Я, конечно, понимаю, что Lua - не тот язык в котором обычно пишутся модули на тысячи строк, но как-то я не встречал упоминаний о влиянии этого на производительность. Хотя если вспомнить World of Warcraft, то ему не мешает.

Пришлось разбираться, т.к. в последних версиях терминала стало всё очень медленно если запущены "большие" скрипты, банально вкладки переключаются со скрипом.
Пальцем в небо - что-то сделали с GC. У меня функции использую локальные переменные, а функций много. В итоге GC надо работать. Как он работает в Квике не знаю, но надеюсь, что независимо от основного потока.

Впрочем, приведённый нагрузочный тест не делает ничего такого, чтобы сильно потреблять память, а падение производительности есть.
 
Цитата
Nikolay написал:
Я пока наблюдаю странное - подключаешь несколько библиотек на 1000 функций - и привет.
    Странно. Я ваш тест проверял в QUIK 13.0.1.2 брокер ВТБ. Никаких особенностей не заметил (с подключением пакетов и без). В вашем тесте мной был выполнен замер Время выполнения всех коллбеков / Общее время выполнения скрипта: ~0,0004.
  Вы выполняли ваш тест на другом ПК?
 
Цитата
TGB написал:
Никаких особенностей не заметил (с подключением пакетов и без).
Сколько у вас колбэков в секунду?
Надо делать так, как надо. А как не надо - делать не надо.
 
Думаю, что всё зависит от времени, потока данных, да и брокера. На ВТБ позже проверю.
Я проверяю на одном небольшом брокере, у которого сервера не особо "широкие".

Вот в 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
 
Цитата
Старатель написал:
Сколько у вас колбэков в секунду?
   На тесте Nikolay:
1) OnQuote всего (в сек.) = ~68
2) OnParam всего (в сек.) = ~482
3) OnParam обработано отфильтрованных (в сек.) = ~16
   В моих скриптах эти колбэки не отрабатываются.
 
Ок. Запустил на VTB  в 10 утра. И это просто финиш. В основном была обработка main за 0-0.5 секунды.

А вот этот кусок за 593.92. Записал даже видео работы, где видно что происходит если запустить скрипт. Редактирование поля в таблице обновляется только после пинка. Цена не соответствует данным в ТТТ.
Как только скрипт остановлен, то ТТТ сразу оживает.

Видео https://disk.360.yandex.ru/i/VcfnlUv3JoFizQ


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
---------------------------------------------------------------------------

колбеки, колбеки ....


---------------------------------------------------------------------------
Enter OnParam SPBFUT CNYRUBF
Exit OnParam 0.0 SPBFUT CNYRUBF 100754 11.603
---------------------------------------------------------------------------
Enter OnParam SPBFUT MMU6
Exit OnParam 0.0 SPBFUT MMU6 100754 2087.15
---------------------------------------------------------------------------
Enter OnParam SPBFUT MMU6
Exit OnParam 0.0 SPBFUT MMU6 100754 2087.15
---------------------------------------------------------------------------
Enter OnParam SPBFUT MMU6
Exit OnParam 0.0 SPBFUT MMU6 100754 2087.15
---------------------------------------------------------------------------
Enter OnParam SPBFUT SiU6
Exit OnParam 0.0 SPBFUT SiU6 100754 79628.0
---------------------------------------------------------------------------
Enter OnParam SPBFUT MMU6
Exit OnParam 0.0 SPBFUT MMU6 100754 2087.15
---------------------------------------------------------------------------
Enter OnParam SPBFUT MMU6
Exit OnParam 0.0 SPBFUT MMU6 100754 2087.15
---------------------------------------------------------------------------
Exit main loop 593.92
---------------------------------------------------------------------------
 
Еще раз тест, что запустил на VTB
Код
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

 
Цитата
Nikolay написал:
Запустил на VTB  в 10 утра. И это просто финиш.
   А если запустить только ваш тест без остальных ваших скритов?
 
Правда здесь уже, видимо, проявилась нагрузка на запись лога внутри колбеков, т.к. запись в файл внутри колбека -  это очень плохая идея.
Поэтому лучше заменить логирование на PrintDbgStr.
Код
local function log(...)
    PrintDbgStr(log_tostring(...))
end

В первой версии теста лога не было. Но, с другой стороны, это тоже такой тест, явно  показывающий как main просто перестает вызываться.

Цитата
TGB написал:
В моих скриптах эти колбэки не отрабатываются.
Тоже, особенно OnQuote. Но бывают задачи где это необходимо и я стал замечать проблемы. Пришлось смотреть, что происходит.
 
Цитата
Nikolay написал:
log('Exit OnParam', os.clock() - s, class_code,  sec_code, res.last_rec, last_price)
Вы так ничего не измерите. Обработка кода внутри колбэка идёт на ура. Оверхэд возникает из-за самого вызова колбэка, даже пустого. Смотрите мой пример выше.[QUOTE]
Надо делать так, как надо. А как не надо - делать не надо.
 
Цитата
Старатель написал:
Вы так ничего не измерите. Обработка кода  внутри  колбэка идёт на ура. Оверхэд возникает из-за самого вызова колбэка, даже пустого. Смотрите мой пример  выше
Возможно, но моя задача понять что вызывает проблемы. Пока я натолкнулся на влияние на работу main при странном сочетание наличия таблицы и обработки её реакций, библиотек и колбеков. Сами колбеки, судя по всему, работают быстро.
 
Цитата
TGB написал:
А если запустить только ваш тест без остальных ваших скритов?
Выключил все скрипты, только этот. Лог в PrintDbgStr. Вот участок с странным временем. Для начала, есть OnParam с 244 млс. Общее время 1.193 сек для 55 отфильтрованных колбеков.

[17220] ---------------------------------------------------------------------------
[17220] Enter main loop Wed Jul 22 11:05:47 2026
17220] Enter OnParam SPBFUT MMU6
[17220] Exit OnParam 0.2440000000006 SPBFUT MMU6 110547 2110.85
[17220] ---------------------------------------------------------------------------
[17220] Enter OnQuote SPBFUT MMU6
[17220] Exit OnQuote 0.0019999999985885 SPBFUT MMU6
[17220] ---------------------------------------------------------------------------
[17220] Enter OnQuote TQBR SBER
[17220] Exit OnQuote 0.0 TQBR SBER
................................................................................................................
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT MMU6
[17220] Exit OnParam 0.0 SPBFUT MMU6 110548 2110.65
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT SiU6
[17220] Exit OnParam 0.0 SPBFUT SiU6 110548 79593.0
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT MMU6
[17220] Exit OnParam 0.0 SPBFUT MMU6 110548 2110.65
[17220] ---------------------------------------------------------------------------
[17220] Exit main loop 1.1930000000011
[17220] ---------------------------------------------------------------------------

И вот такие моменты проявляются. Естественно, чем больше скриптов запущено, тем, возможно, больше влияние.
 
Цитата
Nikolay написал:
Выключил все скрипты, только этот. Лог в PrintDbgStr
  А если отключить запись в пользовательскую таблицу?
 
Цитата
Nikolay написал:
Код
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 подозрительна.
 
Цитата
TGB написал:
функция  getInfoParam в высокочастотном  OnParam подозрительна
Ничего особенного по ней в описании нет. Впрочем, документация очень шаткая на данный момент.

Проблема OnParam в том, что он очень убог. Этот колбек должен быть реализован в виде:

OnParam(STRING class_code, STRING sec_code, TABLE chng_params)

где chng_params - это таблица с изменившимися ключами и их значениями (и еще лучше добавить время пакета в котором это приехало). А подписка на такой колбек должна быть адресной - указать инструмент и набор полей для отслеживания.

В текущей реализации приходится DDOS-ить API чтобы получить данные. Сделки могут быть раз в час, цена посл. сделки может не изменятся, а будет постоянно менять лучший спрос, предложение (есть такие инструменты где роботы постоянно меняют стакан) - постоянный холостой вызов колбека.

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

Цитата
TGB написал:
А если отключить запись в пользовательскую таблицу?
Тогда это будет скрипт без таблицы, по факту. Хотелось бы решить вопрос при наличии таблицы. Я специально добавил постоянный опрос IsWindowClosed.
 
Цитата
Nikolay написал:
Тот же LASTRECORDTIME важен, т.к. иногда приходят опоздавшие данные и их лучше пропускать.
В LASTRECORDTIME проставляется время сервера или клиента?
Надо делать так, как надо. А как не надо - делать не надо.
 
Цитата
Старатель написал:
В LASTRECORDTIME проставляется время сервера или клиента?
По моим наблюдениям - сервера.
 
Цитата
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)
 
Цитата
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
 
Цитата
TGB написал:
Может  быть протестировать измененный вариант:
Вот убрал запрос getInfoParam, запустил. Всего 86 колбеков. Все они в 1 мкс. Но main выполнялся 1.37 сек. Как-то не верится, что банальный разбор такой небольшой очереди будет столько, хоть и через очень неэффективную table.sremove. Возможна и она проблема. Позже сделаю тест с другой двойной очередью без table.sremove.

[17220] Enter main loop Wed Jul 22 17:27:34 2026
[17220] Enter OnQuote SPBFUT MMU6
[17220] Exit OnQuote 0.0 SPBFUT MMU6
[17220] ---------------------------------------------------------------------------
[17220] Enter OnQuote SPBFUT SiU6
[17220] Exit OnQuote 0.0 SPBFUT SiU6
[17220] ---------------------------------------------------------------------------
[17220] Enter OnQuote SPBFUT RIU6
[17220] Exit OnQuote 0.0 SPBFUT RIU6
[17220] ---------------------------------------------------------------------------
[17220] Enter OnQuote TQBR SBER
[17220] Exit OnQuote 0.0 TQBR SBER
[17220] ---------------------------------------------------------------------------
[17220] Enter OnQuote TQBR GAZP
[17220] Exit OnQuote 0.0 TQBR GAZP
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT SiU6
[17220] Exit OnParam 0.0 SPBFUT SiU6 nil 79273.0
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT MMU6
[17220] Exit OnParam 0.0010000000038417 SPBFUT MMU6 nil 2180.4
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT SiU6
[17220] Exit OnParam 0.0010000000038417 SPBFUT SiU6 nil 79273.0
[17220] ---------------------------------------------------------------------------

...................................................................................................................................
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT USDRUBF
[17220] Exit OnParam 0.0 SPBFUT USDRUBF nil 78.34
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT MMU6
[17220] Exit OnParam 0.0 SPBFUT MMU6 nil 2180.4
[17220] ---------------------------------------------------------------------------
[17220] Enter OnParam SPBFUT SiU6
[17220] Exit OnParam 0.0 SPBFUT SiU6 nil 79275.0
[17220] ---------------------------------------------------------------------------
[17220] Exit main loop 1.3700000000026  
 
Цитата
Станислав написал:
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 секунды. И это без их фактической обработки. Особенно это актуально в активные часы торгов, когда событий больше
Надо делать так, как надо. А как не надо - делать не надо.
 
Цитата
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 
 
Ок. Переделал на очередь (по факту - не особо лучше чем с банальной 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 - уже на порядок медленней.

Также однозначно - в колбеках никаких операций ввода-вывода.

Но я так и не могу выявить что-то одно, что может так влиять на интерфейс терминала, кроме банально низкой скорости вообще.
 
На открытии:
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
Надо делать так, как надо. А как не надо - делать не надо.
 
Я уже давно перестал дергать таблицу, т.к. после 7-ой версии терминала она стала такой хрупкой. В реальных скриптах вывод данных только по необходимости, в лучшем случае раз в минуту.
 
Иногда в логах работы скриптов видно, что банальная установка данных в таблицу может занимать 3 секунды.

Что-то типа такого:

 for k,column in ipairs(columns) do
   if values[column]~=nil then
     t:SetValue(line, k, values[column] and tostring(values[column]) or '', tonumber(values[column]) or 0)
   end
 end



Банальный итератор на 20 элементов (в таблице скрипта редко больше), а время столько

[DEBUG 2026-07-23 14:32:59] AddLine MMU6_SPBFUT_1500nt5_1500nt5_1784803613 line 4
[DEBUG 2026-07-23 14:33:02] AddLine sec_line
line:4|MXI-9.26|MMU6|SPBFUT


Три секунды...
 
Цитата
Nikolay написал:
Цитата
nikolz написал:
можете сделать вывод времени входа и выхода для колбеков и для  main  c помощью точного счетчик Windows?
Если без излишеств, просто через os.clock, то сами колбеки работаю микросекунды. main уже зависит о того, что делает, но там тоже не что-то существенное.

Я пока наблюдаю странное - подключаешь несколько библиотек на 1000 функций - и привет. Собственно это в тесте и сделано, чтобы проверить влияние этого.
Я, конечно, понимаю, что Lua - не тот язык в котором обычно пишутся модули на тысячи строк, но как-то я не встречал упоминаний о влиянии этого на производительность. Хотя если вспомнить World of Warcraft, то ему не мешает.

Пришлось разбираться, т.к. в последних версиях терминала стало всё очень медленно если запущены "большие" скрипты, банально вкладки переключаются со скрипом.
Пальцем в небо - что-то сделали с GC. У меня функции использую локальные переменные, а функций много. В итоге GC надо работать. Как он работает в Квике не знаю, но надеюсь, что независимо от основного потока.

Впрочем, приведённый нагрузочный тест не делает ничего такого, чтобы сильно потреблять память, а падение производительности есть.
Наблюдал такой прикол  ставишь индикатора регрессии на большой истории и привет ждешь старта квик очень долго.  На демо все быстро потому что там история маленькая.
 
Для себя сделал вывод.
Разработчики еще 25 лет назад честно написали QUIK - это для подачи заявок брокеру вместо телефона.
Они честно сказали и честно это соблюдают.
-------------------------------
QUIK - не для написания роботв а для подачи заявок и получения информации с биржи
-------------------------------
Роботов надо писать в отдельных приложениях
или хотя бы в отдельных потоках  с одноразовым вызовом колбеков и по возможности без графиков
 
Цитата
nikolz написал:
Наблюдал такой прикол  ставишь индикатора регрессии на большой истории и привет ждешь старта квик очень долго.  На демо все быстро потому что там история маленькая.
Это объяснимо. При старте, график инициализирует расчёт на всё истории дважды минимум (зачем? ответа так и нет). Если индикатор сделан так, что считает каждый бар с начала, что для регрессии очень редко нужно, и баров "до дури", ясно что будет так.
У регрессии сложность же O квадрат от периода, а если не линейная, то ещё хуже. И так каждый бар...
 
Ок. Чтобы показать влияние на работу интерфейса терминала еще сделал такой тест.

Терминал свежий, установлен не более двух недель назад. Брокер ВТБ. Остановил все скрипты, даже автологин.

В тестируемом скрипте добавил запоминание последних значений, чтобы выводились в окно только измененные данные.
Лог через PrintDbgStr. В колбеках вывода в лог нет, только в main.

Запускаем три экземпляра скрипта и смотрим на реакцию терминала.

Один скрипт - почти нет влияния.
Два - уже ТТТ "подвисла" и данные выводятся не так быстро.
Три - всё встало, данные появляются только если "пнуть" таблицу

В архиве скрипты, лог с таймингом работы main, запись видео работы терминала.

https://disk.360.yandex.ru/d/P7xB_poL0QlKfAКод тестового скрипта - по факту, он ничего не делает, кроме обработки колбеков, запроса данных через методы 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
 
что-то тест уж больно большой. Трудно проверить что же он все таки проверяет
 
Э... что же тогда большое.

У нас есть колбеки - настоятельно рекомендуемые к использованию. Есть окно скрипта - единственный способ вывести данные.
Банальная задача - вывести данные, полученные через getParamEx. Также, для какой-то нагрузки - читаем 7 стаканов, при этом даже не разбирая их, а банально беря верхние значения, т.е. просто запрос данных.

И выводим это в таблицу. Всё. Ни транзакций, ни расчетов, ни запросов данных графиков и потока данных - только, по факту, запрос данных ТТТ из терминала.

Проверяем как это терминал переваривает, если таких скриптов несколько. Такое на 7-ой версии терминала даже не вызывало вопросов.
 
Цитата
Nikolay написал:
Э... что же тогда большое.

У нас есть колбеки - настоятельно рекомендуемые к использованию. Есть окно скрипта - единственный способ вывести данные.
Банальная задача - вывести данные, полученные через getParamEx. Также, для какой-то нагрузки - читаем 7 стаканов, при этом даже не разбирая их, а банально беря верхние значения, т.е. просто запрос данных.

И выводим это в таблицу. Всё. Ни транзакций, ни расчетов, ни запросов данных графиков и потока данных - только, по факту, запрос данных ТТТ из терминала.

Проверяем как это терминал переваривает, если таких скриптов несколько. Такое на 7-ой версии терминала даже не вызывало вопросов.
Да стаканы читать тяжело. Но выводить все в таблицы если это робот то это избыточно.
---------------------
Относительно индикатора регрессии считаю в скользящем окне. Этот индикатор лучше чем мувинги.
 
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
 
Цитата
nikolz написал:
Относительно индикатора регрессии считаю в скользящем окне. Этот индикатор лучше чем мувинги.
Так ведь и задачи стоят передними разные. 1. Лучшее описание данных 2. Элементарное усреднение и сглаживание данных. От сюда и применение разное. Их нельзя просто сравнивать, кто лучше кто хуже. Вопрос стоит  под какую задачу сто применяется.
 
Цитата
VPM написал:
Если правильно понял то проблемы уже заметны на 7 инструментах
Иногда они заметны и на меньшем числе. Здесь основной вопрос - это деградация производительности всего терминала при достаточно простых, но частых действиях.
Таблицы уже давно стали влиять на терминал, кажется с 8-ой версии. Но сейчас это уже выходит за рамки нормального. Вполне ожидаемо было бы квотирование процессорного времени скриптам, но сам терминал должен работать без влияния. Здесь же банальная задача - и терминал лежит.

Так что хотелось бы увидеть реакцию разработчиков. Правда сразу можно сказать - на демо Quik Junior такое не наблюдается, точнее, видимо, необходимо ещё больше приложить нагрузку. А вот на реальных брокерских терминалах, особенно если ещё есть поток данных по опционам - то уже даже один скрипт может "положить" терминал.

Поэтому я и не использую колбеки. Но и без них есть проблемы, просто с ними - это видно очень отчетливо.
 
Сильное заметное наблюдаемое влияние было после перехода на  *  брокер Сбер. Пришлось бросить терминал. Интересно было бы сравнить терминал до перехода на  *  и последние навороченные версии?

Но в любом случае метрики: время выполнения, нагрузку, а сейчас добавил еще и лаг лучше держать перед глазами, кроме логирования.
 
У меня 5 брокеров - 4-е терминала постоянно открыто. Сбер не самый "плохой" из них. Я бы даже сказал вполне нормально. Самый тормозной - Кит. Самый отзывчивый Альфа. Прямая корреляция с числом клиентов и мощностью серверов. Есть стойкое ощущение, что обмен данными сервер-клиенты - занимает всё время терминала.
 
Сервера брокеров еще и включают шейпинг трафика, например я вижу на графике скорости передачи ограничение в 10 мегабит. Брокер ВТБ.  
 
Nikolay, PrintDbgStr является синхронизирующим логгером, поэтому все три скрипта синхронизируются по нему (ЕМНИП, даже тогда, когда нет читателя в виде DebugView). Аналогично все SetCell синхронизированы с главным потоком квика (гуи), поэтому все три скрипта борются за какой-нибудь lock, поэтому сама ТТТ стала тупить. Пробовали заменить SetCell на вывод в файл, расположенный на рамдиске, или иную заглушку? Полезно также сравнить с версией, когда SetCell вызывается прямо в колбэке, т.к. по идее тогда можно избежать синхронизации, ведь квик знает, что вызов идёт из гуёвого потока.

getQuoteLevel2 вроде как может тупить даже в гордом одиночестве - возможно потому, что она почему-то ждёт, когда бэкенд доформирует таблицу полностью, вместо того, чтобы отдать предыдущую сразу же.

Роль big lib не ясна, и она не измеряется тестом.

os.clock() кстати совсем не бесплатная функция, советую убрать из колбэков, если они очень часто вызываются


Я согласен, что тест слишком большой для выделения бага. Если Вам так нравится использовать таблицы qlua вместо сторонних гуи либ, я бы предложил искать баг без колбэков вообще, чисто на SetCell из нескольких скриптов с заданной частотой. Если не воспроизведётся, добавлять колбэки по одному, а в них сначала только писать в очередь, делегируя всю обработку в main, и если и так не воспроизведётся, то переносить частично обработку из main назад в колбэки, пока не станет понятно, что тупит именно из-за содержания колбэков.
 
Пока была задача воспроизвести "зависание" терминала. В реальности данные в таблицу не выводятся с такой частотой, да и в последнем  тесте убрал дублирующие установки, что сильно снизило частоту вызовов. Колбеки используются только служебные OnConnect, OnClenUp, OnTransReply, но идут частые запросы к данным через getParamEx, getItem. Здесь просто модельный скрипт с набором действий, которые часто можно встретить в скриптах.
Цитата
funduk написал:
вместо сторонних гуи либ
Я бы и рад их использовать, если писал только себе. Себе мне проще выдать данные по любому обмену, а всё написать вне Квика.

А чтобы найти надо всё отсекать и постепенно подключать, смотреть на результат. Проблема только в том, что работу скрипта ещё можно как-то измерить, а вот работу терминала только визуально. Ты вроде как SetCell вызвал, а данные в таблице появляются только если "пнуть" таблицу.
 
Цитата
Nikolay написал:
Ок. Чтобы показать влияние на работу интерфейса терминала еще сделал такой тест.

Терминал свежий, установлен не более двух недель назад. Брокер ВТБ. Остановил все скрипты, даже автологин.

В тестируемом скрипте добавил запоминание последних значений, чтобы выводились в окно только измененные данные.
Лог через PrintDbgStr. В колбеках вывода в лог нет, только в main.

Запускаем три экземпляра скрипта и смотрим на реакцию терминала.

Один скрипт - почти нет влияния.
Два - уже ТТТ "подвисла" и данные выводятся не так быстро.
Три - всё встало, данные появляются только если "пнуть" таблицу

В архиве скрипты, лог с таймингом работы main, запись видео работы терминала.

https://disk.360.yandex.ru/d/P7xB_poL0QlKfA Код тестового скрипта - по факту, он ничего не делает, кроме обработки колбеков, запроса данных через методы QLUA, и вывода данных в таблицу. Никаких расчётов, даже файловые операции убраны.
Код
Взял скрипты   На демо версии тесты не запускаются  

что не так?
 
Текст скрипта выше, проблем с запуском на 4-ех терминалов нет. Библиотеки в архиве тоже есть.
 
Цитата
nikolz написал:
что не так?
  Наверное, из-за того, что в таблице:
Код
local sec_list      = {
    {'SPBFUT', 'MMU6'},
    {'SPBFUT', 'SiU6'},
    {'SPBFUT', 'RIU6'},
    {'SPBFUT', 'USDRUBF'},
    {'SPBFUT', 'CNYRUBF'},
    {'TQBR', 'SBER'},
    {'TQBR', 'GAZP'}
}

  классы заданы для реального рынка.
Страницы: 1 2 След.
Читают тему
Наверх