diff --git a/piker/ui/_position.py b/piker/ui/_position.py index 77098f6b..07f8c76e 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -21,7 +21,7 @@ Position info and display from __future__ import annotations from dataclasses import dataclass from functools import partial -from math import floor +from math import floor, copysign from typing import Optional @@ -32,8 +32,10 @@ from ._anchors import ( pp_tight_and_right, # wanna keep it straight in the long run gpath_pin, ) -from ..calc import humanize +from ..calc import humanize, pnl from ..clearing._allocate import Allocator, Position +from ..data._normalize import iterticks +from ..data.feed import Feed from ._label import Label from ._lines import LevelLine, order_line from ._style import _font @@ -41,6 +43,70 @@ from ._forms import FieldsForm, FillStatusBar, QLabel from ..log import get_logger log = get_logger(__name__) +_pnl_tasks: dict[str, bool] = {} + + +async def display_pnl( + + feed: Feed, + order_mode: OrderMode, # noqa + +) -> None: + '''Real-time display the current pp's PnL in the appropriate label. + + ``ValueError`` if this task is spawned where there is a net-zero pp. + + ''' + global _pnl_tasks + + pp = order_mode.current_pp + live = pp.live_pp + key = live.symbol.key + + 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 + try: + 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 = order_mode.current_pp.live_pp.size + if size == 0: + # terminate this update task since we're + # no longer in a pp + order_mode.pane.pnl_label.format(pnl=0) + return + + else: + # compute and display pnl status + order_mode.pane.pnl_label.format( + pnl=copysign(1, size) * pnl( + # live.avg_price, + order_mode.current_pp.live_pp.avg_price, + tick['price'], + ), + ) + + # last_tick = time.time() + finally: + assert _pnl_tasks[key] + assert _pnl_tasks.pop(key) @dataclass @@ -116,7 +182,7 @@ class SettingsPane: tracker.show() tracker.hide_info() - mode.display_pnl(tracker) + self.display_pnl(tracker) # load the new account's allocator alloc = tracker.alloc @@ -201,6 +267,52 @@ class SettingsPane: min(used, slots) ) + def display_pnl( + self, + tracker: PositionTracker, + + ) -> bool: + '''Display the PnL for the current symbol and personal positioning (pp). + + If a position is open start a background task which will + real-time update the pnl label in the settings pane. + + ''' + mode = self.order_mode + sym = mode.chart.linked.symbol + size = tracker.live_pp.size + feed = mode.quote_feed + global _pnl_tasks + + if ( + size and + sym.key not in _pnl_tasks + ): + _pnl_tasks[sym.key] = True + + # immediately compute and display pnl status from last quote + self.pnl_label.format( + pnl=copysign(1, size) * pnl( + tracker.live_pp.avg_price, + # last historical close price + feed.shm.array[-1][['close']][0], + ), + ) + + log.info( + f'Starting pnl display for {tracker.alloc.account_name()}') + self.order_mode.nursery.start_soon( + display_pnl, + feed, + mode, + ) + return True + + else: + # set 0% pnl + self.pnl_label.format(pnl=0) + return False + def position_line( diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 83b193ba..1a588360 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -21,7 +21,6 @@ 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,14 +31,12 @@ import tractor import trio from .. import config -from ..calc import pnl from ..clearing._client import open_ems, OrderBook from ..clearing._allocate import ( mk_allocator, Position, ) 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 @@ -507,49 +504,6 @@ class OrderMode: return ids - def display_pnl( - self, - tracker: PositionTracker, - - ) -> bool: - '''Display the PnL for the current symbol and personal positioning (pp). - - If a position is open start a background task which will - real-time update the pnl label in the settings pane. - - ''' - sym = self.chart.linked.symbol - size = tracker.live_pp.size - global _pnl_tasks - if ( - size and - sym.key not in _pnl_tasks - ): - _pnl_tasks[sym.key] = True - - # immediately compute and display pnl status from last quote - self.pane.pnl_label.format( - pnl=copysign(1, size) * pnl( - tracker.live_pp.avg_price, - # last historical close price - self.quote_feed.shm.array[-1][['close']][0], - ), - ) - - log.info( - f'Starting pnl display for {tracker.alloc.account_name()}') - self.nursery.start_soon( - display_pnl, - self.quote_feed, - self, - ) - return True - - else: - # set 0% pnl - self.pane.pnl_label.format(pnl=0) - return False - @asynccontextmanager async def open_order_mode( @@ -740,8 +694,6 @@ async def open_order_mode( # make fill bar and positioning snapshot order_pane.on_ui_settings_change('limit', tracker.alloc.limit()) - # order_pane.on_ui_settings_change('account', pp_account) - order_pane.update_status_ui(pp=tracker) # TODO: create a mode "manager" of sorts? @@ -749,8 +701,8 @@ async def open_order_mode( # so that view handlers can access it view.order_mode = mode - mode.display_pnl(mode.current_pp) order_pane.on_ui_settings_change('account', pp_account) + mode.pane.display_pnl(mode.current_pp) # Begin order-response streaming done() @@ -784,72 +736,6 @@ async def open_order_mode( yield mode -_pnl_tasks: dict[str, bool] = {} - - -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 _pnl_tasks - - pp = order_mode.current_pp - live = pp.live_pp - key = live.symbol.key - - 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 - try: - 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 = order_mode.current_pp.live_pp.size - if size == 0: - # terminate this update task since we're - # no longer in a pp - order_mode.pane.pnl_label.format(pnl=0) - return - - else: - # compute and display pnl status - order_mode.pane.pnl_label.format( - pnl=copysign(1, size) * pnl( - # live.avg_price, - order_mode.current_pp.live_pp.avg_price, - tick['price'], - ), - ) - - # last_tick = time.time() - finally: - assert _pnl_tasks[key] - assert _pnl_tasks.pop(key) - - async def process_trades_and_update_ui( n: trio.Nursery, @@ -884,7 +770,7 @@ async def process_trades_and_update_ui( # update order pane widgets mode.pane.update_status_ui(tracker) - mode.display_pnl(tracker) + mode.pane.display_pnl(tracker) # short circuit to next msg to avoid # unnecessary msg content lookups continue