Что такое вебсокет?
WebSocket — это специальный протокол обмена данными в режиме реального времени между клиентским приложением, таким как веб-браузер, и сервером, обеспечивающий постоянное и эффективное взаимодействие. По сравнению с обычными HTTP-запросами, которые поддерживают только однонаправленное взаимодействие, вебсокеты обеспечивают двунаправленную связь, что позволяет как клиенту, так и серверу отправлять и принимать данные в любой момент времени. Это превращает его в идеальное решение для приложений, которым необходима мгновенная передача данных в реальном времени, таких как чаты, онлайн-игры, финансовые торги и многое другое. В общем, вебсокеты представляют собой метод установления долгосрочного соединения между клиентом и сервером, обеспечивающего постоянную и непрерывную связь.
Bybit имеет в своем арсенале два варианта вебсокетов для своих пользователей. Приватный вебсокет предназначен для пользователей, которые прошли процедуру аутентификации своих аккаунтов и имеют возможность получать информацию о своем персональном аккаунте, такую как ордера, позиции и балансы счета. Открытый вебсокет предназначен для клиентов, которым интересно получать актуальные рыночные данные, такие как текущие котировки, стакан ордеров и объемы торгов, в режиме реального времени. Открытый вебсокет доступен для всех пользователей и не требует прохождения процедуры аутентификации. Bybit предоставляет веб-сокеты как удобное средство для трейдеров, желающих оперативно отслеживать рыночные изменения и проводить операции быстро и эффективно.
Что такое Callback?
В мире веб-сокетов Callback — это кусочек кода, который выполняется автоматически программой в ответ на определенное событие. Например, когда появляются свежие данные от сервера через веб-сокетное соединение, программа может выполнить эту функцию. Таким образом, обратный вызов позволяет реагировать на различные события и делать нужные действия без необходимости постоянно следить за происходящим. В клиентской программе устанавливается специальная функция, которая при возникновении определенного события вызывается автоматически библиотекой веб-сокетов. Таким образом, клиентская программа готовится к реагированию на указанные события, а библиотека веб-сокетов обеспечивает этот механизм вызова функций по необходимости. Другими словами, клиентская программа предоставляет функцию обратного вызова в качестве параметра библиотеке веб-сокетов при настройке соединения, и библиотека вызывает эту функцию всякий раз, когда получает новые данные или другие события с сервера. Это позволяет клиентской программе реагировать на входящие данные в режиме реального времени без постоянного опроса сервера на наличие обновлений.
Примеры
Давай начнем с импорта необходимых модулей и создания двух экземпляров объектов веб-сокета — одного частного и одного публичного.
from pybit.unified_trading import WebSocket
import json
with open('authcreds.json') as j:
creds = json.load(j)
key = creds['KEY_NAME']['key']
secret = creds['KEY_NAME']['secret']
public = WebSocket(channel_type='linear', testnet=False)
private = WebSocket(channel_type='private',
api_key=key,
api_secret=secret,
testnet=False)
Потоковая передача данных Orderbook
Представь себе, что мы хотим передать информацию из вершины книги заказов, где находятся лучшие предложения и предложения определенного объема для каждой ступени. Иными словами, мы собираемся обменяться данными относительно самых привлекательных предложений и объемов торгов на каждом уровне. Пожалуйста, обратите внимание, что в данной функции для обратного вызова используется handle_orderbook_message
. Эта функция будет активироваться при поступлении нового сообщения от Bybit. Таким образом, при получении данных от Bybit будет вызываться функция handle_orderbook_message
для их обработки. Данная функция извлекает необходимые данные и включает их в специальный список, который мы называем книгой заказов.
Для наглядности мы добавили цикл, который демонстрирует: а) скорость роста данного списка; б) как пользователь может получить самые последние данные из этого списка. Для трейдера может быть важно иметь такую функциональность, чтобы удобно и быстро размещать ордера с помощью одного касания.
orderbook = []
def handle_orderbook_message(message):
'''
custom on callback function for orderbook stream , insert given values
in to dictionary to be appended to oderbook list.
None.
'''
data = message.get('data', None)
global orderbook
current = {}
if data:
try:
current['asset'] = data.get('s')
current['best_bid'] = float(data.get('b')[0][0])
current['bid_volume'] = float(data.get('b')[0][1])
current['best_ask'] = float(data.get('a')[0][0])
current['ask_volume'] = float(data.get('a')[0][1])
orderbook.append(current)
except (TypeError, IndexError, AttributeError):
pass
public.orderbook_stream(depth=1, symbol='BTCUSDT', callback=handle_orderbook_message)
import time
for _ in range(10):
print(len(orderbook))
time.sleep(1)
print(orderbook[-1])
'''
2516
{'asset': 'BTCUSDT', 'best_bid': 27905.4, 'bid_volume': 9.077, 'best_ask': 27905.5, 'ask_volume': 1.9}
2554
{'asset': 'BTCUSDT', 'best_bid': 27905.4, 'bid_volume': 1.81, 'best_ask': 27905.5, 'ask_volume': 5.279}
2588
{'asset': 'BTCUSDT', 'best_bid': 27890.1, 'bid_volume': 6.564, 'best_ask': 27890.2, 'ask_volume': 6.493}
2654
{'asset': 'BTCUSDT', 'best_bid': 27890.1, 'bid_volume': 7.214, 'best_ask': 27890.2, 'ask_volume': 0.592}
2677
{'asset': 'BTCUSDT', 'best_bid': 27890.0, 'bid_volume': 18.451, 'best_ask': 27890.1, 'ask_volume': 3.441}
2715
{'asset': 'BTCUSDT', 'best_bid': 27890.0, 'bid_volume': 17.964, 'best_ask': 27890.1, 'ask_volume': 4.489}
2752
{'asset': 'BTCUSDT', 'best_bid': 27890.0, 'bid_volume': 13.091, 'best_ask': 27890.1, 'ask_volume': 6.142}
2782
{'asset': 'BTCUSDT', 'best_bid': 27890.0, 'bid_volume': 10.077, 'best_ask': 27890.1, 'ask_volume': 9.456}
2805
{'asset': 'BTCUSDT', 'best_bid': 27890.0, 'bid_volume': 11.236, 'best_ask': 27890.1, 'ask_volume': 7.933}
2847
{'asset': 'BTCUSDT', 'best_bid': 27890.0, 'bid_volume': 9.355, 'best_ask': 27890.1, 'ask_volume': 9.385}
'''
В дополнение, трейдер может решить сохранить данные из книги заказов в базе данных по достижении определенного объема информации. Хорошим приемом для этой задачи является применение объекта deque
, как показано далее.
from collections import deque
orderbook = deque(maxlen=(1000))
По описанной выше логике убеждаемся, что в списке будет храниться не более 1000 записей данных.
Трансляция торговых операций в режиме реального времени
Предположим, мы хотим передавать информацию о сделках по контракту BTCUSDT в реальном времени и сохранять данные о крупных сделках. В контексте данной статьи мы создадим функцию обратного вызова, которая будет выводить сообщение в консоль при появлении сделки на сумму свыше 100 000 долларов в книге заказов. Трейдеры могут проявлять интерес к большим объемам ордеров, поскольку это может свидетельствовать о направлении движения китов на рынке.
THRESHOLD = 100_000
def handle_trade_message(message):
'''
message example
{
"topic": "publicTrade.BTCUSDT",
"type": "snapshot",
"ts": 1672304486868,
"data": [
{
"T": 1672304486865,
"s": "BTCUSDT",
"S": "Buy",
"v": "0.001",
"p": "16578.50",
"L": "PlusTick",
"i": "20f43950-d8dd-5b31-9112-a178eb6023af",
"BT": false
}
]
}
custom callback to detect trades over a certain size
'''
try:
data = message.get('data')
for trade in data:
value = float(trade.get('v')) * float((trade.get('p')))
if value > THRESHOLD:
print(f"A trade to {trade.get('S')} {value} was just executed")
else:
pass
except (ValueError, AttributeError):
pass
public.trade_stream(symbol='BTCUSDT', callback=handle_trade_message)
'''
A trade to Sell 129486.16549999999 was just executed
A trade to Sell 300585.60000000003 was just executed
A trade to Sell 113302.03649999999 was just executed
A trade to Sell 178719.72749999998 was just executed
A trade to Sell 118065.336 was just executed
'''
Существует огромное количество вариантов применения потоковой передачи данных. Например, мы можем быть заинтересованы в обнаружении заказов по стратегии TWAP или других систематических методов размещения ордеров.
Дублирование информации о ликвидации Coinglass
Предлагаем взглянуть на панель Coinglass для ознакомления с концепцией ликвидации, особенно для тех, кто не знаком с ней. Возможно, Coinglass применяет подход, аналогичный тому, что мы собираемся разработать, для заполнения данных на упомянутой выше панели управления.
Процесс ликвидации криптовалюты наступает тогда, когда биржа или платформа, где была проведена сделка, вынужденно закрывает позицию трейдера в криптовалюте или ином цифровом активе. Это происходит, когда ценность позиции трейдера опускается ниже определенного уровня, который называется ценой ликвидации. Когда такое случится, биржа или торговая платформа автоматически реализует позицию трейдера с целью обеспечить защиту себя и других трейдеров от дальнейших убытков. Принудительное закрытие позиции криптовалюты возможно из-за резких колебаний на рынке, высокой волатильности или других факторов, способных вызвать значительные изменения цен в течение короткого временного интервала.
Почему алгоритмическому трейдеру это может показаться интересным? Поскольку эти позиции закрываются принудительно, они могут быть исполнены по более низкой или высокой текущей рыночной цене. Из-за этого у нас может возникнуть желание вмешаться и разместить ордер в направлении, противоположном данной сделке, так как цена, вероятно, будет более выгодной по сравнению с обычными условиями. Здесь мы разрабатываем скрипт, который вычисляет часовую ликвидацию длинных и коротких позиций для пары BTCUSDT. Кроме того, каждые 30 секунд мы выводим текущий результат.
long_liqs = []
short_liqs = []
def handle_liquidation_message(message):
'''
{
"data": {
"price": "0.03803",
"side": "Buy",
"size": "1637",
"symbol": "BTCUSDT",
"updatedTime": 1673251091822
},
"topic": "liquidation.GALAUSDT",
"ts": 1673251091822,
"type": "snapshot"
}
'''
global long_liqs, short_liqs
try:
data = message.get('data')
if data.get('side') == 'Buy':
usd_val = float(data.get('size')) * float(data.get('price'))
print(f'A long degen just got liquidated for {usd_val}')
long_liqs.append(usd_val)
elif data.get('side') == 'Sell':
usd_val = float(data.get('size')) * float(data.get('price'))
print(f'A short degen just got liquidated for {usd_val}')
short_liqs.append(usd_val)
else:
pass
except (TypeError, ValueError, IndexError):
pass
public.liquidation_stream(symbol='BTCUSDT', callback=handle_liquidation_message)
import datetime as dt
while True:
timenow = dt.datetime.now()
if timenow.minute==0 and timenow.second==0:
print(f'A total of {sum(long_liqs)} longs have been rekt in last hour')
print(f'A total of {sum(short_liqs)} shorts have been rekt in last hour')
#reset the lists for the next hour of slaughter
longs_liqs = []
short_liqs = []
time.sleep(1)
elif timenow.second %30 == 0:
print(f'A total of {sum(long_liqs)} longs have been rekt so far in this hour')
print(f'A total of {sum(short_liqs)} shorts have been rekt so far in this hour')
time.sleep(1)
'''
A total of 0 longs have been rekt so far in this hour
A total of 44839.4481 shorts have been rekt so far in this hour
A total of 0 longs have been rekt so far in this hour
A total of 44839.4481 shorts have been rekt so far in this hour
A short degen just got liquidated for 1261.3905
A short degen just got liquidated for 51481.256400000006
A short degen just got liquidated for 280.413
A short degen just got liquidated for 617.0713999999999
A short degen just got liquidated for 13999.0458
A short degen just got liquidated for 10075.6222
A total of 0 longs have been rekt so far in this hour
A total of 122554.24740000001 shorts have been rekt so far in this hour
A short degen just got liquidated for 1150.9725
A short degen just got liquidated for 1038.8711999999998
A short degen just got liquidated for 2640.7513999999996
A short degen just got liquidated for 140.496
A short degen just got liquidated for 618.5718
A total of 0 longs have been rekt so far in this hour
A total of 128143.9103 shorts have been rekt so far in this hour
A short degen just got liquidated for 365.6185
'''
Потоковая позиция
Важно следить за текущей областью актива, в которой у нас открыта позиция. Нас это интересует с точки зрения контроля рисков и сопоставления ордеров. В следующем действии мы настраиваем поток позиций для мониторинга текущего количества DOGE в нашем распоряжении.
doge_position = 0
def handle_position_message(message):
'''
custom error message to retrieve position of given asset.
'''
global doge_position
try:
data = message.get('data', [])
for pos in data:
if pos['symbol'] == 'DOGEUSDT':
if pos['side'] == 'Sell':
doge_position = - float(pos['size'])
elif pos['side'] == 'Buy':
doge_position = float(pos['size'])
else:
doge_position = 0
else:
pass
except (IndexError, AttributeError, TypeError):
pass
private.position_stream(callback=handle_position_message)
while True:
if doge_position > 0:
print(f'Excellent you just bought {doge_position} DOGE!!!!!!!')
break
elif doge_position < 0:
print(f'Not breaking until you buy a DOGE bagholders must resort to extraordinary measures while underwater')
else:
print('Waiting for you to buy a DOGE ')
Потоковые ордера
Предположим, что мы разрабатываем систему управления ордерами для прослеживания исполнения по заданному алгоритму, и нам также требуется наблюдать за состоянием ордеров. Далее описаны разные стадии, в которые может находиться ордер.
Created
ордер принят системой, но еще не прошел через механизм сопоставленияNew
ордер был размещен успешноRejected
PartiallyFilled
- PartiallyFilledCanceled только spot имеет данный статус ордера
Filled
- Cancelled В деривативах заказы с этим статусом могут иметь исполненное количество.
Untriggered
Triggered
Deactivated
- Active Ордер был инициирован и новый активный ордер успешно размещен. Это финальное состояние успешного условного ордера.
Поскольку разработка системы управления ордерами довольно сложный процесс, мы просто выводим результаты заказов из API ниже
def handle_orders_message(message):
print(message)
private.order_stream(callback=handle_orders_message)
'''
{'topic': 'order', 'id': '2a97094f9f6323e97ca96e862dfb9d96:29938d51d74bc01b:0:01',
'creationTime': 1682980546234, 'data':
[{'avgPrice': '0.07860', 'blockTradeId': '',
'cancelType': 'UNKNOWN', 'category': 'linear',
'closeOnTrigger': False, 'createdTime': '1682980546232',
'cumExecFee': '0.0004716', 'cumExecQty': '10', 'cumExecValue': '0.786',
'leavesQty': '0', 'leavesValue': '0',
'orderId': '89e64947-208c-4fac-a730-b0e2c2f1fa72',
'orderIv': '', 'isLeverage': '',
'lastPriceOnCreated': '0.07859',
'orderStatus': 'Filled', 'orderLinkId': '',
'orderType': 'Market', 'positionIdx': 0,
'price': '0.08251', 'qty': '10', 'reduceOnly': False,
'rejectReason': 'EC_NoError', 'side': 'Buy',
'slTriggerBy': 'UNKNOWN', 'stopLoss': '0.00000',
'stopOrderType': 'UNKNOWN', 'symbol': 'DOGEUSDT',
'takeProfit': '0.00000', 'timeInForce': 'IOC',
'tpTriggerBy': 'UNKNOWN', 'triggerBy': 'UNKNOWN',
'triggerDirection': 0, 'triggerPrice': '0.00000',
'updatedTime': '1682980546233', 'placeType': '',
'smpType': 'None', 'smpGroup': 0, 'smpOrderId': ''}]}
'''