Блог им. jatotrade_com

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.

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

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


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







★36
33 комментария
Напишу чтобы скачать
avatar
Вот блсгодарствуйте
avatar
Красава! Мне не нужно, но спасибо!!!
avatar
Всё это правильно если работать без плечей! Или при торговле фьючерсами лучше иметь большой обьём для обеспечения ГО. Иначе, дядя Коля постучится в твою дверь рано или поздно. Но как насчёт диверсификации открытых позиций шортом? Я допустим беру лонг, через какое то время цена идёт против меня, я почти всегда беру шорт, чтобы не крыть позу с убытком! При таком раскладе даже, в принципе, и стопы не обязательны, но крайне желательны!
avatar
Сергей Серов,
покупайте дешевых дальних опционов при работе с фьючом.
Таким образом и ГО уменшится и дядя Коля уже никогда не постучится.
Все уже давно придумано до нас без нас и для нас.
avatar
_sg_, я бы с удовольствием, но никогда не работал с опционами. Вот хочу освоить!!!
avatar
_sg_, вы этот метод используете?
avatar
Стакан едва ли что дачт, если правильно понял, вот что то бы более глобальное считать. Любопытно по каким алгаритмам у банков роботы работают. Стакан может и поможет у кого быстродействие хорошее. Заметил или показалось, когда кидаешь заявку, в стакане предложение цены там как бы отскскивает в противоположную сторону.
avatar
Великолепно, однозначно плюсую.
Я в своих старых разработках дальше средневзвеса бидов и оферов не пошел. А здесь интересная идея с
«VW DELTA, «VW bid spread» «VW ask spread».
Браво,…
avatar
_sg_, попробуйте для опционов это замацонить, единственное, код писал за час «на коленке», не проверял как будет себя вести например при полном отсутствии либо бидов либо оферов в стакане, на неликвиде или на «планке», надо поэкспериментировать

надо бы скачать, спасибо +
avatar
Вот спасибо! Будем проверять! А обычная маркет-дельта аск-бид по прошедшим сделкам у вас случайно не написада для Квик, чтобы как вертикальные объемы по каждому бару показывала?
avatar
AlexGood, вся торговля по спросу идет, предложение отскакивает, и становится тем же спросом, bid это спрос вроде
avatar
Дык в стакане ММ сидят, и скорее всего не одной заявкой, дробят. Сентимент от этого искажается.
Если бы можно было со 100% уверенностью исключить все биды и аски маркетмейкеров, то анализ стакана имел бы место. И то не по любому инструменту. В USD/RUB, например, такая дичь в стакане при волатильности, что хоть заанлизируйся. За шаг до исполнения ММ свои заявки на пару тысяч лотов снимает или смещает. И плавают там его биды/аски как Г в проруби.
avatar
Kokos Abrikos, 
Дык в стакане ММ сидят, и скорее всего не одной заявкой, дробят. Сентимент от этого искажается.
Если бы можно было со 100% уверенностью исключить все биды и аски маркетмейкеров, то анализ стакана имел бы место. 

то есть идея автора с VW DELTA на практике не очень себя покажет для прогноза будущей цены?
avatar
Все это хорошо, но на практике порой не работает. При большой интенсивности торгов функция getQuoteLevel2(board, ticker)
часто возвращает хрен знает что, а не стакан. если offer более менее терпимо, то bid не можешь получить по несколько минут пока торги не успокоятся.
Очень много вычислений и операций для функции обратного вызова onQuote возьми штук двадцать тикеров и квик повиснет к чертям. И лучше в main записать Subscribe_Level_II_Quotes(CLASS_CODE[i], SEC_CODE[i]), а не то стакан закроешь, и скрипт не работает.
Роджер (веселый)., брал 30 нагрузки нет практически. Ну можно конечно и в мейн засунуть и там просто опрашивать с периодичностью раз в полсекунды — от этого сильно ничего не изменится. Вот при нагрузке часто «встает» лента, а стаканы идут, помедленнее конечно, но это уже вопрос не Квику а к серверу брокера, как он справляется отдавать дату. Но справедливости ради можно кое-что вынести из колбэка. Будет время посмотрю на неделе как на нормальной нагрузке будет вести — с оптимизирую
Евгений Шибаев, Я писал торгового робота и блок рассчитывал мне цену, за которую я могу реализовать денежный объем одномоментно исходя из текущего состояния стакана. Одним словом до какого уровня я смогу продавить стакан своей заявкой. Так наступали моменты что мне nil возвращала функция, особенно по bid.  getQuoteLevel2(board, ticker) не возвращала данные стакана при сильной интенсивности торгов. А еще у меня робот колом становился и повисал из за OnQoute я ее вообще убрал, и начал обращаться к стакану через main когда нужны данные стакана, и все проблемы с повисанием терминала ушли. Но стакан тебе возвращает так нестабильно, что пришлось писать для альтернативный вариант через getParamEx bid. offer и хотя бы видеть крайние точки стакана.
Роджер (веселый).Понятно, ну я попробую на активности в понедельник, если что поправлю, возможность была только после 18 по мск в пятницу погонять, пробоем не было. Кстати вы правы  — нужно как минимум проверку на nil поставить, иначе слетит если что
Евгений Шибаев, Да он как то стакан хитро выгружает, оферы есть, бидов часто нет. Или есть но мало. Но бывает и оферов нет, но редко. И совсем редко он nil выдает по самой таблице getQuoteLevel2(board, ticker). я вот думал, а можно как нибудь в настройках ограничить данные по стакану, к примеру пятью оферами и бидами? Там что на дальних берегах не сильно интересно.
Роджер (веселый).в Квике искал  — нет ограничения настроек глубины стакана. Интересно, можно ли с брокером договориться, им же легче будет… Например, некоторые ленту (тики) только после запроса включают.
Евгений Шибаев, вот и я об этом думаю, иначе не успевают полный стакан передавать  и обрабатывать.
Евгений Шибаев, в ПСБ более слабая система, так они передают урезанные стаканы, в Открытие посильней, так полный. Вот я и думал, может дело в настройках.
Роджер (веселый)., не — дело в серверах брокера
Ого, флудят в стаканы своими лимитками! Спред аж 3000 пунктов в SIZ0, в жизни бы не подумал!

Вероятно, есть смысл делить каждую следующую заявку на 2, чтобы учесть предполагаемую вероятность её исполнения, тогда и дальше 7 позиции можно не заглядывать.
avatar
Добрый день, Евгений. С праздником!
А как там вопрос с новым коннектором?
avatar
asfa, добрый день, спасибо и вас с праздником — до конца след недели закончу, почти все готово.
Евгений Шибаев, ясно, спасибо
avatar
-- ©2020 by Evgeny Shibaev, а пользуются ВСЕ !!!
Очень познавательно Евгений, большое спасибо!
avatar
Спасибо, Respect.
avatar

теги блога Евгений Шибаев

....все тэги



UPDONW