Support live order loading (with caveats)
As you'd expect query and sync the EMS with existing live orders reported by the market venue by packing them in `Status` msgs and sending over the order dialog stream before starting the handler tasks. XXX CAVEAT: - there appears to be no way (at least on the usdtm market/venue) to distinguish between different contracts such as perps vs. the quarterlies? - for now we just assume that the perp is being used since there's no indicator otherwise in the 'symbol' field? - we should maybe open an issue with the futures-connector project to see how they'd recommend solving this discrepancy?basic_buy_bot
parent
dc3ac8de01
commit
5c315ba163
|
@ -49,6 +49,9 @@ from fuzzywuzzy import process as fuzzy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from piker import config
|
from piker import config
|
||||||
|
from piker.clearing._messages import (
|
||||||
|
Order,
|
||||||
|
)
|
||||||
from piker.accounting import (
|
from piker.accounting import (
|
||||||
Asset,
|
Asset,
|
||||||
digits_to_dec,
|
digits_to_dec,
|
||||||
|
@ -378,9 +381,6 @@ class Client:
|
||||||
raise SymbolNotFound(f'No market pairs found!?:\n{resp}')
|
raise SymbolNotFound(f'No market pairs found!?:\n{resp}')
|
||||||
|
|
||||||
pairs_view_subtable: dict[str, Pair] = {}
|
pairs_view_subtable: dict[str, Pair] = {}
|
||||||
# if venue == 'spot':
|
|
||||||
# import tractor
|
|
||||||
# await tractor.breakpoint()
|
|
||||||
|
|
||||||
for item in mkt_pairs:
|
for item in mkt_pairs:
|
||||||
filters_ls: list = item.pop('filters', False)
|
filters_ls: list = item.pop('filters', False)
|
||||||
|
@ -619,6 +619,68 @@ class Client:
|
||||||
signed=True,
|
signed=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_open_orders(
|
||||||
|
self,
|
||||||
|
symbol: str | None = None,
|
||||||
|
|
||||||
|
) -> list[Order]:
|
||||||
|
'''
|
||||||
|
Get all open orders for venue-account.
|
||||||
|
|
||||||
|
WARNING: apparently not specifying the symbol is given
|
||||||
|
a much heavier API "weight" meaning you shouldn't call it
|
||||||
|
often to avoid getting throttled as per:
|
||||||
|
|
||||||
|
'https://binance-docs.github.io/apidocs/futures/en/#current-all-open-orders-user_data
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
'timestamp': binance_timestamp(now()),
|
||||||
|
}
|
||||||
|
if symbol is not None:
|
||||||
|
params['symbol'] = symbol
|
||||||
|
|
||||||
|
resp = await self.mkt_mode_req[self.mkt_mode](
|
||||||
|
'openOrders',
|
||||||
|
params=params,
|
||||||
|
signed=True,
|
||||||
|
action='get',
|
||||||
|
)
|
||||||
|
orders: list[Order] = []
|
||||||
|
for entry in resp:
|
||||||
|
oid: str = entry['clientOrderId']
|
||||||
|
|
||||||
|
# XXX TODO XXX: it appears as though entries have no
|
||||||
|
# indicator from the symbology system which market
|
||||||
|
# / venue the order is from.. which normally isn't
|
||||||
|
# a huge deal since you could assume based on the
|
||||||
|
# endpoint you made the request to, BUT the futes USD-M
|
||||||
|
# endpoints have multiple contracts for the same
|
||||||
|
# symbols (eg. BTCUSDT.PERP, BTCUSDT.230630.. etc.)
|
||||||
|
# NOTE: for now until we have a better system we're
|
||||||
|
# going to assume orders that don't have some kind of
|
||||||
|
# further info in the order resp dict are perps though
|
||||||
|
# likely this will need to change in the future..
|
||||||
|
venue: str = self.mkt_mode.rstrip('_futes')
|
||||||
|
bs_mktid: str = entry['symbol']
|
||||||
|
fqme: str = f'{bs_mktid.lower()}.{venue}.perp'
|
||||||
|
|
||||||
|
orders.append(
|
||||||
|
Order(
|
||||||
|
oid=oid,
|
||||||
|
symbol=fqme,
|
||||||
|
|
||||||
|
action=entry['side'].lower(),
|
||||||
|
price=float(entry['price']),
|
||||||
|
size=float(entry['origQty']),
|
||||||
|
|
||||||
|
exec_mode='live',
|
||||||
|
account=f'binance.{venue}',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return orders
|
||||||
|
|
||||||
async def submit_limit(
|
async def submit_limit(
|
||||||
self,
|
self,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
|
|
|
@ -62,6 +62,8 @@ from piker.clearing._messages import (
|
||||||
BrokerdFill,
|
BrokerdFill,
|
||||||
BrokerdCancel,
|
BrokerdCancel,
|
||||||
BrokerdError,
|
BrokerdError,
|
||||||
|
Status,
|
||||||
|
Order,
|
||||||
)
|
)
|
||||||
from .venues import Pair
|
from .venues import Pair
|
||||||
from .api import Client
|
from .api import Client
|
||||||
|
@ -69,6 +71,10 @@ from .api import Client
|
||||||
log = get_logger('piker.brokers.binance')
|
log = get_logger('piker.brokers.binance')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: factor this into `.clearing._util` (or something)
|
||||||
|
# and use in other backends like kraken which currently has
|
||||||
|
# a less formalized version more or less:
|
||||||
|
# `apiflows[reqid].maps.append(status_msg.to_dict())`
|
||||||
class OrderDialogs(Struct):
|
class OrderDialogs(Struct):
|
||||||
'''
|
'''
|
||||||
Order control dialog (and thus transaction) tracking via
|
Order control dialog (and thus transaction) tracking via
|
||||||
|
@ -79,25 +85,49 @@ class OrderDialogs(Struct):
|
||||||
state using the entire (reverse chronological) msg flow.
|
state using the entire (reverse chronological) msg flow.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
_dialogs: defaultdict[str, ChainMap] = defaultdict(ChainMap)
|
_flows: dict[str, ChainMap] = {}
|
||||||
|
|
||||||
def add_msg(
|
def add_msg(
|
||||||
self,
|
self,
|
||||||
oid: str,
|
oid: str,
|
||||||
msg: dict,
|
msg: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._dialogs[oid].maps.insert(0, msg)
|
|
||||||
|
# NOTE: manually enter a new map on the first msg add to
|
||||||
|
# avoid creating one with an empty dict first entry in
|
||||||
|
# `ChainMap.maps` which is the default if none passed at
|
||||||
|
# init.
|
||||||
|
cm: ChainMap = self._flows.get(oid)
|
||||||
|
if cm:
|
||||||
|
cm.maps.insert(0, msg)
|
||||||
|
else:
|
||||||
|
cm = ChainMap(msg)
|
||||||
|
self._flows[oid] = cm
|
||||||
|
|
||||||
# TODO: wrap all this in the `collections.abc.Mapping` interface?
|
# TODO: wrap all this in the `collections.abc.Mapping` interface?
|
||||||
def get(
|
def get(
|
||||||
self,
|
self,
|
||||||
oid: str,
|
oid: str,
|
||||||
|
|
||||||
) -> ChainMap[str, Any]:
|
) -> ChainMap[str, Any]:
|
||||||
'''
|
'''
|
||||||
Return the dialog `ChainMap` for provided id.
|
Return the dialog `ChainMap` for provided id.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
return self._dialogs.get(oid, None)
|
return self._flows.get(oid, None)
|
||||||
|
|
||||||
|
def pop(
|
||||||
|
self,
|
||||||
|
oid: str,
|
||||||
|
|
||||||
|
) -> ChainMap[str, Any]:
|
||||||
|
'''
|
||||||
|
Pop and thus remove the `ChainMap` containing the msg flow
|
||||||
|
for the given order id.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return self._flows.pop(oid)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_order_requests(
|
async def handle_order_requests(
|
||||||
|
@ -277,11 +307,15 @@ async def open_trade_dialog(
|
||||||
f"{listen_key}@account",
|
f"{listen_key}@account",
|
||||||
f"{listen_key}@balance",
|
f"{listen_key}@balance",
|
||||||
f"{listen_key}@position",
|
f"{listen_key}@position",
|
||||||
|
|
||||||
|
# TODO: does this even work!? seems to cause
|
||||||
|
# a hang on the first msg..? lelelel.
|
||||||
|
# f"{listen_key}@order",
|
||||||
],
|
],
|
||||||
"id": nsid
|
"id": nsid
|
||||||
})
|
})
|
||||||
|
|
||||||
with trio.fail_after(1):
|
with trio.fail_after(6):
|
||||||
msg = await wss.recv_msg()
|
msg = await wss.recv_msg()
|
||||||
assert msg['id'] == nsid
|
assert msg['id'] == nsid
|
||||||
|
|
||||||
|
@ -401,6 +435,24 @@ async def open_trade_dialog(
|
||||||
trio.open_nursery() as tn,
|
trio.open_nursery() as tn,
|
||||||
ctx.open_stream() as ems_stream,
|
ctx.open_stream() as ems_stream,
|
||||||
):
|
):
|
||||||
|
# deliver all pre-exist open orders to EMS thus syncing
|
||||||
|
# state with the binance existing live limit set.
|
||||||
|
open_orders: list[Order] = await client.get_open_orders()
|
||||||
|
|
||||||
|
# fill out `Status` with boxed `Order`s and sync the EMS.
|
||||||
|
for order in open_orders:
|
||||||
|
status_msg = Status(
|
||||||
|
time_ns=time.time_ns(),
|
||||||
|
resp='open',
|
||||||
|
oid=order.oid,
|
||||||
|
reqid=order.oid,
|
||||||
|
|
||||||
|
# embedded order info
|
||||||
|
req=order,
|
||||||
|
src='binance',
|
||||||
|
)
|
||||||
|
dialogs.add_msg(order.oid, order.to_dict())
|
||||||
|
await ems_stream.send(status_msg)
|
||||||
|
|
||||||
tn.start_soon(
|
tn.start_soon(
|
||||||
handle_order_requests,
|
handle_order_requests,
|
||||||
|
@ -565,16 +617,14 @@ async def handle_order_updates(
|
||||||
|
|
||||||
if accum_size_filled == req_size:
|
if accum_size_filled == req_size:
|
||||||
status = 'closed'
|
status = 'closed'
|
||||||
del dialogs._dialogs[oid]
|
dialogs.pop(oid)
|
||||||
|
|
||||||
case 'NEW':
|
case 'NEW':
|
||||||
status = 'open'
|
status = 'open'
|
||||||
|
|
||||||
case 'EXPIRED':
|
case 'EXPIRED':
|
||||||
status = 'canceled'
|
status = 'canceled'
|
||||||
del dialogs._dialogs[oid]
|
dialogs.pop(oid)
|
||||||
|
|
||||||
# case 'TRADE':
|
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
status = status.lower()
|
status = status.lower()
|
||||||
|
|
Loading…
Reference in New Issue