Евгений Шибаев
Евгений Шибаев личный блог
07 ноября 2020, 13:39

QLua скринер стакана. Или стакан к празднику!

Всем привет, и желаю здравствовать!
Вчера один наш коллега попросил решить простенькую задачу — отображать стаканный спред в моменте, то есть (best_ask_price+best_bid_brice)/2 с помощью луа-скрипта. Вот такой был диалог:
QLua скринер стакана. Или стакан к празднику!
Чего уж проще, выдался час свободного времени решил помочь. Но походу разработки, пришли идеи сделать, что-то типа скринера стакана с дополнительной информацией, которая, возможно будет полезна для анализа.
А идеи возникли следующие: добавить для мониторинга объем всех бидов и асков, разницу (дельту) между объемами покупок и продаж. Но это можно теперь наблюдать даже на графике в Квике(не прошло и 15-ти лет). А вот следующая идея показалась мне интересной. Рассчитывать в моменте VWP (Volume Weighted Price) цену взвешенную на объем для бидов и асков по отдельности. Чтобы было понятно о чем идет речь покажу это на примере стакана в Jatotrader для RIZ0:
QLua скринер стакана. Или стакан к празднику!
Volume Weighted Ask Price рассчитывается как отношение суммы произведений объема заявок на продажу на ценовых уровнях на цены этих уровней к общему объему заявок на продажу VWAsk=Sum(Ask[i][Price]*Ask[i][Size])/Sum(Ask[i][Size]). Для бидов — по аналогии. Другими словами, взвешенная на объем цена асков показывает средний ценовой уровень концентрации объема продаж в стакане, а для бидов — объема покупок. Чем ближе с спреду VWAsks, тем давление продавцов больше, и наоборот, чем ближе к спреду VWBids, тем давление покупателей больше.
В итоге, получилась такая табличка:
QLua скринер стакана. Или стакан к празднику!
Столбец по ТЗ (техзаданию) выделен красным. Остальные столбцы: «VW bid price» — взвешенная на объем цена бидов в стакане, «VW bid spread» — расстояние в пунктах цены VW bid price от лучшего бида. «VW ask price» — взвешенная на объем цена асков в стакане, «VW ask spread» — расстояние в пунктах цены «VW ask price» от лучшего аска. «VW DELTA» — показывает разницу между «VW bid spread» и «VW ask spread», т.е. насколько ближе к спреду VWbid по сравнению с  VWask. Если значение отрицательное, например для RIZ0 -6.7, это означает, что «VW ask price» ближе к спреду, чем «VW bid price» примерно на 7 пунктов. 
Sum BIDS — объем всех заявок на покупку, Sum ASKS — объем всех заявок на продажу, DELTA — разница в объеме заявок на покупку и продажу.
Градиентная подсветка «VW DELTA» и «DELTA», обозначает, что чем ярче цвет, тем больше «перевес» одного значения над другим по отношению к сумме этих значений.
Электричества не кушает совсем, в динамике выглядит так https://gifyu.com/image/Ryjm
На самом деле текущее состояние параметров в таблице не столь интересно, как их изменение во времени (динамика). Что я и постараюсь реализовать в Jatotrader на следующей неделе.
Чуть не забыл про "сиськи" сам код, как всегда — несколько строчек:
-- ©2020 by Evgeny Shibaev, а пользуются ВСЕ !!!
-- Таблица, отображающая суммарный объем бидов и асков в стакане, их разницу (DELTA),frfytв процентах рост(падение) инструмента финансового рынка за определенное количество дней
-- Какие инструменты(тикеры) отслеживаем. Таблица пар тикер - площадка
tickers = {["SiZ0"] = "SPBFUT", ["RIZ0"] = "SPBFUT", ["BRZ0"] = "SPBFUT", ["GAZP"] = "TQBR", ["SBER"] = "TQBR", ["YNDX"] = "TQBR"}
-- GZZ0 = "SPBFUT", SRZ0 = "SPBFUT", GMKN = "TQBR", MGNT = "TQBR", SU26207RMFS9 = "TQOB"

