Merge pull request #470 from pikers/decimalization_take_2

Fixed float dust bug on zero position
xdo_and_you
goodboy 2023-03-03 17:34:36 -05:00 committed by GitHub
commit 201f86e482
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 359 additions and 163 deletions

View File

@ -70,7 +70,10 @@ from piker.clearing._messages import (
BrokerdFill, BrokerdFill,
BrokerdError, BrokerdError,
) )
from piker.data._source import Symbol from piker.data._source import (
Symbol,
float_digits,
)
from .api import ( from .api import (
_accounts2clients, _accounts2clients,
con2fqsn, con2fqsn,
@ -304,6 +307,9 @@ async def update_ledger_from_api_trades(
entry['listingExchange'] = pexch entry['listingExchange'] = pexch
# pack in the ``Contract.secType``
entry['asset_type'] = condict['secType']
conf = get_config() conf = get_config()
entries = api_trades_to_ledger_entries( entries = api_trades_to_ledger_entries(
conf['accounts'].inverse, conf['accounts'].inverse,
@ -616,9 +622,10 @@ async def trades_dialogue(
# from the api trades it seems we get a key # from the api trades it seems we get a key
# error from ``update[bsuid]`` ? # error from ``update[bsuid]`` ?
pp = table.pps[bsuid] pp = table.pps[bsuid]
pairinfo = pp.symbol
if msg.size != pp.size: if msg.size != pp.size:
log.error( log.error(
f'Position mismatch {pp.symbol.front_fqsn()}:\n' f'Pos size mismatch {pairinfo.front_fqsn()}:\n'
f'ib: {msg.size}\n' f'ib: {msg.size}\n'
f'piker: {pp.size}\n' f'piker: {pp.size}\n'
) )
@ -1095,13 +1102,15 @@ def norm_trade_records(
''' '''
records: list[Transaction] = [] records: list[Transaction] = []
for tid, record in ledger.items():
for tid, record in ledger.items():
conid = record.get('conId') or record['conid'] conid = record.get('conId') or record['conid']
comms = record.get('commission') comms = record.get('commission')
if comms is None: if comms is None:
comms = -1*record['ibCommission'] comms = -1*record['ibCommission']
price = record.get('price') or record['tradePrice'] 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 # the api doesn't do the -/+ on the quantity for you but flex
# records do.. are you fucking serious ib...!? # records do.. are you fucking serious ib...!?
@ -1144,9 +1153,14 @@ def norm_trade_records(
# special handling of symbol extraction from # special handling of symbol extraction from
# flex records using some ad-hoc schema parsing. # flex records using some ad-hoc schema parsing.
instr = record.get('assetCategory') asset_type: str = record.get('assetCategory') or record['secType']
if instr == 'FUT':
symbol = record['description'][:3] # 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. # try to build out piker fqsn from record.
expiry = record.get( expiry = record.get(
@ -1156,10 +1170,34 @@ def norm_trade_records(
suffix = f'{exch}.{expiry}' suffix = f'{exch}.{expiry}'
expiry = pendulum.parse(expiry) expiry = pendulum.parse(expiry)
fqsn = Symbol.from_fqsn( src: str = record['currency']
pair = Symbol.from_fqsn(
fqsn=f'{symbol}.{suffix}.ib', fqsn=f'{symbol}.{suffix}.ib',
info={}, info={
).front_fqsn().rstrip('.ib') '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 # NOTE: for flex records the normal fields for defining an fqsn
# sometimes won't be available so we rely on two approaches for # sometimes won't be available so we rely on two approaches for
@ -1175,6 +1213,7 @@ def norm_trade_records(
records, records,
Transaction( Transaction(
fqsn=fqsn, fqsn=fqsn,
sym=pair,
tid=tid, tid=tid,
size=size, size=size,
price=price, price=price,
@ -1201,7 +1240,11 @@ def parse_flex_dt(
def api_trades_to_ledger_entries( def api_trades_to_ledger_entries(
accounts: bidict, 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: ) -> dict:
''' '''

View File

@ -770,7 +770,7 @@ async def stream_quotes(
syminfo['price_tick_size'] = max(syminfo['minTick'], min_tick) 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 # a float
syminfo['lot_tick_size'] = 0.0 syminfo['lot_tick_size'] = 0.0

View File

@ -40,6 +40,8 @@ import base64
import trio import trio
from piker import config from piker import config
from piker.data.types import Struct
from piker.data._source import Symbol
from piker.brokers._util import ( from piker.brokers._util import (
resproc, resproc,
SymbolNotFound, 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: class Client:
# global symbol normalization table # global symbol normalization table
_ntable: dict[str, str] = {} _ntable: dict[str, str] = {}
_atable: bidict[str, str] = bidict() _atable: bidict[str, str] = bidict()
_pairs: dict[str, Pair] = {}
def __init__( def __init__(
self, self,
@ -133,13 +177,12 @@ class Client:
'krakenex/2.1.0 (+https://github.com/veox/python3-krakenex)' 'krakenex/2.1.0 (+https://github.com/veox/python3-krakenex)'
}) })
self.conf: dict[str, str] = config self.conf: dict[str, str] = config
self._pairs: list[str] = []
self._name = name self._name = name
self._api_key = api_key self._api_key = api_key
self._secret = secret self._secret = secret
@property @property
def pairs(self) -> dict[str, Any]: def pairs(self) -> dict[str, Pair]:
if self._pairs is None: if self._pairs is None:
raise RuntimeError( raise RuntimeError(
"Make sure to run `cache_symbols()` on startup!" "Make sure to run `cache_symbols()` on startup!"
@ -295,15 +338,28 @@ class Client:
trans: dict[str, Transaction] = {} trans: dict[str, Transaction] = {}
for entry in xfers: 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 # XXX: this is in the asset units (likely) so it isn't
# quite the same as a commisions cost necessarily..) # quite the same as a commisions cost necessarily..)
cost = float(entry['fee']) cost = float(entry['fee'])
fqsn = asset + '.kraken'
pairinfo = Symbol.from_fqsn(
fqsn,
info={
'asset_type': 'crypto',
'lot_tick_size': asset_info['decimals'],
},
)
tran = Transaction( tran = Transaction(
fqsn=asset + '.kraken', fqsn=fqsn,
sym=pairinfo,
tid=entry['txid'], tid=entry['txid'],
dt=pendulum.from_timestamp(entry['time']), dt=pendulum.from_timestamp(entry['time']),
bsuid=f'{asset}{src_asset}', bsuid=f'{asset}{src_asset}',
@ -317,7 +373,7 @@ class Client:
price='NaN', price='NaN',
# XXX: see note above # XXX: see note above
cost=0, cost=cost,
) )
trans[tran.tid] = tran trans[tran.tid] = tran
@ -372,7 +428,7 @@ class Client:
self, self,
pair: Optional[str] = None, pair: Optional[str] = None,
) -> dict[str, dict[str, str]]: ) -> dict[str, Pair] | Pair:
if pair is not None: if pair is not None:
pairs = {'pair': pair} pairs = {'pair': pair}
@ -389,19 +445,36 @@ class Client:
if pair is not None: if pair is not None:
_, data = next(iter(pairs.items())) _, data = next(iter(pairs.items()))
return data return Pair(**data)
else: else:
return pairs return {key: Pair(**data) for key, data in pairs.items()}
async def cache_symbols( async def cache_symbols(self) -> dict:
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: if not self._pairs:
self._pairs = await self.symbol_info() self._pairs.update(await self.symbol_info())
ntable = {} # table of all ws and rest keys to their alt-name values.
for restapikey, info in self._pairs.items(): ntable: dict[str, str] = {}
ntable[restapikey] = ntable[info['wsname']] = info['altname']
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) self._ntable.update(ntable)
@ -411,26 +484,34 @@ class Client:
self, self,
pattern: str, pattern: str,
limit: int = None, limit: int = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
if self._pairs is not None: '''
data = self._pairs Search for a symbol by "alt name"..
else:
data = await self.symbol_info() 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( matches = fuzzy.extractBests(
pattern, pattern,
data, self._pairs,
score_cutoff=50, score_cutoff=50,
) )
# repack in dict form # 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( async def bars(
self, self,
symbol: str = 'XBTUSD', symbol: str = 'XBTUSD',
# UTC 2017-07-02 12:53:20 # 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 count: int = 720, # <- max allowed per query
as_np: bool = True, as_np: bool = True,
@ -506,7 +587,7 @@ class Client:
def normalize_symbol( def normalize_symbol(
cls, cls,
ticker: str ticker: str
) -> str: ) -> tuple[str, Pair]:
''' '''
Normalize symbol names to to a 3x3 pair from the global Normalize symbol names to to a 3x3 pair from the global
definition map which we build out from the data retreived from definition map which we build out from the data retreived from
@ -514,7 +595,7 @@ class Client:
''' '''
ticker = cls._ntable[ticker] ticker = cls._ntable[ticker]
return ticker.lower() return ticker.lower(), cls._pairs[ticker]
@acm @acm

View File

@ -48,6 +48,7 @@ from piker.pp import (
open_trade_ledger, open_trade_ledger,
open_pps, open_pps,
) )
from piker.data._source import Symbol
from piker.clearing._messages import ( from piker.clearing._messages import (
Order, Order,
Status, Status,
@ -469,13 +470,12 @@ async def trades_dialogue(
with ( with (
open_pps( open_pps(
'kraken', 'kraken',
acctid, acctid
write_on_exit=True,
) as table, ) as table,
open_trade_ledger( open_trade_ledger(
'kraken', 'kraken',
acctid, acctid
) as ledger_dict, ) as ledger_dict,
): ):
# transaction-ify the ledger entries # transaction-ify the ledger entries
@ -1197,10 +1197,21 @@ def norm_trade_records(
}[record['type']] }[record['type']]
# we normalize to kraken's `altname` always.. # 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( records[tid] = Transaction(
fqsn=f'{norm_sym}.kraken', fqsn=fqsn,
sym=mktpair,
tid=tid, tid=tid,
size=size, size=size,
price=float(record['price']), price=float(record['price']),

View File

@ -42,56 +42,15 @@ from piker.brokers._util import (
DataUnavailable, DataUnavailable,
) )
from piker.log import get_console_log from piker.log import get_console_log
from piker.data import ShmArray
from piker.data.types import Struct from piker.data.types import Struct
from piker.data._web_bs import open_autorecon_ws, NoBsWs from piker.data._web_bs import open_autorecon_ws, NoBsWs
from . import log from . import log
from .api import ( from .api import (
Client, 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): class OHLC(Struct):
''' '''
Description of the flattened OHLC quote format. Description of the flattened OHLC quote format.
@ -336,17 +295,17 @@ async def stream_quotes(
# transform to upper since piker style is always lower # transform to upper since piker style is always lower
sym = sym.upper() sym = sym.upper()
sym_info = await client.symbol_info(sym) si: Pair = await client.symbol_info(sym)
try: # try:
si = Pair(**sym_info) # validation # si = Pair(**sym_info) # validation
except TypeError: # except TypeError:
fields_diff = set(sym_info) - set(Pair.__struct_fields__) # fields_diff = set(sym_info) - set(Pair.__struct_fields__)
raise TypeError( # raise TypeError(
f'Missing msg fields {fields_diff}' # f'Missing msg fields {fields_diff}'
) # )
syminfo = si.to_dict() syminfo = si.to_dict()
syminfo['price_tick_size'] = 1 / 10**si.pair_decimals syminfo['price_tick_size'] = 1. / 10**si.pair_decimals
syminfo['lot_tick_size'] = 1 / 10**si.lot_decimals syminfo['lot_tick_size'] = 1. / 10**si.lot_decimals
syminfo['asset_type'] = 'crypto' syminfo['asset_type'] = 'crypto'
sym_infos[sym] = syminfo sym_infos[sym] = syminfo
ws_pairs[sym] = si.wsname ws_pairs[sym] = si.wsname

View File

@ -38,6 +38,7 @@ import tractor
from .. import data from .. import data
from ..data.types import Struct from ..data.types import Struct
from ..data._source import Symbol
from ..pp import ( from ..pp import (
Position, Position,
Transaction, Transaction,
@ -81,6 +82,7 @@ class PaperBoi(Struct):
_reqids: bidict _reqids: bidict
_positions: dict[str, Position] _positions: dict[str, Position]
_trade_ledger: dict[str, Any] _trade_ledger: dict[str, Any]
_syms: dict[str, Symbol] = {}
# init edge case L1 spread # init edge case L1 spread
last_ask: tuple[float, float] = (float('inf'), 0) # price, size last_ask: tuple[float, float] = (float('inf'), 0) # price, size
@ -252,6 +254,7 @@ class PaperBoi(Struct):
key = fqsn.rstrip(f'.{self.broker}') key = fqsn.rstrip(f'.{self.broker}')
t = Transaction( t = Transaction(
fqsn=fqsn, fqsn=fqsn,
sym=self._syms[fqsn],
tid=oid, tid=oid,
size=size, size=size,
price=price, price=price,
@ -262,9 +265,11 @@ class PaperBoi(Struct):
with ( with (
open_trade_ledger(self.broker, 'paper') as ledger, open_trade_ledger(self.broker, 'paper') as ledger,
open_pps(self.broker, 'paper', True) as table open_pps(self.broker, 'paper', write_on_exit=True) as table
): ):
ledger.update({oid: t.to_dict()}) tx = t.to_dict()
tx.pop('sym')
ledger.update({oid: tx})
# Write to pps toml right now # Write to pps toml right now
table.update_from_trans({oid: t}) table.update_from_trans({oid: t})
@ -567,6 +572,10 @@ async def trades_dialogue(
# TODO: load postions from ledger file # TODO: load postions from ledger file
_trade_ledger={}, _trade_ledger={},
_syms={
fqsn: flume.symbol
for fqsn, flume in feed.flumes.items()
}
) )
n.start_soon( n.start_soon(

View File

@ -237,6 +237,7 @@ def write(
config: dict, # toml config as dict config: dict, # toml config as dict
name: str = 'brokers', name: str = 'brokers',
path: str = None, path: str = None,
fail_empty: bool = True,
**toml_kwargs, **toml_kwargs,
) -> None: ) -> None:
@ -252,7 +253,7 @@ def write(
log.debug(f"Creating config dir {_config_dir}") log.debug(f"Creating config dir {_config_dir}")
os.makedirs(dirname) os.makedirs(dirname)
if not config: if not config and fail_empty:
raise ValueError( raise ValueError(
"Watch out you're trying to write a blank config!") "Watch out you're trying to write a blank config!")

View File

@ -18,8 +18,11 @@
numpy data source coversion helpers. numpy data source coversion helpers.
""" """
from __future__ import annotations from __future__ import annotations
from decimal import (
Decimal,
ROUND_HALF_EVEN,
)
from typing import Any from typing import Any
import decimal
from bidict import bidict from bidict import bidict
import numpy as np import numpy as np
@ -77,10 +80,14 @@ def mk_fqsn(
def float_digits( def float_digits(
value: float, value: float,
) -> int: ) -> int:
'''
Return the number of precision digits read from a float value.
'''
if value == 0: if value == 0:
return 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: 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): class Symbol(Struct):
''' '''
I guess this is some kinda container thing for dealing with I guess this is some kinda container thing for dealing with
@ -141,10 +198,6 @@ class Symbol(Struct):
suffix: str = '' suffix: str = ''
broker_info: dict[str, dict[str, Any]] = {} broker_info: dict[str, dict[str, Any]] = {}
# specifies a "class" of financial instrument
# ex. stock, futer, option, bond etc.
# @validate_arguments
@classmethod @classmethod
def from_broker_info( def from_broker_info(
cls, cls,
@ -156,14 +209,14 @@ class Symbol(Struct):
) -> Symbol: ) -> Symbol:
tick_size = info.get('price_tick_size', 0.01) 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( return Symbol(
key=symbol, key=symbol,
tick_size=tick_size, tick_size=tick_size,
lot_tick_size=lot_tick_size, lot_tick_size=lot_size,
tick_size_digits=float_digits(tick_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, suffix=suffix,
broker_info={broker: info}, broker_info={broker: info},
) )
@ -244,15 +297,21 @@ class Symbol(Struct):
fqsn = '.'.join(map(str.lower, tokens)) fqsn = '.'.join(map(str.lower, tokens))
return fqsn return fqsn
def iterfqsns(self) -> list[str]: def quantize_size(
keys = [] self,
for broker in self.broker_info.keys(): size: float,
fqsn = mk_fqsn(self.key, broker)
if self.suffix:
fqsn += f'.{self.suffix}'
keys.append(fqsn)
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): def _nan_to_closest_num(array: np.ndarray):

View File

@ -44,7 +44,7 @@ import toml
from . import config from . import config
from .brokers import get_brokermod from .brokers import get_brokermod
from .clearing._messages import BrokerdPosition, Status from .clearing._messages import BrokerdPosition, Status
from .data._source import Symbol from .data._source import Symbol, unpack_fqsn
from .log import get_logger from .log import get_logger
from .data.types import Struct from .data.types import Struct
@ -82,17 +82,19 @@ def open_trade_ledger(
with open(tradesfile, 'rb') as cf: with open(tradesfile, 'rb') as cf:
start = time.time() start = time.time()
ledger = tomli.load(cf) 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() cpy = ledger.copy()
try: try:
yield cpy yield cpy
finally: finally:
if cpy != ledger: if cpy != ledger:
# TODO: show diff output? # TODO: show diff output?
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries # https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries
print(f'Updating ledger for {tradesfile}:\n') log.info(f'Updating ledger for {tradesfile}:\n')
ledger.update(cpy) ledger.update(cpy)
# we write on close the mutated ledger data # we write on close the mutated ledger data
with open(tradesfile, 'w') as cf: with open(tradesfile, 'w') as cf:
toml.dump(ledger, cf) toml.dump(ledger, cf)
@ -102,17 +104,18 @@ class Transaction(Struct, frozen=True):
# TODO: should this be ``.to`` (see below)? # TODO: should this be ``.to`` (see below)?
fqsn: str fqsn: str
sym: Symbol
tid: Union[str, int] # unique transaction id 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
dt: datetime dt: datetime
expiry: Optional[datetime] = None expiry: datetime | None = None
# 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.
bsuid: Optional[Union[str, int]] = None bsuid: Union[str, int] | None = None
# optional fqsn for the source "asset"/money symbol? # optional fqsn for the source "asset"/money symbol?
# from: Optional[str] = None # from: Optional[str] = None
@ -192,6 +195,13 @@ class Position(Struct):
s = d.pop('symbol') s = d.pop('symbol')
fqsn = s.front_fqsn() 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: if self.expiry is None:
d.pop('expiry', None) d.pop('expiry', None)
elif expiry: elif expiry:
@ -466,7 +476,7 @@ class Position(Struct):
if self.split_ratio is not None: if self.split_ratio is not None:
size = round(size * self.split_ratio) size = round(size * self.split_ratio)
return size return float(self.symbol.quantize_size(size))
def minimize_clears( def minimize_clears(
self, self,
@ -510,7 +520,7 @@ class Position(Struct):
'cost': t.cost, 'cost': t.cost,
'price': t.price, 'price': t.price,
'size': t.size, 'size': t.size,
'dt': t.dt, 'dt': t.dt
} }
# TODO: compute these incrementally instead # TODO: compute these incrementally instead
@ -557,7 +567,7 @@ class PpTable(Struct):
Symbol.from_fqsn( Symbol.from_fqsn(
t.fqsn, t.fqsn,
info={}, info={},
), ) if not t.sym else t.sym,
size=0.0, size=0.0,
ppu=0.0, ppu=0.0,
bsuid=t.bsuid, bsuid=t.bsuid,
@ -680,12 +690,21 @@ class PpTable(Struct):
''' '''
# TODO: show diff output? # TODO: show diff output?
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries # 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() # active, closed_pp_objs = table.dump_active()
pp_entries = self.to_toml() pp_entries = self.to_toml()
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 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 # TODO: why tf haven't they already done this for inline
# tables smh.. # tables smh..
enc = PpsEncoder(preserve=True) enc = PpsEncoder(preserve=True)
@ -698,6 +717,7 @@ class PpTable(Struct):
self.conf, self.conf,
'pps', 'pps',
encoder=enc, encoder=enc,
fail_empty=False
) )
@ -881,7 +901,6 @@ def open_pps(
brokername: str, brokername: str,
acctid: str, acctid: str,
write_on_exit: bool = False, write_on_exit: bool = False,
) -> Generator[PpTable, None, None]: ) -> Generator[PpTable, None, None]:
''' '''
Read out broker-specific position entries from Read out broker-specific position entries from
@ -914,6 +933,21 @@ def open_pps(
# and update `PpTable` obj entries. # and update `PpTable` obj entries.
for fqsn, entry in pps.items(): for fqsn, entry in pps.items():
bsuid = entry['bsuid'] 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 # convert clears sub-tables (only in this form
# for toml re-presentation) back into a master table. # for toml re-presentation) back into a master table.
@ -935,8 +969,10 @@ def open_pps(
dtstr = clears_table['dt'] dtstr = clears_table['dt']
dt = pendulum.parse(dtstr) dt = pendulum.parse(dtstr)
clears_table['dt'] = dt clears_table['dt'] = dt
trans.append(Transaction( trans.append(Transaction(
fqsn=bsuid, fqsn=bsuid,
sym=symbol,
bsuid=bsuid, bsuid=bsuid,
tid=tid, tid=tid,
size=clears_table['size'], size=clears_table['size'],
@ -949,7 +985,11 @@ def open_pps(
size = entry['size'] size = entry['size']
# TODO: remove but, handle old field name for now # 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') split_ratio = entry.get('split_ratio')
expiry = entry.get('expiry') expiry = entry.get('expiry')
@ -957,7 +997,7 @@ def open_pps(
expiry = pendulum.parse(expiry) expiry = pendulum.parse(expiry)
pp = pp_objs[bsuid] = Position( pp = pp_objs[bsuid] = Position(
Symbol.from_fqsn(fqsn, info={}), symbol,
size=size, size=size,
ppu=ppu, ppu=ppu,
split_ratio=split_ratio, split_ratio=split_ratio,

View File

@ -84,12 +84,10 @@ async def _async_main(
case {'name': 'position'}: case {'name': 'position'}:
break break
if ( # Teardown piker like a user would
assert_entries raise KeyboardInterrupt
or assert_pps
or assert_zeroed_pps if assert_entries or assert_pps or assert_zeroed_pps or assert_msg:
or assert_msg
):
_assert( _assert(
assert_entries, assert_entries,
assert_pps, assert_pps,
@ -100,9 +98,6 @@ async def _async_main(
executions, executions,
) )
# Teardown piker like a user would
raise KeyboardInterrupt
def _assert( def _assert(
assert_entries, 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( def test_multi_sell(
open_test_pikerd_and_ems: AsyncContextManager, open_test_pikerd_and_ems: AsyncContextManager,
delete_testing_dir delete_testing_dir