Merge pull request #470 from pikers/decimalization_take_2
Fixed float dust bug on zero positionxdo_and_you
						commit
						201f86e482
					
				|  | @ -70,7 +70,10 @@ from piker.clearing._messages import ( | |||
|     BrokerdFill, | ||||
|     BrokerdError, | ||||
| ) | ||||
| from piker.data._source import Symbol | ||||
| from piker.data._source import ( | ||||
|     Symbol, | ||||
|     float_digits, | ||||
| ) | ||||
| from .api import ( | ||||
|     _accounts2clients, | ||||
|     con2fqsn, | ||||
|  | @ -304,6 +307,9 @@ async def update_ledger_from_api_trades( | |||
| 
 | ||||
|         entry['listingExchange'] = pexch | ||||
| 
 | ||||
|         # pack in the ``Contract.secType`` | ||||
|         entry['asset_type'] = condict['secType'] | ||||
| 
 | ||||
|     conf = get_config() | ||||
|     entries = api_trades_to_ledger_entries( | ||||
|         conf['accounts'].inverse, | ||||
|  | @ -616,9 +622,10 @@ async def trades_dialogue( | |||
|                         # from the api trades it seems we get a key | ||||
|                         # error from ``update[bsuid]`` ? | ||||
|                         pp = table.pps[bsuid] | ||||
|                         pairinfo = pp.symbol | ||||
|                         if msg.size != pp.size: | ||||
|                             log.error( | ||||
|                                 f'Position mismatch {pp.symbol.front_fqsn()}:\n' | ||||
|                                 f'Pos size mismatch {pairinfo.front_fqsn()}:\n' | ||||
|                                 f'ib: {msg.size}\n' | ||||
|                                 f'piker: {pp.size}\n' | ||||
|                             ) | ||||
|  | @ -1095,13 +1102,15 @@ def norm_trade_records( | |||
| 
 | ||||
|     ''' | ||||
|     records: list[Transaction] = [] | ||||
|     for tid, record in ledger.items(): | ||||
| 
 | ||||
|     for tid, record in ledger.items(): | ||||
|         conid = record.get('conId') or record['conid'] | ||||
|         comms = record.get('commission') | ||||
|         if comms is None: | ||||
|             comms = -1*record['ibCommission'] | ||||
| 
 | ||||
|         price = record.get('price') or record['tradePrice'] | ||||
|         price_tick_digits = float_digits(price) | ||||
| 
 | ||||
|         # the api doesn't do the -/+ on the quantity for you but flex | ||||
|         # records do.. are you fucking serious ib...!? | ||||
|  | @ -1144,9 +1153,14 @@ def norm_trade_records( | |||
| 
 | ||||
|         # special handling of symbol extraction from | ||||
|         # flex records using some ad-hoc schema parsing. | ||||
|         instr = record.get('assetCategory') | ||||
|         if instr == 'FUT': | ||||
|             symbol = record['description'][:3] | ||||
|         asset_type: str = record.get('assetCategory') or record['secType'] | ||||
| 
 | ||||
|         # TODO: XXX: WOA this is kinda hacky.. probably | ||||
|         # should figure out the correct future pair key more | ||||
|         # explicitly and consistently? | ||||
|         if asset_type == 'FUT': | ||||
|             # (flex) ledger entries don't have any simple 3-char key? | ||||
|             symbol = record['symbol'][:3] | ||||
| 
 | ||||
|         # try to build out piker fqsn from record. | ||||
|         expiry = record.get( | ||||
|  | @ -1156,10 +1170,34 @@ def norm_trade_records( | |||
|             suffix = f'{exch}.{expiry}' | ||||
|             expiry = pendulum.parse(expiry) | ||||
| 
 | ||||
|         fqsn = Symbol.from_fqsn( | ||||
|         src: str = record['currency'] | ||||
| 
 | ||||
|         pair = Symbol.from_fqsn( | ||||
|             fqsn=f'{symbol}.{suffix}.ib', | ||||
|             info={}, | ||||
|         ).front_fqsn().rstrip('.ib') | ||||
|             info={ | ||||
|                 'tick_size_digits': price_tick_digits, | ||||
| 
 | ||||
|                 # NOTE: for "legacy" assets, volume is normally discreet, not | ||||
|                 # a float, but we keep a digit in case the suitz decide | ||||
|                 # to get crazy and change it; we'll be kinda ready | ||||
|                 # schema-wise.. | ||||
|                 'lot_size_digits': 1, | ||||
| 
 | ||||
|                 # TODO: remove when we switching from | ||||
|                 # ``Symbol`` -> ``MktPair`` | ||||
|                 'asset_type': asset_type, | ||||
| 
 | ||||
|                 # TODO: figure out a target fin-type name | ||||
|                 # set and normalize to that here! | ||||
|                 'dst_type': asset_type.lower(), | ||||
| 
 | ||||
|                 # starting to use new key naming as in ``MktPair`` | ||||
|                 # type have drafted... | ||||
|                 'src': src, | ||||
|                 'src_type': 'fiat', | ||||
|             }, | ||||
|         ) | ||||
|         fqsn = pair.front_fqsn().rstrip('.ib') | ||||
| 
 | ||||
|         # NOTE: for flex records the normal fields for defining an fqsn | ||||
|         # sometimes won't be available so we rely on two approaches for | ||||
|  | @ -1175,6 +1213,7 @@ def norm_trade_records( | |||
|             records, | ||||
|             Transaction( | ||||
|                 fqsn=fqsn, | ||||
|                 sym=pair, | ||||
|                 tid=tid, | ||||
|                 size=size, | ||||
|                 price=price, | ||||
|  | @ -1201,7 +1240,11 @@ def parse_flex_dt( | |||
| 
 | ||||
| def api_trades_to_ledger_entries( | ||||
|     accounts: bidict, | ||||
|     trade_entries: list[object], | ||||
| 
 | ||||
|     # TODO: maybe we should just be passing through the | ||||
|     # ``ib_insync.order.Trade`` instance directly here | ||||
|     # instead of pre-casting to dicts? | ||||
|     trade_entries: list[dict], | ||||
| 
 | ||||
| ) -> dict: | ||||
|     ''' | ||||
|  |  | |||
|  | @ -770,7 +770,7 @@ async def stream_quotes( | |||
| 
 | ||||
|             syminfo['price_tick_size'] = max(syminfo['minTick'], min_tick) | ||||
| 
 | ||||
|             # for "traditional" assets, volume is normally discreet, not | ||||
|             # for "legacy" assets, volume is normally discreet, not | ||||
|             # a float | ||||
|             syminfo['lot_tick_size'] = 0.0 | ||||
| 
 | ||||
|  |  | |||
|  | @ -40,6 +40,8 @@ import base64 | |||
| import trio | ||||
| 
 | ||||
| from piker import config | ||||
| from piker.data.types import Struct | ||||
| from piker.data._source import Symbol | ||||
| from piker.brokers._util import ( | ||||
|     resproc, | ||||
|     SymbolNotFound, | ||||
|  | @ -113,11 +115,53 @@ class InvalidKey(ValueError): | |||
|     ''' | ||||
| 
 | ||||
| 
 | ||||
| # https://www.kraken.com/features/api#get-tradable-pairs | ||||
| class Pair(Struct): | ||||
|     altname: str  # alternate pair name | ||||
|     wsname: str  # WebSocket pair name (if available) | ||||
|     aclass_base: str  # asset class of base component | ||||
|     base: str  # asset id of base component | ||||
|     aclass_quote: str  # asset class of quote component | ||||
|     quote: str  # asset id of quote component | ||||
|     lot: str  # volume lot size | ||||
| 
 | ||||
|     cost_decimals: int | ||||
|     costmin: float | ||||
|     pair_decimals: int  # scaling decimal places for pair | ||||
|     lot_decimals: int  # scaling decimal places for volume | ||||
| 
 | ||||
|     # amount to multiply lot volume by to get currency volume | ||||
|     lot_multiplier: float | ||||
| 
 | ||||
|     # array of leverage amounts available when buying | ||||
|     leverage_buy: list[int] | ||||
|     # array of leverage amounts available when selling | ||||
|     leverage_sell: list[int] | ||||
| 
 | ||||
|     # fee schedule array in [volume, percent fee] tuples | ||||
|     fees: list[tuple[int, float]] | ||||
| 
 | ||||
|     # maker fee schedule array in [volume, percent fee] tuples (if on | ||||
|     # maker/taker) | ||||
|     fees_maker: list[tuple[int, float]] | ||||
| 
 | ||||
|     fee_volume_currency: str  # volume discount currency | ||||
|     margin_call: str  # margin call level | ||||
|     margin_stop: str  # stop-out/liquidation margin level | ||||
|     ordermin: float  # minimum order volume for pair | ||||
|     tick_size: float  # min price step size | ||||
|     status: str | ||||
| 
 | ||||
|     short_position_limit: float = 0 | ||||
|     long_position_limit: float = float('inf') | ||||
| 
 | ||||
| 
 | ||||
| class Client: | ||||
| 
 | ||||
|     # global symbol normalization table | ||||
|     _ntable: dict[str, str] = {} | ||||
|     _atable: bidict[str, str] = bidict() | ||||
|     _pairs: dict[str, Pair] = {} | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|  | @ -133,13 +177,12 @@ class Client: | |||
|                 'krakenex/2.1.0 (+https://github.com/veox/python3-krakenex)' | ||||
|         }) | ||||
|         self.conf: dict[str, str] = config | ||||
|         self._pairs: list[str] = [] | ||||
|         self._name = name | ||||
|         self._api_key = api_key | ||||
|         self._secret = secret | ||||
| 
 | ||||
|     @property | ||||
|     def pairs(self) -> dict[str, Any]: | ||||
|     def pairs(self) -> dict[str, Pair]: | ||||
|         if self._pairs is None: | ||||
|             raise RuntimeError( | ||||
|                 "Make sure to run `cache_symbols()` on startup!" | ||||
|  | @ -295,15 +338,28 @@ class Client: | |||
| 
 | ||||
|         trans: dict[str, Transaction] = {} | ||||
|         for entry in xfers: | ||||
|             # look up the normalized name | ||||
|             asset = self._atable[entry['asset']].lower() | ||||
| 
 | ||||
|             # look up the normalized name and asset info | ||||
|             asset_key = entry['asset'] | ||||
|             asset_info = self.assets[asset_key] | ||||
|             asset = self._atable[asset_key].lower() | ||||
| 
 | ||||
|             # XXX: this is in the asset units (likely) so it isn't | ||||
|             # quite the same as a commisions cost necessarily..) | ||||
|             cost = float(entry['fee']) | ||||
| 
 | ||||
|             fqsn = asset + '.kraken' | ||||
|             pairinfo = Symbol.from_fqsn( | ||||
|                 fqsn, | ||||
|                 info={ | ||||
|                     'asset_type': 'crypto', | ||||
|                     'lot_tick_size': asset_info['decimals'], | ||||
|                 }, | ||||
|             ) | ||||
| 
 | ||||
|             tran = Transaction( | ||||
|                 fqsn=asset + '.kraken', | ||||
|                 fqsn=fqsn, | ||||
|                 sym=pairinfo, | ||||
|                 tid=entry['txid'], | ||||
|                 dt=pendulum.from_timestamp(entry['time']), | ||||
|                 bsuid=f'{asset}{src_asset}', | ||||
|  | @ -317,7 +373,7 @@ class Client: | |||
|                 price='NaN', | ||||
| 
 | ||||
|                 # XXX: see note above | ||||
|                 cost=0, | ||||
|                 cost=cost, | ||||
|             ) | ||||
|             trans[tran.tid] = tran | ||||
| 
 | ||||
|  | @ -372,7 +428,7 @@ class Client: | |||
|         self, | ||||
|         pair: Optional[str] = None, | ||||
| 
 | ||||
|     ) -> dict[str, dict[str, str]]: | ||||
|     ) -> dict[str, Pair] | Pair: | ||||
| 
 | ||||
|         if pair is not None: | ||||
|             pairs = {'pair': pair} | ||||
|  | @ -389,19 +445,36 @@ class Client: | |||
| 
 | ||||
|         if pair is not None: | ||||
|             _, data = next(iter(pairs.items())) | ||||
|             return data | ||||
|             return Pair(**data) | ||||
|         else: | ||||
|             return pairs | ||||
|             return {key: Pair(**data) for key, data in pairs.items()} | ||||
| 
 | ||||
|     async def cache_symbols( | ||||
|         self, | ||||
|     ) -> dict: | ||||
|     async def cache_symbols(self) -> dict: | ||||
|         ''' | ||||
|         Load all market pair info build and cache it for downstream use. | ||||
| 
 | ||||
|         A ``._ntable: dict[str, str]`` is available for mapping the | ||||
|         websocket pair name-keys and their http endpoint API (smh) | ||||
|         equivalents to the "alternative name" which is generally the one | ||||
|         we actually want to use XD | ||||
| 
 | ||||
|         ''' | ||||
|         if not self._pairs: | ||||
|             self._pairs = await self.symbol_info() | ||||
|             self._pairs.update(await self.symbol_info()) | ||||
| 
 | ||||
|             ntable = {} | ||||
|             for restapikey, info in self._pairs.items(): | ||||
|                 ntable[restapikey] = ntable[info['wsname']] = info['altname'] | ||||
|             # table of all ws and rest keys to their alt-name values. | ||||
|             ntable: dict[str, str] = {} | ||||
| 
 | ||||
|             for rest_key in list(self._pairs.keys()): | ||||
| 
 | ||||
|                 pair: Pair = self._pairs[rest_key] | ||||
|                 altname = pair.altname | ||||
|                 wsname = pair.wsname | ||||
|                 ntable[rest_key] = ntable[wsname] = altname | ||||
| 
 | ||||
|                 # register the pair under all monikers, a giant flat | ||||
|                 # surjection of all possible names to each info obj. | ||||
|                 self._pairs[altname] = self._pairs[wsname] = pair | ||||
| 
 | ||||
|             self._ntable.update(ntable) | ||||
| 
 | ||||
|  | @ -411,26 +484,34 @@ class Client: | |||
|         self, | ||||
|         pattern: str, | ||||
|         limit: int = None, | ||||
| 
 | ||||
|     ) -> dict[str, Any]: | ||||
|         if self._pairs is not None: | ||||
|             data = self._pairs | ||||
|         else: | ||||
|             data = await self.symbol_info() | ||||
|         ''' | ||||
|         Search for a symbol by "alt name".. | ||||
| 
 | ||||
|         It is expected that the ``Client._pairs`` table | ||||
|         gets populated before conducting the underlying fuzzy-search | ||||
|         over the pair-key set. | ||||
| 
 | ||||
|         ''' | ||||
|         if not len(self._pairs): | ||||
|             await self.cache_symbols() | ||||
|             assert self._pairs, '`Client.cache_symbols()` was never called!?' | ||||
| 
 | ||||
|         matches = fuzzy.extractBests( | ||||
|             pattern, | ||||
|             data, | ||||
|             self._pairs, | ||||
|             score_cutoff=50, | ||||
|         ) | ||||
|         # repack in dict form | ||||
|         return {item[0]['altname']: item[0] for item in matches} | ||||
|         return {item[0].altname: item[0] for item in matches} | ||||
| 
 | ||||
|     async def bars( | ||||
|         self, | ||||
|         symbol: str = 'XBTUSD', | ||||
| 
 | ||||
|         # UTC 2017-07-02 12:53:20 | ||||
|         since: Optional[Union[int, datetime]] = None, | ||||
|         since: Union[int, datetime] | None = None, | ||||
|         count: int = 720,  # <- max allowed per query | ||||
|         as_np: bool = True, | ||||
| 
 | ||||
|  | @ -506,7 +587,7 @@ class Client: | |||
|     def normalize_symbol( | ||||
|         cls, | ||||
|         ticker: str | ||||
|     ) -> str: | ||||
|     ) -> tuple[str, Pair]: | ||||
|         ''' | ||||
|         Normalize symbol names to to a 3x3 pair from the global | ||||
|         definition map which we build out from the data retreived from | ||||
|  | @ -514,7 +595,7 @@ class Client: | |||
| 
 | ||||
|         ''' | ||||
|         ticker = cls._ntable[ticker] | ||||
|         return ticker.lower() | ||||
|         return ticker.lower(), cls._pairs[ticker] | ||||
| 
 | ||||
| 
 | ||||
| @acm | ||||
|  |  | |||
|  | @ -48,6 +48,7 @@ from piker.pp import ( | |||
|     open_trade_ledger, | ||||
|     open_pps, | ||||
| ) | ||||
| from piker.data._source import Symbol | ||||
| from piker.clearing._messages import ( | ||||
|     Order, | ||||
|     Status, | ||||
|  | @ -469,13 +470,12 @@ async def trades_dialogue( | |||
|         with ( | ||||
|             open_pps( | ||||
|                 'kraken', | ||||
|                 acctid, | ||||
|                 write_on_exit=True, | ||||
|                 acctid | ||||
|             ) as table, | ||||
| 
 | ||||
|             open_trade_ledger( | ||||
|                 'kraken', | ||||
|                 acctid, | ||||
|                 acctid | ||||
|             ) as ledger_dict, | ||||
|         ): | ||||
|             # transaction-ify the ledger entries | ||||
|  | @ -1197,10 +1197,21 @@ def norm_trade_records( | |||
|         }[record['type']] | ||||
| 
 | ||||
|         # we normalize to kraken's `altname` always.. | ||||
|         bsuid = norm_sym = Client.normalize_symbol(record['pair']) | ||||
|         bsuid, pair_info = Client.normalize_symbol(record['pair']) | ||||
|         fqsn = f'{bsuid}.kraken' | ||||
| 
 | ||||
|         mktpair = Symbol.from_fqsn( | ||||
|             fqsn, | ||||
|             info={ | ||||
|                 'lot_size_digits': pair_info.lot_decimals, | ||||
|                 'tick_size_digits': pair_info.pair_decimals, | ||||
|                 'asset_type': 'crypto', | ||||
|             }, | ||||
|         ) | ||||
| 
 | ||||
|         records[tid] = Transaction( | ||||
|             fqsn=f'{norm_sym}.kraken', | ||||
|             fqsn=fqsn, | ||||
|             sym=mktpair, | ||||
|             tid=tid, | ||||
|             size=size, | ||||
|             price=float(record['price']), | ||||
|  |  | |||
|  | @ -42,56 +42,15 @@ from piker.brokers._util import ( | |||
|     DataUnavailable, | ||||
| ) | ||||
| from piker.log import get_console_log | ||||
| from piker.data import ShmArray | ||||
| from piker.data.types import Struct | ||||
| from piker.data._web_bs import open_autorecon_ws, NoBsWs | ||||
| from . import log | ||||
| from .api import ( | ||||
|     Client, | ||||
|     Pair, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| # https://www.kraken.com/features/api#get-tradable-pairs | ||||
| class Pair(Struct): | ||||
|     altname: str  # alternate pair name | ||||
|     wsname: str  # WebSocket pair name (if available) | ||||
|     aclass_base: str  # asset class of base component | ||||
|     base: str  # asset id of base component | ||||
|     aclass_quote: str  # asset class of quote component | ||||
|     quote: str  # asset id of quote component | ||||
|     lot: str  # volume lot size | ||||
| 
 | ||||
|     cost_decimals: int | ||||
|     costmin: float | ||||
|     pair_decimals: int  # scaling decimal places for pair | ||||
|     lot_decimals: int  # scaling decimal places for volume | ||||
| 
 | ||||
|     # amount to multiply lot volume by to get currency volume | ||||
|     lot_multiplier: float | ||||
| 
 | ||||
|     # array of leverage amounts available when buying | ||||
|     leverage_buy: list[int] | ||||
|     # array of leverage amounts available when selling | ||||
|     leverage_sell: list[int] | ||||
| 
 | ||||
|     # fee schedule array in [volume, percent fee] tuples | ||||
|     fees: list[tuple[int, float]] | ||||
| 
 | ||||
|     # maker fee schedule array in [volume, percent fee] tuples (if on | ||||
|     # maker/taker) | ||||
|     fees_maker: list[tuple[int, float]] | ||||
| 
 | ||||
|     fee_volume_currency: str  # volume discount currency | ||||
|     margin_call: str  # margin call level | ||||
|     margin_stop: str  # stop-out/liquidation margin level | ||||
|     ordermin: float  # minimum order volume for pair | ||||
|     tick_size: float # min price step size | ||||
|     status: str | ||||
| 
 | ||||
|     short_position_limit: float | ||||
|     long_position_limit: float | ||||
| 
 | ||||
| 
 | ||||
| class OHLC(Struct): | ||||
|     ''' | ||||
|     Description of the flattened OHLC quote format. | ||||
|  | @ -336,17 +295,17 @@ async def stream_quotes( | |||
| 
 | ||||
|             # transform to upper since piker style is always lower | ||||
|             sym = sym.upper() | ||||
|             sym_info = await client.symbol_info(sym) | ||||
|             try: | ||||
|                 si = Pair(**sym_info)  # validation | ||||
|             except TypeError: | ||||
|                 fields_diff = set(sym_info) - set(Pair.__struct_fields__) | ||||
|                 raise TypeError( | ||||
|                     f'Missing msg fields {fields_diff}' | ||||
|                 ) | ||||
|             si: Pair = await client.symbol_info(sym) | ||||
|             # try: | ||||
|             #     si = Pair(**sym_info)  # validation | ||||
|             # except TypeError: | ||||
|             #     fields_diff = set(sym_info) - set(Pair.__struct_fields__) | ||||
|             #     raise TypeError( | ||||
|             #         f'Missing msg fields {fields_diff}' | ||||
|             #     ) | ||||
|             syminfo = si.to_dict() | ||||
|             syminfo['price_tick_size'] = 1 / 10**si.pair_decimals | ||||
|             syminfo['lot_tick_size'] = 1 / 10**si.lot_decimals | ||||
|             syminfo['price_tick_size'] = 1. / 10**si.pair_decimals | ||||
|             syminfo['lot_tick_size'] = 1. / 10**si.lot_decimals | ||||
|             syminfo['asset_type'] = 'crypto' | ||||
|             sym_infos[sym] = syminfo | ||||
|             ws_pairs[sym] = si.wsname | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ import tractor | |||
| 
 | ||||
| from .. import data | ||||
| from ..data.types import Struct | ||||
| from ..data._source import Symbol | ||||
| from ..pp import ( | ||||
|     Position, | ||||
|     Transaction, | ||||
|  | @ -81,6 +82,7 @@ class PaperBoi(Struct): | |||
|     _reqids: bidict | ||||
|     _positions: dict[str, Position] | ||||
|     _trade_ledger: dict[str, Any] | ||||
|     _syms: dict[str, Symbol] = {} | ||||
| 
 | ||||
|     # init edge case L1 spread | ||||
|     last_ask: tuple[float, float] = (float('inf'), 0)  # price, size | ||||
|  | @ -252,6 +254,7 @@ class PaperBoi(Struct): | |||
|         key = fqsn.rstrip(f'.{self.broker}') | ||||
|         t = Transaction( | ||||
|             fqsn=fqsn, | ||||
|             sym=self._syms[fqsn], | ||||
|             tid=oid, | ||||
|             size=size, | ||||
|             price=price, | ||||
|  | @ -261,27 +264,29 @@ class PaperBoi(Struct): | |||
|         ) | ||||
| 
 | ||||
|         with ( | ||||
|                 open_trade_ledger(self.broker, 'paper') as ledger, | ||||
|                 open_pps(self.broker, 'paper', True) as table | ||||
|              ): | ||||
|                 ledger.update({oid: t.to_dict()}) | ||||
|                 # Write to pps toml right now | ||||
|                 table.update_from_trans({oid: t}) | ||||
|             open_trade_ledger(self.broker, 'paper') as ledger, | ||||
|             open_pps(self.broker, 'paper', write_on_exit=True) as table | ||||
|         ): | ||||
|             tx = t.to_dict() | ||||
|             tx.pop('sym') | ||||
|             ledger.update({oid: tx}) | ||||
|             # Write to pps toml right now | ||||
|             table.update_from_trans({oid: t}) | ||||
| 
 | ||||
|                 pp = table.pps[key] | ||||
|                 pp_msg = BrokerdPosition( | ||||
|                     broker=self.broker, | ||||
|                     account='paper', | ||||
|                     symbol=fqsn, | ||||
|                     # TODO: we need to look up the asset currency from | ||||
|                     # broker info. i guess for crypto this can be | ||||
|                     # inferred from the pair? | ||||
|                     currency=key, | ||||
|                     size=pp.size, | ||||
|                     avg_price=pp.ppu, | ||||
|                 ) | ||||
|             pp = table.pps[key] | ||||
|             pp_msg = BrokerdPosition( | ||||
|                 broker=self.broker, | ||||
|                 account='paper', | ||||
|                 symbol=fqsn, | ||||
|                 # TODO: we need to look up the asset currency from | ||||
|                 # broker info. i guess for crypto this can be | ||||
|                 # inferred from the pair? | ||||
|                 currency=key, | ||||
|                 size=pp.size, | ||||
|                 avg_price=pp.ppu, | ||||
|             ) | ||||
| 
 | ||||
|                 await self.ems_trades_stream.send(pp_msg) | ||||
|             await self.ems_trades_stream.send(pp_msg) | ||||
| 
 | ||||
| 
 | ||||
| async def simulate_fills( | ||||
|  | @ -567,6 +572,10 @@ async def trades_dialogue( | |||
| 
 | ||||
|                 # TODO: load postions from ledger file | ||||
|                 _trade_ledger={}, | ||||
|                 _syms={ | ||||
|                     fqsn: flume.symbol | ||||
|                     for fqsn, flume in feed.flumes.items() | ||||
|                 } | ||||
|             ) | ||||
| 
 | ||||
|             n.start_soon( | ||||
|  |  | |||
|  | @ -237,6 +237,7 @@ def write( | |||
|     config: dict,  # toml config as dict | ||||
|     name: str = 'brokers', | ||||
|     path: str = None, | ||||
|     fail_empty: bool = True, | ||||
|     **toml_kwargs, | ||||
| 
 | ||||
| ) -> None: | ||||
|  | @ -252,7 +253,7 @@ def write( | |||
|         log.debug(f"Creating config dir {_config_dir}") | ||||
|         os.makedirs(dirname) | ||||
| 
 | ||||
|     if not config: | ||||
|     if not config and fail_empty: | ||||
|         raise ValueError( | ||||
|             "Watch out you're trying to write a blank config!") | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,8 +18,11 @@ | |||
| numpy data source coversion helpers. | ||||
| """ | ||||
| from __future__ import annotations | ||||
| from decimal import ( | ||||
|     Decimal, | ||||
|     ROUND_HALF_EVEN, | ||||
| ) | ||||
| from typing import Any | ||||
| import decimal | ||||
| 
 | ||||
| from bidict import bidict | ||||
| import numpy as np | ||||
|  | @ -77,10 +80,14 @@ def mk_fqsn( | |||
| def float_digits( | ||||
|     value: float, | ||||
| ) -> int: | ||||
|     ''' | ||||
|     Return the number of precision digits read from a float value. | ||||
| 
 | ||||
|     ''' | ||||
|     if value == 0: | ||||
|         return 0 | ||||
| 
 | ||||
|     return int(-decimal.Decimal(str(value)).as_tuple().exponent) | ||||
|     return int(-Decimal(str(value)).as_tuple().exponent) | ||||
| 
 | ||||
| 
 | ||||
| def ohlc_zeros(length: int) -> np.ndarray: | ||||
|  | @ -127,6 +134,56 @@ def unpack_fqsn(fqsn: str) -> tuple[str, str, str]: | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class MktPair(Struct, frozen=True): | ||||
| 
 | ||||
|     src: str  # source asset name being used to buy | ||||
|     src_type: str  # source asset's financial type/classification name | ||||
|     # ^ specifies a "class" of financial instrument | ||||
|     # egs. stock, futer, option, bond etc. | ||||
| 
 | ||||
|     dst: str  # destination asset name being bought | ||||
|     dst_type: str  # destination asset's financial type/classification name | ||||
| 
 | ||||
|     price_tick: float  # minimum price increment value increment | ||||
|     price_tick_digits: int  # required decimal digits for above | ||||
| 
 | ||||
|     size_tick: float  # minimum size (aka vlm) increment value increment | ||||
|     size_tick_digits: int  # required decimal digits for above | ||||
| 
 | ||||
|     venue: str | None = None  # market venue provider name | ||||
|     expiry: str | None = None  # for derivs, expiry datetime parseable str | ||||
| 
 | ||||
|     # for derivs, info describing contract, egs. | ||||
|     # strike price, call or put, swap type, exercise model, etc. | ||||
|     contract_info: str | None = None | ||||
| 
 | ||||
|     @classmethod | ||||
|     def from_msg( | ||||
|         self, | ||||
|         msg: dict[str, Any], | ||||
| 
 | ||||
|     ) -> MktPair: | ||||
|         ''' | ||||
|         Constructor for a received msg-dict normally received over IPC. | ||||
| 
 | ||||
|         ''' | ||||
|         ... | ||||
| 
 | ||||
|     # fqa, fqma, .. etc. see issue: | ||||
|     # https://github.com/pikers/piker/issues/467 | ||||
|     @property | ||||
|     def fqsn(self) -> str: | ||||
|         ''' | ||||
|         Return the fully qualified market (endpoint) name for the | ||||
|         pair of transacting assets. | ||||
| 
 | ||||
|         ''' | ||||
|         ... | ||||
| 
 | ||||
| 
 | ||||
| # TODO: rework the below `Symbol` (which was originally inspired and | ||||
| # derived from stuff in quantdom) into a simpler, ipc msg ready, market | ||||
| # endpoint meta-data container type as per the drafted interace above. | ||||
| class Symbol(Struct): | ||||
|     ''' | ||||
|     I guess this is some kinda container thing for dealing with | ||||
|  | @ -141,10 +198,6 @@ class Symbol(Struct): | |||
|     suffix: str = '' | ||||
|     broker_info: dict[str, dict[str, Any]] = {} | ||||
| 
 | ||||
|     # specifies a "class" of financial instrument | ||||
|     # ex. stock, futer, option, bond etc. | ||||
| 
 | ||||
|     # @validate_arguments | ||||
|     @classmethod | ||||
|     def from_broker_info( | ||||
|         cls, | ||||
|  | @ -156,14 +209,14 @@ class Symbol(Struct): | |||
|     ) -> Symbol: | ||||
| 
 | ||||
|         tick_size = info.get('price_tick_size', 0.01) | ||||
|         lot_tick_size = info.get('lot_tick_size', 0.0) | ||||
|         lot_size = info.get('lot_tick_size', 0.0) | ||||
| 
 | ||||
|         return Symbol( | ||||
|             key=symbol, | ||||
|             tick_size=tick_size, | ||||
|             lot_tick_size=lot_tick_size, | ||||
|             lot_tick_size=lot_size, | ||||
|             tick_size_digits=float_digits(tick_size), | ||||
|             lot_size_digits=float_digits(lot_tick_size), | ||||
|             lot_size_digits=float_digits(lot_size), | ||||
|             suffix=suffix, | ||||
|             broker_info={broker: info}, | ||||
|         ) | ||||
|  | @ -244,15 +297,21 @@ class Symbol(Struct): | |||
|         fqsn = '.'.join(map(str.lower, tokens)) | ||||
|         return fqsn | ||||
| 
 | ||||
|     def iterfqsns(self) -> list[str]: | ||||
|         keys = [] | ||||
|         for broker in self.broker_info.keys(): | ||||
|             fqsn = mk_fqsn(self.key, broker) | ||||
|             if self.suffix: | ||||
|                 fqsn += f'.{self.suffix}' | ||||
|             keys.append(fqsn) | ||||
|     def quantize_size( | ||||
|         self, | ||||
|         size: float, | ||||
| 
 | ||||
|         return keys | ||||
|     ) -> Decimal: | ||||
|         ''' | ||||
|         Truncate input ``size: float`` using ``Decimal`` | ||||
|         and ``.lot_size_digits``. | ||||
| 
 | ||||
|         ''' | ||||
|         digits = self.lot_size_digits | ||||
|         return Decimal(size).quantize( | ||||
|             Decimal(f'1.{"0".ljust(digits, "0")}'), | ||||
|             rounding=ROUND_HALF_EVEN | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| def _nan_to_closest_num(array: np.ndarray): | ||||
|  |  | |||
							
								
								
									
										70
									
								
								piker/pp.py
								
								
								
								
							
							
						
						
									
										70
									
								
								piker/pp.py
								
								
								
								
							|  | @ -44,7 +44,7 @@ import toml | |||
| from . import config | ||||
| from .brokers import get_brokermod | ||||
| from .clearing._messages import BrokerdPosition, Status | ||||
| from .data._source import Symbol | ||||
| from .data._source import Symbol, unpack_fqsn | ||||
| from .log import get_logger | ||||
| from .data.types import Struct | ||||
| 
 | ||||
|  | @ -82,17 +82,19 @@ def open_trade_ledger( | |||
|     with open(tradesfile, 'rb') as cf: | ||||
|         start = time.time() | ||||
|         ledger = tomli.load(cf) | ||||
|         print(f'Ledger load took {time.time() - start}s') | ||||
|         log.info(f'Ledger load took {time.time() - start}s') | ||||
|         cpy = ledger.copy() | ||||
| 
 | ||||
|     try: | ||||
|         yield cpy | ||||
|     finally: | ||||
|         if cpy != ledger: | ||||
| 
 | ||||
|             # TODO: show diff output? | ||||
|             # https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries | ||||
|             print(f'Updating ledger for {tradesfile}:\n') | ||||
|             ledger.update(cpy)  | ||||
|             log.info(f'Updating ledger for {tradesfile}:\n') | ||||
|             ledger.update(cpy) | ||||
| 
 | ||||
|             # we write on close the mutated ledger data | ||||
|             with open(tradesfile, 'w') as cf: | ||||
|                 toml.dump(ledger, cf) | ||||
|  | @ -102,17 +104,18 @@ class Transaction(Struct, frozen=True): | |||
|     # TODO: should this be ``.to`` (see below)? | ||||
|     fqsn: str | ||||
| 
 | ||||
|     sym: Symbol | ||||
|     tid: Union[str, int]  # unique transaction id | ||||
|     size: float | ||||
|     price: float | ||||
|     cost: float  # commisions or other additional costs | ||||
|     dt: datetime | ||||
|     expiry: Optional[datetime] = None | ||||
|     expiry: datetime | None = None | ||||
| 
 | ||||
|     # optional key normally derived from the broker | ||||
|     # backend which ensures the instrument-symbol this record | ||||
|     # is for is truly unique. | ||||
|     bsuid: Optional[Union[str, int]] = None | ||||
|     bsuid: Union[str, int] | None = None | ||||
| 
 | ||||
|     # optional fqsn for the source "asset"/money symbol? | ||||
|     # from: Optional[str] = None | ||||
|  | @ -192,6 +195,13 @@ class Position(Struct): | |||
|         s = d.pop('symbol') | ||||
|         fqsn = s.front_fqsn() | ||||
| 
 | ||||
|         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['price_tick_size'] | ||||
|         d['lot_tick_size'] = sym_info['lot_tick_size'] | ||||
| 
 | ||||
|         if self.expiry is None: | ||||
|             d.pop('expiry', None) | ||||
|         elif expiry: | ||||
|  | @ -466,7 +476,7 @@ class Position(Struct): | |||
|         if self.split_ratio is not None: | ||||
|             size = round(size * self.split_ratio) | ||||
| 
 | ||||
|         return size | ||||
|         return float(self.symbol.quantize_size(size)) | ||||
| 
 | ||||
|     def minimize_clears( | ||||
|         self, | ||||
|  | @ -510,7 +520,7 @@ class Position(Struct): | |||
|             'cost': t.cost, | ||||
|             'price': t.price, | ||||
|             'size': t.size, | ||||
|             'dt': t.dt, | ||||
|             'dt': t.dt | ||||
|         } | ||||
| 
 | ||||
|         # TODO: compute these incrementally instead | ||||
|  | @ -557,7 +567,7 @@ class PpTable(Struct): | |||
|                     Symbol.from_fqsn( | ||||
|                         t.fqsn, | ||||
|                         info={}, | ||||
|                     ), | ||||
|                     ) if not t.sym else t.sym, | ||||
|                     size=0.0, | ||||
|                     ppu=0.0, | ||||
|                     bsuid=t.bsuid, | ||||
|  | @ -680,11 +690,20 @@ class PpTable(Struct): | |||
|         ''' | ||||
|         # TODO: show diff output? | ||||
|         # https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries | ||||
|         print(f'Updating ``pps.toml`` for {path}:\n') | ||||
| 
 | ||||
|         # active, closed_pp_objs = table.dump_active() | ||||
|         pp_entries = self.to_toml() | ||||
|         self.conf[self.brokername][self.acctid] = pp_entries | ||||
|         if pp_entries: | ||||
|             log.info(f'Updating ``pps.toml`` for {path}:\n') | ||||
|             log.info(f'Current positions:\n{pp_entries}') | ||||
|             self.conf[self.brokername][self.acctid] = pp_entries | ||||
| 
 | ||||
|         elif ( | ||||
|             self.brokername in self.conf and | ||||
|             self.acctid in self.conf[self.brokername] | ||||
|         ): | ||||
|             del self.conf[self.brokername][self.acctid] | ||||
|             if len(self.conf[self.brokername]) == 0: | ||||
|                 del self.conf[self.brokername] | ||||
| 
 | ||||
|         # TODO: why tf haven't they already done this for inline | ||||
|         # tables smh.. | ||||
|  | @ -698,6 +717,7 @@ class PpTable(Struct): | |||
|             self.conf, | ||||
|             'pps', | ||||
|             encoder=enc, | ||||
|             fail_empty=False | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -881,7 +901,6 @@ def open_pps( | |||
|     brokername: str, | ||||
|     acctid: str, | ||||
|     write_on_exit: bool = False, | ||||
| 
 | ||||
| ) -> Generator[PpTable, None, None]: | ||||
|     ''' | ||||
|     Read out broker-specific position entries from | ||||
|  | @ -914,6 +933,21 @@ def open_pps( | |||
|     # and update `PpTable` obj entries. | ||||
|     for fqsn, entry in pps.items(): | ||||
|         bsuid = entry['bsuid'] | ||||
|         symbol = Symbol.from_fqsn( | ||||
|             fqsn, | ||||
| 
 | ||||
|             # 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', '<unknown>'), | ||||
|                 'price_tick_size': entry.get('price_tick_size', 0.01), | ||||
|                 'lot_tick_size': entry.get('lot_tick_size', 0.0), | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         # convert clears sub-tables (only in this form | ||||
|         # for toml re-presentation) back into a master table. | ||||
|  | @ -935,8 +969,10 @@ def open_pps( | |||
|             dtstr = clears_table['dt'] | ||||
|             dt = pendulum.parse(dtstr) | ||||
|             clears_table['dt'] = dt | ||||
| 
 | ||||
|             trans.append(Transaction( | ||||
|                 fqsn=bsuid, | ||||
|                 sym=symbol, | ||||
|                 bsuid=bsuid, | ||||
|                 tid=tid, | ||||
|                 size=clears_table['size'], | ||||
|  | @ -949,7 +985,11 @@ def open_pps( | |||
|         size = entry['size'] | ||||
| 
 | ||||
|         # TODO: remove but, handle old field name for now | ||||
|         ppu = entry.get('ppu', entry.get('be_price', 0)) | ||||
|         ppu = entry.get( | ||||
|             'ppu', | ||||
|             entry.get('be_price', 0), | ||||
|         ) | ||||
| 
 | ||||
|         split_ratio = entry.get('split_ratio') | ||||
| 
 | ||||
|         expiry = entry.get('expiry') | ||||
|  | @ -957,7 +997,7 @@ def open_pps( | |||
|             expiry = pendulum.parse(expiry) | ||||
| 
 | ||||
|         pp = pp_objs[bsuid] = Position( | ||||
|             Symbol.from_fqsn(fqsn, info={}), | ||||
|             symbol, | ||||
|             size=size, | ||||
|             ppu=ppu, | ||||
|             split_ratio=split_ratio, | ||||
|  |  | |||
|  | @ -84,25 +84,20 @@ async def _async_main( | |||
|                         case {'name': 'position'}: | ||||
|                             break | ||||
| 
 | ||||
|         if ( | ||||
|             assert_entries  | ||||
|             or assert_pps  | ||||
|             or assert_zeroed_pps  | ||||
|             or assert_msg | ||||
|         ): | ||||
|             _assert( | ||||
|                 assert_entries, | ||||
|                 assert_pps, | ||||
|                 assert_zeroed_pps, | ||||
|                 pps, | ||||
|                 last_msg, | ||||
|                 size, | ||||
|                 executions, | ||||
|             ) | ||||
| 
 | ||||
|         # Teardown piker like a user would | ||||
|         raise KeyboardInterrupt | ||||
| 
 | ||||
|     if assert_entries or assert_pps or assert_zeroed_pps or assert_msg: | ||||
|         _assert( | ||||
|             assert_entries, | ||||
|             assert_pps, | ||||
|             assert_zeroed_pps, | ||||
|             pps, | ||||
|             last_msg, | ||||
|             size, | ||||
|             executions, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| def _assert( | ||||
|     assert_entries, | ||||
|  | @ -206,8 +201,6 @@ def test_sell( | |||
|         ), | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.xfail(reason='Due to precision issues, this test will currently fail') | ||||
| def test_multi_sell( | ||||
|     open_test_pikerd_and_ems: AsyncContextManager,  | ||||
|     delete_testing_dir | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue