Async-ify order client methods and some renaming
We previously only offered a sync API (which was recently renamed to `.<meth>_nowait()` style) since initially all order control was from our `OrderMode` Qt driven UI/UX. This adds the equivalent async methods for both testing as well as eventual auto-strat driven control B) Also includes a bunch of renaming: - `OrderBook` -> `OrderClient`. - better internal renaming of the client's mem chan vars and add a ref `._ems_stream: tractor.MsgStream`. - drop `get_orders()` factory, just always check for the actor-global instance and always set the ems stream on that client (in case old one was closed).pre_overruns_ctxcancelled
							parent
							
								
									5cb63a67e1
								
							
						
					
					
						commit
						b8d7c05d74
					
				| 
						 | 
				
			
			@ -46,70 +46,86 @@ if TYPE_CHECKING:
 | 
			
		|||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderBook(Struct):
 | 
			
		||||
    '''EMS-client-side order book ctl and tracking.
 | 
			
		||||
class OrderClient(Struct):
 | 
			
		||||
    '''
 | 
			
		||||
    EMS-client-side order book ctl and tracking.
 | 
			
		||||
 | 
			
		||||
    A style similar to "model-view" is used here where this api is
 | 
			
		||||
    provided as a supervised control for an EMS actor which does all the
 | 
			
		||||
    hard/fast work of talking to brokers/exchanges to conduct
 | 
			
		||||
    executions.
 | 
			
		||||
 | 
			
		||||
    Currently, this is mostly for keeping local state to match the EMS
 | 
			
		||||
    and use received events to trigger graphics updates.
 | 
			
		||||
    (A)sync API for submitting orders and alerts to the `emsd` service;
 | 
			
		||||
    this is the main control for execution management from client code.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    # IPC stream to `emsd` actor
 | 
			
		||||
    _ems_stream: tractor.MsgStream
 | 
			
		||||
 | 
			
		||||
    # mem channels used to relay order requests to the EMS daemon
 | 
			
		||||
    _to_ems: trio.abc.SendChannel
 | 
			
		||||
    _from_order_book: trio.abc.ReceiveChannel
 | 
			
		||||
    _to_relay_task: trio.abc.SendChannel
 | 
			
		||||
    _from_sync_order_client: trio.abc.ReceiveChannel
 | 
			
		||||
 | 
			
		||||
    # history table
 | 
			
		||||
    _sent_orders: dict[str, Order] = {}
 | 
			
		||||
 | 
			
		||||
    def send_nowait(
 | 
			
		||||
        self,
 | 
			
		||||
        msg: Order | dict,
 | 
			
		||||
 | 
			
		||||
    ) -> dict:
 | 
			
		||||
    ) -> dict | Order:
 | 
			
		||||
        '''
 | 
			
		||||
        Sync version of ``.send()``.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        self._sent_orders[msg.oid] = msg
 | 
			
		||||
        self._to_ems.send_nowait(msg)
 | 
			
		||||
        self._to_relay_task.send_nowait(msg)
 | 
			
		||||
        return msg
 | 
			
		||||
 | 
			
		||||
    # TODO: make this an async version..
 | 
			
		||||
    def send(
 | 
			
		||||
    async def send(
 | 
			
		||||
        self,
 | 
			
		||||
        msg: Order | dict,
 | 
			
		||||
 | 
			
		||||
    ) -> dict:
 | 
			
		||||
        log.warning('USE `.send_nowait()` instead!')
 | 
			
		||||
        return self.send_nowait(msg)
 | 
			
		||||
    ) -> dict | Order:
 | 
			
		||||
        '''
 | 
			
		||||
        Send a new order msg async to the `emsd` service.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        self._sent_orders[msg.oid] = msg
 | 
			
		||||
        await self._ems_stream.send(msg)
 | 
			
		||||
        return msg
 | 
			
		||||
 | 
			
		||||
    def update_nowait(
 | 
			
		||||
        self,
 | 
			
		||||
 | 
			
		||||
        uuid: str,
 | 
			
		||||
        **data: dict,
 | 
			
		||||
 | 
			
		||||
    ) -> dict:
 | 
			
		||||
        '''
 | 
			
		||||
        Sync version of ``.update()``.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        cmd = self._sent_orders[uuid]
 | 
			
		||||
        msg = cmd.copy(update=data)
 | 
			
		||||
        self._sent_orders[uuid] = msg
 | 
			
		||||
        self._to_ems.send_nowait(msg)
 | 
			
		||||
        return cmd
 | 
			
		||||
        self._to_relay_task.send_nowait(msg)
 | 
			
		||||
        return msg
 | 
			
		||||
 | 
			
		||||
    # TODO: async meth for this!
 | 
			
		||||
    # def update(
 | 
			
		||||
    #     self,
 | 
			
		||||
    #     uuid: str,
 | 
			
		||||
    #     **data: dict,
 | 
			
		||||
    # ) -> dict:
 | 
			
		||||
    #     ...
 | 
			
		||||
 | 
			
		||||
    def cancel_nowait(
 | 
			
		||||
    async def update(
 | 
			
		||||
        self,
 | 
			
		||||
        uuid: str,
 | 
			
		||||
    ) -> bool:
 | 
			
		||||
        **data: dict,
 | 
			
		||||
    ) -> dict:
 | 
			
		||||
        '''
 | 
			
		||||
        Cancel an order (or alert) in the EMS.
 | 
			
		||||
        Update an existing order dialog with a msg updated from
 | 
			
		||||
        ``update`` kwargs.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        cmd = self._sent_orders[uuid]
 | 
			
		||||
        msg = cmd.copy(update=data)
 | 
			
		||||
        self._sent_orders[uuid] = msg
 | 
			
		||||
        await self._ems_stream.send(msg)
 | 
			
		||||
        return msg
 | 
			
		||||
 | 
			
		||||
    def _mk_cancel_msg(
 | 
			
		||||
        self,
 | 
			
		||||
        uuid: str,
 | 
			
		||||
    ) -> Cancel:
 | 
			
		||||
        cmd = self._sent_orders.get(uuid)
 | 
			
		||||
        if not cmd:
 | 
			
		||||
            log.error(
 | 
			
		||||
| 
						 | 
				
			
			@ -118,85 +134,76 @@ class OrderBook(Struct):
 | 
			
		|||
                f'You should report this as a bug!'
 | 
			
		||||
            )
 | 
			
		||||
        fqme = str(cmd.symbol)
 | 
			
		||||
        msg = Cancel(
 | 
			
		||||
        return Cancel(
 | 
			
		||||
            oid=uuid,
 | 
			
		||||
            symbol=fqme,
 | 
			
		||||
        )
 | 
			
		||||
        self._to_ems.send_nowait(msg)
 | 
			
		||||
 | 
			
		||||
    # TODO: make this an async version..
 | 
			
		||||
    def cancel(
 | 
			
		||||
    def cancel_nowait(
 | 
			
		||||
        self,
 | 
			
		||||
        uuid: str,
 | 
			
		||||
    ) -> bool:
 | 
			
		||||
        log.warning('USE `.cancel_nowait()` instead!')
 | 
			
		||||
        return self.cancel_nowait(uuid)
 | 
			
		||||
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        '''
 | 
			
		||||
        Sync version of ``.cancel()``.
 | 
			
		||||
 | 
			
		||||
_orders: OrderBook = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_orders(
 | 
			
		||||
    emsd_uid: tuple[str, str] = None
 | 
			
		||||
) -> OrderBook:
 | 
			
		||||
    """"
 | 
			
		||||
    OrderBook singleton factory per actor.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    if emsd_uid is not None:
 | 
			
		||||
        # TODO: read in target emsd's active book on startup
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    global _orders
 | 
			
		||||
 | 
			
		||||
    if _orders is None:
 | 
			
		||||
        size = 100
 | 
			
		||||
        tx, rx = trio.open_memory_channel(size)
 | 
			
		||||
        brx = broadcast_receiver(rx, size)
 | 
			
		||||
 | 
			
		||||
        # setup local ui event streaming channels for request/resp
 | 
			
		||||
        # streamging with EMS daemon
 | 
			
		||||
        _orders = OrderBook(
 | 
			
		||||
            _to_ems=tx,
 | 
			
		||||
            _from_order_book=brx,
 | 
			
		||||
        '''
 | 
			
		||||
        self._to_relay_task.send_nowait(
 | 
			
		||||
            self._mk_cancel_msg(uuid)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return _orders
 | 
			
		||||
    async def cancel(
 | 
			
		||||
        self,
 | 
			
		||||
        uuid: str,
 | 
			
		||||
 | 
			
		||||
    ) -> bool:
 | 
			
		||||
        '''
 | 
			
		||||
        Cancel an already existintg order (or alert) dialog.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        await self._ems_stream.send(
 | 
			
		||||
            self._mk_cancel_msg(uuid)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: we can get rid of this relay loop once we move
 | 
			
		||||
# order_mode inputs to async code!
 | 
			
		||||
async def relay_order_cmds_from_sync_code(
 | 
			
		||||
_client: OrderClient = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def relay_orders_from_sync_code(
 | 
			
		||||
 | 
			
		||||
    client: OrderClient,
 | 
			
		||||
    symbol_key: str,
 | 
			
		||||
    to_ems_stream: tractor.MsgStream,
 | 
			
		||||
 | 
			
		||||
) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Order streaming task: deliver orders transmitted from UI
 | 
			
		||||
    to downstream consumers.
 | 
			
		||||
    '''
 | 
			
		||||
    Order submission relay task: deliver orders sent from synchronous (UI)
 | 
			
		||||
    code to the EMS via ``OrderClient._from_sync_order_client``.
 | 
			
		||||
 | 
			
		||||
    This is run in the UI actor (usually the one running Qt but could be
 | 
			
		||||
    any other client service code). This process simply delivers order
 | 
			
		||||
    messages to the above ``_to_ems`` send channel (from sync code using
 | 
			
		||||
    messages to the above ``_to_relay_task`` send channel (from sync code using
 | 
			
		||||
    ``.send_nowait()``), these values are pulled from the channel here
 | 
			
		||||
    and relayed to any consumer(s) that called this function using
 | 
			
		||||
    a ``tractor`` portal.
 | 
			
		||||
 | 
			
		||||
    This effectively makes order messages look like they're being
 | 
			
		||||
    "pushed" from the parent to the EMS where local sync code is likely
 | 
			
		||||
    doing the pushing from some UI.
 | 
			
		||||
    doing the pushing from some non-async UI handler.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    book = get_orders()
 | 
			
		||||
    async with book._from_order_book.subscribe() as orders_stream:
 | 
			
		||||
        async for cmd in orders_stream:
 | 
			
		||||
    '''
 | 
			
		||||
    async with (
 | 
			
		||||
        client._from_sync_order_client.subscribe() as sync_order_cmds
 | 
			
		||||
    ):
 | 
			
		||||
        async for cmd in sync_order_cmds:
 | 
			
		||||
            sym = cmd.symbol
 | 
			
		||||
            msg = pformat(cmd)
 | 
			
		||||
 | 
			
		||||
            if sym == symbol_key:
 | 
			
		||||
                log.info(f'Send order cmd:\n{msg}')
 | 
			
		||||
                # send msg over IPC / wire
 | 
			
		||||
                await to_ems_stream.send(cmd)
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                log.warning(
 | 
			
		||||
                    f'Ignoring unmatched order cmd for {sym} != {symbol_key}:'
 | 
			
		||||
| 
						 | 
				
			
			@ -211,7 +218,7 @@ async def open_ems(
 | 
			
		|||
    loglevel: str = 'error',
 | 
			
		||||
 | 
			
		||||
) -> tuple[
 | 
			
		||||
    OrderBook,
 | 
			
		||||
    OrderClient,
 | 
			
		||||
    tractor.MsgStream,
 | 
			
		||||
    dict[
 | 
			
		||||
        # brokername, acctid
 | 
			
		||||
| 
						 | 
				
			
			@ -222,42 +229,15 @@ async def open_ems(
 | 
			
		|||
    dict[str, Status],
 | 
			
		||||
]:
 | 
			
		||||
    '''
 | 
			
		||||
    Spawn an EMS daemon and begin sending orders and receiving
 | 
			
		||||
    alerts.
 | 
			
		||||
    (Maybe) spawn an EMS-daemon (emsd), deliver an `OrderClient` for
 | 
			
		||||
    requesting orders/alerts and a `trades_stream` which delivers all
 | 
			
		||||
    response-msgs.
 | 
			
		||||
 | 
			
		||||
    This EMS tries to reduce most broker's terrible order entry apis to
 | 
			
		||||
    a very simple protocol built on a few easy to grok and/or
 | 
			
		||||
    "rantsy" premises:
 | 
			
		||||
 | 
			
		||||
    - most users will prefer "dark mode" where orders are not submitted
 | 
			
		||||
      to a broker until and execution condition is triggered
 | 
			
		||||
      (aka client-side "hidden orders")
 | 
			
		||||
 | 
			
		||||
    - Brokers over-complicate their apis and generally speaking hire
 | 
			
		||||
      poor designers to create them. We're better off using creating a super
 | 
			
		||||
      minimal, schema-simple, request-event-stream protocol to unify all the
 | 
			
		||||
      existing piles of shit (and shocker, it'll probably just end up
 | 
			
		||||
      looking like a decent crypto exchange's api)
 | 
			
		||||
 | 
			
		||||
    - all order types can be implemented with client-side limit orders
 | 
			
		||||
 | 
			
		||||
    - we aren't reinventing a wheel in this case since none of these
 | 
			
		||||
      brokers are exposing FIX protocol; it is they doing the re-invention.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    TODO: make some fancy diagrams using mermaid.io
 | 
			
		||||
 | 
			
		||||
    the possible set of responses from the stream  is currently:
 | 
			
		||||
    - 'dark_submitted', 'broker_submitted'
 | 
			
		||||
    - 'dark_cancelled', 'broker_cancelled'
 | 
			
		||||
    - 'dark_executed', 'broker_executed'
 | 
			
		||||
    - 'broker_filled'
 | 
			
		||||
    This is a "client side" entrypoint which may spawn the `emsd` service
 | 
			
		||||
    if it can't be discovered and generally speaking is the lowest level
 | 
			
		||||
    broker control client-API.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    # wait for service to connect back to us signalling
 | 
			
		||||
    # ready for order commands
 | 
			
		||||
    book = get_orders()
 | 
			
		||||
 | 
			
		||||
    broker, symbol, suffix = unpack_fqme(fqme)
 | 
			
		||||
 | 
			
		||||
    async with maybe_open_emsd(broker) as portal:
 | 
			
		||||
| 
						 | 
				
			
			@ -291,16 +271,34 @@ async def open_ems(
 | 
			
		|||
            # open 2-way trade command stream
 | 
			
		||||
            ctx.open_stream() as trades_stream,
 | 
			
		||||
        ):
 | 
			
		||||
            # use any pre-existing actor singleton client.
 | 
			
		||||
            global _client
 | 
			
		||||
            if _client is None:
 | 
			
		||||
                size = 100
 | 
			
		||||
                tx, rx = trio.open_memory_channel(size)
 | 
			
		||||
                brx = broadcast_receiver(rx, size)
 | 
			
		||||
 | 
			
		||||
                # setup local ui event streaming channels for request/resp
 | 
			
		||||
                # streamging with EMS daemon
 | 
			
		||||
                _client = OrderClient(
 | 
			
		||||
                    _ems_stream=trades_stream,
 | 
			
		||||
                    _to_relay_task=tx,
 | 
			
		||||
                    _from_sync_order_client=brx,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            _client._ems_stream = trades_stream
 | 
			
		||||
 | 
			
		||||
            # start sync code order msg delivery task
 | 
			
		||||
            async with trio.open_nursery() as n:
 | 
			
		||||
                n.start_soon(
 | 
			
		||||
                    relay_order_cmds_from_sync_code,
 | 
			
		||||
                    relay_orders_from_sync_code,
 | 
			
		||||
                    _client,
 | 
			
		||||
                    fqme,
 | 
			
		||||
                    trades_stream
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                yield (
 | 
			
		||||
                    book,
 | 
			
		||||
                    _client,
 | 
			
		||||
                    trades_stream,
 | 
			
		||||
                    positions,
 | 
			
		||||
                    accounts,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,7 +40,10 @@ from ..accounting import Position
 | 
			
		|||
from ..accounting._allocate import (
 | 
			
		||||
    mk_allocator,
 | 
			
		||||
)
 | 
			
		||||
from ..clearing._client import open_ems, OrderBook
 | 
			
		||||
from ..clearing._client import (
 | 
			
		||||
    open_ems,
 | 
			
		||||
    OrderClient,
 | 
			
		||||
)
 | 
			
		||||
from ._style import _font
 | 
			
		||||
from ..accounting._mktinfo import Symbol
 | 
			
		||||
from ..data.feed import (
 | 
			
		||||
| 
						 | 
				
			
			@ -120,7 +123,7 @@ class OrderMode:
 | 
			
		|||
    chart: ChartPlotWidget  #  type: ignore # noqa
 | 
			
		||||
    hist_chart: ChartPlotWidget  #  type: ignore # noqa
 | 
			
		||||
    nursery: trio.Nursery  # used by ``ui._position`` code?
 | 
			
		||||
    book: OrderBook
 | 
			
		||||
    book: OrderClient
 | 
			
		||||
    lines: LineEditor
 | 
			
		||||
    arrows: ArrowEditor
 | 
			
		||||
    multistatus: MultiStatus
 | 
			
		||||
| 
						 | 
				
			
			@ -679,7 +682,7 @@ async def open_order_mode(
 | 
			
		|||
    multistatus = chart.window().status_bar
 | 
			
		||||
    done = multistatus.open_status('starting order mode..')
 | 
			
		||||
 | 
			
		||||
    book: OrderBook
 | 
			
		||||
    book: OrderClient
 | 
			
		||||
    trades_stream: tractor.MsgStream
 | 
			
		||||
 | 
			
		||||
    # The keys in this dict **must** be in set our set of "normalized"
 | 
			
		||||
| 
						 | 
				
			
			@ -923,7 +926,7 @@ async def process_trades_and_update_ui(
 | 
			
		|||
 | 
			
		||||
    trades_stream: tractor.MsgStream,
 | 
			
		||||
    mode: OrderMode,
 | 
			
		||||
    book: OrderBook,
 | 
			
		||||
    book: OrderClient,
 | 
			
		||||
 | 
			
		||||
) -> None:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -939,7 +942,7 @@ async def process_trades_and_update_ui(
 | 
			
		|||
 | 
			
		||||
async def process_trade_msg(
 | 
			
		||||
    mode: OrderMode,
 | 
			
		||||
    book: OrderBook,
 | 
			
		||||
    book: OrderClient,
 | 
			
		||||
    msg: dict,
 | 
			
		||||
 | 
			
		||||
) -> tuple[Dialog, Status]:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue