Евгений Шибаев
Евгений Шибаев личный блог
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 Комментария
  • Perl
    07 ноября 2020, 14:12
    Напишу чтобы скачать
  • Никто
    07 ноября 2020, 14:13
    Вот блсгодарствуйте
  • Bazilius
    07 ноября 2020, 14:20
    Красава! Мне не нужно, но спасибо!!!
  • Сергей Серов
    07 ноября 2020, 14:23
    Всё это правильно если работать без плечей! Или при торговле фьючерсами лучше иметь большой обьём для обеспечения ГО. Иначе, дядя Коля постучится в твою дверь рано или поздно. Но как насчёт диверсификации открытых позиций шортом? Я допустим беру лонг, через какое то время цена идёт против меня, я почти всегда беру шорт, чтобы не крыть позу с убытком! При таком раскладе даже, в принципе, и стопы не обязательны, но крайне желательны!

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

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