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
from piker.log import get_logger
log = get_logger(__name__)
_reset_tech: Literal[
'vnc',
@ -138,54 +134,54 @@ def i3ipc_xdotool_manual_click_hack() -> None:
# 'IB', # gw running in i3 (newer version?)
]
try:
for name in win_names:
results = t.find_titled(name)
print(f'results for {name}: {results}')
if results:
con = results[0]
print(f'Resetting data feed for {name}')
win_id = str(con.window)
w, h = con.rect.width, con.rect.height
for name in win_names:
results = t.find_titled(name)
print(f'results for {name}: {results}')
if results:
con = results[0]
print(f'Resetting data feed for {name}')
win_id = str(con.window)
w, h = con.rect.width, con.rect.height
# TODO: seems to be a few libs for python but not sure
# if they support all the sub commands we need, order of
# most recent commit history:
# https://github.com/rr-/pyxdotool
# https://github.com/ShaneHutter/pyxdotool
# https://github.com/cphyc/pyxdotool
# TODO: seems to be a few libs for python but not sure
# if they support all the sub commands we need, order of
# most recent commit history:
# https://github.com/rr-/pyxdotool
# https://github.com/ShaneHutter/pyxdotool
# https://github.com/cphyc/pyxdotool
# TODO: only run the reconnect (2nd) kc on a detected
# disconnect?
for key_combo, timeout in [
# only required if we need a connection reset.
# ('ctrl+alt+r', 12),
# data feed reset.
('ctrl+alt+f', 6)
]:
subprocess.call([
'xdotool',
'windowactivate', '--sync', win_id,
# TODO: only run the reconnect (2nd) kc on a detected
# disconnect?
for key_combo, timeout in [
# only required if we need a connection reset.
# ('ctrl+alt+r', 12),
# data feed reset.
('ctrl+alt+f', 6)
]:
subprocess.call([
'xdotool',
'windowactivate', '--sync', win_id,
# move mouse to bottom left of window (where
# there should be nothing to click).
'mousemove_relative', '--sync', str(w-4), str(h-4),
# move mouse to bottom left of window (where there should
# be nothing to click).
'mousemove_relative', '--sync', str(w-4), str(h-4),
# NOTE: we may need to stick a `--retry 3` in here..
'click', '--window', win_id,
'--repeat', '3', '1',
# NOTE: we may need to stick a `--retry 3` in here..
'click', '--window', win_id,
'--repeat', '3', '1',
# hackzorzes
'key', key_combo,
],
timeout=timeout,
)
# hackzorzes
'key', key_combo,
],
timeout=timeout,
)
# re-activate and focus original window
try:
subprocess.call([
'xdotool',
'windowactivate', '--sync', str(orig_win_id),
'click', '--window', str(orig_win_id), '1',
])
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
from piker import config
from piker.accounting import (
from piker.pp import (
Position,
Transaction,
open_trade_ledger,
@ -1153,7 +1153,7 @@ def norm_trade_records(
# special handling of symbol extraction from
# 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
# 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 datetime import datetime
from decimal import Decimal
import itertools
from typing import (
Any,
@ -49,7 +48,7 @@ from piker.brokers._util import (
BrokerError,
DataThrottle,
)
from piker.accounting import Transaction
from piker.pp import Transaction
from . import log
# <uri>/<version>/
@ -249,9 +248,6 @@ class Client:
{},
)
by_bsuid = resp['result']
# TODO: we need to pull out the "asset" decimals
# data and return a `decimal.Decimal` instead here!
return {
self._atable[sym].lower(): float(bal)
for sym, bal in by_bsuid.items()

View File

@ -21,6 +21,7 @@ Order api and machinery
from collections import ChainMap, defaultdict
from contextlib import (
asynccontextmanager as acm,
contextmanager as cm,
)
from functools import partial
from itertools import count
@ -40,18 +41,14 @@ import pendulum
import trio
import tractor
from piker.accounting import (
from piker.pp import (
Position,
PpTable,
Transaction,
open_trade_ledger,
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 (
Order,
Status,
@ -473,14 +470,12 @@ async def trades_dialogue(
with (
open_pps(
'kraken',
acctid,
write_on_exit=True,
acctid
) as table,
open_trade_ledger(
'kraken',
acctid,
acctid
) as ledger_dict,
):
# transaction-ify the ledger entries
@ -499,10 +494,7 @@ async def trades_dialogue(
# what amount of trades-transactions need
# to be reloaded.
balances = await client.get_balances()
# await tractor.breakpoint()
for dst, size in balances.items():
# we don't care about tracking positions
# in the user's source fiat currency.
if (
@ -516,20 +508,45 @@ async def trades_dialogue(
)
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(
dst: str,
size: float,
) -> Position | None:
) -> Position | bool:
src2dst: dict[str, str] = {}
for bsuid in table.pps:
likely_pair = get_likely_pair(
src_fiat,
dst,
bsuid,
)
likely_pair = get_likely_pair(dst, bsuid)
if likely_pair:
src2dst[src_fiat] = dst
@ -557,7 +574,7 @@ async def trades_dialogue(
)
return pp
return None # signal no entry
return False
pos = has_pp(dst, size)
if not pos:
@ -585,11 +602,7 @@ async def trades_dialogue(
# yet and thus this likely pair grabber will
# likely fail.
for bsuid in table.pps:
likely_pair = get_likely_pair(
src_fiat,
dst,
bsuid,
)
likely_pair = get_likely_pair(dst, bsuid)
if likely_pair:
break
else:
@ -711,8 +724,8 @@ async def handle_order_updates(
'''
Main msg handling loop for all things order management.
This code is broken out to make the context explicit and state
variables defined in the signature clear to the reader.
This code is broken out to make the context explicit and state variables
defined in the signature clear to the reader.
'''
async for msg in ws_stream:
@ -1191,13 +1204,7 @@ def norm_trade_records(
fqsn,
info={
'lot_size_digits': pair_info.lot_decimals,
'lot_tick_size': digits_to_dec(
pair_info.lot_decimals,
),
'tick_size_digits': pair_info.pair_decimals,
'price_tick_size': digits_to_dec(
pair_info.pair_decimals,
),
'asset_type': 'crypto',
},
)

View File

@ -25,7 +25,7 @@ from bidict import bidict
from ..data._source import Symbol
from ..data.types import Struct
from ..accounting import Position
from ..pp import Position
_size_units = bidict({

View File

@ -39,7 +39,7 @@ import tractor
from .. import data
from ..data.types import Struct
from ..data._source import Symbol
from ..accounting import (
from ..pp import (
Position,
Transaction,
open_trade_ledger,
@ -58,6 +58,8 @@ from ._messages import (
BrokerdError,
)
from ..config import load
log = get_logger(__name__)

View File

@ -90,21 +90,6 @@ def float_digits(
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:
"""Construct an OHLC field formatted structarray.
@ -228,13 +213,10 @@ class Symbol(Struct):
return Symbol(
key=symbol,
tick_size=tick_size,
lot_tick_size=lot_size,
tick_size_digits=float_digits(tick_size),
lot_size_digits=float_digits(lot_size),
suffix=suffix,
broker_info={broker: info},
)

View File

@ -14,18 +14,20 @@
# 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/>.
'''
Personal/Private position parsing, calculating, summarizing in a way
that doesn't try to cuk most humans who prefer to not lose their moneys..
(looking at you `ib` and dirt-bird friends)
'''
from __future__ import annotations
from contextlib import contextmanager as cm
from pprint import pformat
import os
from os import path
from math import copysign
import re
import time
from typing import (
Any,
Iterator,
@ -36,23 +38,104 @@ from typing import (
import pendulum
from pendulum import datetime, now
import tomli
import toml
from ._ledger import (
Transaction,
iter_by_dt,
open_trade_ledger,
)
from .. import config
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
from . import config
from .brokers import get_brokermod
from .clearing._messages import BrokerdPosition, Status
from .data._source import Symbol, unpack_fqsn
from .log import get_logger
from .data.types import Struct
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):
'''
Basic pp (personal/piker position) model with attached clearing
@ -401,9 +484,7 @@ class Position(Struct):
if self.split_ratio is not None:
size = round(size * self.split_ratio)
return float(
self.symbol.quantize_size(size),
)
return float(self.symbol.quantize_size(size))
def minimize_clears(
self,
@ -483,13 +564,9 @@ class PpTable(Struct):
pps = self.pps
updated: dict[str, Position] = {}
# lifo update all pps from records, ensuring
# we compute the PPU and size sorted in time!
for t in sorted(
trans.values(),
key=lambda t: t.dt,
reverse=True,
):
# lifo update all pps from records
for tid, t in trans.items():
pp = pps.setdefault(
t.bsuid,
@ -513,10 +590,7 @@ class PpTable(Struct):
# included in the current pps state.
if (
t.tid in clears
or (
first_clear_dt
and t.dt < first_clear_dt
)
or first_clear_dt and t.dt < first_clear_dt
):
# NOTE: likely you'll see repeats of the same
# ``Transaction`` passed in here if/when you are restarting
@ -533,8 +607,6 @@ class PpTable(Struct):
for bsuid, pp in updated.items():
pp.ensure_state()
# deliver only the position entries that were actually updated
# (modified the state) from the input transaction set.
return updated
def dump_active(
@ -629,10 +701,8 @@ class PpTable(Struct):
# active, closed_pp_objs = table.dump_active()
pp_entries = self.to_toml()
if pp_entries:
log.info(
f'Updating ``pps.toml``:\n'
f'Current positions:\n{pp_entries}'
)
log.info(f'Updating ``pps.toml`` for {path}:\n')
log.info(f'Current positions:\n{pp_entries}')
self.conf[self.brokername][self.acctid] = pp_entries
elif (
@ -959,3 +1029,19 @@ def open_pps(
finally:
if write_on_exit:
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,
)
from ..clearing._allocate import Allocator
from ..accounting import Position
from ..pp import Position
from ..data._normalize import iterticks
from ..data.feed import (
Feed,

View File

@ -37,7 +37,7 @@ import trio
from PyQt5.QtCore import Qt
from .. import config
from ..accounting import Position
from ..pp import Position
from ..clearing._client import open_ems, OrderBook
from ..clearing._allocate import (
mk_allocator,

View File

@ -16,7 +16,7 @@ from functools import partial
from piker.log import get_logger
from piker.clearing._messages import Order
from piker.accounting import (
from piker.pp import (
open_pps,
)