Vastly simplify order mode line management
- generate lines from staged `Order` msgs - apply level update callback to each order that dynamically updates the order size from the allocator calcs - pass order msg instances to the ems client for submission - update order size on line moves - add `Order` msg and `Symbol` refs to each dialogfsp_feeds
parent
ccad7cfc2a
commit
873d531521
|
@ -219,7 +219,10 @@ async def handle_viewmode_kb_inputs(
|
||||||
|
|
||||||
# XXX: order matters here for line style!
|
# XXX: order matters here for line style!
|
||||||
view.mode._trigger_type = trigger_type
|
view.mode._trigger_type = trigger_type
|
||||||
view.mode.stage_order(action, trigger_type=trigger_type)
|
view.mode.stage_order(
|
||||||
|
action,
|
||||||
|
trigger_type=trigger_type,
|
||||||
|
)
|
||||||
|
|
||||||
prefix = trigger_type + '-' if action != 'alert' else ''
|
prefix = trigger_type + '-' if action != 'alert' else ''
|
||||||
view._chart.window().set_mode_name(f'{prefix}{action}')
|
view._chart.window().set_mode_name(f'{prefix}{action}')
|
||||||
|
|
|
@ -32,10 +32,11 @@ from ..clearing._client import open_ems, OrderBook
|
||||||
from ..data._source import Symbol
|
from ..data._source import Symbol
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._editors import LineEditor, ArrowEditor
|
from ._editors import LineEditor, ArrowEditor
|
||||||
from ._lines import LevelLine
|
from ._lines import order_line, LevelLine
|
||||||
from ._position import PositionTracker
|
from ._position import PositionTracker
|
||||||
from ._window import MultiStatus
|
from ._window import MultiStatus
|
||||||
from ._forms import FieldsForm
|
from ..clearing._messages import Order
|
||||||
|
# from ._forms import FieldsForm
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -48,6 +49,8 @@ class OrderDialog(BaseModel):
|
||||||
'''
|
'''
|
||||||
# TODO: use ``pydantic.UUID4`` field
|
# TODO: use ``pydantic.UUID4`` field
|
||||||
uuid: str
|
uuid: str
|
||||||
|
order: Order
|
||||||
|
symbol: Symbol
|
||||||
line: LevelLine
|
line: LevelLine
|
||||||
last_status_close: Callable = lambda: None
|
last_status_close: Callable = lambda: None
|
||||||
msgs: dict[str, dict] = {}
|
msgs: dict[str, dict] = {}
|
||||||
|
@ -84,54 +87,232 @@ class OrderMode:
|
||||||
arrows: ArrowEditor
|
arrows: ArrowEditor
|
||||||
multistatus: MultiStatus
|
multistatus: MultiStatus
|
||||||
pp: PositionTracker
|
pp: PositionTracker
|
||||||
|
allocator: 'Allocator' # noqa
|
||||||
|
|
||||||
name: str = 'order'
|
name: str = 'order'
|
||||||
|
dialogs: dict[str, OrderDialog] = field(default_factory=dict)
|
||||||
|
|
||||||
_colors = {
|
_colors = {
|
||||||
'alert': 'alert_yellow',
|
'alert': 'alert_yellow',
|
||||||
'buy': 'buy_green',
|
'buy': 'buy_green',
|
||||||
'sell': 'sell_red',
|
'sell': 'sell_red',
|
||||||
}
|
}
|
||||||
_action: str = 'alert'
|
_staged_order: Optional[Order] = None
|
||||||
_exec_mode: str = 'dark'
|
|
||||||
_size: float = 100.0
|
|
||||||
|
|
||||||
dialogs: dict[str, OrderDialog] = field(default_factory=dict)
|
def line_from_order(
|
||||||
|
self,
|
||||||
|
|
||||||
def uuid(self) -> str:
|
order: Order,
|
||||||
return str(uuid.uuid4())
|
symbol: Symbol,
|
||||||
|
|
||||||
@property
|
**line_kwargs,
|
||||||
def pp_config(self) -> FieldsForm:
|
|
||||||
return self.chart.linked.godwidget.pp_config
|
|
||||||
|
|
||||||
def set_exec(
|
) -> LevelLine:
|
||||||
|
|
||||||
|
line = order_line(
|
||||||
|
|
||||||
|
self.chart,
|
||||||
|
# TODO: convert these values into human-readable form
|
||||||
|
# (i.e. with k, m, M, B) type embedded suffixes
|
||||||
|
level=order.price,
|
||||||
|
# level_digits=symbol.digits(),
|
||||||
|
action=order.action,
|
||||||
|
|
||||||
|
size=order.size,
|
||||||
|
# TODO: we need truncation checks in the EMS for this?
|
||||||
|
# size_digits=min(symbol.lot_digits(), 3),
|
||||||
|
color=self._colors[order.action],
|
||||||
|
|
||||||
|
dotted=True if (
|
||||||
|
order.exec_mode == 'dark' and
|
||||||
|
order.action != 'alert'
|
||||||
|
) else False,
|
||||||
|
|
||||||
|
**line_kwargs,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
# define a callback applied for each level change to the line
|
||||||
|
# which will recompute the order size based on allocator
|
||||||
|
# settings:
|
||||||
|
def update_from_size_calc(y: float) -> None:
|
||||||
|
|
||||||
|
order_info = self.allocator.get_order_info(
|
||||||
|
symbol=symbol,
|
||||||
|
price=y,
|
||||||
|
)
|
||||||
|
line.update_labels(order_info)
|
||||||
|
order.price = y
|
||||||
|
order.size = order_info['size']
|
||||||
|
|
||||||
|
# return is used to update labels implicitly
|
||||||
|
return order_info
|
||||||
|
|
||||||
|
update_from_size_calc(order.price)
|
||||||
|
|
||||||
|
# set level update cb
|
||||||
|
# line._on_level_change = partial(update_from_size_calc, order=order)
|
||||||
|
line._on_level_change = update_from_size_calc
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
def stage_order(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
action: str,
|
action: str,
|
||||||
size: Optional[int] = None,
|
trigger_type: str,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''Stage an order for submission.
|
||||||
Set execution mode.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# not initialized yet
|
# not initialized yet
|
||||||
if not self.chart.linked.cursor:
|
chart = self.chart
|
||||||
|
cursor = chart.linked.cursor
|
||||||
|
if not (chart and cursor and cursor.active_plot):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._action = action
|
chart = cursor.active_plot
|
||||||
self.lines.stage_line(
|
price = cursor._datum_xy[1]
|
||||||
|
symbol = self.chart.linked.symbol
|
||||||
|
|
||||||
color=self._colors[action],
|
order = self._staged_order = Order(
|
||||||
# hl_on_hover=True if self._exec_mode == 'live' else False,
|
|
||||||
dotted=True if (
|
|
||||||
self._exec_mode == 'dark' and action != 'alert'
|
|
||||||
) else False,
|
|
||||||
size=size or self._size,
|
|
||||||
action=action,
|
action=action,
|
||||||
|
price=price,
|
||||||
|
size=0,
|
||||||
|
symbol=symbol,
|
||||||
|
brokers=symbol.brokers,
|
||||||
|
oid='', # filled in on submit
|
||||||
|
exec_mode=trigger_type, # dark or live
|
||||||
)
|
)
|
||||||
|
|
||||||
|
line = self.line_from_order(
|
||||||
|
order,
|
||||||
|
symbol,
|
||||||
|
|
||||||
|
show_markers=True,
|
||||||
|
# just for the stage line to avoid
|
||||||
|
# flickering while moving the cursor
|
||||||
|
# around where it might trigger highlight
|
||||||
|
# then non-highlight depending on sensitivity
|
||||||
|
always_show_labels=True,
|
||||||
|
# don't highlight the "staging" line
|
||||||
|
highlight_on_hover=False,
|
||||||
|
# prevent flickering of marker while moving/tracking cursor
|
||||||
|
only_show_markers_on_hover=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
line = self.lines.stage_line(line)
|
||||||
|
|
||||||
|
# hide crosshair y-line and label
|
||||||
|
cursor.hide_xhair()
|
||||||
|
|
||||||
|
# add line to cursor trackers
|
||||||
|
cursor._trackers.add(line)
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
def submit_order(
|
||||||
|
self,
|
||||||
|
|
||||||
|
) -> OrderDialog:
|
||||||
|
'''Send execution order to EMS return a level line to
|
||||||
|
represent the order on a chart.
|
||||||
|
|
||||||
|
'''
|
||||||
|
staged = self._staged_order
|
||||||
|
symbol = staged.symbol
|
||||||
|
oid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# format order data for ems
|
||||||
|
order = staged.copy(
|
||||||
|
update={
|
||||||
|
'symbol': symbol.key,
|
||||||
|
'oid': oid,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
line = self.line_from_order(
|
||||||
|
order,
|
||||||
|
symbol,
|
||||||
|
|
||||||
|
show_markers=True,
|
||||||
|
only_show_markers_on_hover=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# register the "submitted" line under the cursor
|
||||||
|
# to be displayed when above order ack arrives
|
||||||
|
# (means the marker graphic doesn't show on screen until the
|
||||||
|
# order is live in the emsd).
|
||||||
|
|
||||||
|
# TODO: update the line once an ack event comes back
|
||||||
|
# from the EMS!
|
||||||
|
# maybe place a grey line in "submission" mode
|
||||||
|
# which will be updated to it's appropriate action
|
||||||
|
# color once the submission ack arrives.
|
||||||
|
self.lines.submit_line(
|
||||||
|
line=line,
|
||||||
|
uuid=oid,
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog = OrderDialog(
|
||||||
|
uuid=oid,
|
||||||
|
order=order,
|
||||||
|
symbol=symbol,
|
||||||
|
line=line,
|
||||||
|
last_status_close=self.multistatus.open_status(
|
||||||
|
f'submitting {self._trigger_type}-{order.action}',
|
||||||
|
final_msg=f'submitted {self._trigger_type}-{order.action}',
|
||||||
|
clear_on_next=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: create a new ``OrderLine`` with this optional var defined
|
||||||
|
line.dialog = dialog
|
||||||
|
|
||||||
|
# enter submission which will be popped once a response
|
||||||
|
# from the EMS is received to move the order to a different# status
|
||||||
|
self.dialogs[oid] = dialog
|
||||||
|
|
||||||
|
# hook up mouse drag handlers
|
||||||
|
line._on_drag_start = self.order_line_modify_start
|
||||||
|
line._on_drag_end = self.order_line_modify_complete
|
||||||
|
|
||||||
|
# send order cmd to ems
|
||||||
|
self.book.send(order)
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
|
||||||
|
# order-line modify handlers
|
||||||
|
|
||||||
|
def order_line_modify_start(
|
||||||
|
self,
|
||||||
|
line: LevelLine,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
print(f'Line modify: {line}')
|
||||||
|
# cancel original order until new position is found?
|
||||||
|
# TODO: make a config option for this behaviour..
|
||||||
|
|
||||||
|
def order_line_modify_complete(
|
||||||
|
self,
|
||||||
|
line: LevelLine,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
level = line.value()
|
||||||
|
# updateb by level change callback set in ``.line_from_order()``
|
||||||
|
size = line.dialog.order.size
|
||||||
|
|
||||||
|
self.book.update(
|
||||||
|
uuid=line.dialog.uuid,
|
||||||
|
price=level,
|
||||||
|
size=size,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ems response loop handlers
|
||||||
|
|
||||||
def on_submit(
|
def on_submit(
|
||||||
self,
|
self,
|
||||||
uuid: str
|
uuid: str
|
||||||
|
@ -234,80 +415,6 @@ class OrderMode:
|
||||||
f'Received cancel for unsubmitted order {pformat(msg)}'
|
f'Received cancel for unsubmitted order {pformat(msg)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def submit_exec(
|
|
||||||
self,
|
|
||||||
size: Optional[float] = None,
|
|
||||||
|
|
||||||
) -> OrderDialog:
|
|
||||||
"""Send execution order to EMS return a level line to
|
|
||||||
represent the order on a chart.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# 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).
|
|
||||||
oid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
size = size or self._size
|
|
||||||
|
|
||||||
cursor = self.chart.linked.cursor
|
|
||||||
chart = cursor.active_plot
|
|
||||||
y = cursor._datum_xy[1]
|
|
||||||
|
|
||||||
symbol = self.chart.linked.symbol
|
|
||||||
action = self._action
|
|
||||||
|
|
||||||
# TODO: update the line once an ack event comes back
|
|
||||||
# from the EMS!
|
|
||||||
|
|
||||||
# TODO: place a grey line in "submission" mode
|
|
||||||
# which will be updated to it's appropriate action
|
|
||||||
# color once the submission ack arrives.
|
|
||||||
|
|
||||||
# make line graphic if order push was sucessful
|
|
||||||
line = self.lines.create_order_line(
|
|
||||||
oid,
|
|
||||||
level=y,
|
|
||||||
chart=chart,
|
|
||||||
size=size,
|
|
||||||
action=action,
|
|
||||||
)
|
|
||||||
|
|
||||||
dialog = OrderDialog(
|
|
||||||
uuid=oid,
|
|
||||||
line=line,
|
|
||||||
last_status_close=self.multistatus.open_status(
|
|
||||||
f'submitting {self._exec_mode}-{action}',
|
|
||||||
final_msg=f'submitted {self._exec_mode}-{action}',
|
|
||||||
clear_on_next=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: create a new ``OrderLine`` with this optional var defined
|
|
||||||
line.dialog = dialog
|
|
||||||
|
|
||||||
# enter submission which will be popped once a response
|
|
||||||
# from the EMS is received to move the order to a different# status
|
|
||||||
self.dialogs[oid] = dialog
|
|
||||||
|
|
||||||
# hook up mouse drag handlers
|
|
||||||
line._on_drag_start = self.order_line_modify_start
|
|
||||||
line._on_drag_end = self.order_line_modify_complete
|
|
||||||
|
|
||||||
# send order cmd to ems
|
|
||||||
self.book.send(
|
|
||||||
uuid=oid,
|
|
||||||
symbol=symbol.key,
|
|
||||||
brokers=symbol.brokers,
|
|
||||||
price=y,
|
|
||||||
size=size,
|
|
||||||
action=action,
|
|
||||||
exec_mode=self._exec_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
|
|
||||||
def cancel_orders_under_cursor(self) -> list[str]:
|
def cancel_orders_under_cursor(self) -> list[str]:
|
||||||
return self.cancel_orders_from_lines(
|
return self.cancel_orders_from_lines(
|
||||||
self.lines.lines_under_cursor()
|
self.lines.lines_under_cursor()
|
||||||
|
@ -353,32 +460,6 @@ class OrderMode:
|
||||||
|
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
# order-line modify handlers
|
|
||||||
|
|
||||||
def order_line_modify_start(
|
|
||||||
self,
|
|
||||||
line: LevelLine,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
print(f'Line modify: {line}')
|
|
||||||
# cancel original order until new position is found
|
|
||||||
|
|
||||||
def order_line_modify_complete(
|
|
||||||
self,
|
|
||||||
line: LevelLine,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self.book.update(
|
|
||||||
uuid=line.dialog.uuid,
|
|
||||||
|
|
||||||
# TODO: must adjust sizing
|
|
||||||
# - should we round this to a nearest tick here and how?
|
|
||||||
# - need to recompute the size from the pp allocator
|
|
||||||
price=line.value(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def run_order_mode(
|
async def run_order_mode(
|
||||||
|
|
||||||
|
@ -419,8 +500,10 @@ async def run_order_mode(
|
||||||
|
|
||||||
log.info("Opening order mode")
|
log.info("Opening order mode")
|
||||||
|
|
||||||
|
alloc = chart.linked.godwidget.pp_pane.model
|
||||||
pp = PositionTracker(chart)
|
pp = PositionTracker(chart)
|
||||||
pp.hide()
|
pp.hide()
|
||||||
|
alloc._position = pp
|
||||||
|
|
||||||
mode = OrderMode(
|
mode = OrderMode(
|
||||||
chart,
|
chart,
|
||||||
|
@ -429,6 +512,7 @@ async def run_order_mode(
|
||||||
arrows,
|
arrows,
|
||||||
multistatus,
|
multistatus,
|
||||||
pp,
|
pp,
|
||||||
|
allocator=alloc,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: create a mode "manager" of sorts?
|
# TODO: create a mode "manager" of sorts?
|
||||||
|
@ -475,7 +559,7 @@ async def run_order_mode(
|
||||||
|
|
||||||
# start async input handling for chart's view
|
# start async input handling for chart's view
|
||||||
async with (
|
async with (
|
||||||
chart._vb.open_async_input_handler(),
|
chart.view.open_async_input_handler(),
|
||||||
|
|
||||||
# TODO: config form handler nursery
|
# TODO: config form handler nursery
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue