Compare commits
6 Commits
2120eddd01
...
526c77a42d
Author | SHA1 | Date |
---|---|---|
![]() |
526c77a42d | |
![]() |
4b27d8c9b6 | |
![]() |
d18981ed93 | |
![]() |
5735603eef | |
![]() |
2e27c2af31 | |
![]() |
25fb6b10fd |
1420
binance/client.py
1420
binance/client.py
File diff suppressed because it is too large
Load Diff
|
@ -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}"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
----------------
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue