Move `.accounting` related config loaders to subpkg
Like you'd think: - `load_ledger()` -> ._ledger - `load_accounrt()` -> ._pos Also fixup the old `load_pps_from_ledger()` and expose it from a new `.accounting.cli.disect` cli cmd for trying to figure out why pp calcs are totally mucked on stupid ib..basic_buy_bot
parent
032976b118
commit
cf1f4bed75
|
@ -123,6 +123,11 @@ class TransactionLedger(UserDict):
|
||||||
self,
|
self,
|
||||||
t: Transaction,
|
t: Transaction,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
'''
|
||||||
|
Given an input `Transaction`, cast to `dict` and update
|
||||||
|
from it's transaction id.
|
||||||
|
|
||||||
|
'''
|
||||||
self.data[t.tid] = t.to_dict()
|
self.data[t.tid] = t.to_dict()
|
||||||
|
|
||||||
def iter_trans(
|
def iter_trans(
|
||||||
|
@ -259,6 +264,45 @@ def iter_by_dt(
|
||||||
yield tid, data
|
yield tid, data
|
||||||
|
|
||||||
|
|
||||||
|
def load_ledger(
|
||||||
|
brokername: str,
|
||||||
|
acctid: str,
|
||||||
|
|
||||||
|
) -> tuple[dict, Path]:
|
||||||
|
'''
|
||||||
|
Load a ledger (TOML) file from user's config directory:
|
||||||
|
$CONFIG_DIR/accounting/ledgers/trades_<brokername>_<acctid>.toml
|
||||||
|
|
||||||
|
Return its `dict`-content and file path.
|
||||||
|
|
||||||
|
'''
|
||||||
|
import time
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
|
ldir: Path = config._config_dir / 'accounting' / 'ledgers'
|
||||||
|
if not ldir.is_dir():
|
||||||
|
ldir.mkdir()
|
||||||
|
|
||||||
|
fname = f'trades_{brokername}_{acctid}.toml'
|
||||||
|
fpath: Path = ldir / fname
|
||||||
|
|
||||||
|
if not fpath.is_file():
|
||||||
|
log.info(
|
||||||
|
f'Creating new local trades ledger: {fpath}'
|
||||||
|
)
|
||||||
|
fpath.touch()
|
||||||
|
|
||||||
|
with fpath.open(mode='rb') as cf:
|
||||||
|
start = time.time()
|
||||||
|
ledger_dict = tomllib.load(cf)
|
||||||
|
log.debug(f'Ledger load took {time.time() - start}s')
|
||||||
|
|
||||||
|
return ledger_dict, fpath
|
||||||
|
|
||||||
|
|
||||||
@cm
|
@cm
|
||||||
def open_trade_ledger(
|
def open_trade_ledger(
|
||||||
broker: str,
|
broker: str,
|
||||||
|
@ -267,7 +311,7 @@ def open_trade_ledger(
|
||||||
# default is to sort by detected datetime-ish field
|
# default is to sort by detected datetime-ish field
|
||||||
tx_sort: Callable = iter_by_dt,
|
tx_sort: Callable = iter_by_dt,
|
||||||
|
|
||||||
) -> Generator[dict, None, None]:
|
) -> Generator[TransactionLedger, None, None]:
|
||||||
'''
|
'''
|
||||||
Indempotently create and read in a trade log file from the
|
Indempotently create and read in a trade log file from the
|
||||||
``<configuration_dir>/ledgers/`` directory.
|
``<configuration_dir>/ledgers/`` directory.
|
||||||
|
@ -277,7 +321,7 @@ def open_trade_ledger(
|
||||||
name as defined in the user's ``brokers.toml`` config.
|
name as defined in the user's ``brokers.toml`` config.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
ledger_dict, fpath = config.load_ledger(broker, account)
|
ledger_dict, fpath = load_ledger(broker, account)
|
||||||
cpy = ledger_dict.copy()
|
cpy = ledger_dict.copy()
|
||||||
ledger = TransactionLedger(
|
ledger = TransactionLedger(
|
||||||
ledger_dict=cpy,
|
ledger_dict=cpy,
|
||||||
|
|
|
@ -42,6 +42,7 @@ from ._ledger import (
|
||||||
Transaction,
|
Transaction,
|
||||||
iter_by_dt,
|
iter_by_dt,
|
||||||
open_trade_ledger,
|
open_trade_ledger,
|
||||||
|
TransactionLedger,
|
||||||
)
|
)
|
||||||
from ._mktinfo import (
|
from ._mktinfo import (
|
||||||
MktPair,
|
MktPair,
|
||||||
|
@ -49,7 +50,6 @@ from ._mktinfo import (
|
||||||
unpack_fqme,
|
unpack_fqme,
|
||||||
)
|
)
|
||||||
from .. import config
|
from .. import config
|
||||||
from ..brokers import get_brokermod
|
|
||||||
from ..clearing._messages import (
|
from ..clearing._messages import (
|
||||||
BrokerdPosition,
|
BrokerdPosition,
|
||||||
Status,
|
Status,
|
||||||
|
@ -327,7 +327,8 @@ class Position(Struct):
|
||||||
entry: dict[str, Any]
|
entry: dict[str, Any]
|
||||||
for (tid, entry) in self.iter_clears():
|
for (tid, entry) in self.iter_clears():
|
||||||
clear_size = entry['size']
|
clear_size = entry['size']
|
||||||
clear_price = entry['price']
|
clear_price: str | float = entry['price']
|
||||||
|
is_clear: bool = not isinstance(clear_price, str)
|
||||||
|
|
||||||
last_accum_size = asize_h[-1] if asize_h else 0
|
last_accum_size = asize_h[-1] if asize_h else 0
|
||||||
accum_size = last_accum_size + clear_size
|
accum_size = last_accum_size + clear_size
|
||||||
|
@ -340,9 +341,18 @@ class Position(Struct):
|
||||||
asize_h.append(0)
|
asize_h.append(0)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if accum_size == 0:
|
# on transfers we normally write some non-valid
|
||||||
ppu_h.append(0)
|
# price since withdrawal to another account/wallet
|
||||||
asize_h.append(0)
|
# has nothing to do with inter-asset-market prices.
|
||||||
|
# TODO: this should be better handled via a `type: 'tx'`
|
||||||
|
# field as per existing issue surrounding all this:
|
||||||
|
# https://github.com/pikers/piker/issues/510
|
||||||
|
if isinstance(clear_price, str):
|
||||||
|
# TODO: we can't necessarily have this commit to
|
||||||
|
# the overall pos size since we also need to
|
||||||
|
# include other positions contributions to this
|
||||||
|
# balance or we might end up with a -ve balance for
|
||||||
|
# the position..
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# test if the pp somehow went "passed" a net zero size state
|
# test if the pp somehow went "passed" a net zero size state
|
||||||
|
@ -375,7 +385,10 @@ class Position(Struct):
|
||||||
# abs_clear_size = abs(clear_size)
|
# abs_clear_size = abs(clear_size)
|
||||||
abs_new_size = abs(accum_size)
|
abs_new_size = abs(accum_size)
|
||||||
|
|
||||||
if abs_diff > 0:
|
if (
|
||||||
|
abs_diff > 0
|
||||||
|
and is_clear
|
||||||
|
):
|
||||||
|
|
||||||
cost_basis = (
|
cost_basis = (
|
||||||
# cost basis for this clear
|
# cost basis for this clear
|
||||||
|
@ -397,6 +410,12 @@ class Position(Struct):
|
||||||
asize_h.append(accum_size)
|
asize_h.append(accum_size)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# TODO: for PPU we should probably handle txs out
|
||||||
|
# (aka withdrawals) similarly by simply not having
|
||||||
|
# them contrib to the running PPU calc and only
|
||||||
|
# when the next entry clear comes in (which will
|
||||||
|
# then have a higher weighting on the PPU).
|
||||||
|
|
||||||
# on "exit" clears from a given direction,
|
# on "exit" clears from a given direction,
|
||||||
# only the size changes not the price-per-unit
|
# only the size changes not the price-per-unit
|
||||||
# need to be updated since the ppu remains constant
|
# need to be updated since the ppu remains constant
|
||||||
|
@ -734,48 +753,63 @@ class PpTable(Struct):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_pps_from_ledger(
|
def load_account(
|
||||||
|
|
||||||
brokername: str,
|
brokername: str,
|
||||||
acctname: str,
|
acctid: str,
|
||||||
|
|
||||||
# post normalization filter on ledger entries to be processed
|
) -> tuple[dict, Path]:
|
||||||
filter_by: list[dict] | None = None,
|
|
||||||
|
|
||||||
) -> tuple[
|
|
||||||
dict[str, Transaction],
|
|
||||||
dict[str, Position],
|
|
||||||
]:
|
|
||||||
'''
|
'''
|
||||||
Open a ledger file by broker name and account and read in and
|
Load a accounting (with positions) file from
|
||||||
process any trade records into our normalized ``Transaction`` form
|
$CONFIG_DIR/accounting/account.<brokername>.<acctid>.toml
|
||||||
and then update the equivalent ``Pptable`` and deliver the two
|
|
||||||
bs_mktid-mapped dict-sets of the transactions and pps.
|
Where normally $CONFIG_DIR = ~/.config/piker/
|
||||||
|
and we implicitly create a accounting subdir which should
|
||||||
|
normally be linked to a git repo managed by the user B)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
with (
|
legacy_fn: str = f'pps.{brokername}.{acctid}.toml'
|
||||||
open_trade_ledger(brokername, acctname) as ledger,
|
fn: str = f'account.{brokername}.{acctid}.toml'
|
||||||
open_pps(brokername, acctname) as table,
|
|
||||||
):
|
|
||||||
if not ledger:
|
|
||||||
# null case, no ledger file with content
|
|
||||||
return {}
|
|
||||||
|
|
||||||
mod = get_brokermod(brokername)
|
dirpath: Path = config._config_dir / 'accounting'
|
||||||
src_records: dict[str, Transaction] = mod.norm_trade_records(ledger)
|
if not dirpath.is_dir():
|
||||||
|
dirpath.mkdir()
|
||||||
|
|
||||||
if filter_by:
|
conf, path = config.load(
|
||||||
records = {}
|
path=dirpath / fn,
|
||||||
bs_mktids = set(filter_by)
|
decode=tomlkit.parse,
|
||||||
for tid, r in src_records.items():
|
touch_if_dne=True,
|
||||||
if r.bs_mktid in bs_mktids:
|
)
|
||||||
records[tid] = r
|
|
||||||
else:
|
|
||||||
records = src_records
|
|
||||||
|
|
||||||
updated = table.update_from_trans(records)
|
if not conf:
|
||||||
|
legacypath = dirpath / legacy_fn
|
||||||
|
log.warning(
|
||||||
|
f'Your account file is using the legacy `pps.` prefix..\n'
|
||||||
|
f'Rewriting contents to new name -> {path}\n'
|
||||||
|
'Please delete the old file!\n'
|
||||||
|
f'|-> {legacypath}\n'
|
||||||
|
)
|
||||||
|
if legacypath.is_file():
|
||||||
|
legacy_config, _ = config.load(
|
||||||
|
path=legacypath,
|
||||||
|
|
||||||
return records, updated
|
# TODO: move to tomlkit:
|
||||||
|
# - needs to be fixed to support bidict?
|
||||||
|
# https://github.com/sdispater/tomlkit/issues/289
|
||||||
|
# - we need to use or fork's fix to do multiline array
|
||||||
|
# indenting.
|
||||||
|
decode=tomlkit.parse,
|
||||||
|
)
|
||||||
|
conf.update(legacy_config)
|
||||||
|
|
||||||
|
# XXX: override the presumably previously non-existant
|
||||||
|
# file with legacy's contents.
|
||||||
|
config.write(
|
||||||
|
conf,
|
||||||
|
path=path,
|
||||||
|
fail_empty=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return conf, path
|
||||||
|
|
||||||
|
|
||||||
@cm
|
@cm
|
||||||
|
@ -792,7 +826,7 @@ def open_pps(
|
||||||
'''
|
'''
|
||||||
conf: dict
|
conf: dict
|
||||||
conf_path: Path
|
conf_path: Path
|
||||||
conf, conf_path = config.load_account(brokername, acctid)
|
conf, conf_path = load_account(brokername, acctid)
|
||||||
|
|
||||||
if brokername in conf:
|
if brokername in conf:
|
||||||
log.warning(
|
log.warning(
|
||||||
|
@ -927,3 +961,56 @@ def open_pps(
|
||||||
finally:
|
finally:
|
||||||
if write_on_exit:
|
if write_on_exit:
|
||||||
table.write_config()
|
table.write_config()
|
||||||
|
|
||||||
|
|
||||||
|
def load_pps_from_ledger(
|
||||||
|
|
||||||
|
brokername: str,
|
||||||
|
acctname: str,
|
||||||
|
|
||||||
|
# post normalization filter on ledger entries to be processed
|
||||||
|
filter_by_ids: list[str] | None = None,
|
||||||
|
|
||||||
|
) -> tuple[
|
||||||
|
dict[str, Transaction],
|
||||||
|
PpTable,
|
||||||
|
]:
|
||||||
|
'''
|
||||||
|
Open a ledger file by broker name and account and read in and
|
||||||
|
process any trade records into our normalized ``Transaction`` form
|
||||||
|
and then update the equivalent ``Pptable`` and deliver the two
|
||||||
|
bs_mktid-mapped dict-sets of the transactions and pps.
|
||||||
|
|
||||||
|
'''
|
||||||
|
ledger: TransactionLedger
|
||||||
|
table: PpTable
|
||||||
|
with (
|
||||||
|
open_trade_ledger(brokername, acctname) as ledger,
|
||||||
|
open_pps(brokername, acctname) as table,
|
||||||
|
):
|
||||||
|
if not ledger:
|
||||||
|
# null case, no ledger file with content
|
||||||
|
return {}
|
||||||
|
|
||||||
|
from ..brokers import get_brokermod
|
||||||
|
mod = get_brokermod(brokername)
|
||||||
|
src_records: dict[str, Transaction] = mod.norm_trade_records(
|
||||||
|
ledger
|
||||||
|
)
|
||||||
|
|
||||||
|
if not filter_by_ids:
|
||||||
|
# records = src_records
|
||||||
|
records = ledger
|
||||||
|
|
||||||
|
else:
|
||||||
|
records = {}
|
||||||
|
bs_mktids = set(map(str, filter_by_ids))
|
||||||
|
|
||||||
|
# for tid, recdict in ledger.items():
|
||||||
|
for tid, r in src_records.items():
|
||||||
|
if r.bs_mktid in bs_mktids:
|
||||||
|
records[tid] = r.to_dict()
|
||||||
|
|
||||||
|
# updated = table.update_from_trans(records)
|
||||||
|
|
||||||
|
return records, table
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
CLI front end for trades ledger and position tracking management.
|
CLI front end for trades ledger and position tracking management.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
import tractor
|
import tractor
|
||||||
|
@ -29,9 +30,18 @@ from ..service import (
|
||||||
open_piker_runtime,
|
open_piker_runtime,
|
||||||
)
|
)
|
||||||
from ..clearing._messages import BrokerdPosition
|
from ..clearing._messages import BrokerdPosition
|
||||||
from ..config import load_ledger
|
|
||||||
from ..calc import humanize
|
from ..calc import humanize
|
||||||
from ..brokers._daemon import broker_init
|
from ..brokers._daemon import broker_init
|
||||||
|
from ._ledger import (
|
||||||
|
load_ledger,
|
||||||
|
# open_trade_ledger,
|
||||||
|
TransactionLedger,
|
||||||
|
)
|
||||||
|
from ._pos import (
|
||||||
|
PpTable,
|
||||||
|
load_pps_from_ledger,
|
||||||
|
# load_account,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ledger = typer.Typer()
|
ledger = typer.Typer()
|
||||||
|
@ -39,7 +49,7 @@ ledger = typer.Typer()
|
||||||
|
|
||||||
def unpack_fqan(
|
def unpack_fqan(
|
||||||
fully_qualified_account_name: str,
|
fully_qualified_account_name: str,
|
||||||
console: Console | None,
|
console: Console | None = None,
|
||||||
) -> tuple | bool:
|
) -> tuple | bool:
|
||||||
try:
|
try:
|
||||||
brokername, account = fully_qualified_account_name.split('.')
|
brokername, account = fully_qualified_account_name.split('.')
|
||||||
|
@ -225,7 +235,8 @@ def sync(
|
||||||
|
|
||||||
@ledger.command()
|
@ledger.command()
|
||||||
def disect(
|
def disect(
|
||||||
fully_qualified_account_name: str,
|
# "fully_qualified_account_name"
|
||||||
|
fqan: str,
|
||||||
bs_mktid: int, # for ib
|
bs_mktid: int, # for ib
|
||||||
pdb: bool = False,
|
pdb: bool = False,
|
||||||
|
|
||||||
|
@ -235,10 +246,28 @@ def disect(
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
pair: tuple[str, str]
|
pair: tuple[str, str]
|
||||||
if not (pair := unpack_fqan(
|
if not (pair := unpack_fqan(fqan)):
|
||||||
fully_qualified_account_name,
|
raise ValueError('{fqan} malformed!?')
|
||||||
)):
|
|
||||||
return
|
brokername, account = pair
|
||||||
|
|
||||||
|
ledger: TransactionLedger
|
||||||
|
table: PpTable
|
||||||
|
records, table = load_pps_from_ledger(
|
||||||
|
brokername,
|
||||||
|
account,
|
||||||
|
# filter_by_id = {568549458},
|
||||||
|
filter_by_ids={bs_mktid},
|
||||||
|
)
|
||||||
|
breakpoint()
|
||||||
|
# tractor.pause_from_sync()
|
||||||
|
# with open_trade_ledger(
|
||||||
|
# brokername,
|
||||||
|
# account,
|
||||||
|
# ) as ledger:
|
||||||
|
# for tid, rec in ledger.items():
|
||||||
|
# bs_mktid: str = rec['bs_mktid']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -22,7 +22,6 @@ import platform
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
MutableMapping,
|
MutableMapping,
|
||||||
|
@ -310,98 +309,6 @@ def load(
|
||||||
return config, path
|
return config, path
|
||||||
|
|
||||||
|
|
||||||
def load_account(
|
|
||||||
brokername: str,
|
|
||||||
acctid: str,
|
|
||||||
|
|
||||||
) -> tuple[dict, Path]:
|
|
||||||
'''
|
|
||||||
Load a accounting (with positions) file from
|
|
||||||
$CONFIG_DIR/accounting/account.<brokername>.<acctid>.toml
|
|
||||||
|
|
||||||
Where normally $CONFIG_DIR = ~/.config/piker/
|
|
||||||
and we implicitly create a accounting subdir which should
|
|
||||||
normally be linked to a git repo managed by the user B)
|
|
||||||
|
|
||||||
'''
|
|
||||||
legacy_fn: str = f'pps.{brokername}.{acctid}.toml'
|
|
||||||
fn: str = f'account.{brokername}.{acctid}.toml'
|
|
||||||
|
|
||||||
dirpath: Path = _config_dir / 'accounting'
|
|
||||||
if not dirpath.is_dir():
|
|
||||||
dirpath.mkdir()
|
|
||||||
|
|
||||||
config, path = load(
|
|
||||||
path=dirpath / fn,
|
|
||||||
decode=tomlkit.parse,
|
|
||||||
touch_if_dne=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not config:
|
|
||||||
legacypath = dirpath / legacy_fn
|
|
||||||
log.warning(
|
|
||||||
f'Your account file is using the legacy `pps.` prefix..\n'
|
|
||||||
f'Rewriting contents to new name -> {path}\n'
|
|
||||||
'Please delete the old file!\n'
|
|
||||||
f'|-> {legacypath}\n'
|
|
||||||
)
|
|
||||||
if legacypath.is_file():
|
|
||||||
legacy_config, _ = load(
|
|
||||||
path=legacypath,
|
|
||||||
|
|
||||||
# TODO: move to tomlkit:
|
|
||||||
# - needs to be fixed to support bidict?
|
|
||||||
# https://github.com/sdispater/tomlkit/issues/289
|
|
||||||
# - we need to use or fork's fix to do multiline array
|
|
||||||
# indenting.
|
|
||||||
decode=tomlkit.parse,
|
|
||||||
)
|
|
||||||
config.update(legacy_config)
|
|
||||||
|
|
||||||
# XXX: override the presumably previously non-existant
|
|
||||||
# file with legacy's contents.
|
|
||||||
write(
|
|
||||||
config,
|
|
||||||
path=path,
|
|
||||||
fail_empty=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return config, path
|
|
||||||
|
|
||||||
|
|
||||||
def load_ledger(
|
|
||||||
brokername: str,
|
|
||||||
acctid: str,
|
|
||||||
|
|
||||||
) -> tuple[dict, Path]:
|
|
||||||
'''
|
|
||||||
Load a ledger (TOML) file from user's config directory:
|
|
||||||
$CONFIG_DIR/accounting/ledgers/trades_<brokername>_<acctid>.toml
|
|
||||||
|
|
||||||
Return its `dict`-content and file path.
|
|
||||||
|
|
||||||
'''
|
|
||||||
ldir: Path = _config_dir / 'accounting' / 'ledgers'
|
|
||||||
if not ldir.is_dir():
|
|
||||||
ldir.mkdir()
|
|
||||||
|
|
||||||
fname = f'trades_{brokername}_{acctid}.toml'
|
|
||||||
fpath: Path = ldir / fname
|
|
||||||
|
|
||||||
if not fpath.is_file():
|
|
||||||
log.info(
|
|
||||||
f'Creating new local trades ledger: {fpath}'
|
|
||||||
)
|
|
||||||
fpath.touch()
|
|
||||||
|
|
||||||
with fpath.open(mode='rb') as cf:
|
|
||||||
start = time.time()
|
|
||||||
ledger_dict = tomllib.load(cf)
|
|
||||||
log.debug(f'Ledger load took {time.time() - start}s')
|
|
||||||
|
|
||||||
return ledger_dict, fpath
|
|
||||||
|
|
||||||
|
|
||||||
def write(
|
def write(
|
||||||
config: dict, # toml config as dict
|
config: dict, # toml config as dict
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,10 @@ def get_logger(
|
||||||
Return the package log or a sub-log for `name` if provided.
|
Return the package log or a sub-log for `name` if provided.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
return tractor.log.get_logger(name=name, _root_name=_proj_name)
|
return tractor.log.get_logger(
|
||||||
|
name=name,
|
||||||
|
_root_name=_proj_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_console_log(
|
def get_console_log(
|
||||||
|
|
Loading…
Reference in New Issue