sources = {} -- Список источников данных по количеству тикеров
rows = {} -- Список строк в таблице по количеству тикеров
screener = AllocTable() -- Указатель на таблицу
stopped = false -- Остановка скрипта
local max = math.max  -- локальная ссылка на math.max
local min = math.min  -- локальная ссылка на math.min

TICKER_COLUMN       = 0
PRICE_SPREAD_COLUMN = 1
VW_BID_PRICE_COLUMN = 2
VW_BID_PRICE_SPREAD = 3
VW_DELTA_COLUMN     = 4
VW_ASK_PRICE_SPREAD = 5
VW_ASK_PRICE_COLUMN = 6
SUM_BID_COLUMN      = 7
SUM_ASK_COLUMN      = 8
DELTA_COLUMN        = 9

-- Функция вызывается перед вызовом main
function OnInit(path)
   -- Добавляем столбцы в таблицу
   AddColumn(screener, TICKER_COLUMN, "Ticker", true, QTABLE_STRING_TYPE, 15)
   AddColumn(screener, PRICE_SPREAD_COLUMN, "Spread Price", true, QTABLE_DOUBLE_TYPE, 12)   
   AddColumn(screener, VW_BID_PRICE_COLUMN, "VW bid price", true, QTABLE_DOUBLE_TYPE, 15)
   AddColumn(screener, VW_BID_PRICE_SPREAD, "VW bid spread", true, QTABLE_DOUBLE_TYPE, 15)
   AddColumn(screener, VW_ASK_PRICE_COLUMN, "VW ask price", true, QTABLE_DOUBLE_TYPE, 12)
   AddColumn(screener, VW_ASK_PRICE_SPREAD, "VW ask spread", true, QTABLE_DOUBLE_TYPE, 12)
   AddColumn(screener, SUM_BID_COLUMN, "Sum BIDS", true, QTABLE_DOUBLE_TYPE, 12)
   AddColumn(screener, SUM_ASK_COLUMN, "Sum ASKS", true, QTABLE_DOUBLE_TYPE, 12)
   AddColumn(screener, DELTA_COLUMN, "DELTA", true, QTABLE_DOUBLE_TYPE, 12)
   AddColumn(screener, VW_DELTA_COLUMN, "VW DELTA", true, QTABLE_DOUBLE_TYPE, 12)
   CreateWindow(screener)
  -- Даем название  таблице
   SetWindowCaption(screener, "BID-ASK Screener")
   for ticker, board in pairs(tickers) do
       --Для каждого тикера определяем строку в таблице и запоминаем ее в rows
       rows[ticker] = InsertRow(screener, -1)
       --В первом столбце каждой строки будет имя тикера
       SetCell(screener, rows[ticker], 0, ticker)
   end
end

--Функция вызывается при каждом изменении стакана любого тикера
function OnQuote(board, ticker)
   if stopped then return end
   local sec = getSecurityInfo(board, ticker)
   local afterpoint = sec["scale"] + 1 -- Округляем значения на один знак после запятой больше чем знаков в цене
   local avg_format = "%."..afterpoint.."f"
   local level2 = getQuoteLevel2(board, ticker) --Получаем стакан
   local bid_size, ask_size, best_bid_price, best_ask_price, bid_vwsum, ask_vwsum  = 0, 0, 0, 0, 0, 0
   -- Проход по бидам
   for index, bid in ipairs(level2["bid"]) do
       best_bid_price = bid["price"]
       bid_size = bid_size + bid["quantity"]
       bid_vwsum = bid_vwsum + (bid["quantity"] * bid["price"])
   end
   -- Проход по аскам
   for index, ask in ipairs(level2["offer"]) do
       ask_size = ask_size + ask["quantity"]
       ask_vwsum = ask_vwsum + (ask["quantity"] * ask["price"])
   end
   best_ask_price = level2["offer"][1].price
   local VW_BidPrice = bid_vwsum/bid_size
   local VW_AskPrice = ask_vwsum/ask_size
   local VW_BidSpread = best_bid_price - VW_BidPrice
   local VW_AskSpread = VW_AskPrice - best_ask_price
   local DeltaChange = (bid_size - ask_size) / (bid_size + ask_size) * 100
   local SpreadChange = (VW_AskSpread - VW_BidSpread) / (VW_AskSpread + VW_BidSpread) * 100
   local wh = RGB(255,255,255)
   local gr = RGB(0,128,0)
   local rd = RGB(128,0,0)
   SetCell(screener, rows[ticker], DELTA_COLUMN, string.format("%d", bid_size - ask_size))
   -- Подкрашиваем ячейку соответственно росту(падению) и величины роста(падения)
   SetColor(screener, rows[ticker], DELTA_COLUMN, BCellColor(DeltaChange), FCellColor(DeltaChange), BCellColor(DeltaChange), FCellColor(DeltaChange))
   SetCell(screener, rows[ticker], SUM_BID_COLUMN, string.format("%d", bid_size))
   SetColor(screener, rows[ticker], SUM_BID_COLUMN, wh, gr, wh, gr)
   SetCell(screener, rows[ticker], SUM_ASK_COLUMN, string.format("%d", ask_size))
   SetColor(screener, rows[ticker], SUM_ASK_COLUMN, wh, rd, wh, rd)
   SetCell(screener, rows[ticker], VW_BID_PRICE_SPREAD, string.format(avg_format, VW_BidSpread))
   SetColor(screener, rows[ticker], VW_BID_PRICE_SPREAD, wh, gr, wh, gr)
   SetCell(screener, rows[ticker], VW_ASK_PRICE_SPREAD, string.format(avg_format, VW_AskSpread))
   SetColor(screener, rows[ticker], VW_ASK_PRICE_SPREAD, wh, rd, wh, rd)
   SetCell(screener, rows[ticker], VW_DELTA_COLUMN, string.format(avg_format, VW_AskSpread - VW_BidSpread)) 
   SetColor(screener, rows[ticker], VW_DELTA_COLUMN, BCellColor(SpreadChange), FCellColor(SpreadChange), BCellColor(SpreadChange), FCellColor(SpreadChange))
   SetCell(screener, rows[ticker], VW_BID_PRICE_COLUMN, string.format(avg_format, VW_BidPrice))
   SetColor(screener, rows[ticker], VW_BID_PRICE_COLUMN, wh, gr, wh, gr)
   SetCell(screener, rows[ticker], VW_ASK_PRICE_COLUMN, string.format(avg_format, VW_AskPrice))
   SetColor(screener, rows[ticker], VW_ASK_PRICE_COLUMN, wh, rd, wh, rd)
   SetCell(screener, rows[ticker], PRICE_SPREAD_COLUMN, string.format(avg_format, (best_ask_price+best_bid_price)/2))
end

-- Цвет текста в ячейке. Если рост - то цвет "зеленый", падение - "красный"
function FCellColor(change) if change > 0 then return RGB(0,0,0) else return RGB(0,0,0) end end

-- Маленькая "тепловая карта". Делает фон ячейки более интенсивным, взависимости от процента изменения величины
function BCellColor(change)
  bright = math.floor(255 - min(math.abs(change*5), 235),1)  --10 110
  if change > 0 then return RGB(bright,255,bright) else return RGB(255,bright,bright) end
end

-- Функция вызывается перед остановкой скрипта
function OnStop(signal) stopped = true end

-- Функция вызывается перед закрытием квика
function OnClose() stopped = true end;

-- Основная функция выполнения скрипта
function main()
  while not stopped do sleep(1) end
end
Или ссылка ScreenerBidAsk.lua

