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 __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()):
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue