Skip to content

Commit 028d6aa

Browse files
feat: add options websocket market streams (sammchardy#1563)
* add timeout to jobs * feat: add ws support for options * add missing functions * lint * lint tests * update timeout gh action --------- Co-authored-by: carlosmiei <43336371+carlosmiei@users.noreply.github.com>
1 parent d12d6a9 commit 028d6aa

File tree

3 files changed

+247
-61
lines changed

3 files changed

+247
-61
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
build:
3535
needs: lint
3636
runs-on: ubuntu-22.04
37-
timeout-minutes: 20
37+
timeout-minutes: 60
3838
env:
3939
PROXY: "http://51.83.140.52:16301"
4040
TEST_TESTNET: "true"

binance/ws/streams.py

Lines changed: 146 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ class BinanceSocketManager:
3030
FSTREAM_TESTNET_URL = "wss://stream.binancefuture.com/"
3131
DSTREAM_URL = "wss://dstream.binance.{}/"
3232
DSTREAM_TESTNET_URL = "wss://dstream.binancefuture.com/"
33-
VSTREAM_URL = "wss://vstream.binance.{}/"
34-
VSTREAM_TESTNET_URL = "wss://testnetws.binanceops.{}/"
33+
OPTIONS_URL = "wss://nbstream.binance.{}/eoptions/"
3534

3635
WEBSOCKET_DEPTH_5 = "5"
3736
WEBSOCKET_DEPTH_10 = "10"
@@ -47,8 +46,7 @@ def __init__(self, client: AsyncClient, user_timeout=KEEPALIVE_TIMEOUT):
4746
self.STREAM_URL = self.STREAM_URL.format(client.tld)
4847
self.FSTREAM_URL = self.FSTREAM_URL.format(client.tld)
4948
self.DSTREAM_URL = self.DSTREAM_URL.format(client.tld)
50-
self.VSTREAM_URL = self.VSTREAM_URL.format(client.tld)
51-
self.VSTREAM_TESTNET_URL = self.VSTREAM_TESTNET_URL.format(client.tld)
49+
self.OPTIONS_URL = self.OPTIONS_URL.format(client.tld)
5250

5351
self._conns = {}
5452
self._loop = get_loop()
@@ -129,14 +127,12 @@ def _get_futures_socket(
129127
return self._get_socket(path, stream_url, prefix, socket_type=socket_type)
130128

131129
def _get_options_socket(self, path: str, prefix: str = "ws/"):
132-
stream_url = self.VSTREAM_URL
133-
if self.testnet:
134-
stream_url = self.VSTREAM_TESTNET_URL
130+
stream_url = self.OPTIONS_URL
135131
return self._get_socket(
136132
path,
137133
stream_url,
138134
prefix,
139-
is_binary=True,
135+
is_binary=False,
140136
socket_type=BinanceSocketType.OPTIONS,
141137
)
142138

@@ -659,23 +655,6 @@ def index_price_socket(self, symbol: str, fast: bool = True):
659655
symbol.lower() + stream_name, futures_type=FuturesType.COIN_M
660656
)
661657

662-
def futures_depth_socket(
663-
self, symbol: str, depth: str = "10", futures_type=FuturesType.USD_M
664-
):
665-
"""Subscribe to a futures depth data stream
666-
667-
https://binance-docs.github.io/apidocs/futures/en/#partial-book-depth-streams
668-
669-
:param symbol: required
670-
:type symbol: str
671-
:param depth: optional Number of depth entries to return, default 10.
672-
:type depth: str
673-
:param futures_type: use USD-M or COIN-M futures default USD-M
674-
"""
675-
return self._get_futures_socket(
676-
symbol.lower() + "@depth" + str(depth), futures_type=futures_type
677-
)
678-
679658
def symbol_mark_price_socket(
680659
self,
681660
symbol: str,
@@ -874,23 +853,11 @@ def multiplex_socket(self, streams: List[str]):
874853

875854
def options_multiplex_socket(self, streams: List[str]):
876855
"""Start a multiplexed socket using a list of socket names.
877-
User stream sockets can not be included.
878-
879-
Symbols in socket name must be lowercase i.e bnbbtc@aggTrade, neobtc@ticker
880-
881-
Combined stream events are wrapped as follows: {"stream":"<streamName>","data":<rawPayload>}
882-
883-
https://binance-docs.github.io/apidocs/voptions/en/#account-and-trading-interface
884-
885-
:param streams: list of stream names in lower case
886-
:type streams: list
887-
888-
:returns: connection key string if successful, False otherwise
889-
890-
Message Format - see Binance API docs for all types
856+
857+
https://developers.binance.com/docs/derivatives/option/websocket-market-streams
891858
892859
"""
893-
stream_name = "/".join([s.lower() for s in streams])
860+
stream_name = "/".join([s for s in streams])
894861
stream_path = f"streams={stream_name}"
895862
return self._get_options_socket(stream_path, prefix="stream?")
896863

@@ -1036,60 +1003,179 @@ def isolated_margin_socket(self, symbol: str):
10361003
return self._get_account_socket(symbol, stream_url=stream_url)
10371004

10381005
def options_ticker_socket(self, symbol: str):
1039-
"""Subscribe to a 24 hour ticker info stream
1006+
"""Subscribe to a 24-hour ticker info stream for options trading.
10401007
1041-
https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-24-hour-ticker
1008+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/24-hour-TICKER
10421009
1043-
:param symbol: required
1010+
Stream provides real-time 24hr ticker information for all symbols. Only symbols whose ticker info
1011+
changed will be sent. Updates every 1000ms.
1012+
1013+
:param symbol: The option symbol to subscribe to (e.g. "BTC-220930-18000-C")
10441014
:type symbol: str
10451015
"""
1046-
return self._get_options_socket(symbol.lower() + "@ticker")
1016+
return self._get_options_socket(symbol.upper() + "@ticker")
10471017

10481018
def options_ticker_by_expiration_socket(self, symbol: str, expiration_date: str):
1049-
"""Subscribe to a 24 hour ticker info stream
1050-
https://binance-docs.github.io/apidocs/voptions/en/#24-hour-ticker-by-underlying-asset-and-expiration-data
1051-
:param symbol: required
1019+
"""Subscribe to a 24-hour ticker info stream by underlying asset and expiration date.
1020+
1021+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/24-hour-TICKER-by-underlying-asset-and-expiration-data
1022+
1023+
Stream provides real-time 24hr ticker information grouped by underlying asset and expiration date.
1024+
Updates every 1000ms.
1025+
1026+
:param symbol: The underlying asset (e.g., "ETH")
10521027
:type symbol: str
1053-
:param expiration_date : required
1028+
:param expiration_date: The expiration date (e.g., "220930" for Sept 30, 2022)
10541029
:type expiration_date: str
10551030
"""
1056-
return self._get_options_socket(symbol.lower() + "@ticker@" + expiration_date)
1031+
return self._get_options_socket(symbol.upper() + "@ticker@" + expiration_date)
10571032

10581033
def options_recent_trades_socket(self, symbol: str):
1059-
"""Subscribe to a latest completed trades stream
1034+
"""Subscribe to a real-time trade information stream.
1035+
1036+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Trade-Streams
10601037
1061-
https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-latest-completed-trades
1038+
Stream pushes raw trade information for a specific symbol or underlying asset.
1039+
Updates every 50ms.
10621040
1063-
:param symbol: required
1041+
:param symbol: The option symbol or underlying asset (e.g., "BTC-200630-9000-P" or "BTC")
10641042
:type symbol: str
10651043
"""
1066-
return self._get_options_socket(symbol.lower() + "@trade")
1044+
return self._get_options_socket(symbol.upper() + "@trade")
10671045

10681046
def options_kline_socket(
10691047
self, symbol: str, interval=AsyncClient.KLINE_INTERVAL_1MINUTE
10701048
):
1071-
"""Subscribe to a candlestick data stream
1049+
"""Subscribe to a Kline/Candlestick data stream.
1050+
1051+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Kline-Candlestick-Streams
10721052
1073-
https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-candle
1053+
Stream pushes updates to the current klines/candlestick every 1000ms (if existing).
10741054
1075-
:param symbol: required
1055+
Available intervals:
1056+
- Minutes: "1m", "3m", "5m", "15m", "30m"
1057+
- Hours: "1h", "2h", "4h", "6h", "12h"
1058+
- Days: "1d", "3d"
1059+
- Weeks: "1w"
1060+
1061+
:param symbol: The option symbol (e.g., "BTC-200630-9000-P")
10761062
:type symbol: str
10771063
:param interval: Kline interval, default KLINE_INTERVAL_1MINUTE
10781064
:type interval: str
10791065
"""
1080-
return self._get_options_socket(symbol.lower() + "@kline_" + interval)
1066+
return self._get_options_socket(symbol.upper() + "@kline_" + interval)
10811067

10821068
def options_depth_socket(self, symbol: str, depth: str = "10"):
1083-
"""Subscribe to a depth data stream
1069+
"""Subscribe to partial book depth stream for options trading.
1070+
1071+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Partial-Book-Depth-Streams
10841072
1085-
https://binance-docs.github.io/apidocs/voptions/en/#market-streams-payload-depth
1073+
Stream provides top N bids and asks from the order book.
1074+
Default update speed is 500ms if not specified in the stream name.
1075+
1076+
:param symbol: The option symbol (e.g., "BTC-200630-9000-P")
1077+
:type symbol: str
1078+
:param depth: Number of price levels. Valid values: "10", "20", "50", "100"
1079+
:type depth: str
1080+
"""
1081+
return self._get_options_socket(symbol.upper() + "@depth" + str(depth))
1082+
1083+
def futures_depth_socket(self, symbol: str, depth: str = "10", futures_type=FuturesType.USD_M):
1084+
"""Subscribe to a futures depth data stream
1085+
1086+
https://binance-docs.github.io/apidocs/futures/en/#partial-book-depth-streams
10861087
10871088
:param symbol: required
10881089
:type symbol: str
10891090
:param depth: optional Number of depth entries to return, default 10.
10901091
:type depth: str
1092+
:param futures_type: use USD-M or COIN-M futures default USD-M
1093+
"""
1094+
return self._get_futures_socket(
1095+
symbol.lower() + "@depth" + str(depth), futures_type=futures_type
1096+
)
1097+
1098+
def options_new_symbol_socket(self):
1099+
"""Subscribe to a new symbol listing information stream.
1100+
1101+
Stream provides real-time notifications when new option symbols are listed.
1102+
Updates every 50ms.
1103+
1104+
Stream name: option_pair
1105+
1106+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/New-Symbol-Info
1107+
1108+
Response fields include:
1109+
- Event type and timestamps
1110+
- Underlying index (e.g., 'BTCUSDT')
1111+
- Quotation asset (e.g., 'USDT')
1112+
- Trading pair name (e.g., 'BTC-221116-21000-C')
1113+
- Conversion ratio and minimum trade volume
1114+
- Option type (CALL/PUT)
1115+
- Strike price and expiration time
1116+
"""
1117+
return self._get_options_socket("option_pair")
1118+
1119+
def options_open_interest_socket(self, symbol: str, expiration_date: str):
1120+
"""Subscribe to an options open interest stream.
1121+
1122+
Stream provides open interest information for specific underlying asset on specific expiration date.
1123+
Updates every 60 seconds.
1124+
1125+
Stream name format: <underlyingAsset>@openInterest@<expirationDate>
1126+
1127+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Open-Interest
1128+
1129+
Response fields include:
1130+
- Event type and timestamps
1131+
- Option symbol (e.g., 'ETH-221125-2700-C')
1132+
- Open interest in contracts
1133+
- Open interest in USDT
1134+
1135+
:param symbol: The underlying asset (e.g., "ETH")
1136+
:type symbol: str
1137+
:param expiration_date: The expiration date (e.g., "221125" for Nov 25, 2022)
1138+
:type expiration_date: str
1139+
"""
1140+
return self._get_options_socket(symbol.upper() + "@openInterest@" + expiration_date)
1141+
1142+
def options_mark_price_socket(self, symbol: str):
1143+
"""Subscribe to an options mark price stream.
1144+
1145+
Stream provides mark price information for all option symbols on specific underlying asset.
1146+
Updates every 1000ms.
1147+
1148+
Stream name format: <underlyingAsset>@markPrice
1149+
1150+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Mark-Price
1151+
1152+
Response fields include:
1153+
- Event type and timestamps
1154+
- Option symbol (e.g., 'ETH-220930-1500-C')
1155+
- Option mark price
1156+
1157+
:param symbol: The underlying asset (e.g., "ETH")
1158+
:type symbol: str
1159+
"""
1160+
return self._get_options_socket(symbol.upper() + "@markPrice")
1161+
1162+
def options_index_price_socket(self, symbol: str):
1163+
"""Subscribe to an options index price stream.
1164+
1165+
API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Index-Price-Streams
1166+
1167+
Stream provides index price information for underlying assets (e.g., ETHUSDT).
1168+
Updates every 1000ms.
1169+
1170+
Response fields include:
1171+
- Event type and timestamps
1172+
- Underlying symbol (e.g., 'ETHUSDT')
1173+
- Index price
1174+
1175+
:param symbol: The underlying symbol (e.g., "ETHUSDT")
1176+
:type symbol: str
10911177
"""
1092-
return self._get_options_socket(symbol.lower() + "@depth" + str(depth))
1178+
return self._get_options_socket(symbol.upper() + "@index")
10931179

10941180
async def _stop_socket(self, conn_key):
10951181
"""Stop a websocket given the connection key

tests/test_streams_options.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import sys
2+
import pytest
3+
from binance import BinanceSocketManager
4+
5+
pytestmark = [
6+
pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+"),
7+
pytest.mark.asyncio
8+
]
9+
10+
# Test constants
11+
OPTION_SYMBOL = "BTC-250328-40000-P"
12+
UNDERLYING_SYMBOL = "BTC"
13+
EXPIRATION_DATE = "250328"
14+
INTERVAL = "1m"
15+
DEPTH = "20"
16+
17+
async def test_options_ticker(clientAsync):
18+
"""Test options ticker socket"""
19+
bm = BinanceSocketManager(clientAsync)
20+
socket = bm.options_ticker_socket(OPTION_SYMBOL)
21+
async with socket as ts:
22+
msg = await ts.recv()
23+
assert msg['e'] == '24hrTicker'
24+
await clientAsync.close_connection()
25+
26+
async def test_options_ticker_by_expiration(clientAsync):
27+
"""Test options ticker by expiration socket"""
28+
bm = BinanceSocketManager(clientAsync)
29+
socket = bm.options_ticker_by_expiration_socket(UNDERLYING_SYMBOL, EXPIRATION_DATE)
30+
async with socket as ts:
31+
msg = await ts.recv()
32+
assert len(msg) > 0
33+
await clientAsync.close_connection()
34+
35+
async def test_options_recent_trades(clientAsync):
36+
"""Test options recent trades socket"""
37+
bm = BinanceSocketManager(clientAsync)
38+
socket = bm.options_recent_trades_socket(UNDERLYING_SYMBOL)
39+
async with socket as ts:
40+
msg = await ts.recv()
41+
assert msg['e'] == 'trade'
42+
await clientAsync.close_connection()
43+
44+
async def test_options_kline(clientAsync):
45+
"""Test options kline socket"""
46+
bm = BinanceSocketManager(clientAsync)
47+
socket = bm.options_kline_socket(OPTION_SYMBOL, INTERVAL)
48+
async with socket as ts:
49+
msg = await ts.recv()
50+
assert msg['e'] == 'kline'
51+
await clientAsync.close_connection()
52+
53+
async def test_options_depth(clientAsync):
54+
"""Test options depth socket"""
55+
bm = BinanceSocketManager(clientAsync)
56+
socket = bm.options_depth_socket(OPTION_SYMBOL, DEPTH)
57+
async with socket as ts:
58+
msg = await ts.recv()
59+
assert msg['e'] == 'depth'
60+
await clientAsync.close_connection()
61+
62+
async def test_options_multiplex(clientAsync):
63+
"""Test options multiplex socket"""
64+
bm = BinanceSocketManager(clientAsync)
65+
streams = [
66+
f"{OPTION_SYMBOL}@ticker",
67+
f"{OPTION_SYMBOL}@trade",
68+
]
69+
socket = bm.options_multiplex_socket(streams)
70+
async with socket as ts:
71+
msg = await ts.recv()
72+
assert 'stream' in msg
73+
await clientAsync.close_connection()
74+
75+
async def test_options_open_interest(clientAsync):
76+
"""Test options open interest socket"""
77+
bm = BinanceSocketManager(clientAsync)
78+
socket = bm.options_open_interest_socket(UNDERLYING_SYMBOL, EXPIRATION_DATE)
79+
async with socket as ts:
80+
msg = await ts.recv()
81+
assert len(msg) > 0
82+
await clientAsync.close_connection()
83+
84+
async def test_options_mark_price(clientAsync):
85+
"""Test options mark price socket"""
86+
bm = BinanceSocketManager(clientAsync)
87+
socket = bm.options_mark_price_socket(UNDERLYING_SYMBOL)
88+
async with socket as ts:
89+
msg = await ts.recv()
90+
assert len(msg) > 0
91+
await clientAsync.close_connection()
92+
93+
async def test_options_index_price(clientAsync):
94+
"""Test options index price socket"""
95+
bm = BinanceSocketManager(clientAsync)
96+
socket = bm.options_index_price_socket('ETHUSDT')
97+
async with socket as ts:
98+
msg = await ts.recv()
99+
assert msg['e'] == 'index'
100+
await clientAsync.close_connection()

0 commit comments

Comments
 (0)