Как уже говорил, мне очень стыдно)), но у меня есть канал на ютьюбе, но зато нет канала в телеге!

ЗЫ: мои трудозатраты на код — 1 час (или 1700 руб), но так как задачка в итоге получилась интересная для меня — то бесплатно. Написание топика полчаса — а это дофига!

Количество инструментов может быть любым — насколько потянет память, добавите в коде по образцу в список tickers.

Если будет время в скрипт добавлю фичу — настройку глубины просмотра стакана — сейчас стакан берется целиком, а интересно наблюдать за теми, кто «не сыкует» и не прячется за «жирные» заявки, т.е. находится вблизи спреда.

Да, проверить можно будет в понедельник, как откроются торги (надеемся, что откроются).


Искренне Ваш!







34 Комментария
  • Френк френков
    07 ноября 2020, 14:12
    Напишу чтобы скачать
  • Никто
    07 ноября 2020, 14:13
    Вот блсгодарствуйте
  • Bazilius
    07 ноября 2020, 14:20
    Красава! Мне не нужно, но спасибо!!!
  • Сергей Серов
    07 ноября 2020, 14:23
    Всё это правильно если работать без плечей! Или при торговле фьючерсами лучше иметь большой обьём для обеспечения ГО. Иначе, дядя Коля постучится в твою дверь рано или поздно. Но как насчёт диверсификации открытых позиций шортом? Я допустим беру лонг, через какое то время цена идёт против меня, я почти всегда беру шорт, чтобы не крыть позу с убытком! При таком раскладе даже, в принципе, и стопы не обязательны, но крайне желательны!
    • _sg_
      07 ноября 2020, 14:36
      Сергей Серов,
      покупайте дешевых дальних опционов при работе с фьючом.
      Таким образом и ГО уменшится и дядя Коля уже никогда не постучится.
      Все уже давно придумано до нас без нас и для нас.
      • Сергей Серов
        07 ноября 2020, 15:06
        _sg_, я бы с удовольствием, но никогда не работал с опционами. Вот хочу освоить!!!
      • asfa
        07 ноября 2020, 17:50
        _sg_, вы этот метод используете?
  • Никто
    07 ноября 2020, 14:25
    Стакан едва ли что дачт, если правильно понял, вот что то бы более глобальное считать. Любопытно по каким алгаритмам у банков роботы работают. Стакан может и поможет у кого быстродействие хорошее. Заметил или показалось, когда кидаешь заявку, в стакане предложение цены там как бы отскскивает в противоположную сторону.
  • _sg_
    07 ноября 2020, 14:31
    Великолепно, однозначно плюсую.
    Я в своих старых разработках дальше средневзвеса бидов и оферов не пошел. А здесь интересная идея с
    «VW DELTA, «VW bid spread» «VW ask spread».
    Браво,…
  • Igr
    07 ноября 2020, 14:52
    надо бы скачать, спасибо +
  • AlexGood
    07 ноября 2020, 14:56
    Вот спасибо! Будем проверять! А обычная маркет-дельта аск-бид по прошедшим сделкам у вас случайно не написада для Квик, чтобы как вертикальные объемы по каждому бару показывала?
    • Никто
      07 ноября 2020, 18:08
      AlexGood, вся торговля по спросу идет, предложение отскакивает, и становится тем же спросом, bid это спрос вроде
  • Coconut
    07 ноября 2020, 15:21
    Дык в стакане ММ сидят, и скорее всего не одной заявкой, дробят. Сентимент от этого искажается.
    Если бы можно было со 100% уверенностью исключить все биды и аски маркетмейкеров, то анализ стакана имел бы место. И то не по любому инструменту. В USD/RUB, например, такая дичь в стакане при волатильности, что хоть заанлизируйся. За шаг до исполнения ММ свои заявки на пару тысяч лотов снимает или смещает. И плавают там его биды/аски как Г в проруби.
    • AlexGood
      07 ноября 2020, 17:29
      Kokos Abrikos, 
      Дык в стакане ММ сидят, и скорее всего не одной заявкой, дробят. Сентимент от этого искажается.
      Если бы можно было со 100% уверенностью исключить все биды и аски маркетмейкеров, то анализ стакана имел бы место. 

      то есть идея автора с VW DELTA на практике не очень себя покажет для прогноза будущей цены?
  • Роджер (веселый).
    07 ноября 2020, 16:07
    Все это хорошо, но на практике порой не работает. При большой интенсивности торгов функция getQuoteLevel2(board, ticker)
    часто возвращает хрен знает что, а не стакан. если offer более менее терпимо, то bid не можешь получить по несколько минут пока торги не успокоятся.
  • Роджер (веселый).
    07 ноября 2020, 16:10
    Очень много вычислений и операций для функции обратного вызова onQuote возьми штук двадцать тикеров и квик повиснет к чертям. И лучше в main записать Subscribe_Level_II_Quotes(CLASS_CODE[i], SEC_CODE[i]), а не то стакан закроешь, и скрипт не работает.
      • Роджер (веселый).
        07 ноября 2020, 16:32
        Евгений Шибаев, Я писал торгового робота и блок рассчитывал мне цену, за которую я могу реализовать денежный объем одномоментно исходя из текущего состояния стакана. Одним словом до какого уровня я смогу продавить стакан своей заявкой. Так наступали моменты что мне nil возвращала функция, особенно по bid.  getQuoteLevel2(board, ticker) не возвращала данные стакана при сильной интенсивности торгов. А еще у меня робот колом становился и повисал из за OnQoute я ее вообще убрал, и начал обращаться к стакану через main когда нужны данные стакана, и все проблемы с повисанием терминала ушли. Но стакан тебе возвращает так нестабильно, что пришлось писать для альтернативный вариант через getParamEx bid. offer и хотя бы видеть крайние точки стакана.
          • Роджер (веселый).
            07 ноября 2020, 23:14
            Евгений Шибаев, Да он как то стакан хитро выгружает, оферы есть, бидов часто нет. Или есть но мало. Но бывает и оферов нет, но редко. И совсем редко он nil выдает по самой таблице getQuoteLevel2(board, ticker). я вот думал, а можно как нибудь в настройках ограничить данные по стакану, к примеру пятью оферами и бидами? Там что на дальних берегах не сильно интересно.
              • Роджер (веселый).
                08 ноября 2020, 09:56
                Евгений Шибаев, вот и я об этом думаю, иначе не успевают полный стакан передавать  и обрабатывать.
              • Роджер (веселый).
                08 ноября 2020, 10:00
                Евгений Шибаев, в ПСБ более слабая система, так они передают урезанные стаканы, в Открытие посильней, так полный. Вот я и думал, может дело в настройках.
  • Kot_Begemot
    07 ноября 2020, 16:26
    Ого, флудят в стаканы своими лимитками! Спред аж 3000 пунктов в SIZ0, в жизни бы не подумал!

    Вероятно, есть смысл делить каждую следующую заявку на 2, чтобы учесть предполагаемую вероятность её исполнения, тогда и дальше 7 позиции можно не заглядывать.
  • asfa
    07 ноября 2020, 17:57
    Добрый день, Евгений. С праздником!
    А как там вопрос с новым коннектором?
      • asfa
        07 ноября 2020, 19:23
        Евгений Шибаев, ясно, спасибо
  • Михаил Перминов
    07 ноября 2020, 20:25
    -- ©2020 by Evgeny Shibaev, а пользуются ВСЕ !!!
  • Socol
    08 ноября 2020, 06:14
    Очень познавательно Евгений, большое спасибо!
  • A_M_K
    08 ноября 2020, 13:57
    Спасибо, Respect.
  • GoodBargains
    02 июня 2022, 15:24
    Поставил, работает. Только вот почему память все время растет? Что там добавить, чтобы не росла, не подскажите?

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн