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
parent
5ed8544fd1
commit
eba6a77966
|
@ -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()):
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue