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

View File

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

View File

@ -48,7 +48,9 @@ from piker.brokers import (
open_cached_client, open_cached_client,
BrokerError, BrokerError,
) )
from piker.clearing import OrderDialogs from piker.clearing import (
OrderDialogs,
)
from piker.clearing._messages import ( from piker.clearing._messages import (
BrokerdOrder, BrokerdOrder,
BrokerdOrderAck, BrokerdOrderAck,
@ -70,6 +72,33 @@ from .api import Client
log = get_logger('piker.brokers.binance') 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( async def handle_order_requests(
ems_order_stream: tractor.MsgStream, ems_order_stream: tractor.MsgStream,
client: Client, client: Client,

View File

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