commit
86cb8421d9
|
@ -1419,6 +1419,7 @@ async def trades_dialogue(
|
||||||
# - short-sale but securities haven't been located, in this
|
# - short-sale but securities haven't been located, in this
|
||||||
# case we should probably keep the order in some kind of
|
# case we should probably keep the order in some kind of
|
||||||
# weird state or cancel it outright?
|
# weird state or cancel it outright?
|
||||||
|
|
||||||
# status='PendingSubmit', message=''),
|
# status='PendingSubmit', message=''),
|
||||||
# status='Cancelled', message='Error 404,
|
# status='Cancelled', message='Error 404,
|
||||||
# reqId 1550: Order held while securities are located.'),
|
# reqId 1550: Order held while securities are located.'),
|
||||||
|
|
|
@ -35,7 +35,7 @@ from ..data._normalize import iterticks
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._messages import (
|
from ._messages import (
|
||||||
BrokerdCancel, BrokerdOrder, BrokerdOrderAck, BrokerdStatus,
|
BrokerdCancel, BrokerdOrder, BrokerdOrderAck, BrokerdStatus,
|
||||||
BrokerdFill,
|
BrokerdFill, BrokerdPosition,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ class PaperBoi:
|
||||||
_buys: bidict
|
_buys: bidict
|
||||||
_sells: bidict
|
_sells: bidict
|
||||||
_reqids: bidict
|
_reqids: bidict
|
||||||
|
_positions: dict[str, BrokerdPosition]
|
||||||
|
|
||||||
# init edge case L1 spread
|
# init edge case L1 spread
|
||||||
last_ask: Tuple[float, float] = (float('inf'), 0) # price, size
|
last_ask: Tuple[float, float] = (float('inf'), 0) # price, size
|
||||||
|
@ -101,6 +102,9 @@ class PaperBoi:
|
||||||
# in the broker trades event processing loop
|
# in the broker trades event processing loop
|
||||||
await trio.sleep(0.05)
|
await trio.sleep(0.05)
|
||||||
|
|
||||||
|
if action == 'sell':
|
||||||
|
size = -size
|
||||||
|
|
||||||
msg = BrokerdStatus(
|
msg = BrokerdStatus(
|
||||||
status='submitted',
|
status='submitted',
|
||||||
reqid=reqid,
|
reqid=reqid,
|
||||||
|
@ -118,7 +122,7 @@ class PaperBoi:
|
||||||
) or (
|
) or (
|
||||||
action == 'sell' and (clear_price := self.last_bid[0]) >= price
|
action == 'sell' and (clear_price := self.last_bid[0]) >= price
|
||||||
):
|
):
|
||||||
await self.fake_fill(clear_price, size, action, reqid, oid)
|
await self.fake_fill(symbol, clear_price, size, action, reqid, oid)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# register this submissions as a paper live order
|
# register this submissions as a paper live order
|
||||||
|
@ -170,6 +174,8 @@ class PaperBoi:
|
||||||
|
|
||||||
async def fake_fill(
|
async def fake_fill(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
|
symbol: str,
|
||||||
price: float,
|
price: float,
|
||||||
size: float,
|
size: float,
|
||||||
action: str, # one of {'buy', 'sell'}
|
action: str, # one of {'buy', 'sell'}
|
||||||
|
@ -181,6 +187,7 @@ class PaperBoi:
|
||||||
# remaining lots to fill
|
# remaining lots to fill
|
||||||
order_complete: bool = True,
|
order_complete: bool = True,
|
||||||
remaining: float = 0,
|
remaining: float = 0,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Pretend to fill a broker order @ price and size.
|
"""Pretend to fill a broker order @ price and size.
|
||||||
|
|
||||||
|
@ -232,6 +239,49 @@ class PaperBoi:
|
||||||
)
|
)
|
||||||
await self.ems_trades_stream.send(msg.dict())
|
await self.ems_trades_stream.send(msg.dict())
|
||||||
|
|
||||||
|
# lookup any existing position
|
||||||
|
token = f'{symbol}.{self.broker}'
|
||||||
|
pp_msg = self._positions.setdefault(
|
||||||
|
token,
|
||||||
|
BrokerdPosition(
|
||||||
|
broker=self.broker,
|
||||||
|
account='paper',
|
||||||
|
symbol=symbol,
|
||||||
|
# TODO: we need to look up the asset currency from
|
||||||
|
# broker info. i guess for crypto this can be
|
||||||
|
# inferred from the pair?
|
||||||
|
currency='',
|
||||||
|
size=0.0,
|
||||||
|
avg_price=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# "avg position price" calcs
|
||||||
|
# TODO: eventually it'd be nice to have a small set of routines
|
||||||
|
# to do this stuff from a sequence of cleared orders to enable
|
||||||
|
# so called "contextual positions".
|
||||||
|
new_size = size + pp_msg.size
|
||||||
|
|
||||||
|
# old size minus the new size gives us size differential with
|
||||||
|
# +ve -> increase in pp size
|
||||||
|
# -ve -> decrease in pp size
|
||||||
|
size_diff = abs(new_size) - abs(pp_msg.size)
|
||||||
|
|
||||||
|
if new_size == 0:
|
||||||
|
pp_msg.avg_price = 0
|
||||||
|
|
||||||
|
elif size_diff > 0:
|
||||||
|
# only update the "average position price" when the position
|
||||||
|
# size increases not when it decreases (i.e. the position is
|
||||||
|
# being made smaller)
|
||||||
|
pp_msg.avg_price = (
|
||||||
|
abs(size) * price + pp_msg.avg_price * abs(pp_msg.size)
|
||||||
|
) / abs(new_size)
|
||||||
|
|
||||||
|
pp_msg.size = new_size
|
||||||
|
|
||||||
|
await self.ems_trades_stream.send(pp_msg.dict())
|
||||||
|
|
||||||
|
|
||||||
async def simulate_fills(
|
async def simulate_fills(
|
||||||
quote_stream: 'tractor.ReceiveStream', # noqa
|
quote_stream: 'tractor.ReceiveStream', # noqa
|
||||||
|
@ -255,6 +305,7 @@ async def simulate_fills(
|
||||||
|
|
||||||
# this stream may eventually contain multiple symbols
|
# this stream may eventually contain multiple symbols
|
||||||
async for quotes in quote_stream:
|
async for quotes in quote_stream:
|
||||||
|
|
||||||
for sym, quote in quotes.items():
|
for sym, quote in quotes.items():
|
||||||
|
|
||||||
for tick in iterticks(
|
for tick in iterticks(
|
||||||
|
@ -274,6 +325,7 @@ async def simulate_fills(
|
||||||
)
|
)
|
||||||
|
|
||||||
orders = client._buys.get(sym, {})
|
orders = client._buys.get(sym, {})
|
||||||
|
|
||||||
book_sequence = reversed(
|
book_sequence = reversed(
|
||||||
sorted(orders.keys(), key=itemgetter(1)))
|
sorted(orders.keys(), key=itemgetter(1)))
|
||||||
|
|
||||||
|
@ -307,6 +359,7 @@ async def simulate_fills(
|
||||||
|
|
||||||
# clearing price would have filled entirely
|
# clearing price would have filled entirely
|
||||||
await client.fake_fill(
|
await client.fake_fill(
|
||||||
|
symbol=sym,
|
||||||
# todo slippage to determine fill price
|
# todo slippage to determine fill price
|
||||||
price=tick_price,
|
price=tick_price,
|
||||||
size=size,
|
size=size,
|
||||||
|
@ -411,6 +464,9 @@ async def trades_dialogue(
|
||||||
_sells={},
|
_sells={},
|
||||||
|
|
||||||
_reqids={},
|
_reqids={},
|
||||||
|
|
||||||
|
# TODO: load paper positions from ``positions.toml``
|
||||||
|
_positions={},
|
||||||
)
|
)
|
||||||
|
|
||||||
n.start_soon(handle_order_requests, client, ems_stream)
|
n.start_soon(handle_order_requests, client, ems_stream)
|
||||||
|
@ -452,10 +508,5 @@ async def open_paperboi(
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
|
|
||||||
) as (ctx, first):
|
) as (ctx, first):
|
||||||
try:
|
|
||||||
yield ctx, first
|
|
||||||
|
|
||||||
finally:
|
yield ctx, first
|
||||||
# be sure to tear down the paper service on exit
|
|
||||||
with trio.CancelScope(shield=True):
|
|
||||||
await portal.cancel_actor()
|
|
||||||
|
|
|
@ -26,9 +26,11 @@ import numpy as np
|
||||||
|
|
||||||
|
|
||||||
def mk_marker(
|
def mk_marker(
|
||||||
|
|
||||||
style,
|
style,
|
||||||
size: float = 20.0,
|
size: float = 20.0,
|
||||||
use_qgpath: bool = True,
|
use_qgpath: bool = True,
|
||||||
|
|
||||||
) -> QGraphicsPathItem:
|
) -> QGraphicsPathItem:
|
||||||
"""Add a marker to be displayed on the line wrapped in a ``QGraphicsPathItem``
|
"""Add a marker to be displayed on the line wrapped in a ``QGraphicsPathItem``
|
||||||
ready to be placed using scene coordinates (not view).
|
ready to be placed using scene coordinates (not view).
|
||||||
|
|
|
@ -40,13 +40,13 @@ from ._axes import (
|
||||||
PriceAxis,
|
PriceAxis,
|
||||||
YAxisLabel,
|
YAxisLabel,
|
||||||
)
|
)
|
||||||
from ._graphics._cursor import (
|
from ._cursor import (
|
||||||
Cursor,
|
Cursor,
|
||||||
ContentsLabel,
|
ContentsLabel,
|
||||||
)
|
)
|
||||||
from ._l1 import L1Labels
|
from ._l1 import L1Labels
|
||||||
from ._graphics._ohlc import BarItems
|
from ._ohlc import BarItems
|
||||||
from ._graphics._curve import FastAppendCurve
|
from ._curve import FastAppendCurve
|
||||||
from ._style import (
|
from ._style import (
|
||||||
hcolor,
|
hcolor,
|
||||||
CHART_MARGINS,
|
CHART_MARGINS,
|
||||||
|
@ -296,7 +296,7 @@ class LinkedSplits(QtWidgets.QWidget):
|
||||||
|
|
||||||
def set_split_sizes(
|
def set_split_sizes(
|
||||||
self,
|
self,
|
||||||
prop: float = 0.28 # proportion allocated to consumer subcharts
|
prop: float = 0.375 # proportion allocated to consumer subcharts
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the proportion of space allocated for linked subcharts.
|
"""Set the proportion of space allocated for linked subcharts.
|
||||||
"""
|
"""
|
||||||
|
@ -1317,7 +1317,7 @@ async def run_fsp(
|
||||||
# graphics.curve.setFillLevel(50)
|
# graphics.curve.setFillLevel(50)
|
||||||
|
|
||||||
if fsp_func_name == 'rsi':
|
if fsp_func_name == 'rsi':
|
||||||
from ._graphics._lines import level_line
|
from ._lines import level_line
|
||||||
# add moveable over-[sold/bought] lines
|
# add moveable over-[sold/bought] lines
|
||||||
# and labels only for the 70/30 lines
|
# and labels only for the 70/30 lines
|
||||||
level_line(chart, 20)
|
level_line(chart, 20)
|
||||||
|
|
|
@ -27,13 +27,13 @@ import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
from PyQt5.QtCore import QPointF, QRectF
|
from PyQt5.QtCore import QPointF, QRectF
|
||||||
|
|
||||||
from .._style import (
|
from ._style import (
|
||||||
_xaxis_at,
|
_xaxis_at,
|
||||||
hcolor,
|
hcolor,
|
||||||
_font_small,
|
_font_small,
|
||||||
)
|
)
|
||||||
from .._axes import YAxisLabel, XAxisLabel
|
from ._axes import YAxisLabel, XAxisLabel
|
||||||
from ...log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
|
@ -23,7 +23,7 @@ from typing import Tuple
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
from ..._profile import pg_profile_enabled
|
from .._profile import pg_profile_enabled
|
||||||
|
|
||||||
|
|
||||||
# TODO: got a feeling that dropping this inheritance gets us even more speedups
|
# TODO: got a feeling that dropping this inheritance gets us even more speedups
|
|
@ -28,7 +28,7 @@ from PyQt5.QtCore import QPointF
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from ._style import hcolor, _font
|
from ._style import hcolor, _font
|
||||||
from ._graphics._lines import order_line, LevelLine
|
from ._lines import order_line, LevelLine
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -237,7 +237,6 @@ class LineEditor:
|
||||||
log.warning(f'No line for {uuid} could be found?')
|
log.warning(f'No line for {uuid} could be found?')
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
assert line.oid == uuid
|
|
||||||
line.show_labels()
|
line.show_labels()
|
||||||
|
|
||||||
# TODO: other flashy things to indicate the order is active
|
# TODO: other flashy things to indicate the order is active
|
||||||
|
@ -260,18 +259,16 @@ class LineEditor:
|
||||||
self,
|
self,
|
||||||
line: LevelLine = None,
|
line: LevelLine = None,
|
||||||
uuid: str = None,
|
uuid: str = None,
|
||||||
) -> LevelLine:
|
|
||||||
"""Remove a line by refernce or uuid.
|
) -> Optional[LevelLine]:
|
||||||
|
'''Remove a line by refernce or uuid.
|
||||||
|
|
||||||
If no lines or ids are provided remove all lines under the
|
If no lines or ids are provided remove all lines under the
|
||||||
cursor position.
|
cursor position.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
if line:
|
|
||||||
uuid = line.oid
|
|
||||||
|
|
||||||
# try to look up line from our registry
|
# try to look up line from our registry
|
||||||
line = self._order_lines.pop(uuid, None)
|
line = self._order_lines.pop(uuid, line)
|
||||||
if line:
|
if line:
|
||||||
|
|
||||||
# if hovered remove from cursor set
|
# if hovered remove from cursor set
|
||||||
|
@ -284,8 +281,13 @@ class LineEditor:
|
||||||
# just because we never got a un-hover event
|
# just because we never got a un-hover event
|
||||||
cursor.show_xhair()
|
cursor.show_xhair()
|
||||||
|
|
||||||
|
log.debug(f'deleting {line} with oid: {uuid}')
|
||||||
line.delete()
|
line.delete()
|
||||||
return line
|
|
||||||
|
else:
|
||||||
|
log.warning(f'Could not find line for {line}')
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
class SelectRect(QtGui.QGraphicsRectItem):
|
class SelectRect(QtGui.QGraphicsRectItem):
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# piker: trading gear for hackers
|
|
||||||
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of 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/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Internal custom graphics mostly built for low latency and reuse.
|
|
||||||
|
|
||||||
"""
|
|
|
@ -26,9 +26,9 @@ from pyqtgraph import Point, functions as fn
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
from PyQt5.QtCore import QPointF
|
from PyQt5.QtCore import QPointF
|
||||||
|
|
||||||
from .._annotate import mk_marker, qgo_draw_markers
|
from ._annotate import mk_marker, qgo_draw_markers
|
||||||
from .._label import Label, vbr_left, right_axis
|
from ._label import Label, vbr_left, right_axis
|
||||||
from .._style import hcolor, _font
|
from ._style import hcolor, _font
|
||||||
|
|
||||||
|
|
||||||
# TODO: probably worth investigating if we can
|
# TODO: probably worth investigating if we can
|
||||||
|
@ -352,6 +352,21 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
return up_to_l1_sc
|
return up_to_l1_sc
|
||||||
|
|
||||||
|
def marker_right_points(self) -> (float, float, float):
|
||||||
|
|
||||||
|
chart = self._chart
|
||||||
|
l1_len = chart._max_l1_line_len
|
||||||
|
ryaxis = chart.getAxis('right')
|
||||||
|
|
||||||
|
r_axis_x = ryaxis.pos().x()
|
||||||
|
up_to_l1_sc = r_axis_x - l1_len
|
||||||
|
|
||||||
|
size = self._default_mkr_size
|
||||||
|
marker_right = up_to_l1_sc - (1.375 * 2*size)
|
||||||
|
line_end = marker_right - (6/16 * size)
|
||||||
|
|
||||||
|
return line_end, marker_right, r_axis_x
|
||||||
|
|
||||||
def paint(
|
def paint(
|
||||||
self,
|
self,
|
||||||
p: QtGui.QPainter,
|
p: QtGui.QPainter,
|
||||||
|
@ -366,26 +381,14 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
# these are in viewbox coords
|
# these are in viewbox coords
|
||||||
vb_left, vb_right = self._endPoints
|
vb_left, vb_right = self._endPoints
|
||||||
|
|
||||||
chart = self._chart
|
|
||||||
l1_len = chart._max_l1_line_len
|
|
||||||
ryaxis = chart.getAxis('right')
|
|
||||||
|
|
||||||
r_axis_x = ryaxis.pos().x()
|
|
||||||
up_to_l1_sc = r_axis_x - l1_len
|
|
||||||
|
|
||||||
vb = self.getViewBox()
|
vb = self.getViewBox()
|
||||||
|
|
||||||
size = self._default_mkr_size
|
line_end, marker_right, r_axis_x = self.marker_right_points()
|
||||||
marker_right = up_to_l1_sc - (1.375 * 2*size)
|
|
||||||
line_end = marker_right - (6/16 * size)
|
|
||||||
|
|
||||||
if self.show_markers and self.markers:
|
if self.show_markers and self.markers:
|
||||||
|
|
||||||
size = self.markers[0][2]
|
|
||||||
|
|
||||||
p.setPen(self.pen)
|
p.setPen(self.pen)
|
||||||
size = qgo_draw_markers(
|
qgo_draw_markers(
|
||||||
self.markers,
|
self.markers,
|
||||||
self.pen.color(),
|
self.pen.color(),
|
||||||
p,
|
p,
|
||||||
|
@ -438,9 +441,8 @@ class LevelLine(pg.InfiniteLine):
|
||||||
path: QtWidgets.QGraphicsPathItem,
|
path: QtWidgets.QGraphicsPathItem,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# chart = self._chart
|
# add path to scene
|
||||||
vb = self.getViewBox()
|
self.getViewBox().scene().addItem(path)
|
||||||
vb.scene().addItem(path)
|
|
||||||
|
|
||||||
self._marker = path
|
self._marker = path
|
||||||
|
|
||||||
|
@ -758,27 +760,31 @@ def position_line(
|
||||||
vr = vb.state['viewRange']
|
vr = vb.state['viewRange']
|
||||||
ymn, ymx = vr[1]
|
ymn, ymx = vr[1]
|
||||||
level = line.value()
|
level = line.value()
|
||||||
|
path = line._marker
|
||||||
|
|
||||||
if gt := level > ymx or (lt := level < ymn):
|
# provide "nav hub" like indicator for where
|
||||||
|
# the position is on the y-dimension
|
||||||
|
# print(path._height)
|
||||||
|
# print(vb.shape())
|
||||||
|
# print(vb.boundingRect())
|
||||||
|
# print(vb.height())
|
||||||
|
_, marker_right, _ = line.marker_right_points()
|
||||||
|
|
||||||
if chartview.mode.name == 'order':
|
if level > ymx: # pin to top of view
|
||||||
|
path.setPos(
|
||||||
|
QPointF(
|
||||||
|
marker_right,
|
||||||
|
2 + path._height,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# provide "nav hub" like indicator for where
|
elif level < ymn: # pin to bottom of view
|
||||||
# the position is on the y-dimension
|
path.setPos(
|
||||||
if gt:
|
QPointF(
|
||||||
# pin to top of view since position is above current
|
marker_right,
|
||||||
# y-range
|
vb.height() - 16 + path._height,
|
||||||
pass
|
)
|
||||||
|
)
|
||||||
elif lt:
|
|
||||||
# pin to bottom of view since position is above
|
|
||||||
# below y-range
|
|
||||||
pass
|
|
||||||
|
|
||||||
else:
|
|
||||||
# order mode is not active
|
|
||||||
# so hide the pp market
|
|
||||||
line._marker.hide()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# pp line is viewable so show marker
|
# pp line is viewable so show marker
|
||||||
|
@ -812,6 +818,10 @@ def position_line(
|
||||||
style = '>|'
|
style = '>|'
|
||||||
|
|
||||||
arrow_path = mk_marker(style, size=arrow_size)
|
arrow_path = mk_marker(style, size=arrow_size)
|
||||||
|
|
||||||
|
# monkey-cache height for sizing on pp nav-hub
|
||||||
|
arrow_path._height = arrow_path.boundingRect().height()
|
||||||
|
|
||||||
# XXX: uses new marker drawing approach
|
# XXX: uses new marker drawing approach
|
||||||
line.add_marker(arrow_path)
|
line.add_marker(arrow_path)
|
||||||
line.set_level(level)
|
line.set_level(level)
|
|
@ -27,8 +27,8 @@ from PyQt5.QtCore import QLineF, QPointF
|
||||||
# from numba import types as ntypes
|
# from numba import types as ntypes
|
||||||
# from ..data._source import numba_ohlc_dtype
|
# from ..data._source import numba_ohlc_dtype
|
||||||
|
|
||||||
from ..._profile import pg_profile_enabled
|
from .._profile import pg_profile_enabled
|
||||||
from .._style import hcolor
|
from ._style import hcolor
|
||||||
|
|
||||||
|
|
||||||
def _mk_lines_array(
|
def _mk_lines_array(
|
|
@ -29,7 +29,7 @@ import pyqtgraph as pg
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from ._graphics._lines import LevelLine, position_line
|
from ._lines import LevelLine, position_line
|
||||||
from ._editors import LineEditor, ArrowEditor
|
from ._editors import LineEditor, ArrowEditor
|
||||||
from ._window import MultiStatus, main_window
|
from ._window import MultiStatus, main_window
|
||||||
from ..clearing._client import open_ems, OrderBook
|
from ..clearing._client import open_ems, OrderBook
|
||||||
|
@ -41,12 +41,31 @@ log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Position(BaseModel):
|
class Position(BaseModel):
|
||||||
|
'''Basic pp representation with attached fills history.
|
||||||
|
|
||||||
|
'''
|
||||||
symbol: Symbol
|
symbol: Symbol
|
||||||
size: float
|
size: float
|
||||||
avg_price: float
|
avg_price: float # TODO: contextual pricing
|
||||||
fills: Dict[str, Any] = {}
|
fills: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class OrderDialog(BaseModel):
|
||||||
|
'''Trade dialogue meta-data describing the lifetime
|
||||||
|
of an order submission to ``emsd`` from a chart.
|
||||||
|
|
||||||
|
'''
|
||||||
|
uuid: str
|
||||||
|
line: LevelLine
|
||||||
|
last_status_close: Callable = lambda: None
|
||||||
|
msgs: dict[str, dict] = {}
|
||||||
|
fills: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
underscore_attrs_are_private = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OrderMode:
|
class OrderMode:
|
||||||
'''Major mode for placing orders on a chart view.
|
'''Major mode for placing orders on a chart view.
|
||||||
|
@ -60,8 +79,8 @@ class OrderMode:
|
||||||
Current manual:
|
Current manual:
|
||||||
a -> alert
|
a -> alert
|
||||||
s/ctrl -> submission type modifier {on: live, off: dark}
|
s/ctrl -> submission type modifier {on: live, off: dark}
|
||||||
f (fill) -> buy limit order
|
f (fill) -> 'buy' limit order
|
||||||
d (dump) -> sell limit order
|
d (dump) -> 'sell' limit order
|
||||||
c (cancel) -> cancel order under cursor
|
c (cancel) -> cancel order under cursor
|
||||||
cc -> cancel all submitted orders on chart
|
cc -> cancel all submitted orders on chart
|
||||||
mouse click and drag -> modify current order under cursor
|
mouse click and drag -> modify current order under cursor
|
||||||
|
@ -85,8 +104,7 @@ class OrderMode:
|
||||||
_position: Dict[str, Any] = field(default_factory=dict)
|
_position: Dict[str, Any] = field(default_factory=dict)
|
||||||
_position_line: dict = None
|
_position_line: dict = None
|
||||||
|
|
||||||
_pending_submissions: dict[str, (LevelLine, Callable)] = field(
|
dialogs: dict[str, OrderDialog] = field(default_factory=dict)
|
||||||
default_factory=dict)
|
|
||||||
|
|
||||||
def on_position_update(
|
def on_position_update(
|
||||||
self,
|
self,
|
||||||
|
@ -139,33 +157,34 @@ class OrderMode:
|
||||||
action=action,
|
action=action,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_submit(self, uuid: str) -> dict:
|
def on_submit(self, uuid: str) -> OrderDialog:
|
||||||
"""On order submitted event, commit the order line
|
'''Order submitted status event handler.
|
||||||
and registered order uuid, store ack time stamp.
|
|
||||||
|
|
||||||
TODO: annotate order line with submission type ('live' vs.
|
Commit the order line and registered order uuid, store ack time stamp.
|
||||||
'dark').
|
|
||||||
|
|
||||||
"""
|
'''
|
||||||
line = self.lines.commit_line(uuid)
|
line = self.lines.commit_line(uuid)
|
||||||
|
|
||||||
pending = self._pending_submissions.get(uuid)
|
# a submission is the start of a new order dialog
|
||||||
if pending:
|
dialog = self.dialogs[uuid]
|
||||||
order_line, func = pending
|
dialog.line = line
|
||||||
assert order_line is line
|
dialog.last_status_close()
|
||||||
func()
|
|
||||||
|
|
||||||
return line
|
return dialog
|
||||||
|
|
||||||
def on_fill(
|
def on_fill(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
uuid: str,
|
uuid: str,
|
||||||
price: float,
|
price: float,
|
||||||
arrow_index: float,
|
arrow_index: float,
|
||||||
pointing: Optional[str] = None
|
pointing: Optional[str] = None,
|
||||||
|
# delete_line: bool = False,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
line = self.lines._order_lines.get(uuid)
|
dialog = self.dialogs[uuid]
|
||||||
|
line = dialog.line
|
||||||
if line:
|
if line:
|
||||||
self.arrows.add(
|
self.arrows.add(
|
||||||
uuid,
|
uuid,
|
||||||
|
@ -174,6 +193,8 @@ class OrderMode:
|
||||||
pointing=pointing,
|
pointing=pointing,
|
||||||
color=line.color
|
color=line.color
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
log.warn("No line for order {uuid}!?")
|
||||||
|
|
||||||
async def on_exec(
|
async def on_exec(
|
||||||
self,
|
self,
|
||||||
|
@ -181,11 +202,6 @@ class OrderMode:
|
||||||
msg: Dict[str, Any],
|
msg: Dict[str, Any],
|
||||||
) -> None:
|
) -> 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
|
# DESKTOP NOTIFICATIONS
|
||||||
#
|
#
|
||||||
# TODO: this in another task?
|
# TODO: this in another task?
|
||||||
|
@ -212,10 +228,9 @@ class OrderMode:
|
||||||
self.lines.remove_line(uuid=uuid)
|
self.lines.remove_line(uuid=uuid)
|
||||||
self.chart.linked.cursor.show_xhair()
|
self.chart.linked.cursor.show_xhair()
|
||||||
|
|
||||||
pending = self._pending_submissions.pop(uuid, None)
|
dialog = self.dialogs.pop(uuid, None)
|
||||||
if pending:
|
if dialog:
|
||||||
order_line, func = pending
|
dialog.last_status_close()
|
||||||
func()
|
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
f'Received cancel for unsubmitted order {pformat(msg)}'
|
f'Received cancel for unsubmitted order {pformat(msg)}'
|
||||||
|
@ -225,7 +240,7 @@ class OrderMode:
|
||||||
self,
|
self,
|
||||||
size: Optional[float] = None,
|
size: Optional[float] = None,
|
||||||
|
|
||||||
) -> LevelLine:
|
) -> OrderDialog:
|
||||||
"""Send execution order to EMS return a level line to
|
"""Send execution order to EMS return a level line to
|
||||||
represent the order on a chart.
|
represent the order on a chart.
|
||||||
|
|
||||||
|
@ -234,7 +249,7 @@ class OrderMode:
|
||||||
# to be displayed when above order ack arrives
|
# to be displayed when above order ack arrives
|
||||||
# (means the line graphic doesn't show on screen until the
|
# (means the line graphic doesn't show on screen until the
|
||||||
# order is live in the emsd).
|
# order is live in the emsd).
|
||||||
uid = str(uuid.uuid4())
|
oid = str(uuid.uuid4())
|
||||||
|
|
||||||
size = size or self._size
|
size = size or self._size
|
||||||
|
|
||||||
|
@ -246,9 +261,46 @@ class OrderMode:
|
||||||
|
|
||||||
action = self._action
|
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.status_bar.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
|
# send order cmd to ems
|
||||||
self.book.send(
|
self.book.send(
|
||||||
uuid=uid,
|
uuid=oid,
|
||||||
symbol=symbol.key,
|
symbol=symbol.key,
|
||||||
brokers=symbol.brokers,
|
brokers=symbol.brokers,
|
||||||
price=y,
|
price=y,
|
||||||
|
@ -257,36 +309,7 @@ class OrderMode:
|
||||||
exec_mode=self._exec_mode,
|
exec_mode=self._exec_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: update the line once an ack event comes back
|
return dialog
|
||||||
# from the EMS!
|
|
||||||
|
|
||||||
# make line graphic if order push was
|
|
||||||
# sucessful
|
|
||||||
line = self.lines.create_order_line(
|
|
||||||
uid,
|
|
||||||
level=y,
|
|
||||||
chart=chart,
|
|
||||||
size=size,
|
|
||||||
action=action,
|
|
||||||
)
|
|
||||||
line.oid = uid
|
|
||||||
|
|
||||||
# enter submission which will be popped once a response
|
|
||||||
# from the EMS is received to move the order to a different# status
|
|
||||||
self._pending_submissions[uid] = (
|
|
||||||
line,
|
|
||||||
self.status_bar.open_status(
|
|
||||||
f'submitting {self._exec_mode}-{action}',
|
|
||||||
final_msg=f'submitted {self._exec_mode}-{action}',
|
|
||||||
clear_on_next=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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_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(
|
||||||
|
@ -317,16 +340,16 @@ class OrderMode:
|
||||||
|
|
||||||
# cancel all active orders and triggers
|
# cancel all active orders and triggers
|
||||||
for line in lines:
|
for line in lines:
|
||||||
oid = getattr(line, 'oid', None)
|
dialog = getattr(line, 'dialog', None)
|
||||||
|
|
||||||
if oid:
|
if dialog:
|
||||||
self._pending_submissions[oid] = (
|
oid = dialog.uuid
|
||||||
line,
|
|
||||||
self.status_bar.open_status(
|
cancel_status_close = self.status_bar.open_status(
|
||||||
f'cancelling order {oid[:6]}',
|
f'cancelling order {oid[:6]}',
|
||||||
group_key=key,
|
group_key=key,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
dialog.last_status_close = cancel_status_close
|
||||||
|
|
||||||
ids.append(oid)
|
ids.append(oid)
|
||||||
self.book.cancel(uuid=oid)
|
self.book.cancel(uuid=oid)
|
||||||
|
@ -338,16 +361,20 @@ class OrderMode:
|
||||||
def order_line_modify_start(
|
def order_line_modify_start(
|
||||||
self,
|
self,
|
||||||
line: LevelLine,
|
line: LevelLine,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
print(f'Line modify: {line}')
|
print(f'Line modify: {line}')
|
||||||
# cancel original order until new position is found
|
# cancel original order until new position is found
|
||||||
|
|
||||||
def order_line_modify_complete(
|
def order_line_modify_complete(
|
||||||
self,
|
self,
|
||||||
line: LevelLine,
|
line: LevelLine,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.book.update(
|
self.book.update(
|
||||||
uuid=line.oid,
|
uuid=line.dialog.uuid,
|
||||||
|
|
||||||
# TODO: should we round this to a nearest tick here?
|
# TODO: should we round this to a nearest tick here?
|
||||||
price=line.value(),
|
price=line.value(),
|
||||||
|
@ -464,6 +491,10 @@ async def start_order_mode(
|
||||||
resp = msg['resp']
|
resp = msg['resp']
|
||||||
oid = msg['oid']
|
oid = msg['oid']
|
||||||
|
|
||||||
|
dialog = order_mode.dialogs[oid]
|
||||||
|
# record message to dialog tracking
|
||||||
|
dialog.msgs[oid] = msg
|
||||||
|
|
||||||
# response to 'action' request (buy/sell)
|
# response to 'action' request (buy/sell)
|
||||||
if resp in (
|
if resp in (
|
||||||
'dark_submitted',
|
'dark_submitted',
|
||||||
|
@ -496,16 +527,21 @@ async def start_order_mode(
|
||||||
order_mode.on_fill(
|
order_mode.on_fill(
|
||||||
oid,
|
oid,
|
||||||
price=msg['trigger_price'],
|
price=msg['trigger_price'],
|
||||||
arrow_index=get_index(time.time())
|
arrow_index=get_index(time.time()),
|
||||||
)
|
)
|
||||||
|
order_mode.lines.remove_line(uuid=oid)
|
||||||
await order_mode.on_exec(oid, msg)
|
await order_mode.on_exec(oid, msg)
|
||||||
|
|
||||||
# response to completed 'action' request for buy/sell
|
# response to completed 'action' request for buy/sell
|
||||||
elif resp in (
|
elif resp in (
|
||||||
'broker_executed',
|
'broker_executed',
|
||||||
):
|
):
|
||||||
|
# right now this is just triggering a system alert
|
||||||
await order_mode.on_exec(oid, msg)
|
await order_mode.on_exec(oid, msg)
|
||||||
|
|
||||||
|
if msg['brokerd_msg']['remaining'] == 0:
|
||||||
|
order_mode.lines.remove_line(uuid=oid)
|
||||||
|
|
||||||
# each clearing tick is responded individually
|
# each clearing tick is responded individually
|
||||||
elif resp in ('broker_filled',):
|
elif resp in ('broker_filled',):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue