Create an order mode module
parent
7e214180a6
commit
7075a968b4
|
@ -18,10 +18,8 @@
|
||||||
High level Qt chart widgets.
|
High level Qt chart widgets.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from pprint import pformat
|
|
||||||
from typing import Tuple, Dict, Any, Optional, Callable
|
from typing import Tuple, Dict, Any, Optional, Callable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import time
|
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -60,9 +58,9 @@ 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, open_order_mode
|
from ._interaction import ChartView
|
||||||
|
from .order_mode import start_order_mode
|
||||||
from .. import fsp
|
from .. import fsp
|
||||||
from ..exchange._client import open_ems
|
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -1061,95 +1059,7 @@ async def _async_main(
|
||||||
# chart,
|
# chart,
|
||||||
# linked_charts,
|
# linked_charts,
|
||||||
# )
|
# )
|
||||||
|
await start_order_mode(chart, symbol, brokername)
|
||||||
# spawn EMS actor-service
|
|
||||||
async with open_ems(
|
|
||||||
brokername,
|
|
||||||
symbol,
|
|
||||||
) as (book, trades_stream):
|
|
||||||
|
|
||||||
async with open_order_mode(
|
|
||||||
symbol,
|
|
||||||
chart,
|
|
||||||
book,
|
|
||||||
) as order_mode:
|
|
||||||
|
|
||||||
def get_index(time: float):
|
|
||||||
# XXX: not sure why the time is so off here
|
|
||||||
# looks like we're gonna have to do some fixing..
|
|
||||||
ohlc = chart._shm.array
|
|
||||||
indexes = ohlc['time'] >= time
|
|
||||||
|
|
||||||
if any(indexes):
|
|
||||||
return ohlc['index'][indexes[-1]]
|
|
||||||
else:
|
|
||||||
return ohlc['index'][-1]
|
|
||||||
|
|
||||||
# Begin order-response streaming
|
|
||||||
|
|
||||||
# this is where we receive **back** messages
|
|
||||||
# about executions **from** the EMS actor
|
|
||||||
async for msg in trades_stream:
|
|
||||||
|
|
||||||
fmsg = pformat(msg)
|
|
||||||
log.info(f'Received order msg:\n{fmsg}')
|
|
||||||
|
|
||||||
# delete the line from view
|
|
||||||
oid = msg['oid']
|
|
||||||
resp = msg['resp']
|
|
||||||
|
|
||||||
# response to 'action' request (buy/sell)
|
|
||||||
if resp in (
|
|
||||||
'dark_submitted',
|
|
||||||
'broker_submitted'
|
|
||||||
):
|
|
||||||
|
|
||||||
# show line label once order is live
|
|
||||||
order_mode.on_submit(oid)
|
|
||||||
|
|
||||||
# resp to 'cancel' request or error condition
|
|
||||||
# for action request
|
|
||||||
elif resp in (
|
|
||||||
'broker_cancelled',
|
|
||||||
'broker_inactive',
|
|
||||||
'dark_cancelled'
|
|
||||||
):
|
|
||||||
# delete level line from view
|
|
||||||
order_mode.on_cancel(oid)
|
|
||||||
|
|
||||||
elif resp in (
|
|
||||||
'dark_executed'
|
|
||||||
):
|
|
||||||
log.info(f'Dark order triggered for {fmsg}')
|
|
||||||
|
|
||||||
# for alerts add a triangle and remove the
|
|
||||||
# level line
|
|
||||||
if msg['cmd']['action'] == 'alert':
|
|
||||||
|
|
||||||
# should only be one "fill" for an alert
|
|
||||||
order_mode.on_fill(
|
|
||||||
oid,
|
|
||||||
price=msg['trigger_price'],
|
|
||||||
arrow_index=get_index(time.time())
|
|
||||||
)
|
|
||||||
await order_mode.on_exec(oid, msg)
|
|
||||||
|
|
||||||
# response to completed 'action' request for buy/sell
|
|
||||||
elif resp in (
|
|
||||||
'broker_executed',
|
|
||||||
):
|
|
||||||
await order_mode.on_exec(oid, msg)
|
|
||||||
|
|
||||||
# each clearing tick is responded individually
|
|
||||||
elif resp in ('broker_filled',):
|
|
||||||
action = msg['action']
|
|
||||||
# TODO: some kinda progress system
|
|
||||||
order_mode.on_fill(
|
|
||||||
oid,
|
|
||||||
price=msg['price'],
|
|
||||||
arrow_index=get_index(msg['broker_time']),
|
|
||||||
pointing='up' if action == 'buy' else 'down',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def chart_from_quotes(
|
async def chart_from_quotes(
|
||||||
|
|
|
@ -17,14 +17,9 @@
|
||||||
"""
|
"""
|
||||||
Chart view box primitives.
|
Chart view box primitives.
|
||||||
"""
|
"""
|
||||||
import time
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pprint import pformat
|
from typing import Optional, Dict
|
||||||
from typing import Optional, Dict, Callable, Any
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import trio
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from pyqtgraph import ViewBox, Point, QtCore, QtGui
|
from pyqtgraph import ViewBox, Point, QtCore, QtGui
|
||||||
from pyqtgraph import functions as fn
|
from pyqtgraph import functions as fn
|
||||||
|
@ -33,8 +28,6 @@ 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 order_line, LevelLine
|
from ._graphics._lines import order_line, LevelLine
|
||||||
from ..exchange._client import OrderBook
|
|
||||||
from ..data._source import Symbol
|
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -440,230 +433,6 @@ class ArrowEditor:
|
||||||
self.chart.plotItem.removeItem(arrow)
|
self.chart.plotItem.removeItem(arrow)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class OrderMode:
|
|
||||||
"""Major mode for placing orders on a chart view.
|
|
||||||
|
|
||||||
This is the default mode that pairs with "follow mode"
|
|
||||||
(when wathing the rt price update at the current time step)
|
|
||||||
and allows entering orders using the ``a, d, f`` keys and
|
|
||||||
cancelling moused-over orders with the ``c`` key.
|
|
||||||
|
|
||||||
"""
|
|
||||||
chart: 'ChartPlotWidget' # type: ignore # noqa
|
|
||||||
book: OrderBook
|
|
||||||
lines: LineEditor
|
|
||||||
arrows: ArrowEditor
|
|
||||||
_colors = {
|
|
||||||
'alert': 'alert_yellow',
|
|
||||||
'buy': 'buy_green',
|
|
||||||
'sell': 'sell_red',
|
|
||||||
}
|
|
||||||
_action: str = 'alert'
|
|
||||||
_exec_mode: str = 'dark'
|
|
||||||
_size: float = 100.0
|
|
||||||
|
|
||||||
key_map: Dict[str, Callable] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def uuid(self) -> str:
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def set_exec(
|
|
||||||
self,
|
|
||||||
action: str,
|
|
||||||
size: Optional[int] = None,
|
|
||||||
) -> None:
|
|
||||||
"""Set execution mode.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._action = action
|
|
||||||
self.lines.stage_line(
|
|
||||||
color=self._colors[action],
|
|
||||||
# hl_on_hover=True if self._exec_mode == 'live' else False,
|
|
||||||
dotted=True if self._exec_mode == 'dark' else False,
|
|
||||||
size=size or self._size,
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_submit(self, uuid: str) -> dict:
|
|
||||||
"""On order submitted event, commit the order line
|
|
||||||
and registered order uuid, store ack time stamp.
|
|
||||||
|
|
||||||
TODO: annotate order line with submission type ('live' vs.
|
|
||||||
'dark').
|
|
||||||
|
|
||||||
"""
|
|
||||||
line = self.lines.commit_line(uuid)
|
|
||||||
req_msg = self.book._sent_orders.get(uuid)
|
|
||||||
if req_msg:
|
|
||||||
req_msg['ack_time_ns'] = time.time_ns()
|
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
def on_fill(
|
|
||||||
self,
|
|
||||||
uuid: str,
|
|
||||||
price: float,
|
|
||||||
arrow_index: float,
|
|
||||||
pointing: Optional[str] = None
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
line = self.lines._order_lines.get(uuid)
|
|
||||||
if line:
|
|
||||||
self.arrows.add(
|
|
||||||
uuid,
|
|
||||||
arrow_index,
|
|
||||||
price,
|
|
||||||
pointing=pointing,
|
|
||||||
color=line.color
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_exec(
|
|
||||||
self,
|
|
||||||
uuid: str,
|
|
||||||
msg: Dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
# only once all fills have cleared and the execution
|
|
||||||
# is complet do we remove our "order line"
|
|
||||||
line = self.lines.remove_line(uuid=uuid)
|
|
||||||
log.debug(f'deleting {line} with oid: {uuid}')
|
|
||||||
|
|
||||||
# DESKTOP NOTIFICATIONS
|
|
||||||
#
|
|
||||||
# TODO: this in another task?
|
|
||||||
# not sure if this will ever be a bottleneck,
|
|
||||||
# we probably could do graphics stuff first tho?
|
|
||||||
|
|
||||||
# XXX: linux only for now
|
|
||||||
result = await trio.run_process(
|
|
||||||
[
|
|
||||||
'notify-send',
|
|
||||||
'-u', 'normal',
|
|
||||||
'-t', '10000',
|
|
||||||
'piker',
|
|
||||||
f'alert: {msg}',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
log.runtime(result)
|
|
||||||
|
|
||||||
def on_cancel(self, uuid: str) -> None:
|
|
||||||
msg = self.book._sent_orders.pop(uuid, None)
|
|
||||||
if msg is not None:
|
|
||||||
self.lines.remove_line(uuid=uuid)
|
|
||||||
self.chart._cursor.show_xhair()
|
|
||||||
else:
|
|
||||||
log.warning(
|
|
||||||
f'Received cancel for unsubmitted order {pformat(msg)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
def submit_exec(
|
|
||||||
self,
|
|
||||||
size: Optional[float] = None,
|
|
||||||
) -> LevelLine:
|
|
||||||
"""Send execution 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).
|
|
||||||
uid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
size = size or self._size
|
|
||||||
|
|
||||||
chart = self.chart._cursor.active_plot
|
|
||||||
y = chart._cursor._datum_xy[1]
|
|
||||||
|
|
||||||
symbol = self.chart._lc._symbol
|
|
||||||
|
|
||||||
# send order cmd to ems
|
|
||||||
self.book.send(
|
|
||||||
uuid=uid,
|
|
||||||
symbol=symbol.key,
|
|
||||||
brokers=symbol.brokers,
|
|
||||||
price=y,
|
|
||||||
size=size,
|
|
||||||
action=self._action,
|
|
||||||
exec_mode=self._exec_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
# make line graphic if order push was
|
|
||||||
# sucessful
|
|
||||||
line = self.lines.create_order_line(
|
|
||||||
uid,
|
|
||||||
level=y,
|
|
||||||
chart=chart,
|
|
||||||
size=size,
|
|
||||||
)
|
|
||||||
line.oid = uid
|
|
||||||
|
|
||||||
# hook up mouse drag handlers
|
|
||||||
line._on_drag_start = self.order_line_modify_start
|
|
||||||
line._on_drag_end = self.order_line_modify_complete
|
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
def cancel_order_under_cursor(self) -> None:
|
|
||||||
for line in self.lines.lines_under_cursor():
|
|
||||||
self.book.cancel(uuid=line.oid)
|
|
||||||
|
|
||||||
# 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.oid,
|
|
||||||
|
|
||||||
# TODO: should we round this to a nearest tick here?
|
|
||||||
price=line.value(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def open_order_mode(
|
|
||||||
symbol: Symbol,
|
|
||||||
chart: pg.PlotWidget,
|
|
||||||
book: OrderBook,
|
|
||||||
):
|
|
||||||
view = chart._vb
|
|
||||||
lines = LineEditor(view=view, chart=chart, _order_lines=_order_lines)
|
|
||||||
arrows = ArrowEditor(chart, {})
|
|
||||||
|
|
||||||
log.info("Opening order mode")
|
|
||||||
|
|
||||||
mode = OrderMode(chart, book, lines, arrows)
|
|
||||||
view.mode = mode
|
|
||||||
|
|
||||||
asset_type = symbol.type_key
|
|
||||||
|
|
||||||
if asset_type == 'stock':
|
|
||||||
mode._size = 100.0
|
|
||||||
|
|
||||||
elif asset_type in ('future', 'option', 'futures_option'):
|
|
||||||
mode._size = 1.0
|
|
||||||
|
|
||||||
else: # to be safe
|
|
||||||
mode._size = 1.0
|
|
||||||
|
|
||||||
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:
|
||||||
|
|
|
@ -0,0 +1,357 @@
|
||||||
|
# piker: trading gear for hackers
|
||||||
|
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Chart trading at it's finest.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pprint import pformat
|
||||||
|
import time
|
||||||
|
from typing import Optional, Dict, Callable, Any
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import trio
|
||||||
|
|
||||||
|
from ._graphics._lines import LevelLine
|
||||||
|
from ._interaction import LineEditor, ArrowEditor, _order_lines
|
||||||
|
from ..exchange._client import open_ems, OrderBook
|
||||||
|
from ..data._source import Symbol
|
||||||
|
from ..log import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OrderMode:
|
||||||
|
"""Major mode for placing orders on a chart view.
|
||||||
|
|
||||||
|
This is the default mode that pairs with "follow mode"
|
||||||
|
(when wathing the rt price update at the current time step)
|
||||||
|
and allows entering orders using the ``a, d, f`` keys and
|
||||||
|
cancelling moused-over orders with the ``c`` key.
|
||||||
|
|
||||||
|
"""
|
||||||
|
chart: 'ChartPlotWidget' # type: ignore # noqa
|
||||||
|
book: OrderBook
|
||||||
|
lines: LineEditor
|
||||||
|
arrows: ArrowEditor
|
||||||
|
_colors = {
|
||||||
|
'alert': 'alert_yellow',
|
||||||
|
'buy': 'buy_green',
|
||||||
|
'sell': 'sell_red',
|
||||||
|
}
|
||||||
|
_action: str = 'alert'
|
||||||
|
_exec_mode: str = 'dark'
|
||||||
|
_size: float = 100.0
|
||||||
|
|
||||||
|
key_map: Dict[str, Callable] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def uuid(self) -> str:
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
def set_exec(
|
||||||
|
self,
|
||||||
|
action: str,
|
||||||
|
size: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set execution mode.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._action = action
|
||||||
|
self.lines.stage_line(
|
||||||
|
color=self._colors[action],
|
||||||
|
# hl_on_hover=True if self._exec_mode == 'live' else False,
|
||||||
|
dotted=True if self._exec_mode == 'dark' else False,
|
||||||
|
size=size or self._size,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_submit(self, uuid: str) -> dict:
|
||||||
|
"""On order submitted event, commit the order line
|
||||||
|
and registered order uuid, store ack time stamp.
|
||||||
|
|
||||||
|
TODO: annotate order line with submission type ('live' vs.
|
||||||
|
'dark').
|
||||||
|
|
||||||
|
"""
|
||||||
|
line = self.lines.commit_line(uuid)
|
||||||
|
req_msg = self.book._sent_orders.get(uuid)
|
||||||
|
if req_msg:
|
||||||
|
req_msg['ack_time_ns'] = time.time_ns()
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
def on_fill(
|
||||||
|
self,
|
||||||
|
uuid: str,
|
||||||
|
price: float,
|
||||||
|
arrow_index: float,
|
||||||
|
pointing: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
line = self.lines._order_lines.get(uuid)
|
||||||
|
if line:
|
||||||
|
self.arrows.add(
|
||||||
|
uuid,
|
||||||
|
arrow_index,
|
||||||
|
price,
|
||||||
|
pointing=pointing,
|
||||||
|
color=line.color
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_exec(
|
||||||
|
self,
|
||||||
|
uuid: str,
|
||||||
|
msg: Dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# only once all fills have cleared and the execution
|
||||||
|
# is complet do we remove our "order line"
|
||||||
|
line = self.lines.remove_line(uuid=uuid)
|
||||||
|
log.debug(f'deleting {line} with oid: {uuid}')
|
||||||
|
|
||||||
|
# DESKTOP NOTIFICATIONS
|
||||||
|
#
|
||||||
|
# TODO: this in another task?
|
||||||
|
# not sure if this will ever be a bottleneck,
|
||||||
|
# we probably could do graphics stuff first tho?
|
||||||
|
|
||||||
|
# XXX: linux only for now
|
||||||
|
result = await trio.run_process(
|
||||||
|
[
|
||||||
|
'notify-send',
|
||||||
|
'-u', 'normal',
|
||||||
|
'-t', '10000',
|
||||||
|
'piker',
|
||||||
|
f'alert: {msg}',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
log.runtime(result)
|
||||||
|
|
||||||
|
def on_cancel(self, uuid: str) -> None:
|
||||||
|
msg = self.book._sent_orders.pop(uuid, None)
|
||||||
|
if msg is not None:
|
||||||
|
self.lines.remove_line(uuid=uuid)
|
||||||
|
self.chart._cursor.show_xhair()
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f'Received cancel for unsubmitted order {pformat(msg)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def submit_exec(
|
||||||
|
self,
|
||||||
|
size: Optional[float] = None,
|
||||||
|
) -> LevelLine:
|
||||||
|
"""Send execution 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).
|
||||||
|
uid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
size = size or self._size
|
||||||
|
|
||||||
|
chart = self.chart._cursor.active_plot
|
||||||
|
y = chart._cursor._datum_xy[1]
|
||||||
|
|
||||||
|
symbol = self.chart._lc._symbol
|
||||||
|
|
||||||
|
# send order cmd to ems
|
||||||
|
self.book.send(
|
||||||
|
uuid=uid,
|
||||||
|
symbol=symbol.key,
|
||||||
|
brokers=symbol.brokers,
|
||||||
|
price=y,
|
||||||
|
size=size,
|
||||||
|
action=self._action,
|
||||||
|
exec_mode=self._exec_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
# make line graphic if order push was
|
||||||
|
# sucessful
|
||||||
|
line = self.lines.create_order_line(
|
||||||
|
uid,
|
||||||
|
level=y,
|
||||||
|
chart=chart,
|
||||||
|
size=size,
|
||||||
|
)
|
||||||
|
line.oid = uid
|
||||||
|
|
||||||
|
# hook up mouse drag handlers
|
||||||
|
line._on_drag_start = self.order_line_modify_start
|
||||||
|
line._on_drag_end = self.order_line_modify_complete
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
def cancel_order_under_cursor(self) -> None:
|
||||||
|
for line in self.lines.lines_under_cursor():
|
||||||
|
self.book.cancel(uuid=line.oid)
|
||||||
|
|
||||||
|
# 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.oid,
|
||||||
|
|
||||||
|
# TODO: should we round this to a nearest tick here?
|
||||||
|
price=line.value(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def open_order_mode(
|
||||||
|
symbol: Symbol,
|
||||||
|
chart: pg.PlotWidget,
|
||||||
|
book: OrderBook,
|
||||||
|
):
|
||||||
|
view = chart._vb
|
||||||
|
lines = LineEditor(view=view, chart=chart, _order_lines=_order_lines)
|
||||||
|
arrows = ArrowEditor(chart, {})
|
||||||
|
|
||||||
|
log.info("Opening order mode")
|
||||||
|
|
||||||
|
mode = OrderMode(chart, book, lines, arrows)
|
||||||
|
view.mode = mode
|
||||||
|
|
||||||
|
asset_type = symbol.type_key
|
||||||
|
|
||||||
|
if asset_type == 'stock':
|
||||||
|
mode._size = 100.0
|
||||||
|
|
||||||
|
elif asset_type in ('future', 'option', 'futures_option'):
|
||||||
|
mode._size = 1.0
|
||||||
|
|
||||||
|
else: # to be safe
|
||||||
|
mode._size = 1.0
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
async def start_order_mode(
|
||||||
|
chart: 'ChartPlotWidget', # noqa
|
||||||
|
symbol: Symbol,
|
||||||
|
brokername: str,
|
||||||
|
) -> None:
|
||||||
|
# spawn EMS actor-service
|
||||||
|
async with open_ems(
|
||||||
|
brokername,
|
||||||
|
symbol,
|
||||||
|
) as (book, trades_stream):
|
||||||
|
|
||||||
|
async with open_order_mode(
|
||||||
|
symbol,
|
||||||
|
chart,
|
||||||
|
book,
|
||||||
|
) as order_mode:
|
||||||
|
|
||||||
|
def get_index(time: float):
|
||||||
|
# XXX: not sure why the time is so off here
|
||||||
|
# looks like we're gonna have to do some fixing..
|
||||||
|
ohlc = chart._shm.array
|
||||||
|
indexes = ohlc['time'] >= time
|
||||||
|
|
||||||
|
if any(indexes):
|
||||||
|
return ohlc['index'][indexes[-1]]
|
||||||
|
else:
|
||||||
|
return ohlc['index'][-1]
|
||||||
|
|
||||||
|
# Begin order-response streaming
|
||||||
|
|
||||||
|
# this is where we receive **back** messages
|
||||||
|
# about executions **from** the EMS actor
|
||||||
|
async for msg in trades_stream:
|
||||||
|
|
||||||
|
fmsg = pformat(msg)
|
||||||
|
log.info(f'Received order msg:\n{fmsg}')
|
||||||
|
|
||||||
|
# delete the line from view
|
||||||
|
oid = msg['oid']
|
||||||
|
resp = msg['resp']
|
||||||
|
|
||||||
|
# response to 'action' request (buy/sell)
|
||||||
|
if resp in (
|
||||||
|
'dark_submitted',
|
||||||
|
'broker_submitted'
|
||||||
|
):
|
||||||
|
|
||||||
|
# show line label once order is live
|
||||||
|
order_mode.on_submit(oid)
|
||||||
|
|
||||||
|
# resp to 'cancel' request or error condition
|
||||||
|
# for action request
|
||||||
|
elif resp in (
|
||||||
|
'broker_cancelled',
|
||||||
|
'broker_inactive',
|
||||||
|
'dark_cancelled'
|
||||||
|
):
|
||||||
|
# delete level line from view
|
||||||
|
order_mode.on_cancel(oid)
|
||||||
|
|
||||||
|
elif resp in (
|
||||||
|
'dark_executed'
|
||||||
|
):
|
||||||
|
log.info(f'Dark order triggered for {fmsg}')
|
||||||
|
|
||||||
|
# for alerts add a triangle and remove the
|
||||||
|
# level line
|
||||||
|
if msg['cmd']['action'] == 'alert':
|
||||||
|
|
||||||
|
# should only be one "fill" for an alert
|
||||||
|
order_mode.on_fill(
|
||||||
|
oid,
|
||||||
|
price=msg['trigger_price'],
|
||||||
|
arrow_index=get_index(time.time())
|
||||||
|
)
|
||||||
|
await order_mode.on_exec(oid, msg)
|
||||||
|
|
||||||
|
# response to completed 'action' request for buy/sell
|
||||||
|
elif resp in (
|
||||||
|
'broker_executed',
|
||||||
|
):
|
||||||
|
await order_mode.on_exec(oid, msg)
|
||||||
|
|
||||||
|
# each clearing tick is responded individually
|
||||||
|
elif resp in ('broker_filled',):
|
||||||
|
action = msg['action']
|
||||||
|
# TODO: some kinda progress system
|
||||||
|
order_mode.on_fill(
|
||||||
|
oid,
|
||||||
|
price=msg['price'],
|
||||||
|
arrow_index=get_index(msg['broker_time']),
|
||||||
|
pointing='up' if action == 'buy' else 'down',
|
||||||
|
)
|
Loading…
Reference in New Issue