Compare commits

...

6 Commits

Author SHA1 Message Date
Pablo Criado-Perez 526c77a42d
Merge 2e27c2af31 into 4b27d8c9b6 2025-07-31 08:21:21 +03:00
yzh-pelle 4b27d8c9b6
Update of links in the docstrings (#1590)
* spot links in the docstrings updated

* Margin api docstrings updated

* Docstrings for fapi endpoints updated

* Missing dapi endpoints added and dapi docstrings updated

* Missing eapi endpoints added and docstrings updated

* papi endpoints updated

* futures_coin_v1_put_order returned

* Wallet links updated
2025-07-29 10:49:41 +01:00
Pablo Criado-Perez d18981ed93
feat: throw readloopclosed error if trying to connect once read loop is already closed (#1593)
* add timeout to jobs

* lint

* reverse timeout change

* fix failing tests
2025-07-12 11:19:17 +01:00
Pablo Criado-Perez 5735603eef
docs: update websocket error docs (#1591)
* add timeout to jobs

* docs: update websocket errors docs

* revert change
2025-07-12 11:18:49 +01:00
pcriadoperez 2e27c2af31 fix tests 2024-12-11 03:35:32 -06:00
Pablo 25fb6b10fd chore: update round_step_size to use quantize 2024-12-09 05:05:27 +01:00
8 changed files with 1095 additions and 484 deletions

File diff suppressed because it is too large Load Diff

View File

@ -81,6 +81,10 @@ class BinanceWebsocketClosed(Exception):
"""Raised when websocket connection is closed."""
pass
class ReadLoopClosed(Exception):
"""Raised when trying to read from read loop but already closed"""
pass
class NotImplementedException(Exception):
def __init__(self, value):
message = f"Not implemented: {value}"

View File

@ -1,5 +1,5 @@
import asyncio
from decimal import Decimal
from decimal import Decimal, ROUND_DOWN
import json
from typing import Union, Optional, Dict
@ -60,7 +60,9 @@ def interval_to_milliseconds(interval: str) -> Optional[int]:
def round_step_size(
quantity: Union[float, Decimal], step_size: Union[float, Decimal]
quantity: Union[float, Decimal],
step_size: Union[float, Decimal],
rounding=ROUND_DOWN,
) -> float:
"""Rounds a given quantity to a specific step size
@ -70,7 +72,8 @@ def round_step_size(
:return: decimal
"""
quantity = Decimal(str(quantity))
return float(quantity - quantity % Decimal(str(step_size)))
step_size = Decimal(str(step_size))
return float(quantity.quantize(step_size, rounding=rounding))
def convert_ts_str(ts_str):

View File

@ -36,6 +36,7 @@ from binance.exceptions import (
BinanceWebsocketClosed,
BinanceWebsocketUnableToConnect,
BinanceWebsocketQueueOverflow,
ReadLoopClosed,
)
from binance.helpers import get_loop
from binance.ws.constants import WSListenerState
@ -247,6 +248,8 @@ class ReconnectingWebsocket:
"m": f"{e}",
})
break
except Exception as e:
self._log.error(f"Unknown exception: {e.__class__.__name__} ({e})")
finally:
self._handle_read_loop = None # Signal the coro is stopped
self._reconnects = 0
@ -272,6 +275,10 @@ class ReconnectingWebsocket:
async def recv(self):
res = None
while not res:
if not self._handle_read_loop:
raise ReadLoopClosed(
"Read loop has been closed, please reset the websocket connection and listen to the message error."
)
try:
res = await asyncio.wait_for(self._queue.get(), timeout=self.TIMEOUT)
except asyncio.TimeoutError:

View File

@ -216,23 +216,61 @@ can do this.
Websocket Errors
----------------
If an error occurs, a message is sent to the callback to indicate this. The format is
If an error occurs, a message is sent to the callback to indicate this. The format is:
.. code:: python
{
'e': 'error',
'type': 'BinanceWebsocketUnableToConnect',
'm': 'Max reconnect retries reached'
'type': '<ErrorType>',
'm': '<Error message>'
}
# check for it like so
Where:
- `'e'`: Always `'error'` for error messages.
- `'type'`: The type of error encountered (see table below).
- `'m'`: A human-readable error message.
**Possible Error Types:**
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| Type | Description | Typical Action |
+===============================+==============================================================+===============================+
| BinanceWebsocketUnableToConnect| The websocket could not connect after maximum retries. | Check network, restart socket |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| BinanceWebsocketClosed | The websocket connection was closed. The system will attempt | Usually auto-reconnects |
| | to reconnect automatically. | |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| BinanceWebsocketQueueOverflow | The internal message queue exceeded its maximum size | Process messages faster, or |
| | (default 100). | increase queue size |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| CancelledError | The websocket task was cancelled (e.g., on shutdown). | Usually safe to ignore |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| IncompleteReadError | The websocket connection was interrupted during a read. | Will attempt to reconnect |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| gaierror | Network address-related error (e.g., DNS failure). | Check network |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| ConnectionClosedError | The websocket connection was closed unexpectedly. | Will attempt to reconnect |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
| *Other Exception Types* | Any other unexpected error. | Check error message |
+-------------------------------+--------------------------------------------------------------+-------------------------------+
**Example error handling in your callback:**
.. code:: python
def process_message(msg):
if msg['e'] == 'error':
# close and restart the socket
if msg.get('e') == 'error':
print(f"WebSocket error: {msg.get('type')} - {msg.get('m')}")
# Optionally close and restart the socket, or handle as needed
else:
# process message normally
**Notes:**
- Most connection-related errors will trigger automatic reconnection attempts up to 5 times.
- If the queue overflows, consider increasing `max_queue_size` or processing messages more quickly.
- For persistent errors, check your network connection and API credentials.
Websocket Examples
----------------

View File

@ -68,7 +68,7 @@ async def test_ws_futures_create_get_edit_cancel_order_with_orjson(futuresClient
type="LIMIT",
timeInForce="GTC",
quantity=0.1,
price=str(float(ticker["bidPrice"]) + 2),
price=str(float(ticker["bidPrice"]) + 5),
)
assert_contract_order(futuresClientAsync, order)
order = await futuresClientAsync.ws_futures_edit_order(
@ -101,7 +101,7 @@ async def test_ws_futures_create_get_edit_cancel_order_without_orjson(futuresCli
type="LIMIT",
timeInForce="GTC",
quantity=0.1,
price=str(float(ticker["bidPrice"]) + 2),
price=str(float(ticker["bidPrice"]) + 5),
)
assert_contract_order(futuresClientAsync, order)
order = await futuresClientAsync.ws_futures_edit_order(

63
tests/test_helpers.py Normal file
View File

@ -0,0 +1,63 @@
import pytest
from decimal import Decimal, InvalidOperation
from binance.helpers import round_step_size
@pytest.mark.parametrize(
"quantity,step_size,expected",
[
# Basic cases
(1.23456, 0.1, 1.2),
(1.23456, 0.01, 1.23),
(1.23456, 1, 1),
# Edge cases
(0.0, 0.1, 0.0),
(0.1, 0.1, 0.1),
(1.0, 1, 1.0),
# Large numbers
(100.123456, 0.1, 100.1),
(1000.123456, 1, 1000),
# Small step sizes
(1.123456, 0.0001, 1.1234),
(1.123456, 0.00001, 1.12345),
# Decimal inputs
(Decimal("1.23456"), Decimal("0.1"), 1.2),
(Decimal("1.23456"), 0.01, 1.23),
# String conversion edge cases
(1.23456, Decimal("0.01"), 1.23),
("1.23456", "0.01", 1.23),
],
)
def test_round_step_size(quantity, step_size, expected):
"""Test round_step_size with various inputs"""
result = round_step_size(quantity, step_size)
assert result == expected
assert isinstance(result, float)
def test_round_step_size_precision():
"""Test that rounding maintains proper precision"""
# Should maintain step size precision
assert round_step_size(1.123456, 0.0001) == 1.1234
assert round_step_size(1.123456, 0.001) == 1.123
assert round_step_size(1.123456, 0.01) == 1.12
assert round_step_size(1.123456, 0.1) == 1.1
def test_round_step_size_always_rounds_down():
"""Test that values are always rounded down"""
assert round_step_size(1.19, 0.1) == 1.1
assert round_step_size(1.99, 1.0) == 1.9
assert round_step_size(0.99999, 0.1) == 0.9
def test_round_step_size_invalid_inputs():
"""Test error handling for invalid inputs"""
with pytest.raises(InvalidOperation):
round_step_size(None, 0.1) # type: ignore
with pytest.raises((ValueError, InvalidOperation)):
round_step_size("invalid", 0.1) # type: ignore
with pytest.raises((ValueError, InvalidOperation)):
round_step_size(1.23, "invalid") # type: ignore

View File

@ -2,10 +2,10 @@ import sys
import pytest
import gzip
import json
from unittest.mock import patch, create_autospec
from unittest.mock import patch, create_autospec, Mock
from binance.ws.reconnecting_websocket import ReconnectingWebsocket
from binance.ws.constants import WSListenerState
from binance.exceptions import BinanceWebsocketUnableToConnect
from binance.exceptions import BinanceWebsocketUnableToConnect, ReadLoopClosed
from websockets import WebSocketClientProtocol # type: ignore
from websockets.protocol import State
import asyncio
@ -77,6 +77,8 @@ async def test_handle_message_invalid_json():
async def test_recv_message():
ws = ReconnectingWebsocket(url="wss://test.url")
await ws._queue.put({"test": "data"})
# Simulate the read loop being active
ws._handle_read_loop = Mock()
result = await ws.recv()
assert result == {"test": "data"}
@ -206,3 +208,19 @@ async def test_connect_fails_to_connect_after_disconnect():
async def delayed_return():
await asyncio.sleep(0.1) # 100 ms delay
return '{"e": "value"}'
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8+")
@pytest.mark.asyncio
async def test_recv_read_loop_closed():
"""Test that recv() raises ReadLoopClosed when read loop is closed."""
ws = ReconnectingWebsocket(url="wss://test.url")
# Simulate read loop being closed by setting _handle_read_loop to None
ws._handle_read_loop = None
with pytest.raises(ReadLoopClosed) as exc_info:
await ws.recv()
assert "Read loop has been closed" in str(exc_info.value)
assert "please reset the websocket connection" in str(exc_info.value)