Add transaction costs to "fills"
This makes a few major changes but mostly is centered around including transaction (aka trade-clear) costs in the avg breakeven price calculation. TL;DR: - rename `TradeRecord` -> `Transaction`. - make `Position.fills` a `dict[str, float]` which holds each clear's cost value. - change `Transaction.symkey` -> `.bsuid` for "backend symbol unique id". - drop `brokername: str` arg to `update_pps()` - rename `._split_active()` -> `dump_active()` and use input keys verbatim in output map. - in `update_pps_conf()` always incrementally update from trade records even when no `pps.toml` exists yet since it may be both the case that the ledger needs loading **and** the caller is handing new records not yet in the ledger.lifo_pps_ib
							parent
							
								
									c1b63f4757
								
							
						
					
					
						commit
						412138a75b
					
				
							
								
								
									
										106
									
								
								piker/pp.py
								
								
								
								
							
							
						
						
									
										106
									
								
								piker/pp.py
								
								
								
								
							|  | @ -15,9 +15,9 @@ | ||||||
| 
 | 
 | ||||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
| ''' | ''' | ||||||
| Personal/Private position parsing, calculmating, summarizing in a way | Personal/Private position parsing, calculating, summarizing in a way | ||||||
| that doesn't try to cuk most humans who prefer to not lose their moneys.. | that doesn't try to cuk most humans who prefer to not lose their moneys.. | ||||||
| (looking at you `ib` and shitzy friends) | (looking at you `ib` and dirt-bird friends) | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| from contextlib import contextmanager as cm | from contextlib import contextmanager as cm | ||||||
|  | @ -86,9 +86,9 @@ def open_trade_ledger( | ||||||
|                 return toml.dump(ledger, cf) |                 return toml.dump(ledger, cf) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TradeRecord(Struct): | class Transaction(Struct): | ||||||
|     fqsn: str  # normally fqsn |     fqsn: str  # normally fqsn | ||||||
|     tid: Union[str, int] |     tid: Union[str, int]  # unique transaction id | ||||||
|     size: float |     size: float | ||||||
|     price: float |     price: float | ||||||
|     cost: float  # commisions or other additional costs |     cost: float  # commisions or other additional costs | ||||||
|  | @ -98,7 +98,7 @@ class TradeRecord(Struct): | ||||||
|     # optional key normally derived from the broker |     # optional key normally derived from the broker | ||||||
|     # backend which ensures the instrument-symbol this record |     # backend which ensures the instrument-symbol this record | ||||||
|     # is for is truly unique. |     # is for is truly unique. | ||||||
|     symkey: Optional[Union[str, int]] = None |     bsuid: Optional[Union[str, int]] = None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Position(Struct): | class Position(Struct): | ||||||
|  | @ -113,9 +113,14 @@ class Position(Struct): | ||||||
|     # last size and avg entry price |     # last size and avg entry price | ||||||
|     size: float |     size: float | ||||||
|     avg_price: float  # TODO: contextual pricing |     avg_price: float  # TODO: contextual pricing | ||||||
|  |     bsuid: str | ||||||
| 
 | 
 | ||||||
|     # ordered record of known constituent trade messages |     # ordered record of known constituent trade messages | ||||||
|     fills: list[Union[str, int, Status]] = [] |     fills: dict[ | ||||||
|  |         Union[str, int, Status],  # trade id | ||||||
|  |         float,  # cost | ||||||
|  |     ] = {} | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     def to_dict(self): |     def to_dict(self): | ||||||
|         return { |         return { | ||||||
|  | @ -160,6 +165,7 @@ class Position(Struct): | ||||||
|         self, |         self, | ||||||
|         size: float, |         size: float, | ||||||
|         price: float, |         price: float, | ||||||
|  |         cost: float = 0, | ||||||
| 
 | 
 | ||||||
|         # TODO: idea: "real LIFO" dynamic positioning. |         # TODO: idea: "real LIFO" dynamic positioning. | ||||||
|         # - when a trade takes place where the pnl for |         # - when a trade takes place where the pnl for | ||||||
|  | @ -198,6 +204,8 @@ class Position(Struct): | ||||||
|             self.avg_price = ( |             self.avg_price = ( | ||||||
|                 abs(size) * price  # weight of current exec |                 abs(size) * price  # weight of current exec | ||||||
|                 + |                 + | ||||||
|  |                 cost  # transaction cost | ||||||
|  |                 + | ||||||
|                 self.avg_price * abs(self.size)  # weight of previous pp |                 self.avg_price * abs(self.size)  # weight of previous pp | ||||||
|             ) / abs(new_size) |             ) / abs(new_size) | ||||||
| 
 | 
 | ||||||
|  | @ -207,9 +215,7 @@ class Position(Struct): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def update_pps( | def update_pps( | ||||||
|     brokername: str, |     records: dict[str, Transaction], | ||||||
|     records: dict[str, TradeRecord], |  | ||||||
| 
 |  | ||||||
|     pps: Optional[dict[str, Position]] = None |     pps: Optional[dict[str, Position]] = None | ||||||
| 
 | 
 | ||||||
| ) -> dict[str, Position]: | ) -> dict[str, Position]: | ||||||
|  | @ -223,7 +229,7 @@ def update_pps( | ||||||
|     for r in records: |     for r in records: | ||||||
| 
 | 
 | ||||||
|         pp = pps.setdefault( |         pp = pps.setdefault( | ||||||
|             r.fqsn or r.symkey, |             r.fqsn or r.bsuid, | ||||||
| 
 | 
 | ||||||
|             # if no existing pp, allocate fresh one. |             # if no existing pp, allocate fresh one. | ||||||
|             Position( |             Position( | ||||||
|  | @ -233,27 +239,39 @@ def update_pps( | ||||||
|                 ), |                 ), | ||||||
|                 size=0.0, |                 size=0.0, | ||||||
|                 avg_price=0.0, |                 avg_price=0.0, | ||||||
|  |                 bsuid=r.bsuid, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         # don't do updates for ledger records we already have |         # don't do updates for ledger records we already have | ||||||
|         # included in the current pps state. |         # included in the current pps state. | ||||||
|         if r.tid in pp.fills: |         if r.tid in pp.fills: | ||||||
|             # NOTE: likely you'll see repeats of the same |             # NOTE: likely you'll see repeats of the same | ||||||
|             # ``TradeRecord`` passed in here if/when you are restarting |             # ``Transaction`` passed in here if/when you are restarting | ||||||
|             # a ``brokerd.ib`` where the API will re-report trades from |             # a ``brokerd.ib`` where the API will re-report trades from | ||||||
|             # the current session, so we need to make sure we don't |             # the current session, so we need to make sure we don't | ||||||
|             # "double count" these in pp calculations. |             # "double count" these in pp calculations. | ||||||
|             continue |             continue | ||||||
| 
 | 
 | ||||||
|         # lifo style average price calc |         # lifo style "breakeven" price calc | ||||||
|         pp.lifo_update(r.size, r.price) |         pp.lifo_update( | ||||||
|         pp.fills.append(r.tid) |             r.size, | ||||||
|  |             r.price, | ||||||
|  | 
 | ||||||
|  |             # include transaction cost in breakeven price | ||||||
|  |             # and presume the worst case of the same cost | ||||||
|  |             # to exit this transaction (even though in reality | ||||||
|  |             # it will be dynamic based on exit stratetgy). | ||||||
|  |             cost=2*r.cost, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # track clearing costs | ||||||
|  |         pp.fills[r.tid] = r.cost | ||||||
| 
 | 
 | ||||||
|     assert len(set(pp.fills)) == len(pp.fills) |     assert len(set(pp.fills)) == len(pp.fills) | ||||||
|     return pps |     return pps | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _split_active( | def dump_active( | ||||||
|     pps: dict[str, Position], |     pps: dict[str, Position], | ||||||
| 
 | 
 | ||||||
| ) -> tuple[ | ) -> tuple[ | ||||||
|  | @ -277,9 +295,9 @@ def _split_active( | ||||||
|         fqsn = pp.symbol.front_fqsn() |         fqsn = pp.symbol.front_fqsn() | ||||||
|         asdict = pp.to_dict() |         asdict = pp.to_dict() | ||||||
|         if pp.size == 0: |         if pp.size == 0: | ||||||
|             closed[fqsn] = asdict |             closed[k] = asdict | ||||||
|         else: |         else: | ||||||
|             active[fqsn] = asdict |             active[k] = asdict | ||||||
| 
 | 
 | ||||||
|     return active, closed |     return active, closed | ||||||
| 
 | 
 | ||||||
|  | @ -292,7 +310,7 @@ def load_pps_from_ledger( | ||||||
| ) -> tuple[dict, dict]: | ) -> tuple[dict, dict]: | ||||||
|     ''' |     ''' | ||||||
|     Open a ledger file by broker name and account and read in and |     Open a ledger file by broker name and account and read in and | ||||||
|     process any trade records into our normalized ``TradeRecord`` |     process any trade records into our normalized ``Transaction`` | ||||||
|     form and then pass these into the position processing routine |     form and then pass these into the position processing routine | ||||||
|     and deliver the two dict-sets of the active and closed pps. |     and deliver the two dict-sets of the active and closed pps. | ||||||
| 
 | 
 | ||||||
|  | @ -305,9 +323,8 @@ def load_pps_from_ledger( | ||||||
| 
 | 
 | ||||||
|     brokermod = get_brokermod(brokername) |     brokermod = get_brokermod(brokername) | ||||||
|     records = brokermod.norm_trade_records(ledger) |     records = brokermod.norm_trade_records(ledger) | ||||||
|     pps = update_pps(brokername, records) |     pps = update_pps(records) | ||||||
| 
 |     return dump_active(pps) | ||||||
|     return _split_active(pps) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_pps( | def get_pps( | ||||||
|  | @ -326,34 +343,34 @@ def get_pps( | ||||||
| def update_pps_conf( | def update_pps_conf( | ||||||
|     brokername: str, |     brokername: str, | ||||||
|     acctid: str, |     acctid: str, | ||||||
|     trade_records: Optional[list[TradeRecord]] = None, |     trade_records: Optional[list[Transaction]] = None, | ||||||
| 
 | 
 | ||||||
| ) -> dict[str, Position]: | ) -> dict[str, Position]: | ||||||
| 
 | 
 | ||||||
|     conf, path = config.load('pps') |     conf, path = config.load('pps') | ||||||
|     brokersection = conf.setdefault(brokername, {}) |     brokersection = conf.setdefault(brokername, {}) | ||||||
|     entries = brokersection.setdefault(acctid, {}) |     accountsection = pps = brokersection.setdefault(acctid, {}) | ||||||
| 
 |  | ||||||
|     if not entries: |  | ||||||
| 
 | 
 | ||||||
|  |     if not pps: | ||||||
|         # no pps entry yet for this broker/account so parse |         # no pps entry yet for this broker/account so parse | ||||||
|         # any available ledgers to build a pps state. |         # any available ledgers to build a pps state. | ||||||
|         active, closed = load_pps_from_ledger( |         pps, closed = load_pps_from_ledger( | ||||||
|             brokername, |             brokername, | ||||||
|             acctid, |             acctid, | ||||||
|         ) |         ) | ||||||
|  |         if not pps: | ||||||
|  |             log.warning( | ||||||
|  |                 f'No trade history could be loaded for {brokername}:{acctid}' | ||||||
|  |             ) | ||||||
| 
 | 
 | ||||||
|     elif trade_records: |     # unmarshal/load ``pps.toml`` config entries into object form. | ||||||
| 
 |     pp_objs = {} | ||||||
|         # table for map-back to object form |     for fqsn, entry in pps.items(): | ||||||
|         pps = {} |         pp_objs[fqsn] = Position( | ||||||
| 
 |  | ||||||
|         # load ``pps.toml`` config entries back into object form. |  | ||||||
|         for fqsn, entry in entries.items(): |  | ||||||
|             pps[f'{fqsn}.{brokername}'] = Position( |  | ||||||
|             Symbol.from_fqsn(fqsn, info={}), |             Symbol.from_fqsn(fqsn, info={}), | ||||||
|             size=entry['size'], |             size=entry['size'], | ||||||
|             avg_price=entry['avg_price'], |             avg_price=entry['avg_price'], | ||||||
|  |             bsuid=entry['bsuid'], | ||||||
| 
 | 
 | ||||||
|             # XXX: super critical, we need to be sure to include |             # XXX: super critical, we need to be sure to include | ||||||
|             # all pps.toml fills to avoid reusing fills that were |             # all pps.toml fills to avoid reusing fills that were | ||||||
|  | @ -363,20 +380,18 @@ def update_pps_conf( | ||||||
|             fills=entry['fills'], |             fills=entry['fills'], | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         pps = update_pps( |     # update all pp objects from any (new) trade records which | ||||||
|             brokername, |     # were passed in (aka incremental update case). | ||||||
|  |     if trade_records: | ||||||
|  |         pp_objs = update_pps( | ||||||
|             trade_records, |             trade_records, | ||||||
|             pps=pps, |             pps=pp_objs, | ||||||
|         ) |         ) | ||||||
|         active, closed = _split_active(pps) |  | ||||||
| 
 | 
 | ||||||
|     else: |     active, closed = dump_active(pp_objs) | ||||||
|         raise ValueError('wut wut') |  | ||||||
| 
 |  | ||||||
|     for fqsn in closed: |  | ||||||
|         print(f'removing closed pp: {fqsn}') |  | ||||||
|         entries.pop(fqsn, None) |  | ||||||
| 
 | 
 | ||||||
|  |     # dict-serialize all active pps | ||||||
|  |     pp_entries = {} | ||||||
|     for fqsn, pp_dict in active.items(): |     for fqsn, pp_dict in active.items(): | ||||||
|         print(f'Updating active pp: {fqsn}') |         print(f'Updating active pp: {fqsn}') | ||||||
| 
 | 
 | ||||||
|  | @ -386,8 +401,9 @@ def update_pps_conf( | ||||||
|         # XXX: ugh, it's cuz we push the section under |         # XXX: ugh, it's cuz we push the section under | ||||||
|         # the broker name.. maybe we need to rethink this? |         # the broker name.. maybe we need to rethink this? | ||||||
|         brokerless_key = fqsn.rstrip(f'.{brokername}') |         brokerless_key = fqsn.rstrip(f'.{brokername}') | ||||||
|         entries[brokerless_key] = pp_dict |         pp_entries[brokerless_key] = pp_dict | ||||||
| 
 | 
 | ||||||
|  |     conf[brokername][acctid] = pp_entries | ||||||
|     config.write( |     config.write( | ||||||
|         conf, |         conf, | ||||||
|         'pps', |         'pps', | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue