binance: start drafting live order ctl endpoints
First draft originally by @guilledk but update by myself 2 years later xD. Will crash at runtime but at least has the machinery to setup signed requests for auth-ed endpoints B) Also adds a generic `NoSignature` error for when credentials are not present in `brokers.toml` but user is trying to access auth-ed eps with the client.basic_buy_bot
parent
35359861bb
commit
bc4ded2662
|
@ -1,6 +1,6 @@
|
||||||
# piker: trading gear for hackers
|
# piker: trading gear for hackers
|
||||||
# Copyright (C)
|
# Copyright (C)
|
||||||
# Guillermo Rodriguez
|
# Guillermo Rodriguez (aka ze jefe)
|
||||||
# Tyler Goodlet
|
# Tyler Goodlet
|
||||||
# (in stewardship for pikers)
|
# (in stewardship for pikers)
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
Binance backend
|
Binance backend
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from collections import OrderedDict
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
aclosing,
|
aclosing,
|
||||||
|
@ -29,11 +30,16 @@ from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import itertools
|
import itertools
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Union, Optional,
|
Any,
|
||||||
AsyncGenerator, Callable,
|
Union,
|
||||||
|
AsyncIterator,
|
||||||
|
AsyncGenerator,
|
||||||
|
Callable,
|
||||||
)
|
)
|
||||||
|
import hmac
|
||||||
import time
|
import time
|
||||||
|
import decimal
|
||||||
|
import hashlib
|
||||||
import trio
|
import trio
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
import pendulum
|
import pendulum
|
||||||
|
@ -42,6 +48,7 @@ from fuzzywuzzy import process as fuzzy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
|
from .. import config
|
||||||
from .._cacheables import async_lifo_cache
|
from .._cacheables import async_lifo_cache
|
||||||
from ..accounting._mktinfo import (
|
from ..accounting._mktinfo import (
|
||||||
Asset,
|
Asset,
|
||||||
|
@ -66,6 +73,30 @@ from piker.data._web_bs import (
|
||||||
NoBsWs,
|
NoBsWs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ..clearing._messages import (
|
||||||
|
BrokerdOrder,
|
||||||
|
BrokerdOrderAck,
|
||||||
|
# BrokerdCancel,
|
||||||
|
#BrokerdStatus,
|
||||||
|
#BrokerdPosition,
|
||||||
|
#BrokerdFill,
|
||||||
|
# BrokerdError,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = get_logger('piker.brokers.binance')
|
||||||
|
|
||||||
|
|
||||||
|
def get_config() -> dict:
|
||||||
|
conf, path = config.load()
|
||||||
|
|
||||||
|
section = conf.get('binance')
|
||||||
|
|
||||||
|
if not section:
|
||||||
|
log.warning(f'No config section found for binance in {path}')
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
return section
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -197,16 +228,55 @@ class Client:
|
||||||
self._sesh.base_location = _url
|
self._sesh.base_location = _url
|
||||||
self._pairs: dict[str, Pair] = {}
|
self._pairs: dict[str, Pair] = {}
|
||||||
|
|
||||||
|
conf = get_config()
|
||||||
|
self.api_key = conf.get('api', {}).get('key')
|
||||||
|
self.api_secret = conf.get('api', {}).get('secret')
|
||||||
|
|
||||||
|
if self.api_key:
|
||||||
|
self._sesh.headers.update({'X-MBX-APIKEY': self.api_key})
|
||||||
|
|
||||||
|
def _get_signature(self, data: OrderedDict) -> str:
|
||||||
|
if not self.api_secret:
|
||||||
|
raise config.NoSignature(
|
||||||
|
"Can't generate a signature without setting up credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
query_str = '&'.join([
|
||||||
|
f'{_key}={value}'
|
||||||
|
for _key, value in data.items()])
|
||||||
|
log.info(query_str)
|
||||||
|
msg_auth = hmac.new(
|
||||||
|
self.api_secret.encode('utf-8'),
|
||||||
|
query_str.encode('utf-8'),
|
||||||
|
hashlib.sha256
|
||||||
|
)
|
||||||
|
return msg_auth.hexdigest()
|
||||||
|
|
||||||
async def _api(
|
async def _api(
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
params: dict,
|
params: Union[dict, OrderedDict],
|
||||||
|
signed: bool = False,
|
||||||
|
action: str = 'get'
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
|
if signed:
|
||||||
|
params['signature'] = self._get_signature(params)
|
||||||
|
|
||||||
|
if action == 'get':
|
||||||
resp = await self._sesh.get(
|
resp = await self._sesh.get(
|
||||||
path=f'/api/v3/{method}',
|
path=f'/api/v3/{method}',
|
||||||
params=params,
|
params=params,
|
||||||
timeout=float('inf')
|
timeout=float('inf')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif action == 'post':
|
||||||
|
resp = await self._sesh.post(
|
||||||
|
path=f'/api/v3/{method}',
|
||||||
|
params=params,
|
||||||
|
timeout=float('inf')
|
||||||
|
)
|
||||||
|
|
||||||
return resproc(resp, log)
|
return resproc(resp, log)
|
||||||
|
|
||||||
async def exch_info(
|
async def exch_info(
|
||||||
|
@ -284,8 +354,8 @@ class Client:
|
||||||
async def bars(
|
async def bars(
|
||||||
self,
|
self,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
start_dt: Optional[datetime] = None,
|
start_dt: datetime | None = None,
|
||||||
end_dt: Optional[datetime] = None,
|
end_dt: datetime | None = None,
|
||||||
limit: int = 1000, # <- max allowed per query
|
limit: int = 1000, # <- max allowed per query
|
||||||
as_np: bool = True,
|
as_np: bool = True,
|
||||||
|
|
||||||
|
@ -344,6 +414,60 @@ class Client:
|
||||||
) if as_np else bars
|
) if as_np else bars
|
||||||
return array
|
return array
|
||||||
|
|
||||||
|
async def submit_limit(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
side: str, # SELL / BUY
|
||||||
|
quantity: float,
|
||||||
|
price: float,
|
||||||
|
# time_in_force: str = 'GTC',
|
||||||
|
oid: int | None = None,
|
||||||
|
# iceberg_quantity: float | None = None,
|
||||||
|
# order_resp_type: str | None = None,
|
||||||
|
recv_window: int = 60000
|
||||||
|
|
||||||
|
) -> int:
|
||||||
|
symbol = symbol.upper()
|
||||||
|
|
||||||
|
await self.cache_symbols()
|
||||||
|
|
||||||
|
asset_precision = self._pairs[symbol]['baseAssetPrecision']
|
||||||
|
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([
|
||||||
|
('symbol', symbol),
|
||||||
|
('side', side.upper()),
|
||||||
|
('type', 'LIMIT'),
|
||||||
|
('timeInForce', 'GTC'),
|
||||||
|
('quantity', quantity),
|
||||||
|
('price', price),
|
||||||
|
('recvWindow', recv_window),
|
||||||
|
('newOrderRespType', 'ACK'),
|
||||||
|
('timestamp', binance_timestamp(pendulum.now()))
|
||||||
|
])
|
||||||
|
|
||||||
|
if oid:
|
||||||
|
params['newClientOrderId'] = oid
|
||||||
|
|
||||||
|
resp = await self._api(
|
||||||
|
'order/test', # TODO: switch to real `order` endpoint
|
||||||
|
params=params,
|
||||||
|
signed=True,
|
||||||
|
action='post'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp['orderId'] == oid
|
||||||
|
return oid
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def get_client() -> Client:
|
async def get_client() -> Client:
|
||||||
|
@ -660,6 +784,69 @@ async def stream_quotes(
|
||||||
# last = time.time()
|
# last = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_order_requests(
|
||||||
|
ems_order_stream: tractor.MsgStream
|
||||||
|
) -> None:
|
||||||
|
async with open_cached_client('binance') as client:
|
||||||
|
async for request_msg in ems_order_stream:
|
||||||
|
log.info(f'Received order request {request_msg}')
|
||||||
|
|
||||||
|
action = request_msg['action']
|
||||||
|
|
||||||
|
if action in {'buy', 'sell'}:
|
||||||
|
# validate
|
||||||
|
order = BrokerdOrder(**request_msg)
|
||||||
|
|
||||||
|
# call our client api to submit the order
|
||||||
|
reqid = await client.submit_limit(
|
||||||
|
order.symbol,
|
||||||
|
order.action,
|
||||||
|
order.size,
|
||||||
|
order.price,
|
||||||
|
oid=order.oid
|
||||||
|
)
|
||||||
|
|
||||||
|
# deliver ack that order has been submitted to broker routing
|
||||||
|
await ems_order_stream.send(
|
||||||
|
BrokerdOrderAck(
|
||||||
|
# ems order request id
|
||||||
|
oid=order.oid,
|
||||||
|
# broker specific request id
|
||||||
|
reqid=reqid,
|
||||||
|
time_ns=time.time_ns(),
|
||||||
|
).dict()
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action == 'cancel':
|
||||||
|
# msg = BrokerdCancel(**request_msg)
|
||||||
|
# await run_client_method
|
||||||
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.error(f'Unknown order command: {request_msg}')
|
||||||
|
|
||||||
|
|
||||||
|
@tractor.context
|
||||||
|
async def trades_dialogue(
|
||||||
|
ctx: tractor.Context,
|
||||||
|
loglevel: str = None
|
||||||
|
) -> AsyncIterator[dict[str, Any]]:
|
||||||
|
|
||||||
|
# XXX: required to propagate ``tractor`` loglevel to piker logging
|
||||||
|
get_console_log(loglevel or tractor.current_actor().loglevel)
|
||||||
|
|
||||||
|
positions = {} # TODO: get already open pos
|
||||||
|
|
||||||
|
await ctx.started(positions, {})
|
||||||
|
|
||||||
|
async with (
|
||||||
|
ctx.open_stream() as ems_stream,
|
||||||
|
trio.open_nursery() as n
|
||||||
|
):
|
||||||
|
n.start_soon(handle_order_requests, ems_stream)
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def open_symbol_search(
|
async def open_symbol_search(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
|
|
|
@ -173,6 +173,10 @@ _context_defaults = dict(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NoSignature(Exception):
|
||||||
|
'No credentials setup for broker backend!'
|
||||||
|
|
||||||
|
|
||||||
def _override_config_dir(
|
def _override_config_dir(
|
||||||
path: str
|
path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
Loading…
Reference in New Issue