Skip to content

Commit 9ce7e68

Browse files
authored
feat: USD-M futures WebSocket URL category support (#1684)
* feat: Add USD-M futures WebSocket URL category support (public/market/private) Binance is retiring legacy USD-M futures WebSocket URLs on 2026-04-23. This migrates all fstream methods to use the new categorized URL structure. * fix: Limit listenKey query-param format to USD-M futures only, not COIN-M * fix: correct testnet URL in integration tests (fstream not stream)
1 parent 997d499 commit 9ce7e68

File tree

3 files changed

+457
-31
lines changed

3 files changed

+457
-31
lines changed

binance/ws/keepalive_websocket.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,15 @@ async def __aexit__(self, *args, **kwargs):
5454
await super().__aexit__(*args, **kwargs)
5555

5656
def _build_path(self):
57-
self._path = self._listen_key
57+
if self._keepalive_type == "futures":
58+
self._path = f"?listenKey={self._listen_key}"
59+
self._prefix = "ws"
60+
else:
61+
self._path = self._listen_key
5862
time_unit = getattr(self._client, "TIME_UNIT", None)
5963
if time_unit:
60-
self._path = f"{self._listen_key}?timeUnit={time_unit}"
64+
sep = "&" if "?" in self._path else "?"
65+
self._path = f"{self._path}{sep}timeUnit={time_unit}"
6166

6267
async def _before_connect(self):
6368
if self._keepalive_type == "user":

binance/ws/streams.py

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(
7171
self.ws_kwargs = {}
7272

7373
if verbose:
74-
logging.getLogger('binance.ws').setLevel(logging.DEBUG)
74+
logging.getLogger("binance.ws").setLevel(logging.DEBUG)
7575

7676
def _get_stream_url(self, stream_url: Optional[str] = None):
7777
if stream_url:
@@ -134,7 +134,11 @@ def _get_account_socket(
134134
return self._conns[conn_id]
135135

136136
def _get_futures_socket(
137-
self, path: str, futures_type: FuturesType, prefix: str = "stream?streams="
137+
self,
138+
path: str,
139+
futures_type: FuturesType,
140+
prefix: str = "stream?streams=",
141+
category: Optional[str] = None,
138142
):
139143
socket_type: BinanceSocketType = BinanceSocketType.USD_M_FUTURES
140144
if futures_type == FuturesType.USD_M:
@@ -143,6 +147,8 @@ def _get_futures_socket(
143147
stream_url = self.FSTREAM_TESTNET_URL
144148
elif self.demo:
145149
stream_url = self.FSTREAM_DEMO_URL
150+
if category:
151+
stream_url = f"{stream_url}{category}/"
146152
else:
147153
stream_url = self.DSTREAM_URL
148154
if self.testnet:
@@ -174,7 +180,9 @@ def _get_options_socket(self, path: str, base_path: str, prefix: str = "ws/"):
174180
elif base_path == "market":
175181
stream_url = base_url + "market/"
176182
else:
177-
raise ValueError(f"base_path must be 'public' or 'market', got '{base_path}'")
183+
raise ValueError(
184+
f"base_path must be 'public' or 'market', got '{base_path}'"
185+
)
178186

179187
return self._get_socket(
180188
path,
@@ -361,7 +369,9 @@ def kline_futures_socket(
361369
"""
362370

363371
path = f"{symbol.lower()}_{contract_type.value}@continuousKline_{interval}"
364-
return self._get_futures_socket(path, prefix="ws/", futures_type=futures_type)
372+
return self._get_futures_socket(
373+
path, prefix="ws/", futures_type=futures_type, category="market"
374+
)
365375

366376
def miniticker_socket(self, update_time: int = 1000):
367377
"""Start a miniticker websocket for all trades
@@ -487,7 +497,7 @@ def aggtrade_futures_socket(
487497
488498
"""
489499
return self._get_futures_socket(
490-
symbol.lower() + "@aggTrade", futures_type=futures_type
500+
symbol.lower() + "@aggTrade", futures_type=futures_type, category="market"
491501
)
492502

493503
def symbol_miniticker_socket(self, symbol: str):
@@ -642,7 +652,9 @@ def futures_ticker_socket(self):
642652
}
643653
]
644654
"""
645-
return self._get_futures_socket("!ticker@arr", FuturesType.USD_M)
655+
return self._get_futures_socket(
656+
"!ticker@arr", FuturesType.USD_M, category="market"
657+
)
646658

647659
def futures_coin_ticker_socket(self):
648660
"""Start a websocket for all ticker data
@@ -728,7 +740,7 @@ def symbol_mark_price_socket(
728740
"""
729741
stream_name = "@markPrice@1s" if fast else "@markPrice"
730742
return self._get_futures_socket(
731-
symbol.lower() + stream_name, futures_type=futures_type
743+
symbol.lower() + stream_name, futures_type=futures_type, category="market"
732744
)
733745

734746
def all_mark_price_socket(
@@ -755,7 +767,9 @@ def all_mark_price_socket(
755767
]
756768
"""
757769
stream_name = "!markPrice@arr@1s" if fast else "!markPrice@arr"
758-
return self._get_futures_socket(stream_name, futures_type=futures_type)
770+
return self._get_futures_socket(
771+
stream_name, futures_type=futures_type, category="market"
772+
)
759773

760774
def symbol_ticker_futures_socket(
761775
self, symbol: str, futures_type: FuturesType = FuturesType.USD_M
@@ -779,7 +793,7 @@ def symbol_ticker_futures_socket(
779793
]
780794
"""
781795
return self._get_futures_socket(
782-
symbol.lower() + "@bookTicker", futures_type=futures_type
796+
symbol.lower() + "@bookTicker", futures_type=futures_type, category="public"
783797
)
784798

785799
def individual_symbol_ticker_futures_socket(
@@ -800,7 +814,7 @@ def individual_symbol_ticker_futures_socket(
800814
}
801815
"""
802816
return self._get_futures_socket(
803-
symbol.lower() + "@ticker", futures_type=futures_type
817+
symbol.lower() + "@ticker", futures_type=futures_type, category="market"
804818
)
805819

806820
def all_ticker_futures_socket(
@@ -832,7 +846,10 @@ def all_ticker_futures_socket(
832846
]
833847
"""
834848

835-
return self._get_futures_socket(channel, futures_type=futures_type)
849+
category = "public" if "bookTicker" in channel else "market"
850+
return self._get_futures_socket(
851+
channel, futures_type=futures_type, category=category
852+
)
836853

837854
def symbol_book_ticker_socket(self, symbol: str):
838855
"""Start a websocket for the best bid or ask's price or quantity for a specified symbol.
@@ -916,10 +933,15 @@ def options_multiplex_socket(self, streams: List[str]):
916933
"""
917934
stream_name = "/".join(streams)
918935
stream_path = f"streams={stream_name}"
919-
return self._get_options_socket(stream_path, base_path="market", prefix="stream?")
936+
return self._get_options_socket(
937+
stream_path, base_path="market", prefix="stream?"
938+
)
920939

921940
def futures_multiplex_socket(
922-
self, streams: List[str], futures_type: FuturesType = FuturesType.USD_M
941+
self,
942+
streams: List[str],
943+
futures_type: FuturesType = FuturesType.USD_M,
944+
category: str = "market",
923945
):
924946
"""Start a multiplexed socket using a list of socket names.
925947
User stream sockets can not be included.
@@ -932,6 +954,7 @@ def futures_multiplex_socket(
932954
933955
:param streams: list of stream names in lower case
934956
:param futures_type: use USD-M or COIN-M futures default USD-M
957+
:param category: stream category for USD-M futures URL routing ("public", "market", or "private"), default "market"
935958
936959
:returns: connection key string if successful, False otherwise
937960
@@ -940,7 +963,7 @@ def futures_multiplex_socket(
940963
"""
941964
path = f"streams={'/'.join(streams)}"
942965
return self._get_futures_socket(
943-
path, prefix="stream?", futures_type=futures_type
966+
path, prefix="stream?", futures_type=futures_type, category=category
944967
)
945968

946969
def user_socket(self):
@@ -975,6 +998,7 @@ def futures_user_socket(self):
975998
stream_url = self.FSTREAM_TESTNET_URL
976999
elif self.demo:
9771000
stream_url = self.FSTREAM_DEMO_URL
1001+
stream_url += "private/"
9781002
return self._get_account_socket("futures", stream_url=stream_url)
9791003

9801004
def coin_futures_user_socket(self):
@@ -1019,6 +1043,7 @@ def futures_socket(self):
10191043
stream_url = self.FSTREAM_TESTNET_URL
10201044
elif self.demo:
10211045
stream_url = self.FSTREAM_DEMO_URL
1046+
stream_url += "private/"
10221047
return self._get_account_socket("futures", stream_url=stream_url)
10231048

10241049
def coin_futures_socket(self):
@@ -1087,7 +1112,9 @@ def options_ticker_socket(self, symbol: str):
10871112
:param symbol: The option symbol to subscribe to (e.g. "BTC-220930-18000-C")
10881113
:type symbol: str
10891114
"""
1090-
return self._get_options_socket(symbol.lower() + "@optionTicker", base_path="public")
1115+
return self._get_options_socket(
1116+
symbol.lower() + "@optionTicker", base_path="public"
1117+
)
10911118

10921119
def options_ticker_by_expiration_socket(self, symbol: str, expiration_date: str):
10931120
"""Subscribe to a 24-hour ticker info stream by underlying asset and expiration date.
@@ -1105,7 +1132,9 @@ def options_ticker_by_expiration_socket(self, symbol: str, expiration_date: str)
11051132
:param expiration_date: The expiration date (e.g., "251230" for Dec 30, 2025)
11061133
:type expiration_date: str
11071134
"""
1108-
return self._get_options_socket(symbol.lower() + "@optionTicker@" + expiration_date, base_path="public")
1135+
return self._get_options_socket(
1136+
symbol.lower() + "@optionTicker@" + expiration_date, base_path="public"
1137+
)
11091138

11101139
def options_recent_trades_socket(self, symbol: str):
11111140
"""Subscribe to a real-time trade information stream.
@@ -1121,7 +1150,9 @@ def options_recent_trades_socket(self, symbol: str):
11211150
:param symbol: The option symbol or underlying asset (e.g., "BTC-200630-9000-P" or "BTCUSDT")
11221151
:type symbol: str
11231152
"""
1124-
return self._get_options_socket(symbol.lower() + "@optionTrade", base_path="public")
1153+
return self._get_options_socket(
1154+
symbol.lower() + "@optionTrade", base_path="public"
1155+
)
11251156

11261157
def options_kline_socket(
11271158
self, symbol: str, interval=AsyncClient.KLINE_INTERVAL_1MINUTE
@@ -1142,9 +1173,13 @@ def options_kline_socket(
11421173
:param interval: Kline interval, default KLINE_INTERVAL_1MINUTE
11431174
:type interval: str
11441175
"""
1145-
return self._get_options_socket(symbol.lower() + "@kline_" + interval, base_path="market")
1176+
return self._get_options_socket(
1177+
symbol.lower() + "@kline_" + interval, base_path="market"
1178+
)
11461179

1147-
def options_depth_socket(self, symbol: str, depth: str = "10", speed: str = "500ms"):
1180+
def options_depth_socket(
1181+
self, symbol: str, depth: str = "10", speed: str = "500ms"
1182+
):
11481183
"""Subscribe to partial book depth stream for options trading.
11491184
11501185
Top <levels> bids and asks. Valid levels are 5, 10, 20.
@@ -1162,7 +1197,9 @@ def options_depth_socket(self, symbol: str, depth: str = "10", speed: str = "500
11621197
:param speed: Update speed. Valid values: "100ms" or "500ms", default "500ms"
11631198
:type speed: str
11641199
"""
1165-
return self._get_options_socket(symbol.lower() + "@depth" + str(depth) + "@" + speed, base_path="public")
1200+
return self._get_options_socket(
1201+
symbol.lower() + "@depth" + str(depth) + "@" + speed, base_path="public"
1202+
)
11661203

11671204
def options_book_ticker_socket(self, symbol: str):
11681205
"""Subscribe to an options book ticker stream.
@@ -1185,9 +1222,13 @@ def options_book_ticker_socket(self, symbol: str):
11851222
:param symbol: The option symbol (e.g., "BTC-251226-110000-C")
11861223
:type symbol: str
11871224
"""
1188-
return self._get_options_socket(symbol.lower() + "@bookTicker", base_path="public")
1225+
return self._get_options_socket(
1226+
symbol.lower() + "@bookTicker", base_path="public"
1227+
)
11891228

1190-
def futures_depth_socket(self, symbol: str, depth: str = "10", futures_type=FuturesType.USD_M):
1229+
def futures_depth_socket(
1230+
self, symbol: str, depth: str = "10", futures_type=FuturesType.USD_M
1231+
):
11911232
"""Subscribe to a futures depth data stream
11921233
11931234
https://binance-docs.github.io/apidocs/futures/en/#partial-book-depth-streams
@@ -1199,7 +1240,9 @@ def futures_depth_socket(self, symbol: str, depth: str = "10", futures_type=Futu
11991240
:param futures_type: use USD-M or COIN-M futures default USD-M
12001241
"""
12011242
return self._get_futures_socket(
1202-
symbol.lower() + "@depth" + str(depth), futures_type=futures_type
1243+
symbol.lower() + "@depth" + str(depth),
1244+
futures_type=futures_type,
1245+
category="public",
12031246
)
12041247

12051248
def futures_rpi_depth_socket(self, symbol: str, futures_type=FuturesType.USD_M):
@@ -1215,7 +1258,9 @@ def futures_rpi_depth_socket(self, symbol: str, futures_type=FuturesType.USD_M):
12151258
:param futures_type: use USD-M or COIN-M futures default USD-M
12161259
"""
12171260
return self._get_futures_socket(
1218-
symbol.lower() + "@rpiDepth@500ms", futures_type=futures_type
1261+
symbol.lower() + "@rpiDepth@500ms",
1262+
futures_type=futures_type,
1263+
category="public",
12191264
)
12201265

12211266
def options_new_symbol_socket(self):
@@ -1263,7 +1308,10 @@ def options_open_interest_socket(self, symbol: str, expiration_date: str):
12631308
:param expiration_date: The expiration date (e.g., "221125" for Nov 25, 2022)
12641309
:type expiration_date: str
12651310
"""
1266-
return self._get_options_socket(symbol.lower() + "@optionOpenInterest@" + expiration_date, base_path="market")
1311+
return self._get_options_socket(
1312+
symbol.lower() + "@optionOpenInterest@" + expiration_date,
1313+
base_path="market",
1314+
)
12671315

12681316
def options_mark_price_socket(self, symbol: str):
12691317
"""Subscribe to an options mark price stream.
@@ -1287,7 +1335,9 @@ def options_mark_price_socket(self, symbol: str):
12871335
:param symbol: The underlying asset (e.g., "BTCUSDT", "ETHUSDT")
12881336
:type symbol: str
12891337
"""
1290-
return self._get_options_socket(symbol.lower() + "@optionMarkPrice", base_path="market")
1338+
return self._get_options_socket(
1339+
symbol.lower() + "@optionMarkPrice", base_path="market"
1340+
)
12911341

12921342
def options_index_price_socket(self):
12931343
"""Subscribe to an options index price stream.
@@ -1351,8 +1401,7 @@ def __init__(
13511401
async def _before_socket_listener_start(self):
13521402
assert self._client
13531403
self._bsm = BinanceSocketManager(
1354-
client=self._client,
1355-
max_queue_size=self._max_queue_size
1404+
client=self._client, max_queue_size=self._max_queue_size
13561405
)
13571406

13581407
def _start_async_socket(
@@ -1365,7 +1414,9 @@ def _start_async_socket(
13651414
start_time = time.time()
13661415
while not self._bsm:
13671416
if time.time() - start_time > 5:
1368-
raise RuntimeError("Binance Socket Manager failed to initialize after 5 seconds")
1417+
raise RuntimeError(
1418+
"Binance Socket Manager failed to initialize after 5 seconds"
1419+
)
13691420
time.sleep(0.1)
13701421
socket = getattr(self._bsm, socket_name)(**params)
13711422
socket_path: str = path or socket._path # noqa

0 commit comments

Comments
 (0)