Skip to content

Commit 08b060f

Browse files
pcriadoperezPablo
andauthored
feat:add support for microseconds time unit (#1519)
Co-authored-by: Pablo <[email protected]>
1 parent 1552b0c commit 08b060f

File tree

10 files changed

+194
-13
lines changed

10 files changed

+194
-13
lines changed

binance/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from binance.ws.reconnecting_websocket import ReconnectingWebsocket # noqa
2727

28-
from binance.ws.constants import * # noqa
28+
from binance.ws.constants import * # noqa
2929

3030
from binance.exceptions import * # noqa
3131

binance/async_client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def __init__(
3636
private_key: Optional[Union[str, Path]] = None,
3737
private_key_pass: Optional[str] = None,
3838
https_proxy: Optional[str] = None,
39+
time_unit: Optional[str] = None,
3940
):
4041
self.https_proxy = https_proxy
4142
self.loop = loop or get_loop()
@@ -49,6 +50,7 @@ def __init__(
4950
testnet,
5051
private_key,
5152
private_key_pass,
53+
time_unit=time_unit,
5254
)
5355

5456
@classmethod
@@ -132,9 +134,11 @@ async def _request(
132134
if data is not None:
133135
del kwargs["data"]
134136

135-
if signed and self.PRIVATE_KEY and data: # handle issues with signing using eddsa/rsa and POST requests
137+
if (
138+
signed and self.PRIVATE_KEY and data
139+
): # handle issues with signing using eddsa/rsa and POST requests
136140
dict_data = Client.convert_to_dict(data)
137-
signature = dict_data["signature"] if "signature" in dict_data else None
141+
signature = dict_data["signature"] if "signature" in dict_data else None
138142
if signature:
139143
del dict_data["signature"]
140144
url_encoded_data = urlencode(dict_data)

binance/base_client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def __init__(
160160
private_key: Optional[Union[str, Path]] = None,
161161
private_key_pass: Optional[str] = None,
162162
loop: Optional[asyncio.AbstractEventLoop] = None,
163+
time_unit: Optional[str] = None,
163164
):
164165
"""Binance API Client constructor
165166
@@ -175,6 +176,8 @@ def __init__(
175176
:type private_key: optional - str or Path
176177
:param private_key_pass: Password of private key
177178
:type private_key_pass: optional - str
179+
:param time_unit: Time unit to use for requests. Supported values: "MILLISECOND", "MICROSECOND"
180+
:type time_unit: optional - str
178181
179182
"""
180183

@@ -191,6 +194,7 @@ def __init__(
191194

192195
self.API_KEY = api_key
193196
self.API_SECRET = api_secret
197+
self.TIME_UNIT = time_unit
194198
self._is_rsa = False
195199
self.PRIVATE_KEY: Any = self._init_private_key(private_key, private_key_pass)
196200
self.session = self._init_session()
@@ -199,6 +203,8 @@ def __init__(
199203
self.testnet = testnet
200204
self.timestamp_offset = 0
201205
ws_api_url = self.WS_API_TESTNET_URL if testnet else self.WS_API_URL.format(tld)
206+
if self.TIME_UNIT:
207+
ws_api_url += f"?timeUnit={self.TIME_UNIT}"
202208
self.ws_api = WebsocketAPI(url=ws_api_url, tld=tld)
203209
ws_future_url = (
204210
self.WS_FUTURES_TESTNET_URL if testnet else self.WS_FUTURES_URL.format(tld)
@@ -215,6 +221,9 @@ def _get_headers(self) -> Dict:
215221
if self.API_KEY:
216222
assert self.API_KEY
217223
headers["X-MBX-APIKEY"] = self.API_KEY
224+
if self.TIME_UNIT:
225+
assert self.TIME_UNIT
226+
headers["X-MBX-TIME-UNIT"] = self.TIME_UNIT
218227
return headers
219228

220229
def _init_session(self):

binance/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(
3232
private_key: Optional[Union[str, Path]] = None,
3333
private_key_pass: Optional[str] = None,
3434
ping: Optional[bool] = True,
35+
time_unit: Optional[str] = None,
3536
):
3637
super().__init__(
3738
api_key,
@@ -42,6 +43,7 @@ def __init__(
4243
testnet,
4344
private_key,
4445
private_key_pass,
46+
time_unit=time_unit,
4547
)
4648

4749
# init DNS and SSL cert

binance/ws/keepalive_websocket.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(
2828
self._client = client
2929
self._user_timeout = user_timeout or KEEPALIVE_TIMEOUT
3030
self._timer = None
31+
self._listen_key = None
3132

3233
async def __aexit__(self, *args, **kwargs):
3334
if not self._path:
@@ -37,9 +38,16 @@ async def __aexit__(self, *args, **kwargs):
3738
self._timer = None
3839
await super().__aexit__(*args, **kwargs)
3940

41+
def _build_path(self):
42+
self._path = self._listen_key
43+
time_unit = getattr(self._client, "TIME_UNIT", None)
44+
if time_unit and self._keepalive_type == "user":
45+
self._path = f"{self._listen_key}?timeUnit={time_unit}"
46+
4047
async def _before_connect(self):
41-
if not self._path:
42-
self._path = await self._get_listen_key()
48+
if not self._listen_key:
49+
self._listen_key = await self._get_listen_key()
50+
self._build_path()
4351

4452
async def _after_connect(self):
4553
self._start_socket_timer()
@@ -68,24 +76,24 @@ async def _get_listen_key(self):
6876
async def _keepalive_socket(self):
6977
try:
7078
listen_key = await self._get_listen_key()
71-
if listen_key != self._path:
79+
if listen_key != self._listen_key:
7280
self._log.debug("listen key changed: reconnect")
73-
self._path = listen_key
81+
self._build_path()
7482
self._reconnect()
7583
else:
7684
self._log.debug("listen key same: keepalive")
7785
if self._keepalive_type == "user":
78-
await self._client.stream_keepalive(self._path)
86+
await self._client.stream_keepalive(self._listen_key)
7987
elif self._keepalive_type == "margin": # cross-margin
80-
await self._client.margin_stream_keepalive(self._path)
88+
await self._client.margin_stream_keepalive(self._listen_key)
8189
elif self._keepalive_type == "futures":
82-
await self._client.futures_stream_keepalive(self._path)
90+
await self._client.futures_stream_keepalive(self._listen_key)
8391
elif self._keepalive_type == "coin_futures":
84-
await self._client.futures_coin_stream_keepalive(self._path)
92+
await self._client.futures_coin_stream_keepalive(self._listen_key)
8593
else: # isolated margin
8694
# Passing symbol for isolated margin
8795
await self._client.isolated_margin_stream_keepalive(
88-
self._keepalive_type, self._path
96+
self._keepalive_type, self._listen_key
8997
)
9098
except Exception as e:
9199
self._log.error(f"error in keepalive_socket: {e}")

binance/ws/streams.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ def _get_socket(
7575
socket_type: BinanceSocketType = BinanceSocketType.SPOT,
7676
) -> ReconnectingWebsocket:
7777
conn_id = f"{socket_type}_{path}"
78+
time_unit = getattr(self._client, "TIME_UNIT", None)
79+
if time_unit:
80+
path = f"{path}?timeUnit={time_unit}"
7881
if conn_id not in self._conns:
7982
self._conns[conn_id] = ReconnectingWebsocket(
8083
path=path,
@@ -1100,7 +1103,14 @@ def __init__(
11001103
loop: Optional[asyncio.AbstractEventLoop] = None,
11011104
):
11021105
super().__init__(
1103-
api_key, api_secret, requests_params, tld, testnet, session_params, https_proxy, loop
1106+
api_key,
1107+
api_secret,
1108+
requests_params,
1109+
tld,
1110+
testnet,
1111+
session_params,
1112+
https_proxy,
1113+
loop,
11041114
)
11051115
self._bsm: Optional[BinanceSocketManager] = None
11061116

tests/test_async_client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import pytest
22

3+
from binance.async_client import AsyncClient
4+
from .conftest import proxy, api_key, api_secret, testnet
5+
36
pytestmark = [pytest.mark.asyncio]
47

58

@@ -226,3 +229,23 @@ async def test_margin_max_borrowable(clientAsync):
226229
await clientAsync.margin_max_borrowable(
227230
asset="BTC",
228231
)
232+
233+
234+
async def test_time_unit_microseconds():
235+
micro_client = AsyncClient(
236+
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MICROSECOND"
237+
)
238+
micro_trades = await micro_client.get_recent_trades(symbol="BTCUSDT")
239+
assert len(str(micro_trades[0]["time"])) >= 16, (
240+
"Time should be in microseconds (16+ digits)"
241+
)
242+
243+
244+
async def test_time_unit_milloseconds():
245+
milli_client = AsyncClient(
246+
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MILLISECOND"
247+
)
248+
milli_trades = await milli_client.get_recent_trades(symbol="BTCUSDT")
249+
assert len(str(milli_trades[0]["time"])) == 13, (
250+
"Time should be in milliseconds (13 digits)"
251+
)

tests/test_client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import pytest
2+
from binance.client import Client
3+
from .conftest import proxies, api_key, api_secret, testnet
24

35

46
def test_client_initialization(client):
@@ -183,3 +185,31 @@ def test_ws_get_time(client):
183185

184186
def test_ws_get_exchange_info(client):
185187
client.ws_get_exchange_info(symbol="BTCUSDT")
188+
189+
190+
def test_time_unit_microseconds():
191+
micro_client = Client(
192+
api_key,
193+
api_secret,
194+
{"proxies": proxies},
195+
testnet=testnet,
196+
time_unit="MICROSECOND",
197+
)
198+
micro_trades = micro_client.get_recent_trades(symbol="BTCUSDT")
199+
assert len(str(micro_trades[0]["time"])) >= 16, (
200+
"Time should be in microseconds (16+ digits)"
201+
)
202+
203+
204+
def test_time_unit_milloseconds():
205+
milli_client = Client(
206+
api_key,
207+
api_secret,
208+
{"proxies": proxies},
209+
testnet=testnet,
210+
time_unit="MILLISECOND",
211+
)
212+
milli_trades = milli_client.get_recent_trades(symbol="BTCUSDT")
213+
assert len(str(milli_trades[0]["time"])) == 13, (
214+
"Time should be in milliseconds (13 digits)"
215+
)

tests/test_client_ws_api.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from binance.client import Client
2+
from .conftest import proxies, api_key, api_secret, testnet
13
from .test_get_order_book import assert_ob
24

35

@@ -60,3 +62,31 @@ def test_ws_get_time(client):
6062

6163
def test_ws_get_exchange_info(client):
6264
client.ws_get_exchange_info(symbol="BTCUSDT")
65+
66+
67+
def test_ws_time_microseconds():
68+
micro_client = Client(
69+
api_key,
70+
api_secret,
71+
{"proxies": proxies},
72+
testnet=testnet,
73+
time_unit="MICROSECOND",
74+
)
75+
micro_trades = micro_client.ws_get_recent_trades(symbol="BTCUSDT")
76+
assert len(str(micro_trades[0]["time"])) >= 16, (
77+
"WS time should be in microseconds (16+ digits)"
78+
)
79+
80+
81+
def test_ws_time_milliseconds():
82+
milli_client = Client(
83+
api_key,
84+
api_secret,
85+
{"proxies": proxies},
86+
testnet=testnet,
87+
time_unit="MILLISECOND",
88+
)
89+
milli_trades = milli_client.ws_get_recent_trades(symbol="BTCUSDT")
90+
assert len(str(milli_trades[0]["time"])) == 13, (
91+
"WS time should be in milliseconds (13 digits)"
92+
)

tests/test_streams.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
from binance import BinanceSocketManager
33
import pytest
44

5+
from binance.async_client import AsyncClient
6+
from .conftest import proxy, api_key, api_secret, testnet
7+
58

69
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
710
@pytest.mark.asyncio
@@ -13,3 +16,65 @@ async def test_socket_stopped_on_aexit(clientAsync):
1316
ts2 = bm.trade_socket("BNBBTC")
1417
assert ts2 is not ts1, "socket should be removed from _conn on exit"
1518
await clientAsync.close_connection()
19+
20+
21+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
22+
@pytest.mark.asyncio
23+
async def test_socket_spot_market_time_unit_microseconds():
24+
clientAsync = AsyncClient(
25+
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MICROSECOND"
26+
)
27+
bm = BinanceSocketManager(clientAsync)
28+
ts1 = bm.symbol_ticker_socket("BTCUSDT")
29+
async with ts1:
30+
trade = await ts1.recv()
31+
assert len(str(trade["E"])) >= 16, "Time should be in microseconds (16+ digits)"
32+
await clientAsync.close_connection()
33+
34+
35+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
36+
@pytest.mark.asyncio
37+
async def test_socket_spot_market_time_unit_milliseconds():
38+
clientAsync = AsyncClient(
39+
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MILLISECOND"
40+
)
41+
bm = BinanceSocketManager(clientAsync)
42+
ts1 = bm.symbol_ticker_socket("BTCUSDT")
43+
async with ts1:
44+
trade = await ts1.recv()
45+
assert len(str(trade["E"])) == 13, "Time should be in milliseconds (13 digits)"
46+
await clientAsync.close_connection()
47+
48+
49+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
50+
@pytest.mark.asyncio
51+
async def test_socket_spot_user_data_time_unit_microseconds():
52+
clientAsync = AsyncClient(
53+
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MICROSECOND"
54+
)
55+
bm = BinanceSocketManager(clientAsync)
56+
ts1 = bm.user_socket()
57+
async with ts1:
58+
await clientAsync.create_order(
59+
symbol="LTCUSDT", side="BUY", type="MARKET", quantity=0.1
60+
)
61+
trade = await ts1.recv()
62+
assert len(str(trade["E"])) >= 16, "Time should be in microseconds (16+ digits)"
63+
await clientAsync.close_connection()
64+
65+
66+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
67+
@pytest.mark.asyncio
68+
async def test_socket_spot_user_data_time_unit_milliseconds():
69+
clientAsync = AsyncClient(
70+
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MILLISECOND"
71+
)
72+
bm = BinanceSocketManager(clientAsync)
73+
ts1 = bm.user_socket()
74+
async with ts1:
75+
await clientAsync.create_order(
76+
symbol="LTCUSDT", side="BUY", type="MARKET", quantity=0.1
77+
)
78+
trade = await ts1.recv()
79+
assert len(str(trade["E"])) == 13, "Time should be in milliseconds (13 digits)"
80+
await clientAsync.close_connection()

0 commit comments

Comments
 (0)