diff --git a/binance/ws/streams.py b/binance/ws/streams.py index 0e1b9f54..bb45a7cd 100755 --- a/binance/ws/streams.py +++ b/binance/ws/streams.py @@ -29,12 +29,11 @@ class BinanceSocketManager: STREAM_TESTNET_URL = "wss://stream.testnet.binance.vision/" STREAM_DEMO_URL = "wss://demo-stream.binance.com/" FSTREAM_URL = "wss://fstream.binance.{}/" - FSTREAM_TESTNET_URL = "wss://stream.binancefuture.com/" + FSTREAM_TESTNET_URL = "wss://fstream.binancefuture.com/" FSTREAM_DEMO_URL = "wss://fstream.binancefuture.com/" DSTREAM_URL = "wss://dstream.binance.{}/" DSTREAM_TESTNET_URL = "wss://dstream.binancefuture.com/" DSTREAM_DEMO_URL = "wss://dstream.binancefuture.com/" - OPTIONS_URL = "wss://nbstream.binance.{}/eoptions/" WEBSOCKET_DEPTH_5 = "5" WEBSOCKET_DEPTH_10 = "10" @@ -60,7 +59,6 @@ def __init__( self.STREAM_URL = self.STREAM_URL.format(client.tld) self.FSTREAM_URL = self.FSTREAM_URL.format(client.tld) self.DSTREAM_URL = self.DSTREAM_URL.format(client.tld) - self.OPTIONS_URL = self.OPTIONS_URL.format(client.tld) self._conns = {} self._loop = get_loop() @@ -153,8 +151,31 @@ def _get_futures_socket( stream_url = self.DSTREAM_DEMO_URL return self._get_socket(path, stream_url, prefix, socket_type=socket_type) - def _get_options_socket(self, path: str, prefix: str = "ws/"): - stream_url = self.OPTIONS_URL + def _get_options_socket(self, path: str, base_path: str, prefix: str = "ws/"): + """Get an options websocket connection. + + Options use the same base URLs as futures (FSTREAM), with either /public/ or /market/ paths. + + :param path: The stream path/name + :param base_path: Either "public" or "market" to specify which base path to use + :param prefix: The prefix for the connection, either "ws/" or "stream?" + """ + # Select base URL based on testnet/demo mode (reuse FSTREAM URLs) + if self.testnet: + base_url = self.FSTREAM_TESTNET_URL + elif self.demo: + base_url = self.FSTREAM_DEMO_URL + else: + base_url = self.FSTREAM_URL + + # Append the appropriate path (public or market) + if base_path == "public": + stream_url = base_url + "public/" + elif base_path == "market": + stream_url = base_url + "market/" + else: + raise ValueError(f"base_path must be 'public' or 'market', got '{base_path}'") + return self._get_socket( path, stream_url, @@ -881,12 +902,21 @@ def multiplex_socket(self, streams: List[str]): def options_multiplex_socket(self, streams: List[str]): """Start a multiplexed socket using a list of socket names. - https://developers.binance.com/docs/derivatives/option/websocket-market-streams + Combined streams are accessed at /stream?streams=// + All symbols in stream names must be lowercase, but stream type names (like @optionTicker) + preserve their original case. + URL PATH: /market + + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams + + :param streams: List of stream names (e.g., ["btcusdt@optionTicker", "ethusdt@optionMarkPrice"]) + Note: Symbols should be lowercase, but stream types keep original case + :type streams: List[str] """ - stream_name = "/".join([s for s in streams]) + stream_name = "/".join(streams) stream_path = f"streams={stream_name}" - return self._get_options_socket(stream_path, prefix="stream?") + return self._get_options_socket(stream_path, base_path="market", prefix="stream?") def futures_multiplex_socket( self, streams: List[str], futures_type: FuturesType = FuturesType.USD_M @@ -1046,80 +1076,116 @@ def isolated_margin_socket(self, symbol: str): def options_ticker_socket(self, symbol: str): """Subscribe to a 24-hour ticker info stream for options trading. - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/24-hour-TICKER + 24hr ticker info for all symbols. Only symbols whose ticker info changed will be sent. + Updates every 1000ms. - Stream provides real-time 24hr ticker information for all symbols. Only symbols whose ticker info - changed will be sent. Updates every 1000ms. + URL PATH: /public + Stream Name: @optionTicker + + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/24-hour-TICKER :param symbol: The option symbol to subscribe to (e.g. "BTC-220930-18000-C") :type symbol: str """ - return self._get_options_socket(symbol.upper() + "@ticker") + return self._get_options_socket(symbol.lower() + "@optionTicker", base_path="public") def options_ticker_by_expiration_socket(self, symbol: str, expiration_date: str): """Subscribe to a 24-hour ticker info stream by underlying asset and expiration date. - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/24-hour-TICKER-by-underlying-asset-and-expiration-data - - Stream provides real-time 24hr ticker information grouped by underlying asset and expiration date. + 24hr ticker info for underlying asset and expiration date. Updates every 1000ms. - :param symbol: The underlying asset (e.g., "ETH") + URL PATH: /public + Stream Name: @optionTicker@ + + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/24-hour-TICKER + + :param symbol: The underlying asset (e.g., "BTCUSDT") :type symbol: str - :param expiration_date: The expiration date (e.g., "220930" for Sept 30, 2022) + :param expiration_date: The expiration date (e.g., "251230" for Dec 30, 2025) :type expiration_date: str """ - return self._get_options_socket(symbol.upper() + "@ticker@" + expiration_date) + return self._get_options_socket(symbol.lower() + "@optionTicker@" + expiration_date, base_path="public") def options_recent_trades_socket(self, symbol: str): """Subscribe to a real-time trade information stream. - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Trade-Streams - - Stream pushes raw trade information for a specific symbol or underlying asset. + The Trade Streams push raw trade information for specific symbol or underlying asset. Updates every 50ms. - :param symbol: The option symbol or underlying asset (e.g., "BTC-200630-9000-P" or "BTC") + URL PATH: /public + Stream Name: @optionTrade or @optionTrade + + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/Trade-Streams + + :param symbol: The option symbol or underlying asset (e.g., "BTC-200630-9000-P" or "BTCUSDT") :type symbol: str """ - return self._get_options_socket(symbol.upper() + "@trade") + return self._get_options_socket(symbol.lower() + "@optionTrade", base_path="public") def options_kline_socket( self, symbol: str, interval=AsyncClient.KLINE_INTERVAL_1MINUTE ): """Subscribe to a Kline/Candlestick data stream. - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Kline-Candlestick-Streams + The Kline/Candlestick Stream push updates to the current klines/candlestick every 1000ms (if existing). + + URL PATH: /market + Stream Name: @kline_ - Stream pushes updates to the current klines/candlestick every 1000ms (if existing). + Available intervals: "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "3d", "1w" - Available intervals: - - Minutes: "1m", "3m", "5m", "15m", "30m" - - Hours: "1h", "2h", "4h", "6h", "12h" - - Days: "1d", "3d" - - Weeks: "1w" + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/Kline-Candlestick-Streams :param symbol: The option symbol (e.g., "BTC-200630-9000-P") :type symbol: str :param interval: Kline interval, default KLINE_INTERVAL_1MINUTE :type interval: str """ - return self._get_options_socket(symbol.upper() + "@kline_" + interval) + return self._get_options_socket(symbol.lower() + "@kline_" + interval, base_path="market") - def options_depth_socket(self, symbol: str, depth: str = "10"): + def options_depth_socket(self, symbol: str, depth: str = "10", speed: str = "500ms"): """Subscribe to partial book depth stream for options trading. - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Partial-Book-Depth-Streams + Top bids and asks. Valid levels are 5, 10, 20. + Updates every 100ms or 500ms. + + URL PATH: /public + Stream Name: @depth@100ms or @depth@500ms - Stream provides top N bids and asks from the order book. - Default update speed is 500ms if not specified in the stream name. + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/Partial-Book-Depth-Streams :param symbol: The option symbol (e.g., "BTC-200630-9000-P") :type symbol: str - :param depth: Number of price levels. Valid values: "10", "20", "50", "100" + :param depth: Number of price levels. Valid values: "5", "10", "20" :type depth: str + :param speed: Update speed. Valid values: "100ms" or "500ms", default "500ms" + :type speed: str + """ + return self._get_options_socket(symbol.lower() + "@depth" + str(depth) + "@" + speed, base_path="public") + + def options_book_ticker_socket(self, symbol: str): + """Subscribe to an options book ticker stream. + + Pushes any update to the best bid or ask's price or quantity in real-time for a specified symbol. + + URL PATH: /public + Stream Name: @bookTicker + + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/Bookticker + + Response fields include: + - Event type: 'bookTicker' + - Order book updateId + - Symbol + - Best bid price and quantity + - Best ask price and quantity + - Transaction time and event time + + :param symbol: The option symbol (e.g., "BTC-251226-110000-C") + :type symbol: str """ - return self._get_options_socket(symbol.upper() + "@depth" + str(depth)) + return self._get_options_socket(symbol.lower() + "@bookTicker", base_path="public") def futures_depth_socket(self, symbol: str, depth: str = "10", futures_type=FuturesType.USD_M): """Subscribe to a futures depth data stream @@ -1158,9 +1224,10 @@ def options_new_symbol_socket(self): Stream provides real-time notifications when new option symbols are listed. Updates every 50ms. - Stream name: option_pair + URL PATH: /market + Stream Name: !optionSymbol - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/New-Symbol-Info + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/New-Symbol-Info Response fields include: - Event type and timestamps @@ -1171,17 +1238,19 @@ def options_new_symbol_socket(self): - Option type (CALL/PUT) - Strike price and expiration time """ - return self._get_options_socket("option_pair") + return self._get_options_socket("!optionSymbol", base_path="market") def options_open_interest_socket(self, symbol: str, expiration_date: str): """Subscribe to an options open interest stream. - Stream provides open interest information for specific underlying asset on specific expiration date. + Option open interest for specific underlying asset on specific expiration date. Updates every 60 seconds. - Stream name format: @openInterest@ + URL PATH: /market + Stream Name: underlying@optionOpenInterest@ + Example: ethusdt@optionOpenInterest@221125 - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Open-Interest + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/Open-Interest Response fields include: - Event type and timestamps @@ -1189,50 +1258,55 @@ def options_open_interest_socket(self, symbol: str, expiration_date: str): - Open interest in contracts - Open interest in USDT - :param symbol: The underlying asset (e.g., "ETH") + :param symbol: The underlying asset (e.g., "ETHUSDT") :type symbol: str :param expiration_date: The expiration date (e.g., "221125" for Nov 25, 2022) :type expiration_date: str """ - return self._get_options_socket(symbol.upper() + "@openInterest@" + expiration_date) + return self._get_options_socket(symbol.lower() + "@optionOpenInterest@" + expiration_date, base_path="market") def options_mark_price_socket(self, symbol: str): """Subscribe to an options mark price stream. - Stream provides mark price information for all option symbols on specific underlying asset. + The mark price for all option symbols on specific underlying asset. Updates every 1000ms. - Stream name format: @markPrice + URL PATH: /market + Stream Name: @optionMarkPrice + Example: btcusdt@optionMarkPrice - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Mark-Price + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/Mark-Price Response fields include: - Event type and timestamps - - Option symbol (e.g., 'ETH-220930-1500-C') - - Option mark price + - Option symbol (e.g., 'BTC-251120-126000-C') + - Mark price, index price + - Best bid/ask prices and quantities + - Implied volatility, delta, theta, gamma, vega - :param symbol: The underlying asset (e.g., "ETH") + :param symbol: The underlying asset (e.g., "BTCUSDT", "ETHUSDT") :type symbol: str """ - return self._get_options_socket(symbol.upper() + "@markPrice") + return self._get_options_socket(symbol.lower() + "@optionMarkPrice", base_path="market") - def options_index_price_socket(self, symbol: str): + def options_index_price_socket(self): """Subscribe to an options index price stream. - API Reference: https://developers.binance.com/docs/derivatives/option/websocket-market-streams/Index-Price-Streams - - Stream provides index price information for underlying assets (e.g., ETHUSDT). + Underlying (e.g., ETHUSDT, BTCUSDT) index stream for all symbols. Updates every 1000ms. - Response fields include: - - Event type and timestamps - - Underlying symbol (e.g., 'ETHUSDT') - - Index price + URL PATH: /market + Stream Name: !index@arr - :param symbol: The underlying symbol (e.g., "ETHUSDT") - :type symbol: str + API Reference: https://developers.binance.com/docs/derivatives/options-trading/websocket-market-streams/Index-Price-Streams + + Response is an array with index price for all underlying assets: + - Event type: 'indexPrice' + - Event time + - Underlying symbol (e.g., 'ETHUSDT', 'BTCUSDT') + - Index price """ - return self._get_options_socket(symbol.upper() + "@index") + return self._get_options_socket("!index@arr", base_path="market") async def _stop_socket(self, conn_key): """Stop a websocket given the connection key diff --git a/tests/test_client_ws_futures_requests.py b/tests/test_client_ws_futures_requests.py index 5c16f925..a8540fae 100644 --- a/tests/test_client_ws_futures_requests.py +++ b/tests/test_client_ws_futures_requests.py @@ -122,7 +122,7 @@ def test_ws_futures_create_conditional_order_auto_routing(futuresClient): ticker = futuresClient.ws_futures_get_order_book_ticker(symbol="LTCUSDT") positions = futuresClient.ws_futures_v2_account_position(symbol="LTCUSDT") - trigger_price = float(ticker["askPrice"]) * 1.5 + trigger_price = round(float(ticker["askPrice"]) * 1.5, 2) order = futuresClient.ws_futures_create_order( symbol=ticker["symbol"], side="BUY", @@ -151,7 +151,7 @@ def test_ws_futures_conditional_order_with_stop_price(futuresClient): # Create a TAKE_PROFIT_MARKET order with stopPrice (should be converted to triggerPrice) # Use a price above current market price for SELL TAKE_PROFIT - trigger_price = float(ticker["askPrice"]) * 1.5 + trigger_price = round(float(ticker["askPrice"]) * 1.5, 2) order = futuresClient.ws_futures_create_order( symbol=ticker["symbol"], side="SELL", diff --git a/tests/test_streams_options.py b/tests/test_streams_options.py index 1fb91b7b..f069ce1b 100644 --- a/tests/test_streams_options.py +++ b/tests/test_streams_options.py @@ -4,20 +4,24 @@ from binance import BinanceSocketManager pytestmark = [ - pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+"), - pytest.mark.asyncio + pytest.mark.skipif( + sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+" + ), + pytest.mark.asyncio, ] # Configure logger for this module logger = logging.getLogger(__name__) -# Test constants -OPTION_SYMBOL = "BTC-251226-60000-P" -UNDERLYING_SYMBOL = "BTC" -EXPIRATION_DATE = "251226" +# Test constants - Using most actively traded options as of Dec 2025 +# BTC-260925-50000-C: 18.22 contracts, $788k daily volume +OPTION_SYMBOL = "BTC-260925-50000-C" +UNDERLYING_SYMBOL = "BTCUSDT" # Options API requires full symbol format +EXPIRATION_DATE = "260925" # September 25, 2026 INTERVAL = "1m" DEPTH = "20" + async def test_options_ticker(clientAsync): """Test options ticker socket""" logger.info(f"Starting options ticker test for symbol: {OPTION_SYMBOL}") @@ -27,13 +31,16 @@ async def test_options_ticker(clientAsync): logger.debug("Waiting for ticker message...") msg = await ts.recv() logger.info(f"Received ticker message: {msg}") - assert msg['e'] == '24hrTicker' + assert msg["e"] == "24hrTicker" logger.info("Options ticker test completed successfully") await clientAsync.close_connection() + async def test_options_ticker_by_expiration(clientAsync): """Test options ticker by expiration socket""" - logger.info(f"Starting options ticker by expiration test for {UNDERLYING_SYMBOL}, expiration: {EXPIRATION_DATE}") + logger.info( + f"Starting options ticker by expiration test for {UNDERLYING_SYMBOL}, expiration: {EXPIRATION_DATE}" + ) bm = BinanceSocketManager(clientAsync) socket = bm.options_ticker_by_expiration_socket(UNDERLYING_SYMBOL, EXPIRATION_DATE) async with socket as ts: @@ -44,6 +51,7 @@ async def test_options_ticker_by_expiration(clientAsync): logger.info("Options ticker by expiration test completed successfully") await clientAsync.close_connection() + async def test_options_recent_trades(clientAsync): """Test options recent trades socket""" logger.info(f"Starting options recent trades test for {UNDERLYING_SYMBOL}") @@ -53,23 +61,27 @@ async def test_options_recent_trades(clientAsync): logger.debug("Waiting for trade message...") msg = await ts.recv() logger.info(f"Received trade message: {msg}") - assert msg['e'] == 'trade' + assert msg["e"] == "trade" logger.info("Options recent trades test completed successfully") await clientAsync.close_connection() + async def test_options_kline(clientAsync): """Test options kline socket""" - logger.info(f"Starting options kline test for {OPTION_SYMBOL}, interval: {INTERVAL}") + logger.info( + f"Starting options kline test for {OPTION_SYMBOL}, interval: {INTERVAL}" + ) bm = BinanceSocketManager(clientAsync) socket = bm.options_kline_socket(OPTION_SYMBOL, INTERVAL) async with socket as ts: logger.debug("Waiting for kline message...") msg = await ts.recv() logger.info(f"Received kline message: {msg}") - assert msg['e'] == 'kline' + assert msg["e"] == "kline" logger.info("Options kline test completed successfully") await clientAsync.close_connection() + async def test_options_depth(clientAsync): """Test options depth socket""" logger.info(f"Starting options depth test for {OPTION_SYMBOL}, depth: {DEPTH}") @@ -79,10 +91,11 @@ async def test_options_depth(clientAsync): logger.debug("Waiting for depth message...") msg = await ts.recv() logger.info(f"Received depth message: {msg}") - assert msg['e'] == 'depth' + assert msg["e"] == "depth" logger.info("Options depth test completed successfully") await clientAsync.close_connection() + async def test_options_multiplex(clientAsync): """Test options multiplex socket""" streams = [ @@ -96,13 +109,16 @@ async def test_options_multiplex(clientAsync): logger.debug("Waiting for multiplex message...") msg = await ts.recv() logger.info(f"Received multiplex message: {msg}") - assert 'stream' in msg + assert "stream" in msg logger.info("Options multiplex test completed successfully") await clientAsync.close_connection() + async def test_options_open_interest(clientAsync): """Test options open interest socket""" - logger.info(f"Starting options open interest test for {UNDERLYING_SYMBOL}, expiration: {EXPIRATION_DATE}") + logger.info( + f"Starting options open interest test for {UNDERLYING_SYMBOL}, expiration: {EXPIRATION_DATE}" + ) bm = BinanceSocketManager(clientAsync) socket = bm.options_open_interest_socket(UNDERLYING_SYMBOL, EXPIRATION_DATE) async with socket as ts: @@ -113,6 +129,7 @@ async def test_options_open_interest(clientAsync): logger.info("Options open interest test completed successfully") await clientAsync.close_connection() + async def test_options_mark_price(clientAsync): """Test options mark price socket""" logger.info(f"Starting options mark price test for {UNDERLYING_SYMBOL}") @@ -126,16 +143,32 @@ async def test_options_mark_price(clientAsync): logger.info("Options mark price test completed successfully") await clientAsync.close_connection() + async def test_options_index_price(clientAsync): """Test options index price socket""" - symbol = 'ETHUSDT' - logger.info(f"Starting options index price test for {symbol}") + logger.info("Starting options index price test") bm = BinanceSocketManager(clientAsync) - socket = bm.options_index_price_socket(symbol) + socket = bm.options_index_price_socket() async with socket as ts: logger.debug("Waiting for index price message...") msg = await ts.recv() - logger.info(f"Received index price message: {msg}") - assert msg['e'] == 'index' + logger.info(f"Received index price message with {len(msg)} items") + assert len(msg) > 0 + assert msg[0]["e"] == "indexPrice" logger.info("Options index price test completed successfully") await clientAsync.close_connection() + + +@pytest.mark.skip(reason="Not enough traffic") +async def test_options_new_symbol(clientAsync): + """Test options new symbol socket""" + logger.info("Starting options new symbol test") + bm = BinanceSocketManager(clientAsync) + socket = bm.options_new_symbol_socket() + async with socket as ts: + logger.debug("Waiting for new symbol message...") + msg = await ts.recv() + logger.info(f"Received new symbol message: {msg}") + assert "e" in msg + logger.info("Options new symbol test completed successfully") + await clientAsync.close_connection()