Support simulated live order modification in paper engine

basic_orders
Tyler Goodlet 2021-03-07 13:34:03 -05:00
parent 919ecab732
commit 0ade7daebc
1 changed files with 45 additions and 26 deletions

View File

@ -19,8 +19,9 @@ Fake trading for forward testing.
""" """
from datetime import datetime from datetime import datetime
from operator import itemgetter
import time import time
from typing import Tuple from typing import Tuple, Optional
import uuid import uuid
from bidict import bidict from bidict import bidict
@ -32,8 +33,9 @@ from ..data._normalize import iterticks
@dataclass @dataclass
class PaperBoi: class PaperBoi:
"""Emulates a broker order client providing the same API and """
order-event response event stream format but with methods for Emulates a broker order client providing the same API and
delivering an order-event response stream but with methods for
triggering desired events based on forward testing engine triggering desired events based on forward testing engine
requirements. requirements.
@ -59,13 +61,23 @@ class PaperBoi:
price: float, price: float,
action: str, action: str,
size: float, size: float,
brid: Optional[str],
) -> int: ) -> int:
"""Place an order and return integer request id provided by client. """Place an order and return integer request id provided by client.
""" """
# the trades stream expects events in the form
# {'local_trades': (event_name, msg)} if brid is None:
reqid = str(uuid.uuid4()) reqid = str(uuid.uuid4())
# register order internally
self._reqids[reqid] = (oid, symbol, action, price)
else:
# order is already existing, this is a modify
(oid, symbol, action, old_price) = self._reqids[brid]
assert old_price != price
reqid = brid
if action == 'alert': if action == 'alert':
# bypass all fill simulation # bypass all fill simulation
@ -95,9 +107,6 @@ class PaperBoi:
}), }),
}) })
# register order internally
self._reqids[reqid] = (oid, symbol, action, price)
# if we're already a clearing price simulate an immediate fill # if we're already a clearing price simulate an immediate fill
if ( if (
action == 'buy' and (clear_price := self.last_ask[0]) <= price action == 'buy' and (clear_price := self.last_ask[0]) <= price
@ -116,8 +125,12 @@ class PaperBoi:
elif action == 'sell': elif action == 'sell':
orders = self._sells orders = self._sells
# set the simulated order in the respective table for lookup
# and trigger by the simulated clearing task normally
# running ``simulate_fills()``.
# buys/sells: (symbol -> (price -> order)) # buys/sells: (symbol -> (price -> order))
orders.setdefault(symbol, {})[price] = (size, oid, reqid, action) orders.setdefault(symbol, {})[(oid, price)] = (size, reqid, action)
return reqid return reqid
@ -131,9 +144,9 @@ class PaperBoi:
oid, symbol, action, price = self._reqids[reqid] oid, symbol, action, price = self._reqids[reqid]
if action == 'buy': if action == 'buy':
self._buys[symbol].pop(price) self._buys[symbol].pop((oid, price))
elif action == 'sell': elif action == 'sell':
self._sells[symbol].pop(price) self._sells[symbol].pop((oid, price))
# TODO: net latency model # TODO: net latency model
await trio.sleep(0.05) await trio.sleep(0.05)
@ -174,6 +187,8 @@ class PaperBoi:
# TODO: net latency model # TODO: net latency model
await trio.sleep(0.05) await trio.sleep(0.05)
# the trades stream expects events in the form
# {'local_trades': (event_name, msg)}
await self._to_trade_stream.send({ await self._to_trade_stream.send({
'local_trades': ('fill', { 'local_trades': ('fill', {
@ -267,20 +282,22 @@ async def simulate_fills(
buys = client._buys.get(sym, {}) buys = client._buys.get(sym, {})
# iterate book prices descending # iterate book prices descending
for our_bid in reversed(sorted(buys.keys())): for oid, our_bid in reversed(
sorted(buys.keys(), key=itemgetter(1))
):
if tick_price < our_bid: if tick_price < our_bid:
# retreive order info # retreive order info
(size, oid, reqid, action) = buys.pop(our_bid) (size, reqid, action) = buys.pop((oid, our_bid))
# clearing price would have filled entirely # clearing price would have filled entirely
await client.fake_fill( await client.fake_fill(
# todo slippage to determine fill price # todo slippage to determine fill price
tick_price, price=tick_price,
size, size=size,
action, action=action,
reqid, reqid=reqid,
oid, oid=oid,
) )
else: else:
# prices are interated in sorted order so # prices are interated in sorted order so
@ -297,19 +314,21 @@ async def simulate_fills(
sells = client._sells.get(sym, {}) sells = client._sells.get(sym, {})
# iterate book prices ascending # iterate book prices ascending
for our_ask in sorted(sells.keys()): for oid, our_ask in sorted(
sells.keys(), key=itemgetter(1)
):
if tick_price > our_ask: if tick_price > our_ask:
# retreive order info # retreive order info
(size, oid, reqid, action) = sells.pop(our_ask) (size, reqid, action) = sells.pop((oid, our_ask))
# clearing price would have filled entirely # clearing price would have filled entirely
await client.fake_fill( await client.fake_fill(
tick_price, price=tick_price,
size, size=size,
action, action=action,
reqid, reqid=reqid,
oid, oid=oid,
) )
else: else:
# prices are interated in sorted order so # prices are interated in sorted order so