ib: relay submission errors, allow adhoc mkt overrides
This is a tricky edge case we weren't handling prior; an example is submitting a limit order with a price tick precision which mismatches that supported (probably bc IB reported the wrong one..) and IB responds immediately with an error event (via a special code..) but doesn't include any `Trade` object(s) nor details beyond the `reqid`. So, we have to do a little reverse EMS order lookup on our own and ideally indicate to the requester which order failed and *why*. To enable this we, - create a `flows: OrderDialogs` instance and pass it to most order/event relay tasks, particularly ensuring we update update ASAP in `handle_order_requests()` such that any successful submit has an `Ack` recorded in the flow. - on such errors lookup the `.symbol` / `Order` from the `flow` and respond back to the EMS with as many details as possible about the prior msg history. - always explicitly relay `error` events which don't fall into the sensible filtered set and wrap in a `BrokerdError.broker_details['flow']: dict` snapshot for the EMS. - in `symbols.get_mkt_info()` support adhoc lookup for `MktPair` inputs and when defined we re-construct with those inputs; in this case we do this for a first mkt: `'vtgn.nasdaq'`..account_tests
parent
562d027ee6
commit
f66a1f8b23
|
@ -20,6 +20,7 @@ Order and trades endpoints for use with ``piker``'s EMS.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from contextlib import ExitStack
|
||||
from collections import ChainMap
|
||||
from functools import partial
|
||||
from pprint import pformat
|
||||
import time
|
||||
|
@ -135,18 +136,19 @@ async def handle_order_requests(
|
|||
action: str = request_msg['action']
|
||||
account: str = request_msg['account']
|
||||
acct_number = accounts_def.get(account)
|
||||
oid: str = request_msg['oid']
|
||||
|
||||
if not acct_number:
|
||||
log.error(
|
||||
f'An IB account number for name {account} is not found?\n'
|
||||
'Make sure you have all TWS and GW instances running.'
|
||||
)
|
||||
await ems_order_stream.send(
|
||||
BrokerdError(
|
||||
oid=request_msg['oid'],
|
||||
symbol=request_msg['symbol'],
|
||||
reason=f'No account found: `{account}` ?',
|
||||
)
|
||||
err_msg = BrokerdError(
|
||||
oid=oid,
|
||||
symbol=request_msg['symbol'],
|
||||
reason=f'No account found: `{account}` ?',
|
||||
)
|
||||
await ems_order_stream.send(err_msg)
|
||||
continue
|
||||
|
||||
client = _accounts2clients.get(account)
|
||||
|
@ -155,11 +157,12 @@ async def handle_order_requests(
|
|||
f'An IB client for account name {account} is not found.\n'
|
||||
'Make sure you have all TWS and GW instances running.'
|
||||
)
|
||||
await ems_order_stream.send(BrokerdError(
|
||||
oid=request_msg['oid'],
|
||||
err_msg = BrokerdError(
|
||||
oid=oid,
|
||||
symbol=request_msg['symbol'],
|
||||
reason=f'No api client loaded for account: `{account}` ?',
|
||||
))
|
||||
)
|
||||
await ems_order_stream.send(err_msg)
|
||||
continue
|
||||
|
||||
if action in {'buy', 'sell'}:
|
||||
|
@ -185,23 +188,26 @@ async def handle_order_requests(
|
|||
account=acct_number,
|
||||
reqid=reqid,
|
||||
)
|
||||
str_reqid: str = str(reqid)
|
||||
if reqid is None:
|
||||
await ems_order_stream.send(BrokerdError(
|
||||
oid=request_msg['oid'],
|
||||
err_msg = BrokerdError(
|
||||
oid=oid,
|
||||
symbol=request_msg['symbol'],
|
||||
reason='Order already active?',
|
||||
))
|
||||
)
|
||||
await ems_order_stream.send(err_msg)
|
||||
|
||||
# deliver ack that order has been submitted to broker routing
|
||||
ack = BrokerdOrderAck(
|
||||
# ems order request id
|
||||
oid=order.oid,
|
||||
# broker specific request id
|
||||
reqid=reqid,
|
||||
reqid=str_reqid,
|
||||
account=account,
|
||||
)
|
||||
await ems_order_stream.send(ack)
|
||||
flows.add_msg(reqid, ack.to_dict())
|
||||
flows.add_msg(str_reqid, order.to_dict())
|
||||
flows.add_msg(str_reqid, ack.to_dict())
|
||||
|
||||
elif action == 'cancel':
|
||||
msg = BrokerdCancel(**request_msg)
|
||||
|
@ -441,7 +447,7 @@ async def aggr_open_orders(
|
|||
deats = await proxy.con_deats(contracts=[con])
|
||||
fqme = list(deats)[0]
|
||||
|
||||
reqid = order.orderId
|
||||
reqid: str = str(order.orderId)
|
||||
|
||||
# TODO: maybe embed a ``BrokerdOrder`` instead
|
||||
# since then we can directly load it on the client
|
||||
|
@ -449,7 +455,7 @@ async def aggr_open_orders(
|
|||
msg = Status(
|
||||
time_ns=time.time_ns(),
|
||||
resp='open',
|
||||
oid=str(reqid),
|
||||
oid=reqid,
|
||||
reqid=reqid,
|
||||
|
||||
# embedded order info
|
||||
|
@ -1213,6 +1219,8 @@ async def deliver_trade_events(
|
|||
# `Client.inline_errors()::push_err()`
|
||||
err: dict = item
|
||||
|
||||
# never relay errors for non-broker related issues
|
||||
# https://interactivebrokers.github.io/tws-api/message_codes.html
|
||||
code: int = err['error_code']
|
||||
if code in {
|
||||
200, # uhh
|
||||
|
@ -1221,43 +1229,42 @@ async def deliver_trade_events(
|
|||
162,
|
||||
165,
|
||||
|
||||
# WARNING codes:
|
||||
# https://interactivebrokers.github.io/tws-api/message_codes.html#warning_codes
|
||||
# Attribute 'Outside Regular Trading Hours' is
|
||||
# " 'ignored based on the order type and
|
||||
# destination. PlaceOrder is now ' 'being
|
||||
# processed.',
|
||||
2109,
|
||||
|
||||
# XXX: lol this isn't even documented..
|
||||
# 'No market data during competing live session'
|
||||
1669,
|
||||
}:
|
||||
continue
|
||||
|
||||
reqid: str = err['reqid']
|
||||
acnt: str = flows.get(reqid)['account']
|
||||
reqid: str = str(err['reqid'])
|
||||
reason: str = err['reason']
|
||||
|
||||
if err['reqid'] == -1:
|
||||
log.error(f'TWS external order error:\n{pformat(err)}')
|
||||
|
||||
flow: ChainMap = flows.get(reqid)
|
||||
|
||||
# TODO: we don't want to relay data feed / lookup errors
|
||||
# so we need some further filtering logic here..
|
||||
# for most cases the 'status' block above should take
|
||||
# care of this.
|
||||
await ems_stream.send(
|
||||
BrokerdStatus(
|
||||
status='error',
|
||||
reqid=reqid,
|
||||
reason=reason,
|
||||
time_ns=time.time_ns(),
|
||||
account=acnt,
|
||||
broker_details={'name': 'ib'},
|
||||
)
|
||||
)
|
||||
|
||||
canceled = BrokerdStatus(
|
||||
err_msg = BrokerdError(
|
||||
reqid=reqid,
|
||||
time_ns=time.time_ns(), # cuz why not
|
||||
status='canceled',
|
||||
reason=reason,
|
||||
account=acnt,
|
||||
broker_details={'name': 'ib'},
|
||||
broker_details={
|
||||
'name': 'ib',
|
||||
'flow': dict(flow),
|
||||
},
|
||||
)
|
||||
await ems_stream.send(canceled)
|
||||
flows.add_msg(reqid, canceled.to_dict())
|
||||
flows.add_msg(reqid, err_msg.to_dict())
|
||||
await ems_stream.send(err_msg)
|
||||
|
||||
case 'event':
|
||||
|
||||
|
|
|
@ -132,6 +132,12 @@ _adhoc_fiat_set = set((
|
|||
).split(' ,')
|
||||
)
|
||||
|
||||
# manually discovered tick discrepancies,
|
||||
# onl god knows how or why they'd cuck these up..
|
||||
_adhoc_mkt_infos: dict[int | str, dict] = {
|
||||
'vtgn.nasdaq': {'price_tick': Decimal('0.01')},
|
||||
}
|
||||
|
||||
|
||||
# map of symbols to contract ids
|
||||
_adhoc_symbol_map = {
|
||||
|
@ -511,6 +517,7 @@ async def get_mkt_info(
|
|||
venue = con.primaryExchange or con.exchange
|
||||
|
||||
price_tick: Decimal = Decimal(str(details.minTick))
|
||||
# price_tick: Decimal = Decimal('0.01')
|
||||
|
||||
if atype == 'stock':
|
||||
# XXX: GRRRR they don't support fractional share sizes for
|
||||
|
@ -541,14 +548,15 @@ async def get_mkt_info(
|
|||
atype='fiat',
|
||||
tx_tick=Decimal('0.01'), # right?
|
||||
)
|
||||
dst = Asset(
|
||||
name=con.symbol.lower(),
|
||||
atype=atype,
|
||||
tx_tick=size_tick,
|
||||
)
|
||||
|
||||
mkt = MktPair(
|
||||
dst=Asset(
|
||||
name=con.symbol.lower(),
|
||||
atype=atype,
|
||||
tx_tick=size_tick,
|
||||
),
|
||||
src=src,
|
||||
dst=dst,
|
||||
|
||||
price_tick=price_tick,
|
||||
size_tick=size_tick,
|
||||
|
@ -563,6 +571,15 @@ async def get_mkt_info(
|
|||
_fqme_without_src=(atype != 'fiat'),
|
||||
)
|
||||
|
||||
# just.. wow.
|
||||
if entry := _adhoc_mkt_infos.get(mkt.bs_fqme):
|
||||
log.warning(f'Frickin {mkt.fqme} has an adhoc {entry}..')
|
||||
new = mkt.to_dict()
|
||||
new['price_tick'] = entry['price_tick']
|
||||
new['src'] = src
|
||||
new['dst'] = dst
|
||||
mkt = MktPair(**new)
|
||||
|
||||
# if possible register the bs_mktid to the just-built
|
||||
# mkt so that it can be retreived by order mode tasks later.
|
||||
# TODO NOTE: this is going to be problematic if/when we split
|
||||
|
|
Loading…
Reference in New Issue