Compare commits

..

No commits in common. "d649749e7da6de171aca6be356febd04875b2b48" and "eb51033b18586c397bb0e4a2fd8703ac30841be2" have entirely different histories.

13 changed files with 207 additions and 355 deletions

View File

@ -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)}'
)

View File

@ -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

View File

@ -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?')

View File

@ -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

View File

@ -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()

View File

@ -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',
}, },
) )

View File

@ -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({

View File

@ -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__)

View File

@ -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},
) )

View File

@ -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)}'
)

View File

@ -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,

View File

@ -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,

View File

@ -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,
) )