From eba6a779660b1ddd7b284646b3e462350441fe80 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 7 Aug 2023 09:55:45 -0400 Subject: [PATCH] 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. --- piker/accounting/_ledger.py | 9 ++++-- piker/brokers/binance/__init__.py | 2 ++ piker/brokers/binance/broker.py | 31 +++++++++++++++++- piker/clearing/_paper_engine.py | 54 +++++++++++++++++++++++++------ 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/piker/accounting/_ledger.py b/piker/accounting/_ledger.py index 8eac518a..82a77107 100644 --- a/piker/accounting/_ledger.py +++ b/piker/accounting/_ledger.py @@ -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()): diff --git a/piker/brokers/binance/__init__.py b/piker/brokers/binance/__init__.py index da63a67c..830b1acf 100644 --- a/piker/brokers/binance/__init__.py +++ b/piker/brokers/binance/__init__.py @@ -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', diff --git a/piker/brokers/binance/broker.py b/piker/brokers/binance/broker.py index 6dcf99b5..ff6a2ff5 100644 --- a/piker/brokers/binance/broker.py +++ b/piker/brokers/binance/broker.py @@ -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, diff --git a/piker/clearing/_paper_engine.py b/piker/clearing/_paper_engine.py index 6af25426..f1c1a3d9 100644 --- a/piker/clearing/_paper_engine.py +++ b/piker/clearing/_paper_engine.py @@ -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',