binance: add deposits/withdrawals API support

From @guilledk,
- Drop Decimal quantize for now
- Minor tweaks to trades_dialogue proto
basic_buy_bot
Guillermo Rodriguez 2022-02-19 18:03:45 -03:00 committed by Tyler Goodlet
parent eaaf6e4cc1
commit 7c00ca0254
1 changed files with 127 additions and 60 deletions

View File

@ -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':