ib: fully handle `MktPair.src` and `.dst` in ledger loading

In an effort to properly support fiat pairs (aka forex) as well as more
generally insert a fully-qualified `MktPair` in for the
`Transaction.sys`. Note that there's a bit of special handling for API
`Contract`s-as-dict records vs. flex-report-from-xml equivalents.
account_tests
Tyler Goodlet 2023-06-29 14:04:24 -04:00
parent c0929c042a
commit 10ebc855e4
1 changed files with 120 additions and 46 deletions

View File

@ -29,6 +29,7 @@ from bidict import bidict
import pendulum import pendulum
from piker.accounting import ( from piker.accounting import (
Asset,
dec_digits, dec_digits,
digits_to_dec, digits_to_dec,
Transaction, Transaction,
@ -43,10 +44,12 @@ def norm_trade_records(
) -> dict[str, Transaction]: ) -> dict[str, Transaction]:
''' '''
Normalize a flex report or API retrieved executions Normalize (xml) flex-report or (recent) API trade records into
ledger into our standard record format. our ledger format with parsing for `MktPair` and `Asset`
extraction to fill in the `Transaction.sys: MktPair` field.
''' '''
# select: list[transactions] = []
records: list[Transaction] = [] records: list[Transaction] = []
for tid, record in ledger.items(): for tid, record in ledger.items():
@ -64,26 +67,25 @@ def norm_trade_records(
'SLD': -1, 'SLD': -1,
}[record['side']] }[record['side']]
exch = record['exchange'] symbol: str = record['symbol']
lexch = record.get('listingExchange') exch: str = record.get('listingExchange') or record['exchange']
# NOTE: remove null values since `tomlkit` can't serialize # NOTE: remove null values since `tomlkit` can't serialize
# them to file. # them to file.
dnc = record.pop('deltaNeutralContract', False) if dnc := record.pop('deltaNeutralContract', None):
if dnc is not None:
record['deltaNeutralContract'] = dnc record['deltaNeutralContract'] = dnc
suffix = lexch or exch
symbol = record['symbol']
# likely an opts contract record from a flex report.. # likely an opts contract record from a flex report..
# TODO: no idea how to parse ^ the strike part from flex.. # TODO: no idea how to parse ^ the strike part from flex..
# (00010000 any, or 00007500 tsla, ..) # (00010000 any, or 00007500 tsla, ..)
# we probably must do the contract lookup for this? # we probably must do the contract lookup for this?
if ' ' in symbol or '--' in exch: if (
' ' in symbol
or '--' in exch
):
underlying, _, tail = symbol.partition(' ') underlying, _, tail = symbol.partition(' ')
suffix = exch = 'opt' exch: str = 'opt'
expiry = tail[:6] expiry: str = tail[:6]
# otype = tail[6] # otype = tail[6]
# strike = tail[7:] # strike = tail[7:]
@ -108,45 +110,107 @@ def norm_trade_records(
'assetCategory' 'assetCategory'
) or record.get('secType', 'STK') ) or record.get('secType', 'STK')
# TODO: XXX: WOA this is kinda hacky.. probably if (expiry := (
# should figure out the correct future pair key more record.get('lastTradeDateOrContractMonth')
# explicitly and consistently? or record.get('expiry')
if asset_type == 'FUT': )
# (flex) ledger entries don't have any simple 3-char key? ):
symbol = record['symbol'][:3] expiry: str = str(expiry).strip(' ')
asset_type: str = 'future' # NOTE: we directly use the (simple and usually short)
# date-string expiry token when packing the `MktPair`
# since we want the fqme to contain *that* token.
# It might make sense later to instead parse and then
# render different output str format(s) for this same
# purpose depending on asset-type-market down the road.
# Eg. for derivs we use the short token only for fqme
# but use the isoformat('T') for transactions and
# account file position entries?
# dt_str: str = pendulum.parse(expiry).isoformat('T')
elif asset_type == 'STK': # XXX: pretty much all legacy market assets have a fiat
asset_type: str = 'stock' # currency (denomination) determined by their venue.
currency: str = record['currency']
# try to build out piker fqme from record. src = Asset(
expiry = ( name=currency.lower(),
record.get('lastTradeDateOrContractMonth') atype='fiat',
or record.get('expiry') tx_tick=Decimal('0.01'),
) )
if expiry: match asset_type:
expiry = str(expiry).strip(' ') case 'FUT':
suffix = f'{exch}.{expiry}' # (flex) ledger entries don't have any simple 3-char key?
expiry = pendulum.parse(expiry) # TODO: XXX: WOA this is kinda hacky.. probably
# should figure out the correct future pair key more
# explicitly and consistently?
symbol: str = symbol[:3]
dst = Asset(
name=symbol.lower(),
atype='future',
tx_tick=Decimal('1'),
)
case 'STK':
dst = Asset(
name=symbol.lower(),
atype='stock',
tx_tick=Decimal('1'),
)
case 'CASH':
if currency not in symbol:
# likely a dict-casted `Forex` contract which
# has .symbol as the dst and .currency as the
# src.
name: str = symbol.lower()
else:
# likely a flex-report record which puts
# EUR.USD as the symbol field and just USD in
# the currency field.
name: str = symbol.lower().replace(f'.{src.name}', '')
dst = Asset(
name=name,
atype='fiat',
tx_tick=Decimal('0.01'),
)
case 'OPT':
dst = Asset(
name=symbol.lower(),
atype='option',
tx_tick=Decimal('1'),
)
# try to build out piker fqme from record.
# src: str = record['currency'] # src: str = record['currency']
price_tick: Decimal = digits_to_dec(dec_digits(price)) price_tick: Decimal = digits_to_dec(dec_digits(price))
pair = MktPair.from_fqme( # NOTE: can't serlialize `tomlkit.String` so cast to native
fqme=f'{symbol}.{suffix}.ib', atype: str = str(dst.atype)
pair = MktPair(
bs_mktid=str(conid), bs_mktid=str(conid),
_atype=str(asset_type), # XXX: can't serlialize `tomlkit.String` dst=dst,
price_tick=price_tick, price_tick=price_tick,
# NOTE: for "legacy" assets, volume is normally discreet, not # NOTE: for "legacy" assets, volume is normally discreet, not
# a float, but we keep a digit in case the suitz decide # a float, but we keep a digit in case the suitz decide
# to get crazy and change it; we'll be kinda ready # to get crazy and change it; we'll be kinda ready
# schema-wise.. # schema-wise..
size_tick='1', size_tick=Decimal('1'),
src=src, # XXX: normally always a fiat
_atype=atype,
venue=exch,
expiry=expiry,
broker='ib',
_fqme_without_src=(atype != 'fiat'),
) )
fqme = pair.fqme fqme: str = pair.fqme
# NOTE: for flex records the normal fields for defining an fqme # NOTE: for flex records the normal fields for defining an fqme
# sometimes won't be available so we rely on two approaches for # sometimes won't be available so we rely on two approaches for
@ -158,22 +222,32 @@ def norm_trade_records(
# should already have entries if the pps are still open, in # should already have entries if the pps are still open, in
# which case, we can pull the fqme from that table (see # which case, we can pull the fqme from that table (see
# `trades_dialogue()` above). # `trades_dialogue()` above).
trans = Transaction(
fqme=fqme,
sym=pair,
tid=tid,
size=size,
price=price,
cost=comms,
dt=dt,
expiry=expiry,
bs_mktid=str(conid),
)
insort( insort(
records, records,
Transaction( trans,
fqme=fqme,
sym=pair,
tid=tid,
size=size,
price=price,
cost=comms,
dt=dt,
expiry=expiry,
bs_mktid=str(conid),
),
key=lambda t: t.dt key=lambda t: t.dt
) )
# if (
# atype == 'fiat'
# or atype == 'option'
# ):
# select.append(trans)
# if select:
# breakpoint()
return {r.tid: r for r in records} return {r.tid: r for r in records}