diff --git a/piker/accounting/_ledger.py b/piker/accounting/_ledger.py index 74bee9ad..df7bb4aa 100644 --- a/piker/accounting/_ledger.py +++ b/piker/accounting/_ledger.py @@ -33,9 +33,9 @@ import tomli import toml from .. import config -from ..data._source import Symbol from ..data.types import Struct from ..log import get_logger +from ._mktinfo import Symbol log = get_logger(__name__) diff --git a/piker/accounting/_mktinfo.py b/piker/accounting/_mktinfo.py new file mode 100644 index 00000000..a9036170 --- /dev/null +++ b/piker/accounting/_mktinfo.py @@ -0,0 +1,302 @@ +# piker: trading gear for hackers +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License + +# along with this program. If not, see . + +''' +Market (pair) meta-info layer: sane addressing semantics and meta-data +for cross-provider marketplaces. + +We intoduce the concept of, + +- a FQMA: fully qualified market address, +- a sane schema for FQMAs including derivatives, +- a msg-serializeable description of markets for + easy sharing with other pikers B) + +''' +from __future__ import annotations +from decimal import ( + Decimal, + ROUND_HALF_EVEN, +) +from typing import ( + Any, +) + +from ..data.types import Struct + + +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 + @property + def size_tick_digits(self) -> int: + return self.size_tick + + 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. + + ''' + ... + + +def mk_fqsn( + provider: str, + symbol: str, + +) -> str: + ''' + Generate a "fully qualified symbol name" which is + a reverse-hierarchical cross broker/provider symbol + + ''' + return '.'.join([symbol, provider]).lower() + + +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(str(value)).as_tuple().exponent) + + +def digits_to_dec( + ndigits: int, +) -> Decimal: + ''' + Return the minimum float value for an input integer value. + + eg. 3 -> 0.001 + + ''' + if ndigits == 0: + return Decimal('0') + + return Decimal('0.' + '0'*(ndigits-1) + '1') + + +def unpack_fqsn(fqsn: str) -> tuple[str, str, str]: + ''' + Unpack a fully-qualified-symbol-name to ``tuple``. + + ''' + venue = '' + suffix = '' + + # TODO: probably reverse the order of all this XD + tokens = fqsn.split('.') + if len(tokens) < 3: + # probably crypto + symbol, broker = tokens + return ( + broker, + symbol, + '', + ) + + elif len(tokens) > 3: + symbol, venue, suffix, broker = tokens + else: + symbol, venue, broker = tokens + suffix = '' + + # head, _, broker = fqsn.rpartition('.') + # symbol, _, suffix = head.rpartition('.') + return ( + broker, + '.'.join([symbol, venue]), + suffix, + ) + +# 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 + all the different meta-data formats from brokers? + + ''' + key: str + tick_size: float = 0.01 + lot_tick_size: float = 0.0 # "volume" precision as min step value + tick_size_digits: int = 2 + lot_size_digits: int = 0 + suffix: str = '' + broker_info: dict[str, dict[str, Any]] = {} + + @classmethod + def from_broker_info( + cls, + broker: str, + symbol: str, + info: dict[str, Any], + suffix: str = '', + + ) -> Symbol: + + tick_size = info.get('price_tick_size', 0.01) + lot_size = info.get('lot_tick_size', 0.0) + + return Symbol( + key=symbol, + + tick_size=tick_size, + lot_tick_size=lot_size, + + tick_size_digits=float_digits(tick_size), + lot_size_digits=float_digits(lot_size), + + suffix=suffix, + broker_info={broker: info}, + ) + + @classmethod + def from_fqsn( + cls, + fqsn: str, + info: dict[str, Any], + + ) -> Symbol: + broker, key, suffix = unpack_fqsn(fqsn) + return cls.from_broker_info( + broker, + key, + info=info, + suffix=suffix, + ) + + @property + def type_key(self) -> str: + return list(self.broker_info.values())[0]['asset_type'] + + @property + def brokers(self) -> list[str]: + return list(self.broker_info.keys()) + + def nearest_tick(self, value: float) -> float: + ''' + Return the nearest tick value based on mininum increment. + + ''' + mult = 1 / self.tick_size + return round(value * mult) / mult + + def front_feed(self) -> tuple[str, str]: + ''' + Return the "current" feed key for this symbol. + + (i.e. the broker + symbol key in a tuple). + + ''' + return ( + list(self.broker_info.keys())[0], + self.key, + ) + + def tokens(self) -> tuple[str]: + broker, key = self.front_feed() + if self.suffix: + return (key, self.suffix, broker) + else: + return (key, broker) + + @property + def fqsn(self) -> str: + return '.'.join(self.tokens()).lower() + + def front_fqsn(self) -> str: + ''' + fqsn = "fully qualified symbol name" + + Basically the idea here is for all client-ish code (aka programs/actors + that ask the provider agnostic layers in the stack for data) should be + able to tell which backend / venue / derivative each data feed/flow is + from by an explicit string key of the current form: + + ... + + TODO: I have thoughts that we should actually change this to be + more like an "attr lookup" (like how the web should have done + urls, but marketting peeps ruined it etc. etc.): + + ... + + ''' + tokens = self.tokens() + fqsn = '.'.join(map(str.lower, tokens)) + return fqsn + + def quantize_size( + self, + size: float, + + ) -> 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 + ) + + diff --git a/piker/accounting/_pos.py b/piker/accounting/_pos.py index 2a9ca0d8..204e7a8e 100644 --- a/piker/accounting/_pos.py +++ b/piker/accounting/_pos.py @@ -43,10 +43,13 @@ from ._ledger import ( iter_by_dt, open_trade_ledger, ) +from ._mktinfo import ( + Symbol, + unpack_fqsn, +) from .. import config from ..brokers import get_brokermod from ..clearing._messages import BrokerdPosition, Status -from ..data._source import Symbol, unpack_fqsn from ..data.types import Struct from ..log import get_logger @@ -154,6 +157,7 @@ class Position(Struct): inline_table['tid'] = tid toml_clears_list.append(inline_table) + d['clears'] = toml_clears_list return fqsn, d diff --git a/piker/brokers/ib/api.py b/piker/brokers/ib/api.py index bfa66a9d..c6513204 100644 --- a/piker/brokers/ib/api.py +++ b/piker/brokers/ib/api.py @@ -644,7 +644,7 @@ class Client: # fqsn parsing stage # ------------------ if '.ib' in pattern: - from ..data._source import unpack_fqsn + from ..accounting._mktinfo import unpack_fqsn _, symbol, expiry = unpack_fqsn(pattern) else: diff --git a/piker/brokers/ib/broker.py b/piker/brokers/ib/broker.py index bc65d6a2..77f0bb53 100644 --- a/piker/brokers/ib/broker.py +++ b/piker/brokers/ib/broker.py @@ -70,7 +70,7 @@ from piker.clearing._messages import ( BrokerdFill, BrokerdError, ) -from piker.data._source import ( +from piker.accounting._mktinfo import ( Symbol, float_digits, ) diff --git a/piker/brokers/kraken/api.py b/piker/brokers/kraken/api.py index 74ad734b..82479329 100644 --- a/piker/brokers/kraken/api.py +++ b/piker/brokers/kraken/api.py @@ -42,7 +42,7 @@ import trio from piker import config from piker.data.types import Struct -from piker.data._source import Symbol +from piker.accounting._mktinfo import Symbol from piker.brokers._util import ( resproc, SymbolNotFound, diff --git a/piker/brokers/kraken/broker.py b/piker/brokers/kraken/broker.py index 5d1bbb01..72d6f0fe 100644 --- a/piker/brokers/kraken/broker.py +++ b/piker/brokers/kraken/broker.py @@ -48,7 +48,7 @@ from piker.accounting import ( open_pps, get_likely_pair, ) -from piker.data._source import ( +from piker.accounting._mktinfo import ( Symbol, digits_to_dec, ) diff --git a/piker/clearing/_allocate.py b/piker/clearing/_allocate.py index c457de05..023d1e92 100644 --- a/piker/clearing/_allocate.py +++ b/piker/clearing/_allocate.py @@ -23,7 +23,7 @@ from typing import Optional from bidict import bidict -from ..data._source import Symbol +from ..accounting._mktinfo import Symbol from ..data.types import Struct from ..accounting import Position diff --git a/piker/clearing/_client.py b/piker/clearing/_client.py index 7d03406a..ee176f87 100644 --- a/piker/clearing/_client.py +++ b/piker/clearing/_client.py @@ -27,6 +27,7 @@ import trio import tractor from tractor.trionics import broadcast_receiver +from ..accounting._mktinfo import unpack_fqsn from ..log import get_logger from ..data.types import Struct from ..service import maybe_open_emsd @@ -228,7 +229,6 @@ async def open_ems( # ready for order commands book = get_orders() - from ..data._source import unpack_fqsn broker, symbol, suffix = unpack_fqsn(fqsn) async with maybe_open_emsd(broker) as portal: diff --git a/piker/clearing/_ems.py b/piker/clearing/_ems.py index 477da310..b2c4c614 100644 --- a/piker/clearing/_ems.py +++ b/piker/clearing/_ems.py @@ -43,7 +43,7 @@ import tractor from ..log import get_logger from ..data._normalize import iterticks -from ..data._source import ( +from ..accounting._mktinfo import ( unpack_fqsn, mk_fqsn, float_digits, @@ -521,7 +521,6 @@ class Router(Struct): none already exists. ''' - from ..data._source import unpack_fqsn broker, symbol, suffix = unpack_fqsn(fqsn) async with ( diff --git a/piker/clearing/_messages.py b/piker/clearing/_messages.py index c7693b9f..f084af05 100644 --- a/piker/clearing/_messages.py +++ b/piker/clearing/_messages.py @@ -29,7 +29,7 @@ from typing import ( from msgspec import field -from ..data._source import Symbol +from ..accounting._mktinfo import Symbol from ..data.types import Struct diff --git a/piker/clearing/_paper_engine.py b/piker/clearing/_paper_engine.py index 39d5a474..00611e6d 100644 --- a/piker/clearing/_paper_engine.py +++ b/piker/clearing/_paper_engine.py @@ -38,7 +38,7 @@ import tractor from .. import data from ..data.types import Struct -from ..data._source import Symbol +from ..accounting._mktinfo import Symbol from ..accounting import ( Position, Transaction, diff --git a/piker/data/_source.py b/piker/data/_source.py index e503105e..61c2e52f 100644 --- a/piker/data/_source.py +++ b/piker/data/_source.py @@ -28,8 +28,12 @@ from bidict import bidict import numpy as np from .types import Struct -# from numba import from_dtype - +from ..accounting._mktinfo import ( + # mkfqsn, + unpack_fqsn, + # digits_to_dec, + float_digits, +) ohlc_fields = [ ('time', float), @@ -50,6 +54,7 @@ base_ohlc_dtype = np.dtype(ohlc_fields) # TODO: for now need to construct this manually for readonly arrays, see # https://github.com/numba/numba/issues/4511 +# from numba import from_dtype # numba_ohlc_dtype = from_dtype(base_ohlc_dtype) # map time frame "keys" to seconds values @@ -64,47 +69,6 @@ tf_in_1s = bidict({ }) -def mk_fqsn( - provider: str, - symbol: str, - -) -> str: - ''' - Generate a "fully qualified symbol name" which is - a reverse-hierarchical cross broker/provider symbol - - ''' - return '.'.join([symbol, provider]).lower() - - -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(str(value)).as_tuple().exponent) - - -def digits_to_dec( - ndigits: int, -) -> Decimal: - ''' - Return the minimum float value for an input integer value. - - eg. 3 -> 0.001 - - ''' - if ndigits == 0: - return Decimal('0') - - return Decimal('0.' + '0'*(ndigits-1) + '1') - - def ohlc_zeros(length: int) -> np.ndarray: """Construct an OHLC field formatted structarray. @@ -115,223 +79,6 @@ def ohlc_zeros(length: int) -> np.ndarray: return np.zeros(length, dtype=base_ohlc_dtype) -def unpack_fqsn(fqsn: str) -> tuple[str, str, str]: - ''' - Unpack a fully-qualified-symbol-name to ``tuple``. - - ''' - venue = '' - suffix = '' - - # TODO: probably reverse the order of all this XD - tokens = fqsn.split('.') - if len(tokens) < 3: - # probably crypto - symbol, broker = tokens - return ( - broker, - symbol, - '', - ) - - elif len(tokens) > 3: - symbol, venue, suffix, broker = tokens - else: - symbol, venue, broker = tokens - suffix = '' - - # head, _, broker = fqsn.rpartition('.') - # symbol, _, suffix = head.rpartition('.') - return ( - broker, - '.'.join([symbol, venue]), - suffix, - ) - - -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 - all the different meta-data formats from brokers? - - ''' - key: str - tick_size: float = 0.01 - lot_tick_size: float = 0.0 # "volume" precision as min step value - tick_size_digits: int = 2 - lot_size_digits: int = 0 - suffix: str = '' - broker_info: dict[str, dict[str, Any]] = {} - - @classmethod - def from_broker_info( - cls, - broker: str, - symbol: str, - info: dict[str, Any], - suffix: str = '', - - ) -> Symbol: - - tick_size = info.get('price_tick_size', 0.01) - lot_size = info.get('lot_tick_size', 0.0) - - return Symbol( - key=symbol, - - tick_size=tick_size, - lot_tick_size=lot_size, - - tick_size_digits=float_digits(tick_size), - lot_size_digits=float_digits(lot_size), - - suffix=suffix, - broker_info={broker: info}, - ) - - @classmethod - def from_fqsn( - cls, - fqsn: str, - info: dict[str, Any], - - ) -> Symbol: - broker, key, suffix = unpack_fqsn(fqsn) - return cls.from_broker_info( - broker, - key, - info=info, - suffix=suffix, - ) - - @property - def type_key(self) -> str: - return list(self.broker_info.values())[0]['asset_type'] - - @property - def brokers(self) -> list[str]: - return list(self.broker_info.keys()) - - def nearest_tick(self, value: float) -> float: - ''' - Return the nearest tick value based on mininum increment. - - ''' - mult = 1 / self.tick_size - return round(value * mult) / mult - - def front_feed(self) -> tuple[str, str]: - ''' - Return the "current" feed key for this symbol. - - (i.e. the broker + symbol key in a tuple). - - ''' - return ( - list(self.broker_info.keys())[0], - self.key, - ) - - def tokens(self) -> tuple[str]: - broker, key = self.front_feed() - if self.suffix: - return (key, self.suffix, broker) - else: - return (key, broker) - - @property - def fqsn(self) -> str: - return '.'.join(self.tokens()).lower() - - def front_fqsn(self) -> str: - ''' - fqsn = "fully qualified symbol name" - - Basically the idea here is for all client-ish code (aka programs/actors - that ask the provider agnostic layers in the stack for data) should be - able to tell which backend / venue / derivative each data feed/flow is - from by an explicit string key of the current form: - - ... - - TODO: I have thoughts that we should actually change this to be - more like an "attr lookup" (like how the web should have done - urls, but marketting peeps ruined it etc. etc.): - - ... - - ''' - tokens = self.tokens() - fqsn = '.'.join(map(str.lower, tokens)) - return fqsn - - def quantize_size( - self, - size: float, - - ) -> 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): """Return interpolated values instead of NaN. diff --git a/piker/data/feed.py b/piker/data/feed.py index 7efd5eb3..5e1a1aec 100644 --- a/piker/data/feed.py +++ b/piker/data/feed.py @@ -70,11 +70,11 @@ from ._sharedmem import ( ) from .ingest import get_ingestormod from .types import Struct -from ._source import ( - base_iohlc_dtype, +from ..accounting._mktinfo import ( Symbol, unpack_fqsn, ) +from ._source import base_iohlc_dtype from ..ui import _search from ._sampling import ( open_sample_stream, diff --git a/piker/data/flows.py b/piker/data/flows.py index 9d8b3103..19615f61 100644 --- a/piker/data/flows.py +++ b/piker/data/flows.py @@ -30,10 +30,10 @@ import tractor import pendulum import numpy as np -from .types import Struct -from ._source import ( +from ..accounting._mktinfo import ( Symbol, ) +from .types import Struct from ._sharedmem import ( attach_shm_array, ShmArray, diff --git a/piker/fsp/_engine.py b/piker/fsp/_engine.py index 37852cfc..a77e662f 100644 --- a/piker/fsp/_engine.py +++ b/piker/fsp/_engine.py @@ -45,7 +45,7 @@ from ..data._sampling import ( _default_delay_s, open_sample_stream, ) -from ..data._source import Symbol +from ..accounting._mktinfo import Symbol from ._api import ( Fsp, _load_builtins, diff --git a/piker/ui/_app.py b/piker/ui/_app.py index 9978dbe3..0e7dad47 100644 --- a/piker/ui/_app.py +++ b/piker/ui/_app.py @@ -28,7 +28,7 @@ from ..service import maybe_spawn_brokerd from . import _event from ._exec import run_qtractor from ..data.feed import install_brokerd_search -from ..data._source import unpack_fqsn +from ..accounting._mktinfo import unpack_fqsn from . import _search from ._chart import GodWidget from ..log import get_logger diff --git a/piker/ui/_axes.py b/piker/ui/_axes.py index 62214f60..040d0552 100644 --- a/piker/ui/_axes.py +++ b/piker/ui/_axes.py @@ -29,7 +29,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QPointF from . import _pg_overrides as pgo -from ..data._source import float_digits +from ..accounting._mktinfo import float_digits from ._label import Label from ._style import DpiAwareFont, hcolor, _font from ._interaction import ChartView diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 7811278b..b05d6fcf 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -68,7 +68,7 @@ from ..data.feed import ( Feed, Flume, ) -from ..data._source import Symbol +from ..accounting._mktinfo import Symbol from ..log import get_logger from ._interaction import ChartView from ._forms import FieldsForm diff --git a/piker/ui/_fsp.py b/piker/ui/_fsp.py index 6e600743..960b287a 100644 --- a/piker/ui/_fsp.py +++ b/piker/ui/_fsp.py @@ -46,7 +46,7 @@ from ..data._sharedmem import ( try_read, ) from ..data.feed import Flume -from ..data._source import Symbol +from ..accounting._mktinfo import Symbol from ._chart import ( ChartPlotWidget, LinkedSplits, diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index bf60c0e6..6ac0f1f4 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -42,7 +42,7 @@ from ..clearing._allocate import ( mk_allocator, ) from ._style import _font -from ..data._source import Symbol +from ..accounting._mktinfo import Symbol from ..data.feed import ( Feed, Flume, diff --git a/tests/test_feeds.py b/tests/test_feeds.py index a79ca861..0435ed61 100644 --- a/tests/test_feeds.py +++ b/tests/test_feeds.py @@ -13,7 +13,7 @@ from piker.data import ( ShmArray, open_feed, ) -from piker.data._source import ( +from piker.accounting._mktinfo import ( unpack_fqsn, )