Get order cancellation working
parent
dba8457be9
commit
39e4953a6a
227
piker/_ems.py
227
piker/_ems.py
|
@ -18,12 +18,11 @@
|
||||||
In suit parlance: "Execution management systems"
|
In suit parlance: "Execution management systems"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import (
|
from typing import (
|
||||||
AsyncIterator, Dict, Callable, Tuple,
|
AsyncIterator, Dict, Callable, Tuple,
|
||||||
Any,
|
|
||||||
)
|
)
|
||||||
# import uuid
|
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
|
@ -71,11 +70,6 @@ class OrderBook:
|
||||||
symbol: 'Symbol',
|
symbol: 'Symbol',
|
||||||
price: float
|
price: float
|
||||||
) -> str:
|
) -> str:
|
||||||
# XXX: should make this an explicit attr
|
|
||||||
# it's assigned inside ``.add_plot()``
|
|
||||||
# lc = self.view.linked_charts
|
|
||||||
|
|
||||||
# uid = str(uuid.uuid4())
|
|
||||||
cmd = {
|
cmd = {
|
||||||
'msg': 'alert',
|
'msg': 'alert',
|
||||||
'price': price,
|
'price': price,
|
||||||
|
@ -86,17 +80,22 @@ class OrderBook:
|
||||||
self._sent_orders[uuid] = cmd
|
self._sent_orders[uuid] = cmd
|
||||||
self._to_ems.send_nowait(cmd)
|
self._to_ems.send_nowait(cmd)
|
||||||
|
|
||||||
async def buy(self, price: float) -> str:
|
def buy(self, price: float) -> str:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def sell(self, price: float) -> str:
|
def sell(self, price: float) -> str:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def cancel(self, oid: str) -> bool:
|
def cancel(self, uuid: str) -> bool:
|
||||||
"""Cancel an order (or alert) from the EMS.
|
"""Cancel an order (or alert) from the EMS.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
...
|
cmd = {
|
||||||
|
'msg': 'cancel',
|
||||||
|
'oid': uuid,
|
||||||
|
}
|
||||||
|
self._sent_orders[uuid] = cmd
|
||||||
|
self._to_ems.send_nowait(cmd)
|
||||||
|
|
||||||
# higher level operations
|
# higher level operations
|
||||||
|
|
||||||
|
@ -138,9 +137,7 @@ async def send_order_cmds():
|
||||||
"pushed" from the parent to the EMS actor.
|
"pushed" from the parent to the EMS actor.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
global _from_order_book
|
global _from_order_book
|
||||||
# book = get_orders()
|
|
||||||
|
|
||||||
async for cmd in _from_order_book:
|
async for cmd in _from_order_book:
|
||||||
|
|
||||||
|
@ -148,27 +145,6 @@ async def send_order_cmds():
|
||||||
log.info(f'sending order cmd: {cmd}')
|
log.info(f'sending order cmd: {cmd}')
|
||||||
yield cmd
|
yield cmd
|
||||||
|
|
||||||
# lc = order['chart']
|
|
||||||
# symbol = order['symol']
|
|
||||||
# msg = order['msg']
|
|
||||||
# price = order['price']
|
|
||||||
# oid = order['oid']
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
# oid = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# cmd = {
|
|
||||||
# 'price': price,
|
|
||||||
# 'action': 'alert',
|
|
||||||
# 'symbol': symbol.key,
|
|
||||||
# 'brokers': symbol.brokers,
|
|
||||||
# 'msg': msg,
|
|
||||||
# 'price': price,
|
|
||||||
# 'oid': oid,
|
|
||||||
# }
|
|
||||||
|
|
||||||
# book._sent_orders[oid] = cmd
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: numba all of this
|
# TODO: numba all of this
|
||||||
def mk_check(trigger_price, known_last) -> Callable[[float, float], bool]:
|
def mk_check(trigger_price, known_last) -> Callable[[float, float], bool]:
|
||||||
|
@ -210,13 +186,13 @@ class _ExecBook:
|
||||||
# levels which have an executable action (eg. alert, order, signal)
|
# levels which have an executable action (eg. alert, order, signal)
|
||||||
orders: Dict[
|
orders: Dict[
|
||||||
Tuple[str, str],
|
Tuple[str, str],
|
||||||
Tuple[
|
Dict[
|
||||||
# predicates
|
str, # uuid
|
||||||
Callable[[float], bool],
|
Tuple[
|
||||||
|
Callable[[float], bool], # predicate
|
||||||
# actions
|
str, # name
|
||||||
Callable[[float], Dict[str, Any]],
|
dict, # cmd / msg type
|
||||||
|
]
|
||||||
]
|
]
|
||||||
] = field(default_factory=dict)
|
] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@ -273,6 +249,7 @@ async def exec_orders(
|
||||||
# XXX: optimize this for speed
|
# XXX: optimize this for speed
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
for sym, quote in quotes.items():
|
for sym, quote in quotes.items():
|
||||||
|
|
||||||
execs = book.orders.get((broker, sym))
|
execs = book.orders.get((broker, sym))
|
||||||
|
@ -289,7 +266,7 @@ async def exec_orders(
|
||||||
if not execs:
|
if not execs:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for oid, pred, name, cmd in tuple(execs):
|
for oid, (pred, name, cmd) in tuple(execs.items()):
|
||||||
|
|
||||||
# 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")
|
||||||
|
@ -299,25 +276,19 @@ async def exec_orders(
|
||||||
cmd['index'] = feed.shm._last.value - 1
|
cmd['index'] = feed.shm._last.value - 1
|
||||||
# current shm array index
|
# current shm array index
|
||||||
cmd['trigger_price'] = price
|
cmd['trigger_price'] = price
|
||||||
|
cmd['msg'] = 'executed'
|
||||||
|
|
||||||
await ctx.send_yield(cmd)
|
await ctx.send_yield(cmd)
|
||||||
# await ctx.send_yield({
|
|
||||||
# 'type': 'alert',
|
|
||||||
# '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}')
|
print(f'removing pred for {oid}')
|
||||||
execs.remove((oid, pred, name, cmd))
|
pred, name, cmd = execs.pop(oid)
|
||||||
|
|
||||||
print(f'execs are {execs}')
|
print(f'execs are {execs}')
|
||||||
|
|
||||||
|
print(f'execs scan took: {time.time() - start}')
|
||||||
# feed teardown
|
# feed teardown
|
||||||
|
|
||||||
|
|
||||||
|
@ -333,6 +304,8 @@ async def stream_and_route(ctx, ui_name):
|
||||||
actor = tractor.current_actor()
|
actor = tractor.current_actor()
|
||||||
book = get_book()
|
book = get_book()
|
||||||
|
|
||||||
|
_active_execs: Dict[str, (str, str)] = {}
|
||||||
|
|
||||||
# new router entry point
|
# new router entry point
|
||||||
async with tractor.wait_for_actor(ui_name) as portal:
|
async with tractor.wait_for_actor(ui_name) as portal:
|
||||||
|
|
||||||
|
@ -341,52 +314,63 @@ 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):
|
||||||
|
|
||||||
|
log.info(f'{cmd} received in {actor.uid}')
|
||||||
msg = cmd['msg']
|
msg = cmd['msg']
|
||||||
|
|
||||||
if msg == 'cancel':
|
|
||||||
# TODO:
|
|
||||||
pass
|
|
||||||
|
|
||||||
trigger_price = cmd['price']
|
|
||||||
sym = cmd['symbol']
|
|
||||||
brokers = cmd['brokers']
|
|
||||||
oid = cmd['oid']
|
oid = cmd['oid']
|
||||||
|
|
||||||
if msg == 'alert':
|
if msg == 'cancel':
|
||||||
log.info(f'Alert {cmd} received in {actor.uid}')
|
# destroy exec
|
||||||
|
pred, name, cmd = book.orders[_active_execs[oid]].pop(oid)
|
||||||
|
|
||||||
broker = brokers[0]
|
# ack-cmdond that order is live
|
||||||
last = book.lasts.get((broker, sym))
|
await ctx.send_yield({'msg': 'cancelled', 'oid': oid})
|
||||||
|
|
||||||
if last is None: # spawn new brokerd feed task
|
continue
|
||||||
|
|
||||||
quote = await n.start(
|
elif msg in ('alert', 'buy', 'sell',):
|
||||||
exec_orders,
|
|
||||||
ctx,
|
|
||||||
# TODO: eventually support N-brokers
|
|
||||||
broker,
|
|
||||||
sym,
|
|
||||||
trigger_price,
|
|
||||||
)
|
|
||||||
print(f"received first quote {quote}")
|
|
||||||
|
|
||||||
last = book.lasts[(broker, sym)]
|
trigger_price = cmd['price']
|
||||||
print(f'Known last is {last}')
|
sym = cmd['symbol']
|
||||||
|
brokers = cmd['brokers']
|
||||||
|
|
||||||
# Auto-gen scanner predicate:
|
broker = brokers[0]
|
||||||
# we automatically figure out what the alert check condition
|
last = book.lasts.get((broker, sym))
|
||||||
# should be based on the current first price received from the
|
|
||||||
# feed, instead of being like every other shitty tina platform
|
|
||||||
# that makes the user choose the predicate operator.
|
|
||||||
pred, name = mk_check(trigger_price, last)
|
|
||||||
|
|
||||||
# create list of executions on first entry
|
if last is None: # spawn new brokerd feed task
|
||||||
book.orders.setdefault((broker, sym), []).append(
|
|
||||||
(oid, pred, name, cmd)
|
|
||||||
)
|
|
||||||
|
|
||||||
# ack-respond that order is live
|
quote = await n.start(
|
||||||
await ctx.send_yield({'msg': 'ack', 'oid': oid})
|
exec_orders,
|
||||||
|
ctx,
|
||||||
|
# TODO: eventually support N-brokers
|
||||||
|
broker,
|
||||||
|
sym,
|
||||||
|
trigger_price,
|
||||||
|
)
|
||||||
|
print(f"received first quote {quote}")
|
||||||
|
|
||||||
|
last = book.lasts[(broker, sym)]
|
||||||
|
print(f'Known last is {last}')
|
||||||
|
|
||||||
|
# Auto-gen scanner predicate:
|
||||||
|
# we automatically figure out what the alert check
|
||||||
|
# condition should be based on the current first
|
||||||
|
# price received from the feed, instead of being
|
||||||
|
# like every other shitty tina platform that makes
|
||||||
|
# the user choose the predicate operator.
|
||||||
|
pred, name = mk_check(trigger_price, last)
|
||||||
|
|
||||||
|
# create list of executions on first entry
|
||||||
|
book.orders.setdefault(
|
||||||
|
(broker, sym), {})[oid] = (pred, name, cmd)
|
||||||
|
|
||||||
|
# reverse lookup for cancellations
|
||||||
|
_active_execs[oid] = (broker, sym)
|
||||||
|
|
||||||
|
# ack-cmdond that order is live
|
||||||
|
await ctx.send_yield({
|
||||||
|
'msg': 'active',
|
||||||
|
'oid': oid
|
||||||
|
})
|
||||||
|
|
||||||
# continue and wait on next order cmd
|
# continue and wait on next order cmd
|
||||||
|
|
||||||
|
@ -410,7 +394,7 @@ async def spawn_router_stream_alerts(
|
||||||
|
|
||||||
portal = await n.start_actor(
|
portal = await n.start_actor(
|
||||||
subactor_name,
|
subactor_name,
|
||||||
rpc_module_paths=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
stream = await portal.run(
|
stream = await portal.run(
|
||||||
stream_and_route,
|
stream_and_route,
|
||||||
|
@ -424,51 +408,52 @@ async def spawn_router_stream_alerts(
|
||||||
# begin the trigger-alert stream
|
# begin the trigger-alert stream
|
||||||
# this is where we receive **back** messages
|
# this is where we receive **back** messages
|
||||||
# about executions **from** the EMS actor
|
# about executions **from** the EMS actor
|
||||||
async for alert in stream:
|
async for msg in stream:
|
||||||
|
|
||||||
# delete the line from view
|
# delete the line from view
|
||||||
oid = alert['oid']
|
oid = msg['oid']
|
||||||
msg_type = alert['msg']
|
resp = msg['msg']
|
||||||
|
|
||||||
if msg_type == 'ack':
|
if resp in ('active',):
|
||||||
print(f"order accepted: {alert}")
|
print(f"order accepted: {msg}")
|
||||||
|
|
||||||
# show line label once order is live
|
# show line label once order is live
|
||||||
order_mode.lines.commit_line(oid)
|
order_mode.lines.commit_line(oid)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
order_mode.arrows.add(
|
elif resp in ('cancelled',):
|
||||||
oid,
|
|
||||||
alert['index'],
|
|
||||||
alert['price'],
|
|
||||||
pointing='up' if alert['name'] == 'up' else 'down'
|
|
||||||
)
|
|
||||||
|
|
||||||
# print(f'_lines: {_lines}')
|
# delete level from view
|
||||||
print(f'deleting line with oid: {oid}')
|
order_mode.lines.remove_line(uuid=oid)
|
||||||
|
print(f'deleting line with oid: {oid}')
|
||||||
|
|
||||||
# delete level from view
|
elif resp in ('executed',):
|
||||||
order_mode.lines.remove_line(uuid=oid)
|
|
||||||
|
|
||||||
# chart._vb._lines_editor
|
order_mode.lines.remove_line(uuid=oid)
|
||||||
# _lines.pop(oid).delete()
|
print(f'deleting line with oid: {oid}')
|
||||||
|
|
||||||
# TODO: this in another task?
|
order_mode.arrows.add(
|
||||||
# not sure if this will ever be a bottleneck,
|
oid,
|
||||||
# we probably could do graphics stuff first tho?
|
msg['index'],
|
||||||
|
msg['price'],
|
||||||
|
pointing='up' if msg['name'] == 'up' else 'down'
|
||||||
|
)
|
||||||
|
|
||||||
# XXX: linux only for now
|
# DESKTOP NOTIFICATIONS
|
||||||
result = await trio.run_process(
|
#
|
||||||
[
|
# TODO: this in another task?
|
||||||
'notify-send',
|
# not sure if this will ever be a bottleneck,
|
||||||
'-u', 'normal',
|
# we probably could do graphics stuff first tho?
|
||||||
'-t', '10000',
|
|
||||||
'piker',
|
|
||||||
f'alert: {alert}',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
log.runtime(result)
|
|
||||||
|
|
||||||
# do we need this?
|
# XXX: linux only for now
|
||||||
# await _from_ems.put(alert)
|
result = await trio.run_process(
|
||||||
|
[
|
||||||
|
'notify-send',
|
||||||
|
'-u', 'normal',
|
||||||
|
'-t', '10000',
|
||||||
|
'piker',
|
||||||
|
f'alert: {msg}',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
log.runtime(result)
|
||||||
|
|
|
@ -308,6 +308,7 @@ class LineEditor:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
line = self._order_lines[uuid]
|
line = self._order_lines[uuid]
|
||||||
|
line.oid = uuid
|
||||||
line.label.show()
|
line.label.show()
|
||||||
|
|
||||||
# TODO: other flashy things to indicate the order is active
|
# TODO: other flashy things to indicate the order is active
|
||||||
|
@ -316,6 +317,13 @@ class LineEditor:
|
||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
def lines_under_cursor(self):
|
||||||
|
"""Get the line(s) under the cursor position.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Delete any hoverable under the cursor
|
||||||
|
return self.chart._cursor._hovered
|
||||||
|
|
||||||
def remove_line(
|
def remove_line(
|
||||||
self,
|
self,
|
||||||
line: LevelLine = None,
|
line: LevelLine = None,
|
||||||
|
@ -328,21 +336,17 @@ class LineEditor:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if line:
|
if line:
|
||||||
# If line is passed delete it
|
uuid = line.oid
|
||||||
line.delete()
|
|
||||||
|
|
||||||
elif uuid:
|
# try to look up line from our registry
|
||||||
# try to look up line from our registry
|
line = self._order_lines.pop(uuid)
|
||||||
self._order_lines.pop(uuid).delete()
|
|
||||||
|
|
||||||
else:
|
# if hovered remove from cursor set
|
||||||
# Delete any hoverable under the cursor
|
hovered = self.chart._cursor._hovered
|
||||||
cursor = self.chart._cursor
|
if line in hovered:
|
||||||
|
hovered.remove(line)
|
||||||
|
|
||||||
for item in cursor._hovered:
|
line.delete()
|
||||||
# hovered items must also offer
|
|
||||||
# a ``.delete()`` method
|
|
||||||
item.delete()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -392,10 +396,15 @@ class OrderMode:
|
||||||
"""Major mode for placing orders on a chart view.
|
"""Major mode for placing orders on a chart view.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
chart: 'ChartPlotWidget'
|
chart: 'ChartPlotWidget' # type: ignore # noqa
|
||||||
book: OrderBook
|
book: OrderBook
|
||||||
lines: LineEditor
|
lines: LineEditor
|
||||||
arrows: ArrowEditor
|
arrows: ArrowEditor
|
||||||
|
_arrow_colors = {
|
||||||
|
'alert': 'alert_yellow',
|
||||||
|
'buy': 'buy_green',
|
||||||
|
'sell': 'sell_red',
|
||||||
|
}
|
||||||
|
|
||||||
key_map: Dict[str, Callable] = field(default_factory=dict)
|
key_map: Dict[str, Callable] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@ -664,7 +673,6 @@ class ChartView(ViewBox):
|
||||||
price=y
|
price=y
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def keyReleaseEvent(self, ev):
|
def keyReleaseEvent(self, ev):
|
||||||
"""
|
"""
|
||||||
Key release to normally to trigger release of input mode
|
Key release to normally to trigger release of input mode
|
||||||
|
@ -686,7 +694,6 @@ 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.mode.lines.unstage_line()
|
self.mode.lines.unstage_line()
|
||||||
|
|
||||||
def keyPressEvent(self, ev):
|
def keyPressEvent(self, ev):
|
||||||
|
@ -732,13 +739,14 @@ 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.mode.lines.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()
|
mode = self.mode
|
||||||
self.mode.lines.remove_line()
|
for line in mode.lines.lines_under_cursor():
|
||||||
|
mode.book.cancel(uuid=line.oid)
|
||||||
|
|
||||||
# XXX: Leaving this for light reference purposes, there
|
# XXX: Leaving this for light reference purposes, there
|
||||||
# seems to be some work to at least gawk at for history mgmt.
|
# seems to be some work to at least gawk at for history mgmt.
|
||||||
|
|
Loading…
Reference in New Issue