Create an "order mode"

Our first major UI "mode" (yes kinda like the modes in emacs) that has
handles to a client side order book api, line and arrow editors, and
interacts with a spawned `emsd` (the EMS daemon actor).

Buncha cleaning and fixes in here for various thingers as well.
basic_alerts
Tyler Goodlet 2021-01-07 12:03:18 -05:00
parent 8d66a17daf
commit 282cc85ba0
4 changed files with 375 additions and 172 deletions

View File

@ -20,12 +20,11 @@ In suit parlance: "Execution management systems"
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import ( from typing import (
AsyncIterator, List, Dict, Callable, Tuple, AsyncIterator, Dict, Callable, Tuple,
Any, Any,
) )
# import uuid # import uuid
import pyqtgraph as pg
import trio import trio
from trio_typing import TaskStatus from trio_typing import TaskStatus
import tractor import tractor
@ -33,32 +32,59 @@ import tractor
from . import data from . import data
from .log import get_logger from .log import get_logger
from .data._source import Symbol from .data._source import Symbol
from .ui._style import hcolor
log = get_logger(__name__) log = get_logger(__name__)
_to_router: trio.abc.SendChannel = None # setup local ui event streaming channels for request/resp
_from_ui: trio.abc.ReceiveChannel = None # streamging with EMS daemon
_lines = {} _to_ems, _from_order_book = trio.open_memory_channel(100)
_local_book = {}
@dataclass @dataclass
class OrderBoi: class OrderBook:
"""'Buy' (client ?) side order book ctl and tracking. """Buy-side (client-side ?) order book ctl and tracking.
Mostly for keeping local state to match the EMS and use A style similar to "model-view" is used here where this api is
events to trigger graphics updates. 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, mostly for keeping local state to match the EMS and use
received events to trigger graphics updates.
""" """
orders: Dict[str, dict] = field(default_factory=dict) _sent_orders: Dict[str, dict] = field(default_factory=dict)
_cmds_from_ui: trio.abc.ReceiveChannel = _from_ui _confirmed_orders: Dict[str, dict] = field(default_factory=dict)
async def alert(self, price: float) -> str: _to_ems: trio.abc.SendChannel = _to_ems
... _from_order_book: trio.abc.ReceiveChannel = _from_order_book
def on_fill(self, uuid: str) -> None:
cmd = self._sent_orders[uuid]
log.info(f"Order executed: {cmd}")
self._confirmed_orders[uuid] = cmd
def alert(
self,
uuid: str,
symbol: 'Symbol',
price: float
) -> str:
# XXX: should make this an explicit attr
# it's assigned inside ``.add_plot()``
# lc = self.view.linked_charts
# uid = str(uuid.uuid4())
cmd = {
'msg': 'alert',
'price': price,
'symbol': symbol.key,
'brokers': symbol.brokers,
'oid': uuid,
}
self._sent_orders[uuid] = cmd
self._to_ems.send_nowait(cmd)
async def buy(self, price: float) -> str: async def buy(self, price: float) -> str:
... ...
@ -66,21 +92,34 @@ class OrderBoi:
async def sell(self, price: float) -> str: async def sell(self, price: float) -> str:
... ...
async def cancel(self, oid: str) -> bool:
"""Cancel an order (or alert) from the EMS.
"""
...
# higher level operations
async def transmit_to_broker(self, price: float) -> str:
...
async def modify(self, oid: str, price) -> bool: async def modify(self, oid: str, price) -> bool:
... ...
async def cancel(self, oid: str) -> bool:
... _orders: OrderBook = None
_orders: OrderBoi = None def get_orders(emsd_uid: Tuple[str, str] = None) -> OrderBook:
if emsd_uid is not None:
# TODO: read in target emsd's active book on startup
pass
def get_orders() -> OrderBoi:
global _orders global _orders
if _orders is None: if _orders is None:
_orders = OrderBoi _orders = OrderBook()
return _orders return _orders
@ -91,48 +130,44 @@ async def send_order_cmds():
to downstream consumers. to downstream consumers.
This is run in the UI actor (usually the one running Qt). This is run in the UI actor (usually the one running Qt).
The UI simply delivers order messages to the above ``_to_router`` The UI simply delivers order messages to the above ``_to_ems``
send channel (from sync code using ``.send_nowait()``), these values send channel (from sync code using ``.send_nowait()``), these values
are pulled from the channel here and send to any consumer(s). are pulled from the channel here and send to any consumer(s).
This effectively makes order messages look like they're being
"pushed" from the parent to the EMS actor.
""" """
global _from_ui
async for order in _from_ui: global _from_order_book
# book = get_orders()
lc = order['chart'] async for cmd in _from_order_book:
symbol = lc.symbol
tp = order['type'] # send msg over IPC / wire
price = order['price'] log.info(f'sending order cmd: {cmd}')
oid = order['oid'] yield cmd
# lc = order['chart']
# symbol = order['symol']
# msg = order['msg']
# price = order['price']
# oid = order['oid']
print(f'oid: {oid}')
# TODO # TODO
# oid = str(uuid.uuid4()) # oid = str(uuid.uuid4())
cmd = { # cmd = {
'price': price, # 'price': price,
'action': 'alert', # 'action': 'alert',
'symbol': symbol.key, # 'symbol': symbol.key,
'brokers': symbol.brokers, # 'brokers': symbol.brokers,
'type': tp, # 'msg': msg,
'price': price, # 'price': price,
'oid': oid, # 'oid': oid,
} # }
_local_book[oid] = cmd # book._sent_orders[oid] = cmd
yield cmd
# streaming tasks which check for conditions per symbol per broker
_scan_tasks: Dict[str, List] = {}
# levels which have an executable action (eg. alert, order, signal)
_levels: Dict[str, list] = {}
# up to date last values from target streams
_last_values: Dict[str, float] = {}
# TODO: numba all of this # TODO: numba all of this
@ -146,26 +181,22 @@ def mk_check(trigger_price, known_last) -> Callable[[float, float], bool]:
avoiding the case where the a predicate returns true immediately. avoiding the case where the a predicate returns true immediately.
""" """
# str compares:
# https://stackoverflow.com/questions/46708708/compare-strings-in-numba-compiled-function
if trigger_price >= known_last: if trigger_price >= known_last:
def check_gt(price: float) -> bool: def check_gt(price: float) -> bool:
if price >= trigger_price: return price >= trigger_price
return True
else:
return False
return check_gt, 'gt' return check_gt, 'down'
elif trigger_price <= known_last: elif trigger_price <= known_last:
def check_lt(price: float) -> bool: def check_lt(price: float) -> bool:
if price <= trigger_price: return price <= trigger_price
return True
else:
return False
return check_lt, 'lt' return check_lt, 'up'
@dataclass @dataclass
@ -173,9 +204,10 @@ class _ExecBook:
"""EMS-side execution book. """EMS-side execution book.
Contains conditions for executions (aka "orders"). Contains conditions for executions (aka "orders").
A singleton instance is created per EMS actor. A singleton instance is created per EMS actor (for now).
""" """
# levels which have an executable action (eg. alert, order, signal)
orders: Dict[ orders: Dict[
Tuple[str, str], Tuple[str, str],
Tuple[ Tuple[
@ -188,7 +220,7 @@ class _ExecBook:
] ]
] = field(default_factory=dict) ] = field(default_factory=dict)
# most recent values # tracks most recent values per symbol each from data feed
lasts: Dict[ lasts: Dict[
Tuple[str, str], Tuple[str, str],
float float
@ -236,6 +268,11 @@ async def exec_orders(
with stream.shield(): with stream.shield():
async for quotes in stream: async for quotes in stream:
##############################
# begin price actions sequence
# XXX: optimize this for speed
##############################
for sym, quote in quotes.items(): for sym, quote in quotes.items():
execs = book.orders.get((broker, sym)) execs = book.orders.get((broker, sym))
@ -249,27 +286,36 @@ async def exec_orders(
# update to keep new cmds informed # update to keep new cmds informed
book.lasts[(broker, symbol)] = price book.lasts[(broker, symbol)] = price
# begin price actions sequence
if not execs: if not execs:
continue continue
for oid, pred, action in tuple(execs): for oid, pred, name, cmd in tuple(execs):
# push trigger msg back to parent as an "alert" # push trigger msg back to parent as an "alert"
# (mocking for eg. a "fill") # (mocking for eg. a "fill")
if pred(price): if pred(price):
name = action(price)
await ctx.send_yield({ cmd['name'] = name
'type': 'alert', cmd['index'] = feed.shm._last.value - 1
'price': price, # current shm array index
# current shm array index cmd['trigger_price'] = price
'index': feed.shm._last.value - 1,
'name': name, await ctx.send_yield(cmd)
'oid': oid, # await ctx.send_yield({
}) # 'type': 'alert',
execs.remove((oid, pred, action)) # 'price': price,
# # current shm array index
# 'index': feed.shm._last.value - 1,
# 'name': name,
# 'oid': oid,
# })
print( print(
f"GOT ALERT FOR {exec_price} @ \n{tick}\n") f"GOT ALERT FOR {exec_price} @ \n{tick}\n")
print(f'removing pred for {oid}')
execs.remove((oid, pred, name, cmd))
print(f'execs are {execs}') print(f'execs are {execs}')
# feed teardown # feed teardown
@ -279,6 +325,10 @@ async def exec_orders(
async def stream_and_route(ctx, ui_name): async def stream_and_route(ctx, ui_name):
"""Order router (sub)actor entrypoint. """Order router (sub)actor entrypoint.
This is the daemon (child) side routine which starts an EMS
runtime per broker/feed and and begins streaming back alerts
from executions back to subscribers.
""" """
actor = tractor.current_actor() actor = tractor.current_actor()
book = get_book() book = get_book()
@ -291,19 +341,18 @@ async def stream_and_route(ctx, ui_name):
async for cmd in await portal.run(send_order_cmds): async for cmd in await portal.run(send_order_cmds):
action = cmd.pop('action') msg = cmd['msg']
if action == 'cancel': if msg == 'cancel':
# TODO:
pass pass
tp = cmd.pop('type')
trigger_price = cmd['price'] trigger_price = cmd['price']
sym = cmd['symbol'] sym = cmd['symbol']
brokers = cmd['brokers'] brokers = cmd['brokers']
oid = cmd['oid'] oid = cmd['oid']
if tp == 'alert': if msg == 'alert':
log.info(f'Alert {cmd} received in {actor.uid}') log.info(f'Alert {cmd} received in {actor.uid}')
broker = brokers[0] broker = brokers[0]
@ -333,14 +382,17 @@ async def stream_and_route(ctx, ui_name):
# create list of executions on first entry # create list of executions on first entry
book.orders.setdefault((broker, sym), []).append( book.orders.setdefault((broker, sym), []).append(
(oid, pred, lambda p: name) (oid, pred, name, cmd)
) )
# ack-respond that order is live
await ctx.send_yield({'msg': 'ack', 'oid': oid})
# continue and wait on next order cmd # continue and wait on next order cmd
async def spawn_router_stream_alerts( async def spawn_router_stream_alerts(
chart, order_mode,
symbol: Symbol, symbol: Symbol,
# lines: 'LinesEditor', # lines: 'LinesEditor',
task_status: TaskStatus[str] = trio.TASK_STATUS_IGNORED, task_status: TaskStatus[str] = trio.TASK_STATUS_IGNORED,
@ -349,13 +401,11 @@ async def spawn_router_stream_alerts(
alerts. alerts.
""" """
# setup local ui event streaming channels
global _from_ui, _to_router, _lines
_to_router, _from_ui = trio.open_memory_channel(100)
actor = tractor.current_actor() actor = tractor.current_actor()
subactor_name = 'piker.ems' subactor_name = 'emsd'
# TODO: add ``maybe_spawn_emsd()`` for this
async with tractor.open_nursery() as n: async with tractor.open_nursery() as n:
portal = await n.start_actor( portal = await n.start_actor(
@ -369,32 +419,40 @@ async def spawn_router_stream_alerts(
async with tractor.wait_for_actor(subactor_name): async with tractor.wait_for_actor(subactor_name):
# let parent task continue # let parent task continue
task_status.started(_to_router) task_status.started(_to_ems)
# begin the trigger-alert stream
# this is where we receive **back** messages
# about executions **from** the EMS actor
async for alert in stream: async for alert in stream:
yb = pg.mkBrush(hcolor('alert_yellow'))
angle = 90 if alert['name'] == 'lt' else -90
arrow = pg.ArrowItem(
angle=angle,
baseAngle=0,
headLen=5,
headWidth=2,
tailLen=None,
brush=yb,
)
arrow.setPos(alert['index'], alert['price'])
chart.plotItem.addItem(arrow)
# delete the line from view # delete the line from view
oid = alert['oid'] oid = alert['oid']
print(f'_lines: {_lines}') msg_type = alert['msg']
if msg_type == 'ack':
print(f"order accepted: {alert}")
# show line label once order is live
order_mode.lines.commit_line(oid)
continue
order_mode.arrows.add(
oid,
alert['index'],
alert['price'],
pointing='up' if alert['name'] == 'up' else 'down'
)
# print(f'_lines: {_lines}')
print(f'deleting line with oid: {oid}') print(f'deleting line with oid: {oid}')
chart._vb._lines_editor # delete level from view
_lines.pop(oid).delete() order_mode.lines.remove_line(uuid=oid)
# chart._vb._lines_editor
# _lines.pop(oid).delete()
# TODO: this in another task? # TODO: this in another task?
# not sure if this will ever be a bottleneck, # not sure if this will ever be a bottleneck,
@ -411,3 +469,6 @@ async def spawn_router_stream_alerts(
], ],
) )
log.runtime(result) log.runtime(result)
# do we need this?
# await _from_ems.put(alert)

View File

@ -57,7 +57,7 @@ from .. import data
from ..data import maybe_open_shm_array from ..data import maybe_open_shm_array
from ..log import get_logger from ..log import get_logger
from ._exec import run_qtractor, current_screen from ._exec import run_qtractor, current_screen
from ._interaction import ChartView from ._interaction import ChartView, open_order_mode
from .. import fsp from .. import fsp
from .._ems import spawn_router_stream_alerts from .._ems import spawn_router_stream_alerts
@ -301,6 +301,7 @@ class LinkedSplitCharts(QtGui.QWidget):
array=array, array=array,
parent=self.splitter, parent=self.splitter,
linked_charts=self,
axisItems={ axisItems={
'bottom': xaxis, 'bottom': xaxis,
'right': PriceAxis(linked_charts=self) 'right': PriceAxis(linked_charts=self)
@ -310,9 +311,9 @@ class LinkedSplitCharts(QtGui.QWidget):
**cpw_kwargs, **cpw_kwargs,
) )
# give viewbox a reference to primary chart # give viewbox as reference to chart
# allowing for kb controls and interactions # allowing for kb controls and interactions on **this** widget
# (see our custom view in `._interactions.py`) # (see our custom view mode in `._interactions.py`)
cv.chart = cpw cv.chart = cpw
cpw.plotItem.vb.linked_charts = self cpw.plotItem.vb.linked_charts = self
@ -375,6 +376,7 @@ class ChartPlotWidget(pg.PlotWidget):
# the data view we generate graphics from # the data view we generate graphics from
name: str, name: str,
array: np.ndarray, array: np.ndarray,
linked_charts: LinkedSplitCharts,
static_yrange: Optional[Tuple[float, float]] = None, static_yrange: Optional[Tuple[float, float]] = None,
cursor: Optional[Cursor] = None, cursor: Optional[Cursor] = None,
**kwargs, **kwargs,
@ -390,6 +392,7 @@ class ChartPlotWidget(pg.PlotWidget):
**kwargs **kwargs
) )
self.name = name self.name = name
self._lc = linked_charts
# self.setViewportMargins(0, 0, 0, 0) # self.setViewportMargins(0, 0, 0, 0)
self._ohlc = array # readonly view of ohlc data self._ohlc = array # readonly view of ohlc data
@ -934,17 +937,6 @@ async def _async_main(
wap_in_history, wap_in_history,
) )
# spawn EMS actor-service
router_send_chan = await n.start(
spawn_router_stream_alerts,
chart,
symbol,
)
# wait for router to come up before setting
# enabling send channel on chart
linked_charts._to_router = router_send_chan
# wait for a first quote before we start any update tasks # wait for a first quote before we start any update tasks
quote = await feed.receive() quote = await feed.receive()
@ -958,8 +950,26 @@ async def _async_main(
linked_charts linked_charts
) )
# probably where we'll eventually start the user input loop async with open_order_mode(
await trio.sleep_forever() chart,
) as order_mode:
# TODO: this should probably be implicitly spawned
# inside the above mngr?
# spawn EMS actor-service
to_ems_chan = await n.start(
spawn_router_stream_alerts,
order_mode,
symbol,
)
# wait for router to come up before setting
# enabling send channel on chart
linked_charts._to_ems = to_ems_chan
# probably where we'll eventually start the user input loop
await trio.sleep_forever()
async def chart_from_quotes( async def chart_from_quotes(
@ -1019,7 +1029,7 @@ async def chart_from_quotes(
chart, chart,
# determine precision/decimal lengths # determine precision/decimal lengths
digits=max(float_digits(last), 2), digits=max(float_digits(last), 2),
size_digits=min(float_digits(volume), 3) size_digits=min(float_digits(last), 3)
) )
# TODO: # TODO:

View File

@ -157,7 +157,7 @@ class L1Labels:
self, self,
chart: 'ChartPlotWidget', # noqa chart: 'ChartPlotWidget', # noqa
digits: int = 2, digits: int = 2,
size_digits: int = 0, size_digits: int = 3,
font_size_inches: float = _down_2_font_inches_we_like, font_size_inches: float = _down_2_font_inches_we_like,
) -> None: ) -> None:

View File

@ -17,8 +17,9 @@
""" """
UX interaction customs. UX interaction customs.
""" """
from contextlib import asynccontextmanager
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional, Dict, Callable
import uuid import uuid
import pyqtgraph as pg import pyqtgraph as pg
@ -29,7 +30,7 @@ import numpy as np
from ..log import get_logger from ..log import get_logger
from ._style import _min_points_to_show, hcolor, _font from ._style import _min_points_to_show, hcolor, _font
from ._graphics._lines import level_line, LevelLine from ._graphics._lines import level_line, LevelLine
from .._ems import _lines from .._ems import get_orders, OrderBook
log = get_logger(__name__) log = get_logger(__name__)
@ -198,10 +199,17 @@ class SelectRect(QtGui.QGraphicsRectItem):
self.hide() self.hide()
# global store of order-lines graphics
# keyed by uuid4 strs - used to sync draw
# order lines **after** the order is 100%
# active in emsd
_order_lines: Dict[str, LevelLine] = {}
@dataclass @dataclass
class LineEditor: class LineEditor:
view: 'ChartView' view: 'ChartView'
_lines: field(default_factory=dict) _order_lines: field(default_factory=_order_lines)
chart: 'ChartPlotWidget' = None # type: ignore # noqa chart: 'ChartPlotWidget' = None # type: ignore # noqa
_active_staged_line: LevelLine = None _active_staged_line: LevelLine = None
_stage_line: LevelLine = None _stage_line: LevelLine = None
@ -223,6 +231,7 @@ class LineEditor:
line = level_line( line = level_line(
chart, chart,
level=y, level=y,
digits=chart._lc.symbol.digits(),
color=color, color=color,
# don't highlight the "staging" line # don't highlight the "staging" line
@ -264,46 +273,48 @@ class LineEditor:
self._stage_line.hide() self._stage_line.hide()
self._stage_line.label.hide() self._stage_line.label.hide()
# if line:
# line.delete()
self._active_staged_line = None self._active_staged_line = None
# show the crosshair y line # show the crosshair y line
hl = cursor.graphics[chart]['hl'] hl = cursor.graphics[chart]['hl']
hl.show() hl.show()
def commit_line(self) -> LevelLine: def create_line(self, uuid: str) -> LevelLine:
line = self._active_staged_line line = self._active_staged_line
if line: if not line:
chart = self.chart._cursor.active_plot raise RuntimeError("No line commit is currently staged!?")
y = chart._cursor._datum_xy[1] chart = self.chart._cursor.active_plot
y = chart._cursor._datum_xy[1]
# XXX: should make this an explicit attr line = level_line(
# it's assigned inside ``.add_plot()`` chart,
lc = self.view.linked_charts level=y,
color='alert_yellow',
digits=chart._lc.symbol.digits(),
show_label=False,
)
oid = str(uuid.uuid4()) # register for later lookup/deletion
lc._to_router.send_nowait({ self._order_lines[uuid] = line
'chart': lc, return line, y
'type': 'alert',
'price': y,
'oid': oid,
# 'symbol': lc.chart.name,
# 'brokers': lc.symbol.brokers,
# 'price': y,
})
line = level_line( def commit_line(self, uuid: str) -> LevelLine:
chart, """Commit a "staged line" to view.
level=y,
color='alert_yellow',
)
# register for later
_lines[oid] = line
log.debug(f'clicked y: {y}') Submits the line graphic under the cursor as a (new) permanent
graphic in view.
"""
line = self._order_lines[uuid]
line.label.show()
# TODO: other flashy things to indicate the order is active
log.debug(f'Level active for level: {line.value()}')
return line
def remove_line( def remove_line(
self, self,
@ -316,18 +327,114 @@ class LineEditor:
cursor position. cursor position.
""" """
# Delete any hoverable under the cursor
cursor = self.chart._cursor
if line: if line:
# If line is passed delete it
line.delete() line.delete()
elif uuid:
# try to look up line from our registry
self._order_lines.pop(uuid).delete()
else: else:
# Delete any hoverable under the cursor
cursor = self.chart._cursor
for item in cursor._hovered: for item in cursor._hovered:
# hovered items must also offer # hovered items must also offer
# a ``.delete()`` method # a ``.delete()`` method
item.delete() item.delete()
@dataclass
class ArrowEditor:
chart: 'ChartPlotWidget' # noqa
_arrows: field(default_factory=dict)
def add(
self,
uid: str,
x: float,
y: float,
color='default',
pointing: str = 'up',
) -> pg.ArrowItem:
"""Add an arrow graphic to view at given (x, y).
"""
yb = pg.mkBrush(hcolor('alert_yellow'))
angle = 90 if pointing == 'up' else -90
arrow = pg.ArrowItem(
angle=angle,
baseAngle=0,
headLen=5,
headWidth=2,
tailLen=None,
brush=yb,
)
arrow.setPos(x, y)
self._arrows[uid] = arrow
# render to view
self.chart.plotItem.addItem(arrow)
return arrow
def remove(self, arrow) -> bool:
self.chart.plotItem.removeItem(arrow)
@dataclass
class OrderMode:
"""Major mode for placing orders on a chart view.
"""
chart: 'ChartPlotWidget'
book: OrderBook
lines: LineEditor
arrows: ArrowEditor
key_map: Dict[str, Callable] = field(default_factory=dict)
def uuid(self) -> str:
return str(uuid.uuid4())
@asynccontextmanager
async def open_order_mode(
chart,
):
# global _order_lines
view = chart._vb
book = get_orders()
lines = LineEditor(view=view, _order_lines=_order_lines, chart=chart)
arrows = ArrowEditor(chart, {})
log.info("Opening order mode")
mode = OrderMode(chart, book, lines, arrows)
view.mode = mode
# # setup local ui event streaming channels for request/resp
# # streamging with EMS daemon
# global _to_ems, _from_order_book
# _to_ems, _from_order_book = trio.open_memory_channel(100)
try:
yield mode
finally:
# XXX special teardown handling like for ex.
# - cancelling orders if needed?
# - closing positions if desired?
# - switching special condition orders to safer/more reliable variants
log.info("Closing order mode")
class ChartView(ViewBox): class ChartView(ViewBox):
"""Price chart view box with interaction behaviors you'd expect from """Price chart view box with interaction behaviors you'd expect from
any interactive platform: any interactive platform:
@ -336,6 +443,7 @@ class ChartView(ViewBox):
- vertical scrolling on y-axis - vertical scrolling on y-axis
- zoom on x to most recent in view datum - zoom on x to most recent in view datum
- zoom on right-click-n-drag to cursor position - zoom on right-click-n-drag to cursor position
""" """
def __init__( def __init__(
self, self,
@ -350,9 +458,11 @@ class ChartView(ViewBox):
self.addItem(self.select_box, ignoreBounds=True) self.addItem(self.select_box, ignoreBounds=True)
self._chart: 'ChartPlotWidget' = None # noqa self._chart: 'ChartPlotWidget' = None # noqa
self._lines_editor = LineEditor(view=self, _lines=_lines) # self._lines_editor = LineEditor(view=self, _lines=_lines)
self.mode = None
# kb ctrls processing
self._key_buffer = [] self._key_buffer = []
self._active_staged_line: LevelLine = None # noqa
@property @property
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
@ -362,7 +472,7 @@ class ChartView(ViewBox):
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa
self._chart = chart self._chart = chart
self.select_box.chart = chart self.select_box.chart = chart
self._lines_editor.chart = chart # self._lines_editor.chart = chart
def wheelEvent(self, ev, axis=None): def wheelEvent(self, ev, axis=None):
"""Override "center-point" location for scrolling. """Override "center-point" location for scrolling.
@ -533,8 +643,27 @@ class ChartView(ViewBox):
ev.accept() ev.accept()
# commit the "staged" line under the cursor # self._lines_editor.commit_line()
self._lines_editor.commit_line()
# send order to EMS
# register the "staged" line under the cursor
# to be displayed when above order ack arrives
# (means the line graphic doesn't show on screen until the
# order is live in the emsd).
mode = self.mode
uuid = mode.uuid()
# make line graphic
line, y = mode.lines.create_line(uuid)
# send order cmd to ems
mode.book.alert(
uuid=uuid,
symbol=mode.chart._lc._symbol,
price=y
)
def keyReleaseEvent(self, ev): def keyReleaseEvent(self, ev):
""" """
@ -557,7 +686,8 @@ class ChartView(ViewBox):
if text == 'a': if text == 'a':
# draw "staged" line under cursor position # draw "staged" line under cursor position
self._lines_editor.unstage_line() # self._lines_editor.unstage_line()
self.mode.lines.unstage_line()
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
""" """
@ -580,12 +710,11 @@ class ChartView(ViewBox):
# ctl # ctl
if mods == QtCore.Qt.ControlModifier: if mods == QtCore.Qt.ControlModifier:
# print("CTRL")
# TODO: ctrl-c as cancel? # TODO: ctrl-c as cancel?
# https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9 # https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9
# if ev.text() == 'c': # if ev.text() == 'c':
# self.rbScaleBox.hide() # self.rbScaleBox.hide()
pass print(f"CTRL + key:{key} + text:{text}")
# alt # alt
if mods == QtCore.Qt.AltModifier: if mods == QtCore.Qt.AltModifier:
@ -603,13 +732,16 @@ class ChartView(ViewBox):
elif text == 'a': elif text == 'a':
# add a line at the current cursor # add a line at the current cursor
self._lines_editor.stage_line() # self._lines_editor.stage_line()
self.mode.lines.stage_line()
elif text == 'd': elif text == 'd':
# delete any lines under the cursor # delete any lines under the cursor
self._lines_editor.remove_line() # self._lines_editor.remove_line()
self.mode.lines.remove_line()
# Leaving this for light reference purposes # XXX: Leaving this for light reference purposes, there
# seems to be some work to at least gawk at for history mgmt.
# Key presses are used only when mouse mode is RectMode # Key presses are used only when mouse mode is RectMode
# The following events are implemented: # The following events are implemented: