Compare commits
97 Commits
a8598b17fb
...
f847ff0797
| Author | SHA1 | Date |
|---|---|---|
|
|
f847ff0797 | |
|
|
60861e0a42 | |
|
|
09e4cb1ad5 | |
|
|
58114849af | |
|
|
583a33f0e4 | |
|
|
169a48801b | |
|
|
7e85554bf4 | |
|
|
8905097140 | |
|
|
0b8096875d | |
|
|
3f0e121731 | |
|
|
2bc8dd0955 | |
|
|
91dc7d5d54 | |
|
|
0edead4a93 | |
|
|
e895e7ea38 | |
|
|
fa55778551 | |
|
|
59f900ca5e | |
|
|
74576f7881 | |
|
|
f9e1266f46 | |
|
|
6c708c409a | |
|
|
53b8a38ac3 | |
|
|
5248a85ce7 | |
|
|
afa985fdd6 | |
|
|
a4af384aa8 | |
|
|
0330c387aa | |
|
|
a988c8f9c3 | |
|
|
d3f30f5be2 | |
|
|
55d759650c | |
|
|
46461c6105 | |
|
|
b0e2db902e | |
|
|
a865bf38a7 | |
|
|
ba91dc6790 | |
|
|
cd3b7d7d57 | |
|
|
935474303e | |
|
|
100e1b2fa6 | |
|
|
751ee3996f | |
|
|
a7bf5a6d40 | |
|
|
4e96161f36 | |
|
|
6a62c7d8a9 | |
|
|
0001f35e17 | |
|
|
2074eeea4f | |
|
|
1799171705 | |
|
|
cffefac615 | |
|
|
fc2d727fdb | |
|
|
05dde42f70 | |
|
|
04e423e6bd | |
|
|
793a454463 | |
|
|
b8b4f1b80f | |
|
|
1cf041d8e6 | |
|
|
831b6cfb21 | |
|
|
e5f7e8de9d | |
|
|
871bb2620e | |
|
|
de980a69e0 | |
|
|
ab9f01caf2 | |
|
|
d85632ba9b | |
|
|
8294ca6487 | |
|
|
87385a4e2d | |
|
|
b3c5478017 | |
|
|
6c9a78c5a0 | |
|
|
da223f7a55 | |
|
|
49fe0a3398 | |
|
|
29fc3b8a8b | |
|
|
1bfe777637 | |
|
|
c694d915f1 | |
|
|
c120cb51a4 | |
|
|
7c20231f16 | |
|
|
d809c79788 | |
|
|
9f2f8a1664 | |
|
|
9f141635d1 | |
|
|
0604ca7c82 | |
|
|
82c2256271 | |
|
|
a743fa28b5 | |
|
|
07fbe859c3 | |
|
|
db0872e350 | |
|
|
878002aee0 | |
|
|
c9e6510535 | |
|
|
4cae3778c1 | |
|
|
ff49ff0376 | |
|
|
b884febd5f | |
|
|
291508a9b1 | |
|
|
7498c221a8 | |
|
|
64828d2fe1 | |
|
|
1e6fa8675d | |
|
|
51fb871f57 | |
|
|
ffd6438b88 | |
|
|
5449141ec4 | |
|
|
5337f8abee | |
|
|
0329a6d852 | |
|
|
ff045f699f | |
|
|
6d6ca1a908 | |
|
|
a00e9c0e64 | |
|
|
cb694700c2 | |
|
|
11c931f65d | |
|
|
60390ae596 | |
|
|
9592735aaa | |
|
|
49841f5b91 | |
|
|
b2827ef3c3 | |
|
|
2fc4ccf011 |
20
README.rst
20
README.rst
|
|
@ -93,13 +93,13 @@ bc why install with `python` when you can faster with `rust` ::
|
||||||
# ^ astral's docs,
|
# ^ astral's docs,
|
||||||
# https://docs.astral.sh/uv/concepts/projects/sync/
|
# https://docs.astral.sh/uv/concepts/projects/sync/
|
||||||
|
|
||||||
include all GUIs ::
|
include all GUIs (ex. for charting)::
|
||||||
|
|
||||||
uv sync --extra uis
|
uv sync --extra uis
|
||||||
|
|
||||||
AND with all our hacking tools::
|
AND with all our hacking tools and WIP integrations::
|
||||||
|
|
||||||
uv sync --dev --extra uis
|
uv sync --dev --all-extras
|
||||||
|
|
||||||
|
|
||||||
Ensure you can run the root-daemon::
|
Ensure you can run the root-daemon::
|
||||||
|
|
@ -107,13 +107,21 @@ Ensure you can run the root-daemon::
|
||||||
uv run pikerd [-l info --pdb]
|
uv run pikerd [-l info --pdb]
|
||||||
|
|
||||||
|
|
||||||
hacky install on nixos
|
install on nix(os)
|
||||||
**********************
|
******************
|
||||||
``NixOS`` is our core devs' distro of choice for which we offer
|
``NixOS`` is our core devs' distro of choice for which we offer
|
||||||
a stringently defined development shell envoirment that can be loaded with::
|
a stringently defined development shell envoirment that can currently
|
||||||
|
be applied in one of 2 ways::
|
||||||
|
|
||||||
|
# ONLY if running on X11
|
||||||
nix-shell default.nix
|
nix-shell default.nix
|
||||||
|
|
||||||
|
Or if you prefer flakes style and a modern DE::
|
||||||
|
|
||||||
|
# ONLY if also running on Wayland
|
||||||
|
nix develop # for default bash
|
||||||
|
nix develop -c uv run xonsh # for @goodboy's preferred sh B)
|
||||||
|
|
||||||
|
|
||||||
start a chart
|
start a chart
|
||||||
*************
|
*************
|
||||||
|
|
|
||||||
37
default.nix
37
default.nix
|
|
@ -11,11 +11,12 @@ let
|
||||||
libxkbcommonStorePath = lib.getLib libxkbcommon;
|
libxkbcommonStorePath = lib.getLib libxkbcommon;
|
||||||
xcbutilcursorStorePath = lib.getLib xcb-util-cursor;
|
xcbutilcursorStorePath = lib.getLib xcb-util-cursor;
|
||||||
|
|
||||||
qtpyStorePath = lib.getLib python312Packages.qtpy;
|
pypkgs = python313Packages;
|
||||||
pyqt6StorePath = lib.getLib python312Packages.pyqt6;
|
qtpyStorePath = lib.getLib pypkgs.qtpy;
|
||||||
pyqt6SipStorePath = lib.getLib python312Packages.pyqt6-sip;
|
pyqt6StorePath = lib.getLib pypkgs.pyqt6;
|
||||||
rapidfuzzStorePath = lib.getLib python312Packages.rapidfuzz;
|
pyqt6SipStorePath = lib.getLib pypkgs.pyqt6-sip;
|
||||||
qdarkstyleStorePath = lib.getLib python312Packages.qdarkstyle;
|
rapidfuzzStorePath = lib.getLib pypkgs.rapidfuzz;
|
||||||
|
qdarkstyleStorePath = lib.getLib pypkgs.qdarkstyle;
|
||||||
|
|
||||||
xorgLibX11StorePath = lib.getLib xorg.libX11;
|
xorgLibX11StorePath = lib.getLib xorg.libX11;
|
||||||
xorgLibxcbStorePath = lib.getLib xorg.libxcb;
|
xorgLibxcbStorePath = lib.getLib xorg.libxcb;
|
||||||
|
|
@ -51,12 +52,12 @@ stdenv.mkDerivation {
|
||||||
xorg.xcbutilrenderutil
|
xorg.xcbutilrenderutil
|
||||||
|
|
||||||
# Python requirements.
|
# Python requirements.
|
||||||
python312Full
|
python313
|
||||||
python312Packages.uv
|
uv
|
||||||
python312Packages.qdarkstyle
|
pypkgs.qdarkstyle
|
||||||
python312Packages.rapidfuzz
|
pypkgs.rapidfuzz
|
||||||
python312Packages.pyqt6
|
pypkgs.pyqt6
|
||||||
python312Packages.qtpy
|
pypkgs.qtpy
|
||||||
];
|
];
|
||||||
src = null;
|
src = null;
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|
@ -113,11 +114,11 @@ stdenv.mkDerivation {
|
||||||
|
|
||||||
export LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH
|
||||||
|
|
||||||
RPDFUZZ_PATH="${rapidfuzzStorePath}/lib/python3.12/site-packages"
|
RPDFUZZ_PATH="${rapidfuzzStorePath}/lib/python3.13/site-packages"
|
||||||
QDRKSTYLE_PATH="${qdarkstyleStorePath}/lib/python3.12/site-packages"
|
QDRKSTYLE_PATH="${qdarkstyleStorePath}/lib/python3.13/site-packages"
|
||||||
QTPY_PATH="${qtpyStorePath}/lib/python3.12/site-packages"
|
QTPY_PATH="${qtpyStorePath}/lib/python3.13/site-packages"
|
||||||
PYQT6_PATH="${pyqt6StorePath}/lib/python3.12/site-packages"
|
PYQT6_PATH="${pyqt6StorePath}/lib/python3.13/site-packages"
|
||||||
PYQT6_SIP_PATH="${pyqt6SipStorePath}/lib/python3.12/site-packages"
|
PYQT6_SIP_PATH="${pyqt6SipStorePath}/lib/python3.13/site-packages"
|
||||||
|
|
||||||
PATCH="$PATCH:$RPDFUZZ_PATH"
|
PATCH="$PATCH:$RPDFUZZ_PATH"
|
||||||
PATCH="$PATCH:$QDRKSTYLE_PATH"
|
PATCH="$PATCH:$QDRKSTYLE_PATH"
|
||||||
|
|
@ -127,8 +128,8 @@ stdenv.mkDerivation {
|
||||||
|
|
||||||
export PATCH
|
export PATCH
|
||||||
|
|
||||||
# Install deps
|
# install all dev and extras
|
||||||
uv lock
|
uv sync --dev --all-extras
|
||||||
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,338 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
from decimal import (
|
|
||||||
Decimal,
|
|
||||||
)
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
# import polars as pl
|
|
||||||
import trio
|
|
||||||
import tractor
|
|
||||||
from datetime import datetime
|
|
||||||
# from pprint import pformat
|
|
||||||
from piker.brokers.deribit.api import (
|
|
||||||
get_client,
|
|
||||||
maybe_open_oi_feed,
|
|
||||||
)
|
|
||||||
from piker.storage import open_storage_client, StorageClient
|
|
||||||
from piker.log import get_logger
|
|
||||||
import sys
|
|
||||||
import pyqtgraph as pg
|
|
||||||
from PyQt6 import QtCore
|
|
||||||
from pyqtgraph import ScatterPlotItem, InfiniteLine
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
from cryptofeed.symbols import Symbol
|
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
|
||||||
# XXX, use 2 newlines between top level LOC (even between these
|
|
||||||
# imports and the next function line ;)
|
|
||||||
|
|
||||||
def check_if_complete(
|
|
||||||
oi: dict[str, dict[str, Decimal | None]]
|
|
||||||
) -> bool:
|
|
||||||
return all(
|
|
||||||
oi[strike]['C'] is not None
|
|
||||||
and
|
|
||||||
oi[strike]['P'] is not None for strike in oi
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def max_pain_daemon(
|
|
||||||
) -> None:
|
|
||||||
oi_by_strikes: dict[str, dict[str, Decimal | None]]
|
|
||||||
instruments: list[Symbol] = []
|
|
||||||
expiry_dates: list[str]
|
|
||||||
expiry_date: str
|
|
||||||
currency: str = 'btc'
|
|
||||||
kind: str = 'option'
|
|
||||||
|
|
||||||
async with get_client(
|
|
||||||
) as client:
|
|
||||||
expiry_dates: list[str] = await client.get_expiration_dates(
|
|
||||||
currency=currency,
|
|
||||||
kind=kind
|
|
||||||
)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
f'Available expiries for {currency!r}-{kind}:\n'
|
|
||||||
f'{expiry_dates}\n'
|
|
||||||
)
|
|
||||||
expiry_date: str = input(
|
|
||||||
'Please enter a valid expiration date: '
|
|
||||||
).upper()
|
|
||||||
print('Starting little daemon...')
|
|
||||||
|
|
||||||
# maybe move this type annot down to the assignment line?
|
|
||||||
oi_by_strikes: dict[str, dict[str, Decimal]]
|
|
||||||
instruments = await client.get_instruments(
|
|
||||||
expiry_date=expiry_date,
|
|
||||||
)
|
|
||||||
oi_by_strikes = client.get_strikes_dict(instruments)
|
|
||||||
|
|
||||||
|
|
||||||
def get_total_intrinsic_values(
|
|
||||||
oi_by_strikes: dict[str, dict[str, Decimal]]
|
|
||||||
) -> dict[str, dict[str, Decimal]]:
|
|
||||||
call_cash: Decimal = Decimal(0)
|
|
||||||
put_cash: Decimal = Decimal(0)
|
|
||||||
intrinsic_values: dict[str, dict[str, Decimal]] = {}
|
|
||||||
closes: list = sorted(Decimal(close) for close in oi_by_strikes)
|
|
||||||
|
|
||||||
for strike, oi in oi_by_strikes.items():
|
|
||||||
s = Decimal(strike)
|
|
||||||
call_cash = sum(max(0, (s - c) * oi_by_strikes[str(c)]['C']) for c in closes)
|
|
||||||
put_cash = sum(max(0, (c - s) * oi_by_strikes[str(c)]['P']) for c in closes)
|
|
||||||
|
|
||||||
intrinsic_values[strike] = {
|
|
||||||
'C': call_cash,
|
|
||||||
'P': put_cash,
|
|
||||||
'total': call_cash + put_cash,
|
|
||||||
}
|
|
||||||
|
|
||||||
return intrinsic_values
|
|
||||||
|
|
||||||
def get_intrinsic_value_and_max_pain(
|
|
||||||
intrinsic_values: dict[str, dict[str, Decimal]]
|
|
||||||
):
|
|
||||||
# We meed to find the lowest value, so we start at
|
|
||||||
# infinity to ensure that, and the max_pain must be
|
|
||||||
# an amount greater than zero.
|
|
||||||
total_intrinsic_value: Decimal = Decimal('Infinity')
|
|
||||||
max_pain: Decimal = Decimal(0)
|
|
||||||
|
|
||||||
for strike, oi in oi_by_strikes.items():
|
|
||||||
s = Decimal(strike)
|
|
||||||
if intrinsic_values[strike]['total'] < total_intrinsic_value:
|
|
||||||
total_intrinsic_value = intrinsic_values[strike]['total']
|
|
||||||
max_pain = s
|
|
||||||
|
|
||||||
return total_intrinsic_value, max_pain
|
|
||||||
|
|
||||||
def plot_graph(
|
|
||||||
oi_by_strikes: dict[str, dict[str, Decimal]],
|
|
||||||
plot,
|
|
||||||
):
|
|
||||||
"""Update the bar graph with new open interest data."""
|
|
||||||
plot.clear()
|
|
||||||
|
|
||||||
intrinsic_values = get_total_intrinsic_values(oi_by_strikes)
|
|
||||||
|
|
||||||
for strike_str in sorted(oi_by_strikes, key=lambda x: int(x)):
|
|
||||||
strike = int(strike_str)
|
|
||||||
calls_val = float(oi_by_strikes[strike_str]['C'])
|
|
||||||
puts_val = float(oi_by_strikes[strike_str]['P'])
|
|
||||||
|
|
||||||
bar_c = pg.BarGraphItem(
|
|
||||||
x=[strike - 100],
|
|
||||||
height=[calls_val],
|
|
||||||
width=200,
|
|
||||||
pen='w',
|
|
||||||
brush=(0, 0, 255, 150)
|
|
||||||
)
|
|
||||||
plot.addItem(bar_c)
|
|
||||||
|
|
||||||
bar_p = pg.BarGraphItem(
|
|
||||||
x=[strike + 100],
|
|
||||||
height=[puts_val],
|
|
||||||
width=200,
|
|
||||||
pen='w',
|
|
||||||
brush=(255, 0, 0, 150)
|
|
||||||
)
|
|
||||||
plot.addItem(bar_p)
|
|
||||||
|
|
||||||
total_val = float(intrinsic_values[strike_str]['total']) / 100000
|
|
||||||
|
|
||||||
scatter_iv = ScatterPlotItem(
|
|
||||||
x=[strike],
|
|
||||||
y=[total_val],
|
|
||||||
pen=pg.mkPen(color=(0, 255, 0), width=2),
|
|
||||||
brush=pg.mkBrush(0, 255, 0, 150),
|
|
||||||
size=3,
|
|
||||||
symbol='o'
|
|
||||||
)
|
|
||||||
plot.addItem(scatter_iv)
|
|
||||||
|
|
||||||
_, max_pain = get_intrinsic_value_and_max_pain(intrinsic_values)
|
|
||||||
|
|
||||||
vertical_line = InfiniteLine(
|
|
||||||
pos=max_pain,
|
|
||||||
angle=90,
|
|
||||||
pen=pg.mkPen(color='yellow', width=1, style=QtCore.Qt.PenStyle.DotLine),
|
|
||||||
label=f'Max pain: {max_pain:,.0f}',
|
|
||||||
labelOpts={
|
|
||||||
'position': 0.85,
|
|
||||||
'color': 'yellow',
|
|
||||||
'movable': True
|
|
||||||
}
|
|
||||||
)
|
|
||||||
plot.addItem(vertical_line)
|
|
||||||
|
|
||||||
def update_oi_by_strikes(msg: tuple):
|
|
||||||
nonlocal oi_by_strikes
|
|
||||||
if 'oi' == msg[0]:
|
|
||||||
strike_price = msg[1]['strike_price']
|
|
||||||
option_type = msg[1]['option_type']
|
|
||||||
open_interest = msg[1]['open_interest']
|
|
||||||
oi_by_strikes.setdefault(
|
|
||||||
strike_price, {}
|
|
||||||
).update(
|
|
||||||
{option_type: open_interest}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define the structured dtype
|
|
||||||
dtype = np.dtype([
|
|
||||||
('time', int),
|
|
||||||
('oi', float),
|
|
||||||
('oi_calc', float),
|
|
||||||
])
|
|
||||||
async def write_open_interest_on_file(msg: tuple, client: StorageClient):
|
|
||||||
if 'oi' == msg[0]:
|
|
||||||
nonlocal expiry_date
|
|
||||||
timestamp = msg[1]['timestamp']
|
|
||||||
strike_price = msg[1]["strike_price"]
|
|
||||||
option_type = msg[1]['option_type'].lower()
|
|
||||||
col_sym_key = f'btc-{expiry_date.lower()}-{strike_price}-{option_type}'
|
|
||||||
|
|
||||||
# Create the numpy array with sample data
|
|
||||||
data = np.array([
|
|
||||||
(
|
|
||||||
int(timestamp),
|
|
||||||
float(msg[1]['open_interest']),
|
|
||||||
np.nan,
|
|
||||||
),
|
|
||||||
], dtype=dtype)
|
|
||||||
|
|
||||||
path: Path = await client.write_oi(
|
|
||||||
col_sym_key,
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
# TODO, use std logging like this throughout for status
|
|
||||||
# emissions on console!
|
|
||||||
log.info(f'Wrote OI history to {path}')
|
|
||||||
|
|
||||||
def get_max_pain(
|
|
||||||
oi_by_strikes: dict[str, dict[str, Decimal]]
|
|
||||||
) -> dict[str, str | Decimal]:
|
|
||||||
'''
|
|
||||||
This method requires only the strike_prices and oi for call
|
|
||||||
and puts, the closes list are the same as the strike_prices
|
|
||||||
the idea is to sum all the calls and puts cash for each strike
|
|
||||||
and the ITM strikes from that strike, the lowest value is what we
|
|
||||||
are looking for the intrinsic value.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
nonlocal timestamp
|
|
||||||
|
|
||||||
intrinsic_values = get_total_intrinsic_values(oi_by_strikes)
|
|
||||||
|
|
||||||
total_intrinsic_value, max_pain = get_intrinsic_value_and_max_pain(intrinsic_values)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'expiry_date': expiry_date,
|
|
||||||
'total_intrinsic_value': total_intrinsic_value,
|
|
||||||
'max_pain': max_pain,
|
|
||||||
}
|
|
||||||
|
|
||||||
async with (
|
|
||||||
open_storage_client() as (_, storage),
|
|
||||||
|
|
||||||
maybe_open_oi_feed(
|
|
||||||
instruments,
|
|
||||||
) as oi_feed,
|
|
||||||
):
|
|
||||||
# Initialize QApplication
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
|
|
||||||
win = pg.GraphicsLayoutWidget(show=True)
|
|
||||||
win.setWindowTitle('Calls (blue) vs Puts (red)')
|
|
||||||
|
|
||||||
plot = win.addPlot(title='OI by Strikes')
|
|
||||||
plot.showGrid(x=True, y=True)
|
|
||||||
print('Plot initialized...')
|
|
||||||
|
|
||||||
async for msg in oi_feed:
|
|
||||||
|
|
||||||
# In memory oi_by_strikes dict, all message are filtered here
|
|
||||||
# and the dict is updated with the open interest data
|
|
||||||
update_oi_by_strikes(msg)
|
|
||||||
|
|
||||||
# Write on file using storage client
|
|
||||||
await write_open_interest_on_file(msg, storage)
|
|
||||||
|
|
||||||
# Max pain calcs, before start we must gather all the open interest for
|
|
||||||
# all the strike prices and option types available for a expiration date
|
|
||||||
if check_if_complete(oi_by_strikes):
|
|
||||||
if 'oi' == msg[0]:
|
|
||||||
# Here we must read for the filesystem all the latest open interest value for
|
|
||||||
# each instrument for that specific expiration date, that means look up for the
|
|
||||||
# last update got the instrument btc-{expity_date}-*oi1s.parquet (1s because is
|
|
||||||
# hardcoded to something, sorry.)
|
|
||||||
timestamp = msg[1]['timestamp']
|
|
||||||
max_pain = get_max_pain(oi_by_strikes)
|
|
||||||
# intrinsic_values = get_total_intrinsic_values(oi_by_strikes)
|
|
||||||
|
|
||||||
# graph here
|
|
||||||
plot_graph(oi_by_strikes, plot)
|
|
||||||
|
|
||||||
# TODO, use a single multiline string with `()`
|
|
||||||
# and drop the multiple `print()` calls (this
|
|
||||||
# should be done elsewhere in this file as well!
|
|
||||||
#
|
|
||||||
# As per the docs,
|
|
||||||
# https://docs.python.org/3/reference/lexical_analysis.html#string-literal-concatenation
|
|
||||||
# you could instead do,
|
|
||||||
# print(
|
|
||||||
# '-----------------------------------------------\n'
|
|
||||||
# f'timestamp: {datetime.fromtimestamp(max_pain['timestamp'])}\n'
|
|
||||||
# )
|
|
||||||
# WHY?
|
|
||||||
# |_ less ctx-switches/calls to `print()`
|
|
||||||
# |_ the `str` can then be modified / passed
|
|
||||||
# around as a variable more easily if needed in
|
|
||||||
# the future ;)
|
|
||||||
#
|
|
||||||
# ALSO, i believe there already is a stdlib
|
|
||||||
# module to do "alignment" of text which you
|
|
||||||
# could try for doing the right-side alignment,
|
|
||||||
# https://docs.python.org/3/library/textwrap.html#textwrap.indent
|
|
||||||
#
|
|
||||||
print('-----------------------------------------------')
|
|
||||||
print(f'timestamp: {datetime.fromtimestamp(max_pain['timestamp'])}')
|
|
||||||
print(f'expiry_date: {max_pain['expiry_date']}')
|
|
||||||
print(f'max_pain: {max_pain['max_pain']:,.0f}')
|
|
||||||
print(f'total intrinsic value: {max_pain['total_intrinsic_value']:,.0f}')
|
|
||||||
print('-----------------------------------------------')
|
|
||||||
|
|
||||||
# Process GUI events to keep the window responsive
|
|
||||||
app.processEvents()
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
|
||||||
debug_mode=True,
|
|
||||||
loglevel='info',
|
|
||||||
) as an:
|
|
||||||
from tractor import log
|
|
||||||
log.get_console_log(level='info')
|
|
||||||
|
|
||||||
ptl: tractor.Portal = await an.start_actor(
|
|
||||||
'max_pain_daemon',
|
|
||||||
enable_modules=[__name__],
|
|
||||||
infect_asyncio=True,
|
|
||||||
# ^TODO, we can actually run this in the root-actor now
|
|
||||||
# if needed as per 2nd "section" in,
|
|
||||||
# https://pikers.dev/goodboy/tractor/pulls/2
|
|
||||||
#
|
|
||||||
# NOTE, will first require us porting to modern
|
|
||||||
# `tractor:main` though ofc!
|
|
||||||
|
|
||||||
)
|
|
||||||
await ptl.run(max_pain_daemon)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
trio.run(main)
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
## Max Pain Calculation for Deribit Options
|
|
||||||
|
|
||||||
This feature, which calculates the max pain point for options traded
|
|
||||||
on the Deribit exchange using cryptofeed library.
|
|
||||||
|
|
||||||
- Functions in the api module for fetching options data from Deribit.
|
|
||||||
[commit](https://pikers.dev/pikers/piker/commit/da55856dd2876291f55a06eb0561438a912d8241)
|
|
||||||
|
|
||||||
- Compute the max pain point based on open interest data using
|
|
||||||
deribit's api.
|
|
||||||
[commit](https://pikers.dev/pikers/piker/commit/0d9d6e15ba0edeb662ec97f7599dd66af3046b94)
|
|
||||||
|
|
||||||
### How to test it?
|
|
||||||
|
|
||||||
**Before start:** in order to get this working with `uv`, you
|
|
||||||
**must** use my [`tractor` fork](https://pikers.dev/ntorres/tractor/src/branch/aio_abandons)
|
|
||||||
and this branch: `aio_abandons`, the reason is that I cherry-pick the
|
|
||||||
`uv_migration` that guille made, for some reason that a didn't dive
|
|
||||||
into, in my system y need tractor using `uv` too. quite hacky
|
|
||||||
I guess.
|
|
||||||
|
|
||||||
1. `uv lock`
|
|
||||||
|
|
||||||
2. `uv run --no-dev python examples/max_pain.py`
|
|
||||||
|
|
||||||
3. A message should be display, enter one of the expiration date
|
|
||||||
available.
|
|
||||||
|
|
||||||
4. The script should be up and running.
|
|
||||||
123
flake.lock
123
flake.lock
|
|
@ -1,135 +1,24 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1689068808,
|
|
||||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1689068808,
|
|
||||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nix-github-actions": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"poetry2nix",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1688870561,
|
|
||||||
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nix-github-actions",
|
|
||||||
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "nix-github-actions",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1692174805,
|
"lastModified": 1765779637,
|
||||||
"narHash": "sha256-xmNPFDi/AUMIxwgOH/IVom55Dks34u1g7sFKKebxUm0=",
|
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
|
||||||
"owner": "NixOS",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "caac0eb6bdcad0b32cb2522e03e4002c8975c62e",
|
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "nixos",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"poetry2nix": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nix-github-actions": "nix-github-actions",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1692048894,
|
|
||||||
"narHash": "sha256-cDw03rso2V4CDc3Mll0cHN+ztzysAvdI8pJ7ybbz714=",
|
|
||||||
"ref": "refs/heads/pyqt6",
|
|
||||||
"rev": "b059ad4c3051f45d6c912e17747aae37a9ec1544",
|
|
||||||
"revCount": 2276,
|
|
||||||
"type": "git",
|
|
||||||
"url": "file:///home/lord_fomo/repos/poetry2nix"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "file:///home/lord_fomo/repos/poetry2nix"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
"nixpkgs": "nixpkgs"
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"poetry2nix": "poetry2nix"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
243
flake.nix
243
flake.nix
|
|
@ -1,180 +1,103 @@
|
||||||
# NOTE: to convert to a poetry2nix env like this here are the
|
# An "impure" template thx to `pyproject.nix`,
|
||||||
# steps:
|
# https://pyproject-nix.github.io/pyproject.nix/templates.html#impure
|
||||||
# - install poetry in your system nix config
|
# https://github.com/pyproject-nix/pyproject.nix/blob/master/templates/impure/flake.nix
|
||||||
# - convert the repo to use poetry using `poetry init`:
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#initialising-a-pre-existing-project
|
|
||||||
# - then manually ensuring all deps are converted over:
|
|
||||||
# - add this file to the repo and commit it
|
|
||||||
# -
|
|
||||||
|
|
||||||
# GROKin tips:
|
|
||||||
# - CLI eps are (ostensibly) added via an `entry_points.txt`:
|
|
||||||
# - https://packaging.python.org/en/latest/specifications/entry-points/#file-format
|
|
||||||
# - https://github.com/nix-community/poetry2nix/blob/master/editable.nix#L49
|
|
||||||
{
|
{
|
||||||
description = "piker: trading gear for hackers (pkged with poetry2nix)";
|
description = "An impure `piker` overlay using `uv` with Nix(OS)";
|
||||||
|
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs = {
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
# see https://github.com/nix-community/poetry2nix/tree/master#api
|
|
||||||
inputs.poetry2nix = {
|
|
||||||
# url = "github:nix-community/poetry2nix";
|
|
||||||
# url = "github:K900/poetry2nix/qt5-explicit-deps";
|
|
||||||
url = "/home/lord_fomo/repos/poetry2nix";
|
|
||||||
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs =
|
||||||
self,
|
{ nixpkgs, ... }:
|
||||||
nixpkgs,
|
let
|
||||||
flake-utils,
|
inherit (nixpkgs) lib;
|
||||||
poetry2nix,
|
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
|
||||||
}:
|
in
|
||||||
# TODO: build cross-OS and use the `${system}` var thingy..
|
{
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
devShells = forAllSystems (
|
||||||
let
|
system:
|
||||||
# use PWD as sources
|
let
|
||||||
projectDir = ./.;
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
pyproject = ./pyproject.toml;
|
|
||||||
poetrylock = ./poetry.lock;
|
|
||||||
|
|
||||||
# TODO: port to 3.11 and support both versions?
|
# do store-path extractions
|
||||||
python = "python3.10";
|
qt6baseStorePath = lib.getLib pkgs.qt6.qtbase;
|
||||||
|
# ?TODO? can remove below since manual linking not needed?
|
||||||
|
# qt6QtWaylandStorePath = lib.getLib pkgs.qt6.qtwayland;
|
||||||
|
|
||||||
# for more functions and examples.
|
# XXX NOTE XXX, for now we overlay specific pkgs via
|
||||||
# inherit
|
# a major-version-pinned-`cpython`
|
||||||
# (poetry2nix.legacyPackages.${system})
|
cpython = "python313";
|
||||||
# mkPoetryApplication;
|
pypkgs = pkgs."${cpython}Packages";
|
||||||
# pkgs = nixpkgs.legacyPackages.${system};
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
|
||||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
packages = with pkgs; [
|
||||||
lib = pkgs.lib;
|
# XXX, ensure sh completions active!
|
||||||
p2npkgs = poetry2nix.legacyPackages.x86_64-linux;
|
bashInteractive
|
||||||
|
bash-completion
|
||||||
|
|
||||||
# define all pkg overrides per dep, see edgecases.md:
|
# dev utils
|
||||||
# https://github.com/nix-community/poetry2nix/blob/master/docs/edgecases.md
|
ruff
|
||||||
# TODO: add these into the json file:
|
pypkgs.ruff
|
||||||
# https://github.com/nix-community/poetry2nix/blob/master/overrides/build-systems.json
|
|
||||||
pypkgs-build-requirements = {
|
|
||||||
asyncvnc = [ "setuptools" ];
|
|
||||||
eventkit = [ "setuptools" ];
|
|
||||||
ib-insync = [ "setuptools" "flake8" ];
|
|
||||||
msgspec = [ "setuptools"];
|
|
||||||
pdbp = [ "setuptools" ];
|
|
||||||
pyqt6-sip = [ "setuptools" ];
|
|
||||||
tabcompleter = [ "setuptools" ];
|
|
||||||
tractor = [ "setuptools" ];
|
|
||||||
tricycle = [ "setuptools" ];
|
|
||||||
trio-typing = [ "setuptools" ];
|
|
||||||
trio-util = [ "setuptools" ];
|
|
||||||
xonsh = [ "setuptools" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
# auto-generate override entries
|
qt6.qtwayland
|
||||||
p2n-overrides = p2npkgs.defaultPoetryOverrides.extend (self: super:
|
qt6.qtbase
|
||||||
builtins.mapAttrs (package: build-requirements:
|
|
||||||
(builtins.getAttr package super).overridePythonAttrs (old: {
|
|
||||||
buildInputs = (
|
|
||||||
old.buildInputs or [ ]
|
|
||||||
) ++ (
|
|
||||||
builtins.map (
|
|
||||||
pkg: if builtins.isString pkg then builtins.getAttr pkg super else pkg
|
|
||||||
) build-requirements
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) pypkgs-build-requirements
|
|
||||||
);
|
|
||||||
|
|
||||||
# override some ahead-of-time compiled extensions
|
uv
|
||||||
# to be built with their wheels.
|
python313 # ?TODO^ how to set from `cpython` above?
|
||||||
ahot_overrides = p2n-overrides.extend(
|
pypkgs.pyqt6
|
||||||
final: prev: {
|
pypkgs.pyqt6-sip
|
||||||
|
pypkgs.qtpy
|
||||||
|
pypkgs.qdarkstyle
|
||||||
|
pypkgs.rapidfuzz
|
||||||
|
];
|
||||||
|
|
||||||
# llvmlite = prev.llvmlite.override {
|
shellHook = ''
|
||||||
# preferWheel = false;
|
# unmask to debug **this** dev-shell-hook
|
||||||
# };
|
# set -e
|
||||||
|
|
||||||
# TODO: get this workin with p2n and nixpkgs..
|
# set qt-base/plugin path(s)
|
||||||
# pyqt6 = prev.pyqt6.override {
|
QTBASE_PATH="${qt6baseStorePath}/lib"
|
||||||
# preferWheel = true;
|
QT_PLUGIN_PATH="${qt6baseStorePath}/lib/qt-6/plugins"
|
||||||
# };
|
QT_QPA_PLATFORM_PLUGIN_PATH="$QT_PLUGIN_PATH/platforms"
|
||||||
|
|
||||||
# NOTE: this DOESN'T work atm but after a fix
|
# link in Qt cc lib paths from <nixpkgs>
|
||||||
# to poetry2nix, it will and actually this line
|
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QTBASE_PATH"
|
||||||
# won't be needed - thanks @k900:
|
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QT_PLUGIN_PATH"
|
||||||
# https://github.com/nix-community/poetry2nix/pull/1257
|
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QT_QPA_PLATFORM_PLUGIN_PATH"
|
||||||
pyqt5 = prev.pyqt5.override {
|
|
||||||
# withWebkit = false;
|
|
||||||
preferWheel = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
# see PR from @k900:
|
# link-in c++ stdlib for various AOT-ext-pkgs (numpy, etc.)
|
||||||
# https://github.com/nix-community/poetry2nix/pull/1257
|
LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
|
||||||
# pyqt5-qt5 = prev.pyqt5-qt5.override {
|
|
||||||
# withWebkit = false;
|
|
||||||
# preferWheel = true;
|
|
||||||
# };
|
|
||||||
|
|
||||||
# TODO: patch in an override for polars to build
|
export LD_LIBRARY_PATH
|
||||||
# from src! See the details likely needed from
|
|
||||||
# the cryptography entry:
|
|
||||||
# https://github.com/nix-community/poetry2nix/blob/master/overrides/default.nix#L426-L435
|
|
||||||
polars = prev.polars.override {
|
|
||||||
preferWheel = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
# WHY!? -> output-attrs that `nix develop` scans for:
|
# RUNTIME-SETTINGS
|
||||||
# https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-develop.html#flake-output-attributes
|
#
|
||||||
in
|
# ------ Qt ------
|
||||||
rec {
|
# XXX, unmask to debug qt .so linking/loading deats
|
||||||
packages = {
|
# export QT_DEBUG_PLUGINS=1
|
||||||
# piker = poetry2nix.legacyPackages.x86_64-linux.mkPoetryEditablePackage {
|
#
|
||||||
# editablePackageSources = { piker = ./piker; };
|
# ALSO, for *modern linux* DEs,
|
||||||
|
# - maybe set wayland-mode (TODO, parametrtize this!)
|
||||||
|
# * a chosen wayland-mode shell-integration
|
||||||
|
export QT_QPA_PLATFORM="wayland"
|
||||||
|
export QT_WAYLAND_SHELL_INTEGRATION="xdg-shell"
|
||||||
|
|
||||||
piker = p2npkgs.mkPoetryApplication {
|
# ------ uv ------
|
||||||
projectDir = projectDir;
|
# - always use the ./py313/ venv-subdir
|
||||||
|
export UV_PROJECT_ENVIRONMENT="py313"
|
||||||
|
# sync project-env with all extras
|
||||||
|
uv sync --dev --all-extras --no-group lint
|
||||||
|
|
||||||
# SEE ABOVE for auto-genned input set, override
|
# ------ TIPS ------
|
||||||
# buncha deps with extras.. like `setuptools` mostly.
|
# NOTE, to launch the py-venv installed `xonsh` (like @goodboy)
|
||||||
# TODO: maybe propose a patch to p2n to show that you
|
# run the `nix develop` cmd with,
|
||||||
# can even do this in the edgecases docs?
|
# >> nix develop -c uv run xonsh
|
||||||
overrides = ahot_overrides;
|
'';
|
||||||
|
|
||||||
# XXX: won't work on llvmlite..
|
|
||||||
# preferWheels = true;
|
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
);
|
||||||
# devShells.default = pkgs.mkShell {
|
};
|
||||||
# projectDir = projectDir;
|
|
||||||
# python = "python3.10";
|
|
||||||
# overrides = ahot_overrides;
|
|
||||||
# inputsFrom = [ self.packages.x86_64-linux.piker ];
|
|
||||||
# packages = packages;
|
|
||||||
# # packages = [ poetry2nix.packages.${system}.poetry ];
|
|
||||||
# };
|
|
||||||
|
|
||||||
# TODO: grok the difference here..
|
|
||||||
# - avoid re-cloning git repos on every develop entry..
|
|
||||||
# - ideally allow hacking on the src code of some deps
|
|
||||||
# (tractor, pyqtgraph, tomlkit, etc.) WITHOUT having to
|
|
||||||
# re-install them every time a change is made.
|
|
||||||
# - boot a usable xonsh inside the poetry virtualenv when
|
|
||||||
# defined via a custom entry point?
|
|
||||||
devShells.default = p2npkgs.mkPoetryEnv {
|
|
||||||
# env = p2npkgs.mkPoetryEnv {
|
|
||||||
projectDir = projectDir;
|
|
||||||
python = pkgs.python310;
|
|
||||||
overrides = ahot_overrides;
|
|
||||||
editablePackageSources = packages;
|
|
||||||
# piker = "./";
|
|
||||||
# tractor = "../tractor/";
|
|
||||||
# }; # wut?
|
|
||||||
};
|
|
||||||
}
|
|
||||||
); # end of .outputs scope
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ import tomli_w # for fast ledger writing
|
||||||
|
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker import config
|
from piker import config
|
||||||
from ..log import get_logger
|
from piker.log import get_logger
|
||||||
from .calc import (
|
from .calc import (
|
||||||
iter_by_dt,
|
iter_by_dt,
|
||||||
)
|
)
|
||||||
|
|
@ -239,7 +239,9 @@ class TransactionLedger(UserDict):
|
||||||
|
|
||||||
symcache: SymbologyCache = self._symcache
|
symcache: SymbologyCache = self._symcache
|
||||||
towrite: dict[str, Any] = {}
|
towrite: dict[str, Any] = {}
|
||||||
for tid, txdict in self.tx_sort(self.data.copy()):
|
for tid, txdict in self.tx_sort(
|
||||||
|
self.data.copy()
|
||||||
|
):
|
||||||
# write blank-str expiry for non-expiring assets
|
# write blank-str expiry for non-expiring assets
|
||||||
if (
|
if (
|
||||||
'expiry' in txdict
|
'expiry' in txdict
|
||||||
|
|
@ -377,7 +379,7 @@ def open_trade_ledger(
|
||||||
account,
|
account,
|
||||||
dirpath=_fp,
|
dirpath=_fp,
|
||||||
)
|
)
|
||||||
cpy = ledger_dict.copy()
|
cpy: dict = ledger_dict.copy()
|
||||||
|
|
||||||
# XXX NOTE: if not provided presume we are being called from
|
# XXX NOTE: if not provided presume we are being called from
|
||||||
# sync code and need to maybe run `trio` to generate..
|
# sync code and need to maybe run `trio` to generate..
|
||||||
|
|
@ -406,7 +408,13 @@ def open_trade_ledger(
|
||||||
account=account,
|
account=account,
|
||||||
mod=mod,
|
mod=mod,
|
||||||
symcache=symcache,
|
symcache=symcache,
|
||||||
tx_sort=getattr(mod, 'tx_sort', tx_sort),
|
|
||||||
|
# NOTE: allow backends to provide custom ledger sorting
|
||||||
|
tx_sort=getattr(
|
||||||
|
mod,
|
||||||
|
'tx_sort',
|
||||||
|
tx_sort,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
yield ledger
|
yield ledger
|
||||||
|
|
|
||||||
|
|
@ -305,8 +305,8 @@ class MktPair(Struct, frozen=True):
|
||||||
# config right?
|
# config right?
|
||||||
# src_type: AssetTypeName
|
# src_type: AssetTypeName
|
||||||
|
|
||||||
# for derivs, info describing contract, egs.
|
# for derivs, info describing contract, egs. strike price, call
|
||||||
# strike price, call or put, swap type, exercise model, etc.
|
# or put, swap type, exercise model, etc.
|
||||||
contract_info: list[str] | None = None
|
contract_info: list[str] | None = None
|
||||||
|
|
||||||
# TODO: rename to sectype since all of these can
|
# TODO: rename to sectype since all of these can
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ from types import ModuleType
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Iterator,
|
Iterator,
|
||||||
Generator
|
Generator,
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
|
|
@ -59,8 +60,10 @@ from ..clearing._messages import (
|
||||||
BrokerdPosition,
|
BrokerdPosition,
|
||||||
)
|
)
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.data._symcache import SymbologyCache
|
from piker.log import get_logger
|
||||||
from ..log import get_logger
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from piker.data._symcache import SymbologyCache
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -502,6 +505,17 @@ class Account(Struct):
|
||||||
|
|
||||||
_mktmap_table: dict[str, MktPair] | None = None,
|
_mktmap_table: dict[str, MktPair] | None = None,
|
||||||
|
|
||||||
|
only_require: list[str]|True = True,
|
||||||
|
# ^list of fqmes that are "required" to be processed from
|
||||||
|
# this ledger pass; we often don't care about others and
|
||||||
|
# definitely shouldn't always error in such cases.
|
||||||
|
# (eg. broker backend loaded that doesn't yet supsport the
|
||||||
|
# symcache but also, inside the paper engine we don't ad-hoc
|
||||||
|
# request `get_mkt_info()` for every symbol in the ledger,
|
||||||
|
# only the one for which we're simulating against).
|
||||||
|
# TODO, not sure if there's a better soln for this, ideally
|
||||||
|
# all backends get symcache support afap i guess..
|
||||||
|
|
||||||
) -> dict[str, Position]:
|
) -> dict[str, Position]:
|
||||||
'''
|
'''
|
||||||
Update the internal `.pps[str, Position]` table from input
|
Update the internal `.pps[str, Position]` table from input
|
||||||
|
|
@ -544,11 +558,32 @@ class Account(Struct):
|
||||||
if _mktmap_table is None:
|
if _mktmap_table is None:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
required: bool = (
|
||||||
|
only_require is True
|
||||||
|
or (
|
||||||
|
only_require is not True
|
||||||
|
and
|
||||||
|
fqme in only_require
|
||||||
|
)
|
||||||
|
)
|
||||||
# XXX: caller is allowed to provide a fallback
|
# XXX: caller is allowed to provide a fallback
|
||||||
# mktmap table for the case where a new position is
|
# mktmap table for the case where a new position is
|
||||||
# being added and the preloaded symcache didn't
|
# being added and the preloaded symcache didn't
|
||||||
# have this entry prior (eg. with frickin IB..)
|
# have this entry prior (eg. with frickin IB..)
|
||||||
mkt = _mktmap_table[fqme]
|
if (
|
||||||
|
not (mkt := _mktmap_table.get(fqme))
|
||||||
|
and
|
||||||
|
required
|
||||||
|
):
|
||||||
|
raise
|
||||||
|
|
||||||
|
elif not required:
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
# should be an entry retreived somewhere
|
||||||
|
assert mkt
|
||||||
|
|
||||||
|
|
||||||
if not (pos := pps.get(bs_mktid)):
|
if not (pos := pps.get(bs_mktid)):
|
||||||
|
|
||||||
|
|
@ -665,7 +700,7 @@ class Account(Struct):
|
||||||
def write_config(self) -> None:
|
def write_config(self) -> None:
|
||||||
'''
|
'''
|
||||||
Write the current account state to the user's account TOML file, normally
|
Write the current account state to the user's account TOML file, normally
|
||||||
something like ``pps.toml``.
|
something like `pps.toml`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# TODO: show diff output?
|
# TODO: show diff output?
|
||||||
|
|
|
||||||
|
|
@ -268,9 +268,6 @@ def iter_by_dt(
|
||||||
(v := tx.get(k))
|
(v := tx.get(k))
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
# TODO? remove yah?
|
|
||||||
# v = tx[k] if isdict else tx.dt
|
|
||||||
|
|
||||||
# only call parser on the value if not None from
|
# only call parser on the value if not None from
|
||||||
# the `parsers` table above (when NOT using
|
# the `parsers` table above (when NOT using
|
||||||
# `.get()`), otherwise pass through the value and
|
# `.get()`), otherwise pass through the value and
|
||||||
|
|
@ -287,6 +284,14 @@ def iter_by_dt(
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
log.debug(
|
||||||
|
f'Parser-field not found in txn\n'
|
||||||
|
f'\n'
|
||||||
|
f'parser-field: {k!r}\n'
|
||||||
|
f'txn: {tx!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f'Trying next..\n'
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# XXX: we should never really get here bc it means some kinda
|
# XXX: we should never really get here bc it means some kinda
|
||||||
|
|
@ -295,15 +300,24 @@ def iter_by_dt(
|
||||||
# -> set the `debug_mode = True` if you want to trace such
|
# -> set the `debug_mode = True` if you want to trace such
|
||||||
# cases from REPL ;)
|
# cases from REPL ;)
|
||||||
else:
|
else:
|
||||||
debug_mode: bool = False
|
# XXX: we should really never get here..
|
||||||
report: str = (
|
# only if a ledger record has no expected sort(able)
|
||||||
f'Invalid txn time ??\n'
|
# field will we likely hit this.. like with ze IB.
|
||||||
f'txn-id: {k!r}\n'
|
# if no sortable field just deliver epoch?
|
||||||
f'{k!r}: {v!r}\n'
|
log.warning(
|
||||||
|
'No (time) sortable field for TXN:\n'
|
||||||
|
f'{tx!r}\n'
|
||||||
)
|
)
|
||||||
if debug_mode:
|
report: str = (
|
||||||
|
f'No supported time-field found in txn !?\n'
|
||||||
|
f'\n'
|
||||||
|
f'supported-time-fields: {parsers!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f'txn: {tx!r}\n'
|
||||||
|
)
|
||||||
|
if debug:
|
||||||
with maybe_open_crash_handler(
|
with maybe_open_crash_handler(
|
||||||
pdb=debug_mode,
|
pdb=debug,
|
||||||
raise_on_exit=False,
|
raise_on_exit=False,
|
||||||
):
|
):
|
||||||
raise ValueError(report)
|
raise ValueError(report)
|
||||||
|
|
@ -314,8 +328,6 @@ def iter_by_dt(
|
||||||
_invalid.append(tx)
|
_invalid.append(tx)
|
||||||
return from_timestamp(0.)
|
return from_timestamp(0.)
|
||||||
|
|
||||||
# breakpoint()
|
|
||||||
|
|
||||||
entry: tuple[str, dict]|Transaction
|
entry: tuple[str, dict]|Transaction
|
||||||
invalid: list = []
|
invalid: list = []
|
||||||
for entry in sorted(
|
for entry in sorted(
|
||||||
|
|
@ -329,8 +341,6 @@ def iter_by_dt(
|
||||||
log.warning(
|
log.warning(
|
||||||
f'Ignoring txn w invalid timestamp ??\n'
|
f'Ignoring txn w invalid timestamp ??\n'
|
||||||
f'{pformat(entry)}\n'
|
f'{pformat(entry)}\n'
|
||||||
# f'txn-id: {k!r}\n'
|
|
||||||
# f'{k!r}: {v!r}\n'
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,8 @@ def disect(
|
||||||
assert not df.is_empty()
|
assert not df.is_empty()
|
||||||
|
|
||||||
# muck around in pdbp REPL
|
# muck around in pdbp REPL
|
||||||
breakpoint()
|
# tractor.devx.mk_pdb().set_trace()
|
||||||
|
# breakpoint()
|
||||||
|
|
||||||
# TODO: we REALLY need a better console REPL for this
|
# TODO: we REALLY need a better console REPL for this
|
||||||
# kinda thing..
|
# kinda thing..
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ __brokers__: list[str] = [
|
||||||
'ib',
|
'ib',
|
||||||
'kraken',
|
'kraken',
|
||||||
'kucoin',
|
'kucoin',
|
||||||
'deribit',
|
|
||||||
|
|
||||||
# broken but used to work
|
# broken but used to work
|
||||||
# 'questrade',
|
# 'questrade',
|
||||||
|
|
@ -62,6 +61,7 @@ __brokers__: list[str] = [
|
||||||
# wstrade
|
# wstrade
|
||||||
# iex
|
# iex
|
||||||
|
|
||||||
|
# deribit
|
||||||
# bitso
|
# bitso
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -98,13 +98,14 @@ async def open_cached_client(
|
||||||
If one has not been setup do it and cache it.
|
If one has not been setup do it and cache it.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
brokermod = get_brokermod(brokername)
|
brokermod: ModuleType = get_brokermod(brokername)
|
||||||
|
|
||||||
|
# TODO: make abstract or `typing.Protocol`
|
||||||
|
# client: Client
|
||||||
async with maybe_open_context(
|
async with maybe_open_context(
|
||||||
acm_func=brokermod.get_client,
|
acm_func=brokermod.get_client,
|
||||||
kwargs=kwargs,
|
kwargs=kwargs,
|
||||||
|
|
||||||
) as (cache_hit, client):
|
) as (cache_hit, client):
|
||||||
|
|
||||||
if cache_hit:
|
if cache_hit:
|
||||||
log.runtime(f'Reusing existing {client}')
|
log.runtime(f'Reusing existing {client}')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -471,11 +471,15 @@ def search(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# global opts
|
# global opts
|
||||||
brokermods = list(config['brokermods'].values())
|
brokermods: list[ModuleType] = list(config['brokermods'].values())
|
||||||
|
|
||||||
|
# TODO: this is coming from the `search --pdb` NOT from
|
||||||
|
# the `piker --pdb` XD ..
|
||||||
|
# -[ ] pull from the parent click ctx's values..dumdum
|
||||||
|
# assert pdb
|
||||||
|
|
||||||
# define tractor entrypoint
|
# define tractor entrypoint
|
||||||
async def main(func):
|
async def main(func):
|
||||||
|
|
||||||
async with maybe_open_pikerd(
|
async with maybe_open_pikerd(
|
||||||
loglevel=config['loglevel'],
|
loglevel=config['loglevel'],
|
||||||
debug_mode=pdb,
|
debug_mode=pdb,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ routines should be primitive data types where possible.
|
||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import (
|
||||||
|
Any,
|
||||||
|
)
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
|
@ -34,8 +36,10 @@ from ..accounting import MktPair
|
||||||
|
|
||||||
|
|
||||||
async def api(brokername: str, methname: str, **kwargs) -> dict:
|
async def api(brokername: str, methname: str, **kwargs) -> dict:
|
||||||
"""Make (proxy through) a broker API call by name and return its result.
|
'''
|
||||||
"""
|
Make (proxy through) a broker API call by name and return its result.
|
||||||
|
|
||||||
|
'''
|
||||||
brokermod = get_brokermod(brokername)
|
brokermod = get_brokermod(brokername)
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
meth = getattr(client, methname, None)
|
meth = getattr(client, methname, None)
|
||||||
|
|
@ -62,10 +66,14 @@ async def api(brokername: str, methname: str, **kwargs) -> dict:
|
||||||
|
|
||||||
async def stocks_quote(
|
async def stocks_quote(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
tickers: List[str]
|
tickers: list[str]
|
||||||
) -> Dict[str, Dict[str, Any]]:
|
|
||||||
"""Return quotes dict for ``tickers``.
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
'''
|
||||||
|
Return a `dict` of snapshot quotes for the provided input
|
||||||
|
`tickers`: a `list` of fqmes.
|
||||||
|
|
||||||
|
'''
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
return await client.quote(tickers)
|
return await client.quote(tickers)
|
||||||
|
|
||||||
|
|
@ -74,13 +82,15 @@ async def stocks_quote(
|
||||||
async def option_chain(
|
async def option_chain(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
date: Optional[str] = None,
|
date: str|None = None,
|
||||||
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
"""Return option chain for ``symbol`` for ``date``.
|
'''
|
||||||
|
Return option chain for ``symbol`` for ``date``.
|
||||||
|
|
||||||
By default all expiries are returned. If ``date`` is provided
|
By default all expiries are returned. If ``date`` is provided
|
||||||
then contract quotes for that single expiry are returned.
|
then contract quotes for that single expiry are returned.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
if date:
|
if date:
|
||||||
id = int((await client.tickers2ids([symbol]))[symbol])
|
id = int((await client.tickers2ids([symbol]))[symbol])
|
||||||
|
|
@ -98,7 +108,7 @@ async def option_chain(
|
||||||
# async def contracts(
|
# async def contracts(
|
||||||
# brokermod: ModuleType,
|
# brokermod: ModuleType,
|
||||||
# symbol: str,
|
# symbol: str,
|
||||||
# ) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
# ) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
# """Return option contracts (all expiries) for ``symbol``.
|
# """Return option contracts (all expiries) for ``symbol``.
|
||||||
# """
|
# """
|
||||||
# async with brokermod.get_client() as client:
|
# async with brokermod.get_client() as client:
|
||||||
|
|
@ -110,15 +120,24 @@ async def bars(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
"""Return option contracts (all expiries) for ``symbol``.
|
'''
|
||||||
"""
|
Return option contracts (all expiries) for ``symbol``.
|
||||||
|
|
||||||
|
'''
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
return await client.bars(symbol, **kwargs)
|
return await client.bars(symbol, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def search_w_brokerd(name: str, pattern: str) -> dict:
|
async def search_w_brokerd(
|
||||||
|
name: str,
|
||||||
|
pattern: str,
|
||||||
|
) -> dict:
|
||||||
|
|
||||||
|
# TODO: WHY NOT WORK!?!
|
||||||
|
# when we `step` through the next block?
|
||||||
|
# import tractor
|
||||||
|
# await tractor.pause()
|
||||||
async with open_cached_client(name) as client:
|
async with open_cached_client(name) as client:
|
||||||
|
|
||||||
# TODO: support multiple asset type concurrent searches.
|
# TODO: support multiple asset type concurrent searches.
|
||||||
|
|
@ -130,12 +149,12 @@ async def symbol_search(
|
||||||
pattern: str,
|
pattern: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
) -> dict[str, dict[str, dict[str, Any]]]:
|
||||||
'''
|
'''
|
||||||
Return symbol info from broker.
|
Return symbol info from broker.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
results = []
|
results: list[str] = []
|
||||||
|
|
||||||
async def search_backend(
|
async def search_backend(
|
||||||
brokermod: ModuleType
|
brokermod: ModuleType
|
||||||
|
|
@ -143,6 +162,13 @@ async def symbol_search(
|
||||||
|
|
||||||
brokername: str = mod.name
|
brokername: str = mod.name
|
||||||
|
|
||||||
|
# TODO: figure this the FUCK OUT
|
||||||
|
# -> ok so obvi in the root actor any async task that's
|
||||||
|
# spawned outside the main tractor-root-actor task needs to
|
||||||
|
# call this..
|
||||||
|
# await tractor.devx._debug.maybe_init_greenback()
|
||||||
|
# tractor.pause_from_sync()
|
||||||
|
|
||||||
async with maybe_spawn_brokerd(
|
async with maybe_spawn_brokerd(
|
||||||
mod.name,
|
mod.name,
|
||||||
infect_asyncio=getattr(
|
infect_asyncio=getattr(
|
||||||
|
|
@ -162,7 +188,6 @@ async def symbol_search(
|
||||||
))
|
))
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
|
|
||||||
for mod in brokermods:
|
for mod in brokermods:
|
||||||
n.start_soon(search_backend, mod.name)
|
n.start_soon(search_backend, mod.name)
|
||||||
|
|
||||||
|
|
@ -172,11 +197,13 @@ async def symbol_search(
|
||||||
async def mkt_info(
|
async def mkt_info(
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> MktPair:
|
) -> MktPair:
|
||||||
'''
|
'''
|
||||||
Return MktPair info from broker including src and dst assets.
|
Return the `piker.accounting.MktPair` info struct from a given
|
||||||
|
backend broker tradable src/dst asset pair.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with open_cached_client(brokermod.name) as client:
|
async with open_cached_client(brokermod.name) as client:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ from .api import (
|
||||||
get_client,
|
get_client,
|
||||||
)
|
)
|
||||||
from .feed import (
|
from .feed import (
|
||||||
get_mkt_info,
|
|
||||||
open_history_client,
|
open_history_client,
|
||||||
open_symbol_search,
|
open_symbol_search,
|
||||||
stream_quotes,
|
stream_quotes,
|
||||||
|
|
@ -35,20 +34,15 @@ from .feed import (
|
||||||
# open_trade_dialog,
|
# open_trade_dialog,
|
||||||
# norm_trade_records,
|
# norm_trade_records,
|
||||||
# )
|
# )
|
||||||
from .venues import (
|
|
||||||
OptionPair,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'get_client',
|
'get_client',
|
||||||
# 'trades_dialogue',
|
# 'trades_dialogue',
|
||||||
'get_mkt_info',
|
|
||||||
'open_history_client',
|
'open_history_client',
|
||||||
'open_symbol_search',
|
'open_symbol_search',
|
||||||
'stream_quotes',
|
'stream_quotes',
|
||||||
'OptionPair',
|
|
||||||
# 'norm_trade_records',
|
# 'norm_trade_records',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,59 +18,38 @@
|
||||||
Deribit backend.
|
Deribit backend.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
|
||||||
from contextlib import asynccontextmanager as acm
|
from contextlib import asynccontextmanager as acm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import (
|
from typing import Any, Optional, Callable
|
||||||
# Any,
|
|
||||||
# Optional,
|
|
||||||
Callable,
|
|
||||||
)
|
|
||||||
# from pprint import pformat
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import cryptofeed
|
|
||||||
import trio
|
import trio
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
from pendulum import (
|
import pendulum
|
||||||
from_timestamp,
|
from rapidfuzz import process as fuzzy
|
||||||
)
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
from piker.accounting import (
|
from piker.brokers import open_cached_client
|
||||||
Asset,
|
from piker.log import get_logger, get_console_log
|
||||||
MktPair,
|
from piker.data import ShmArray
|
||||||
unpack_fqme,
|
from piker.brokers._util import (
|
||||||
)
|
BrokerError,
|
||||||
from piker.brokers import (
|
|
||||||
open_cached_client,
|
|
||||||
NoData,
|
|
||||||
DataUnavailable,
|
DataUnavailable,
|
||||||
)
|
)
|
||||||
from piker._cacheables import (
|
|
||||||
async_lifo_cache,
|
|
||||||
)
|
|
||||||
from piker.log import (
|
|
||||||
get_logger,
|
|
||||||
mk_repr,
|
|
||||||
)
|
|
||||||
from piker.data.validate import FeedInit
|
|
||||||
|
|
||||||
|
from cryptofeed import FeedHandler
|
||||||
|
from cryptofeed.defines import (
|
||||||
|
DERIBIT, L1_BOOK, TRADES, OPTION, CALL, PUT
|
||||||
|
)
|
||||||
|
from cryptofeed.symbols import Symbol
|
||||||
|
|
||||||
from .api import (
|
from .api import (
|
||||||
Client,
|
Client, Trade,
|
||||||
# get_config,
|
get_config,
|
||||||
piker_sym_to_cb_sym,
|
str_to_cb_sym, piker_sym_to_cb_sym, cb_sym_to_deribit_inst,
|
||||||
cb_sym_to_deribit_inst,
|
|
||||||
str_to_cb_sym,
|
|
||||||
maybe_open_price_feed
|
maybe_open_price_feed
|
||||||
)
|
)
|
||||||
from .venues import (
|
|
||||||
Pair,
|
|
||||||
OptionPair,
|
|
||||||
Trade,
|
|
||||||
)
|
|
||||||
|
|
||||||
_spawn_kwargs = {
|
_spawn_kwargs = {
|
||||||
'infect_asyncio': True,
|
'infect_asyncio': True,
|
||||||
|
|
@ -85,215 +64,90 @@ async def open_history_client(
|
||||||
mkt: MktPair,
|
mkt: MktPair,
|
||||||
) -> tuple[Callable, int]:
|
) -> tuple[Callable, int]:
|
||||||
|
|
||||||
|
fnstrument: str = mkt.bs_fqme
|
||||||
# TODO implement history getter for the new storage layer.
|
# TODO implement history getter for the new storage layer.
|
||||||
async with open_cached_client('deribit') as client:
|
async with open_cached_client('deribit') as client:
|
||||||
|
|
||||||
pair: OptionPair = client._pairs[mkt.dst.name]
|
|
||||||
# XXX NOTE, the cuckers use ms !!!
|
|
||||||
creation_time_s: int = pair.creation_timestamp/1000
|
|
||||||
|
|
||||||
async def get_ohlc(
|
async def get_ohlc(
|
||||||
timeframe: float,
|
end_dt: Optional[datetime] = None,
|
||||||
end_dt: datetime | None = None,
|
start_dt: Optional[datetime] = None,
|
||||||
start_dt: datetime | None = None,
|
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
np.ndarray,
|
np.ndarray,
|
||||||
datetime, # start
|
datetime, # start
|
||||||
datetime, # end
|
datetime, # end
|
||||||
]:
|
]:
|
||||||
if timeframe != 60:
|
|
||||||
raise DataUnavailable('Only 1m bars are supported')
|
|
||||||
|
|
||||||
array: np.ndarray = await client.bars(
|
array = await client.bars(
|
||||||
mkt,
|
instrument,
|
||||||
start_dt=start_dt,
|
start_dt=start_dt,
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
)
|
)
|
||||||
if len(array) == 0:
|
if len(array) == 0:
|
||||||
if (
|
raise DataUnavailable
|
||||||
end_dt is None
|
|
||||||
):
|
|
||||||
raise DataUnavailable(
|
|
||||||
'No history seems to exist yet?\n\n'
|
|
||||||
f'{mkt}'
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
end_dt
|
|
||||||
and
|
|
||||||
end_dt.timestamp() < creation_time_s
|
|
||||||
):
|
|
||||||
# the contract can't have history
|
|
||||||
# before it was created.
|
|
||||||
pair_type_str: str = type(pair).__name__
|
|
||||||
create_dt: datetime = from_timestamp(creation_time_s)
|
|
||||||
raise DataUnavailable(
|
|
||||||
f'No history prior to\n'
|
|
||||||
f'`{pair_type_str}.creation_timestamp: int = '
|
|
||||||
f'{pair.creation_timestamp}\n\n'
|
|
||||||
f'------ deribit sux ------\n'
|
|
||||||
f'WHICH IN "NORMAL PEOPLE WHO USE EPOCH TIME" form is,\n'
|
|
||||||
f'creation_time_s: {creation_time_s}\n'
|
|
||||||
f'create_dt: {create_dt}\n'
|
|
||||||
)
|
|
||||||
raise NoData(
|
|
||||||
f'No frame for {start_dt} -> {end_dt}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
start_dt = from_timestamp(array[0]['time'])
|
start_dt = pendulum.from_timestamp(array[0]['time'])
|
||||||
end_dt = from_timestamp(array[-1]['time'])
|
end_dt = pendulum.from_timestamp(array[-1]['time'])
|
||||||
|
|
||||||
times = array['time']
|
|
||||||
if not times.any():
|
|
||||||
raise ValueError(
|
|
||||||
'Bad frame with null-times?\n\n'
|
|
||||||
f'{times}'
|
|
||||||
)
|
|
||||||
|
|
||||||
if end_dt is None:
|
|
||||||
inow: int = round(time.time())
|
|
||||||
if (inow - times[-1]) > 60:
|
|
||||||
await tractor.pause()
|
|
||||||
|
|
||||||
return array, start_dt, end_dt
|
return array, start_dt, end_dt
|
||||||
|
|
||||||
yield (
|
yield get_ohlc, {'erlangs': 3, 'rate': 3}
|
||||||
get_ohlc,
|
|
||||||
{ # backfill config
|
|
||||||
'erlangs': 3,
|
|
||||||
'rate': 3,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@async_lifo_cache()
|
|
||||||
async def get_mkt_info(
|
|
||||||
fqme: str,
|
|
||||||
|
|
||||||
) -> tuple[MktPair, Pair|OptionPair] | None:
|
|
||||||
|
|
||||||
# uppercase since kraken bs_mktid is always upper
|
|
||||||
if 'deribit' not in fqme.lower():
|
|
||||||
fqme += '.deribit'
|
|
||||||
|
|
||||||
mkt_mode: str = ''
|
|
||||||
broker, mkt_ep, venue, expiry = unpack_fqme(fqme)
|
|
||||||
|
|
||||||
# NOTE: we always upper case all tokens to be consistent with
|
|
||||||
# binance's symbology style for pairs, like `BTCUSDT`, but in
|
|
||||||
# theory we could also just keep things lower case; as long as
|
|
||||||
# we're consistent and the symcache matches whatever this func
|
|
||||||
# returns, always!
|
|
||||||
expiry: str = expiry.upper()
|
|
||||||
venue: str = venue.upper()
|
|
||||||
# venue_lower: str = venue.lower()
|
|
||||||
|
|
||||||
mkt_mode: str = 'option'
|
|
||||||
|
|
||||||
async with open_cached_client(
|
|
||||||
'deribit',
|
|
||||||
) as client:
|
|
||||||
|
|
||||||
assets: dict[str, Asset] = await client.get_assets()
|
|
||||||
pair_str: str = mkt_ep.lower()
|
|
||||||
|
|
||||||
pair: Pair = await client.exch_info(
|
|
||||||
sym=pair_str,
|
|
||||||
)
|
|
||||||
mkt_mode = pair.venue
|
|
||||||
client.mkt_mode = mkt_mode
|
|
||||||
|
|
||||||
dst: Asset | None = assets.get(pair.bs_dst_asset)
|
|
||||||
src: Asset | None = assets.get(pair.bs_src_asset)
|
|
||||||
|
|
||||||
mkt = MktPair(
|
|
||||||
dst=dst,
|
|
||||||
src=src,
|
|
||||||
price_tick=pair.price_tick,
|
|
||||||
size_tick=pair.size_tick,
|
|
||||||
bs_mktid=pair.symbol,
|
|
||||||
venue=mkt_mode,
|
|
||||||
broker='deribit',
|
|
||||||
_atype=mkt_mode,
|
|
||||||
_fqme_without_src=True,
|
|
||||||
|
|
||||||
# expiry=pair.expiry,
|
|
||||||
# XXX TODO, currently we don't use it since it's
|
|
||||||
# already "described" in the `OptionPair.symbol: str`
|
|
||||||
# and if we slap in the ISO repr it's kinda hideous..
|
|
||||||
# -[ ] figure out the best either std
|
|
||||||
)
|
|
||||||
return mkt, pair
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_quotes(
|
async def stream_quotes(
|
||||||
|
|
||||||
send_chan: trio.abc.SendChannel,
|
send_chan: trio.abc.SendChannel,
|
||||||
symbols: list[str],
|
symbols: list[str],
|
||||||
feed_is_live: trio.Event,
|
feed_is_live: trio.Event,
|
||||||
|
loglevel: str = None,
|
||||||
|
|
||||||
# startup sync
|
# startup sync
|
||||||
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
|
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
# XXX: required to propagate ``tractor`` loglevel to piker logging
|
||||||
Open a live quote stream for the market set defined by `symbols`.
|
get_console_log(loglevel or tractor.current_actor().loglevel)
|
||||||
|
|
||||||
Internally this starts a `cryptofeed.FeedHandler` inside an `asyncio`-side
|
sym = symbols[0]
|
||||||
task and relays through L1 and `Trade` msgs here to our `trio.Task`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
sym = symbols[0].split('.')[0]
|
|
||||||
init_msgs: list[FeedInit] = []
|
|
||||||
|
|
||||||
# multiline nested `dict` formatter (since rn quote-msgs are
|
|
||||||
# just that).
|
|
||||||
pfmt: Callable[[str], str] = mk_repr(
|
|
||||||
# so we can see `deribit`'s delightfully mega-long bs fields..
|
|
||||||
maxstring=100,
|
|
||||||
)
|
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
open_cached_client('deribit') as client,
|
open_cached_client('deribit') as client,
|
||||||
send_chan as send_chan
|
send_chan as send_chan
|
||||||
):
|
):
|
||||||
mkt: MktPair
|
|
||||||
pair: Pair
|
|
||||||
mkt, pair = await get_mkt_info(sym)
|
|
||||||
|
|
||||||
# build out init msgs according to latest spec
|
init_msgs = {
|
||||||
init_msgs.append(
|
# pass back token, and bool, signalling if we're the writer
|
||||||
FeedInit(
|
# and that history has been written
|
||||||
mkt_info=mkt,
|
sym: {
|
||||||
)
|
'symbol_info': {
|
||||||
)
|
'asset_type': 'option',
|
||||||
# build `cryptofeed` feed-handle
|
'price_tick_size': 0.0005
|
||||||
cf_sym: cryptofeed.Symbol = piker_sym_to_cb_sym(sym)
|
},
|
||||||
|
'shm_write_opts': {'sum_tick_vml': False},
|
||||||
|
'fqsn': sym,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
from_cf: tractor.to_asyncio.LinkedTaskChannel
|
nsym = piker_sym_to_cb_sym(sym)
|
||||||
async with maybe_open_price_feed(sym) as from_cf:
|
|
||||||
|
|
||||||
# load the "last trades" summary
|
async with maybe_open_price_feed(sym) as stream:
|
||||||
last_trades_res: cryptofeed.LastTradesResult = await client.last_trades(
|
|
||||||
cb_sym_to_deribit_inst(cf_sym),
|
|
||||||
count=1,
|
|
||||||
)
|
|
||||||
last_trades: list[Trade] = last_trades_res.trades
|
|
||||||
|
|
||||||
# TODO, do we even need this or will the above always
|
cache = await client.cache_symbols()
|
||||||
# work?
|
|
||||||
# if not last_trades:
|
|
||||||
# await tractor.pause()
|
|
||||||
# async for typ, quote in from_cf:
|
|
||||||
# if typ == 'trade':
|
|
||||||
# last_trade = Trade(**(quote['data']))
|
|
||||||
# break
|
|
||||||
|
|
||||||
# else:
|
last_trades = (await client.last_trades(
|
||||||
last_trade = Trade(
|
cb_sym_to_deribit_inst(nsym), count=1)).trades
|
||||||
**(last_trades[0])
|
|
||||||
)
|
|
||||||
|
|
||||||
first_quote: dict = {
|
if len(last_trades) == 0:
|
||||||
|
last_trade = None
|
||||||
|
async for typ, quote in stream:
|
||||||
|
if typ == 'trade':
|
||||||
|
last_trade = Trade(**(quote['data']))
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
last_trade = Trade(**(last_trades[0]))
|
||||||
|
|
||||||
|
first_quote = {
|
||||||
'symbol': sym,
|
'symbol': sym,
|
||||||
'last': last_trade.price,
|
'last': last_trade.price,
|
||||||
'brokerd_ts': last_trade.timestamp,
|
'brokerd_ts': last_trade.timestamp,
|
||||||
|
|
@ -304,84 +158,13 @@ async def stream_quotes(
|
||||||
'broker_ts': last_trade.timestamp
|
'broker_ts': last_trade.timestamp
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
task_status.started((
|
task_status.started((init_msgs, first_quote))
|
||||||
init_msgs,
|
|
||||||
first_quote,
|
|
||||||
))
|
|
||||||
|
|
||||||
feed_is_live.set()
|
feed_is_live.set()
|
||||||
|
|
||||||
# NOTE XXX, static for now!
|
async for typ, quote in stream:
|
||||||
# => since this only handles ONE mkt feed at a time we
|
topic = quote['symbol']
|
||||||
# don't need a lookup table to map interleaved quotes
|
await send_chan.send({topic: quote})
|
||||||
# from multiple possible mkt-pairs
|
|
||||||
topic: str = mkt.bs_fqme
|
|
||||||
|
|
||||||
# deliver until cancelled
|
|
||||||
async for typ, ref in from_cf:
|
|
||||||
match typ:
|
|
||||||
case 'trade':
|
|
||||||
trade: cryptofeed.types.Trade = ref
|
|
||||||
|
|
||||||
# TODO, re-impl this according to teh ideal
|
|
||||||
# fqme for opts that we choose!!
|
|
||||||
bs_fqme: str = cb_sym_to_deribit_inst(
|
|
||||||
str_to_cb_sym(trade.symbol)
|
|
||||||
).lower()
|
|
||||||
|
|
||||||
piker_quote: dict = {
|
|
||||||
'symbol': bs_fqme,
|
|
||||||
'last': trade.price,
|
|
||||||
'broker_ts': time.time(),
|
|
||||||
# ^TODO, name this `brokerd/datad_ts` and
|
|
||||||
# use `time.time_ns()` ??
|
|
||||||
'ticks': [{
|
|
||||||
'type': 'trade',
|
|
||||||
'price': float(trade.price),
|
|
||||||
'size': float(trade.amount),
|
|
||||||
'broker_ts': trade.timestamp,
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
log.info(
|
|
||||||
f'deribit {typ!r} quote for {sym!r}\n\n'
|
|
||||||
f'{trade}\n\n'
|
|
||||||
f'{pfmt(piker_quote)}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'l1':
|
|
||||||
book: cryptofeed.types.L1Book = ref
|
|
||||||
|
|
||||||
# TODO, so this is where we can possibly change things
|
|
||||||
# and instead lever the `MktPair.bs_fqme: str` output?
|
|
||||||
bs_fqme: str = cb_sym_to_deribit_inst(
|
|
||||||
str_to_cb_sym(book.symbol)
|
|
||||||
).lower()
|
|
||||||
|
|
||||||
piker_quote: dict = {
|
|
||||||
'symbol': bs_fqme,
|
|
||||||
'ticks': [
|
|
||||||
|
|
||||||
{'type': 'bid',
|
|
||||||
'price': float(book.bid_price),
|
|
||||||
'size': float(book.bid_size)},
|
|
||||||
|
|
||||||
{'type': 'bsize',
|
|
||||||
'price': float(book.bid_price),
|
|
||||||
'size': float(book.bid_size),},
|
|
||||||
|
|
||||||
{'type': 'ask',
|
|
||||||
'price': float(book.ask_price),
|
|
||||||
'size': float(book.ask_size),},
|
|
||||||
|
|
||||||
{'type': 'asize',
|
|
||||||
'price': float(book.ask_price),
|
|
||||||
'size': float(book.ask_size),}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
await send_chan.send({
|
|
||||||
topic: piker_quote,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
|
|
@ -391,21 +174,12 @@ async def open_symbol_search(
|
||||||
async with open_cached_client('deribit') as client:
|
async with open_cached_client('deribit') as client:
|
||||||
|
|
||||||
# load all symbols locally for fast search
|
# load all symbols locally for fast search
|
||||||
# cache = client._pairs
|
cache = await client.cache_symbols()
|
||||||
await ctx.started()
|
await ctx.started()
|
||||||
|
|
||||||
async with ctx.open_stream() as stream:
|
async with ctx.open_stream() as stream:
|
||||||
pattern: str
|
|
||||||
async for pattern in stream:
|
async for pattern in stream:
|
||||||
|
# repack in dict form
|
||||||
# NOTE: pattern fuzzy-matching is done within
|
await stream.send(
|
||||||
# the methd impl.
|
await client.search_symbols(pattern))
|
||||||
pairs: dict[str, Pair] = await client.search_symbols(
|
|
||||||
pattern,
|
|
||||||
)
|
|
||||||
# repack in fqme-keyed table
|
|
||||||
byfqme: dict[str, Pair] = {}
|
|
||||||
for pair in pairs.values():
|
|
||||||
byfqme[pair.bs_fqme] = pair
|
|
||||||
|
|
||||||
await stream.send(byfqme)
|
|
||||||
|
|
|
||||||
|
|
@ -1,196 +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/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Per market data-type definitions and schemas types.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
import pendulum
|
|
||||||
from typing import (
|
|
||||||
Literal,
|
|
||||||
Optional,
|
|
||||||
)
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from piker.types import Struct
|
|
||||||
|
|
||||||
|
|
||||||
# API endpoint paths by venue / sub-API
|
|
||||||
_domain: str = 'deribit.com'
|
|
||||||
_url = f'https://www.{_domain}'
|
|
||||||
|
|
||||||
# WEBsocketz
|
|
||||||
_ws_url: str = f'wss://www.{_domain}/ws/api/v2'
|
|
||||||
|
|
||||||
# test nets
|
|
||||||
_testnet_ws_url: str = f'wss://test.{_domain}/ws/api/v2'
|
|
||||||
|
|
||||||
MarketType = Literal[
|
|
||||||
'option'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_eps(venue: MarketType) -> tuple[str, str]:
|
|
||||||
'''
|
|
||||||
Return API ep root paths per venue.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return {
|
|
||||||
'option': (
|
|
||||||
_ws_url,
|
|
||||||
),
|
|
||||||
}[venue]
|
|
||||||
|
|
||||||
|
|
||||||
class Pair(Struct, frozen=True, kw_only=True):
|
|
||||||
|
|
||||||
symbol: str
|
|
||||||
|
|
||||||
# src
|
|
||||||
quote_currency: str # 'BTC'
|
|
||||||
|
|
||||||
# dst
|
|
||||||
base_currency: str # "BTC",
|
|
||||||
|
|
||||||
tick_size: float # 0.0001 # [{'above_price': 0.005, 'tick_size': 0.0005}]
|
|
||||||
tick_size_steps: list[dict[str, float]]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def price_tick(self) -> Decimal:
|
|
||||||
return Decimal(str(self.tick_size_steps[0]['above_price']))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size_tick(self) -> Decimal:
|
|
||||||
return Decimal(str(self.tick_size))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bs_fqme(self) -> str:
|
|
||||||
return f'{self.symbol}'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bs_mktid(self) -> str:
|
|
||||||
return f'{self.symbol}.{self.venue}'
|
|
||||||
|
|
||||||
|
|
||||||
class OptionPair(Pair, frozen=True):
|
|
||||||
|
|
||||||
taker_commission: float # 0.0003
|
|
||||||
strike: float # 5000.0
|
|
||||||
settlement_period: str # 'day'
|
|
||||||
settlement_currency: str # "BTC",
|
|
||||||
rfq: bool # false
|
|
||||||
price_index: str # 'btc_usd'
|
|
||||||
option_type: str # 'call'
|
|
||||||
min_trade_amount: float # 0.1
|
|
||||||
maker_commission: float # 0.0003
|
|
||||||
kind: str # 'option'
|
|
||||||
is_active: bool # true
|
|
||||||
instrument_type: str # 'reversed'
|
|
||||||
instrument_name: str # 'BTC-1SEP24-55000-C'
|
|
||||||
instrument_id: int # 364671
|
|
||||||
expiration_timestamp: int # 1725177600000
|
|
||||||
creation_timestamp: int # 1724918461000
|
|
||||||
counter_currency: str # 'USD'
|
|
||||||
contract_size: float # '1.0'
|
|
||||||
block_trade_tick_size: float # '0.0001'
|
|
||||||
block_trade_min_trade_amount: int # '25'
|
|
||||||
block_trade_commission: float # '0.003'
|
|
||||||
|
|
||||||
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
|
|
||||||
ns_path: str = 'piker.brokers.deribit:OptionPair'
|
|
||||||
|
|
||||||
# TODO, impl this without the MM:SS part of
|
|
||||||
# the `'THH:MM:SS..'` etc..
|
|
||||||
@property
|
|
||||||
def expiry(self) -> str:
|
|
||||||
iso_date = pendulum.from_timestamp(
|
|
||||||
self.expiration_timestamp / 1000
|
|
||||||
).isoformat()
|
|
||||||
return iso_date
|
|
||||||
|
|
||||||
@property
|
|
||||||
def venue(self) -> str:
|
|
||||||
return f'{self.instrument_type}_option'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bs_fqme(self) -> str:
|
|
||||||
return f'{self.symbol}'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bs_src_asset(self) -> str:
|
|
||||||
return f'{self.quote_currency}'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bs_dst_asset(self) -> str:
|
|
||||||
return f'{self.symbol}'
|
|
||||||
|
|
||||||
|
|
||||||
PAIRTYPES: dict[MarketType, Pair] = {
|
|
||||||
'option': OptionPair,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class JSONRPCResult(Struct):
|
|
||||||
id: int
|
|
||||||
usIn: int
|
|
||||||
usOut: int
|
|
||||||
usDiff: int
|
|
||||||
testnet: bool
|
|
||||||
jsonrpc: str = '2.0'
|
|
||||||
error: Optional[dict] = None
|
|
||||||
result: Optional[list[dict]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class JSONRPCChannel(Struct):
|
|
||||||
method: str
|
|
||||||
params: dict
|
|
||||||
jsonrpc: str = '2.0'
|
|
||||||
|
|
||||||
|
|
||||||
class KLinesResult(Struct):
|
|
||||||
low: list[float]
|
|
||||||
cost: list[float]
|
|
||||||
high: list[float]
|
|
||||||
open: list[float]
|
|
||||||
close: list[float]
|
|
||||||
ticks: list[int]
|
|
||||||
status: str
|
|
||||||
volume: list[float]
|
|
||||||
|
|
||||||
|
|
||||||
class Trade(Struct):
|
|
||||||
iv: float
|
|
||||||
price: float
|
|
||||||
amount: float
|
|
||||||
trade_id: str
|
|
||||||
contracts: float
|
|
||||||
direction: str
|
|
||||||
trade_seq: int
|
|
||||||
timestamp: int
|
|
||||||
mark_price: float
|
|
||||||
index_price: float
|
|
||||||
tick_direction: int
|
|
||||||
instrument_name: str
|
|
||||||
combo_id: Optional[str] = '',
|
|
||||||
combo_trade_id: Optional[int] = 0,
|
|
||||||
block_trade_id: Optional[str] = '',
|
|
||||||
block_trade_leg_count: Optional[int] = 0,
|
|
||||||
|
|
||||||
|
|
||||||
class LastTradesResult(Struct):
|
|
||||||
trades: list[Trade]
|
|
||||||
has_more: bool
|
|
||||||
|
|
@ -97,10 +97,6 @@ from ._util import (
|
||||||
get_logger,
|
get_logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ?TODO? this can now be removed since it was originally to extend
|
|
||||||
# with a `bar_vwap` field that we removed from the default ohlcv
|
|
||||||
# dtype since it's better calculated in an FSP func
|
|
||||||
#
|
|
||||||
_bar_load_dtype: list[tuple[str, type]] = [
|
_bar_load_dtype: list[tuple[str, type]] = [
|
||||||
# NOTE XXX: only part that's diff
|
# NOTE XXX: only part that's diff
|
||||||
# from our default fields where
|
# from our default fields where
|
||||||
|
|
|
||||||
|
|
@ -613,7 +613,7 @@ async def get_bars(
|
||||||
data_cs.cancel()
|
data_cs.cancel()
|
||||||
|
|
||||||
# spawn new data reset task
|
# spawn new data reset task
|
||||||
data_cs, reset_done = await nurse.start(
|
data_cs, reset_done = await tn.start(
|
||||||
partial(
|
partial(
|
||||||
wait_on_data_reset,
|
wait_on_data_reset,
|
||||||
proxy,
|
proxy,
|
||||||
|
|
@ -635,12 +635,12 @@ async def get_bars(
|
||||||
unset_resetter: bool = False
|
unset_resetter: bool = False
|
||||||
async with (
|
async with (
|
||||||
tractor.trionics.collapse_eg(),
|
tractor.trionics.collapse_eg(),
|
||||||
trio.open_nursery() as nurse
|
trio.open_nursery() as tn
|
||||||
):
|
):
|
||||||
|
|
||||||
# start history request that we allow
|
# start history request that we allow
|
||||||
# to run indefinitely until a result is acquired
|
# to run indefinitely until a result is acquired
|
||||||
nurse.start_soon(query)
|
tn.start_soon(query)
|
||||||
|
|
||||||
# start history reset loop which waits up to the timeout
|
# start history reset loop which waits up to the timeout
|
||||||
# for a result before triggering a data feed reset.
|
# for a result before triggering a data feed reset.
|
||||||
|
|
@ -660,7 +660,7 @@ async def get_bars(
|
||||||
unset_resetter: bool = True
|
unset_resetter: bool = True
|
||||||
|
|
||||||
# spawn new data reset task
|
# spawn new data reset task
|
||||||
data_cs, reset_done = await nurse.start(
|
data_cs, reset_done = await tn.start(
|
||||||
partial(
|
partial(
|
||||||
wait_on_data_reset,
|
wait_on_data_reset,
|
||||||
proxy,
|
proxy,
|
||||||
|
|
@ -896,7 +896,10 @@ async def open_aio_quote_stream(
|
||||||
symbol: str,
|
symbol: str,
|
||||||
contract: Contract|None = None,
|
contract: Contract|None = None,
|
||||||
|
|
||||||
) -> trio.abc.ReceiveStream:
|
) -> (
|
||||||
|
trio.abc.Channel| # iface
|
||||||
|
tractor.to_asyncio.LinkedTaskChannel # actually
|
||||||
|
):
|
||||||
'''
|
'''
|
||||||
Open a real-time `Ticker` quote stream from an `asyncio.Task`
|
Open a real-time `Ticker` quote stream from an `asyncio.Task`
|
||||||
spawned via `tractor.to_asyncio.open_channel_from()`, deliver the
|
spawned via `tractor.to_asyncio.open_channel_from()`, deliver the
|
||||||
|
|
@ -919,6 +922,7 @@ async def open_aio_quote_stream(
|
||||||
yield from_aio
|
yield from_aio
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from_aio: tractor.to_asyncio.LinkedTaskChannel
|
||||||
async with tractor.to_asyncio.open_channel_from(
|
async with tractor.to_asyncio.open_channel_from(
|
||||||
_setup_quote_stream,
|
_setup_quote_stream,
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
|
|
@ -1164,6 +1168,7 @@ async def stream_quotes(
|
||||||
)
|
)
|
||||||
cs: trio.CancelScope|None = None
|
cs: trio.CancelScope|None = None
|
||||||
startup: bool = True
|
startup: bool = True
|
||||||
|
iter_quotes: trio.abc.Channel
|
||||||
while (
|
while (
|
||||||
startup
|
startup
|
||||||
or
|
or
|
||||||
|
|
@ -1172,11 +1177,11 @@ async def stream_quotes(
|
||||||
with trio.CancelScope() as cs:
|
with trio.CancelScope() as cs:
|
||||||
async with (
|
async with (
|
||||||
tractor.trionics.collapse_eg(),
|
tractor.trionics.collapse_eg(),
|
||||||
trio.open_nursery() as nurse,
|
trio.open_nursery() as tn,
|
||||||
open_aio_quote_stream(
|
open_aio_quote_stream(
|
||||||
symbol=sym,
|
symbol=sym,
|
||||||
contract=con,
|
contract=con,
|
||||||
) as stream,
|
) as iter_quotes,
|
||||||
):
|
):
|
||||||
# ?TODO? can we rm this - particularly for `ib_async`?
|
# ?TODO? can we rm this - particularly for `ib_async`?
|
||||||
# ugh, clear ticks since we've consumed them
|
# ugh, clear ticks since we've consumed them
|
||||||
|
|
@ -1205,9 +1210,9 @@ async def stream_quotes(
|
||||||
await rt_ev.wait()
|
await rt_ev.wait()
|
||||||
cs.cancel() # cancel called should now be set
|
cs.cancel() # cancel called should now be set
|
||||||
|
|
||||||
nurse.start_soon(reset_on_feed)
|
tn.start_soon(reset_on_feed)
|
||||||
|
|
||||||
async with aclosing(stream):
|
async with aclosing(iter_quotes):
|
||||||
# if syminfo.get('no_vlm', False):
|
# if syminfo.get('no_vlm', False):
|
||||||
if not init_msg.shm_write_opts['has_vlm']:
|
if not init_msg.shm_write_opts['has_vlm']:
|
||||||
|
|
||||||
|
|
@ -1222,19 +1227,21 @@ async def stream_quotes(
|
||||||
# wait for real volume on feed (trading might be
|
# wait for real volume on feed (trading might be
|
||||||
# closed)
|
# closed)
|
||||||
while True:
|
while True:
|
||||||
ticker = await stream.receive()
|
ticker = await iter_quotes.receive()
|
||||||
|
|
||||||
# for a real volume contract we rait for
|
# for a real volume contract we rait for
|
||||||
# the first "real" trade to take place
|
# the first "real" trade to take place
|
||||||
if (
|
if (
|
||||||
# not calc_price
|
# not calc_price
|
||||||
# and not ticker.rtTime
|
# and not ticker.rtTime
|
||||||
not ticker.rtTime
|
False
|
||||||
|
# not ticker.rtTime
|
||||||
):
|
):
|
||||||
# spin consuming tickers until we
|
# spin consuming tickers until we
|
||||||
# get a real market datum
|
# get a real market datum
|
||||||
log.debug(f"New unsent ticker: {ticker}")
|
log.debug(f"New unsent ticker: {ticker}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug("Received first volume tick")
|
log.debug("Received first volume tick")
|
||||||
# ugh, clear ticks since we've
|
# ugh, clear ticks since we've
|
||||||
|
|
@ -1250,13 +1257,18 @@ async def stream_quotes(
|
||||||
log.debug(f"First ticker received {quote}")
|
log.debug(f"First ticker received {quote}")
|
||||||
|
|
||||||
# tell data-layer spawner-caller that live
|
# tell data-layer spawner-caller that live
|
||||||
# quotes are now streaming.
|
# quotes are now active desptie not having
|
||||||
|
# necessarily received a first vlm/clearing
|
||||||
|
# tick.
|
||||||
|
ticker = await iter_quotes.receive()
|
||||||
feed_is_live.set()
|
feed_is_live.set()
|
||||||
|
fqme: str = quote['fqme']
|
||||||
|
await send_chan.send({fqme: quote})
|
||||||
|
|
||||||
# last = time.time()
|
# last = time.time()
|
||||||
async for ticker in stream:
|
async for ticker in iter_quotes:
|
||||||
quote = normalize(ticker)
|
quote = normalize(ticker)
|
||||||
fqme = quote['fqme']
|
fqme: str = quote['fqme']
|
||||||
log.debug(
|
log.debug(
|
||||||
f'Sending quote\n'
|
f'Sending quote\n'
|
||||||
f'{quote}'
|
f'{quote}'
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,7 @@ async def open_trade_dialog(
|
||||||
# to be reloaded.
|
# to be reloaded.
|
||||||
balances: dict[str, float] = await client.get_balances()
|
balances: dict[str, float] = await client.get_balances()
|
||||||
|
|
||||||
verify_balances(
|
await verify_balances(
|
||||||
acnt,
|
acnt,
|
||||||
src_fiat,
|
src_fiat,
|
||||||
balances,
|
balances,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,12 @@ import tractor
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
|
# TODO, port to `httpx`/`trio-websocket` whenver i get back to
|
||||||
|
# writing a proper ws-api streamer for this backend (since the data
|
||||||
|
# feeds are free now) as per GH feat-req:
|
||||||
|
# https://github.com/pikers/piker/issues/509
|
||||||
|
#
|
||||||
import asks
|
import asks
|
||||||
|
|
||||||
from ..calc import humanize, percent_change
|
from ..calc import humanize, percent_change
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,6 @@ class OrderClient(Struct):
|
||||||
|
|
||||||
|
|
||||||
async def relay_orders_from_sync_code(
|
async def relay_orders_from_sync_code(
|
||||||
|
|
||||||
client: OrderClient,
|
client: OrderClient,
|
||||||
symbol_key: str,
|
symbol_key: str,
|
||||||
to_ems_stream: tractor.MsgStream,
|
to_ems_stream: tractor.MsgStream,
|
||||||
|
|
@ -245,6 +244,11 @@ async def open_ems(
|
||||||
|
|
||||||
async with maybe_open_emsd(
|
async with maybe_open_emsd(
|
||||||
broker,
|
broker,
|
||||||
|
# XXX NOTE, LOL so this determines the daemon `emsd` loglevel
|
||||||
|
# then FYI.. that's kinda wrong no?
|
||||||
|
# -[ ] shouldn't it be set by `pikerd -l` or no?
|
||||||
|
# -[ ] would make a lot more sense to have a subsys ctl for
|
||||||
|
# levels.. like `-l emsd.info` or something?
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
) as portal:
|
) as portal:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -655,7 +655,11 @@ class Router(Struct):
|
||||||
flume = feed.flumes[fqme]
|
flume = feed.flumes[fqme]
|
||||||
first_quote: dict = flume.first_quote
|
first_quote: dict = flume.first_quote
|
||||||
book: DarkBook = self.get_dark_book(broker)
|
book: DarkBook = self.get_dark_book(broker)
|
||||||
book.lasts[fqme]: float = float(first_quote['last'])
|
|
||||||
|
if not (last := first_quote.get('last')):
|
||||||
|
last: float = flume.rt_shm.array[-1]['close']
|
||||||
|
|
||||||
|
book.lasts[fqme]: float = float(last)
|
||||||
|
|
||||||
async with self.maybe_open_brokerd_dialog(
|
async with self.maybe_open_brokerd_dialog(
|
||||||
brokermod=brokermod,
|
brokermod=brokermod,
|
||||||
|
|
@ -718,7 +722,7 @@ class Router(Struct):
|
||||||
subs = self.subscribers[sub_key]
|
subs = self.subscribers[sub_key]
|
||||||
|
|
||||||
sent_some: bool = False
|
sent_some: bool = False
|
||||||
for client_stream in subs:
|
for client_stream in subs.copy():
|
||||||
try:
|
try:
|
||||||
await client_stream.send(msg)
|
await client_stream.send(msg)
|
||||||
sent_some = True
|
sent_some = True
|
||||||
|
|
@ -1014,10 +1018,14 @@ async def translate_and_relay_brokerd_events(
|
||||||
status_msg.brokerd_msg = msg
|
status_msg.brokerd_msg = msg
|
||||||
status_msg.src = msg.broker_details['name']
|
status_msg.src = msg.broker_details['name']
|
||||||
|
|
||||||
await router.client_broadcast(
|
if not status_msg.req:
|
||||||
status_msg.req.symbol,
|
# likely some order change state?
|
||||||
status_msg,
|
await tractor.pause()
|
||||||
)
|
else:
|
||||||
|
await router.client_broadcast(
|
||||||
|
status_msg.req.symbol,
|
||||||
|
status_msg,
|
||||||
|
)
|
||||||
|
|
||||||
if status == 'closed':
|
if status == 'closed':
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,8 @@ class PaperBoi(Struct):
|
||||||
|
|
||||||
# transmit pp msg to ems
|
# transmit pp msg to ems
|
||||||
pp: Position = self.acnt.pps[bs_mktid]
|
pp: Position = self.acnt.pps[bs_mktid]
|
||||||
|
# TODO, this will break if `require_only=True` was passed to
|
||||||
|
# `.update_from_ledger()`
|
||||||
|
|
||||||
pp_msg = BrokerdPosition(
|
pp_msg = BrokerdPosition(
|
||||||
broker=self.broker,
|
broker=self.broker,
|
||||||
|
|
@ -653,6 +655,7 @@ async def open_trade_dialog(
|
||||||
# in) use manually constructed table from calling
|
# in) use manually constructed table from calling
|
||||||
# the `.get_mkt_info()` provider EP above.
|
# the `.get_mkt_info()` provider EP above.
|
||||||
_mktmap_table=mkt_by_fqme,
|
_mktmap_table=mkt_by_fqme,
|
||||||
|
only_require=list(mkt_by_fqme),
|
||||||
)
|
)
|
||||||
|
|
||||||
pp_msgs: list[BrokerdPosition] = []
|
pp_msgs: list[BrokerdPosition] = []
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ subsys: str = 'piker.clearing'
|
||||||
|
|
||||||
log = get_logger(subsys)
|
log = get_logger(subsys)
|
||||||
|
|
||||||
|
# TODO, oof doesn't this ignore the `loglevel` then???
|
||||||
get_console_log = partial(
|
get_console_log = partial(
|
||||||
get_console_log,
|
get_console_log,
|
||||||
name=subsys,
|
name=subsys,
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,12 @@ class Sampler:
|
||||||
# history loading.
|
# history loading.
|
||||||
incr_task_cs: trio.CancelScope | None = None
|
incr_task_cs: trio.CancelScope | None = None
|
||||||
|
|
||||||
|
bcast_errors: tuple[Exception] = (
|
||||||
|
trio.BrokenResourceError,
|
||||||
|
trio.ClosedResourceError,
|
||||||
|
trio.EndOfChannel,
|
||||||
|
)
|
||||||
|
|
||||||
# holds all the ``tractor.Context`` remote subscriptions for
|
# holds all the ``tractor.Context`` remote subscriptions for
|
||||||
# a particular sample period increment event: all subscribers are
|
# a particular sample period increment event: all subscribers are
|
||||||
# notified on a step.
|
# notified on a step.
|
||||||
|
|
@ -258,14 +264,15 @@ class Sampler:
|
||||||
subs: set
|
subs: set
|
||||||
last_ts, subs = pair
|
last_ts, subs = pair
|
||||||
|
|
||||||
task = trio.lowlevel.current_task()
|
# NOTE, for debugging pub-sub issues
|
||||||
log.debug(
|
# task = trio.lowlevel.current_task()
|
||||||
f'SUBS {self.subscribers}\n'
|
# log.debug(
|
||||||
f'PAIR {pair}\n'
|
# f'AlL-SUBS@{period_s!r}: {self.subscribers}\n'
|
||||||
f'TASK: {task}: {id(task)}\n'
|
# f'PAIR: {pair}\n'
|
||||||
f'broadcasting {period_s} -> {last_ts}\n'
|
# f'TASK: {task}: {id(task)}\n'
|
||||||
# f'consumers: {subs}'
|
# f'broadcasting {period_s} -> {last_ts}\n'
|
||||||
)
|
# f'consumers: {subs}'
|
||||||
|
# )
|
||||||
borked: set[MsgStream] = set()
|
borked: set[MsgStream] = set()
|
||||||
sent: set[MsgStream] = set()
|
sent: set[MsgStream] = set()
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -282,13 +289,11 @@ class Sampler:
|
||||||
await stream.send(msg)
|
await stream.send(msg)
|
||||||
sent.add(stream)
|
sent.add(stream)
|
||||||
|
|
||||||
except (
|
except self.bcast_errors as err:
|
||||||
trio.BrokenResourceError,
|
|
||||||
trio.ClosedResourceError,
|
|
||||||
trio.EndOfChannel,
|
|
||||||
):
|
|
||||||
log.error(
|
log.error(
|
||||||
f'{stream._ctx.chan.uid} dropped connection'
|
f'Connection dropped for IPC ctx\n'
|
||||||
|
f'{stream._ctx}\n\n'
|
||||||
|
f'Due to {type(err)}'
|
||||||
)
|
)
|
||||||
borked.add(stream)
|
borked.add(stream)
|
||||||
else:
|
else:
|
||||||
|
|
@ -395,7 +400,8 @@ async def register_with_sampler(
|
||||||
finally:
|
finally:
|
||||||
if (
|
if (
|
||||||
sub_for_broadcasts
|
sub_for_broadcasts
|
||||||
and subs
|
and
|
||||||
|
subs
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
subs.remove(stream)
|
subs.remove(stream)
|
||||||
|
|
@ -562,8 +568,7 @@ async def open_sample_stream(
|
||||||
|
|
||||||
|
|
||||||
async def sample_and_broadcast(
|
async def sample_and_broadcast(
|
||||||
|
bus: _FeedsBus,
|
||||||
bus: _FeedsBus, # noqa
|
|
||||||
rt_shm: ShmArray,
|
rt_shm: ShmArray,
|
||||||
hist_shm: ShmArray,
|
hist_shm: ShmArray,
|
||||||
quote_stream: trio.abc.ReceiveChannel,
|
quote_stream: trio.abc.ReceiveChannel,
|
||||||
|
|
@ -583,11 +588,33 @@ async def sample_and_broadcast(
|
||||||
|
|
||||||
overruns = Counter()
|
overruns = Counter()
|
||||||
|
|
||||||
|
# NOTE, only used for debugging live-data-feed issues, though
|
||||||
|
# this should be resolved more correctly in the future using the
|
||||||
|
# new typed-msgspec feats of `tractor`!
|
||||||
|
#
|
||||||
|
# XXX, a multiline nested `dict` formatter (since rn quote-msgs
|
||||||
|
# are just that).
|
||||||
|
# pfmt: Callable[[str], str] = mk_repr()
|
||||||
|
|
||||||
# iterate stream delivered by broker
|
# iterate stream delivered by broker
|
||||||
async for quotes in quote_stream:
|
async for quotes in quote_stream:
|
||||||
# print(quotes)
|
# print(quotes)
|
||||||
|
|
||||||
# TODO: ``numba`` this!
|
# XXX WARNING XXX only enable for debugging bc ow can cost
|
||||||
|
# ALOT of perf with HF-feedz!!!
|
||||||
|
#
|
||||||
|
# log.info(
|
||||||
|
# 'Rx live quotes:\n'
|
||||||
|
# f'{pfmt(quotes)}'
|
||||||
|
# )
|
||||||
|
|
||||||
|
# TODO,
|
||||||
|
# -[ ] `numba` or `cython`-nize this loop possibly?
|
||||||
|
# |_alternatively could we do it in rust somehow by upacking
|
||||||
|
# arrow msgs instead of using `msgspec`?
|
||||||
|
# -[ ] use `msgspec.Struct` support in new typed-msging from
|
||||||
|
# `tractor` to ensure only allowed msgs are transmitted?
|
||||||
|
#
|
||||||
for broker_symbol, quote in quotes.items():
|
for broker_symbol, quote in quotes.items():
|
||||||
# TODO: in theory you can send the IPC msg *before* writing
|
# TODO: in theory you can send the IPC msg *before* writing
|
||||||
# to the sharedmem array to decrease latency, however, that
|
# to the sharedmem array to decrease latency, however, that
|
||||||
|
|
@ -660,6 +687,21 @@ async def sample_and_broadcast(
|
||||||
sub_key: str = broker_symbol.lower()
|
sub_key: str = broker_symbol.lower()
|
||||||
subs: set[Sub] = bus.get_subs(sub_key)
|
subs: set[Sub] = bus.get_subs(sub_key)
|
||||||
|
|
||||||
|
# TODO, figure out how to make this useful whilst
|
||||||
|
# incoporating feed "pausing" ..
|
||||||
|
#
|
||||||
|
# if not subs:
|
||||||
|
# all_bs_fqmes: list[str] = list(
|
||||||
|
# bus._subscribers.keys()
|
||||||
|
# )
|
||||||
|
# log.warning(
|
||||||
|
# f'No subscribers for {brokername!r} live-quote ??\n'
|
||||||
|
# f'broker_symbol: {broker_symbol}\n\n'
|
||||||
|
|
||||||
|
# f'Maybe the backend-sys symbol does not match one of,\n'
|
||||||
|
# f'{pfmt(all_bs_fqmes)}\n'
|
||||||
|
# )
|
||||||
|
|
||||||
# NOTE: by default the broker backend doesn't append
|
# NOTE: by default the broker backend doesn't append
|
||||||
# it's own "name" into the fqme schema (but maybe it
|
# it's own "name" into the fqme schema (but maybe it
|
||||||
# should?) so we have to manually generate the correct
|
# should?) so we have to manually generate the correct
|
||||||
|
|
@ -729,18 +771,14 @@ async def sample_and_broadcast(
|
||||||
if lags > 10:
|
if lags > 10:
|
||||||
await tractor.pause()
|
await tractor.pause()
|
||||||
|
|
||||||
except (
|
except Sampler.bcast_errors as ipc_err:
|
||||||
trio.BrokenResourceError,
|
|
||||||
trio.ClosedResourceError,
|
|
||||||
trio.EndOfChannel,
|
|
||||||
):
|
|
||||||
ctx: Context = ipc._ctx
|
ctx: Context = ipc._ctx
|
||||||
chan: Channel = ctx.chan
|
chan: Channel = ctx.chan
|
||||||
if ctx:
|
if ctx:
|
||||||
log.warning(
|
log.warning(
|
||||||
'Dropped `brokerd`-quotes-feed connection:\n'
|
f'Dropped `brokerd`-feed for {broker_symbol!r} due to,\n'
|
||||||
f'{broker_symbol}:'
|
f'x>) {ctx.cid}@{chan.uid}'
|
||||||
f'{ctx.cid}@{chan.uid}'
|
f'|_{ipc_err!r}\n\n'
|
||||||
)
|
)
|
||||||
if sub.throttle_rate:
|
if sub.throttle_rate:
|
||||||
assert ipc._closed
|
assert ipc._closed
|
||||||
|
|
@ -757,12 +795,11 @@ async def sample_and_broadcast(
|
||||||
|
|
||||||
|
|
||||||
async def uniform_rate_send(
|
async def uniform_rate_send(
|
||||||
|
|
||||||
rate: float,
|
rate: float,
|
||||||
quote_stream: trio.abc.ReceiveChannel,
|
quote_stream: trio.abc.ReceiveChannel,
|
||||||
stream: MsgStream,
|
stream: MsgStream,
|
||||||
|
|
||||||
task_status: TaskStatus = trio.TASK_STATUS_IGNORED,
|
task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -780,13 +817,16 @@ async def uniform_rate_send(
|
||||||
https://gist.github.com/njsmith/7ea44ec07e901cb78ebe1dd8dd846cb9
|
https://gist.github.com/njsmith/7ea44ec07e901cb78ebe1dd8dd846cb9
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# TODO: compute the approx overhead latency per cycle
|
# ?TODO? dynamically compute the **actual** approx overhead latency per cycle
|
||||||
left_to_sleep = throttle_period = 1/rate - 0.000616
|
# instead of this magic # bidinezz?
|
||||||
|
throttle_period: float = 1/rate - 0.000616
|
||||||
|
left_to_sleep: float = throttle_period
|
||||||
|
|
||||||
# send cycle state
|
# send cycle state
|
||||||
|
first_quote: dict|None
|
||||||
first_quote = last_quote = None
|
first_quote = last_quote = None
|
||||||
last_send = time.time()
|
last_send: float = time.time()
|
||||||
diff = 0
|
diff: float = 0
|
||||||
|
|
||||||
task_status.started()
|
task_status.started()
|
||||||
ticks_by_type: dict[
|
ticks_by_type: dict[
|
||||||
|
|
@ -797,22 +837,28 @@ async def uniform_rate_send(
|
||||||
clear_types = _tick_groups['clears']
|
clear_types = _tick_groups['clears']
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
# compute the remaining time to sleep for this throttled cycle
|
# compute the remaining time to sleep for this throttled cycle
|
||||||
left_to_sleep = throttle_period - diff
|
left_to_sleep: float = throttle_period - diff
|
||||||
|
|
||||||
if left_to_sleep > 0:
|
if left_to_sleep > 0:
|
||||||
|
cs: trio.CancelScope
|
||||||
with trio.move_on_after(left_to_sleep) as cs:
|
with trio.move_on_after(left_to_sleep) as cs:
|
||||||
|
sym: str
|
||||||
|
last_quote: dict
|
||||||
try:
|
try:
|
||||||
sym, last_quote = await quote_stream.receive()
|
sym, last_quote = await quote_stream.receive()
|
||||||
except trio.EndOfChannel:
|
except trio.EndOfChannel:
|
||||||
log.exception(f"feed for {stream} ended?")
|
log.exception(
|
||||||
|
f'Live stream for feed for ended?\n'
|
||||||
|
f'<=c\n'
|
||||||
|
f' |_[{stream!r}\n'
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
diff = time.time() - last_send
|
diff: float = time.time() - last_send
|
||||||
|
|
||||||
if not first_quote:
|
if not first_quote:
|
||||||
first_quote = last_quote
|
first_quote: float = last_quote
|
||||||
# first_quote['tbt'] = ticks_by_type
|
# first_quote['tbt'] = ticks_by_type
|
||||||
|
|
||||||
if (throttle_period - diff) > 0:
|
if (throttle_period - diff) > 0:
|
||||||
|
|
@ -873,11 +919,12 @@ async def uniform_rate_send(
|
||||||
# TODO: now if only we could sync this to the display
|
# TODO: now if only we could sync this to the display
|
||||||
# rate timing exactly lul
|
# rate timing exactly lul
|
||||||
try:
|
try:
|
||||||
await stream.send({sym: first_quote})
|
await stream.send({
|
||||||
|
sym: first_quote
|
||||||
|
})
|
||||||
except tractor.RemoteActorError as rme:
|
except tractor.RemoteActorError as rme:
|
||||||
if rme.type is not tractor._exceptions.StreamOverrun:
|
if rme.type is not tractor._exceptions.StreamOverrun:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
ctx = stream._ctx
|
ctx = stream._ctx
|
||||||
chan = ctx.chan
|
chan = ctx.chan
|
||||||
log.warning(
|
log.warning(
|
||||||
|
|
@ -885,20 +932,28 @@ async def uniform_rate_send(
|
||||||
f'{sym}:{ctx.cid}@{chan.uid}'
|
f'{sym}:{ctx.cid}@{chan.uid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# NOTE: any of these can be raised by `tractor`'s IPC
|
||||||
|
# transport-layer and we want to be highly resilient
|
||||||
|
# to consumers which crash or lose network connection.
|
||||||
|
# I.e. we **DO NOT** want to crash and propagate up to
|
||||||
|
# ``pikerd`` these kinds of errors!
|
||||||
except (
|
except (
|
||||||
# NOTE: any of these can be raised by ``tractor``'s IPC
|
|
||||||
# transport-layer and we want to be highly resilient
|
|
||||||
# to consumers which crash or lose network connection.
|
|
||||||
# I.e. we **DO NOT** want to crash and propagate up to
|
|
||||||
# ``pikerd`` these kinds of errors!
|
|
||||||
trio.ClosedResourceError,
|
|
||||||
trio.BrokenResourceError,
|
|
||||||
ConnectionResetError,
|
ConnectionResetError,
|
||||||
trio.EndOfChannel,
|
) + Sampler.bcast_errors as ipc_err:
|
||||||
):
|
match ipc_err:
|
||||||
# if the feed consumer goes down then drop
|
case trio.EndOfChannel():
|
||||||
# out of this rate limiter
|
log.info(
|
||||||
log.warning(f'{stream} closed')
|
f'{stream} terminated by peer,\n'
|
||||||
|
f'{ipc_err!r}'
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
# if the feed consumer goes down then drop
|
||||||
|
# out of this rate limiter
|
||||||
|
log.warning(
|
||||||
|
f'{stream} closed due to,\n'
|
||||||
|
f'{ipc_err!r}'
|
||||||
|
)
|
||||||
|
|
||||||
await stream.aclose()
|
await stream.aclose()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ from pathlib import Path
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
Callable,
|
||||||
Sequence,
|
Sequence,
|
||||||
Hashable,
|
Hashable,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
|
@ -56,7 +57,7 @@ from piker.brokers import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..accounting import (
|
from piker.accounting import (
|
||||||
Asset,
|
Asset,
|
||||||
MktPair,
|
MktPair,
|
||||||
)
|
)
|
||||||
|
|
@ -161,57 +162,68 @@ class SymbologyCache(Struct):
|
||||||
'Implement `Client.get_assets()`!'
|
'Implement `Client.get_assets()`!'
|
||||||
)
|
)
|
||||||
|
|
||||||
if get_mkt_pairs := getattr(client, 'get_mkt_pairs', None):
|
get_mkt_pairs: Callable|None = getattr(
|
||||||
|
client,
|
||||||
pairs: dict[str, Struct] = await get_mkt_pairs()
|
'get_mkt_pairs',
|
||||||
for bs_fqme, pair in pairs.items():
|
None,
|
||||||
|
)
|
||||||
# NOTE: every backend defined pair should
|
if not get_mkt_pairs:
|
||||||
# declare it's ns path for roundtrip
|
|
||||||
# serialization lookup.
|
|
||||||
if not getattr(pair, 'ns_path', None):
|
|
||||||
raise TypeError(
|
|
||||||
f'Pair-struct for {self.mod.name} MUST define a '
|
|
||||||
'`.ns_path: str`!\n'
|
|
||||||
f'{pair}'
|
|
||||||
)
|
|
||||||
|
|
||||||
entry = await self.mod.get_mkt_info(pair.bs_fqme)
|
|
||||||
if not entry:
|
|
||||||
continue
|
|
||||||
|
|
||||||
mkt: MktPair
|
|
||||||
pair: Struct
|
|
||||||
mkt, _pair = entry
|
|
||||||
assert _pair is pair, (
|
|
||||||
f'`{self.mod.name}` backend probably has a '
|
|
||||||
'keying-symmetry problem between the pair-`Struct` '
|
|
||||||
'returned from `Client.get_mkt_pairs()`and the '
|
|
||||||
'module level endpoint: `.get_mkt_info()`\n\n'
|
|
||||||
"Here's the struct diff:\n"
|
|
||||||
f'{_pair - pair}'
|
|
||||||
)
|
|
||||||
# NOTE XXX: this means backends MUST implement
|
|
||||||
# a `Struct.bs_mktid: str` field to provide
|
|
||||||
# a native-keyed map to their own symbol
|
|
||||||
# set(s).
|
|
||||||
self.pairs[pair.bs_mktid] = pair
|
|
||||||
|
|
||||||
# NOTE: `MktPair`s are keyed here using piker's
|
|
||||||
# internal FQME schema so that search,
|
|
||||||
# accounting and feed init can be accomplished
|
|
||||||
# a sane, uniform, normalized basis.
|
|
||||||
self.mktmaps[mkt.fqme] = mkt
|
|
||||||
|
|
||||||
self.pair_ns_path: str = tractor.msg.NamespacePath.from_ref(
|
|
||||||
pair,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
log.warning(
|
log.warning(
|
||||||
'No symbology cache `Pair` support for `{provider}`..\n'
|
'No symbology cache `Pair` support for `{provider}`..\n'
|
||||||
'Implement `Client.get_mkt_pairs()`!'
|
'Implement `Client.get_mkt_pairs()`!'
|
||||||
)
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
pairs: dict[str, Struct] = await get_mkt_pairs()
|
||||||
|
if not pairs:
|
||||||
|
log.warning(
|
||||||
|
'No pairs from intial {provider!r} sym-cache request?\n\n'
|
||||||
|
'`Client.get_mkt_pairs()` -> {pairs!r} ?'
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
for bs_fqme, pair in pairs.items():
|
||||||
|
if not getattr(pair, 'ns_path', None):
|
||||||
|
# XXX: every backend defined pair must declare
|
||||||
|
# a `.ns_path: tractor.NamespacePath` to enable
|
||||||
|
# roundtrip serialization lookup from a local
|
||||||
|
# cache file.
|
||||||
|
raise TypeError(
|
||||||
|
f'Pair-struct for {self.mod.name} MUST define a '
|
||||||
|
'`.ns_path: str`!\n\n'
|
||||||
|
f'{pair!r}'
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = await self.mod.get_mkt_info(pair.bs_fqme)
|
||||||
|
if not entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
mkt: MktPair
|
||||||
|
pair: Struct
|
||||||
|
mkt, _pair = entry
|
||||||
|
assert _pair is pair, (
|
||||||
|
f'`{self.mod.name}` backend probably has a '
|
||||||
|
'keying-symmetry problem between the pair-`Struct` '
|
||||||
|
'returned from `Client.get_mkt_pairs()`and the '
|
||||||
|
'module level endpoint: `.get_mkt_info()`\n\n'
|
||||||
|
"Here's the struct diff:\n"
|
||||||
|
f'{_pair - pair}'
|
||||||
|
)
|
||||||
|
# NOTE XXX: this means backends MUST implement
|
||||||
|
# a `Struct.bs_mktid: str` field to provide
|
||||||
|
# a native-keyed map to their own symbol
|
||||||
|
# set(s).
|
||||||
|
self.pairs[pair.bs_mktid] = pair
|
||||||
|
|
||||||
|
# NOTE: `MktPair`s are keyed here using piker's
|
||||||
|
# internal FQME schema so that search,
|
||||||
|
# accounting and feed init can be accomplished
|
||||||
|
# a sane, uniform, normalized basis.
|
||||||
|
self.mktmaps[mkt.fqme] = mkt
|
||||||
|
|
||||||
|
self.pair_ns_path: str = tractor.msg.NamespacePath.from_ref(
|
||||||
|
pair,
|
||||||
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -796,7 +796,6 @@ async def install_brokerd_search(
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def maybe_open_feed(
|
async def maybe_open_feed(
|
||||||
|
|
||||||
fqmes: list[str],
|
fqmes: list[str],
|
||||||
loglevel: str | None = None,
|
loglevel: str | None = None,
|
||||||
|
|
||||||
|
|
@ -850,13 +849,12 @@ async def maybe_open_feed(
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def open_feed(
|
async def open_feed(
|
||||||
|
|
||||||
fqmes: list[str],
|
fqmes: list[str],
|
||||||
|
|
||||||
loglevel: str | None = None,
|
loglevel: str|None = None,
|
||||||
allow_overruns: bool = True,
|
allow_overruns: bool = True,
|
||||||
start_stream: bool = True,
|
start_stream: bool = True,
|
||||||
tick_throttle: float | None = None, # Hz
|
tick_throttle: float|None = None, # Hz
|
||||||
|
|
||||||
allow_remote_ctl_ui: bool = False,
|
allow_remote_ctl_ui: bool = False,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,10 @@ from ._sharedmem import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
_Token,
|
_Token,
|
||||||
)
|
)
|
||||||
|
from piker.accounting import MktPair
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..accounting import MktPair
|
from piker.data.feed import Feed
|
||||||
from .feed import Feed
|
|
||||||
|
|
||||||
|
|
||||||
class Flume(Struct):
|
class Flume(Struct):
|
||||||
|
|
@ -82,7 +82,7 @@ class Flume(Struct):
|
||||||
|
|
||||||
# TODO: do we need this really if we can pull the `Portal` from
|
# TODO: do we need this really if we can pull the `Portal` from
|
||||||
# ``tractor``'s internals?
|
# ``tractor``'s internals?
|
||||||
feed: Feed | None = None
|
feed: Feed|None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rt_shm(self) -> ShmArray:
|
def rt_shm(self) -> ShmArray:
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,9 @@ def validate_backend(
|
||||||
)
|
)
|
||||||
if ep is None:
|
if ep is None:
|
||||||
log.warning(
|
log.warning(
|
||||||
f'Provider backend {mod.name} is missing '
|
f'Provider backend {mod.name!r} is missing '
|
||||||
f'{daemon_name} support :(\n'
|
f'{daemon_name!r} support?\n'
|
||||||
f'The following endpoint is missing: {name}'
|
f'|_module endpoint-func missing: {name!r}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
inits: list[
|
inits: list[
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
Log like a forester!
|
Log like a forester!
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import reprlib
|
|
||||||
import json
|
import json
|
||||||
|
import reprlib
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
)
|
)
|
||||||
|
|
@ -90,6 +90,8 @@ def colorize_json(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO, eventually defer to the version in `modden` once
|
||||||
|
# it becomes a dep!
|
||||||
def mk_repr(
|
def mk_repr(
|
||||||
**repr_kws,
|
**repr_kws,
|
||||||
) -> Callable[[str], str]:
|
) -> Callable[[str], str]:
|
||||||
|
|
|
||||||
|
|
@ -137,16 +137,6 @@ class StorageClient(
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
async def write_oi(
|
|
||||||
self,
|
|
||||||
fqme: str,
|
|
||||||
oi: np.ndarray,
|
|
||||||
append_and_duplicate: bool = True,
|
|
||||||
limit: int = int(800e3),
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class TimeseriesNotFound(Exception):
|
class TimeseriesNotFound(Exception):
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -111,24 +111,6 @@ def mk_ohlcv_shm_keyed_filepath(
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def mk_oi_shm_keyed_filepath(
|
|
||||||
fqme: str,
|
|
||||||
period: float | int,
|
|
||||||
datadir: Path,
|
|
||||||
|
|
||||||
) -> Path:
|
|
||||||
|
|
||||||
if period < 1.:
|
|
||||||
raise ValueError('Sample period should be >= 1.!?')
|
|
||||||
|
|
||||||
path: Path = (
|
|
||||||
datadir
|
|
||||||
/
|
|
||||||
f'{fqme}.oi{int(period)}s.parquet'
|
|
||||||
)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def unpack_fqme_from_parquet_filepath(path: Path) -> str:
|
def unpack_fqme_from_parquet_filepath(path: Path) -> str:
|
||||||
|
|
||||||
filename: str = str(path.name)
|
filename: str = str(path.name)
|
||||||
|
|
@ -190,11 +172,7 @@ class NativeStorageClient:
|
||||||
|
|
||||||
key: str = path.name.rstrip('.parquet')
|
key: str = path.name.rstrip('.parquet')
|
||||||
fqme, _, descr = key.rpartition('.')
|
fqme, _, descr = key.rpartition('.')
|
||||||
if 'ohlcv' in descr:
|
prefix, _, suffix = descr.partition('ohlcv')
|
||||||
prefix, _, suffix = descr.partition('ohlcv')
|
|
||||||
elif 'oi' in descr:
|
|
||||||
prefix, _, suffix = descr.partition('oi')
|
|
||||||
|
|
||||||
period: int = int(suffix.strip('s'))
|
period: int = int(suffix.strip('s'))
|
||||||
|
|
||||||
# cache description data
|
# cache description data
|
||||||
|
|
@ -391,61 +369,6 @@ class NativeStorageClient:
|
||||||
timeframe,
|
timeframe,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _write_oi(
|
|
||||||
self,
|
|
||||||
fqme: str,
|
|
||||||
oi: np.ndarray,
|
|
||||||
|
|
||||||
) -> Path:
|
|
||||||
'''
|
|
||||||
Sync version of the public interface meth, since we don't
|
|
||||||
currently actually need or support an async impl.
|
|
||||||
|
|
||||||
'''
|
|
||||||
path: Path = mk_oi_shm_keyed_filepath(
|
|
||||||
fqme=fqme,
|
|
||||||
period=1,
|
|
||||||
datadir=self._datadir,
|
|
||||||
)
|
|
||||||
if isinstance(oi, np.ndarray):
|
|
||||||
new_df: pl.DataFrame = tsp.np2pl(oi)
|
|
||||||
else:
|
|
||||||
new_df = oi
|
|
||||||
|
|
||||||
if path.exists():
|
|
||||||
old_df = pl.read_parquet(path)
|
|
||||||
df = pl.concat([old_df, new_df])
|
|
||||||
else:
|
|
||||||
df = new_df
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
df.write_parquet(path)
|
|
||||||
delay: float = round(
|
|
||||||
time.time() - start,
|
|
||||||
ndigits=6,
|
|
||||||
)
|
|
||||||
log.info(
|
|
||||||
f'parquet write took {delay} secs\n'
|
|
||||||
f'file path: {path}'
|
|
||||||
)
|
|
||||||
return path
|
|
||||||
|
|
||||||
async def write_oi(
|
|
||||||
self,
|
|
||||||
fqme: str,
|
|
||||||
oi: np.ndarray,
|
|
||||||
|
|
||||||
) -> Path:
|
|
||||||
'''
|
|
||||||
Write input oi time series for fqme and sampling period
|
|
||||||
to (local) disk.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return self._write_oi(
|
|
||||||
fqme,
|
|
||||||
oi,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def delete_ts(
|
async def delete_ts(
|
||||||
self,
|
self,
|
||||||
key: str,
|
key: str,
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,8 @@ dependencies = [
|
||||||
"trio-util >=0.7.0, <0.8.0",
|
"trio-util >=0.7.0, <0.8.0",
|
||||||
"trio-websocket >=0.10.3, <0.11.0",
|
"trio-websocket >=0.10.3, <0.11.0",
|
||||||
"typer >=0.9.0, <1.0.0",
|
"typer >=0.9.0, <1.0.0",
|
||||||
"rapidfuzz >=3.5.2, <4.0.0",
|
|
||||||
"pdbp >=1.5.0, <2.0.0",
|
|
||||||
"trio >=0.27",
|
"trio >=0.27",
|
||||||
"pendulum >=3.0.0, <4.0.0",
|
"pendulum",
|
||||||
"httpx >=0.27.0, <0.28.0",
|
"httpx >=0.27.0, <0.28.0",
|
||||||
"cryptofeed >=2.4.0, <3.0.0",
|
"cryptofeed >=2.4.0, <3.0.0",
|
||||||
"pyarrow>=18.0.0",
|
"pyarrow>=18.0.0",
|
||||||
|
|
@ -78,8 +76,14 @@ dependencies = [
|
||||||
"numba>=0.61.0",
|
"numba>=0.61.0",
|
||||||
"pyvnc",
|
"pyvnc",
|
||||||
]
|
]
|
||||||
|
# ------ dependencies ------
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
|
# TODO: add an `--only daemon` group for running non-ui / pikerd
|
||||||
|
# service tree in distributed mode B)
|
||||||
|
# https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
uis = [
|
uis = [
|
||||||
# https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies
|
# https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies
|
||||||
# TODO: make sure the levenshtein shit compiles on nix..
|
# TODO: make sure the levenshtein shit compiles on nix..
|
||||||
|
|
@ -92,12 +96,11 @@ uis = [
|
||||||
# for consideration,
|
# for consideration,
|
||||||
# - 'visidata'
|
# - 'visidata'
|
||||||
|
|
||||||
# TODO: add an `--only daemon` group for running non-ui / pikerd
|
"qdarkstyle >=3.0.2, <4.0.0",
|
||||||
# service tree in distributed mode B)
|
"pyqt6 >=6.7.0, <7.0.0",
|
||||||
# https://docs.astral.sh/uv/concepts/projects/dependencies/#optional-dependencies
|
"pyqtgraph",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
# TODO: a toolset that makes debugging a `pikerd` service (tree) easy
|
# TODO: a toolset that makes debugging a `pikerd` service (tree) easy
|
||||||
# to hack on directly using more or less the local env:
|
# to hack on directly using more or less the local env:
|
||||||
# - xonsh + xxh
|
# - xonsh + xxh
|
||||||
|
|
@ -106,19 +109,46 @@ uis = [
|
||||||
#
|
#
|
||||||
# console ehancements and eventually remote debugging extras/helpers.
|
# console ehancements and eventually remote debugging extras/helpers.
|
||||||
# use `uv --dev` to enable
|
# use `uv --dev` to enable
|
||||||
dev = [
|
repl = [
|
||||||
"pytest",
|
# debug
|
||||||
"elasticsearch >=8.9.0, <9.0.0",
|
"pdbp >=1.5.0, <2.0.0",
|
||||||
"prompt-toolkit ==3.0.40",
|
|
||||||
"cython >=3.0.0, <4.0.0",
|
|
||||||
"greenback >=1.1.1, <2.0.0",
|
"greenback >=1.1.1, <2.0.0",
|
||||||
"ruff>=0.9.6",
|
"xonsh",
|
||||||
|
"prompt-toolkit ==3.0.40",
|
||||||
"pyperclip>=1.9.0",
|
"pyperclip>=1.9.0",
|
||||||
"i3ipc>=2.2.1",
|
|
||||||
|
|
||||||
# ?from git, see below.
|
|
||||||
"xonsh",
|
|
||||||
]
|
]
|
||||||
|
testing = [
|
||||||
|
"pytest",
|
||||||
|
]
|
||||||
|
de = [
|
||||||
|
# DE-specific
|
||||||
|
"i3ipc>=2.2.1",
|
||||||
|
]
|
||||||
|
dev = [
|
||||||
|
# https://docs.astral.sh/uv/concepts/projects/dependencies/#development-dependencies
|
||||||
|
"cython >=3.0.0, <4.0.0",
|
||||||
|
|
||||||
|
# nested deps-groups
|
||||||
|
# https://docs.astral.sh/uv/concepts/projects/dependencies/#nesting-groups
|
||||||
|
{include-group = 'uis'},
|
||||||
|
{include-group = 'repl'},
|
||||||
|
{include-group = 'testing'},
|
||||||
|
{include-group = 'de'},
|
||||||
|
]
|
||||||
|
lint = [
|
||||||
|
# XXX, with flake.nix needs to be from nixpkgs
|
||||||
|
"ruff>=0.9.6"
|
||||||
|
#
|
||||||
|
# ^TODO? these markers don't work; use deps-flags for now?
|
||||||
|
# ; os_name != 'nixos' and platform_system != 'NixOS'",
|
||||||
|
# ; defined('IN_NIX_SHELL')",
|
||||||
|
]
|
||||||
|
dbs = [
|
||||||
|
"elasticsearch >=8.9.0, <9.0.0",
|
||||||
|
]
|
||||||
|
# ------ dependency-groups ------
|
||||||
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
# https://docs.pytest.org/en/stable/reference/reference.html#configuration-options
|
# https://docs.pytest.org/en/stable/reference/reference.html#configuration-options
|
||||||
|
|
@ -131,24 +161,29 @@ console_output_style = 'progress'
|
||||||
# https://docs.pytest.org/en/stable/how-to/plugins.html#disabling-plugins-from-autoloading
|
# https://docs.pytest.org/en/stable/how-to/plugins.html#disabling-plugins-from-autoloading
|
||||||
# https://docs.pytest.org/en/stable/how-to/plugins.html#deactivating-unregistering-a-plugin-by-name
|
# https://docs.pytest.org/en/stable/how-to/plugins.html#deactivating-unregistering-a-plugin-by-name
|
||||||
addopts = '-p no:xonsh'
|
addopts = '-p no:xonsh'
|
||||||
|
# ------ tool.pytest ------
|
||||||
|
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
piker = "piker.cli:cli"
|
piker = "piker.cli:cli"
|
||||||
pikerd = "piker.cli:pikerd"
|
pikerd = "piker.cli:pikerd"
|
||||||
ledger = "piker.accounting.cli:ledger"
|
ledger = "piker.accounting.cli:ledger"
|
||||||
|
# ------ project.scripts ------
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
include = ["piker"]
|
include = ["piker"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
include = ["piker"]
|
include = ["piker"]
|
||||||
|
# ------ tool.hatch ------
|
||||||
|
|
||||||
|
|
||||||
# TODO? move to a `uv.toml`?
|
# TODO? move to a `uv.toml`?
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
python-preference = 'system'
|
python-preference = 'system'
|
||||||
python-downloads = 'manual'
|
python-downloads = 'manual'
|
||||||
|
# https://docs.astral.sh/uv/concepts/projects/dependencies/#default-groups
|
||||||
|
default-groups = ['uis', 'dev']
|
||||||
|
# ------ tool.uv ------
|
||||||
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
|
|
@ -156,12 +191,10 @@ pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
|
||||||
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
|
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
|
||||||
pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
||||||
|
|
||||||
# TODO, long term we should be synced to upstream `main` branch!
|
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
||||||
# tractor = { git = "https://github.com/goodboy/tractor.git", branch ="piker_pin" }
|
tractor = { git = "https://github.com/goodboy/tractor.git", branch ="piker_pin" }
|
||||||
tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
||||||
|
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "main" }
|
||||||
# goodboy's dev-env
|
# ------ goodboy ------
|
||||||
# XXX for @goodboy's hackin dev env, usually there's something new in
|
# hackin dev-envs, usually there's something new he's hackin in..
|
||||||
# the runtime being seriously tested here Bp
|
# tractor = { path = "../tractor", editable = true }
|
||||||
# tractor = { path = "../tractor/", editable = true }
|
|
||||||
# xonsh = { path = "../xonsh", editable = true }
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue