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
parent
c0929c042a
commit
10ebc855e4
|
@ -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
|
|
||||||
# explicitly and consistently?
|
|
||||||
if asset_type == 'FUT':
|
|
||||||
# (flex) ledger entries don't have any simple 3-char key?
|
|
||||||
symbol = record['symbol'][:3]
|
|
||||||
asset_type: str = 'future'
|
|
||||||
|
|
||||||
elif asset_type == 'STK':
|
|
||||||
asset_type: str = 'stock'
|
|
||||||
|
|
||||||
# try to build out piker fqme from record.
|
|
||||||
expiry = (
|
|
||||||
record.get('lastTradeDateOrContractMonth')
|
record.get('lastTradeDateOrContractMonth')
|
||||||
or record.get('expiry')
|
or record.get('expiry')
|
||||||
)
|
)
|
||||||
|
):
|
||||||
|
expiry: str = str(expiry).strip(' ')
|
||||||
|
# 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')
|
||||||
|
|
||||||
if expiry:
|
# XXX: pretty much all legacy market assets have a fiat
|
||||||
expiry = str(expiry).strip(' ')
|
# currency (denomination) determined by their venue.
|
||||||
suffix = f'{exch}.{expiry}'
|
currency: str = record['currency']
|
||||||
expiry = pendulum.parse(expiry)
|
src = Asset(
|
||||||
|
name=currency.lower(),
|
||||||
|
atype='fiat',
|
||||||
|
tx_tick=Decimal('0.01'),
|
||||||
|
)
|
||||||
|
|
||||||
|
match asset_type:
|
||||||
|
case 'FUT':
|
||||||
|
# (flex) ledger entries don't have any simple 3-char key?
|
||||||
|
# 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,9 +222,7 @@ 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).
|
||||||
insort(
|
trans = Transaction(
|
||||||
records,
|
|
||||||
Transaction(
|
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
sym=pair,
|
sym=pair,
|
||||||
tid=tid,
|
tid=tid,
|
||||||
|
@ -170,10 +232,22 @@ def norm_trade_records(
|
||||||
dt=dt,
|
dt=dt,
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
bs_mktid=str(conid),
|
bs_mktid=str(conid),
|
||||||
),
|
)
|
||||||
|
insort(
|
||||||
|
records,
|
||||||
|
trans,
|
||||||
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}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue