binance: add deposits/withdrawals API support
From @guilledk, - Drop Decimal quantize for now - Minor tweaks to trades_dialogue protobasic_buy_bot
parent
eaaf6e4cc1
commit
7c00ca0254
|
@ -39,13 +39,15 @@ from typing import (
|
||||||
)
|
)
|
||||||
import hmac
|
import hmac
|
||||||
import time
|
import time
|
||||||
import decimal
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
import pendulum
|
from pendulum import (
|
||||||
|
now,
|
||||||
|
from_timestamp,
|
||||||
|
)
|
||||||
import asks
|
import asks
|
||||||
from fuzzywuzzy import process as fuzzy
|
from fuzzywuzzy import process as fuzzy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -79,10 +81,10 @@ from piker.data._web_bs import (
|
||||||
from ..clearing._messages import (
|
from ..clearing._messages import (
|
||||||
BrokerdOrder,
|
BrokerdOrder,
|
||||||
BrokerdOrderAck,
|
BrokerdOrderAck,
|
||||||
# BrokerdCancel,
|
BrokerdStatus,
|
||||||
#BrokerdStatus,
|
BrokerdPosition,
|
||||||
#BrokerdPosition,
|
BrokerdFill,
|
||||||
#BrokerdFill,
|
BrokerdCancel,
|
||||||
# BrokerdError,
|
# BrokerdError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -108,6 +110,7 @@ log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_url = 'https://api.binance.com'
|
_url = 'https://api.binance.com'
|
||||||
|
_sapi_url = 'https://api.binance.com'
|
||||||
_fapi_url = 'https://testnet.binancefuture.com'
|
_fapi_url = 'https://testnet.binancefuture.com'
|
||||||
|
|
||||||
|
|
||||||
|
@ -238,18 +241,25 @@ class Client:
|
||||||
self._sesh = asks.Session(connections=4)
|
self._sesh = asks.Session(connections=4)
|
||||||
self._sesh.base_location: str = _url
|
self._sesh.base_location: str = _url
|
||||||
|
|
||||||
# testnet EP sesh
|
# futes testnet rest EPs
|
||||||
self._fapi_sesh = asks.Session(connections=4)
|
self._fapi_sesh = asks.Session(connections=4)
|
||||||
self._fapi_sesh.base_location = _fapi_url
|
self._fapi_sesh.base_location = _fapi_url
|
||||||
|
|
||||||
|
# sync rest API
|
||||||
|
self._sapi_sesh = asks.Session(connections=4)
|
||||||
|
self._sapi_sesh.base_location = _sapi_url
|
||||||
|
|
||||||
conf: dict = get_config()
|
conf: dict = get_config()
|
||||||
self.api_key: str = conf.get('api_key', '')
|
self.api_key: str = conf.get('api_key', '')
|
||||||
self.api_secret: str = conf.get('api_secret', '')
|
self.api_secret: str = conf.get('api_secret', '')
|
||||||
|
|
||||||
|
self.watchlist = conf.get('watchlist', [])
|
||||||
|
|
||||||
if self.api_key:
|
if self.api_key:
|
||||||
api_key_header = {'X-MBX-APIKEY': self.api_key}
|
api_key_header = {'X-MBX-APIKEY': self.api_key}
|
||||||
self._sesh.headers.update(api_key_header)
|
self._sesh.headers.update(api_key_header)
|
||||||
self._fapi_sesh.headers.update(api_key_header)
|
self._fapi_sesh.headers.update(api_key_header)
|
||||||
|
self._sapi_sesh.headers.update(api_key_header)
|
||||||
|
|
||||||
def _get_signature(self, data: OrderedDict) -> str:
|
def _get_signature(self, data: OrderedDict) -> str:
|
||||||
|
|
||||||
|
@ -310,6 +320,25 @@ class Client:
|
||||||
|
|
||||||
return resproc(resp, log)
|
return resproc(resp, log)
|
||||||
|
|
||||||
|
async def _sapi(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
params: Union[dict, OrderedDict],
|
||||||
|
signed: bool = False,
|
||||||
|
action: str = 'get'
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
|
if signed:
|
||||||
|
params['signature'] = self._get_signature(params)
|
||||||
|
|
||||||
|
resp = await getattr(self._sapi_sesh, action)(
|
||||||
|
path=f'/sapi/v1/{method}',
|
||||||
|
params=params,
|
||||||
|
timeout=float('inf')
|
||||||
|
)
|
||||||
|
|
||||||
|
return resproc(resp, log)
|
||||||
|
|
||||||
async def exch_info(
|
async def exch_info(
|
||||||
self,
|
self,
|
||||||
sym: str | None = None,
|
sym: str | None = None,
|
||||||
|
@ -392,7 +421,7 @@ class Client:
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
if end_dt is None:
|
if end_dt is None:
|
||||||
end_dt = pendulum.now('UTC').add(minutes=1)
|
end_dt = now('UTC').add(minutes=1)
|
||||||
|
|
||||||
if start_dt is None:
|
if start_dt is None:
|
||||||
start_dt = end_dt.start_of(
|
start_dt = end_dt.start_of(
|
||||||
|
@ -444,6 +473,58 @@ class Client:
|
||||||
) if as_np else bars
|
) if as_np else bars
|
||||||
return array
|
return array
|
||||||
|
|
||||||
|
async def get_positions(
|
||||||
|
self,
|
||||||
|
recv_window: int = 60000
|
||||||
|
) -> tuple:
|
||||||
|
positions = {}
|
||||||
|
volumes = {}
|
||||||
|
|
||||||
|
for sym in self.watchlist:
|
||||||
|
log.info(f'doing {sym}...')
|
||||||
|
params = OrderedDict([
|
||||||
|
('symbol', sym),
|
||||||
|
('recvWindow', recv_window),
|
||||||
|
('timestamp', binance_timestamp(now()))
|
||||||
|
])
|
||||||
|
resp = await self._api(
|
||||||
|
'allOrders',
|
||||||
|
params=params,
|
||||||
|
signed=True
|
||||||
|
)
|
||||||
|
log.info(f'done. len {len(resp)}')
|
||||||
|
await trio.sleep(3)
|
||||||
|
|
||||||
|
return positions, volumes
|
||||||
|
|
||||||
|
async def get_deposits(
|
||||||
|
self,
|
||||||
|
recv_window: int = 60000
|
||||||
|
) -> list:
|
||||||
|
|
||||||
|
params = OrderedDict([
|
||||||
|
('recvWindow', recv_window),
|
||||||
|
('timestamp', binance_timestamp(now()))
|
||||||
|
])
|
||||||
|
return await self._sapi(
|
||||||
|
'capital/deposit/hisrec',
|
||||||
|
params=params,
|
||||||
|
signed=True)
|
||||||
|
|
||||||
|
async def get_withdrawls(
|
||||||
|
self,
|
||||||
|
recv_window: int = 60000
|
||||||
|
) -> list:
|
||||||
|
|
||||||
|
params = OrderedDict([
|
||||||
|
('recvWindow', recv_window),
|
||||||
|
('timestamp', binance_timestamp(now()))
|
||||||
|
])
|
||||||
|
return await self._sapi(
|
||||||
|
'capital/withdraw/history',
|
||||||
|
params=params,
|
||||||
|
signed=True)
|
||||||
|
|
||||||
async def submit_limit(
|
async def submit_limit(
|
||||||
self,
|
self,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
|
@ -461,18 +542,8 @@ class Client:
|
||||||
|
|
||||||
await self.cache_symbols()
|
await self.cache_symbols()
|
||||||
|
|
||||||
asset_precision = self._pairs[symbol]['baseAssetPrecision']
|
# asset_precision = self._pairs[symbol]['baseAssetPrecision']
|
||||||
quote_precision = self._pairs[symbol]['quoteAssetPrecision']
|
# quote_precision = self._pairs[symbol]['quoteAssetPrecision']
|
||||||
|
|
||||||
quantity = Decimal(quantity).quantize(
|
|
||||||
Decimal(1 ** -asset_precision),
|
|
||||||
rounding=decimal.ROUND_HALF_EVEN
|
|
||||||
)
|
|
||||||
|
|
||||||
price = Decimal(price).quantize(
|
|
||||||
Decimal(1 ** -quote_precision),
|
|
||||||
rounding=decimal.ROUND_HALF_EVEN
|
|
||||||
)
|
|
||||||
|
|
||||||
params = OrderedDict([
|
params = OrderedDict([
|
||||||
('symbol', symbol),
|
('symbol', symbol),
|
||||||
|
@ -483,21 +554,21 @@ class Client:
|
||||||
('price', price),
|
('price', price),
|
||||||
('recvWindow', recv_window),
|
('recvWindow', recv_window),
|
||||||
('newOrderRespType', 'ACK'),
|
('newOrderRespType', 'ACK'),
|
||||||
('timestamp', binance_timestamp(pendulum.now()))
|
('timestamp', binance_timestamp(now()))
|
||||||
])
|
])
|
||||||
|
|
||||||
if oid:
|
if oid:
|
||||||
params['newClientOrderId'] = oid
|
params['newClientOrderId'] = oid
|
||||||
|
|
||||||
resp = await self._api(
|
resp = await self._api(
|
||||||
'order/test', # TODO: switch to real `order` endpoint
|
'order',
|
||||||
params=params,
|
params=params,
|
||||||
signed=True,
|
signed=True,
|
||||||
action='post'
|
action='post'
|
||||||
)
|
)
|
||||||
|
log.info(resp)
|
||||||
assert resp['orderId'] == oid
|
# return resp['orderId']
|
||||||
return oid
|
return resp['orderId']
|
||||||
|
|
||||||
async def submit_cancel(
|
async def submit_cancel(
|
||||||
self,
|
self,
|
||||||
|
@ -511,10 +582,10 @@ class Client:
|
||||||
('symbol', symbol),
|
('symbol', symbol),
|
||||||
('orderId', oid),
|
('orderId', oid),
|
||||||
('recvWindow', recv_window),
|
('recvWindow', recv_window),
|
||||||
('timestamp', binance_timestamp(pendulum.now()))
|
('timestamp', binance_timestamp(now()))
|
||||||
])
|
])
|
||||||
|
|
||||||
await self._api(
|
return await self._api(
|
||||||
'order',
|
'order',
|
||||||
params=params,
|
params=params,
|
||||||
signed=True,
|
signed=True,
|
||||||
|
@ -522,11 +593,11 @@ class Client:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_listen_key(self) -> str:
|
async def get_listen_key(self) -> str:
|
||||||
return await self._api(
|
return (await self._api(
|
||||||
'userDataStream',
|
'userDataStream',
|
||||||
params={},
|
params={},
|
||||||
action='post'
|
action='post'
|
||||||
)['listenKey']
|
))['listenKey']
|
||||||
|
|
||||||
async def keep_alive_key(self, listen_key: str) -> None:
|
async def keep_alive_key(self, listen_key: str) -> None:
|
||||||
await self._fapi(
|
await self._fapi(
|
||||||
|
@ -557,7 +628,7 @@ class Client:
|
||||||
key = await self.get_listen_key()
|
key = await self.get_listen_key()
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
n.start_soon(periodic_keep_alive, key)
|
n.start_soon(periodic_keep_alive, self, key)
|
||||||
yield key
|
yield key
|
||||||
n.cancel_scope.cancel()
|
n.cancel_scope.cancel()
|
||||||
|
|
||||||
|
@ -733,8 +804,8 @@ async def open_history_client(
|
||||||
if (inow - times[-1]) > 60:
|
if (inow - times[-1]) > 60:
|
||||||
await tractor.breakpoint()
|
await tractor.breakpoint()
|
||||||
|
|
||||||
start_dt = pendulum.from_timestamp(times[0])
|
start_dt = from_timestamp(times[0])
|
||||||
end_dt = pendulum.from_timestamp(times[-1])
|
end_dt = from_timestamp(times[-1])
|
||||||
|
|
||||||
return array, start_dt, end_dt
|
return array, start_dt, end_dt
|
||||||
|
|
||||||
|
@ -873,15 +944,15 @@ async def stream_quotes(
|
||||||
# hz = 1/period if period else float('inf')
|
# hz = 1/period if period else float('inf')
|
||||||
# if hz > 60:
|
# if hz > 60:
|
||||||
# log.info(f'Binance quotez : {hz}')
|
# log.info(f'Binance quotez : {hz}')
|
||||||
|
|
||||||
topic = msg['symbol'].lower()
|
if typ == 'l1':
|
||||||
await send_chan.send({topic: msg})
|
topic = msg['symbol'].lower()
|
||||||
|
await send_chan.send({topic: msg})
|
||||||
# last = time.time()
|
# last = time.time()
|
||||||
|
|
||||||
|
|
||||||
async def handle_order_requests(
|
async def handle_order_requests(
|
||||||
ems_order_stream: tractor.MsgStream,
|
ems_order_stream: tractor.MsgStream
|
||||||
symbol: str
|
|
||||||
) -> None:
|
) -> None:
|
||||||
async with open_cached_client('binance') as client:
|
async with open_cached_client('binance') as client:
|
||||||
async for request_msg in ems_order_stream:
|
async for request_msg in ems_order_stream:
|
||||||
|
@ -938,43 +1009,39 @@ async def trades_dialogue(
|
||||||
# ledger: TransactionLedger
|
# ledger: TransactionLedger
|
||||||
|
|
||||||
# TODO: load pps and accounts using accounting apis!
|
# TODO: load pps and accounts using accounting apis!
|
||||||
# positions: dict = {}
|
positions: list[BrokerdPosition] = []
|
||||||
# accounts: set[str] = set()
|
accounts: list[str] = ['binance.default']
|
||||||
# await ctx.started((positions, {}))
|
await ctx.started((positions, accounts))
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
ctx.open_stream() as ems_stream,
|
ctx.open_stream() as ems_stream,
|
||||||
trio.open_nursery() as n,
|
trio.open_nursery() as n,
|
||||||
open_cached_client('binance') as client,
|
open_cached_client('binance') as client,
|
||||||
# client.manage_listen_key() as listen_key,
|
client.manage_listen_key() as listen_key,
|
||||||
):
|
):
|
||||||
n.start_soon(handle_order_requests, ems_stream)
|
n.start_soon(handle_order_requests, ems_stream)
|
||||||
await trio.sleep_forever()
|
# await trio.sleep_forever()
|
||||||
|
|
||||||
async with open_autorecon_ws(
|
async with open_autorecon_ws(
|
||||||
f'wss://stream.binance.com:9443/ws/{listen_key}',
|
f'wss://stream.binance.com:9443/ws/{listen_key}',
|
||||||
) as ws:
|
) as ws:
|
||||||
event = await ws.recv_msg()
|
event = await ws.recv_msg()
|
||||||
|
|
||||||
|
# https://binance-docs.github.io/apidocs/spot/en/#payload-balance-update
|
||||||
if event.get('e') == 'executionReport':
|
if event.get('e') == 'executionReport':
|
||||||
"""
|
|
||||||
https://binance-docs.github.io/apidocs/spot/en/#payload-balance-update
|
|
||||||
"""
|
|
||||||
|
|
||||||
oid = event.get('c')
|
oid: str = event.get('c')
|
||||||
side = event.get('S').lower()
|
side: str = event.get('S').lower()
|
||||||
status = event.get('X')
|
status: str = event.get('X')
|
||||||
order_qty = float(event.get('q'))
|
order_qty: float = float(event.get('q'))
|
||||||
filled_qty = float(event.get('z'))
|
filled_qty: float = float(event.get('z'))
|
||||||
cumm_transacted_qty = float(event.get('Z'))
|
cum_transacted_qty: float = float(event.get('Z'))
|
||||||
price_avg = cum_transacted_qty / filled_qty
|
price_avg: float = cum_transacted_qty / filled_qty
|
||||||
|
broker_time: float = float(event.get('T'))
|
||||||
|
commission_amount: float = float(event.get('n'))
|
||||||
|
commission_asset: float = event.get('N')
|
||||||
|
|
||||||
broker_time = float(event.get('T'))
|
if status == 'TRADE':
|
||||||
|
|
||||||
commission_amount = float(event.get('n'))
|
|
||||||
commission_asset = event.get('N')
|
|
||||||
|
|
||||||
if status == 'TRADE':
|
|
||||||
if order_qty == filled_qty:
|
if order_qty == filled_qty:
|
||||||
msg = BrokerdFill(
|
msg = BrokerdFill(
|
||||||
reqid=oid,
|
reqid=oid,
|
||||||
|
@ -993,7 +1060,7 @@ async def trades_dialogue(
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if status == 'NEW':
|
if status == 'NEW':
|
||||||
status = 'submitted'
|
status = 'submitted'
|
||||||
|
|
||||||
elif status == 'CANCELED':
|
elif status == 'CANCELED':
|
||||||
|
|
Loading…
Reference in New Issue