diff --git a/piker/clearing/_ems.py b/piker/clearing/_ems.py index af5fe690..6547fb1e 100644 --- a/piker/clearing/_ems.py +++ b/piker/clearing/_ems.py @@ -76,7 +76,6 @@ if TYPE_CHECKING: # TODO: numba all of this def mk_check( - trigger_price: float, known_last: float, action: str, @@ -1190,12 +1189,16 @@ async def process_client_order_cmds( submitting live orders immediately if requested by the client. ''' - # cmd: dict + # TODO, only allow `msgspec.Struct` form! + cmd: dict async for cmd in client_order_stream: - log.info(f'Received order cmd:\n{pformat(cmd)}') + log.info( + f'Received order cmd:\n' + f'{pformat(cmd)}\n' + ) # CAWT DAMN we need struct support! - oid = str(cmd['oid']) + oid: str = str(cmd['oid']) # register this stream as an active order dialog (msg flow) for # this order id such that translated message from the brokerd @@ -1301,7 +1304,7 @@ async def process_client_order_cmds( case { 'oid': oid, 'symbol': fqme, - 'price': trigger_price, + 'price': price, 'size': size, 'action': ('buy' | 'sell') as action, 'exec_mode': ('live' | 'paper'), @@ -1333,7 +1336,7 @@ async def process_client_order_cmds( symbol=sym, action=action, - price=trigger_price, + price=price, size=size, account=req.account, ) @@ -1355,7 +1358,11 @@ async def process_client_order_cmds( # (``translate_and_relay_brokerd_events()`` above) will # handle relaying the ems side responses back to # the client/cmd sender from this request - log.info(f'Sending live order to {broker}:\n{pformat(msg)}') + log.info( + f'Sending live order to {broker}:\n' + f'{pformat(msg)}' + ) + await brokerd_order_stream.send(msg) # an immediate response should be ``BrokerdOrderAck`` @@ -1371,7 +1378,7 @@ async def process_client_order_cmds( case { 'oid': oid, 'symbol': fqme, - 'price': trigger_price, + 'price': price, 'size': size, 'exec_mode': exec_mode, 'action': action, @@ -1399,7 +1406,12 @@ async def process_client_order_cmds( if isnan(last): last = flume.rt_shm.array[-1]['close'] - pred = mk_check(trigger_price, last, action) + trigger_price: float = float(price) + pred = mk_check( + trigger_price, + last, + action, + ) # NOTE: for dark orders currently we submit # the triggered live order at a price 5 ticks @@ -1539,7 +1551,7 @@ async def _emsd_main( ctx: tractor.Context, fqme: str, exec_mode: str, # ('paper', 'live') - loglevel: str | None = None, + loglevel: str|None = None, ) -> tuple[ dict[ diff --git a/piker/clearing/_messages.py b/piker/clearing/_messages.py index 51a3860c..788fe669 100644 --- a/piker/clearing/_messages.py +++ b/piker/clearing/_messages.py @@ -19,6 +19,7 @@ Clearing sub-system message and protocols. """ from __future__ import annotations +from decimal import Decimal from typing import ( Literal, ) @@ -71,7 +72,15 @@ class Order(Struct): symbol: str # | MktPair account: str # should we set a default as '' ? - price: float + # https://docs.python.org/3/library/decimal.html#decimal-objects + # + # ?TODO? decimal usage throughout? + # -[ ] possibly leverage the `Encoder(decimal_format='number')` + # bit? + # |_https://jcristharif.com/msgspec/supported-types.html#decimal + # -[ ] should we also use it for .size? + # + price: Decimal size: float # -ve is "sell", +ve is "buy" brokers: list[str] = [] @@ -178,7 +187,7 @@ class BrokerdOrder(Struct): time_ns: int symbol: str # fqme - price: float + price: Decimal size: float # TODO: if we instead rely on a +ve/-ve size to determine diff --git a/piker/clearing/_paper_engine.py b/piker/clearing/_paper_engine.py index 60835598..e9daf1a5 100644 --- a/piker/clearing/_paper_engine.py +++ b/piker/clearing/_paper_engine.py @@ -510,7 +510,7 @@ async def handle_order_requests( reqid = await client.submit_limit( oid=order.oid, symbol=f'{order.symbol}.{client.broker}', - price=order.price, + price=float(order.price), action=order.action, size=order.size, # XXX: by default 0 tells ``ib_insync`` methods that diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index d5720e8a..47a3bb97 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -21,6 +21,7 @@ Chart trading, the only way to scalp. from __future__ import annotations from contextlib import asynccontextmanager from dataclasses import dataclass, field +from decimal import Decimal from functools import partial from pprint import pformat import time @@ -41,7 +42,6 @@ from piker.accounting import ( Position, mk_allocator, MktPair, - Symbol, ) from piker.clearing import ( open_ems, @@ -143,6 +143,15 @@ class OrderMode: } _staged_order: Order | None = None + @property + def curr_mkt(self) -> MktPair: + ''' + Deliver the currently selected `MktPair` according + chart state. + + ''' + return self.chart.linked.mkt + def on_level_change_update_next_order_info( self, level: float, @@ -172,7 +181,11 @@ class OrderMode: line.update_labels(order_info) # update bound-in staged order - order.price = level + mkt: MktPair = self.curr_mkt + order.price: Decimal = mkt.quantize( + size=level, + quantity_type='price', + ) order.size = order_info['size'] # when an order is changed we flip the settings side-pane to @@ -187,7 +200,9 @@ class OrderMode: ) -> LevelLine: - level = order.price + # TODO, if we instead just always decimalize at the ems layer + # we can avoid this back-n-forth casting? + level = float(order.price) line = order_line( chart or self.chart, @@ -224,7 +239,11 @@ class OrderMode: # the order mode allocator but we still need to update the # "staged" order message we'll send to the ems def update_order_price(y: float) -> None: - order.price = y + mkt: MktPair = self.curr_mkt + order.price: Decimal = mkt.quantize( + size=y, + quantity_type='price', + ) line._on_level_change = update_order_price @@ -275,34 +294,31 @@ class OrderMode: chart = cursor.linked.chart if ( not chart - and cursor - and cursor.active_plot + and + cursor + and + cursor.active_plot ): return chart = cursor.active_plot - price = cursor._datum_xy[1] + price: float = cursor._datum_xy[1] if not price: # zero prices are not supported by any means # since that's illogical / a no-op. return - mkt: MktPair = self.chart.linked.mkt - - # NOTE : we could also use instead, - # mkt.quantize(price, quantity_type='price') - # but it returns a Decimal and it's probably gonna - # be slower? # TODO: should we be enforcing this precision - # at a different layer in the stack? right now - # any precision error will literally be relayed - # all the way back from the backend. - - price = round( - price, - ndigits=mkt.price_tick_digits, + # at a different layer in the stack? + # |_ might require `MktPair` tracking in the EMS? + # |_ right now any precision error will be relayed + # all the way back from the backend and vice-versa.. + # + mkt: MktPair = self.curr_mkt + price: Decimal = mkt.quantize( + size=price, + quantity_type='price', ) - order = self._staged_order = Order( action=action, price=price, @@ -378,7 +394,7 @@ class OrderMode: 'oid': oid, }) - if order.price <= 0: + if float(order.price) <= 0: log.error( '*!? Invalid `Order.price <= 0` ?!*\n' # TODO: make this present multi-line in object form @@ -515,14 +531,15 @@ class OrderMode: # if an order msg is provided update the line # **from** that msg. if order: - if order.price <= 0: + price: float = float(order.price) + if price <= 0: log.error(f'Order has 0 price, cancelling..\n{order}') self.cancel_orders([order.oid]) return None - line.set_level(order.price) + line.set_level(price) self.on_level_change_update_next_order_info( - level=order.price, + level=price, line=line, order=order, # use the corresponding position tracker for the @@ -681,9 +698,9 @@ class OrderMode: ) -> Dialog | None: # NOTE: the `.order` attr **must** be set with the # equivalent order msg in order to be loaded. - order = msg.req + order: Order = msg.req oid = str(msg.oid) - symbol = order.symbol + symbol: str = order.symbol # TODO: MEGA UGGG ZONEEEE! src = msg.src @@ -702,13 +719,22 @@ class OrderMode: order.oid = str(order.oid) order.brokers = [brokername] - # TODO: change this over to `MktPair`, but it's - # gonna be tough since we don't have any such data - # really in our clearing msg schema.. - order.symbol = Symbol.from_fqme( - fqsn=fqme, - info={}, - ) + # ?TODO? change this over to `MktPair`, but it's gonna be + # tough since we don't have any such data really in our + # clearing msg schema.. + # BUT WAIT! WHY do we even want/need this!? + # + # order.symbol = self.curr_mkt + # + # XXX, the old approach.. which i don't quire member why.. + # -[ ] verify we for sure don't require this any more! + # |_https://github.com/pikers/piker/issues/517 + # + # order.symbol = Symbol.from_fqme( + # fqsn=fqme, + # info={}, + # ) + maybe_dialog: Dialog | None = self.submit_order( send_msg=False, order=order, @@ -1101,7 +1127,7 @@ async def process_trade_msg( ) ) ): - msg.req = order + msg.req: Order = order dialog: ( Dialog # NOTE: on an invalid order submission (eg. @@ -1166,7 +1192,7 @@ async def process_trade_msg( tm = time.time() mode.on_fill( oid, - price=req.price, + price=float(req.price), time_s=tm, ) mode.lines.remove_line(uuid=oid) @@ -1221,7 +1247,7 @@ async def process_trade_msg( tm = details['broker_time'] mode.on_fill( oid, - price=details['price'], + price=float(details['price']), time_s=tm, pointing='up' if action == 'buy' else 'down', ) diff --git a/tests/test_ems.py b/tests/test_ems.py index c2f5d7a8..4a9e4a4c 100644 --- a/tests/test_ems.py +++ b/tests/test_ems.py @@ -179,7 +179,7 @@ def test_ems_err_on_bad_broker( # NOTE: emsd should error on the actor's enabled modules # import phase, when looking for a backend named `doggy`. except tractor.RemoteActorError as re: - assert re.type == ModuleNotFoundError + assert re.type is ModuleNotFoundError run_and_tollerate_cancels(load_bad_fqme)