commit
77a687bced
|
@ -96,7 +96,7 @@ class Allocator(Struct):
|
||||||
def next_order_info(
|
def next_order_info(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
# we only need a startup size for exit calcs, we can the
|
# we only need a startup size for exit calcs, we can then
|
||||||
# determine how large slots should be if the initial pp size was
|
# determine how large slots should be if the initial pp size was
|
||||||
# larger then the current live one, and the live one is smaller
|
# larger then the current live one, and the live one is smaller
|
||||||
# then the initial config settings.
|
# then the initial config settings.
|
||||||
|
@ -137,12 +137,14 @@ class Allocator(Struct):
|
||||||
|
|
||||||
# an entry (adding-to or starting a pp)
|
# an entry (adding-to or starting a pp)
|
||||||
if (
|
if (
|
||||||
action == 'buy' and live_size > 0 or
|
|
||||||
action == 'sell' and live_size < 0 or
|
|
||||||
live_size == 0
|
live_size == 0
|
||||||
|
or (action == 'buy' and live_size > 0)
|
||||||
|
or action == 'sell' and live_size < 0
|
||||||
):
|
):
|
||||||
|
order_size = min(
|
||||||
order_size = min(slot_size, l_sub_pp)
|
slot_size,
|
||||||
|
max(l_sub_pp, 0),
|
||||||
|
)
|
||||||
|
|
||||||
# an exit (removing-from or going to net-zero pp)
|
# an exit (removing-from or going to net-zero pp)
|
||||||
else:
|
else:
|
||||||
|
@ -242,14 +244,6 @@ class Allocator(Struct):
|
||||||
return round(prop * self.slots)
|
return round(prop * self.slots)
|
||||||
|
|
||||||
|
|
||||||
_derivs = (
|
|
||||||
'future',
|
|
||||||
'continuous_future',
|
|
||||||
'option',
|
|
||||||
'futures_option',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_allocator(
|
def mk_allocator(
|
||||||
|
|
||||||
symbol: Symbol,
|
symbol: Symbol,
|
||||||
|
@ -276,45 +270,9 @@ def mk_allocator(
|
||||||
'currency_limit': 6e3,
|
'currency_limit': 6e3,
|
||||||
'slots': 6,
|
'slots': 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults.update(user_def)
|
defaults.update(user_def)
|
||||||
|
|
||||||
alloc = Allocator(
|
return Allocator(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
**defaults,
|
**defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
asset_type = symbol.type_key
|
|
||||||
|
|
||||||
# specific configs by asset class / type
|
|
||||||
|
|
||||||
if asset_type in _derivs:
|
|
||||||
# since it's harder to know how currency "applies" in this case
|
|
||||||
# given leverage properties
|
|
||||||
alloc.size_unit = '# units'
|
|
||||||
|
|
||||||
# set units limit to slots size thus making make the next
|
|
||||||
# entry step 1.0
|
|
||||||
alloc.units_limit = alloc.slots
|
|
||||||
|
|
||||||
else:
|
|
||||||
alloc.size_unit = 'currency'
|
|
||||||
|
|
||||||
# if the current position is already greater then the limit
|
|
||||||
# settings, increase the limit to the current position
|
|
||||||
if alloc.size_unit == 'currency':
|
|
||||||
startup_size = startup_pp.size * startup_pp.ppu
|
|
||||||
|
|
||||||
if startup_size > alloc.currency_limit:
|
|
||||||
alloc.currency_limit = round(startup_size, ndigits=2)
|
|
||||||
|
|
||||||
else:
|
|
||||||
startup_size = abs(startup_pp.size)
|
|
||||||
|
|
||||||
if startup_size > alloc.units_limit:
|
|
||||||
alloc.units_limit = startup_size
|
|
||||||
|
|
||||||
if asset_type in _derivs:
|
|
||||||
alloc.slots = alloc.units_limit
|
|
||||||
|
|
||||||
return alloc
|
|
||||||
|
|
|
@ -499,7 +499,7 @@ async def open_brokerd_trades_dialogue(
|
||||||
):
|
):
|
||||||
# XXX: really we only want one stream per `emsd` actor
|
# XXX: really we only want one stream per `emsd` actor
|
||||||
# to relay global `brokerd` order events unless we're
|
# to relay global `brokerd` order events unless we're
|
||||||
# doing to expect each backend to relay only orders
|
# going to expect each backend to relay only orders
|
||||||
# affiliated with a particular ``trades_dialogue()``
|
# affiliated with a particular ``trades_dialogue()``
|
||||||
# session (seems annoying for implementers). So, here
|
# session (seems annoying for implementers). So, here
|
||||||
# we cache the relay task and instead of running multiple
|
# we cache the relay task and instead of running multiple
|
||||||
|
@ -612,9 +612,10 @@ async def translate_and_relay_brokerd_events(
|
||||||
|
|
||||||
brokerd_msg: dict[str, Any]
|
brokerd_msg: dict[str, Any]
|
||||||
async for brokerd_msg in brokerd_trades_stream:
|
async for brokerd_msg in brokerd_trades_stream:
|
||||||
|
fmsg = pformat(brokerd_msg)
|
||||||
log.info(
|
log.info(
|
||||||
f'Received broker trade event:\n'
|
f'Received broker trade event:\n'
|
||||||
f'{pformat(brokerd_msg)}'
|
f'{fmsg}'
|
||||||
)
|
)
|
||||||
match brokerd_msg:
|
match brokerd_msg:
|
||||||
|
|
||||||
|
@ -666,7 +667,11 @@ async def translate_and_relay_brokerd_events(
|
||||||
# cancelled by the ems controlling client before we
|
# cancelled by the ems controlling client before we
|
||||||
# received this ack, in which case we relay that cancel
|
# received this ack, in which case we relay that cancel
|
||||||
# signal **asap** to the backend broker
|
# signal **asap** to the backend broker
|
||||||
status_msg = book._active[oid]
|
status_msg = book._active.get(oid)
|
||||||
|
if not status_msg:
|
||||||
|
log.warning(f'Rx Ack for closed/unknown order?: {oid}')
|
||||||
|
continue
|
||||||
|
|
||||||
req = status_msg.req
|
req = status_msg.req
|
||||||
if req and req.action == 'cancel':
|
if req and req.action == 'cancel':
|
||||||
# assign newly providerd broker backend request id
|
# assign newly providerd broker backend request id
|
||||||
|
@ -692,7 +697,7 @@ async def translate_and_relay_brokerd_events(
|
||||||
} if status_msg := book._active.get(oid):
|
} if status_msg := book._active.get(oid):
|
||||||
|
|
||||||
msg = BrokerdError(**brokerd_msg)
|
msg = BrokerdError(**brokerd_msg)
|
||||||
log.error(pformat(msg)) # XXX make one when it's blank?
|
log.error(fmsg) # XXX make one when it's blank?
|
||||||
|
|
||||||
# TODO: figure out how this will interact with EMS clients
|
# TODO: figure out how this will interact with EMS clients
|
||||||
# for ex. on an error do we react with a dark orders
|
# for ex. on an error do we react with a dark orders
|
||||||
|
@ -726,8 +731,19 @@ async def translate_and_relay_brokerd_events(
|
||||||
# TODO: maybe pack this into a composite type that
|
# TODO: maybe pack this into a composite type that
|
||||||
# contains both the IPC stream as well the
|
# contains both the IPC stream as well the
|
||||||
# msg-chain/dialog.
|
# msg-chain/dialog.
|
||||||
ems_client_order_stream = router.dialogues[oid]
|
ems_client_order_stream = router.dialogues.get(oid)
|
||||||
status_msg = book._active[oid]
|
status_msg = book._active.get(oid)
|
||||||
|
|
||||||
|
if (
|
||||||
|
not ems_client_order_stream
|
||||||
|
or not status_msg
|
||||||
|
):
|
||||||
|
log.warning(
|
||||||
|
'Received status for unknown dialog {oid}:\n'
|
||||||
|
'{fmsg}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
status_msg.resp = status
|
status_msg.resp = status
|
||||||
|
|
||||||
# retrieve existing live flow
|
# retrieve existing live flow
|
||||||
|
@ -762,12 +778,19 @@ async def translate_and_relay_brokerd_events(
|
||||||
'name': 'fill',
|
'name': 'fill',
|
||||||
'reqid': reqid, # brokerd generated order-request id
|
'reqid': reqid, # brokerd generated order-request id
|
||||||
# 'symbol': sym, # paper engine doesn't have this, nbd?
|
# 'symbol': sym, # paper engine doesn't have this, nbd?
|
||||||
} if (
|
}:
|
||||||
oid := book._ems2brokerd_ids.inverse.get(reqid)
|
oid = book._ems2brokerd_ids.inverse.get(reqid)
|
||||||
):
|
if not oid:
|
||||||
|
# TODO: maybe we could optionally check for an
|
||||||
|
# ``.oid`` in the msg since we're planning to
|
||||||
|
# maybe-kinda offer that via using ``Status``
|
||||||
|
# in the longer run anyway?
|
||||||
|
log.warning(f'Unkown fill for {fmsg}')
|
||||||
|
continue
|
||||||
|
|
||||||
# proxy through the "fill" result(s)
|
# proxy through the "fill" result(s)
|
||||||
msg = BrokerdFill(**brokerd_msg)
|
msg = BrokerdFill(**brokerd_msg)
|
||||||
log.info(f'Fill for {oid} cleared with:\n{pformat(msg)}')
|
log.info(f'Fill for {oid} cleared with:\n{fmsg}')
|
||||||
|
|
||||||
ems_client_order_stream = router.dialogues[oid]
|
ems_client_order_stream = router.dialogues[oid]
|
||||||
|
|
||||||
|
@ -796,7 +819,7 @@ async def translate_and_relay_brokerd_events(
|
||||||
# registered from a previous order/status load?
|
# registered from a previous order/status load?
|
||||||
log.error(
|
log.error(
|
||||||
f'Unknown/transient status msg:\n'
|
f'Unknown/transient status msg:\n'
|
||||||
f'{pformat(brokerd_msg)}\n'
|
f'{fmsg}\n'
|
||||||
'Unable to relay message to client side!?'
|
'Unable to relay message to client side!?'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -841,7 +864,7 @@ async def translate_and_relay_brokerd_events(
|
||||||
'name': 'status',
|
'name': 'status',
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
}:
|
}:
|
||||||
log.error(f'Broker error:\n{pformat(brokerd_msg)}')
|
log.error(f'Broker error:\n{fmsg}')
|
||||||
# XXX: we presume the brokerd cancels its own order
|
# XXX: we presume the brokerd cancels its own order
|
||||||
|
|
||||||
# TOO FAST ``BrokerdStatus`` that arrives
|
# TOO FAST ``BrokerdStatus`` that arrives
|
||||||
|
@ -862,7 +885,7 @@ async def translate_and_relay_brokerd_events(
|
||||||
status_msg = book._active[oid]
|
status_msg = book._active[oid]
|
||||||
msg += (
|
msg += (
|
||||||
f'last status msg: {pformat(status_msg)}\n\n'
|
f'last status msg: {pformat(status_msg)}\n\n'
|
||||||
f'this msg:{pformat(brokerd_msg)}\n'
|
f'this msg:{fmsg}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
log.warning(msg)
|
log.warning(msg)
|
||||||
|
|
|
@ -18,9 +18,11 @@
|
||||||
Fake trading for forward testing.
|
Fake trading for forward testing.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from collections import defaultdict
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
import itertools
|
||||||
import time
|
import time
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
@ -72,8 +74,8 @@ class PaperBoi(Struct):
|
||||||
|
|
||||||
# map of paper "live" orders which be used
|
# map of paper "live" orders which be used
|
||||||
# to simulate fills based on paper engine settings
|
# to simulate fills based on paper engine settings
|
||||||
_buys: dict
|
_buys: defaultdict[str, bidict]
|
||||||
_sells: dict
|
_sells: defaultdict[str, bidict]
|
||||||
_reqids: bidict
|
_reqids: bidict
|
||||||
_positions: dict[str, Position]
|
_positions: dict[str, Position]
|
||||||
_trade_ledger: dict[str, Any]
|
_trade_ledger: dict[str, Any]
|
||||||
|
@ -106,7 +108,6 @@ class PaperBoi(Struct):
|
||||||
if entry:
|
if entry:
|
||||||
# order is already existing, this is a modify
|
# order is already existing, this is a modify
|
||||||
(oid, symbol, action, old_price) = entry
|
(oid, symbol, action, old_price) = entry
|
||||||
assert old_price != price
|
|
||||||
is_modify = True
|
is_modify = True
|
||||||
else:
|
else:
|
||||||
# register order internally
|
# register order internally
|
||||||
|
@ -167,10 +168,10 @@ class PaperBoi(Struct):
|
||||||
|
|
||||||
if is_modify:
|
if is_modify:
|
||||||
# remove any existing order for the old price
|
# remove any existing order for the old price
|
||||||
orders[symbol].pop((oid, old_price))
|
orders[symbol].pop(oid)
|
||||||
|
|
||||||
# buys/sells: (symbol -> (price -> order))
|
# buys/sells: {symbol -> bidict[oid, (<price data>)]}
|
||||||
orders.setdefault(symbol, {})[(oid, price)] = (size, reqid, action)
|
orders[symbol][oid] = (price, size, reqid, action)
|
||||||
|
|
||||||
return reqid
|
return reqid
|
||||||
|
|
||||||
|
@ -183,16 +184,15 @@ class PaperBoi(Struct):
|
||||||
oid, symbol, action, price = self._reqids[reqid]
|
oid, symbol, action, price = self._reqids[reqid]
|
||||||
|
|
||||||
if action == 'buy':
|
if action == 'buy':
|
||||||
self._buys[symbol].pop((oid, price))
|
self._buys[symbol].pop(oid, None)
|
||||||
elif action == 'sell':
|
elif action == 'sell':
|
||||||
self._sells[symbol].pop((oid, price))
|
self._sells[symbol].pop(oid, None)
|
||||||
|
|
||||||
# TODO: net latency model
|
# TODO: net latency model
|
||||||
await trio.sleep(0.05)
|
await trio.sleep(0.05)
|
||||||
|
|
||||||
msg = BrokerdStatus(
|
msg = BrokerdStatus(
|
||||||
status='canceled',
|
status='canceled',
|
||||||
# account=f'paper_{self.broker}',
|
|
||||||
account='paper',
|
account='paper',
|
||||||
reqid=reqid,
|
reqid=reqid,
|
||||||
time_ns=time.time_ns(),
|
time_ns=time.time_ns(),
|
||||||
|
@ -203,7 +203,7 @@ class PaperBoi(Struct):
|
||||||
async def fake_fill(
|
async def fake_fill(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
symbol: str,
|
fqsn: str,
|
||||||
price: float,
|
price: float,
|
||||||
size: float,
|
size: float,
|
||||||
action: str, # one of {'buy', 'sell'}
|
action: str, # one of {'buy', 'sell'}
|
||||||
|
@ -257,34 +257,34 @@ class PaperBoi(Struct):
|
||||||
await self.ems_trades_stream.send(msg)
|
await self.ems_trades_stream.send(msg)
|
||||||
|
|
||||||
# lookup any existing position
|
# lookup any existing position
|
||||||
token = f'{symbol}.{self.broker}'
|
key = fqsn.rstrip(f'.{self.broker}')
|
||||||
pp = self._positions.setdefault(
|
pp = self._positions.setdefault(
|
||||||
token,
|
fqsn,
|
||||||
Position(
|
Position(
|
||||||
Symbol(
|
Symbol(
|
||||||
key=symbol,
|
key=key,
|
||||||
broker_info={self.broker: {}},
|
broker_info={self.broker: {}},
|
||||||
),
|
),
|
||||||
size=size,
|
size=size,
|
||||||
ppu=price,
|
ppu=price,
|
||||||
bsuid=symbol,
|
bsuid=key,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
t = Transaction(
|
t = Transaction(
|
||||||
fqsn=symbol,
|
fqsn=fqsn,
|
||||||
tid=oid,
|
tid=oid,
|
||||||
size=size,
|
size=size,
|
||||||
price=price,
|
price=price,
|
||||||
cost=0, # TODO: cost model
|
cost=0, # TODO: cost model
|
||||||
dt=pendulum.from_timestamp(fill_time_s),
|
dt=pendulum.from_timestamp(fill_time_s),
|
||||||
bsuid=symbol,
|
bsuid=key,
|
||||||
)
|
)
|
||||||
pp.add_clear(t)
|
pp.add_clear(t)
|
||||||
|
|
||||||
pp_msg = BrokerdPosition(
|
pp_msg = BrokerdPosition(
|
||||||
broker=self.broker,
|
broker=self.broker,
|
||||||
account='paper',
|
account='paper',
|
||||||
symbol=symbol,
|
symbol=fqsn,
|
||||||
# TODO: we need to look up the asset currency from
|
# TODO: we need to look up the asset currency from
|
||||||
# broker info. i guess for crypto this can be
|
# broker info. i guess for crypto this can be
|
||||||
# inferred from the pair?
|
# inferred from the pair?
|
||||||
|
@ -325,10 +325,30 @@ async def simulate_fills(
|
||||||
# dark order price filter(s)
|
# dark order price filter(s)
|
||||||
types=('ask', 'bid', 'trade', 'last')
|
types=('ask', 'bid', 'trade', 'last')
|
||||||
):
|
):
|
||||||
# print(tick)
|
tick_price = tick['price']
|
||||||
|
|
||||||
|
buys: bidict[str, tuple] = client._buys[sym]
|
||||||
|
iter_buys = reversed(sorted(
|
||||||
|
buys.values(),
|
||||||
|
key=itemgetter(0),
|
||||||
|
))
|
||||||
|
|
||||||
|
def sell_on_bid(our_price):
|
||||||
|
return tick_price <= our_price
|
||||||
|
|
||||||
|
sells: bidict[str, tuple] = client._sells[sym]
|
||||||
|
iter_sells = sorted(
|
||||||
|
sells.values(),
|
||||||
|
key=itemgetter(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
def buy_on_ask(our_price):
|
||||||
|
return tick_price >= our_price
|
||||||
|
|
||||||
match tick:
|
match tick:
|
||||||
case {
|
case {
|
||||||
'price': tick_price,
|
'price': tick_price,
|
||||||
|
# 'type': ('ask' | 'trade' | 'last'),
|
||||||
'type': 'ask',
|
'type': 'ask',
|
||||||
}:
|
}:
|
||||||
client.last_ask = (
|
client.last_ask = (
|
||||||
|
@ -336,48 +356,66 @@ async def simulate_fills(
|
||||||
tick.get('size', client.last_ask[1]),
|
tick.get('size', client.last_ask[1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
orders = client._buys.get(sym, {})
|
iter_entries = zip(
|
||||||
book_sequence = reversed(
|
iter_buys,
|
||||||
sorted(orders.keys(), key=itemgetter(1)))
|
itertools.repeat(sell_on_bid)
|
||||||
|
)
|
||||||
def pred(our_price):
|
|
||||||
return tick_price <= our_price
|
|
||||||
|
|
||||||
case {
|
case {
|
||||||
'price': tick_price,
|
'price': tick_price,
|
||||||
|
# 'type': ('bid' | 'trade' | 'last'),
|
||||||
'type': 'bid',
|
'type': 'bid',
|
||||||
}:
|
}:
|
||||||
client.last_bid = (
|
client.last_bid = (
|
||||||
tick_price,
|
tick_price,
|
||||||
tick.get('size', client.last_bid[1]),
|
tick.get('size', client.last_bid[1]),
|
||||||
)
|
)
|
||||||
orders = client._sells.get(sym, {})
|
|
||||||
book_sequence = sorted(
|
|
||||||
orders.keys(),
|
|
||||||
key=itemgetter(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
def pred(our_price):
|
iter_entries = zip(
|
||||||
return tick_price >= our_price
|
iter_sells,
|
||||||
|
itertools.repeat(buy_on_ask)
|
||||||
|
)
|
||||||
|
|
||||||
case {
|
case {
|
||||||
'price': tick_price,
|
'price': tick_price,
|
||||||
'type': ('trade' | 'last'),
|
'type': ('trade' | 'last'),
|
||||||
}:
|
}:
|
||||||
# TODO: simulate actual book queues and our orders
|
# in the clearing price / last price case we
|
||||||
# place in it, might require full L2 data?
|
# want to iterate both sides of our book for
|
||||||
continue
|
# clears since we don't know which direction the
|
||||||
|
# price is going to move (especially with HFT)
|
||||||
|
# and thus we simply interleave both sides (buys
|
||||||
|
# and sells) until one side clears and then
|
||||||
|
# break until the next tick?
|
||||||
|
def interleave():
|
||||||
|
for pair in zip(
|
||||||
|
iter_buys,
|
||||||
|
iter_sells,
|
||||||
|
):
|
||||||
|
for order_info, pred in zip(
|
||||||
|
pair,
|
||||||
|
itertools.cycle([sell_on_bid, buy_on_ask]),
|
||||||
|
):
|
||||||
|
yield order_info, pred
|
||||||
|
|
||||||
# iterate book prices descending
|
iter_entries = interleave()
|
||||||
for oid, our_price in book_sequence:
|
|
||||||
if pred(our_price):
|
|
||||||
|
|
||||||
# retreive order info
|
# iterate all potentially clearable book prices
|
||||||
(size, reqid, action) = orders.pop((oid, our_price))
|
# in FIFO order per side.
|
||||||
|
for order_info, pred in iter_entries:
|
||||||
|
(our_price, size, reqid, action) = order_info
|
||||||
|
|
||||||
|
clearable = pred(our_price)
|
||||||
|
if clearable:
|
||||||
|
# pop and retreive order info
|
||||||
|
oid = {
|
||||||
|
'buy': buys,
|
||||||
|
'sell': sells
|
||||||
|
}[action].inverse.pop(order_info)
|
||||||
|
|
||||||
# clearing price would have filled entirely
|
# clearing price would have filled entirely
|
||||||
await client.fake_fill(
|
await client.fake_fill(
|
||||||
symbol=sym,
|
fqsn=sym,
|
||||||
# todo slippage to determine fill price
|
# todo slippage to determine fill price
|
||||||
price=tick_price,
|
price=tick_price,
|
||||||
size=size,
|
size=size,
|
||||||
|
@ -385,9 +423,6 @@ async def simulate_fills(
|
||||||
reqid=reqid,
|
reqid=reqid,
|
||||||
oid=oid,
|
oid=oid,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# prices are iterated in sorted order so we're done
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_order_requests(
|
async def handle_order_requests(
|
||||||
|
@ -403,15 +438,21 @@ async def handle_order_requests(
|
||||||
case {'action': ('buy' | 'sell')}:
|
case {'action': ('buy' | 'sell')}:
|
||||||
order = BrokerdOrder(**request_msg)
|
order = BrokerdOrder(**request_msg)
|
||||||
account = order.account
|
account = order.account
|
||||||
|
|
||||||
|
# error on bad inputs
|
||||||
|
reason = None
|
||||||
if account != 'paper':
|
if account != 'paper':
|
||||||
log.error(
|
reason = f'No account found:`{account}` (paper only)?'
|
||||||
'This is a paper account,'
|
|
||||||
' only a `paper` selection is valid'
|
elif order.size == 0:
|
||||||
)
|
reason = 'Invalid size: 0'
|
||||||
|
|
||||||
|
if reason:
|
||||||
|
log.error(reason)
|
||||||
await ems_order_stream.send(BrokerdError(
|
await ems_order_stream.send(BrokerdError(
|
||||||
oid=order.oid,
|
oid=order.oid,
|
||||||
symbol=order.symbol,
|
symbol=order.symbol,
|
||||||
reason=f'Paper only. No account found: `{account}` ?',
|
reason=reason,
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -428,7 +469,7 @@ async def handle_order_requests(
|
||||||
# call our client api to submit the order
|
# call our client api to submit the order
|
||||||
reqid = await client.submit_limit(
|
reqid = await client.submit_limit(
|
||||||
oid=order.oid,
|
oid=order.oid,
|
||||||
symbol=order.symbol,
|
symbol=f'{order.symbol}.{client.broker}',
|
||||||
price=order.price,
|
price=order.price,
|
||||||
action=order.action,
|
action=order.action,
|
||||||
size=order.size,
|
size=order.size,
|
||||||
|
@ -451,20 +492,20 @@ async def handle_order_requests(
|
||||||
|
|
||||||
|
|
||||||
_reqids: bidict[str, tuple] = {}
|
_reqids: bidict[str, tuple] = {}
|
||||||
_buys: dict[
|
_buys: defaultdict[
|
||||||
str,
|
str, # symbol
|
||||||
dict[
|
bidict[
|
||||||
tuple[str, float],
|
str, # oid
|
||||||
tuple[float, str, str],
|
tuple[float, float, str, str], # order info
|
||||||
]
|
]
|
||||||
] = {}
|
] = defaultdict(bidict)
|
||||||
_sells: dict[
|
_sells: defaultdict[
|
||||||
str,
|
str, # symbol
|
||||||
dict[
|
bidict[
|
||||||
tuple[str, float],
|
str, # oid
|
||||||
tuple[float, str, str],
|
tuple[float, float, str, str], # order info
|
||||||
]
|
]
|
||||||
] = {}
|
] = defaultdict(bidict)
|
||||||
_positions: dict[str, Position] = {}
|
_positions: dict[str, Position] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -501,7 +542,6 @@ async def trades_dialogue(
|
||||||
|
|
||||||
# TODO: load paper positions per broker from .toml config file
|
# TODO: load paper positions per broker from .toml config file
|
||||||
# and pass as symbol to position data mapping: ``dict[str, dict]``
|
# and pass as symbol to position data mapping: ``dict[str, dict]``
|
||||||
# await ctx.started(all_positions)
|
|
||||||
await ctx.started((pp_msgs, ['paper']))
|
await ctx.started((pp_msgs, ['paper']))
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
|
|
|
@ -166,12 +166,29 @@ class SettingsPane:
|
||||||
key: str,
|
key: str,
|
||||||
value: str,
|
value: str,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Try to apply some input setting (by the user), revert to previous setting if it fails
|
||||||
|
display new value if applied.
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.apply_setting(key, value)
|
||||||
|
self.update_status_ui(pp=self.order_mode.current_pp)
|
||||||
|
|
||||||
|
def apply_setting(
|
||||||
|
self,
|
||||||
|
|
||||||
|
key: str,
|
||||||
|
value: str,
|
||||||
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
'''
|
'''
|
||||||
Called on any order pane edit field value change.
|
Called on any order pane edit field value change.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
mode = self.order_mode
|
mode = self.order_mode
|
||||||
|
tracker = mode.current_pp
|
||||||
|
alloc = tracker.alloc
|
||||||
|
|
||||||
# an account switch request
|
# an account switch request
|
||||||
if key == 'account':
|
if key == 'account':
|
||||||
|
@ -207,25 +224,28 @@ class SettingsPane:
|
||||||
# load the new account's allocator
|
# load the new account's allocator
|
||||||
alloc = tracker.alloc
|
alloc = tracker.alloc
|
||||||
|
|
||||||
else:
|
|
||||||
tracker = mode.current_pp
|
|
||||||
alloc = tracker.alloc
|
|
||||||
|
|
||||||
size_unit = alloc.size_unit
|
|
||||||
|
|
||||||
# WRITE any settings to current pp's allocator
|
# WRITE any settings to current pp's allocator
|
||||||
try:
|
|
||||||
if key == 'size_unit':
|
if key == 'size_unit':
|
||||||
# implicit re-write of value if input
|
# implicit re-write of value if input
|
||||||
# is the "text name" of the units.
|
# is the "text name" of the units.
|
||||||
# yah yah, i know this is badd..
|
# yah yah, i know this is badd..
|
||||||
alloc.size_unit = value
|
alloc.size_unit = value
|
||||||
else:
|
|
||||||
|
elif key != 'account': # numeric fields entry
|
||||||
|
try:
|
||||||
value = puterize(value)
|
value = puterize(value)
|
||||||
|
except ValueError as err:
|
||||||
|
log.error(err.args[0])
|
||||||
|
return False
|
||||||
|
|
||||||
if key == 'limit':
|
if key == 'limit':
|
||||||
|
if value <= 0:
|
||||||
|
log.error('limit must be > 0')
|
||||||
|
return False
|
||||||
|
|
||||||
pp = mode.current_pp.live_pp
|
pp = mode.current_pp.live_pp
|
||||||
|
|
||||||
if size_unit == 'currency':
|
if alloc.size_unit == 'currency':
|
||||||
dsize = pp.dsize
|
dsize = pp.dsize
|
||||||
if dsize > value:
|
if dsize > value:
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -247,29 +267,42 @@ class SettingsPane:
|
||||||
|
|
||||||
elif key == 'slots':
|
elif key == 'slots':
|
||||||
if value <= 0:
|
if value <= 0:
|
||||||
raise ValueError('slots must be > 0')
|
# raise ValueError('slots must be > 0')
|
||||||
|
log.error('limit must be > 0')
|
||||||
|
return False
|
||||||
|
|
||||||
alloc.slots = int(value)
|
alloc.slots = int(value)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.error(f'Unknown setting {key}')
|
log.error(f'Unknown setting {key}')
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
|
# don't log account "change" case since it'll be submitted
|
||||||
|
# on every mouse interaction.
|
||||||
log.info(f'settings change: {key}: {value}')
|
log.info(f'settings change: {key}: {value}')
|
||||||
|
|
||||||
except ValueError:
|
# TODO: maybe return a diff of settings so if we can an error we
|
||||||
log.error(f'Invalid value for `{key}`: {value}')
|
# can have general input handling code to report it through the
|
||||||
|
# UI in some way?
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_status_ui(
|
||||||
|
self,
|
||||||
|
pp: PositionTracker,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
alloc = pp.alloc
|
||||||
|
slots = alloc.slots
|
||||||
|
used = alloc.slots_used(pp.live_pp)
|
||||||
|
|
||||||
# READ out settings and update the status UI / settings widgets
|
# READ out settings and update the status UI / settings widgets
|
||||||
suffix = {'currency': ' $', 'units': ' u'}[size_unit]
|
suffix = {'currency': ' $', 'units': ' u'}[alloc.size_unit]
|
||||||
limit = alloc.limit()
|
limit = alloc.limit()
|
||||||
|
|
||||||
# TODO: a reverse look up from the position to the equivalent
|
|
||||||
# account(s), if none then look to user config for default?
|
|
||||||
self.update_status_ui(pp=tracker)
|
|
||||||
|
|
||||||
step_size, currency_per_slot = alloc.step_sizes()
|
step_size, currency_per_slot = alloc.step_sizes()
|
||||||
|
|
||||||
if size_unit == 'currency':
|
if alloc.size_unit == 'currency':
|
||||||
step_size = currency_per_slot
|
step_size = currency_per_slot
|
||||||
|
|
||||||
self.step_label.format(
|
self.step_label.format(
|
||||||
|
@ -287,23 +320,7 @@ class SettingsPane:
|
||||||
self.form.fields['limit'].setText(str(limit))
|
self.form.fields['limit'].setText(str(limit))
|
||||||
|
|
||||||
# update of level marker size label based on any new settings
|
# update of level marker size label based on any new settings
|
||||||
tracker.update_from_pp()
|
pp.update_from_pp()
|
||||||
|
|
||||||
# TODO: maybe return a diff of settings so if we can an error we
|
|
||||||
# can have general input handling code to report it through the
|
|
||||||
# UI in some way?
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update_status_ui(
|
|
||||||
self,
|
|
||||||
|
|
||||||
pp: PositionTracker,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
alloc = pp.alloc
|
|
||||||
slots = alloc.slots
|
|
||||||
used = alloc.slots_used(pp.live_pp)
|
|
||||||
|
|
||||||
# calculate proportion of position size limit
|
# calculate proportion of position size limit
|
||||||
# that exists and display in fill bar
|
# that exists and display in fill bar
|
||||||
|
@ -441,6 +458,14 @@ def position_line(
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
_derivs = (
|
||||||
|
'future',
|
||||||
|
'continuous_future',
|
||||||
|
'option',
|
||||||
|
'futures_option',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PositionTracker:
|
class PositionTracker:
|
||||||
'''
|
'''
|
||||||
Track and display real-time positions for a single symbol
|
Track and display real-time positions for a single symbol
|
||||||
|
@ -547,14 +572,54 @@ class PositionTracker:
|
||||||
def update_from_pp(
|
def update_from_pp(
|
||||||
self,
|
self,
|
||||||
position: Optional[Position] = None,
|
position: Optional[Position] = None,
|
||||||
|
set_as_startup: bool = False,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Update graphics and data from average price and size passed in our
|
'''
|
||||||
EMS ``BrokerdPosition`` msg.
|
Update graphics and data from average price and size passed in
|
||||||
|
our EMS ``BrokerdPosition`` msg.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# live pp updates
|
# live pp updates
|
||||||
pp = position or self.live_pp
|
pp = position or self.live_pp
|
||||||
|
if set_as_startup:
|
||||||
|
startup_pp = pp
|
||||||
|
else:
|
||||||
|
startup_pp = self.startup_pp
|
||||||
|
alloc = self.alloc
|
||||||
|
|
||||||
|
# update allocator settings
|
||||||
|
asset_type = pp.symbol.type_key
|
||||||
|
|
||||||
|
# specific configs by asset class / type
|
||||||
|
if asset_type in _derivs:
|
||||||
|
# since it's harder to know how currency "applies" in this case
|
||||||
|
# given leverage properties
|
||||||
|
alloc.size_unit = '# units'
|
||||||
|
|
||||||
|
# set units limit to slots size thus making make the next
|
||||||
|
# entry step 1.0
|
||||||
|
alloc.units_limit = alloc.slots
|
||||||
|
|
||||||
|
else:
|
||||||
|
alloc.size_unit = 'currency'
|
||||||
|
|
||||||
|
# if the current position is already greater then the limit
|
||||||
|
# settings, increase the limit to the current position
|
||||||
|
if alloc.size_unit == 'currency':
|
||||||
|
startup_size = self.startup_pp.size * startup_pp.ppu
|
||||||
|
|
||||||
|
if startup_size > alloc.currency_limit:
|
||||||
|
alloc.currency_limit = round(startup_size, ndigits=2)
|
||||||
|
|
||||||
|
else:
|
||||||
|
startup_size = abs(startup_pp.size)
|
||||||
|
|
||||||
|
if startup_size > alloc.units_limit:
|
||||||
|
alloc.units_limit = startup_size
|
||||||
|
|
||||||
|
if asset_type in _derivs:
|
||||||
|
alloc.slots = alloc.units_limit
|
||||||
|
|
||||||
self.update_line(
|
self.update_line(
|
||||||
pp.ppu,
|
pp.ppu,
|
||||||
|
@ -564,7 +629,7 @@ class PositionTracker:
|
||||||
|
|
||||||
# label updates
|
# label updates
|
||||||
self.size_label.fields['slots_used'] = round(
|
self.size_label.fields['slots_used'] = round(
|
||||||
self.alloc.slots_used(pp), ndigits=1)
|
alloc.slots_used(pp), ndigits=1)
|
||||||
self.size_label.render()
|
self.size_label.render()
|
||||||
|
|
||||||
if pp.size == 0:
|
if pp.size == 0:
|
||||||
|
|
|
@ -639,22 +639,6 @@ async def open_order_mode(
|
||||||
iter(accounts.keys())
|
iter(accounts.keys())
|
||||||
) if accounts else 'paper'
|
) if accounts else 'paper'
|
||||||
|
|
||||||
# Pack position messages by account, should only be one-to-one.
|
|
||||||
# NOTE: requires the backend exactly specifies
|
|
||||||
# the expected symbol key in its positions msg.
|
|
||||||
pps_by_account = {}
|
|
||||||
for (broker, acctid), msgs in position_msgs.items():
|
|
||||||
for msg in msgs:
|
|
||||||
|
|
||||||
sym = msg['symbol']
|
|
||||||
if (
|
|
||||||
(sym == symkey) or (
|
|
||||||
# mega-UGH, i think we need to fix the FQSN
|
|
||||||
# stuff sooner then later..
|
|
||||||
sym == symkey.removesuffix(f'.{broker}'))
|
|
||||||
):
|
|
||||||
pps_by_account[acctid] = msg
|
|
||||||
|
|
||||||
# update pp trackers with data relayed from ``brokerd``.
|
# update pp trackers with data relayed from ``brokerd``.
|
||||||
for account_name in accounts:
|
for account_name in accounts:
|
||||||
|
|
||||||
|
@ -667,10 +651,6 @@ async def open_order_mode(
|
||||||
# XXX: BLEH, do we care about this on the client side?
|
# XXX: BLEH, do we care about this on the client side?
|
||||||
bsuid=symbol,
|
bsuid=symbol,
|
||||||
)
|
)
|
||||||
msg = pps_by_account.get(account_name)
|
|
||||||
if msg:
|
|
||||||
log.info(f'Loading pp for {symkey}:\n{pformat(msg)}')
|
|
||||||
startup_pp.update_from_msg(msg)
|
|
||||||
|
|
||||||
# allocator config
|
# allocator config
|
||||||
alloc = mk_allocator(
|
alloc = mk_allocator(
|
||||||
|
@ -766,7 +746,6 @@ async def open_order_mode(
|
||||||
# to order sync pane handler
|
# to order sync pane handler
|
||||||
for key in ('account', 'size_unit',):
|
for key in ('account', 'size_unit',):
|
||||||
w = form.fields[key]
|
w = form.fields[key]
|
||||||
|
|
||||||
w.currentTextChanged.connect(
|
w.currentTextChanged.connect(
|
||||||
partial(
|
partial(
|
||||||
order_pane.on_selection_change,
|
order_pane.on_selection_change,
|
||||||
|
@ -789,6 +768,18 @@ async def open_order_mode(
|
||||||
# Begin order-response streaming
|
# Begin order-response streaming
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
# Pack position messages by account, should only be one-to-one.
|
||||||
|
# NOTE: requires the backend exactly specifies
|
||||||
|
# the expected symbol key in its positions msg.
|
||||||
|
for (broker, acctid), msgs in position_msgs.items():
|
||||||
|
for msg in msgs:
|
||||||
|
log.info(f'Loading pp for {symkey}:\n{pformat(msg)}')
|
||||||
|
await process_trade_msg(
|
||||||
|
mode,
|
||||||
|
book,
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
|
||||||
# start async input handling for chart's view
|
# start async input handling for chart's view
|
||||||
async with (
|
async with (
|
||||||
|
|
||||||
|
@ -876,8 +867,7 @@ async def process_trade_msg(
|
||||||
log.info(f'{fqsn} matched pp msg: {fmsg}')
|
log.info(f'{fqsn} matched pp msg: {fmsg}')
|
||||||
tracker = mode.trackers[msg['account']]
|
tracker = mode.trackers[msg['account']]
|
||||||
tracker.live_pp.update_from_msg(msg)
|
tracker.live_pp.update_from_msg(msg)
|
||||||
# update order pane widgets
|
tracker.update_from_pp(set_as_startup=True) # status/pane UI
|
||||||
tracker.update_from_pp()
|
|
||||||
mode.pane.update_status_ui(tracker)
|
mode.pane.update_status_ui(tracker)
|
||||||
|
|
||||||
if tracker.live_pp.size:
|
if tracker.live_pp.size:
|
||||||
|
|
Loading…
Reference in New Issue