Skip to content

Commit

Permalink
feat:add support for microseconds time unit (#1519)
Browse files Browse the repository at this point in the history
Co-authored-by: Pablo <[email protected]>
  • Loading branch information
pcriadoperez and Pablo authored Dec 23, 2024
1 parent 1552b0c commit 08b060f
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 13 deletions.
2 changes: 1 addition & 1 deletion binance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from binance.ws.reconnecting_websocket import ReconnectingWebsocket # noqa

from binance.ws.constants import * # noqa
from binance.ws.constants import * # noqa

from binance.exceptions import * # noqa

Expand Down
8 changes: 6 additions & 2 deletions binance/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
private_key: Optional[Union[str, Path]] = None,
private_key_pass: Optional[str] = None,
https_proxy: Optional[str] = None,
time_unit: Optional[str] = None,
):
self.https_proxy = https_proxy
self.loop = loop or get_loop()
Expand All @@ -49,6 +50,7 @@ def __init__(
testnet,
private_key,
private_key_pass,
time_unit=time_unit,
)

@classmethod
Expand Down Expand Up @@ -132,9 +134,11 @@ async def _request(
if data is not None:
del kwargs["data"]

if signed and self.PRIVATE_KEY and data: # handle issues with signing using eddsa/rsa and POST requests
if (
signed and self.PRIVATE_KEY and data
): # handle issues with signing using eddsa/rsa and POST requests
dict_data = Client.convert_to_dict(data)
signature = dict_data["signature"] if "signature" in dict_data else None
signature = dict_data["signature"] if "signature" in dict_data else None
if signature:
del dict_data["signature"]
url_encoded_data = urlencode(dict_data)
Expand Down
9 changes: 9 additions & 0 deletions binance/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def __init__(
private_key: Optional[Union[str, Path]] = None,
private_key_pass: Optional[str] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
time_unit: Optional[str] = None,
):
"""Binance API Client constructor
Expand All @@ -175,6 +176,8 @@ def __init__(
:type private_key: optional - str or Path
:param private_key_pass: Password of private key
:type private_key_pass: optional - str
:param time_unit: Time unit to use for requests. Supported values: "MILLISECOND", "MICROSECOND"
:type time_unit: optional - str
"""

Expand All @@ -191,6 +194,7 @@ def __init__(

self.API_KEY = api_key
self.API_SECRET = api_secret
self.TIME_UNIT = time_unit
self._is_rsa = False
self.PRIVATE_KEY: Any = self._init_private_key(private_key, private_key_pass)
self.session = self._init_session()
Expand All @@ -199,6 +203,8 @@ def __init__(
self.testnet = testnet
self.timestamp_offset = 0
ws_api_url = self.WS_API_TESTNET_URL if testnet else self.WS_API_URL.format(tld)
if self.TIME_UNIT:
ws_api_url += f"?timeUnit={self.TIME_UNIT}"
self.ws_api = WebsocketAPI(url=ws_api_url, tld=tld)
ws_future_url = (
self.WS_FUTURES_TESTNET_URL if testnet else self.WS_FUTURES_URL.format(tld)
Expand All @@ -215,6 +221,9 @@ def _get_headers(self) -> Dict:
if self.API_KEY:
assert self.API_KEY
headers["X-MBX-APIKEY"] = self.API_KEY
if self.TIME_UNIT:
assert self.TIME_UNIT
headers["X-MBX-TIME-UNIT"] = self.TIME_UNIT
return headers

def _init_session(self):
Expand Down
2 changes: 2 additions & 0 deletions binance/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(
private_key: Optional[Union[str, Path]] = None,
private_key_pass: Optional[str] = None,
ping: Optional[bool] = True,
time_unit: Optional[str] = None,
):
super().__init__(
api_key,
Expand All @@ -42,6 +43,7 @@ def __init__(
testnet,
private_key,
private_key_pass,
time_unit=time_unit,
)

# init DNS and SSL cert
Expand Down
26 changes: 17 additions & 9 deletions binance/ws/keepalive_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(
self._client = client
self._user_timeout = user_timeout or KEEPALIVE_TIMEOUT
self._timer = None
self._listen_key = None

async def __aexit__(self, *args, **kwargs):
if not self._path:
Expand All @@ -37,9 +38,16 @@ async def __aexit__(self, *args, **kwargs):
self._timer = None
await super().__aexit__(*args, **kwargs)

def _build_path(self):
self._path = self._listen_key
time_unit = getattr(self._client, "TIME_UNIT", None)
if time_unit and self._keepalive_type == "user":
self._path = f"{self._listen_key}?timeUnit={time_unit}"

async def _before_connect(self):
if not self._path:
self._path = await self._get_listen_key()
if not self._listen_key:
self._listen_key = await self._get_listen_key()
self._build_path()

async def _after_connect(self):
self._start_socket_timer()
Expand Down Expand Up @@ -68,24 +76,24 @@ async def _get_listen_key(self):
async def _keepalive_socket(self):
try:
listen_key = await self._get_listen_key()
if listen_key != self._path:
if listen_key != self._listen_key:
self._log.debug("listen key changed: reconnect")
self._path = listen_key
self._build_path()
self._reconnect()
else:
self._log.debug("listen key same: keepalive")
if self._keepalive_type == "user":
await self._client.stream_keepalive(self._path)
await self._client.stream_keepalive(self._listen_key)
elif self._keepalive_type == "margin": # cross-margin
await self._client.margin_stream_keepalive(self._path)
await self._client.margin_stream_keepalive(self._listen_key)
elif self._keepalive_type == "futures":
await self._client.futures_stream_keepalive(self._path)
await self._client.futures_stream_keepalive(self._listen_key)
elif self._keepalive_type == "coin_futures":
await self._client.futures_coin_stream_keepalive(self._path)
await self._client.futures_coin_stream_keepalive(self._listen_key)
else: # isolated margin
# Passing symbol for isolated margin
await self._client.isolated_margin_stream_keepalive(
self._keepalive_type, self._path
self._keepalive_type, self._listen_key
)
except Exception as e:
self._log.error(f"error in keepalive_socket: {e}")
Expand Down
12 changes: 11 additions & 1 deletion binance/ws/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def _get_socket(
socket_type: BinanceSocketType = BinanceSocketType.SPOT,
) -> ReconnectingWebsocket:
conn_id = f"{socket_type}_{path}"
time_unit = getattr(self._client, "TIME_UNIT", None)
if time_unit:
path = f"{path}?timeUnit={time_unit}"
if conn_id not in self._conns:
self._conns[conn_id] = ReconnectingWebsocket(
path=path,
Expand Down Expand Up @@ -1100,7 +1103,14 @@ def __init__(
loop: Optional[asyncio.AbstractEventLoop] = None,
):
super().__init__(
api_key, api_secret, requests_params, tld, testnet, session_params, https_proxy, loop
api_key,
api_secret,
requests_params,
tld,
testnet,
session_params,
https_proxy,
loop,
)
self._bsm: Optional[BinanceSocketManager] = None

Expand Down
23 changes: 23 additions & 0 deletions tests/test_async_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import pytest

from binance.async_client import AsyncClient
from .conftest import proxy, api_key, api_secret, testnet

pytestmark = [pytest.mark.asyncio]


Expand Down Expand Up @@ -226,3 +229,23 @@ async def test_margin_max_borrowable(clientAsync):
await clientAsync.margin_max_borrowable(
asset="BTC",
)


async def test_time_unit_microseconds():
micro_client = AsyncClient(
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MICROSECOND"
)
micro_trades = await micro_client.get_recent_trades(symbol="BTCUSDT")
assert len(str(micro_trades[0]["time"])) >= 16, (
"Time should be in microseconds (16+ digits)"
)


async def test_time_unit_milloseconds():
milli_client = AsyncClient(
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MILLISECOND"
)
milli_trades = await milli_client.get_recent_trades(symbol="BTCUSDT")
assert len(str(milli_trades[0]["time"])) == 13, (
"Time should be in milliseconds (13 digits)"
)
30 changes: 30 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import pytest
from binance.client import Client
from .conftest import proxies, api_key, api_secret, testnet


def test_client_initialization(client):
Expand Down Expand Up @@ -183,3 +185,31 @@ def test_ws_get_time(client):

def test_ws_get_exchange_info(client):
client.ws_get_exchange_info(symbol="BTCUSDT")


def test_time_unit_microseconds():
micro_client = Client(
api_key,
api_secret,
{"proxies": proxies},
testnet=testnet,
time_unit="MICROSECOND",
)
micro_trades = micro_client.get_recent_trades(symbol="BTCUSDT")
assert len(str(micro_trades[0]["time"])) >= 16, (
"Time should be in microseconds (16+ digits)"
)


def test_time_unit_milloseconds():
milli_client = Client(
api_key,
api_secret,
{"proxies": proxies},
testnet=testnet,
time_unit="MILLISECOND",
)
milli_trades = milli_client.get_recent_trades(symbol="BTCUSDT")
assert len(str(milli_trades[0]["time"])) == 13, (
"Time should be in milliseconds (13 digits)"
)
30 changes: 30 additions & 0 deletions tests/test_client_ws_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from binance.client import Client
from .conftest import proxies, api_key, api_secret, testnet
from .test_get_order_book import assert_ob


Expand Down Expand Up @@ -60,3 +62,31 @@ def test_ws_get_time(client):

def test_ws_get_exchange_info(client):
client.ws_get_exchange_info(symbol="BTCUSDT")


def test_ws_time_microseconds():
micro_client = Client(
api_key,
api_secret,
{"proxies": proxies},
testnet=testnet,
time_unit="MICROSECOND",
)
micro_trades = micro_client.ws_get_recent_trades(symbol="BTCUSDT")
assert len(str(micro_trades[0]["time"])) >= 16, (
"WS time should be in microseconds (16+ digits)"
)


def test_ws_time_milliseconds():
milli_client = Client(
api_key,
api_secret,
{"proxies": proxies},
testnet=testnet,
time_unit="MILLISECOND",
)
milli_trades = milli_client.ws_get_recent_trades(symbol="BTCUSDT")
assert len(str(milli_trades[0]["time"])) == 13, (
"WS time should be in milliseconds (13 digits)"
)
65 changes: 65 additions & 0 deletions tests/test_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from binance import BinanceSocketManager
import pytest

from binance.async_client import AsyncClient
from .conftest import proxy, api_key, api_secret, testnet


@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
@pytest.mark.asyncio
Expand All @@ -13,3 +16,65 @@ async def test_socket_stopped_on_aexit(clientAsync):
ts2 = bm.trade_socket("BNBBTC")
assert ts2 is not ts1, "socket should be removed from _conn on exit"
await clientAsync.close_connection()


@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
@pytest.mark.asyncio
async def test_socket_spot_market_time_unit_microseconds():
clientAsync = AsyncClient(
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MICROSECOND"
)
bm = BinanceSocketManager(clientAsync)
ts1 = bm.symbol_ticker_socket("BTCUSDT")
async with ts1:
trade = await ts1.recv()
assert len(str(trade["E"])) >= 16, "Time should be in microseconds (16+ digits)"
await clientAsync.close_connection()


@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
@pytest.mark.asyncio
async def test_socket_spot_market_time_unit_milliseconds():
clientAsync = AsyncClient(
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MILLISECOND"
)
bm = BinanceSocketManager(clientAsync)
ts1 = bm.symbol_ticker_socket("BTCUSDT")
async with ts1:
trade = await ts1.recv()
assert len(str(trade["E"])) == 13, "Time should be in milliseconds (13 digits)"
await clientAsync.close_connection()


@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
@pytest.mark.asyncio
async def test_socket_spot_user_data_time_unit_microseconds():
clientAsync = AsyncClient(
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MICROSECOND"
)
bm = BinanceSocketManager(clientAsync)
ts1 = bm.user_socket()
async with ts1:
await clientAsync.create_order(
symbol="LTCUSDT", side="BUY", type="MARKET", quantity=0.1
)
trade = await ts1.recv()
assert len(str(trade["E"])) >= 16, "Time should be in microseconds (16+ digits)"
await clientAsync.close_connection()


@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
@pytest.mark.asyncio
async def test_socket_spot_user_data_time_unit_milliseconds():
clientAsync = AsyncClient(
api_key, api_secret, https_proxy=proxy, testnet=testnet, time_unit="MILLISECOND"
)
bm = BinanceSocketManager(clientAsync)
ts1 = bm.user_socket()
async with ts1:
await clientAsync.create_order(
symbol="LTCUSDT", side="BUY", type="MARKET", quantity=0.1
)
trade = await ts1.recv()
assert len(str(trade["E"])) == 13, "Time should be in milliseconds (13 digits)"
await clientAsync.close_connection()

0 comments on commit 08b060f

Please sign in to comment.