diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 567e8435..af96cb30 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -41,7 +41,6 @@ from .._daemon import ( maybe_spawn_brokerd, ) from ..brokers import get_brokermod -from ..calc import percent_change from ._axes import ( DynamicDateAxis, PriceAxis, @@ -68,7 +67,6 @@ from ..data import maybe_open_shm_array from ..data.feed import open_feed, Feed, install_brokerd_search from ..data._source import Symbol from ..data._sharedmem import ShmArray -from ..data._normalize import iterticks from .. import brokers from ..log import get_logger from ._exec import run_qtractor @@ -1841,49 +1839,14 @@ async def display_symbol_data( async with ( open_order_mode( + feed, chart, symbol, provider, order_mode_started ) as order_mode, ): - pp = order_mode.pp - live = pp.live_pp - - if live.size < 0: - types = ('ask', 'last') - - elif live.size > 0: - types = ('bid', 'last') - - else: - raise RuntimeError('No pp?!?!') - - # real-time update pnl on the order mode - async with feed.stream.subscribe() as bstream: - last_tick = time.time() - async for quotes in bstream: - - now = time.time() - period = now - last_tick - - for sym, quote in quotes.items(): - - for tick in iterticks(quote, types): - print(f'{1/period} Hz') - - # compute and display pnl status - order_mode.pane.pnl_label.format( - pnl=round( - live.size * percent_change( - live.avg_price, - tick['price'], - ), - ndigits=2, - ) - ) - - last_tick = time.time() + await trio.sleep_forever() async def load_provider_search( diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index cf46567d..6a10ae45 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 contextlib import asynccontextmanager from dataclasses import dataclass, field from functools import partial +from math import copysign from pprint import pformat import time from typing import Optional, Dict, Callable, Any @@ -32,8 +33,11 @@ import tractor import trio from .. import brokers +from ..calc import percent_change from ..clearing._client import open_ems, OrderBook from ..data._source import Symbol +from ..data._normalize import iterticks +from ..data.feed import Feed from ..log import get_logger from ._editors import LineEditor, ArrowEditor from ._lines import order_line, LevelLine @@ -466,6 +470,7 @@ class OrderMode: @asynccontextmanager async def open_order_mode( + feed: Feed, chart: 'ChartPlotWidget', # noqa symbol: Symbol, brokername: str, @@ -520,7 +525,7 @@ async def open_order_mode( form = chart.sidepane form.model = alloc - pp_tracker = PositionTracker(chart) + pp_tracker = PositionTracker(chart, alloc) pp_tracker.hide() # order pane widgets and allocation model @@ -574,6 +579,35 @@ async def open_order_mode( pp_tracker.update(msg, position=pp_tracker.startup_pp) pp_tracker.update(msg) + live_pp = mode.pp.live_pp + size = live_pp.size + if size: + global _zero_pp + _zero_pp = False + + # compute and display pnl status immediately + mode.pane.pnl_label.format( + pnl=round( + copysign(1, size) * percent_change( + live_pp.avg_price, + # last historical close price + feed.shm.array[-1][['close']][0], + ), + ndigits=2, + ) + ) + + # spawn updater task + n.start_soon( + display_pnl, + feed, + mode, + ) + + else: + # set 0% pnl + mode.pane.pnl_label.format(pnl=0) + # make fill bar and positioning snapshot order_pane.init_status_ui() @@ -601,16 +635,83 @@ async def open_order_mode( n.start_soon( process_trades_and_update_ui, + n, + feed, mode, trades_stream, book, ) yield mode - # await trio.sleep_forever() + + +_zero_pp: bool = True + + +async def display_pnl( + feed: Feed, + order_mode: OrderMode, +) -> None: + '''Real-time display the current pp's PnL in the appropriate label. + + Error if this task is spawned where there is a net-zero pp. + + ''' + global _zero_pp + assert not _zero_pp + + pp = order_mode.pp + live = pp.live_pp + + if live.size < 0: + types = ('ask', 'last', 'last', 'utrade') + + elif live.size > 0: + types = ('bid', 'last', 'last', 'utrade') + + else: + raise RuntimeError('No pp?!?!') + + # real-time update pnl on the status pane + async with feed.stream.subscribe() as bstream: + last_tick = time.time() + async for quotes in bstream: + + now = time.time() + period = now - last_tick + + for sym, quote in quotes.items(): + + for tick in iterticks(quote, types): + print(f'{1/period} Hz') + + size = live.size + + if size == 0: + # terminate this update task since we're + # no longer in a pp + _zero_pp = True + order_mode.pane.pnl_label.format(pnl=0) + return + + else: + # compute and display pnl status + order_mode.pane.pnl_label.format( + pnl=round( + copysign(1, size) * percent_change( + live.avg_price, + tick['price'], + ), + ndigits=2, + ) + ) + + last_tick = time.time() async def process_trades_and_update_ui( + n: trio.Nursery, + feed: Feed, mode: OrderMode, trades_stream: tractor.MsgStream, book: OrderBook, @@ -619,6 +720,7 @@ async def process_trades_and_update_ui( get_index = mode.chart.get_index tracker = mode.pp + global _zero_pp # this is where we receive **back** messages # about executions **from** the EMS actor @@ -640,6 +742,13 @@ async def process_trades_and_update_ui( # update order pane widgets mode.pane.update_status_ui() + if mode.pp.live_pp.size and _zero_pp: + _zero_pp = False + n.start_soon( + display_pnl, + feed, + mode, + ) # short circuit to next msg to avoid # uncessary msg content lookups continue