Раз уж QPILE больше не развивается и его всё больше заменяет QLua, давайте перенесём комбинацию Ctrl+F11 с пункта меню "QPILE скрипты" на пункт меню "Lua скрипты". Полагаю, что пользователей, довольных этим нововведением, будет больше, чем недовольных, а реализация от разработчиков терминала практически не потребует усилий.
В каждую из папок устанавливаете соответствующий дистрибутив программы, рекомендованный каждым из брокеров (версии могут быть разными в зависимости от брокера).
Пользуетесь терминалами независимо друг от друга, возможно одновременно.
Для старта этого достаточно. Если хочется одинаковые настройки в каждом из терминалов, придётся делать дополнительные действия и рисковать несовместимостью wnd-файлов настроек из-за разных версий терминалов.
Это не сообщение о какой-то ошибке или проблеме, наоборот, это сообщение, которое может помочь программирующим на qlua скрипты роботов.
Допустим, что Ваш скрипт создаёт одно или несколько окон на своей вкладке. Если не прилагать усилий и просто создавать окна через CreateWindow, эти окна получают некоторый размер и местоположение, определяемые автоматически. Не всегда это удобно. Можно после создания указать конкретное местоположение и размеры окна с помощью функции SetWindowPos(tableId, x, y, dx, dy). Однако, откуда взять значения x, y, dx, dy? Их можно подбирать методом последовательных приближений, но ведь это неудобно!
Предлагается следующий подход. Сначала Вы располагаете графики, окна Вашего скрипта и прочие элементы так, чтобы было удобно. Потом запускаете предлагаемый ниже код. Он создаёт пустое окно, которое можно перемещать и изменять его размер с помощью мышки, накладывая это окно ровно поверх каждого окна Вашего скрипта. При этом в заголовке накладываемого окна динамически обновляются параметры x, y, dx, dy, которые нужно будет подставить в функцию SetWindowPos(tableId, x, y, dx, dy) в Вашем скрипте. При этом подбор параметров можно осуществить сразу, а не методом последовательных приближений.
Вот такой вот калибровщик положения и размеров окон получается.
Если я изобрёл велосипед, а все нормальные программисты пользуются подобными штуками, отнесусь к этому с пониманием.
Код
--
-- Подгонка размеров окна.
--
local interrupted = false
function OnStop()
interrupted = true
end
function main()
local tId = AllocTable()
CreateWindow(tId)
local topPrev, leftPrev, bottomPrev, rightPrev = 0, 0, 0, 0
while not interrupted do
if IsWindowClosed(tId) then
break
end
local top, left, bottom, right = GetWindowRect(tId)
if top == nil or left == nil or bottom == nil or right == nil then
break
end
if topPrev ~= top or leftPrev ~= left or bottomPrev ~= bottom or rightPrev ~= right then
SetWindowCaption(tId, "x=" .. tostring(left)
.. ",y=" .. tostring(top)
.. ",dx=" .. tostring(right - left)
.. ",dy=" .. tostring(bottom - top))
topPrev = top
leftPrev = left
bottomPrev = bottom
rightPrev = right
else
sleep(50)
end
end
end
В принципе, ничего не мешает в каждом коллбэке написать проверку типа: если очередь на выполнение, пополняемая из потока main непуста, выполнить функции из неё. У этого подхода есть недостатки: 1) придётся написать в каждом коллбэке строчку, вызывающую работу с очередью функций; 2) если рыночных коллбэков не происходит (например, торги остановились), main не получит требуемый результат, хотя поток коллбэков свободен.
Просьба к разработчикам терминала обратить внимание на это сообщение и дать свои конструктивные комментарии.
Допустим, что у нас есть экземпляр DataSource, который содержит в себе 5-минутные свечки:
Код
local ds = CreateDataSource(classCode, secCode, INTERVAL_M5)
Если ds используется в потоке коллбэков, то есть гарантия, что пока в коде коллбэка идёт работа с этим экземпляром (например, итерирование по индексу от 1 до ds:Size()), данные внутри него (количество свечей, их high, low, close, volume) не меняются.
Вопрос в том, как добиться стабильности внутреннего состояния в main-потоке? Ведь если я запомню в переменной размер DataSource
Код
local size = ds:Size()
перед началом цикла, то в процессе итерирования могут как добавиться новые свечи, так и обновиться старые (скажем, сначала обновилась последняя свеча, а потом добавилась новая). При этом могут возникать неприятные эффекты типа в потоке main прочитали high последней свечи, потом поток коллбэков обновил свечу так, что новое значение close стало больше уже прочитанного значения high, а потом поток main увидел последнее значение close, которое больше прочитанного ранее high.
Не уверен, что будет происходить с содержимым DataSource при смене торговой сессии, скорее всего, ничего хорошего.
Чтобы гарантированно иметь консистентные данные, можно, например, использовать ds:SetUpdateCallback, в котором заранее производить копирование состояния ds или его изменений в другой объект, и складывать в очередь, разгребая которую из потока main до опустошения, можно всегда получить последнее консистентное состояние ds. Сейчас у меня реализован этот вариант, но кажется, что он излишне нагружает скрипт, т.к. свечки нужны в потоке main раз в 5 минут, а обновление свечей в потоке коллбэков идёт постоянно.
Не уверен, что у меня есть 100% рабочий рецепт получения консистентных данных из DataSource в потоке main, но можно пытаться делать многократное чтение данных, когда параметры каждой свечи с номером i читаются до тех пор, пока не окажется, что ds:Size() не изменился и ds:T(i), ..., ds:C(i) совпадают с прочитанными ранее, после чего полагаем, что свеча i актуальна и можно переходить к следующей.
В более сложных ситуациях подобного рода проблему полностью решил бы следующий подход, когда есть возможность запуска функции в потоке коллбэков с API типа следующего:
Код
ExecuteCallback(function()
-- обращение к данным ds, которое будет произведено в потоке коллбэков
end)
В языке программирования Java в GUI-приложениях на Swing аналогом является вызов
Код
SwingUtilities.invokeLater(Runnable doRun)
Вопросы/пожелания к разработчикам: 1) рассмотреть возможность введения подобной возможности в терминал; 2) либо отказать, либо реализовать в сжатые сроки.
Главное -- понять, что дневная свеча по фьючерсу на Мосбирже строится с 19:00 (вечерняя торговая сессия) до 18:45 следующего дня, и чем это отличается от терминала (с 10:00 по 23:50 в течение суток).
У меня такая же фигня. Если в этой таблице установить курсор на первую строку, то всё нормально. Если же смотреть на последние строки, то получается как у Вас. Я как-то видео этого безобразия записывал и разработчикам отсылал. Только они ничего поделать не смогли.
Если по теме, то кажется, что с рендерингом этой таблицы истории изменения параметров какие-то реальные проблемы.
Логика ожидания полученных данных из ds (объект datasource, для которого был успешен вызов CreateDataSource), но не более timeout секунд, может быть примерно такая:
Код
local function init(ds, timeout)
local deadline = os.time() + timeout
while os.time() < deadline do
if ds:Size() > 0 then
return true
end
sleep(100)
end
return false
end
Если функция вернула false, значит что-то пошло не так. Таймаут устанавливается пользователем. Я обычно ставлю 15 секунд.
Если не хватило памяти под объекты, значит надо чаще терминал перезагружать. Не может он справиться с большим объёмом данных и/или внутренние ошибки происходят.
Разработчикам пора в дистрибутив терминала включить вот такой bat-файл, чтобы простыню из нескольких пунктов про то, что нужно удалить, не писать.
Код
del /F /Q acnt.dat
del /F /Q alerts.dat
del /F /Q alltrade.dat
del /F /Q banners.dat
del /F /Q classes.dat
del /F /Q firms.dat
del /F /Q limits.dat
del /F /Q locales.dat
del /F /Q orders.dat
del /F /Q par.dat
del /F /Q portfolio.dat
del /F /Q scripts.dat
del /F /Q search.dat
del /F /Q sec.dat
del /F /Q tmsg.dat
del /F /Q tradermsg.dat
del /F /Q trades.dat
del /F /Q trans.dat
del /F /Q transresult.dat
del /F /Q info.log
del /F /Q portfolio.log
info.exe -clear
Ещё (у меня очень редко) бывает, что терминал глючит и тогда корректный lua-код начинает работать некорректно. Ощущение, что при долгой или интенсивной работе терминала изредка происходят какие-то внутренние ошибки, после чего ссылки на некоторые функции и поля таблиц портятся. Чтобы этого избежать, перезапускаю терминал раз в 2-3 дня.
Надеюсь, что в Вашем случае это, всё-таки, ошибка программиста, и можно это исправить. Попробуйте переписать код так, чтобы не было вот таких фрагментов:
Код
value.tbl.bid[indexBid].price
а были примерно такие:
Код
local tbl = value.tbl
local bids = tbl.bid
local price = bids[indexBid].price
Это позволит локализовать ошибку и избавит от многократных обращений к таблицам (см. повторяющиеся фрагменты типа value.tbl.bid, это, к тому же, замедляет код).
Также, если indexBid равен 0 или nil, то стакан частично или полностью пустой.
А зачем Вам "дальний край" стакана value.tbl.bid[indexBid].price ? Обычно value.tbl.bid[1].price более важное значение.
В lua есть 2 функции, pcall и xpcall, для перехвата ошибок, которые могут возникнуть при исполнении кода. Посмотрите в документации к языку lua. Например, здесь http://www.lua.ru/doc/5.1.html даётся описание обеих этих функций.
Я делаю так: если в результате sendTransaction получается результат "", считаем, что транзакция отправлена, будем ждать для неё OnTransReply и OnOrder, иначе рапортуем об ошибке и думаем, что делать дальше (для лимитной заявки слать/не слать новую, для kill-заявки пытаться/не пытаться ещё раз послать kill-заявку).
Когда приходит OnTransReply() / OnOrder(), то разбираемся, что произошло и модифицируем состояние системы. Там логика сложная и в ссылке как-то описана.
Мне сильно помог такой подход: предполагать, что подавляющее большинство транзакций отправлены и обработаны без проблем, а там, где возникли какие-то ошибки, работают "заплатки", разбирающиеся с частными патологиями. Тут всё (OnOrder(), OnTrade() и OnTransReply()) надо использовать.
Реально в коде используется проверка статусов 3, 4, 5 и 13.
nero333 написал: _sk_ , а сможете ли вы запустить скрипт из другого потока и, если да, то каким образом?
Скрипт -- это некий код, часть из которого исполняется в main-потоке, а часть в потоке коллбэков. Запускается скрипт кнопкой "Запустить". После этого начинает выполняться код main() и время от времени будут вызываться коллбэки. Фраза "запустить скрипт из другого потока" кажется некорректной.
финамовец, Вам уже пора в командировку съездить в Новосибирск к разработчикам ARQA вместе со своим компьютером. Как вариант -- дать разработчикам доступ через TeamViewer, чтобы они сами всё видели, что Вы видите своими глазами.
nero333 написал: _sk_, спасибо, это хорошая идея, думаю реализовать что-то подобное. Вопрос был к разработчикам относительно кнопки "Остановить", аварийный выход - это всё-таки когда скрипт падает с выводом ошибки в поле "Ошибки выполнения скрипта".
Кнопка "Остановить" это не аварийный выход, а просто остановка скрипта. Принудительная (в Вашем термине "аварийная") остановка происходит не по нажатию кнопки, а по истечении таймаута 5 сек. И это не зависит от того что в это время делает main, что-бы Вы туда не написали скрипт принудительно завершится. Таймаут можно изменить, это делается в return события OnStop (см документацию QLUA.chm) Если Вам что-то нужно снять заявки перед остановкой скрипта, делайте это в самом OnStop
OnStop -- это последний коллбэк, который будет вызван при остановке скрипта. Однако, чтобы корректно сделать всё, что необходимо для снятия отправленных заявок в общем случае, не получится. Например, если транзакция на постановку заявки отправлена, OnTransReply ещё не пришёл, а пришёл OnStop. Как снять заявку, если мы ещё не знаем и не узнаем её orderNum? Никак.
Кроме того, в OnStop делать завершение неудобно, т.к. логика обработки находится в main-потоке.
Именно поэтому нормальная практика прерывания потока в многопоточном программировании состоит в уведомлении другого потока о том, что надо завершить работу. Эта практика и была предложена.
nero333 написал: Он не остановит работу, ведь main продолжает работать. Что вы посоветуете сделать, как выйти из main-a только удостоверившись, что все заявки сняты (а это можно сделать только по результатам колбека OnOrder)?
У меня используется следующая схема. Скрипт создаёт окно, где отображается информация о его работе (позиции, прибыли/убытки и др.). Если его закрывают нажатием на крестик в правом верхнем углу окна, то скрипт понимает, что окно закрыто (IsWindowClosed(windowId) == true), поднимает флаг, аналогичный Вашему IS_STOPPING, снимает необходимые заявки, закрывает файлы, в которые шла запись и т.п., после чего завершает исполнение функции main. Получается, что использование крестика в правом верхнем углу окна -- это штатный выход из скрипта, а нажатие кнопки "Остановить" в окне скриптов -- аварийный выход.
Когда ещё был старый форум, там это обсуждалось. Пусть кто-нибудь из представителей ARQA дальше по теме видимости изменения переменных отвечает (даёт ссылку на соответствующее обсуждение или заново тут ответит).
Приведённый мною код работает с конца 2013 года без каких-либо проблем.
Цитата
Моя блокировка отличается от такой блокировке только тем, что разблокировка происходит по уведомлению из другого треда напрямую непосредственно в момент, когда нужно произвести действия.
Конечно, так и надо делать. Только в стандартном Lua для этого нет средств, а в QLua, наверное, через sinsert как-то можно извратиться и реализовать Lock. На практике же повелось так, что в main-потоке большинство пишет цикл, совершающий нужные проверки/действия и засыпающий, если это надо/есть возможность.
В коде выше чтение и запись в submit/get не синхронизированы. Что случится, если вызов будет произведен одновременно на двух родных потоках (мейн и колбек)? Например, колбек вызовет submit, который уже модифицирует очередь при присвеоении, но еще не завершит операцию, а get уже начнет из нее читать?
submit меняет только tail, get меняет только head. И указатели двигаются только в те моменты, когда структура готова. За видимость переменных из разных потоков разработчики QLua уже подумали.
Цитата
Все же, хотелось бы избежать копирования всего и вся из коллбеков в очередь.
Передавайте только те данные, которые нужны. Полагаю, что если использовать что-то блокирующее, у Вас производительность понизится.
Как эвристику можно использовать следующее наблюдение: если при чтении из очереди она оказалась пуста, можно поставить sleep с аргументом побольше, если же непуста, то вообще не вызывать sleep.
Если очередь разгребается быстрее, чем пополняется, то в памяти растёт только массив, на базе которого реализована очередь. Этот массив занимает в памяти относительно небольшой размер, а перезапуск скрипта, который происходит раз в несколько дней, приводит к старту с малого размера массива.
Наблюдаю проблему в терминале 7.14.1.7 уже второй раз.
Строим графики цен нескольких инструментов в одном окне на низком таймфрейме (5 мин, например). Накладываем уровни Фибоначчи на график одной из цен. Ждём некоторое время, пока появляются новые свечи и график не сдвинется так, что уже не видно тех точек, по которым уровни Фибоначчи были построен. У меня обычно это наступает на следующий день после построения уровней. Возможно, надо чтобы был рестарт сервера, когда графики исчезают, а потом снова появляются.
В результате видим вот такие два графика. Первый -- это если проскроллировать график цен в прошлое, чтобы были видны точки, по которым строились уровни, а второй -- если проскроллировать график цен обратно. Когда большая свеча уходит из окна, график цен меняет свой масштаб по вертикали и при этом уровни уносит чёрт знает куда.
Один из вариантов кардинально решить проблему многопоточности при программировании на QLua описан ниже.
В потоке main делаем всю логику торгового робота, а из потока коллбэков только передаём данные в поток main через очередь. Очередь решает задачу синхронизации, коллбэки завершают свою работу максимально быстро, не тормозя UI терминала.
Реализация очереди функций на базе массива позволяет в потоке коллбэков указать, какие данные нужно использовать, и как именно. При получении функции в потоке main остаётся лишь запустить её.
Код
--
-- Реализация очереди из функций и их исполнения.
--
local Executor = {}
--- Конструктор.
-- @param self объект
local function new(self)
local queue = {
head = 1,
tail = 0
}
setmetatable(queue, self)
self.__index = self
return queue
end
Executor.new = new
--- Поместить функцию в очередь на выполнение.
-- @param self объект
-- @param f функция
local function submit(self, f)
if type(f) == "function" then
self[self.tail + 1] = f
self.tail = self.tail + 1
end
end
Executor.submit = submit
--- Получить очередную функцию из очереди.
-- @param self объект
-- @return функция
local function get(self)
if self.head > self.tail then
return nil
else
local f = self[self.head]
self[self.head] = nil
self.head = self.head + 1
return f
end
end
Executor.get = get
--- Выполнять функции из очереди либо пока они там есть, либо пока не будет выполнено указанное количество функций.
-- @param self объект
-- @param max максимальное количество исполняемых за один раз функций
local function execute(self, max)
max = max or 1000000
while max > 0 do
local f = self:get()
if f == nil then
return
else
f()
max = max - 1
end
end
end
Executor.execute = execute
return Executor
Используется это примерно так.
Код
local tradeCounters = {} -- таблица[secCode], содержащая количество обезличенных сделок по каждому инструменту
В потоке коллбэков пишем, например:
Код
function OnAllTrade(t)
executor:submit(function()
tradeCounters[t.sec_code] = (tradeCounters[t.sec_code] or 0) + 1
end)
end
В потоке main пишем цикл, достаточно часто вызывающий функцию execute, т.е. что-то типа:
Код
while not interrupted do
....
executor:execute()
....
end
В результате в потоке main выполнится функция, которую мы задали в потоке коллбэков с данными, которые в тот момент были доступны.
Не знаю, у меня в 7.14.1.7 для SRZ7 (сбер) совпадает с рассчитанной и с сайтом = 0.59.
По SRZ7 у меня тоже совпадает. Я писал про SiZ7.
В отчёте от биржи за вчерашний день комиссия за сделку по SiZ7 указана 0.82 руб. В терминале (создать окно -> сделки -> редактировать таблицу + добавить столбцы про комиссию) видим 0.91 руб с утра и 0.92 руб во второй половине дня. В торговых роботах из коллбэков OnTrade приходит комиссия 0.91/0.92 руб.
Настоятельная просьба к разработчикам терминала оперативно пояснить, какая комиссия по SiZ7 правильная? Если проблема в терминале/сервере -- укажите. Если проблема в том, что биржа так транслирует данные -- тоже укажите.
Сейчас терминал (7.12) транслирует комиссию по SiZ7 равную 0.91 руб/контракт, а на сайте биржи указано, что должна быть 0.82 руб/контракт. Где правда? Это биржа так транслирует данные?
Очень хочется, чтобы хоть у кого-то получилось понять, как гарантированно воспроизвести проблемы, подобные описанным выше. У меня не получилось, хотя я даже некий скрипт специально писал и отправлял разработчикам для тестов. Пока не будет чёткого алгоритма воспроизведения проблемы разработчики нам вряд ли помогут, к сожалению.
Отключение тиковых графиков проблему не решает, а вот отключение трансляции обезличенных сделок -- по первым ощущения ее устраняет. Мне они категорически нужны, не понимаю что делать.
Похоже, что есть некоторые ошибки в терминале, которые проявляются только под нагрузкой (и, может быть, только в некоторые дни, скажем, связанные с экспирацией или ещё какими-то событиями). Трансляция обезличенных сделок таковой является.
Для минимизации вероятности проблем можно перезапускать терминал каждый день или через день.
Конечно, хотелось бы, чтобы разработчики пофиксили причину, поскольку регулярный перезапуск терминала делать неудобно.
Если посмотреть внимательно на вопрос, то станет ясно, что он был об обезличенных сделках (OnAllTrade), а не о сделках пользователя терминала (OnTrade).
Не знаю, что надо делать с мордами невнимательных комментаторов.
Отдельные биты, как я понимаю, были сделаны разработчиками терминала из-за того, что в обезличенных сделках, представляющих собой данные по индексам, оба бита нулевые.
local SELL_FLAG = 1
local BUY_FLAG = 2
function OnAllTrade(allTrade)
...
local buySell = 0
if bit.band(currTrade.flags, BUY_FLAG) == BUY_FLAG then
buySell = 1
elseif bit.band(currTrade.flags, SELL_FLAG) == SELL_FLAG then
buySell = -1
end
...
end
Стандартная схема работы виртуальных машин: 1) выделяем какой-то объём памяти для работы; 2) работаем, периодически вызывая сборщик мусора (мелкая гребёнка); 3) если после сборки мусора осталось мало свободной памяти, выделяем больше памяти (периодическое повышение объёма) и продолжаем работать с пункта 2). Продвинутые виртуальные машины умеют уменьшать объём выделенной памяти, если потребность в ней снизилась. Похоже, что виртуальная машина lua к таким не относится.
Удаление элементов в больших таблицах., Крайне медленная работа table.remove и возможные обходные пути для быстрого удаления большого числа элементов крупных массивов/таблиц.
В такой структуре данных легко добавлять в конец, удалять из начала и итерировать по элементам. Если в буфере кончилось свободное место, можно увеличить его размер, скажем, вдвое.
Я думал collectgarbage() оценивает память используемую всем терминалом, я не прав?
Нет, это память, используемая lua-машиной отдельного скрипта.
Я так понимаю, Вы вызываете сборщик мусора в цикле вложенным в main используя счетчик времени или используете setpause"и setstepmul. Можете привести элемент кода с этой операцией?
Создаём файл Timer.lua примерно такого содержания:
Код
--
-- Реализация таймера.
--
-- Пример использования:
-- local Timer = require("util.Timer")
-- local timer = Timer:new()
-- timer:setInterval(10)
-- ...
-- if timer:isTimeout() then ...
--
local Timer = {}
--- Конструктор.
-- @param self объект
local function new(self)
local timer = {
deadline = os.time()
}
setmetatable(timer, self)
self.__index = self
return timer
end
Timer.new = new
--- Задать интервал, по прошествии которого считается, что таймер сработал.
-- @param self объект
-- @param duration число секунд, через которое должен сработать таймер.
local function setInterval(self, duration)
self.deadline = os.time() + duration
end
Timer.setInterval = setInterval
--- Задать момент времени, по прошествии которого считается, что таймер сработал.
-- @param self объект
-- @param date таблица, задающая дату, когда должен сработать таймер.
local function setDate(self, date)
self.deadline = os.time(date)
end
Timer.setDate = setDate
--- Узнать, через сколько секунд осталось до момента срабатывания таймера.
-- @param self объект
-- @return количество секунд оставшееся до срабатывания таймера или 0, если время срабатывания прошло
local function getSecondsLeft(self)
return math.max(0, self.deadline - os.time())
end
Timer.getSecondsLeft = getSecondsLeft
--- Узнать, наступил ли момент срабатывания таймера.
-- @param self объект
-- @return true/false
local function isTimeout(self)
return os.time() >= self.deadline
end
Timer.isTimeout = isTimeout
return Timer
Используется примерно так:
Код
local function printMemoryUsed()
logger:debug("Memory used: " .. math.ceil(collectgarbage("count") / 1024) .. "M")
end
local function run()
while not interrupted do
...
if garbageTimer:isTimeout() then
printMemoryUsed()
logger:debug("Collecting garbage...")
collectgarbage()
printMemoryUsed()
garbageTimer:setInterval(600)
end
...
end
end
function main()
...
run()
...
end
Чтобы исчерпать всю память надо написать очень плохой скрипт.
Или использовать один терминал для интенсивной торговли по большому количеству счетов и инструментов. По-хорошему, надо переходить на что-то более адекватное, но пока (из-за дешевизны решения) приходится мириться с недостатками терминала.
У меня компьютер аналогичный, только памяти 16 Гб и так было несколько раз при запуске нескольких скриптов сразу.
Цитата
1. Подскажите, какова возможная причина проблемы? 2. У кого были похожие ситуации, с чем они связаны и как удалось избавиться от проблем?
Виртуальным Lua-машинам не хватает памяти. Например, сборщик мусора не успевает справляться или есть какая-то утечка памяти в скриптах. Иногда это из-за внутренних ошибок терминала.
Лучший вариант по надёжности -- это перезапускать терминал перед началом торгов (после смены торговой сессии), раз в 5-15 минут в каждом скрипте вызывать collectgarbage() для сборки мусора.
Цитата
3. Как самостоятельно попытаться идентифицировать источник проблемы и оптимизировать код? 4. Есть ли возможность использовать для LUA какие-либо программы отладки, которые анализируют.
Программы для отладки мне неизвестны. Можно периодически писать в лог потребление памяти каждым скриптом (функция collectgarbage("count") выдаёт количество выделенной памяти в килобайтах) до сборки мусора и после неё. Так, возможно, поймёте, в чём проблема.
Почему-то не видно комментариев разработчиков терминала. Несоответствие объёма в свече и суммарного объёма по сделкам этой свечи кажется критичным багом.
Пусть на совести биржи останется тот факт, что сделка прошла ровно в момент 23:50. Однако, вопрос к разработчикам терминала: почему на графике RIU7 свечка, начинающаяся в 23:50, имеет объём 2, когда в сделке был объём 1?
Где правда?
Если серьёзно, то это ошибка терминала, ведь свечи строит терминал, а не биржа.