Compare commits
No commits in common. "d649749e7da6de171aca6be356febd04875b2b48" and "eb51033b18586c397bb0e4a2fd8703ac30841be2" have entirely different histories.
d649749e7d
...
eb51033b18
|
@ -1,92 +0,0 @@
|
||||||
# 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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
'''
|
|
||||||
"Accounting for degens": count dem numberz that tracks how much you got
|
|
||||||
for tendiez.
|
|
||||||
|
|
||||||
'''
|
|
||||||
from ..log import get_logger
|
|
||||||
|
|
||||||
from ._pos import (
|
|
||||||
Transaction,
|
|
||||||
open_trade_ledger,
|
|
||||||
PpTable,
|
|
||||||
)
|
|
||||||
from ._pos import (
|
|
||||||
open_pps,
|
|
||||||
load_pps_from_ledger,
|
|
||||||
Position,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'Transaction',
|
|
||||||
'open_trade_ledger',
|
|
||||||
'PpTable',
|
|
||||||
'open_pps',
|
|
||||||
'load_pps_from_ledger',
|
|
||||||
'Position',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_likely_pair(
|
|
||||||
src: str,
|
|
||||||
dst: str,
|
|
||||||
bsuid: str,
|
|
||||||
|
|
||||||
) -> str:
|
|
||||||
'''
|
|
||||||
Attempt to get the likely trading pair matching a given destination
|
|
||||||
asset `dst: str`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
src_name_start = bsuid.rindex(src)
|
|
||||||
except (
|
|
||||||
ValueError, # substr not found
|
|
||||||
):
|
|
||||||
# TODO: handle nested positions..(i.e.
|
|
||||||
# positions where the src fiat was used to
|
|
||||||
# buy some other dst which was furhter used
|
|
||||||
# to buy another dst..)
|
|
||||||
log.warning(
|
|
||||||
f'No src fiat {src} found in {bsuid}?'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
likely_dst = bsuid[:src_name_start]
|
|
||||||
if likely_dst == dst:
|
|
||||||
return bsuid
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
from pprint import pformat
|
|
||||||
|
|
||||||
args = sys.argv
|
|
||||||
assert len(args) > 1, 'Specifiy account(s) from `brokers.toml`'
|
|
||||||
args = args[1:]
|
|
||||||
for acctid in args:
|
|
||||||
broker, name = acctid.split('.')
|
|
||||||
trans, updated_pps = load_pps_from_ledger(broker, name)
|
|
||||||
print(
|
|
||||||
f'Processing transactions into pps for {broker}:{acctid}\n'
|
|
||||||
f'{pformat(trans)}\n\n'
|
|
||||||
f'{pformat(updated_pps)}'
|
|
||||||
)
|
|
|
@ -1,125 +0,0 @@
|
||||||
# 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 <https://www.gnu.org/licenses/>.
|
|
||||||
from __future__ import annotations
|
|
||||||
from contextlib import contextmanager as cm
|
|
||||||
import os
|
|
||||||
from os import path
|
|
||||||
import time
|
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Iterator,
|
|
||||||
Union,
|
|
||||||
Generator
|
|
||||||
)
|
|
||||||
|
|
||||||
from pendulum import (
|
|
||||||
datetime,
|
|
||||||
)
|
|
||||||
import tomli
|
|
||||||
import toml
|
|
||||||
|
|
||||||
from .. import config
|
|
||||||
from ..data._source import Symbol
|
|
||||||
from ..data.types import Struct
|
|
||||||
from ..log import get_logger
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@cm
|
|
||||||
def open_trade_ledger(
|
|
||||||
broker: str,
|
|
||||||
account: str,
|
|
||||||
|
|
||||||
) -> Generator[dict, None, None]:
|
|
||||||
'''
|
|
||||||
Indempotently create and read in a trade log file from the
|
|
||||||
``<configuration_dir>/ledgers/`` directory.
|
|
||||||
|
|
||||||
Files are named per broker account of the form
|
|
||||||
``<brokername>_<accountname>.toml``. The ``accountname`` here is the
|
|
||||||
name as defined in the user's ``brokers.toml`` config.
|
|
||||||
|
|
||||||
'''
|
|
||||||
ldir = path.join(config._config_dir, 'ledgers')
|
|
||||||
if not path.isdir(ldir):
|
|
||||||
os.makedirs(ldir)
|
|
||||||
|
|
||||||
fname = f'trades_{broker}_{account}.toml'
|
|
||||||
tradesfile = path.join(ldir, fname)
|
|
||||||
|
|
||||||
if not path.isfile(tradesfile):
|
|
||||||
log.info(
|
|
||||||
f'Creating new local trades ledger: {tradesfile}'
|
|
||||||
)
|
|
||||||
with open(tradesfile, 'w') as cf:
|
|
||||||
pass # touch
|
|
||||||
with open(tradesfile, 'rb') as cf:
|
|
||||||
start = time.time()
|
|
||||||
ledger = tomli.load(cf)
|
|
||||||
log.info(f'Ledger load took {time.time() - start}s')
|
|
||||||
cpy = ledger.copy()
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield cpy
|
|
||||||
finally:
|
|
||||||
if cpy != ledger:
|
|
||||||
|
|
||||||
# TODO: show diff output?
|
|
||||||
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries
|
|
||||||
log.info(f'Updating ledger for {tradesfile}:\n')
|
|
||||||
ledger.update(cpy)
|
|
||||||
|
|
||||||
# we write on close the mutated ledger data
|
|
||||||
with open(tradesfile, 'w') as cf:
|
|
||||||
toml.dump(ledger, cf)
|
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Struct, frozen=True):
|
|
||||||
# TODO: should this be ``.to`` (see below)?
|
|
||||||
fqsn: str
|
|
||||||
|
|
||||||
sym: Symbol
|
|
||||||
tid: Union[str, int] # unique transaction id
|
|
||||||
size: float
|
|
||||||
price: float
|
|
||||||
cost: float # commisions or other additional costs
|
|
||||||
dt: datetime
|
|
||||||
expiry: datetime | None = None
|
|
||||||
|
|
||||||
# optional key normally derived from the broker
|
|
||||||
# backend which ensures the instrument-symbol this record
|
|
||||||
# is for is truly unique.
|
|
||||||
bsuid: Union[str, int] | None = None
|
|
||||||
|
|
||||||
# optional fqsn for the source "asset"/money symbol?
|
|
||||||
# from: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
def iter_by_dt(
|
|
||||||
clears: dict[str, Any],
|
|
||||||
) -> Iterator[tuple[str, dict]]:
|
|
||||||
'''
|
|
||||||
Iterate entries of a ``clears: dict`` table sorted by entry recorded
|
|
||||||
datetime presumably set at the ``'dt'`` field in each entry.
|
|
||||||
|
|
||||||
'''
|
|
||||||
for tid, data in sorted(
|
|
||||||
list(clears.items()),
|
|
||||||
key=lambda item: item[1]['dt'],
|
|
||||||
):
|
|
||||||
yield tid, data
|
|
|
@ -24,10 +24,6 @@ import subprocess
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
from piker.log import get_logger
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
_reset_tech: Literal[
|
_reset_tech: Literal[
|
||||||
'vnc',
|
'vnc',
|
||||||
|
@ -138,7 +134,6 @@ def i3ipc_xdotool_manual_click_hack() -> None:
|
||||||
# 'IB', # gw running in i3 (newer version?)
|
# 'IB', # gw running in i3 (newer version?)
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
|
||||||
for name in win_names:
|
for name in win_names:
|
||||||
results = t.find_titled(name)
|
results = t.find_titled(name)
|
||||||
print(f'results for {name}: {results}')
|
print(f'results for {name}: {results}')
|
||||||
|
@ -167,8 +162,8 @@ def i3ipc_xdotool_manual_click_hack() -> None:
|
||||||
'xdotool',
|
'xdotool',
|
||||||
'windowactivate', '--sync', win_id,
|
'windowactivate', '--sync', win_id,
|
||||||
|
|
||||||
# move mouse to bottom left of window (where
|
# move mouse to bottom left of window (where there should
|
||||||
# there should be nothing to click).
|
# be nothing to click).
|
||||||
'mousemove_relative', '--sync', str(w-4), str(h-4),
|
'mousemove_relative', '--sync', str(w-4), str(h-4),
|
||||||
|
|
||||||
# NOTE: we may need to stick a `--retry 3` in here..
|
# NOTE: we may need to stick a `--retry 3` in here..
|
||||||
|
@ -182,10 +177,11 @@ def i3ipc_xdotool_manual_click_hack() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
# re-activate and focus original window
|
# re-activate and focus original window
|
||||||
|
try:
|
||||||
subprocess.call([
|
subprocess.call([
|
||||||
'xdotool',
|
'xdotool',
|
||||||
'windowactivate', '--sync', str(orig_win_id),
|
'windowactivate', '--sync', str(orig_win_id),
|
||||||
'click', '--window', str(orig_win_id), '1',
|
'click', '--window', str(orig_win_id), '1',
|
||||||
])
|
])
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
log.exception('xdotool timed out?')
|
log.exception(f'xdotool timed out?')
|
||||||
|
|
|
@ -51,7 +51,7 @@ from ib_insync.objects import Position as IbPosition
|
||||||
import pendulum
|
import pendulum
|
||||||
|
|
||||||
from piker import config
|
from piker import config
|
||||||
from piker.accounting import (
|
from piker.pp import (
|
||||||
Position,
|
Position,
|
||||||
Transaction,
|
Transaction,
|
||||||
open_trade_ledger,
|
open_trade_ledger,
|
||||||
|
@ -1153,7 +1153,7 @@ 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.
|
||||||
asset_type: str = record.get('assetCategory') or record.get('secType', 'STK')
|
asset_type: str = record.get('assetCategory') or record['secType']
|
||||||
|
|
||||||
# TODO: XXX: WOA this is kinda hacky.. probably
|
# TODO: XXX: WOA this is kinda hacky.. probably
|
||||||
# should figure out the correct future pair key more
|
# should figure out the correct future pair key more
|
||||||
|
|
|
@ -20,7 +20,6 @@ Kraken web API wrapping.
|
||||||
'''
|
'''
|
||||||
from contextlib import asynccontextmanager as acm
|
from contextlib import asynccontextmanager as acm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
|
||||||
import itertools
|
import itertools
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
@ -49,7 +48,7 @@ from piker.brokers._util import (
|
||||||
BrokerError,
|
BrokerError,
|
||||||
DataThrottle,
|
DataThrottle,
|
||||||
)
|
)
|
||||||
from piker.accounting import Transaction
|
from piker.pp import Transaction
|
||||||
from . import log
|
from . import log
|
||||||
|
|
||||||
# <uri>/<version>/
|
# <uri>/<version>/
|
||||||
|
@ -249,9 +248,6 @@ class Client:
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
by_bsuid = resp['result']
|
by_bsuid = resp['result']
|
||||||
|
|
||||||
# TODO: we need to pull out the "asset" decimals
|
|
||||||
# data and return a `decimal.Decimal` instead here!
|
|
||||||
return {
|
return {
|
||||||
self._atable[sym].lower(): float(bal)
|
self._atable[sym].lower(): float(bal)
|
||||||
for sym, bal in by_bsuid.items()
|
for sym, bal in by_bsuid.items()
|
||||||
|
|
|
@ -21,6 +21,7 @@ Order api and machinery
|
||||||
from collections import ChainMap, defaultdict
|
from collections import ChainMap, defaultdict
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
|
contextmanager as cm,
|
||||||
)
|
)
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
@ -40,18 +41,14 @@ import pendulum
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
from piker.accounting import (
|
from piker.pp import (
|
||||||
Position,
|
Position,
|
||||||
PpTable,
|
PpTable,
|
||||||
Transaction,
|
Transaction,
|
||||||
open_trade_ledger,
|
open_trade_ledger,
|
||||||
open_pps,
|
open_pps,
|
||||||
get_likely_pair,
|
|
||||||
)
|
|
||||||
from piker.data._source import (
|
|
||||||
Symbol,
|
|
||||||
digits_to_dec,
|
|
||||||
)
|
)
|
||||||
|
from piker.data._source import Symbol
|
||||||
from piker.clearing._messages import (
|
from piker.clearing._messages import (
|
||||||
Order,
|
Order,
|
||||||
Status,
|
Status,
|
||||||
|
@ -473,14 +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
|
||||||
|
@ -499,10 +494,7 @@ async def trades_dialogue(
|
||||||
# what amount of trades-transactions need
|
# what amount of trades-transactions need
|
||||||
# to be reloaded.
|
# to be reloaded.
|
||||||
balances = await client.get_balances()
|
balances = await client.get_balances()
|
||||||
# await tractor.breakpoint()
|
|
||||||
|
|
||||||
for dst, size in balances.items():
|
for dst, size in balances.items():
|
||||||
|
|
||||||
# we don't care about tracking positions
|
# we don't care about tracking positions
|
||||||
# in the user's source fiat currency.
|
# in the user's source fiat currency.
|
||||||
if (
|
if (
|
||||||
|
@ -516,20 +508,45 @@ async def trades_dialogue(
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
def get_likely_pair(
|
||||||
|
dst: str,
|
||||||
|
bsuid: str,
|
||||||
|
src_fiat: str = src_fiat
|
||||||
|
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Attempt to get the likely trading pair masting
|
||||||
|
a given destination asset `dst: str`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
src_name_start = bsuid.rindex(src_fiat)
|
||||||
|
except (
|
||||||
|
ValueError, # substr not found
|
||||||
|
):
|
||||||
|
# TODO: handle nested positions..(i.e.
|
||||||
|
# positions where the src fiat was used to
|
||||||
|
# buy some other dst which was furhter used
|
||||||
|
# to buy another dst..)
|
||||||
|
log.warning(
|
||||||
|
f'No src fiat {src_fiat} found in {bsuid}?'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
likely_dst = bsuid[:src_name_start]
|
||||||
|
if likely_dst == dst:
|
||||||
|
return bsuid
|
||||||
|
|
||||||
def has_pp(
|
def has_pp(
|
||||||
dst: str,
|
dst: str,
|
||||||
size: float,
|
size: float,
|
||||||
|
|
||||||
) -> Position | None:
|
) -> Position | bool:
|
||||||
|
|
||||||
src2dst: dict[str, str] = {}
|
src2dst: dict[str, str] = {}
|
||||||
|
|
||||||
for bsuid in table.pps:
|
for bsuid in table.pps:
|
||||||
likely_pair = get_likely_pair(
|
likely_pair = get_likely_pair(dst, bsuid)
|
||||||
src_fiat,
|
|
||||||
dst,
|
|
||||||
bsuid,
|
|
||||||
)
|
|
||||||
if likely_pair:
|
if likely_pair:
|
||||||
src2dst[src_fiat] = dst
|
src2dst[src_fiat] = dst
|
||||||
|
|
||||||
|
@ -557,7 +574,7 @@ async def trades_dialogue(
|
||||||
)
|
)
|
||||||
return pp
|
return pp
|
||||||
|
|
||||||
return None # signal no entry
|
return False
|
||||||
|
|
||||||
pos = has_pp(dst, size)
|
pos = has_pp(dst, size)
|
||||||
if not pos:
|
if not pos:
|
||||||
|
@ -585,11 +602,7 @@ async def trades_dialogue(
|
||||||
# yet and thus this likely pair grabber will
|
# yet and thus this likely pair grabber will
|
||||||
# likely fail.
|
# likely fail.
|
||||||
for bsuid in table.pps:
|
for bsuid in table.pps:
|
||||||
likely_pair = get_likely_pair(
|
likely_pair = get_likely_pair(dst, bsuid)
|
||||||
src_fiat,
|
|
||||||
dst,
|
|
||||||
bsuid,
|
|
||||||
)
|
|
||||||
if likely_pair:
|
if likely_pair:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -711,8 +724,8 @@ async def handle_order_updates(
|
||||||
'''
|
'''
|
||||||
Main msg handling loop for all things order management.
|
Main msg handling loop for all things order management.
|
||||||
|
|
||||||
This code is broken out to make the context explicit and state
|
This code is broken out to make the context explicit and state variables
|
||||||
variables defined in the signature clear to the reader.
|
defined in the signature clear to the reader.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async for msg in ws_stream:
|
async for msg in ws_stream:
|
||||||
|
@ -1191,13 +1204,7 @@ def norm_trade_records(
|
||||||
fqsn,
|
fqsn,
|
||||||
info={
|
info={
|
||||||
'lot_size_digits': pair_info.lot_decimals,
|
'lot_size_digits': pair_info.lot_decimals,
|
||||||
'lot_tick_size': digits_to_dec(
|
|
||||||
pair_info.lot_decimals,
|
|
||||||
),
|
|
||||||
'tick_size_digits': pair_info.pair_decimals,
|
'tick_size_digits': pair_info.pair_decimals,
|
||||||
'price_tick_size': digits_to_dec(
|
|
||||||
pair_info.pair_decimals,
|
|
||||||
),
|
|
||||||
'asset_type': 'crypto',
|
'asset_type': 'crypto',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ from bidict import bidict
|
||||||
|
|
||||||
from ..data._source import Symbol
|
from ..data._source import Symbol
|
||||||
from ..data.types import Struct
|
from ..data.types import Struct
|
||||||
from ..accounting import Position
|
from ..pp import Position
|
||||||
|
|
||||||
|
|
||||||
_size_units = bidict({
|
_size_units = bidict({
|
||||||
|
|
|
@ -39,7 +39,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 ..data._source import Symbol
|
||||||
from ..accounting import (
|
from ..pp import (
|
||||||
Position,
|
Position,
|
||||||
Transaction,
|
Transaction,
|
||||||
open_trade_ledger,
|
open_trade_ledger,
|
||||||
|
@ -58,6 +58,8 @@ from ._messages import (
|
||||||
BrokerdError,
|
BrokerdError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ..config import load
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -90,21 +90,6 @@ def float_digits(
|
||||||
return int(-Decimal(str(value)).as_tuple().exponent)
|
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:
|
def ohlc_zeros(length: int) -> np.ndarray:
|
||||||
"""Construct an OHLC field formatted structarray.
|
"""Construct an OHLC field formatted structarray.
|
||||||
|
|
||||||
|
@ -228,13 +213,10 @@ class Symbol(Struct):
|
||||||
|
|
||||||
return Symbol(
|
return Symbol(
|
||||||
key=symbol,
|
key=symbol,
|
||||||
|
|
||||||
tick_size=tick_size,
|
tick_size=tick_size,
|
||||||
lot_tick_size=lot_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_size),
|
lot_size_digits=float_digits(lot_size),
|
||||||
|
|
||||||
suffix=suffix,
|
suffix=suffix,
|
||||||
broker_info={broker: info},
|
broker_info={broker: info},
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,18 +14,20 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Personal/Private position parsing, calculating, summarizing in a way
|
Personal/Private position parsing, calculating, summarizing in a way
|
||||||
that doesn't try to cuk most humans who prefer to not lose their moneys..
|
that doesn't try to cuk most humans who prefer to not lose their moneys..
|
||||||
|
|
||||||
(looking at you `ib` and dirt-bird friends)
|
(looking at you `ib` and dirt-bird friends)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import contextmanager as cm
|
from contextlib import contextmanager as cm
|
||||||
|
from pprint import pformat
|
||||||
|
import os
|
||||||
|
from os import path
|
||||||
from math import copysign
|
from math import copysign
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Iterator,
|
Iterator,
|
||||||
|
@ -36,23 +38,104 @@ from typing import (
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
from pendulum import datetime, now
|
from pendulum import datetime, now
|
||||||
|
import tomli
|
||||||
import toml
|
import toml
|
||||||
|
|
||||||
from ._ledger import (
|
from . import config
|
||||||
Transaction,
|
from .brokers import get_brokermod
|
||||||
iter_by_dt,
|
from .clearing._messages import BrokerdPosition, Status
|
||||||
open_trade_ledger,
|
from .data._source import Symbol, unpack_fqsn
|
||||||
)
|
from .log import get_logger
|
||||||
from .. import config
|
from .data.types import Struct
|
||||||
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
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@cm
|
||||||
|
def open_trade_ledger(
|
||||||
|
broker: str,
|
||||||
|
account: str,
|
||||||
|
|
||||||
|
) -> Generator[dict, None, None]:
|
||||||
|
'''
|
||||||
|
Indempotently create and read in a trade log file from the
|
||||||
|
``<configuration_dir>/ledgers/`` directory.
|
||||||
|
|
||||||
|
Files are named per broker account of the form
|
||||||
|
``<brokername>_<accountname>.toml``. The ``accountname`` here is the
|
||||||
|
name as defined in the user's ``brokers.toml`` config.
|
||||||
|
|
||||||
|
'''
|
||||||
|
ldir = path.join(config._config_dir, 'ledgers')
|
||||||
|
if not path.isdir(ldir):
|
||||||
|
os.makedirs(ldir)
|
||||||
|
|
||||||
|
fname = f'trades_{broker}_{account}.toml'
|
||||||
|
tradesfile = path.join(ldir, fname)
|
||||||
|
|
||||||
|
if not path.isfile(tradesfile):
|
||||||
|
log.info(
|
||||||
|
f'Creating new local trades ledger: {tradesfile}'
|
||||||
|
)
|
||||||
|
with open(tradesfile, 'w') as cf:
|
||||||
|
pass # touch
|
||||||
|
with open(tradesfile, 'rb') as cf:
|
||||||
|
start = time.time()
|
||||||
|
ledger = tomli.load(cf)
|
||||||
|
log.info(f'Ledger load took {time.time() - start}s')
|
||||||
|
cpy = ledger.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield cpy
|
||||||
|
finally:
|
||||||
|
if cpy != ledger:
|
||||||
|
|
||||||
|
# TODO: show diff output?
|
||||||
|
# https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries
|
||||||
|
log.info(f'Updating ledger for {tradesfile}:\n')
|
||||||
|
ledger.update(cpy)
|
||||||
|
|
||||||
|
# we write on close the mutated ledger data
|
||||||
|
with open(tradesfile, 'w') as cf:
|
||||||
|
toml.dump(ledger, cf)
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(Struct, frozen=True):
|
||||||
|
# TODO: should this be ``.to`` (see below)?
|
||||||
|
fqsn: str
|
||||||
|
|
||||||
|
sym: Symbol
|
||||||
|
tid: Union[str, int] # unique transaction id
|
||||||
|
size: float
|
||||||
|
price: float
|
||||||
|
cost: float # commisions or other additional costs
|
||||||
|
dt: datetime
|
||||||
|
expiry: datetime | None = None
|
||||||
|
|
||||||
|
# optional key normally derived from the broker
|
||||||
|
# backend which ensures the instrument-symbol this record
|
||||||
|
# is for is truly unique.
|
||||||
|
bsuid: Union[str, int] | None = None
|
||||||
|
|
||||||
|
# optional fqsn for the source "asset"/money symbol?
|
||||||
|
# from: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def iter_by_dt(
|
||||||
|
clears: dict[str, Any],
|
||||||
|
) -> Iterator[tuple[str, dict]]:
|
||||||
|
'''
|
||||||
|
Iterate entries of a ``clears: dict`` table sorted by entry recorded
|
||||||
|
datetime presumably set at the ``'dt'`` field in each entry.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for tid, data in sorted(
|
||||||
|
list(clears.items()),
|
||||||
|
key=lambda item: item[1]['dt'],
|
||||||
|
):
|
||||||
|
yield tid, data
|
||||||
|
|
||||||
|
|
||||||
class Position(Struct):
|
class Position(Struct):
|
||||||
'''
|
'''
|
||||||
Basic pp (personal/piker position) model with attached clearing
|
Basic pp (personal/piker position) model with attached clearing
|
||||||
|
@ -401,9 +484,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 float(
|
return float(self.symbol.quantize_size(size))
|
||||||
self.symbol.quantize_size(size),
|
|
||||||
)
|
|
||||||
|
|
||||||
def minimize_clears(
|
def minimize_clears(
|
||||||
self,
|
self,
|
||||||
|
@ -483,13 +564,9 @@ class PpTable(Struct):
|
||||||
pps = self.pps
|
pps = self.pps
|
||||||
updated: dict[str, Position] = {}
|
updated: dict[str, Position] = {}
|
||||||
|
|
||||||
# lifo update all pps from records, ensuring
|
# lifo update all pps from records
|
||||||
# we compute the PPU and size sorted in time!
|
for tid, t in trans.items():
|
||||||
for t in sorted(
|
|
||||||
trans.values(),
|
|
||||||
key=lambda t: t.dt,
|
|
||||||
reverse=True,
|
|
||||||
):
|
|
||||||
pp = pps.setdefault(
|
pp = pps.setdefault(
|
||||||
t.bsuid,
|
t.bsuid,
|
||||||
|
|
||||||
|
@ -513,10 +590,7 @@ class PpTable(Struct):
|
||||||
# included in the current pps state.
|
# included in the current pps state.
|
||||||
if (
|
if (
|
||||||
t.tid in clears
|
t.tid in clears
|
||||||
or (
|
or first_clear_dt and t.dt < first_clear_dt
|
||||||
first_clear_dt
|
|
||||||
and t.dt < first_clear_dt
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
# NOTE: likely you'll see repeats of the same
|
# NOTE: likely you'll see repeats of the same
|
||||||
# ``Transaction`` passed in here if/when you are restarting
|
# ``Transaction`` passed in here if/when you are restarting
|
||||||
|
@ -533,8 +607,6 @@ class PpTable(Struct):
|
||||||
for bsuid, pp in updated.items():
|
for bsuid, pp in updated.items():
|
||||||
pp.ensure_state()
|
pp.ensure_state()
|
||||||
|
|
||||||
# deliver only the position entries that were actually updated
|
|
||||||
# (modified the state) from the input transaction set.
|
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
def dump_active(
|
def dump_active(
|
||||||
|
@ -629,10 +701,8 @@ class PpTable(Struct):
|
||||||
# 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:
|
if pp_entries:
|
||||||
log.info(
|
log.info(f'Updating ``pps.toml`` for {path}:\n')
|
||||||
f'Updating ``pps.toml``:\n'
|
log.info(f'Current positions:\n{pp_entries}')
|
||||||
f'Current positions:\n{pp_entries}'
|
|
||||||
)
|
|
||||||
self.conf[self.brokername][self.acctid] = pp_entries
|
self.conf[self.brokername][self.acctid] = pp_entries
|
||||||
|
|
||||||
elif (
|
elif (
|
||||||
|
@ -959,3 +1029,19 @@ def open_pps(
|
||||||
finally:
|
finally:
|
||||||
if write_on_exit:
|
if write_on_exit:
|
||||||
table.write_config()
|
table.write_config()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
args = sys.argv
|
||||||
|
assert len(args) > 1, 'Specifiy account(s) from `brokers.toml`'
|
||||||
|
args = args[1:]
|
||||||
|
for acctid in args:
|
||||||
|
broker, name = acctid.split('.')
|
||||||
|
trans, updated_pps = load_pps_from_ledger(broker, name)
|
||||||
|
print(
|
||||||
|
f'Processing transactions into pps for {broker}:{acctid}\n'
|
||||||
|
f'{pformat(trans)}\n\n'
|
||||||
|
f'{pformat(updated_pps)}'
|
||||||
|
)
|
|
@ -47,7 +47,7 @@ from ..calc import (
|
||||||
puterize,
|
puterize,
|
||||||
)
|
)
|
||||||
from ..clearing._allocate import Allocator
|
from ..clearing._allocate import Allocator
|
||||||
from ..accounting import Position
|
from ..pp import Position
|
||||||
from ..data._normalize import iterticks
|
from ..data._normalize import iterticks
|
||||||
from ..data.feed import (
|
from ..data.feed import (
|
||||||
Feed,
|
Feed,
|
||||||
|
|
|
@ -37,7 +37,7 @@ import trio
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
from .. import config
|
from .. import config
|
||||||
from ..accounting import Position
|
from ..pp import Position
|
||||||
from ..clearing._client import open_ems, OrderBook
|
from ..clearing._client import open_ems, OrderBook
|
||||||
from ..clearing._allocate import (
|
from ..clearing._allocate import (
|
||||||
mk_allocator,
|
mk_allocator,
|
||||||
|
|
|
@ -16,7 +16,7 @@ from functools import partial
|
||||||
|
|
||||||
from piker.log import get_logger
|
from piker.log import get_logger
|
||||||
from piker.clearing._messages import Order
|
from piker.clearing._messages import Order
|
||||||
from piker.accounting import (
|
from piker.pp import (
|
||||||
open_pps,
|
open_pps,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue