From 72c97d4672e40642ae7b85eb960ea7e328b3ac29 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 17 Mar 2023 19:45:43 -0400 Subject: [PATCH] Handle read and write of `pps.toml` using `MktPair` Add a logic branch for now that switches on an instance check. Generally swap over all `Position.symbol` and `Transaction.sym` refs to `MktPair`. Do a wholesale rename of all `.bsuid` var names to `.bs_mktid`. --- piker/accounting/__init__.py | 10 +-- piker/accounting/_ledger.py | 7 +- piker/accounting/_pos.py | 150 +++++++++++++++++++------------- piker/brokers/ib/README.rst | 2 +- piker/brokers/ib/broker.py | 24 ++--- piker/clearing/_allocate.py | 2 +- piker/clearing/_paper_engine.py | 4 +- piker/ui/order_mode.py | 2 +- 8 files changed, 112 insertions(+), 89 deletions(-) diff --git a/piker/accounting/__init__.py b/piker/accounting/__init__.py index a371f7c2..7d7fbb85 100644 --- a/piker/accounting/__init__.py +++ b/piker/accounting/__init__.py @@ -48,7 +48,7 @@ __all__ = [ def get_likely_pair( src: str, dst: str, - bsuid: str, + bs_mktid: str, ) -> str: ''' @@ -57,7 +57,7 @@ def get_likely_pair( ''' try: - src_name_start = bsuid.rindex(src) + src_name_start = bs_mktid.rindex(src) except ( ValueError, # substr not found ): @@ -66,13 +66,13 @@ def get_likely_pair( # buy some other dst which was furhter used # to buy another dst..) log.warning( - f'No src fiat {src} found in {bsuid}?' + f'No src fiat {src} found in {bs_mktid}?' ) return - likely_dst = bsuid[:src_name_start] + likely_dst = bs_mktid[:src_name_start] if likely_dst == dst: - return bsuid + return bs_mktid if __name__ == '__main__': diff --git a/piker/accounting/_ledger.py b/piker/accounting/_ledger.py index 1718dda1..14fca94c 100644 --- a/piker/accounting/_ledger.py +++ b/piker/accounting/_ledger.py @@ -129,12 +129,7 @@ class Transaction(Struct, frozen=True): # in the "their backend/system" sense; i.e. this uid for the market # as defined (internally) in some namespace defined by the broker # service. - bsuid: str | int | None = None - - @property - def bs_mktid(self) -> str | int | None: - print(f'STOP USING .bsuid` for {self.fqme}') - return self.bs_mktid + bs_mktid: str | int | None = None # XXX NOTE: this will come from the `MktPair` # instead of defined here right? diff --git a/piker/accounting/_pos.py b/piker/accounting/_pos.py index 43f62ed9..288a702e 100644 --- a/piker/accounting/_pos.py +++ b/piker/accounting/_pos.py @@ -45,7 +45,8 @@ from ._ledger import ( ) from ._mktinfo import ( Symbol, - # MktPair, + MktPair, + Asset, unpack_fqsn, ) from .. import config @@ -63,7 +64,7 @@ class Position(Struct): transaction history. ''' - symbol: Symbol # | MktPair + symbol: Symbol | MktPair # can be +ve or -ve for long/short size: float @@ -72,17 +73,17 @@ class Position(Struct): # zero for the entirety of the current "trade state". ppu: float - # unique backend symbol id - bsuid: str + # unique "backend system market id" + bs_mktid: str - split_ratio: Optional[int] = None + split_ratio: int | None = None # ordered record of known constituent trade messages clears: dict[ Union[str, int, Status], # trade id dict[str, Any], # transaction history summaries ] = {} - first_clear_dt: Optional[datetime] = None + first_clear_dt: datetime | None = None expiry: Optional[datetime] = None @@ -117,19 +118,34 @@ class Position(Struct): fqsn = s.fqme broker, key, suffix = unpack_fqsn(fqsn) - sym_info = s.broker_info[broker] - d['asset_type'] = sym_info['asset_type'] - d['price_tick_size'] = ( - sym_info.get('price_tick_size') - or - s.tick_size - ) - d['lot_tick_size'] = ( - sym_info.get('lot_tick_size') - or - s.lot_tick_size - ) + if isinstance(s, Symbol): + sym_info = s.broker_info[broker] + d['asset_type'] = sym_info['asset_type'] + d['price_tick'] = ( + sym_info.get('price_tick_size') + or + s.tick_size + ) + d['size_tick'] = ( + sym_info.get('lot_tick_size') + or + s.lot_tick_size + ) + + # the newwww wayyy B) + else: + mkt = s + assert isinstance(mkt, MktPair) + + # an asset resolved mkt where we have ``Asset`` info about + # each tradeable asset in the market. + if mkt.resolved: + dst: Asset = mkt.dst + d['asset_type'] = dst.atype + + d['price_tick'] = mkt.price_tick + d['size_tick'] = mkt.size_tick if self.expiry is None: d.pop('expiry', None) @@ -217,14 +233,19 @@ class Position(Struct): # XXX: better place to do this? symbol = self.symbol - lot_size_digits = symbol.lot_size_digits + # TODO: switch to new fields..? + # .size_tick_digits, .price_tick_digits + size_tick_digits = symbol.lot_size_digits + price_tick_digits = symbol.tick_size_digits + self.ppu = round( + # TODO: change this to ppu? msg['avg_price'], - ndigits=symbol.tick_size_digits, + ndigits=price_tick_digits, ) self.size = round( msg['size'], - ndigits=lot_size_digits, + ndigits=size_tick_digits, ) @property @@ -490,7 +511,7 @@ class PpTable(Struct): reverse=True, ): pp = pps.setdefault( - t.bsuid, + t.bs_mktid, # if no existing pp, allocate fresh one. Position( @@ -500,7 +521,7 @@ class PpTable(Struct): ) if not t.sym else t.sym, size=0.0, ppu=0.0, - bsuid=t.bsuid, + bs_mktid=t.bs_mktid, expiry=t.expiry, ) ) @@ -526,10 +547,10 @@ class PpTable(Struct): # update clearing table pp.add_clear(t) - updated[t.bsuid] = pp + updated[t.bs_mktid] = pp # minimize clears tables and update sizing. - for bsuid, pp in updated.items(): + for bs_mktid, pp in updated.items(): pp.ensure_state() # deliver only the position entries that were actually updated @@ -557,14 +578,8 @@ class PpTable(Struct): open_pp_objs: dict[str, Position] = {} pp_objs = self.pps - for bsuid in list(pp_objs): - pp = pp_objs[bsuid] - - # XXX: debug hook for size mismatches - # qqqbsuid = 320227571 - # if bsuid == qqqbsuid: - # breakpoint() - + for bs_mktid in list(pp_objs): + pp = pp_objs[bs_mktid] pp.ensure_state() if ( @@ -583,10 +598,10 @@ class PpTable(Struct): # ignored; the closed positions won't be written to the # ``pps.toml`` since ``pp_active_entries`` above is what's # written. - closed_pp_objs[bsuid] = pp + closed_pp_objs[bs_mktid] = pp else: - open_pp_objs[bsuid] = pp + open_pp_objs[bs_mktid] = pp return open_pp_objs, closed_pp_objs @@ -600,7 +615,7 @@ class PpTable(Struct): # we don't store in the ``pps.toml``. to_toml_dict = {} - for bsuid, pos in active.items(): + for bs_mktid, pos in active.items(): # keep the minimal amount of clears that make up this # position since the last net-zero state. @@ -674,7 +689,7 @@ def load_pps_from_ledger( Open a ledger file by broker name and account and read in and process any trade records into our normalized ``Transaction`` form and then update the equivalent ``Pptable`` and deliver the two - bsuid-mapped dict-sets of the transactions and pps. + bs_mktid-mapped dict-sets of the transactions and pps. ''' with ( @@ -690,9 +705,9 @@ def load_pps_from_ledger( if filter_by: records = {} - bsuids = set(filter_by) + bs_mktids = set(filter_by) for tid, r in src_records.items(): - if r.bsuid in bsuids: + if r.bs_mktid in bs_mktids: records[tid] = r else: records = src_records @@ -868,22 +883,35 @@ def open_pps( # unmarshal/load ``pps.toml`` config entries into object form # and update `PpTable` obj entries. - for fqsn, entry in pps.items(): - bsuid = entry['bsuid'] - symbol = Symbol.from_fqsn( - fqsn, + for fqme, entry in pps.items(): - # NOTE & TODO: right now we fill in the defaults from - # `.data._source.Symbol` but eventually these should always - # either be already written to the pos table or provided at - # write time to ensure always having these values somewhere - # and thus allowing us to get our pos sizing precision - # correct! - info={ - 'asset_type': entry.get('asset_type', ''), - 'price_tick_size': entry.get('price_tick_size', 0.01), - 'lot_tick_size': entry.get('lot_tick_size', 0.0), - } + # atype = entry.get('asset_type', '') + + # unique broker market id + bs_mktid = ( + entry.get('bsuid') + or entry.get('bs_mktid') + ) + price_tick = ( + entry.get('price_tick_size') + or entry.get('price_tick') + or 0.01 + ) + size_tick = ( + entry.get('lot_tick_size') + or entry.get('size_tick') + or 0.0 + ) + + # load the pair using the fqme which + # will make the pair "unresolved" until + # the backend broker actually loads + # the market and position info. + mkt = MktPair.from_fqme( + fqme, + price_tick=price_tick, + size_tick=size_tick, + bs_mktid=bs_mktid ) # convert clears sub-tables (only in this form @@ -893,7 +921,7 @@ def open_pps( # index clears entries in "object" form by tid in a top # level dict instead of a list (as is presented in our # ``pps.toml``). - clears = pp_objs.setdefault(bsuid, {}) + clears = pp_objs.setdefault(bs_mktid, {}) # TODO: should be make a ``Struct`` for clear/event entries? # convert "clear events table" from the toml config (list of @@ -908,9 +936,9 @@ def open_pps( clears_table['dt'] = dt trans.append(Transaction( - fqsn=bsuid, - sym=symbol, - bsuid=bsuid, + fqsn=bs_mktid, + sym=mkt, + bs_mktid=bs_mktid, tid=tid, size=clears_table['size'], price=clears_table['price'], @@ -933,13 +961,13 @@ def open_pps( if expiry: expiry = pendulum.parse(expiry) - pp = pp_objs[bsuid] = Position( - symbol, + pp = pp_objs[bs_mktid] = Position( + mkt, size=size, ppu=ppu, split_ratio=split_ratio, expiry=expiry, - bsuid=entry['bsuid'], + bs_mktid=bs_mktid, ) # XXX: super critical, we need to be sure to include diff --git a/piker/brokers/ib/README.rst b/piker/brokers/ib/README.rst index c8661317..d56b52ca 100644 --- a/piker/brokers/ib/README.rst +++ b/piker/brokers/ib/README.rst @@ -127,7 +127,7 @@ your ``pps.toml`` file will have position entries like, [ib.algopaper."mnq.globex.20221216"] size = -1.0 ppu = 12423.630576923071 - bsuid = 515416577 + bs_mktid = 515416577 expiry = "2022-12-16T00:00:00+00:00" clears = [ { dt = "2022-08-31T18:54:46+00:00", ppu = 12423.630576923071, accum_size = -19.0, price = 12372.75, size = 1.0, cost = 0.57, tid = "0000e1a7.630f5e5a.01.01" }, diff --git a/piker/brokers/ib/broker.py b/piker/brokers/ib/broker.py index 0944bf86..66dfe212 100644 --- a/piker/brokers/ib/broker.py +++ b/piker/brokers/ib/broker.py @@ -335,12 +335,12 @@ async def update_and_audit_msgs( msgs: list[BrokerdPosition] = [] for p in pps: - bsuid = p.bsuid + bs_mktid = p.bs_mktid # retreive equivalent ib reported position message # for comparison/audit versus the piker equivalent # breakeven pp calcs. - ibppmsg = cids2pps.get((acctid, bsuid)) + ibppmsg = cids2pps.get((acctid, bs_mktid)) if ibppmsg: msg = BrokerdPosition( @@ -555,18 +555,18 @@ async def trades_dialogue( # collect all ib-pp reported positions so that we can be # sure know which positions to update from the ledger if # any are missing from the ``pps.toml`` - bsuid, msg = pack_position(pos) + bs_mktid, msg = pack_position(pos) acctid = msg.account = accounts_def.inverse[msg.account] acctid = acctid.strip('ib.') - cids2pps[(acctid, bsuid)] = msg + cids2pps[(acctid, bs_mktid)] = msg assert msg.account in accounts, ( f'Position for unknown account: {msg.account}') ledger = ledgers[acctid] table = tables[acctid] - pp = table.pps.get(bsuid) + pp = table.pps.get(bs_mktid) if ( not pp or pp.size != msg.size @@ -605,12 +605,12 @@ async def trades_dialogue( # the updated output (maybe this is a bug?) but # if you create a pos from TWS and then load it # from the api trades it seems we get a key - # error from ``update[bsuid]`` ? - pp = table.pps.get(bsuid) + # error from ``update[bs_mktid]`` ? + pp = table.pps.get(bs_mktid) if not pp: log.error( f'The contract id for {msg} may have ' - f'changed to {bsuid}\nYou may need to ' + f'changed to {bs_mktid}\nYou may need to ' 'adjust your ledger for this, skipping ' 'for now.' ) @@ -620,8 +620,8 @@ async def trades_dialogue( # the updated output (maybe this is a bug?) but # if you create a pos from TWS and then load it # from the api trades it seems we get a key - # error from ``update[bsuid]`` ? - pp = table.pps[bsuid] + # error from ``update[bs_mktid]`` ? + pp = table.pps[bs_mktid] pairinfo = pp.symbol if msg.size != pp.size: log.error( @@ -760,7 +760,7 @@ async def emit_pp_update( # re-formatted pps as msgs to the ems. for pos in filter( bool, - [active.get(r.bsuid), closed.get(r.bsuid)] + [active.get(r.bs_mktid), closed.get(r.bs_mktid)] ): msgs = await update_and_audit_msgs( acctid, @@ -1225,7 +1225,7 @@ def norm_trade_records( cost=comms, dt=dt, expiry=expiry, - bsuid=conid, + bs_mktid=conid, ), key=lambda t: t.dt ) diff --git a/piker/clearing/_allocate.py b/piker/clearing/_allocate.py index 023d1e92..657ba8e1 100644 --- a/piker/clearing/_allocate.py +++ b/piker/clearing/_allocate.py @@ -206,7 +206,7 @@ class Allocator(Struct): symbol=sym, size=order_size, ppu=price, - bsuid=sym, + bs_mktid=sym, ) ) diff --git a/piker/clearing/_paper_engine.py b/piker/clearing/_paper_engine.py index 1726aed6..bfec7260 100644 --- a/piker/clearing/_paper_engine.py +++ b/piker/clearing/_paper_engine.py @@ -1,5 +1,5 @@ # piker: trading gear for hackers -# Copyright (C) Tyler Goodlet (in stewardship for piker0) +# Copyright (C) Tyler Goodlet (in stewardship for pikers) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -258,7 +258,7 @@ class PaperBoi(Struct): price=price, cost=0, # TODO: cost model dt=pendulum.from_timestamp(fill_time_s), - bsuid=key, + bs_mktid=key, ) with ( diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 43598d9b..e6c4ed33 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -737,7 +737,7 @@ async def open_order_mode( ppu=0, # XXX: BLEH, do we care about this on the client side? - bsuid=symbol, + bs_mktid=symbol.key, ) # allocator config