Add paper-engine cost simulation support

If a backend declares a top level `get_cost()` (provisional name)
we call it in the paper engine to try and simulate costs according to
the provider's own schedule. For now only `binance` has support (via the
ep def) but ideally we can fill these in incrementally as users start
forward testing on multiple cexes.
account_tests
Tyler Goodlet 2023-08-07 09:55:45 -04:00
parent 5ed8544fd1
commit eba6a77966
4 changed files with 83 additions and 13 deletions

View File

@ -21,6 +21,7 @@ Trade and transaction ledger processing.
from __future__ import annotations
from collections import UserDict
from contextlib import contextmanager as cm
from functools import partial
from pathlib import Path
from pprint import pformat
from types import ModuleType
@ -189,9 +190,13 @@ class TransactionLedger(UserDict):
if self.account == 'paper':
from piker.clearing import _paper_engine
norm_trade = _paper_engine.norm_trade
norm_trade: Callable = partial(
_paper_engine.norm_trade,
brokermod=self.mod,
)
else:
norm_trade = self.mod.norm_trade
norm_trade: Callable = self.mod.norm_trade
# datetime-sort and pack into txs
for tid, txdict in self.tx_sort(self.data.items()):

View File

@ -32,6 +32,7 @@ from .feed import (
)
from .broker import (
open_trade_dialog,
get_cost,
)
from .venues import (
SpotPair,
@ -41,6 +42,7 @@ from .venues import (
__all__ = [
'get_client',
'get_mkt_info',
'get_cost',
'SpotPair',
'FutesPair',
'open_trade_dialog',

View File

@ -48,7 +48,9 @@ from piker.brokers import (
open_cached_client,
BrokerError,
)
from piker.clearing import OrderDialogs
from piker.clearing import (
OrderDialogs,
)
from piker.clearing._messages import (
BrokerdOrder,
BrokerdOrderAck,
@ -70,6 +72,33 @@ from .api import Client
log = get_logger('piker.brokers.binance')
# Fee schedule template, mostly for paper engine fees modelling.
# https://www.binance.com/en/support/faq/what-are-market-makers-and-takers-360007720071
def get_cost(
price: float,
size: float,
is_taker: bool = False,
) -> float:
# https://www.binance.com/en/fee/trading
cb: float = price * size
match is_taker:
case True:
return cb * 0.001000
case False if cb < 1e6:
return cb * 0.001000
case False if 1e6 >= cb < 5e6:
return cb * 0.000900
# NOTE: there's more but are you really going
# to have a cb bigger then this per trade?
case False if cb >= 5e6:
return cb * 0.000800
async def handle_order_requests(
ems_order_stream: tractor.MsgStream,
client: Client,

View File

@ -41,7 +41,7 @@ import tractor
from piker.brokers import get_brokermod
from piker.accounting import (
Account,
CostModel,
# CostModel,
MktPair,
Position,
Transaction,
@ -51,12 +51,13 @@ from piker.accounting import (
unpack_fqme,
)
from piker.data import (
Struct,
Feed,
SymbologyCache,
iterticks,
open_feed,
open_symcache,
)
from piker.types import Struct
from ._util import (
log, # sub-sys logger
get_console_log,
@ -84,7 +85,7 @@ class PaperBoi(Struct):
ems_trades_stream: tractor.MsgStream
acnt: Account
ledger: TransactionLedger
fees: CostModel
fees: Callable
# map of paper "live" orders which be used
# to simulate fills based on paper engine settings
@ -266,12 +267,17 @@ class PaperBoi(Struct):
# we don't actually have any unique backend symbol ourselves
# other then this thing, our fqme address.
bs_mktid: str = fqme
if fees := self.fees:
cost: float = fees(price, size)
else:
cost: float = 0
t = Transaction(
fqme=fqme,
tid=oid,
size=size,
price=price,
cost=0, # TODO: cost model
cost=cost,
dt=pendulum.from_timestamp(fill_time_s),
bs_mktid=bs_mktid,
)
@ -296,7 +302,7 @@ class PaperBoi(Struct):
account='paper',
symbol=fqme,
size=pp.size,
size=pp.cumsize,
avg_price=pp.ppu,
# TODO: we need to look up the asset currency from
@ -657,7 +663,7 @@ async def open_trade_dialog(
broker=broker,
account='paper',
symbol=pos.mkt.fqme,
size=pos.size,
size=pos.cumsize,
avg_price=pos.ppu,
))
@ -681,6 +687,7 @@ async def open_trade_dialog(
await trio.sleep_forever()
return
feed: Feed
async with (
open_feed(
[fqme],
@ -689,9 +696,15 @@ async def open_trade_dialog(
):
# sanity check all the mkt infos
for fqme, flume in feed.flumes.items():
mkt = symcache.mktmaps.get(fqme) or mkt_by_fqme[fqme]
mkt: MktPair = symcache.mktmaps.get(fqme) or mkt_by_fqme[fqme]
assert mkt == flume.mkt
get_cost: Callable = getattr(
brokermod,
'get_cost',
None,
)
async with (
ctx.open_stream() as ems_stream,
trio.open_nursery() as n,
@ -701,6 +714,7 @@ async def open_trade_dialog(
ems_trades_stream=ems_stream,
acnt=acnt,
ledger=ledger,
fees=get_cost,
_buys=_buys,
_sells=_sells,
@ -776,6 +790,9 @@ def norm_trade(
pairs: dict[str, Struct],
symcache: SymbologyCache | None = None,
# fees: CostModel | None = None,
brokermod: ModuleType | None = None,
) -> Transaction:
from pendulum import (
DateTime,
@ -788,13 +805,30 @@ def norm_trade(
expiry: str | None = txdict.get('expiry')
fqme: str = txdict.get('fqme') or txdict.pop('fqsn')
price: float = txdict['price']
size: float = txdict['size']
cost: float = txdict.get('cost', 0)
if (
brokermod
and (get_cost := getattr(
brokermod,
'get_cost',
False,
))
):
cost = get_cost(
price,
size,
is_taker=True,
)
return Transaction(
fqme=fqme,
tid=txdict['tid'],
dt=dt,
price=txdict['price'],
size=txdict['size'],
cost=txdict.get('cost', 0),
price=price,
size=size,
cost=cost,
bs_mktid=txdict['bs_mktid'],
expiry=parse(expiry) if expiry else None,
etype='clear',