Compare commits
40 Commits
a15a4ec8cd
...
d6f3f659fb
| Author | SHA1 | Date |
|---|---|---|
|
|
d6f3f659fb | |
|
|
8e89943a9e | |
|
|
4069168d30 | |
|
|
04c1d999f3 | |
|
|
47d0cfa2ff | |
|
|
4c30547128 | |
|
|
4f845f5010 | |
|
|
9110edd77b | |
|
|
ed9c211b96 | |
|
|
f1b27e9696 | |
|
|
959d04024b | |
|
|
6f8a361e80 | |
|
|
2d678e1582 | |
|
|
48493e50b0 | |
|
|
f73b981173 | |
|
|
d5edd3484f | |
|
|
bac8317a4a | |
|
|
eb78437994 | |
|
|
88353ffef8 | |
|
|
ec4e6ec742 | |
|
|
205058de21 | |
|
|
f11ab5f0aa | |
|
|
8718ad4874 | |
|
|
3a515afccd | |
|
|
88732a67d5 | |
|
|
858cfce958 | |
|
|
51d109f7e7 | |
|
|
76f199df3b | |
|
|
4e3cd7f986 | |
|
|
1fb0fe3a04 | |
|
|
de5b1737b4 | |
|
|
1776242413 | |
|
|
848c8ae533 | |
|
|
fdea8556d7 | |
|
|
be28d083e4 | |
|
|
8701b517e7 | |
|
|
f39b362bc4 | |
|
|
d2e1d6ce91 | |
|
|
d0966e0363 | |
|
|
4081336bd3 |
|
|
@ -237,8 +237,8 @@ async def open_history_client(
|
|||
|
||||
async def get_ohlc(
|
||||
timeframe: float,
|
||||
end_dt: datetime | None = None,
|
||||
start_dt: datetime | None = None,
|
||||
end_dt: datetime|None = None,
|
||||
start_dt: datetime|None = None,
|
||||
|
||||
) -> tuple[
|
||||
np.ndarray,
|
||||
|
|
@ -275,9 +275,15 @@ async def open_history_client(
|
|||
f'{times}'
|
||||
)
|
||||
|
||||
# XXX, debug any case where the latest 1m bar we get is
|
||||
# already another "sample's-step-old"..
|
||||
if end_dt is None:
|
||||
inow: int = round(time.time())
|
||||
if (inow - times[-1]) > 60:
|
||||
if (
|
||||
_time_step := (inow - times[-1])
|
||||
>
|
||||
timeframe * 2
|
||||
):
|
||||
await tractor.pause()
|
||||
|
||||
start_dt = from_timestamp(times[0])
|
||||
|
|
@ -291,7 +297,7 @@ async def open_history_client(
|
|||
async def get_mkt_info(
|
||||
fqme: str,
|
||||
|
||||
) -> tuple[MktPair, Pair] | None:
|
||||
) -> tuple[MktPair, Pair]|None:
|
||||
|
||||
# uppercase since kraken bs_mktid is always upper
|
||||
if 'binance' not in fqme.lower():
|
||||
|
|
@ -368,7 +374,7 @@ async def get_mkt_info(
|
|||
if 'futes' in mkt_mode:
|
||||
assert isinstance(pair, FutesPair)
|
||||
|
||||
dst: Asset | None = assets.get(pair.bs_dst_asset)
|
||||
dst: Asset|None = assets.get(pair.bs_dst_asset)
|
||||
if (
|
||||
not dst
|
||||
# TODO: a known asset DNE list?
|
||||
|
|
@ -427,7 +433,7 @@ async def subscribe(
|
|||
# might get ack from ws server, or maybe some
|
||||
# other msg still in transit..
|
||||
res = await ws.recv_msg()
|
||||
subid: str | None = res.get('id')
|
||||
subid: str|None = res.get('id')
|
||||
if subid:
|
||||
assert res['id'] == subid
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,9 @@ async def vnc_click_hack(
|
|||
'connection': 'r'
|
||||
}[reset_type]
|
||||
|
||||
with tractor.devx.open_crash_handler():
|
||||
with tractor.devx.open_crash_handler(
|
||||
ignore={TimeoutError,},
|
||||
):
|
||||
client = await AsyncVNCClient.connect(
|
||||
VNCConfig(
|
||||
host=host,
|
||||
|
|
@ -324,14 +326,20 @@ def i3ipc_fin_wins_titled(
|
|||
)
|
||||
|
||||
|
||||
|
||||
def i3ipc_xdotool_manual_click_hack() -> None:
|
||||
'''
|
||||
Do the data reset hack but expecting a local X-window using `xdotool`.
|
||||
|
||||
'''
|
||||
focussed, matches = i3ipc_fin_wins_titled()
|
||||
orig_win_id = focussed.window
|
||||
try:
|
||||
orig_win_id = focussed.window
|
||||
except AttributeError:
|
||||
# XXX if .window cucks we prolly aren't intending to
|
||||
# use this and/or just woke up from suspend..
|
||||
log.exception('xdotool invalid usage ya ??\n')
|
||||
return
|
||||
|
||||
try:
|
||||
for name, con in matches:
|
||||
print(f'Resetting data feed for {name}')
|
||||
|
|
@ -379,99 +387,3 @@ def i3ipc_xdotool_manual_click_hack() -> None:
|
|||
])
|
||||
except subprocess.TimeoutExpired:
|
||||
log.exception('xdotool timed out?')
|
||||
|
||||
|
||||
|
||||
def is_current_time_in_range(
|
||||
start_dt: datetime,
|
||||
end_dt: datetime,
|
||||
) -> bool:
|
||||
'''
|
||||
Check if current time is within the datetime range.
|
||||
|
||||
Use any/the-same timezone as provided by `start_dt.tzinfo` value
|
||||
in the range.
|
||||
|
||||
'''
|
||||
now: datetime = datetime.now(start_dt.tzinfo)
|
||||
return start_dt <= now <= end_dt
|
||||
|
||||
|
||||
# TODO, put this into `._util` and call it from here!
|
||||
#
|
||||
# NOTE, this was generated by @guille from a gpt5 prompt
|
||||
# and was originally thot to be needed before learning about
|
||||
# `ib_insync.contract.ContractDetails._parseSessions()` and
|
||||
# it's downstream meths..
|
||||
#
|
||||
# This is still likely useful to keep for now to parse the
|
||||
# `.tradingHours: str` value manually if we ever decide
|
||||
# to move off `ib_async` and implement our own `trio`/`anyio`
|
||||
# based version Bp
|
||||
#
|
||||
# >attempt to parse the retarted ib "time stampy thing" they
|
||||
# >do for "venue hours" with this.. written by
|
||||
# >gpt5-"thinking",
|
||||
#
|
||||
|
||||
|
||||
def parse_trading_hours(
|
||||
spec: str,
|
||||
tz: TzInfo|None = None
|
||||
) -> dict[
|
||||
date,
|
||||
tuple[datetime, datetime]
|
||||
]|None:
|
||||
'''
|
||||
Parse venue hours like:
|
||||
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
|
||||
|
||||
Returns `dict[date] = (open_dt, close_dt)` or `None` if
|
||||
closed.
|
||||
|
||||
'''
|
||||
if (
|
||||
not isinstance(spec, str)
|
||||
or
|
||||
not spec
|
||||
):
|
||||
raise ValueError('spec must be a non-empty string')
|
||||
|
||||
out: dict[
|
||||
date,
|
||||
tuple[datetime, datetime]
|
||||
]|None = {}
|
||||
|
||||
for part in (p.strip() for p in spec.split(';') if p.strip()):
|
||||
if part.endswith(':CLOSED'):
|
||||
day_s, _ = part.split(':', 1)
|
||||
d = datetime.strptime(day_s, '%Y%m%d').date()
|
||||
out[d] = None
|
||||
continue
|
||||
|
||||
try:
|
||||
start_s, end_s = part.split('-', 1)
|
||||
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
|
||||
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
|
||||
except ValueError as exc:
|
||||
raise ValueError(f'invalid segment: {part}') from exc
|
||||
|
||||
if tz is not None:
|
||||
start_dt = start_dt.replace(tzinfo=tz)
|
||||
end_dt = end_dt.replace(tzinfo=tz)
|
||||
|
||||
out[start_dt.date()] = (start_dt, end_dt)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# ORIG desired usage,
|
||||
#
|
||||
# TODO, for non-drunk tomorrow,
|
||||
# - call above fn and check that `output[today] is not None`
|
||||
# trading_hrs: dict = parse_trading_hours(
|
||||
# details.tradingHours
|
||||
# )
|
||||
# liq_hrs: dict = parse_trading_hours(
|
||||
# details.liquidHours
|
||||
# )
|
||||
|
|
|
|||
|
|
@ -50,10 +50,11 @@ import tractor
|
|||
from tractor import to_asyncio
|
||||
from tractor import trionics
|
||||
from pendulum import (
|
||||
from_timestamp,
|
||||
DateTime,
|
||||
Duration,
|
||||
duration as mk_duration,
|
||||
from_timestamp,
|
||||
Interval,
|
||||
)
|
||||
from eventkit import Event
|
||||
from ib_insync import (
|
||||
|
|
@ -91,10 +92,15 @@ from .symbols import (
|
|||
_exch_skip_list,
|
||||
_futes_venues,
|
||||
)
|
||||
from ._util import (
|
||||
log,
|
||||
# only for the ib_sync internal logging
|
||||
get_logger,
|
||||
from ...log import get_logger
|
||||
from .venues import (
|
||||
is_venue_open,
|
||||
sesh_times,
|
||||
is_venue_closure,
|
||||
)
|
||||
|
||||
log = get_logger(
|
||||
name=__name__,
|
||||
)
|
||||
|
||||
_bar_load_dtype: list[tuple[str, type]] = [
|
||||
|
|
@ -180,7 +186,7 @@ class NonShittyIB(IB):
|
|||
# override `ib_insync` internal loggers so we can see wtf
|
||||
# it's doing..
|
||||
self._logger = get_logger(
|
||||
'ib_insync.ib',
|
||||
name=__name__,
|
||||
)
|
||||
self._createEvents()
|
||||
|
||||
|
|
@ -188,7 +194,7 @@ class NonShittyIB(IB):
|
|||
self.wrapper = NonShittyWrapper(self)
|
||||
self.client = ib_client.Client(self.wrapper)
|
||||
self.client._logger = get_logger(
|
||||
'ib_insync.client',
|
||||
name='ib_insync.client',
|
||||
)
|
||||
|
||||
# self.errorEvent += self._onError
|
||||
|
|
@ -260,6 +266,16 @@ def remove_handler_on_err(
|
|||
event.disconnect(handler)
|
||||
|
||||
|
||||
# (originally?) i thot that,
|
||||
# > "EST in ISO 8601 format is required.."
|
||||
#
|
||||
# XXX, but see `ib_async`'s impl,
|
||||
# - `ib_async.ib.IB.reqHistoricalDataAsync()`
|
||||
# - `ib_async.util.formatIBDatetime()`
|
||||
# below is EPOCH.
|
||||
_iso8601_epoch_in_est: str = "1970-01-01T00:00:00.000000-05:00"
|
||||
|
||||
|
||||
class Client:
|
||||
'''
|
||||
IB wrapped for our broker backend API.
|
||||
|
|
@ -333,9 +349,11 @@ class Client:
|
|||
self,
|
||||
fqme: str,
|
||||
|
||||
# EST in ISO 8601 format is required... below is EPOCH
|
||||
start_dt: datetime|str = "1970-01-01T00:00:00.000000-05:00",
|
||||
end_dt: datetime|str = "",
|
||||
# EST in ISO 8601 format is required..
|
||||
# XXX, see `ib_async.ib.IB.reqHistoricalDataAsync()`
|
||||
# below is EPOCH.
|
||||
start_dt: datetime|None = None, # _iso8601_epoch_in_est,
|
||||
end_dt: datetime|None = None,
|
||||
|
||||
# ohlc sample period in seconds
|
||||
sample_period_s: int = 1,
|
||||
|
|
@ -346,9 +364,17 @@ class Client:
|
|||
|
||||
**kwargs,
|
||||
|
||||
) -> tuple[BarDataList, np.ndarray, Duration]:
|
||||
) -> tuple[
|
||||
BarDataList,
|
||||
np.ndarray,
|
||||
Duration,
|
||||
]:
|
||||
'''
|
||||
Retreive OHLCV bars for a fqme over a range to the present.
|
||||
Retreive the `fqme`'s OHLCV-bars for the time-range "until `end_dt`".
|
||||
|
||||
Notes:
|
||||
- IB's api doesn't support a `start_dt` (which is why default
|
||||
is null) so we only use it for bar-frame duration checking.
|
||||
|
||||
'''
|
||||
# See API docs here:
|
||||
|
|
@ -363,13 +389,19 @@ class Client:
|
|||
|
||||
dt_duration: Duration = (
|
||||
duration
|
||||
or default_dt_duration
|
||||
or
|
||||
default_dt_duration
|
||||
)
|
||||
|
||||
# TODO: maybe remove all this?
|
||||
global _enters
|
||||
if not end_dt:
|
||||
end_dt = ''
|
||||
if end_dt is None:
|
||||
end_dt: str = ''
|
||||
|
||||
else:
|
||||
est_end_dt = end_dt.in_tz('EST')
|
||||
if est_end_dt != end_dt:
|
||||
breakpoint()
|
||||
|
||||
_enters += 1
|
||||
|
||||
|
|
@ -438,58 +470,116 @@ class Client:
|
|||
+ query_info
|
||||
)
|
||||
|
||||
# TODO: we could maybe raise ``NoData`` instead if we
|
||||
# TODO: we could maybe raise `NoData` instead if we
|
||||
# rewrite the method in the first case?
|
||||
# right now there's no way to detect a timeout..
|
||||
return [], np.empty(0), dt_duration
|
||||
|
||||
log.info(query_info)
|
||||
|
||||
# ------ GAP-DETECTION ------
|
||||
# NOTE XXX: ensure minimum duration in bars?
|
||||
# => recursively call this method until we get at least as
|
||||
# many bars such that they sum in aggregate to the the
|
||||
# desired total time (duration) at most.
|
||||
# - if you query over a gap and get no data
|
||||
# that may short circuit the history
|
||||
if (
|
||||
# XXX XXX XXX
|
||||
# => WHY DID WE EVEN NEED THIS ORIGINALLY!? <=
|
||||
# XXX XXX XXX
|
||||
False
|
||||
and end_dt
|
||||
):
|
||||
if end_dt:
|
||||
nparr: np.ndarray = bars_to_np(bars)
|
||||
times: np.ndarray = nparr['time']
|
||||
first: float = times[0]
|
||||
tdiff: float = times[-1] - first
|
||||
last: float = times[-1]
|
||||
# frame_dur: float = times[-1] - first
|
||||
|
||||
details: ContractDetails = (
|
||||
await self.ib.reqContractDetailsAsync(contract)
|
||||
)[0]
|
||||
# convert to makt-native tz
|
||||
tz: str = details.timeZoneId
|
||||
end_dt = end_dt.in_tz(tz)
|
||||
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
||||
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
||||
tdiff: int = (
|
||||
last_dt
|
||||
-
|
||||
first_dt
|
||||
).in_seconds() + sample_period_s
|
||||
_open_now: bool = is_venue_open(
|
||||
con_deats=details,
|
||||
)
|
||||
|
||||
# XXX, do gap detections.
|
||||
has_closure_gap: bool = False
|
||||
if (
|
||||
last_dt.add(seconds=sample_period_s)
|
||||
<
|
||||
end_dt
|
||||
):
|
||||
open_time, close_time = sesh_times(details)
|
||||
# XXX, always calc gap in mkt-venue-local timezone
|
||||
gap: Interval = end_dt - last_dt
|
||||
if not (
|
||||
has_closure_gap := is_venue_closure(
|
||||
gap=gap,
|
||||
con_deats=details,
|
||||
time_step_s=sample_period_s,
|
||||
)):
|
||||
log.warning(
|
||||
f'Invalid non-closure gap for {fqme!r} ?!?\n'
|
||||
f'is-open-now: {_open_now}\n'
|
||||
f'\n'
|
||||
f'{gap}\n'
|
||||
)
|
||||
log.warning(
|
||||
f'Detected NON venue-closure GAP ??\n'
|
||||
f'{gap}\n'
|
||||
)
|
||||
breakpoint()
|
||||
else:
|
||||
assert has_closure_gap
|
||||
log.debug(
|
||||
f'Detected venue closure gap (weekend),\n'
|
||||
f'{gap}\n'
|
||||
)
|
||||
|
||||
if (
|
||||
# len(bars) * sample_period_s) < dt_duration.in_seconds()
|
||||
tdiff < dt_duration.in_seconds()
|
||||
# and False
|
||||
start_dt is None
|
||||
and (
|
||||
tdiff
|
||||
<
|
||||
dt_duration.in_seconds()
|
||||
)
|
||||
and
|
||||
not has_closure_gap
|
||||
):
|
||||
end_dt: DateTime = from_timestamp(first)
|
||||
log.warning(
|
||||
log.error(
|
||||
f'Frame result was shorter then {dt_duration}!?\n'
|
||||
'Recursing for more bars:\n'
|
||||
f'end_dt: {end_dt}\n'
|
||||
f'dt_duration: {dt_duration}\n'
|
||||
# f'\n'
|
||||
# f'Recursing for more bars:\n'
|
||||
)
|
||||
(
|
||||
r_bars,
|
||||
r_arr,
|
||||
r_duration,
|
||||
) = await self.bars(
|
||||
fqme,
|
||||
start_dt=start_dt,
|
||||
end_dt=end_dt,
|
||||
sample_period_s=sample_period_s,
|
||||
# XXX, debug!
|
||||
breakpoint()
|
||||
# XXX ? TODO? recursively try to re-request?
|
||||
# => i think *NO* right?
|
||||
#
|
||||
# (
|
||||
# r_bars,
|
||||
# r_arr,
|
||||
# r_duration,
|
||||
# ) = await self.bars(
|
||||
# fqme,
|
||||
# start_dt=start_dt,
|
||||
# end_dt=end_dt,
|
||||
# sample_period_s=sample_period_s,
|
||||
|
||||
# TODO: make a table for Duration to
|
||||
# the ib str values in order to use this?
|
||||
# duration=duration,
|
||||
)
|
||||
r_bars.extend(bars)
|
||||
bars = r_bars
|
||||
# # TODO: make a table for Duration to
|
||||
# # the ib str values in order to use this?
|
||||
# # duration=duration,
|
||||
# )
|
||||
# r_bars.extend(bars)
|
||||
# bars = r_bars
|
||||
|
||||
nparr: np.ndarray = bars_to_np(bars)
|
||||
|
||||
|
|
@ -689,13 +779,14 @@ class Client:
|
|||
ContFuture(symbol, exchange=exchange)
|
||||
))[0]
|
||||
else:
|
||||
con = (await self.ib.qualifyContractsAsync(
|
||||
cons = (await self.ib.qualifyContractsAsync(
|
||||
Future(
|
||||
symbol,
|
||||
exchange=exchange,
|
||||
lastTradeDateOrContractMonth=expiry,
|
||||
)
|
||||
))[0]
|
||||
))
|
||||
con = cons[0]
|
||||
|
||||
return con
|
||||
|
||||
|
|
@ -784,9 +875,16 @@ class Client:
|
|||
# crypto$
|
||||
elif exch == 'PAXOS': # btc.paxos
|
||||
con = Crypto(
|
||||
symbol=symbol,
|
||||
currency=currency,
|
||||
symbol=symbol.upper(),
|
||||
currency='USD',
|
||||
exchange='PAXOS',
|
||||
)
|
||||
# XXX, on `ib_insync` when first tried this,
|
||||
# > Error 10299, reqId 141: Expected what to show is
|
||||
# > AGGTRADES, please use that instead of TRADES.,
|
||||
# > contract: Crypto(conId=479624278, symbol='BTC',
|
||||
# > exchange='PAXOS', currency='USD',
|
||||
# > localSymbol='BTC.USD', tradingClass='BTC')
|
||||
|
||||
# stonks
|
||||
else:
|
||||
|
|
@ -895,7 +993,6 @@ class Client:
|
|||
async def get_sym_details(
|
||||
self,
|
||||
fqme: str,
|
||||
|
||||
) -> tuple[
|
||||
Contract,
|
||||
ContractDetails,
|
||||
|
|
@ -1534,6 +1631,7 @@ async def open_aio_client_method_relay(
|
|||
|
||||
) -> None:
|
||||
|
||||
# with tractor.devx.maybe_open_crash_handler() as _bxerr:
|
||||
# sync with `open_client_proxy()` caller
|
||||
chan.started_nowait(client)
|
||||
|
||||
|
|
@ -1543,7 +1641,11 @@ async def open_aio_client_method_relay(
|
|||
# relay all method requests to ``asyncio``-side client and deliver
|
||||
# back results
|
||||
while not chan._to_trio._closed: # <- TODO, better check like `._web_bs`?
|
||||
msg: tuple[str, dict]|dict|None = await chan.get()
|
||||
msg: (
|
||||
None
|
||||
|tuple[str, dict]
|
||||
|dict
|
||||
) = await chan.get()
|
||||
match msg:
|
||||
case None: # termination sentinel
|
||||
log.info('asyncio `Client` method-proxy SHUTDOWN!')
|
||||
|
|
|
|||
|
|
@ -69,9 +69,9 @@ from .api import (
|
|||
Contract,
|
||||
RequestError,
|
||||
)
|
||||
from .venues import is_venue_open
|
||||
from ._util import (
|
||||
data_reset_hack,
|
||||
is_current_time_in_range,
|
||||
)
|
||||
from .symbols import get_mkt_info
|
||||
|
||||
|
|
@ -178,8 +178,8 @@ async def open_history_client(
|
|||
|
||||
async def get_hist(
|
||||
timeframe: float,
|
||||
end_dt: datetime | None = None,
|
||||
start_dt: datetime | None = None,
|
||||
end_dt: datetime|None = None,
|
||||
start_dt: datetime|None = None,
|
||||
|
||||
) -> tuple[np.ndarray, str]:
|
||||
|
||||
|
|
@ -203,7 +203,8 @@ async def open_history_client(
|
|||
latency = time.time() - query_start
|
||||
if (
|
||||
not timedout
|
||||
# and latency <= max_timeout
|
||||
# and
|
||||
# latency <= max_timeout
|
||||
):
|
||||
count += 1
|
||||
mean += latency / count
|
||||
|
|
@ -219,8 +220,10 @@ async def open_history_client(
|
|||
)
|
||||
if (
|
||||
end_dt
|
||||
and head_dt
|
||||
and end_dt <= head_dt
|
||||
and
|
||||
head_dt
|
||||
and
|
||||
end_dt <= head_dt
|
||||
):
|
||||
raise DataUnavailable(
|
||||
f'First timestamp is {head_dt}\n'
|
||||
|
|
@ -262,12 +265,43 @@ async def open_history_client(
|
|||
vlm = bars_array['volume']
|
||||
vlm[vlm < 0] = 0
|
||||
|
||||
return bars_array, first_dt, last_dt
|
||||
# XXX, if a start-limit was passed ensure we only
|
||||
# return history that far back!
|
||||
if (
|
||||
start_dt
|
||||
and
|
||||
first_dt < start_dt
|
||||
):
|
||||
trimmed_bars = bars_array[
|
||||
bars_array['time'] >= start_dt.timestamp()
|
||||
]
|
||||
if (
|
||||
trimmed_first_dt := from_timestamp(trimmed_bars['time'][0])
|
||||
!=
|
||||
start_dt
|
||||
):
|
||||
# TODO! rm this once we're more confident it never hits!
|
||||
# breakpoint()
|
||||
raise RuntimeError(
|
||||
f'OHLC-bars array start is gt `start_dt` limit !!\n'
|
||||
f'start_dt: {start_dt}\n'
|
||||
f'first_dt: {first_dt}\n'
|
||||
f'trimmed_first_dt: {trimmed_first_dt}\n'
|
||||
)
|
||||
|
||||
# XXX, overwrite with start_dt-limited frame
|
||||
bars_array = trimmed_bars
|
||||
|
||||
return (
|
||||
bars_array,
|
||||
first_dt,
|
||||
last_dt,
|
||||
)
|
||||
|
||||
# TODO: it seems like we can do async queries for ohlc
|
||||
# but getting the order right still isn't working and I'm not
|
||||
# quite sure why.. needs some tinkering and probably
|
||||
# a lookthrough of the ``ib_insync`` machinery, for eg. maybe
|
||||
# a lookthrough of the `ib_insync` machinery, for eg. maybe
|
||||
# we have to do the batch queries on the `asyncio` side?
|
||||
yield (
|
||||
get_hist,
|
||||
|
|
@ -390,14 +424,13 @@ _failed_resets: int = 0
|
|||
|
||||
|
||||
async def get_bars(
|
||||
|
||||
proxy: MethodProxy,
|
||||
fqme: str,
|
||||
timeframe: int,
|
||||
|
||||
# blank to start which tells ib to look up the latest datum
|
||||
end_dt: str = '',
|
||||
start_dt: str | None = '',
|
||||
end_dt: datetime|None = None,
|
||||
start_dt: datetime|None = None,
|
||||
|
||||
# TODO: make this more dynamic based on measured frame rx latency?
|
||||
# how long before we trigger a feed reset (seconds)
|
||||
|
|
@ -451,6 +484,9 @@ async def get_bars(
|
|||
dt_duration,
|
||||
) = await proxy.bars(
|
||||
fqme=fqme,
|
||||
# XXX TODO! LOL we're not using this and IB dun
|
||||
# support it anyway..
|
||||
# start_dt=start_dt,
|
||||
end_dt=end_dt,
|
||||
sample_period_s=timeframe,
|
||||
|
||||
|
|
@ -701,7 +737,7 @@ async def _setup_quote_stream(
|
|||
# '294', # Trade rate / minute
|
||||
# '295', # Vlm rate / minute
|
||||
),
|
||||
contract: Contract | None = None,
|
||||
contract: Contract|None = None,
|
||||
|
||||
) -> trio.abc.ReceiveChannel:
|
||||
'''
|
||||
|
|
@ -723,7 +759,12 @@ async def _setup_quote_stream(
|
|||
# XXX since this is an `asyncio.Task`, we must use
|
||||
# tractor.pause_from_sync()
|
||||
|
||||
caccount_name, client = get_preferred_data_client(accts2clients)
|
||||
(
|
||||
_account_name,
|
||||
client,
|
||||
) = get_preferred_data_client(
|
||||
accts2clients,
|
||||
)
|
||||
contract = (
|
||||
contract
|
||||
or
|
||||
|
|
@ -1058,14 +1099,9 @@ async def stream_quotes(
|
|||
)
|
||||
|
||||
# is venue active rn?
|
||||
venue_is_open: bool = any(
|
||||
is_current_time_in_range(
|
||||
start_dt=sesh.start,
|
||||
end_dt=sesh.end,
|
||||
)
|
||||
for sesh in details.tradingSessions()
|
||||
venue_is_open: bool = is_venue_open(
|
||||
con_deats=details,
|
||||
)
|
||||
|
||||
init_msg = FeedInit(mkt_info=mkt)
|
||||
|
||||
# NOTE, tell sampler (via config) to skip vlm summing for dst
|
||||
|
|
@ -1082,6 +1118,7 @@ async def stream_quotes(
|
|||
|
||||
con: Contract = details.contract
|
||||
first_ticker: Ticker|None = None
|
||||
first_quote: dict[str, Any] = {}
|
||||
|
||||
timeout: float = 1.6
|
||||
with trio.move_on_after(timeout) as quote_cs:
|
||||
|
|
@ -1134,15 +1171,14 @@ async def stream_quotes(
|
|||
first_quote,
|
||||
))
|
||||
|
||||
# it's not really live but this will unblock
|
||||
# the brokerd feed task to tell the ui to update?
|
||||
feed_is_live.set()
|
||||
|
||||
# block and let data history backfill code run.
|
||||
# XXX obvi given the venue is closed, we never expect feed
|
||||
# to come up; a taskc should be the only way to
|
||||
# terminate this task.
|
||||
await trio.sleep_forever()
|
||||
#
|
||||
# ^^XXX^^TODO! INSTEAD impl a `trio.sleep()` for the
|
||||
# duration until the venue opens!!
|
||||
|
||||
# ?TODO, we could instead spawn a task that waits on a feed
|
||||
# to start and let it wait indefinitely..instead of this
|
||||
|
|
@ -1166,6 +1202,9 @@ async def stream_quotes(
|
|||
'Rxed init quote:\n'
|
||||
f'{pformat(first_quote)}'
|
||||
)
|
||||
# signal `.data.feed` layer that mkt quotes are LIVE
|
||||
feed_is_live.set()
|
||||
|
||||
cs: trio.CancelScope|None = None
|
||||
startup: bool = True
|
||||
iter_quotes: trio.abc.Channel
|
||||
|
|
@ -1213,55 +1252,12 @@ async def stream_quotes(
|
|||
tn.start_soon(reset_on_feed)
|
||||
|
||||
async with aclosing(iter_quotes):
|
||||
# if syminfo.get('no_vlm', False):
|
||||
if not init_msg.shm_write_opts['has_vlm']:
|
||||
|
||||
# generally speaking these feeds don't
|
||||
# include vlm data.
|
||||
atype: str = mkt.dst.atype
|
||||
log.info(
|
||||
f'No-vlm {mkt.fqme}@{atype}, skipping quote poll'
|
||||
)
|
||||
|
||||
else:
|
||||
# wait for real volume on feed (trading might be
|
||||
# closed)
|
||||
while True:
|
||||
ticker = await iter_quotes.receive()
|
||||
|
||||
# for a real volume contract we rait for
|
||||
# the first "real" trade to take place
|
||||
if (
|
||||
# not calc_price
|
||||
# and not ticker.rtTime
|
||||
False
|
||||
# not ticker.rtTime
|
||||
):
|
||||
# spin consuming tickers until we
|
||||
# get a real market datum
|
||||
log.debug(f"New unsent ticker: {ticker}")
|
||||
continue
|
||||
|
||||
else:
|
||||
log.debug("Received first volume tick")
|
||||
# ugh, clear ticks since we've
|
||||
# consumed them (ahem, ib_insync is
|
||||
# truly stateful trash)
|
||||
# ticker.ticks = []
|
||||
|
||||
# XXX: this works because we don't use
|
||||
# ``aclosing()`` above?
|
||||
break
|
||||
|
||||
quote = normalize(ticker)
|
||||
log.debug(f"First ticker received {quote}")
|
||||
|
||||
# tell data-layer spawner-caller that live
|
||||
# quotes are now active desptie not having
|
||||
# necessarily received a first vlm/clearing
|
||||
# tick.
|
||||
ticker = await iter_quotes.receive()
|
||||
feed_is_live.set()
|
||||
quote = normalize(ticker)
|
||||
fqme: str = quote['fqme']
|
||||
await send_chan.send({fqme: quote})
|
||||
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ _adhoc_fiat_set = set((
|
|||
|
||||
# manually discovered tick discrepancies,
|
||||
# onl god knows how or why they'd cuck these up..
|
||||
_adhoc_mkt_infos: dict[int | str, dict] = {
|
||||
_adhoc_mkt_infos: dict[int|str, dict] = {
|
||||
'vtgn.nasdaq': {'price_tick': Decimal('0.01')},
|
||||
}
|
||||
|
||||
|
|
@ -488,8 +488,7 @@ def con2fqme(
|
|||
@async_lifo_cache()
|
||||
async def get_mkt_info(
|
||||
fqme: str,
|
||||
|
||||
proxy: MethodProxy | None = None,
|
||||
proxy: MethodProxy|None = None,
|
||||
|
||||
) -> tuple[MktPair, ibis.ContractDetails]:
|
||||
|
||||
|
|
@ -522,7 +521,11 @@ async def get_mkt_info(
|
|||
if atype == 'commodity':
|
||||
venue: str = 'cmdty'
|
||||
else:
|
||||
venue = con.primaryExchange or con.exchange
|
||||
venue: str = (
|
||||
con.primaryExchange
|
||||
or
|
||||
con.exchange
|
||||
)
|
||||
|
||||
price_tick: Decimal = Decimal(str(details.minTick))
|
||||
ib_min_tick_gt_2: Decimal = Decimal('0.01')
|
||||
|
|
@ -550,7 +553,7 @@ async def get_mkt_info(
|
|||
size_tick: Decimal = Decimal(
|
||||
str(details.minSize).rstrip('0')
|
||||
)
|
||||
# |-> TODO: there is also the Contract.sizeIncrement, bt wtf is it?
|
||||
# ?TODO, there is also the Contract.sizeIncrement, bt wtf is it?
|
||||
|
||||
# NOTE: this is duplicate from the .broker.norm_trade_records()
|
||||
# routine, we should factor all this parsing somewhere..
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
(Multi-)venue mgmt helpers.
|
||||
|
||||
IB generally supports all "legacy" trading venues, those mostly owned
|
||||
by ICE and friends.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from datetime import ( # noqa
|
||||
datetime,
|
||||
date,
|
||||
tzinfo as TzInfo,
|
||||
)
|
||||
from typing import (
|
||||
Iterator,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import exchange_calendars as xcals
|
||||
from pendulum import (
|
||||
now,
|
||||
Duration,
|
||||
Interval,
|
||||
Time,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ib_insync import (
|
||||
TradingSession,
|
||||
ContractDetails,
|
||||
)
|
||||
from exchange_calendars.exchange_calendars import (
|
||||
ExchangeCalendar,
|
||||
)
|
||||
from pandas import (
|
||||
# DatetimeIndex,
|
||||
TimeDelta,
|
||||
Timestamp,
|
||||
)
|
||||
|
||||
|
||||
def has_weekend(
|
||||
period: Interval,
|
||||
) -> bool:
|
||||
'''
|
||||
Predicate to for a period being within
|
||||
days 6->0 (sat->sun).
|
||||
|
||||
'''
|
||||
has_weekend: bool = False
|
||||
for dt in period:
|
||||
if dt.day_of_week in [0, 6]: # 0=Sunday, 6=Saturday
|
||||
has_weekend = True
|
||||
break
|
||||
|
||||
return has_weekend
|
||||
|
||||
|
||||
def has_holiday(
|
||||
con_deats: ContractDetails,
|
||||
period: Interval,
|
||||
) -> bool:
|
||||
'''
|
||||
Using the `exchange_calendars` lib detect if a time-gap `period`
|
||||
is contained in a known "cash hours" closure.
|
||||
|
||||
'''
|
||||
tz: str = con_deats.timeZoneId
|
||||
exch: str = con_deats.contract.primaryExchange
|
||||
cal: ExchangeCalendar = xcals.get_calendar(exch)
|
||||
end: datetime = period.end
|
||||
# _start: datetime = period.start
|
||||
# ?TODO, can rm ya?
|
||||
# => not that useful?
|
||||
# dti: DatetimeIndex = cal.sessions_in_range(
|
||||
# _start.date(),
|
||||
# end.date(),
|
||||
# )
|
||||
prev_close: Timestamp = cal.previous_close(
|
||||
end.date()
|
||||
).tz_convert(tz)
|
||||
prev_open: Timestamp = cal.previous_open(
|
||||
end.date()
|
||||
).tz_convert(tz)
|
||||
# now do relative from prev_ values ^
|
||||
# to get the next open which should match
|
||||
# "contain" the end of the gap.
|
||||
next_open: Timestamp = cal.next_open(
|
||||
prev_open,
|
||||
).tz_convert(tz)
|
||||
next_open: Timestamp = cal.next_open(
|
||||
prev_open,
|
||||
).tz_convert(tz)
|
||||
_next_close: Timestamp = cal.next_close(
|
||||
prev_close
|
||||
).tz_convert(tz)
|
||||
cash_gap: TimeDelta = next_open - prev_close
|
||||
is_holiday_gap = (
|
||||
cash_gap
|
||||
>
|
||||
period
|
||||
)
|
||||
# XXX, debug
|
||||
# breakpoint()
|
||||
return is_holiday_gap
|
||||
|
||||
|
||||
def is_current_time_in_range(
|
||||
sesh: Interval,
|
||||
when: datetime|None = None,
|
||||
) -> bool:
|
||||
'''
|
||||
Check if current time is within the datetime range.
|
||||
|
||||
Use any/the-same timezone as provided by `start_dt.tzinfo` value
|
||||
in the range.
|
||||
|
||||
'''
|
||||
when: datetime = when or now()
|
||||
return when in sesh
|
||||
|
||||
|
||||
def iter_sessions(
|
||||
con_deats: ContractDetails,
|
||||
) -> Iterator[Interval]:
|
||||
'''
|
||||
Yield `pendulum.Interval`s for all
|
||||
`ibas.ContractDetails.tradingSessions() -> TradingSession`s.
|
||||
|
||||
'''
|
||||
sesh: TradingSession
|
||||
for sesh in con_deats.tradingSessions():
|
||||
yield Interval(*sesh)
|
||||
|
||||
|
||||
def sesh_times(
|
||||
con_deats: ContractDetails,
|
||||
) -> tuple[Time, Time]:
|
||||
'''
|
||||
Based on the earliest trading session provided by the IB API,
|
||||
get the (day-agnostic) times for the start/end.
|
||||
|
||||
'''
|
||||
earliest_sesh: Interval = next(iter_sessions(con_deats))
|
||||
return (
|
||||
earliest_sesh.start.time(),
|
||||
earliest_sesh.end.time(),
|
||||
)
|
||||
# ^?TODO, use `.diff()` to get point-in-time-agnostic period?
|
||||
# https://pendulum.eustace.io/docs/#difference
|
||||
|
||||
|
||||
def is_venue_open(
|
||||
con_deats: ContractDetails,
|
||||
when: datetime|Duration|None = None,
|
||||
) -> bool:
|
||||
'''
|
||||
Check if market-venue is open during `when`, which defaults to
|
||||
"now".
|
||||
|
||||
'''
|
||||
sesh: Interval
|
||||
for sesh in iter_sessions(con_deats):
|
||||
if is_current_time_in_range(
|
||||
sesh=sesh,
|
||||
when=when,
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_venue_closure(
|
||||
gap: Interval,
|
||||
con_deats: ContractDetails,
|
||||
time_step_s: int,
|
||||
) -> bool:
|
||||
'''
|
||||
Check if a provided time-`gap` is just an (expected) trading
|
||||
venue closure period.
|
||||
|
||||
'''
|
||||
open: Time
|
||||
close: Time
|
||||
open, close = sesh_times(con_deats)
|
||||
|
||||
# ensure times are in mkt-native timezone
|
||||
tz: str = con_deats.timeZoneId
|
||||
start = gap.start.in_tz(tz)
|
||||
start_t = start.time()
|
||||
end = gap.end.in_tz(tz)
|
||||
end_t = end.time()
|
||||
if (
|
||||
(
|
||||
start_t in (
|
||||
close,
|
||||
close.subtract(seconds=time_step_s)
|
||||
)
|
||||
and
|
||||
end_t in (
|
||||
open,
|
||||
open.add(seconds=time_step_s),
|
||||
)
|
||||
)
|
||||
or
|
||||
has_weekend(gap)
|
||||
or
|
||||
has_holiday(
|
||||
con_deats=con_deats,
|
||||
period=gap,
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
# breakpoint()
|
||||
return False
|
||||
|
||||
|
||||
# TODO, put this into `._util` and call it from here!
|
||||
#
|
||||
# NOTE, this was generated by @guille from a gpt5 prompt
|
||||
# and was originally thot to be needed before learning about
|
||||
# `ib_insync.contract.ContractDetails._parseSessions()` and
|
||||
# it's downstream meths..
|
||||
#
|
||||
# This is still likely useful to keep for now to parse the
|
||||
# `.tradingHours: str` value manually if we ever decide
|
||||
# to move off `ib_async` and implement our own `trio`/`anyio`
|
||||
# based version Bp
|
||||
#
|
||||
# >attempt to parse the retarted ib "time stampy thing" they
|
||||
# >do for "venue hours" with this.. written by
|
||||
# >gpt5-"thinking",
|
||||
#
|
||||
|
||||
|
||||
def parse_trading_hours(
|
||||
spec: str,
|
||||
tz: TzInfo|None = None
|
||||
) -> dict[
|
||||
date,
|
||||
tuple[datetime, datetime]
|
||||
]|None:
|
||||
'''
|
||||
Parse venue hours like:
|
||||
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
|
||||
|
||||
Returns `dict[date] = (open_dt, close_dt)` or `None` if
|
||||
closed.
|
||||
|
||||
'''
|
||||
if (
|
||||
not isinstance(spec, str)
|
||||
or
|
||||
not spec
|
||||
):
|
||||
raise ValueError('spec must be a non-empty string')
|
||||
|
||||
out: dict[
|
||||
date,
|
||||
tuple[datetime, datetime]
|
||||
]|None = {}
|
||||
|
||||
for part in (p.strip() for p in spec.split(';') if p.strip()):
|
||||
if part.endswith(':CLOSED'):
|
||||
day_s, _ = part.split(':', 1)
|
||||
d = datetime.strptime(day_s, '%Y%m%d').date()
|
||||
out[d] = None
|
||||
continue
|
||||
|
||||
try:
|
||||
start_s, end_s = part.split('-', 1)
|
||||
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
|
||||
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
|
||||
except ValueError as exc:
|
||||
raise ValueError(f'invalid segment: {part}') from exc
|
||||
|
||||
if tz is not None:
|
||||
start_dt = start_dt.replace(tzinfo=tz)
|
||||
end_dt = end_dt.replace(tzinfo=tz)
|
||||
|
||||
out[start_dt.date()] = (start_dt, end_dt)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# ORIG desired usage,
|
||||
#
|
||||
# TODO, for non-drunk tomorrow,
|
||||
# - call above fn and check that `output[today] is not None`
|
||||
# trading_hrs: dict = parse_trading_hours(
|
||||
# details.tradingHours
|
||||
# )
|
||||
# liq_hrs: dict = parse_trading_hours(
|
||||
# details.liquidHours
|
||||
# )
|
||||
|
|
@ -80,20 +80,20 @@ class Sampler:
|
|||
This non-instantiated type is meant to be a singleton within
|
||||
a `samplerd` actor-service spawned once by the user wishing to
|
||||
time-step-sample (real-time) quote feeds, see
|
||||
``.service.maybe_open_samplerd()`` and the below
|
||||
``register_with_sampler()``.
|
||||
`.service.maybe_open_samplerd()` and the below
|
||||
`register_with_sampler()`.
|
||||
|
||||
'''
|
||||
service_nursery: None | trio.Nursery = None
|
||||
service_nursery: None|trio.Nursery = None
|
||||
|
||||
# TODO: we could stick these in a composed type to avoid
|
||||
# angering the "i hate module scoped variables crowd" (yawn).
|
||||
# TODO: we could stick these in a composed type to avoid angering
|
||||
# the "i hate module scoped variables crowd" (yawn).
|
||||
ohlcv_shms: dict[float, list[ShmArray]] = {}
|
||||
|
||||
# holds one-task-per-sample-period tasks which are spawned as-needed by
|
||||
# data feed requests with a given detected time step usually from
|
||||
# history loading.
|
||||
incr_task_cs: trio.CancelScope | None = None
|
||||
incr_task_cs: trio.CancelScope|None = None
|
||||
|
||||
bcast_errors: tuple[Exception] = (
|
||||
trio.BrokenResourceError,
|
||||
|
|
@ -248,8 +248,8 @@ class Sampler:
|
|||
async def broadcast(
|
||||
self,
|
||||
period_s: float,
|
||||
time_stamp: float | None = None,
|
||||
info: dict | None = None,
|
||||
time_stamp: float|None = None,
|
||||
info: dict|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -313,7 +313,7 @@ class Sampler:
|
|||
@classmethod
|
||||
async def broadcast_all(
|
||||
self,
|
||||
info: dict | None = None,
|
||||
info: dict|None = None,
|
||||
) -> None:
|
||||
|
||||
# NOTE: take a copy of subs since removals can happen
|
||||
|
|
@ -330,12 +330,12 @@ class Sampler:
|
|||
async def register_with_sampler(
|
||||
ctx: Context,
|
||||
period_s: float,
|
||||
shms_by_period: dict[float, dict] | None = None,
|
||||
shms_by_period: dict[float, dict]|None = None,
|
||||
|
||||
open_index_stream: bool = True, # open a 2way stream for sample step msgs?
|
||||
sub_for_broadcasts: bool = True, # sampler side to send step updates?
|
||||
|
||||
) -> None:
|
||||
) -> set[int]:
|
||||
|
||||
get_console_log(tractor.current_actor().loglevel)
|
||||
incr_was_started: bool = False
|
||||
|
|
@ -362,7 +362,12 @@ async def register_with_sampler(
|
|||
|
||||
# insert the base 1s period (for OHLC style sampling) into
|
||||
# the increment buffer set to update and shift every second.
|
||||
if shms_by_period is not None:
|
||||
if (
|
||||
shms_by_period is not None
|
||||
# and
|
||||
# feed_is_live.is_set()
|
||||
# ^TODO? pass it in instead?
|
||||
):
|
||||
from ._sharedmem import (
|
||||
attach_shm_array,
|
||||
_Token,
|
||||
|
|
@ -376,12 +381,17 @@ async def register_with_sampler(
|
|||
readonly=False,
|
||||
)
|
||||
shms_by_period[period] = shm
|
||||
Sampler.ohlcv_shms.setdefault(period, []).append(shm)
|
||||
Sampler.ohlcv_shms.setdefault(
|
||||
period,
|
||||
[],
|
||||
).append(shm)
|
||||
|
||||
assert Sampler.ohlcv_shms
|
||||
|
||||
# unblock caller
|
||||
await ctx.started(set(Sampler.ohlcv_shms.keys()))
|
||||
await ctx.started(
|
||||
set(Sampler.ohlcv_shms.keys())
|
||||
)
|
||||
|
||||
if open_index_stream:
|
||||
try:
|
||||
|
|
@ -427,7 +437,7 @@ async def register_with_sampler(
|
|||
|
||||
async def spawn_samplerd(
|
||||
|
||||
loglevel: str | None = None,
|
||||
loglevel: str|None = None,
|
||||
**extra_tractor_kwargs
|
||||
|
||||
) -> bool:
|
||||
|
|
@ -473,7 +483,7 @@ async def spawn_samplerd(
|
|||
@acm
|
||||
async def maybe_open_samplerd(
|
||||
|
||||
loglevel: str | None = None,
|
||||
loglevel: str|None = None,
|
||||
**pikerd_kwargs,
|
||||
|
||||
) -> tractor.Portal: # noqa
|
||||
|
|
@ -498,11 +508,11 @@ async def maybe_open_samplerd(
|
|||
@acm
|
||||
async def open_sample_stream(
|
||||
period_s: float,
|
||||
shms_by_period: dict[float, dict] | None = None,
|
||||
shms_by_period: dict[float, dict]|None = None,
|
||||
open_index_stream: bool = True,
|
||||
sub_for_broadcasts: bool = True,
|
||||
|
||||
cache_key: str | None = None,
|
||||
cache_key: str|None = None,
|
||||
allow_new_sampler: bool = True,
|
||||
|
||||
ensure_is_active: bool = False,
|
||||
|
|
@ -533,6 +543,8 @@ async def open_sample_stream(
|
|||
# yield bistream
|
||||
# else:
|
||||
|
||||
ctx: tractor.Context
|
||||
shm_periods: set[int] # in `int`-seconds
|
||||
async with (
|
||||
# XXX: this should be singleton on a host,
|
||||
# a lone broker-daemon per provider should be
|
||||
|
|
@ -547,10 +559,10 @@ async def open_sample_stream(
|
|||
'open_index_stream': open_index_stream,
|
||||
'sub_for_broadcasts': sub_for_broadcasts,
|
||||
},
|
||||
) as (ctx, first)
|
||||
) as (ctx, shm_periods)
|
||||
):
|
||||
if ensure_is_active:
|
||||
assert len(first) > 1
|
||||
assert len(shm_periods) > 1
|
||||
|
||||
async with (
|
||||
ctx.open_stream(
|
||||
|
|
|
|||
|
|
@ -520,9 +520,12 @@ def open_shm_array(
|
|||
|
||||
# "unlink" created shm on process teardown by
|
||||
# pushing teardown calls onto actor context stack
|
||||
stack = tractor.current_actor().lifetime_stack
|
||||
stack.callback(shmarr.close)
|
||||
stack.callback(shmarr.destroy)
|
||||
stack = tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
).lifetime_stack
|
||||
if stack:
|
||||
stack.callback(shmarr.close)
|
||||
stack.callback(shmarr.destroy)
|
||||
|
||||
return shmarr
|
||||
|
||||
|
|
@ -607,7 +610,10 @@ def attach_shm_array(
|
|||
_known_tokens[key] = token
|
||||
|
||||
# "close" attached shm on actor teardown
|
||||
tractor.current_actor().lifetime_stack.callback(sha.close)
|
||||
if (actor := tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
)):
|
||||
actor.lifetime_stack.callback(sha.close)
|
||||
|
||||
return sha
|
||||
|
||||
|
|
|
|||
|
|
@ -447,7 +447,13 @@ def ldshm(
|
|||
)
|
||||
# last chance manual overwrites in REPL
|
||||
# await tractor.pause()
|
||||
assert aids
|
||||
if not aids:
|
||||
log.warning(
|
||||
f'No gaps were found !?\n'
|
||||
f'fqme: {fqme!r}\n'
|
||||
f'timeframe: {period_s!r}\n'
|
||||
f"WELL THAT'S GOOD NOOZ!\n"
|
||||
)
|
||||
tf2aids[period_s] = aids
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -275,6 +275,18 @@ def get_null_segs(
|
|||
# diff of abs index steps between each zeroed row
|
||||
absi_zdiff: np.ndarray = np.diff(absi_zeros)
|
||||
|
||||
if zero_t.size < 2:
|
||||
try:
|
||||
breakpoint()
|
||||
except RuntimeError:
|
||||
# XXX, if greenback not active from
|
||||
# piker store ldshm cmd..
|
||||
log.exception(
|
||||
"Can't debug single-sample null!\n"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# scan for all frame-indices where the
|
||||
# zeroed-row-abs-index-step-diff is greater then the
|
||||
# expected increment of 1.
|
||||
|
|
@ -434,8 +446,8 @@ def get_null_segs(
|
|||
|
||||
def iter_null_segs(
|
||||
timeframe: float,
|
||||
frame: Frame | None = None,
|
||||
null_segs: tuple | None = None,
|
||||
frame: Frame|None = None,
|
||||
null_segs: tuple|None = None,
|
||||
|
||||
) -> Generator[
|
||||
tuple[
|
||||
|
|
@ -487,7 +499,8 @@ def iter_null_segs(
|
|||
start_dt = None
|
||||
if (
|
||||
absi_start is not None
|
||||
and start_t != 0
|
||||
and
|
||||
start_t != 0
|
||||
):
|
||||
fi_start: int = absi_start - absi_first
|
||||
start_row: Seq = frame[fi_start]
|
||||
|
|
@ -501,8 +514,8 @@ def iter_null_segs(
|
|||
yield (
|
||||
absi_start, absi_end, # abs indices
|
||||
fi_start, fi_end, # relative "frame" indices
|
||||
start_t, end_t,
|
||||
start_dt, end_dt,
|
||||
start_t, end_t, # epoch times
|
||||
start_dt, end_dt, # dts
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,57 @@ import tractor
|
|||
|
||||
from piker.data._formatters import BGM
|
||||
from piker.storage import log
|
||||
from piker.ui._style import get_fonts
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from piker.ui._remote_ctl import AnnotCtl
|
||||
|
||||
|
||||
def humanize_duration(
|
||||
seconds: float,
|
||||
) -> str:
|
||||
'''
|
||||
Convert duration in seconds to short human-readable form.
|
||||
|
||||
Uses smallest appropriate time unit:
|
||||
- d: days
|
||||
- h: hours
|
||||
- m: minutes
|
||||
- s: seconds
|
||||
|
||||
Examples:
|
||||
- 86400 -> "1d"
|
||||
- 28800 -> "8h"
|
||||
- 180 -> "3m"
|
||||
- 45 -> "45s"
|
||||
|
||||
'''
|
||||
abs_secs: float = abs(seconds)
|
||||
|
||||
if abs_secs >= 86400:
|
||||
days: float = abs_secs / 86400
|
||||
if days >= 10 or days == int(days):
|
||||
return f'{int(days)}d'
|
||||
return f'{days:.1f}d'
|
||||
|
||||
elif abs_secs >= 3600:
|
||||
hours: float = abs_secs / 3600
|
||||
if hours >= 10 or hours == int(hours):
|
||||
return f'{int(hours)}h'
|
||||
return f'{hours:.1f}h'
|
||||
|
||||
elif abs_secs >= 60:
|
||||
mins: float = abs_secs / 60
|
||||
if mins >= 10 or mins == int(mins):
|
||||
return f'{int(mins)}m'
|
||||
return f'{mins:.1f}m'
|
||||
|
||||
else:
|
||||
if abs_secs >= 10 or abs_secs == int(abs_secs):
|
||||
return f'{int(abs_secs)}s'
|
||||
return f'{abs_secs:.1f}s'
|
||||
|
||||
|
||||
async def markup_gaps(
|
||||
fqme: str,
|
||||
timeframe: float,
|
||||
|
|
@ -42,12 +88,25 @@ async def markup_gaps(
|
|||
wdts: pl.DataFrame,
|
||||
gaps: pl.DataFrame,
|
||||
|
||||
# XXX, switch on to see txt showing a "humanized" label of each
|
||||
# gap's duration.
|
||||
show_txt: bool = False,
|
||||
|
||||
) -> dict[int, dict]:
|
||||
'''
|
||||
Remote annotate time-gaps in a dt-fielded ts (normally OHLC)
|
||||
with rectangles.
|
||||
|
||||
'''
|
||||
# XXX: force chart redraw FIRST to ensure PlotItem coordinate
|
||||
# system is properly initialized before we position annotations!
|
||||
# Without this, annotations may be misaligned on first creation
|
||||
# due to Qt/pyqtgraph initialization race conditions.
|
||||
await actl.redraw(
|
||||
fqme=fqme,
|
||||
timeframe=timeframe,
|
||||
)
|
||||
|
||||
aids: dict[int] = {}
|
||||
for i in range(gaps.height):
|
||||
row: pl.DataFrame = gaps[i]
|
||||
|
|
@ -55,6 +114,7 @@ async def markup_gaps(
|
|||
# the gap's RIGHT-most bar's OPEN value
|
||||
# at that time (sample) step.
|
||||
iend: int = row['index'][0]
|
||||
|
||||
# dt: datetime = row['dt'][0]
|
||||
# dt_prev: datetime = row['dt_prev'][0]
|
||||
# dt_end_t: float = dt.timestamp()
|
||||
|
|
@ -121,20 +181,34 @@ async def markup_gaps(
|
|||
# iend += margin
|
||||
# istart -= margin
|
||||
|
||||
rect_gap: float = BGM*3/8
|
||||
opn: float = row['open'][0]
|
||||
cls: float = prev_r['close'][0]
|
||||
ro: tuple[float, float] = (
|
||||
iend + rect_gap + 1,
|
||||
opn,
|
||||
)
|
||||
|
||||
# get gap duration for humanized label
|
||||
gap_dur_s: float = row['s_diff'][0]
|
||||
gap_label: str = humanize_duration(gap_dur_s)
|
||||
|
||||
# XXX: get timestamps for server-side index lookup
|
||||
start_time: float = prev_r['time'][0]
|
||||
end_time: float = row['time'][0]
|
||||
|
||||
# BGM=0.16 is the normal diff from overlap between bars, SO
|
||||
# just go slightly "in" from that "between them".
|
||||
from_idx: int = BGM - .06 # = .10
|
||||
lc: tuple[float, float] = (
|
||||
istart - rect_gap, # + 1 ,
|
||||
istart + 1 - from_idx,
|
||||
cls,
|
||||
)
|
||||
ro: tuple[float, float] = (
|
||||
iend + from_idx,
|
||||
opn,
|
||||
)
|
||||
|
||||
diff: float = cls - opn
|
||||
sgn: float = copysign(1, diff)
|
||||
up_gap: bool = sgn == -1
|
||||
down_gap: bool = sgn == 1
|
||||
flat: bool = sgn == 0
|
||||
|
||||
color: str = 'dad_blue'
|
||||
# TODO? mks more sense to have up/down coloring?
|
||||
|
|
@ -149,30 +223,80 @@ async def markup_gaps(
|
|||
start_pos=lc,
|
||||
end_pos=ro,
|
||||
color=color,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
)
|
||||
|
||||
# add up/down rects
|
||||
aid: int = await actl.add_rect(**rect_kwargs)
|
||||
aid: int|None = await actl.add_rect(**rect_kwargs)
|
||||
if aid is None:
|
||||
log.error(
|
||||
f'Failed to add rect for,\n'
|
||||
f'{rect_kwargs!r}\n'
|
||||
f'\n'
|
||||
f'Skipping to next gap!\n'
|
||||
)
|
||||
continue
|
||||
|
||||
assert aid
|
||||
aids[aid] = rect_kwargs
|
||||
direction: str = (
|
||||
'down' if sgn == 1
|
||||
'down' if down_gap
|
||||
else 'up'
|
||||
)
|
||||
# TODO! mk this a `msgspec.Struct` which we deserialize
|
||||
# on the server side!
|
||||
# XXX: send timestamp for server-side index lookup
|
||||
# to ensure alignment with current shm state
|
||||
gap_time: float = row['time'][0]
|
||||
arrow_kwargs: dict[str, Any] = dict(
|
||||
fqme=fqme,
|
||||
timeframe=timeframe,
|
||||
x=iend,
|
||||
x=iend, # fallback if timestamp lookup fails
|
||||
y=cls,
|
||||
time=gap_time, # for server-side index lookup
|
||||
color=color,
|
||||
alpha=160,
|
||||
alpha=169,
|
||||
pointing=direction,
|
||||
# TODO: expose these as params to markup_gaps()?
|
||||
headLen=10,
|
||||
headWidth=2.222,
|
||||
pxMode=True,
|
||||
)
|
||||
|
||||
aid: int = await actl.add_arrow(
|
||||
**arrow_kwargs
|
||||
)
|
||||
|
||||
# add duration label to RHS of arrow
|
||||
if up_gap:
|
||||
anchor = (0, 0)
|
||||
# ^XXX? i dun get dese dims.. XD
|
||||
elif down_gap:
|
||||
anchor = (0, 1) # XXX y, x?
|
||||
else: # no-gap?
|
||||
assert flat
|
||||
anchor = (0, 0) # up from bottom
|
||||
|
||||
# use a slightly smaller font for gap label txt.
|
||||
font, small_font = get_fonts()
|
||||
font_size: int = small_font.px_size - 1
|
||||
assert isinstance(font_size, int)
|
||||
|
||||
if show_txt:
|
||||
text_aid: int = await actl.add_text(
|
||||
fqme=fqme,
|
||||
timeframe=timeframe,
|
||||
text=gap_label,
|
||||
x=iend + 1, # fallback if timestamp lookup fails
|
||||
y=cls,
|
||||
time=gap_time, # server-side index lookup
|
||||
color=color,
|
||||
anchor=anchor,
|
||||
font_size=font_size,
|
||||
)
|
||||
aids[text_aid] = {'text': gap_label}
|
||||
|
||||
# tell chart to redraw all its
|
||||
# graphics view layers Bo
|
||||
await actl.redraw(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from pendulum import (
|
|||
Duration,
|
||||
duration as mk_duration,
|
||||
from_timestamp,
|
||||
timezone,
|
||||
)
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
|
|
@ -57,9 +58,7 @@ from piker.brokers import NoData
|
|||
from piker.accounting import (
|
||||
MktPair,
|
||||
)
|
||||
from piker.data._util import (
|
||||
log,
|
||||
)
|
||||
from piker.log import get_logger
|
||||
from ..data._sharedmem import (
|
||||
maybe_open_shm_array,
|
||||
ShmArray,
|
||||
|
|
@ -97,6 +96,9 @@ if TYPE_CHECKING:
|
|||
# from .feed import _FeedsBus
|
||||
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
# `ShmArray` buffer sizing configuration:
|
||||
_mins_in_day = int(60 * 24)
|
||||
# how much is probably dependent on lifestyle
|
||||
|
|
@ -242,6 +244,18 @@ async def maybe_fill_null_segments(
|
|||
end_dt=end_dt,
|
||||
)
|
||||
|
||||
if (
|
||||
frame_start_dt := (
|
||||
from_timestamp(array['time'][0])
|
||||
) < backfill_until_dt
|
||||
):
|
||||
log.error(
|
||||
f'Invalid frame_start !?\n'
|
||||
f'frame_start_dt: {frame_start_dt!r}\n'
|
||||
f'backfill_until_dt: {backfill_until_dt!r}\n'
|
||||
)
|
||||
await tractor.pause()
|
||||
|
||||
# XXX TODO: pretty sure if i plot tsla, btcusdt.binance
|
||||
# and mnq.cme.ib this causes a Qt crash XXDDD
|
||||
|
||||
|
|
@ -389,7 +403,9 @@ async def start_backfill(
|
|||
|
||||
# based on the sample step size, maybe load a certain amount history
|
||||
update_start_on_prepend: bool = False
|
||||
if backfill_until_dt is None:
|
||||
if (
|
||||
_until_was_none := (backfill_until_dt is None)
|
||||
):
|
||||
|
||||
# TODO: per-provider default history-durations?
|
||||
# -[ ] inside the `open_history_client()` config allow
|
||||
|
|
@ -423,6 +439,8 @@ async def start_backfill(
|
|||
last_start_dt: datetime = backfill_from_dt
|
||||
next_prepend_index: int = backfill_from_shm_index
|
||||
|
||||
est = timezone('EST')
|
||||
|
||||
while last_start_dt > backfill_until_dt:
|
||||
log.info(
|
||||
f'Requesting {timeframe}s frame:\n'
|
||||
|
|
@ -436,9 +454,10 @@ async def start_backfill(
|
|||
next_end_dt,
|
||||
) = await get_hist(
|
||||
timeframe,
|
||||
end_dt=last_start_dt,
|
||||
end_dt=(end_dt_param := last_start_dt),
|
||||
)
|
||||
except NoData as _daterr:
|
||||
except NoData as nodata:
|
||||
_nodata = nodata
|
||||
orig_last_start_dt: datetime = last_start_dt
|
||||
gap_report: str = (
|
||||
f'EMPTY FRAME for `end_dt: {last_start_dt}`?\n'
|
||||
|
|
@ -506,8 +525,32 @@ async def start_backfill(
|
|||
==
|
||||
next_start_dt.timestamp()
|
||||
)
|
||||
assert (
|
||||
(last_time := time[-1])
|
||||
==
|
||||
next_end_dt.timestamp()
|
||||
)
|
||||
|
||||
assert time[-1] == next_end_dt.timestamp()
|
||||
frame_last_dt = from_timestamp(last_time)
|
||||
if (
|
||||
frame_last_dt.add(seconds=timeframe)
|
||||
<
|
||||
end_dt_param
|
||||
):
|
||||
est_frame_last_dt = est.convert(frame_last_dt)
|
||||
est_end_dt_param = est.convert(end_dt_param)
|
||||
log.warning(
|
||||
f'Provider frame ending BEFORE requested end_dt={end_dt_param} ??\n'
|
||||
f'frame_last_dt (EST): {est_frame_last_dt!r}\n'
|
||||
f'end_dt_param (EST): {est_end_dt_param!r}\n'
|
||||
f'\n'
|
||||
f'Likely contains,\n'
|
||||
f'- a venue closure.\n'
|
||||
f'- (maybe?) missing data ?\n'
|
||||
)
|
||||
# ?TODO, check against venue closure hours
|
||||
# if/when provided by backend?
|
||||
await tractor.pause()
|
||||
|
||||
expected_dur: Interval = (
|
||||
last_start_dt.subtract(
|
||||
|
|
@ -569,6 +612,26 @@ async def start_backfill(
|
|||
'0 BARS TO PUSH after diff!?\n'
|
||||
f'{next_start_dt} -> {last_start_dt}'
|
||||
)
|
||||
await tractor.pause()
|
||||
|
||||
# Check if we're about to exceed buffer capacity BEFORE
|
||||
# attempting the push
|
||||
if (next_prepend_index - ln) < 0:
|
||||
log.warning(
|
||||
f'Backfill would exceed buffer capacity!\n'
|
||||
f'next_prepend_index: {next_prepend_index}\n'
|
||||
f'frame size: {ln}\n'
|
||||
f'Truncating to fit remaining space..\n'
|
||||
)
|
||||
# only push what fits
|
||||
to_push = to_push[-(next_prepend_index):]
|
||||
ln = len(to_push)
|
||||
|
||||
if ln == 0:
|
||||
log.warning(
|
||||
'No space left in buffer, stopping backfill!'
|
||||
)
|
||||
break
|
||||
|
||||
# bail gracefully on shm allocation overrun/full
|
||||
# condition
|
||||
|
|
@ -590,6 +653,14 @@ async def start_backfill(
|
|||
next_prepend_index = next_prepend_index - ln
|
||||
last_start_dt = next_start_dt
|
||||
|
||||
# Stop if we've hit buffer start
|
||||
if next_prepend_index <= 0:
|
||||
log.warning(
|
||||
f'Reached buffer start (index={next_prepend_index}), '
|
||||
f'stopping backfill'
|
||||
)
|
||||
break
|
||||
|
||||
except ValueError as ve:
|
||||
_ve = ve
|
||||
log.error(
|
||||
|
|
@ -616,7 +687,7 @@ async def start_backfill(
|
|||
},
|
||||
})
|
||||
|
||||
# can't push the entire frame? so
|
||||
# XXX, can't push the entire frame? so
|
||||
# push only the amount that can fit..
|
||||
break
|
||||
|
||||
|
|
@ -674,8 +745,16 @@ async def start_backfill(
|
|||
deduped,
|
||||
diff,
|
||||
) = dedupe(df)
|
||||
# if diff:
|
||||
# sort_diff(df)
|
||||
if diff:
|
||||
log.warning(
|
||||
f'Found {diff!r} duplicates in tsdb! '
|
||||
f'=> Overwriting with `deduped` data !! <=\n'
|
||||
)
|
||||
await storage.write_ohlcv(
|
||||
col_sym_key,
|
||||
deduped,
|
||||
timeframe,
|
||||
)
|
||||
|
||||
else:
|
||||
# finally filled gap
|
||||
|
|
@ -1242,6 +1321,7 @@ async def manage_history(
|
|||
some_data_ready: trio.Event,
|
||||
feed_is_live: trio.Event,
|
||||
timeframe: float = 60, # in seconds
|
||||
wait_for_live_timeout: float = 0.5,
|
||||
|
||||
task_status: TaskStatus[
|
||||
tuple[ShmArray, ShmArray]
|
||||
|
|
@ -1390,12 +1470,26 @@ async def manage_history(
|
|||
1: rt_shm,
|
||||
60: hist_shm,
|
||||
}
|
||||
async with open_sample_stream(
|
||||
period_s=1.,
|
||||
shms_by_period={
|
||||
|
||||
shms_by_period: dict|None = None
|
||||
with trio.move_on_after(wait_for_live_timeout) as cs:
|
||||
await feed_is_live.wait()
|
||||
|
||||
if cs.cancelled_caught:
|
||||
log.warning(
|
||||
f'No live feed within {wait_for_live_timeout!r}s\n'
|
||||
f'fqme: {mkt.fqme!r}\n'
|
||||
f'NOT activating shm-buffer-sampler!!\n'
|
||||
)
|
||||
|
||||
if feed_is_live.is_set():
|
||||
shms_by_period: dict[int, dict] = {
|
||||
1.: rt_shm.token,
|
||||
60.: hist_shm.token,
|
||||
},
|
||||
}
|
||||
async with open_sample_stream(
|
||||
period_s=1.,
|
||||
shms_by_period=shms_by_period,
|
||||
|
||||
# NOTE: we want to only open a stream for doing
|
||||
# broadcasts on backfill operations, not receive the
|
||||
|
|
|
|||
|
|
@ -27,15 +27,15 @@ import trio
|
|||
from piker.ui.qt import (
|
||||
QEvent,
|
||||
)
|
||||
from ..service import maybe_spawn_brokerd
|
||||
from . import _chart
|
||||
from . import _event
|
||||
from ._exec import run_qtractor
|
||||
from ..data.feed import install_brokerd_search
|
||||
from ..data._symcache import open_symcache
|
||||
from ..accounting import unpack_fqme
|
||||
from . import _search
|
||||
from ._chart import GodWidget
|
||||
from ..accounting import unpack_fqme
|
||||
from ..data._symcache import open_symcache
|
||||
from ..data.feed import install_brokerd_search
|
||||
from ..log import get_logger
|
||||
from ..service import maybe_spawn_brokerd
|
||||
from ._exec import run_qtractor
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
|
@ -73,8 +73,8 @@ async def load_provider_search(
|
|||
|
||||
async def _async_main(
|
||||
|
||||
# implicit required argument provided by ``qtractor_run()``
|
||||
main_widget: GodWidget,
|
||||
# implicit required argument provided by `qtractor_run()`
|
||||
main_widget: _chart.GodWidget,
|
||||
|
||||
syms: list[str],
|
||||
brokers: dict[str, ModuleType],
|
||||
|
|
@ -87,6 +87,9 @@ async def _async_main(
|
|||
Provision the "main" widget with initial symbol data and root nursery.
|
||||
|
||||
"""
|
||||
# set as singleton
|
||||
_chart._godw = main_widget
|
||||
|
||||
from . import _display
|
||||
from ._pg_overrides import _do_overrides
|
||||
_do_overrides()
|
||||
|
|
@ -201,6 +204,6 @@ def _main(
|
|||
brokermods,
|
||||
piker_loglevel,
|
||||
),
|
||||
main_widget_type=GodWidget,
|
||||
main_widget_type=_chart.GodWidget,
|
||||
tractor_kwargs=tractor_kwargs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -82,6 +82,25 @@ if TYPE_CHECKING:
|
|||
log = get_logger(__name__)
|
||||
|
||||
|
||||
_godw: GodWidget|None = None
|
||||
|
||||
def get_godw() -> GodWidget:
|
||||
'''
|
||||
Get the top level "god widget", the root/central-most Qt
|
||||
widget-object set as `QMainWindow.setCentralWidget(_godw)`.
|
||||
|
||||
See `piker.ui._exec` for the runtime init details and all the
|
||||
machinery for running `trio` on the Qt event loop in guest mode.
|
||||
|
||||
'''
|
||||
if _godw is None:
|
||||
raise RuntimeError(
|
||||
'No god-widget initialized ??\n'
|
||||
'Have you called `run_qtractor()` yet?\n'
|
||||
)
|
||||
return _godw
|
||||
|
||||
|
||||
class GodWidget(QWidget):
|
||||
'''
|
||||
"Our lord and savior, the holy child of window-shua, there is no
|
||||
|
|
@ -104,7 +123,7 @@ class GodWidget(QWidget):
|
|||
|
||||
super().__init__(parent)
|
||||
|
||||
self.search: SearchWidget | None = None
|
||||
self.search: SearchWidget|None = None
|
||||
|
||||
self.hbox = QHBoxLayout(self)
|
||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -123,9 +142,9 @@ class GodWidget(QWidget):
|
|||
tuple[LinkedSplits, LinkedSplits],
|
||||
] = {}
|
||||
|
||||
self.hist_linked: LinkedSplits | None = None
|
||||
self.rt_linked: LinkedSplits | None = None
|
||||
self._active_cursor: Cursor | None = None
|
||||
self.hist_linked: LinkedSplits|None = None
|
||||
self.rt_linked: LinkedSplits|None = None
|
||||
self._active_cursor: Cursor|None = None
|
||||
|
||||
# assigned in the startup func `_async_main()`
|
||||
self._root_n: trio.Nursery = None
|
||||
|
|
@ -369,9 +388,9 @@ class ChartnPane(QFrame):
|
|||
https://doc.qt.io/qt-5/qwidget.html#composite-widgets
|
||||
|
||||
'''
|
||||
sidepane: FieldsForm | SearchWidget
|
||||
sidepane: FieldsForm|SearchWidget
|
||||
hbox: QHBoxLayout
|
||||
chart: ChartPlotWidget | None = None
|
||||
chart: ChartPlotWidget|None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -387,13 +406,13 @@ class ChartnPane(QFrame):
|
|||
self.chart = None
|
||||
|
||||
hbox = self.hbox = QHBoxLayout(self)
|
||||
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||
hbox.setAlignment(Qt.AlignTop|Qt.AlignLeft)
|
||||
hbox.setContentsMargins(0, 0, 0, 0)
|
||||
hbox.setSpacing(3)
|
||||
|
||||
def set_sidepane(
|
||||
self,
|
||||
sidepane: FieldsForm | SearchWidget,
|
||||
sidepane: FieldsForm|SearchWidget,
|
||||
) -> None:
|
||||
|
||||
# add sidepane **after** chart; place it on axis side
|
||||
|
|
@ -404,7 +423,7 @@ class ChartnPane(QFrame):
|
|||
self._sidepane = sidepane
|
||||
|
||||
@property
|
||||
def sidepane(self) -> FieldsForm | SearchWidget:
|
||||
def sidepane(self) -> FieldsForm|SearchWidget:
|
||||
return self._sidepane
|
||||
|
||||
|
||||
|
|
@ -450,7 +469,7 @@ class LinkedSplits(QWidget):
|
|||
# chart-local graphics state that can be passed to
|
||||
# a ``graphic_update_cycle()`` call by any task wishing to
|
||||
# update the UI for a given "chart instance".
|
||||
self.display_state: DisplayState | None = None
|
||||
self.display_state: DisplayState|None = None
|
||||
|
||||
self._mkt: MktPair = None
|
||||
|
||||
|
|
@ -486,7 +505,7 @@ class LinkedSplits(QWidget):
|
|||
|
||||
def set_split_sizes(
|
||||
self,
|
||||
prop: float | None = None,
|
||||
prop: float|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -567,8 +586,8 @@ class LinkedSplits(QWidget):
|
|||
|
||||
# style?
|
||||
self.chart.setFrameStyle(
|
||||
QFrame.Shape.StyledPanel |
|
||||
QFrame.Shadow.Plain
|
||||
QFrame.Shape.StyledPanel
|
||||
|QFrame.Shadow.Plain
|
||||
)
|
||||
|
||||
return self.chart
|
||||
|
|
@ -580,11 +599,11 @@ class LinkedSplits(QWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: str | None = None,
|
||||
array_key: str|None = None,
|
||||
style: str = 'line',
|
||||
_is_main: bool = False,
|
||||
|
||||
sidepane: QWidget | None = None,
|
||||
sidepane: QWidget|None = None,
|
||||
draw_kwargs: dict = {},
|
||||
|
||||
**cpw_kwargs,
|
||||
|
|
@ -687,7 +706,7 @@ class LinkedSplits(QWidget):
|
|||
cpw.plotItem.vb.linked = self
|
||||
cpw.setFrameStyle(
|
||||
QFrame.Shape.StyledPanel
|
||||
# | QFrame.Shadow.Plain
|
||||
# |QFrame.Shadow.Plain
|
||||
)
|
||||
|
||||
# don't show the little "autoscale" A label.
|
||||
|
|
@ -800,7 +819,7 @@ class LinkedSplits(QWidget):
|
|||
|
||||
def resize_sidepanes(
|
||||
self,
|
||||
from_linked: LinkedSplits | None = None,
|
||||
from_linked: LinkedSplits|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -874,7 +893,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# TODO: load from config
|
||||
use_open_gl: bool = False,
|
||||
|
||||
static_yrange: tuple[float, float] | None = None,
|
||||
static_yrange: tuple[float, float]|None = None,
|
||||
|
||||
parent=None,
|
||||
**kwargs,
|
||||
|
|
@ -889,7 +908,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
# NOTE: must be set bfore calling ``.mk_vb()``
|
||||
self.linked = linkedsplits
|
||||
self.sidepane: FieldsForm | None = None
|
||||
self.sidepane: FieldsForm|None = None
|
||||
|
||||
# source of our custom interactions
|
||||
self.cv = self.mk_vb(name)
|
||||
|
|
@ -923,7 +942,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self.useOpenGL(use_open_gl)
|
||||
self.name = name
|
||||
self.data_key = data_key or name
|
||||
self.qframe: ChartnPane | None = None
|
||||
self.qframe: ChartnPane|None = None
|
||||
|
||||
# scene-local placeholder for book graphics
|
||||
# sizing to avoid overlap with data contents
|
||||
|
|
@ -934,7 +953,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# registry of overlay curve names
|
||||
self._vizs: dict[str, Viz] = {}
|
||||
|
||||
self.feed: Feed | None = None
|
||||
self.feed: Feed|None = None
|
||||
|
||||
self._labels = {} # registry of underlying graphics
|
||||
self._ysticks = {} # registry of underlying graphics
|
||||
|
|
@ -1027,7 +1046,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
def increment_view(
|
||||
self,
|
||||
datums: int = 1,
|
||||
vb: ChartView | None = None,
|
||||
vb: ChartView|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -1058,8 +1077,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
def overlay_plotitem(
|
||||
self,
|
||||
name: str,
|
||||
index: int | None = None,
|
||||
axis_title: str | None = None,
|
||||
index: int|None = None,
|
||||
axis_title: str|None = None,
|
||||
axis_side: str = 'right',
|
||||
axis_kwargs: dict = {},
|
||||
|
||||
|
|
@ -1147,14 +1166,14 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: str | None = None,
|
||||
array_key: str|None = None,
|
||||
overlay: bool = False,
|
||||
color: str | None = None,
|
||||
color: str|None = None,
|
||||
add_label: bool = True,
|
||||
pi: pg.PlotItem | None = None,
|
||||
pi: pg.PlotItem|None = None,
|
||||
step_mode: bool = False,
|
||||
is_ohlc: bool = False,
|
||||
add_sticky: None | str = 'right',
|
||||
add_sticky: None|str = 'right',
|
||||
|
||||
**graphics_kwargs,
|
||||
|
||||
|
|
@ -1252,7 +1271,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# use the tick size precision for display
|
||||
name = name or pi.name
|
||||
mkt: MktPair = self.linked.mkt
|
||||
digits: int | None = None
|
||||
digits: int|None = None
|
||||
if name in mkt.fqme:
|
||||
digits = mkt.price_tick_digits
|
||||
|
||||
|
|
@ -1286,7 +1305,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: str | None = None,
|
||||
array_key: str|None = None,
|
||||
**draw_curve_kwargs,
|
||||
|
||||
) -> Viz:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import pyqtgraph as pg
|
|||
|
||||
from piker.ui.qt import (
|
||||
QtWidgets,
|
||||
QGraphicsItem,
|
||||
Qt,
|
||||
QLineF,
|
||||
QRectF,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ class ArrowEditor(Struct):
|
|||
] = None,
|
||||
alpha: int = 255,
|
||||
zval: float = 1e9,
|
||||
headLen: float|None = None,
|
||||
headWidth: float|None = None,
|
||||
tailLen: float|None = None,
|
||||
tailWidth: float|None = None,
|
||||
pxMode: bool = True,
|
||||
|
||||
) -> pg.ArrowItem:
|
||||
'''
|
||||
|
|
@ -109,6 +114,15 @@ class ArrowEditor(Struct):
|
|||
# scale arrow sizing to dpi-aware font
|
||||
size = _font.font.pixelSize() * 0.8
|
||||
|
||||
# allow caller override of head dimensions
|
||||
if headLen is None:
|
||||
headLen = size
|
||||
if headWidth is None:
|
||||
headWidth = size/2
|
||||
# tail params default to None (no tail)
|
||||
if tailWidth is None:
|
||||
tailWidth = 3
|
||||
|
||||
color = color or 'default'
|
||||
color = QColor(hcolor(color))
|
||||
color.setAlpha(alpha)
|
||||
|
|
@ -117,10 +131,11 @@ class ArrowEditor(Struct):
|
|||
arrow = pg.ArrowItem(
|
||||
angle=angle,
|
||||
baseAngle=0,
|
||||
headLen=size,
|
||||
headWidth=size/2,
|
||||
tailLen=None,
|
||||
pxMode=True,
|
||||
headLen=headLen,
|
||||
headWidth=headWidth,
|
||||
tailLen=tailLen,
|
||||
tailWidth=tailWidth,
|
||||
pxMode=pxMode,
|
||||
# coloring
|
||||
pen=pen,
|
||||
brush=brush,
|
||||
|
|
@ -154,7 +169,10 @@ class ArrowEditor(Struct):
|
|||
f'{arrow!r}\n'
|
||||
)
|
||||
for linked in self.godw.iter_linked():
|
||||
linked.chart.plotItem.removeItem(arrow)
|
||||
if not (chart := linked.chart):
|
||||
continue
|
||||
|
||||
chart.plotItem.removeItem(arrow)
|
||||
try:
|
||||
arrows.remove(arrow)
|
||||
except ValueError:
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ def run_qtractor(
|
|||
window_type: QMainWindow = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Run the Qt event loop and embed `trio` via guest mode on it.
|
||||
|
||||
'''
|
||||
# avoids annoying message when entering debugger from qt loop
|
||||
pyqtRemoveInputHook()
|
||||
|
||||
|
|
@ -170,7 +174,7 @@ def run_qtractor(
|
|||
# hook into app focus change events
|
||||
app.focusChanged.connect(window.on_focus_change)
|
||||
|
||||
instance = main_widget_type()
|
||||
instance: GodWidget = main_widget_type()
|
||||
instance.window = window
|
||||
|
||||
# override tractor's defaults
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ log = get_logger(__name__)
|
|||
def update_fsp_chart(
|
||||
viz,
|
||||
graphics_name: str,
|
||||
array_key: str | None,
|
||||
array_key: str|None,
|
||||
**kwargs,
|
||||
|
||||
) -> None:
|
||||
|
|
@ -87,7 +87,11 @@ def update_fsp_chart(
|
|||
|
||||
# guard against unreadable case
|
||||
if not last_row:
|
||||
log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}')
|
||||
log.warning(
|
||||
f'Read-race on shm array,\n'
|
||||
f'graphics_name: {graphics_name!r}\n'
|
||||
f'shm.token: {shm.token}\n'
|
||||
)
|
||||
return
|
||||
|
||||
# update graphics
|
||||
|
|
@ -203,7 +207,6 @@ async def open_fsp_actor_cluster(
|
|||
|
||||
|
||||
async def run_fsp_ui(
|
||||
|
||||
linkedsplits: LinkedSplits,
|
||||
flume: Flume,
|
||||
started: trio.Event,
|
||||
|
|
@ -471,7 +474,7 @@ class FspAdmin:
|
|||
target: Fsp,
|
||||
conf: dict[str, dict[str, Any]],
|
||||
|
||||
worker_name: str | None = None,
|
||||
worker_name: str|None = None,
|
||||
loglevel: str = 'info',
|
||||
|
||||
) -> (Flume, trio.Event):
|
||||
|
|
@ -623,8 +626,10 @@ async def open_fsp_admin(
|
|||
event.set()
|
||||
|
||||
|
||||
# TODO, passing in `pikerd` related settings here!
|
||||
# [ ] read in the `tractor` setting for `enable_transports: list`
|
||||
# from the root `conf.toml`!
|
||||
async def open_vlm_displays(
|
||||
|
||||
linked: LinkedSplits,
|
||||
flume: Flume,
|
||||
dvlm: bool = True,
|
||||
|
|
@ -634,12 +639,12 @@ async def open_vlm_displays(
|
|||
|
||||
) -> None:
|
||||
'''
|
||||
Volume subchart displays.
|
||||
Vlm (volume) subchart displays.
|
||||
|
||||
Since "volume" is often included directly alongside OHLCV price
|
||||
data, we don't really need a separate FSP-actor + shm array for it
|
||||
since it's likely already directly adjacent to OHLC samples from the
|
||||
data provider.
|
||||
data, we don't really need a separate FSP-actor + shm array for
|
||||
it since it's likely already directly adjacent to OHLC samples
|
||||
from the data provider.
|
||||
|
||||
Further only if volume data is detected (it sometimes isn't provided
|
||||
eg. forex, certain commodities markets) will volume dependent FSPs
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from piker.brokers import SymbolNotFound
|
|||
from piker.ui.qt import (
|
||||
QGraphicsItem,
|
||||
)
|
||||
from PyQt6.QtGui import QFont
|
||||
from ._display import DisplayState
|
||||
from ._interaction import ChartView
|
||||
from ._editors import (
|
||||
|
|
@ -57,6 +58,7 @@ from ._editors import (
|
|||
)
|
||||
from ._chart import ChartPlotWidget
|
||||
from ._dataviz import Viz
|
||||
from ._style import hcolor
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
|
@ -93,7 +95,7 @@ _annots: AnnotsTable = {}
|
|||
_editors: EditorsTable = {}
|
||||
|
||||
def rm_annot(
|
||||
annot: ArrowEditor|SelectRect
|
||||
annot: ArrowEditor|SelectRect|pg.TextItem
|
||||
) -> bool:
|
||||
global _editors
|
||||
match annot:
|
||||
|
|
@ -114,6 +116,12 @@ def rm_annot(
|
|||
annot.delete()
|
||||
return True
|
||||
|
||||
case pg.TextItem():
|
||||
scene = annot.scene()
|
||||
if scene:
|
||||
scene.removeItem(annot)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -141,12 +149,76 @@ async def serve_rc_annots(
|
|||
'kwargs': dict(kwargs),
|
||||
}:
|
||||
ds: DisplayState = _dss[fqme]
|
||||
chart: ChartPlotWidget = {
|
||||
60: ds.hist_chart,
|
||||
1: ds.chart,
|
||||
}[timeframe]
|
||||
try:
|
||||
chart: ChartPlotWidget = {
|
||||
60: ds.hist_chart,
|
||||
1: ds.chart,
|
||||
}[timeframe]
|
||||
except KeyError:
|
||||
msg: str = (
|
||||
f'No chart for timeframe={timeframe}s, '
|
||||
f'skipping rect annotation'
|
||||
)
|
||||
log.exeception(msg)
|
||||
await annot_req_stream.send({'error': msg})
|
||||
continue
|
||||
|
||||
cv: ChartView = chart.cv
|
||||
|
||||
# NEW: if timestamps provided, lookup current indices
|
||||
# from shm to ensure alignment with current buffer
|
||||
# state
|
||||
start_time = kwargs.pop('start_time', None)
|
||||
end_time = kwargs.pop('end_time', None)
|
||||
if (
|
||||
start_time is not None
|
||||
and end_time is not None
|
||||
):
|
||||
viz: Viz = chart.get_viz(fqme)
|
||||
shm = viz.shm
|
||||
arr = shm.array
|
||||
|
||||
# lookup start index
|
||||
start_matches = arr[arr['time'] == start_time]
|
||||
if len(start_matches) == 0:
|
||||
msg: str = (
|
||||
f'No shm entry for start_time={start_time}, '
|
||||
f'skipping rect'
|
||||
)
|
||||
log.error(msg)
|
||||
await annot_req_stream.send({'error': msg})
|
||||
continue
|
||||
|
||||
# lookup end index
|
||||
end_matches = arr[arr['time'] == end_time]
|
||||
if len(end_matches) == 0:
|
||||
msg: str = (
|
||||
f'No shm entry for end_time={end_time}, '
|
||||
f'skipping rect'
|
||||
)
|
||||
log.error(msg)
|
||||
await annot_req_stream.send({'error': msg})
|
||||
continue
|
||||
|
||||
# get close price from start bar, open from end
|
||||
# bar
|
||||
start_idx = float(start_matches[0]['index'])
|
||||
end_idx = float(end_matches[0]['index'])
|
||||
start_close = float(start_matches[0]['close'])
|
||||
end_open = float(end_matches[0]['open'])
|
||||
|
||||
# reconstruct start_pos and end_pos with
|
||||
# looked-up indices
|
||||
from_idx: float = 0.16 - 0.06 # BGM offset
|
||||
kwargs['start_pos'] = (
|
||||
start_idx + 1 - from_idx,
|
||||
start_close,
|
||||
)
|
||||
kwargs['end_pos'] = (
|
||||
end_idx + from_idx,
|
||||
end_open,
|
||||
)
|
||||
|
||||
# annot type lookup from cmd
|
||||
rect = SelectRect(
|
||||
viewbox=cv,
|
||||
|
|
@ -165,6 +237,12 @@ async def serve_rc_annots(
|
|||
# delegate generically to the requested method
|
||||
getattr(rect, meth)(**kwargs)
|
||||
rect.show()
|
||||
|
||||
# XXX: store absolute coords for repositioning
|
||||
# during viz redraws (eg backfill updates)
|
||||
rect._meth = meth
|
||||
rect._kwargs = kwargs
|
||||
|
||||
aid: int = id(rect)
|
||||
annots[aid] = rect
|
||||
aids: set[int] = ctxs[ipc_key][1]
|
||||
|
|
@ -183,18 +261,52 @@ async def serve_rc_annots(
|
|||
'color': color,
|
||||
'aid': str()|None as aid,
|
||||
'alpha': int(alpha),
|
||||
'headLen': int()|float()|None as headLen,
|
||||
'headWidth': int()|float()|None as headWidth,
|
||||
'tailLen': int()|float()|None as tailLen,
|
||||
'tailWidth': int()|float()|None as tailWidth,
|
||||
'pxMode': bool(pxMode),
|
||||
'time': int()|float()|None as timestamp,
|
||||
},
|
||||
# ?TODO? split based on method fn-sigs?
|
||||
# 'pointing',
|
||||
}:
|
||||
ds: DisplayState = _dss[fqme]
|
||||
chart: ChartPlotWidget = {
|
||||
60: ds.hist_chart,
|
||||
1: ds.chart,
|
||||
}[timeframe]
|
||||
try:
|
||||
chart: ChartPlotWidget = {
|
||||
60: ds.hist_chart,
|
||||
1: ds.chart,
|
||||
}[timeframe]
|
||||
except KeyError:
|
||||
log.warning(
|
||||
f'No chart for timeframe={timeframe}s, '
|
||||
f'skipping arrow annotation'
|
||||
)
|
||||
# return -1 to indicate failure
|
||||
await annot_req_stream.send(-1)
|
||||
continue
|
||||
cv: ChartView = chart.cv
|
||||
godw = chart.linked.godwidget
|
||||
|
||||
# NEW: if timestamp provided, lookup current index
|
||||
# from shm to ensure alignment with current buffer
|
||||
# state
|
||||
if timestamp is not None:
|
||||
viz: Viz = chart.get_viz(fqme)
|
||||
shm = viz.shm
|
||||
arr = shm.array
|
||||
# find index where time matches timestamp
|
||||
matches = arr[arr['time'] == timestamp]
|
||||
if len(matches) == 0:
|
||||
log.error(
|
||||
f'No shm entry for timestamp={timestamp}, '
|
||||
f'skipping arrow annotation'
|
||||
)
|
||||
await annot_req_stream.send(-1)
|
||||
continue
|
||||
# use the matched row's index as x
|
||||
x = float(matches[0]['index'])
|
||||
|
||||
arrows = ArrowEditor(godw=godw)
|
||||
# `.add/.remove()` API
|
||||
if meth != 'add':
|
||||
|
|
@ -213,15 +325,109 @@ async def serve_rc_annots(
|
|||
pointing=pointing,
|
||||
color=color,
|
||||
alpha=alpha,
|
||||
headLen=headLen,
|
||||
headWidth=headWidth,
|
||||
tailLen=tailLen,
|
||||
tailWidth=tailWidth,
|
||||
pxMode=pxMode,
|
||||
)
|
||||
# XXX: store absolute coords for repositioning
|
||||
# during viz redraws (eg backfill updates)
|
||||
arrow._abs_x = x
|
||||
arrow._abs_y = y
|
||||
|
||||
annots[aid] = arrow
|
||||
_editors[aid] = arrows
|
||||
aids: set[int] = ctxs[ipc_key][1]
|
||||
aids.add(aid)
|
||||
await annot_req_stream.send(aid)
|
||||
|
||||
# TODO, use `pg.TextItem` to put a humaized
|
||||
# time label beside the arrows
|
||||
case {
|
||||
'cmd': 'TextItem',
|
||||
'fqme': fqme,
|
||||
'timeframe': timeframe,
|
||||
'kwargs': {
|
||||
'text': str(text),
|
||||
'x': int()|float() as x,
|
||||
'y': int()|float() as y,
|
||||
'color': color,
|
||||
'anchor': list(anchor),
|
||||
'font_size': int()|None as font_size,
|
||||
'time': int()|float()|None as timestamp,
|
||||
},
|
||||
}:
|
||||
ds: DisplayState = _dss[fqme]
|
||||
try:
|
||||
chart: ChartPlotWidget = {
|
||||
60: ds.hist_chart,
|
||||
1: ds.chart,
|
||||
}[timeframe]
|
||||
except KeyError:
|
||||
log.warning(
|
||||
f'No chart for timeframe={timeframe}s, '
|
||||
f'skipping text annotation'
|
||||
)
|
||||
await annot_req_stream.send(-1)
|
||||
continue
|
||||
|
||||
# NEW: if timestamp provided, lookup current index
|
||||
# from shm to ensure alignment with current buffer
|
||||
# state
|
||||
if timestamp is not None:
|
||||
viz: Viz = chart.get_viz(fqme)
|
||||
shm = viz.shm
|
||||
arr = shm.array
|
||||
# find index where time matches timestamp
|
||||
matches = arr[arr['time'] == timestamp]
|
||||
if len(matches) == 0:
|
||||
log.error(
|
||||
f'No shm entry for timestamp={timestamp}, '
|
||||
f'skipping text annotation'
|
||||
)
|
||||
await annot_req_stream.send(-1)
|
||||
continue
|
||||
# use the matched row's index as x, +1 for text
|
||||
# offset
|
||||
x = float(matches[0]['index']) + 1
|
||||
|
||||
# convert named color to hex
|
||||
color_hex: str = hcolor(color)
|
||||
|
||||
# create text item
|
||||
text_item: pg.TextItem = pg.TextItem(
|
||||
text=text,
|
||||
color=color_hex,
|
||||
anchor=anchor,
|
||||
|
||||
# ?TODO, pin to github:main for this?
|
||||
# legacy, can have scaling ish?
|
||||
# ensureInBounds=True,
|
||||
)
|
||||
|
||||
# apply font size (default to DpiAwareFont if not
|
||||
# provided)
|
||||
if font_size is None:
|
||||
from ._style import get_fonts
|
||||
font, font_small = get_fonts()
|
||||
font_size = font_small.px_size - 1
|
||||
|
||||
qfont: QFont = text_item.textItem.font()
|
||||
qfont.setPixelSize(font_size)
|
||||
text_item.setFont(qfont)
|
||||
|
||||
text_item.setPos(x, y)
|
||||
chart.plotItem.addItem(text_item)
|
||||
|
||||
# XXX: store absolute coords for repositioning
|
||||
# during viz redraws (eg backfill updates)
|
||||
text_item._abs_x = x
|
||||
text_item._abs_y = y
|
||||
|
||||
aid: str = str(uuid4())
|
||||
annots[aid] = text_item
|
||||
aids: set[int] = ctxs[ipc_key][1]
|
||||
aids.add(aid)
|
||||
await annot_req_stream.send(aid)
|
||||
|
||||
case {
|
||||
'cmd': 'remove',
|
||||
|
|
@ -262,6 +468,38 @@ async def serve_rc_annots(
|
|||
)
|
||||
viz.reset_graphics()
|
||||
|
||||
# XXX: reposition all annotations to ensure they
|
||||
# stay aligned with viz data after reset (eg during
|
||||
# backfill when abs-index range changes)
|
||||
n_repositioned: int = 0
|
||||
for aid, annot in annots.items():
|
||||
# arrows and text items use abs x,y coords
|
||||
if (
|
||||
hasattr(annot, '_abs_x')
|
||||
and
|
||||
hasattr(annot, '_abs_y')
|
||||
):
|
||||
annot.setPos(
|
||||
annot._abs_x,
|
||||
annot._abs_y,
|
||||
)
|
||||
n_repositioned += 1
|
||||
|
||||
# rects use method + kwargs
|
||||
elif (
|
||||
hasattr(annot, '_meth')
|
||||
and
|
||||
hasattr(annot, '_kwargs')
|
||||
):
|
||||
getattr(annot, annot._meth)(**annot._kwargs)
|
||||
n_repositioned += 1
|
||||
|
||||
if n_repositioned:
|
||||
log.info(
|
||||
f'Repositioned {n_repositioned} annotation(s) '
|
||||
f'after viz redraw'
|
||||
)
|
||||
|
||||
case _:
|
||||
log.error(
|
||||
'Unknown remote annotation cmd:\n'
|
||||
|
|
@ -350,36 +588,47 @@ class AnnotCtl(Struct):
|
|||
|
||||
from_acm: bool = False,
|
||||
|
||||
) -> int:
|
||||
# NEW: optional timestamps for server-side index lookup
|
||||
start_time: float|None = None,
|
||||
end_time: float|None = None,
|
||||
|
||||
) -> int|None:
|
||||
'''
|
||||
Add a `SelectRect` annotation to the target view, return
|
||||
the instances `id(obj)` from the remote UI actor.
|
||||
|
||||
'''
|
||||
ipc: MsgStream = self._get_ipc(fqme)
|
||||
await ipc.send({
|
||||
'fqme': fqme,
|
||||
'cmd': 'SelectRect',
|
||||
'timeframe': timeframe,
|
||||
# 'meth': str(meth),
|
||||
'meth': 'set_view_pos' if domain == 'view' else 'set_scene_pos',
|
||||
'kwargs': {
|
||||
'start_pos': tuple(start_pos),
|
||||
'end_pos': tuple(end_pos),
|
||||
'color': color,
|
||||
'update_label': False,
|
||||
},
|
||||
})
|
||||
aid: int = await ipc.receive()
|
||||
self._ipcs[aid] = ipc
|
||||
if not from_acm:
|
||||
self._annot_stack.push_async_callback(
|
||||
partial(
|
||||
self.remove,
|
||||
aid,
|
||||
with trio.fail_after(3):
|
||||
await ipc.send({
|
||||
'fqme': fqme,
|
||||
'cmd': 'SelectRect',
|
||||
'timeframe': timeframe,
|
||||
# 'meth': str(meth),
|
||||
'meth': 'set_view_pos' if domain == 'view' else 'set_scene_pos',
|
||||
'kwargs': {
|
||||
'start_pos': tuple(start_pos),
|
||||
'end_pos': tuple(end_pos),
|
||||
'color': color,
|
||||
'update_label': False,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
},
|
||||
})
|
||||
aid: int|dict = await ipc.receive()
|
||||
match aid:
|
||||
case {'error': str(msg)}:
|
||||
log.error(msg)
|
||||
return None
|
||||
self._ipcs[aid] = ipc
|
||||
if not from_acm:
|
||||
self._annot_stack.push_async_callback(
|
||||
partial(
|
||||
self.remove,
|
||||
aid,
|
||||
)
|
||||
)
|
||||
)
|
||||
return aid
|
||||
return aid
|
||||
|
||||
async def remove(
|
||||
self,
|
||||
|
|
@ -441,41 +690,116 @@ class AnnotCtl(Struct):
|
|||
# domain: str = 'view', # or 'scene'
|
||||
color: str = 'dad_blue',
|
||||
alpha: int = 116,
|
||||
headLen: float|None = None,
|
||||
headWidth: float|None = None,
|
||||
tailLen: float|None = None,
|
||||
tailWidth: float|None = None,
|
||||
pxMode: bool = True,
|
||||
|
||||
from_acm: bool = False,
|
||||
|
||||
) -> int:
|
||||
# NEW: optional timestamp for server-side index lookup
|
||||
time: float|None = None,
|
||||
|
||||
) -> int|None:
|
||||
'''
|
||||
Add a `SelectRect` annotation to the target view, return
|
||||
the instances `id(obj)` from the remote UI actor.
|
||||
|
||||
'''
|
||||
ipc: MsgStream = self._get_ipc(fqme)
|
||||
await ipc.send({
|
||||
'fqme': fqme,
|
||||
'cmd': 'ArrowEditor',
|
||||
'timeframe': timeframe,
|
||||
# 'meth': str(meth),
|
||||
'meth': 'add',
|
||||
'kwargs': {
|
||||
'x': float(x),
|
||||
'y': float(y),
|
||||
'color': color,
|
||||
'pointing': pointing, # up|down
|
||||
'alpha': alpha,
|
||||
'aid': None,
|
||||
},
|
||||
})
|
||||
aid: int = await ipc.receive()
|
||||
self._ipcs[aid] = ipc
|
||||
if not from_acm:
|
||||
self._annot_stack.push_async_callback(
|
||||
partial(
|
||||
self.remove,
|
||||
aid,
|
||||
with trio.fail_after(3):
|
||||
await ipc.send({
|
||||
'fqme': fqme,
|
||||
'cmd': 'ArrowEditor',
|
||||
'timeframe': timeframe,
|
||||
# 'meth': str(meth),
|
||||
'meth': 'add',
|
||||
'kwargs': {
|
||||
'x': float(x),
|
||||
'y': float(y),
|
||||
'color': color,
|
||||
'pointing': pointing, # up|down
|
||||
'alpha': alpha,
|
||||
'aid': None,
|
||||
'headLen': headLen,
|
||||
'headWidth': headWidth,
|
||||
'tailLen': tailLen,
|
||||
'tailWidth': tailWidth,
|
||||
'pxMode': pxMode,
|
||||
'time': time, # for server-side index lookup
|
||||
},
|
||||
})
|
||||
aid: int|dict = await ipc.receive()
|
||||
match aid:
|
||||
case {'error': str(msg)}:
|
||||
log.error(msg)
|
||||
return None
|
||||
|
||||
self._ipcs[aid] = ipc
|
||||
if not from_acm:
|
||||
self._annot_stack.push_async_callback(
|
||||
partial(
|
||||
self.remove,
|
||||
aid,
|
||||
)
|
||||
)
|
||||
)
|
||||
return aid
|
||||
return aid
|
||||
|
||||
async def add_text(
|
||||
self,
|
||||
fqme: str,
|
||||
timeframe: float,
|
||||
text: str,
|
||||
x: float,
|
||||
y: float,
|
||||
color: str|tuple = 'dad_blue',
|
||||
anchor: tuple[float, float] = (0, 1),
|
||||
font_size: int|None = None,
|
||||
|
||||
from_acm: bool = False,
|
||||
|
||||
# NEW: optional timestamp for server-side index lookup
|
||||
time: float|None = None,
|
||||
|
||||
) -> int|None:
|
||||
'''
|
||||
Add a `pg.TextItem` annotation to the target view.
|
||||
|
||||
anchor: (x, y) where (0,0) is upper-left, (1,1) is lower-right
|
||||
font_size: pixel size for font, defaults to `_font.font.pixelSize()`
|
||||
|
||||
'''
|
||||
ipc: MsgStream = self._get_ipc(fqme)
|
||||
with trio.fail_after(3):
|
||||
await ipc.send({
|
||||
'fqme': fqme,
|
||||
'cmd': 'TextItem',
|
||||
'timeframe': timeframe,
|
||||
'kwargs': {
|
||||
'text': text,
|
||||
'x': float(x),
|
||||
'y': float(y),
|
||||
'color': color,
|
||||
'anchor': tuple(anchor),
|
||||
'font_size': font_size,
|
||||
'time': time, # for server-side index lookup
|
||||
},
|
||||
})
|
||||
aid: int|dict = await ipc.receive()
|
||||
match aid:
|
||||
case {'error': str(msg)}:
|
||||
log.error(msg)
|
||||
return None
|
||||
self._ipcs[aid] = ipc
|
||||
if not from_acm:
|
||||
self._annot_stack.push_async_callback(
|
||||
partial(
|
||||
self.remove,
|
||||
aid,
|
||||
)
|
||||
)
|
||||
return aid
|
||||
|
||||
|
||||
@acm
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class DpiAwareFont:
|
|||
) -> None:
|
||||
|
||||
self._font_size_calc_key: str = _font_size_key
|
||||
self._font_size: int | None = None
|
||||
self._font_size: int|None = None
|
||||
|
||||
# Read preferred font size from main config file if it exists
|
||||
conf, path = config.load('conf', touch_if_dne=True)
|
||||
|
|
@ -107,7 +107,22 @@ class DpiAwareFont:
|
|||
|
||||
@property
|
||||
def px_size(self) -> int:
|
||||
return self._qfont.pixelSize()
|
||||
size: int = self._qfont.pixelSize()
|
||||
|
||||
# XXX, when no Qt app has been spawned this will always be
|
||||
# invalid..
|
||||
# SO, just return any conf.toml value.
|
||||
if size == -1:
|
||||
if (conf_size := self._font_size) is None:
|
||||
raise ValueError(
|
||||
f'No valid `{type(_font).__name__}.px_size` set?\n'
|
||||
f'\n'
|
||||
f'-> `ui.font_size` is NOT set in `conf.toml`\n'
|
||||
f'-> no Qt app is active ??\n'
|
||||
)
|
||||
return conf_size
|
||||
|
||||
return size
|
||||
|
||||
def configure_to_dpi(self, screen: QtGui.QScreen | None = None):
|
||||
'''
|
||||
|
|
@ -221,6 +236,20 @@ def _config_fonts_to_screen() -> None:
|
|||
_font_small.configure_to_dpi()
|
||||
|
||||
|
||||
def get_fonts() -> tuple[
|
||||
DpiAwareFont,
|
||||
DpiAwareFont,
|
||||
]:
|
||||
'''
|
||||
Get the singleton font pair (of instances) from which all other
|
||||
UI/UX should be "scaled around".
|
||||
|
||||
See `DpiAwareFont` for (internal) deats.
|
||||
|
||||
'''
|
||||
return _font, _font_small
|
||||
|
||||
|
||||
# TODO: re-compute font size when main widget switches screens?
|
||||
# https://forum.qt.io/topic/54136/how-do-i-get-the-qscreen-my-widget-is-on-qapplication-desktop-screen-returns-a-qwidget-and-qobject_cast-qscreen-returns-null/3
|
||||
|
||||
|
|
@ -308,6 +337,7 @@ def hcolor(name: str) -> str:
|
|||
'cool_green': '#33b864',
|
||||
'dull_green': '#74a662',
|
||||
'hedge_green': '#518360',
|
||||
'lilypad_green': '#839c84',
|
||||
|
||||
# orders and alerts
|
||||
'alert_yellow': '#e2d083',
|
||||
|
|
@ -335,6 +365,7 @@ def hcolor(name: str) -> str:
|
|||
'sell_red': '#b6003f',
|
||||
# 'sell_red': '#d00048',
|
||||
'sell_red_light': '#f85462',
|
||||
'wine': '#69212d',
|
||||
|
||||
# 'sell_red': '#f85462',
|
||||
# 'sell_red_light': '#ff4d5c',
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ class MultiStatus:
|
|||
|
||||
self,
|
||||
msg: str,
|
||||
final_msg: str | None = None,
|
||||
final_msg: str|None = None,
|
||||
clear_on_next: bool = False,
|
||||
group_key: Union[bool, str] | None = False,
|
||||
group_key: Union[bool, str]|None = False,
|
||||
|
||||
) -> Union[Callable[..., None], str]:
|
||||
'''
|
||||
|
|
@ -175,11 +175,11 @@ class MainWindow(QMainWindow):
|
|||
self.setWindowTitle(self.title)
|
||||
|
||||
# set by runtime after `trio` is engaged.
|
||||
self.godwidget: GodWidget | None = None
|
||||
self.godwidget: GodWidget|None = None
|
||||
|
||||
self._status_bar: QStatusBar = None
|
||||
self._status_label: QLabel = None
|
||||
self._size: tuple[int, int] | None = None
|
||||
self._size: tuple[int, int]|None = None
|
||||
|
||||
@property
|
||||
def mode_label(self) -> QLabel:
|
||||
|
|
@ -202,7 +202,7 @@ class MainWindow(QMainWindow):
|
|||
label.setMargin(2)
|
||||
label.setAlignment(
|
||||
QtCore.Qt.AlignVCenter
|
||||
| QtCore.Qt.AlignRight
|
||||
|QtCore.Qt.AlignRight
|
||||
)
|
||||
self.statusBar().addPermanentWidget(label)
|
||||
label.show()
|
||||
|
|
@ -288,7 +288,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
def configure_to_desktop(
|
||||
self,
|
||||
size: tuple[int, int] | None = None,
|
||||
size: tuple[int, int]|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ dependencies = [
|
|||
"trio-typing>=0.10.0",
|
||||
"numba>=0.61.0",
|
||||
"pyvnc",
|
||||
"exchange-calendars>=4.13.1",
|
||||
]
|
||||
# ------ dependencies ------
|
||||
# NOTE, by default we ship only a "headless" deps set bc
|
||||
|
|
@ -116,7 +117,6 @@ uis = [
|
|||
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'},
|
||||
|
|
@ -134,6 +134,10 @@ repl = [
|
|||
"prompt-toolkit ==3.0.40",
|
||||
"pyperclip>=1.9.0",
|
||||
|
||||
# for @claude's `snippets/claude_debug_helper.py` it uses to do
|
||||
# "offline" debug/crash REPL-in alongside a dev.
|
||||
"pexpect>=4.9.0",
|
||||
|
||||
# ?TODO, new stuff to consider..
|
||||
# "visidata" # console numerics
|
||||
# "xxh" # for remote `xonsh`-ing
|
||||
|
|
@ -191,10 +195,15 @@ pyqtgraph = { git = "https://github.com/pikers/pyqtgraph.git" }
|
|||
tomlkit = { git = "https://github.com/pikers/tomlkit.git", branch ="piker_pin" }
|
||||
pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
||||
|
||||
# to get fancy next-cmd/suggestion feats prior to 0.22.2 B)
|
||||
# https://github.com/xonsh/xonsh/pull/6037
|
||||
# https://github.com/xonsh/xonsh/pull/6048
|
||||
xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
||||
|
||||
# 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 = "main" }
|
||||
# ------ goodboy ------
|
||||
# hackin dev-envs, usually there's something new he's hackin in..
|
||||
# tractor = { path = "../tractor", editable = true }
|
||||
tractor = { path = "../tractor", editable = true }
|
||||
|
|
|
|||
191
uv.lock
191
uv.lock
|
|
@ -2,8 +2,12 @@ version = 1
|
|||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version < '3.14'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||
"python_full_version < '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -416,6 +420,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exchange-calendars"
|
||||
version = "4.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "korean-lunar-calendar" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pyluach" },
|
||||
{ name = "toolz" },
|
||||
{ name = "tzdata" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/fd/1bda66b3c2fefbf54b8cf765c9d8001b12654b5a897a21b0c6c9f55de5e3/exchange_calendars-4.13.1.tar.gz", hash = "sha256:42a4c7296da1f71b9625c668c9b3359cf5de4a2ffca28842b230e062bb4961ba", size = 4119843, upload-time = "2026-02-05T00:15:03.947Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b7/fffe7d5a6da6be10b43be96640f31d4191e746de66b046cc1a6ea5fc4f26/exchange_calendars-4.13.1-py3-none-any.whl", hash = "sha256:cf39d2128a4da3ac253283f91ab63d79930a68196a3aac811091a4e38b6cbe49", size = 211538, upload-time = "2026-02-05T00:15:05.694Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
|
|
@ -659,6 +680,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/42/d3/c3db0b92a0ff39c3e08f168cd382c24bf021d4a96fc89b47a3e55294f883/keysymdef-1.2.0-py2.py3-none-any.whl", hash = "sha256:19a5c2263a861f3ff884a1f58e2b4f7efa319ffc9d11f9ba8e20129babc31a9e", size = 20146, upload-time = "2023-02-25T00:22:36.318Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "korean-lunar-calendar"
|
||||
version = "0.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/93/a0bd2bd53ab19330e83ecc5652b7774ae86fd2fee19bc05ad220cf9db08b/korean_lunar_calendar-0.3.1.tar.gz", hash = "sha256:eb2c485124a061016926bdea6d89efdf9b9fdbf16db55895b6cf1e5bec17b857", size = 9877, upload-time = "2022-09-16T10:53:25.713Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/96/30f3fe51b336bb6da4714f4fdad7bbdce8f13af79af2eb75e22908f3f9f4/korean_lunar_calendar-0.3.1-py3-none-any.whl", hash = "sha256:392757135c492c4f42a604e6038042953c35c6f449dda5f27e3f86a7f9c943e5", size = 9033, upload-time = "2022-09-16T10:53:23.771Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "llvmlite"
|
||||
version = "0.45.1"
|
||||
|
|
@ -953,6 +983,58 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdbp"
|
||||
version = "1.8.2"
|
||||
|
|
@ -1000,6 +1082,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/6e/23/e98758924d1b3aac11a626268eabf7f3cf177e7837c28d47bf84c64532d0/pendulum-3.1.0-py3-none-any.whl", hash = "sha256:f9178c2a8e291758ade1e8dd6371b1d26d08371b4c7730a6e9a3ef8b16ebae0f", size = 111799, upload-time = "2025-04-19T14:02:34.739Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pexpect"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ptyprocess" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "piker"
|
||||
version = "0.1.0a0.dev0"
|
||||
|
|
@ -1011,6 +1105,7 @@ dependencies = [
|
|||
{ name = "colorama" },
|
||||
{ name = "colorlog" },
|
||||
{ name = "cryptofeed" },
|
||||
{ name = "exchange-calendars" },
|
||||
{ name = "httpx" },
|
||||
{ name = "ib-insync" },
|
||||
{ name = "msgspec" },
|
||||
|
|
@ -1047,6 +1142,7 @@ dev = [
|
|||
{ name = "greenback" },
|
||||
{ name = "i3ipc" },
|
||||
{ name = "pdbp" },
|
||||
{ name = "pexpect" },
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "pyperclip" },
|
||||
{ name = "pyqt6" },
|
||||
|
|
@ -1062,6 +1158,7 @@ lint = [
|
|||
repl = [
|
||||
{ name = "greenback" },
|
||||
{ name = "pdbp" },
|
||||
{ name = "pexpect" },
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "pyperclip" },
|
||||
{ name = "xonsh" },
|
||||
|
|
@ -1084,6 +1181,7 @@ requires-dist = [
|
|||
{ name = "colorama", specifier = ">=0.4.6,<0.5.0" },
|
||||
{ name = "colorlog", specifier = ">=6.7.0,<7.0.0" },
|
||||
{ name = "cryptofeed", specifier = ">=2.4.0,<3.0.0" },
|
||||
{ name = "exchange-calendars", specifier = ">=4.13.1" },
|
||||
{ name = "httpx", specifier = ">=0.27.0,<0.28.0" },
|
||||
{ name = "ib-insync", specifier = ">=0.9.86,<0.10.0" },
|
||||
{ name = "msgspec", specifier = ">=0.19.0,<0.20" },
|
||||
|
|
@ -1099,7 +1197,7 @@ requires-dist = [
|
|||
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
||||
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
||||
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
||||
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=piker_pin" },
|
||||
{ name = "tractor", editable = "../tractor" },
|
||||
{ name = "trio", specifier = ">=0.27" },
|
||||
{ name = "trio-typing", specifier = ">=0.10.0" },
|
||||
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
||||
|
|
@ -1116,6 +1214,7 @@ dev = [
|
|||
{ name = "greenback", specifier = ">=1.1.1,<2.0.0" },
|
||||
{ name = "i3ipc", specifier = ">=2.2.1" },
|
||||
{ name = "pdbp", specifier = ">=1.8.2,<2.0.0" },
|
||||
{ name = "pexpect", specifier = ">=4.9.0" },
|
||||
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "pyqt6", specifier = ">=6.7.0,<7.0.0" },
|
||||
|
|
@ -1123,15 +1222,16 @@ dev = [
|
|||
{ name = "pytest" },
|
||||
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
|
||||
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
|
||||
{ name = "xonsh" },
|
||||
{ name = "xonsh", git = "https://github.com/xonsh/xonsh.git?branch=main" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
||||
repl = [
|
||||
{ name = "greenback", specifier = ">=1.1.1,<2.0.0" },
|
||||
{ name = "pdbp", specifier = ">=1.8.2,<2.0.0" },
|
||||
{ name = "pexpect", specifier = ">=4.9.0" },
|
||||
{ name = "prompt-toolkit", specifier = "==3.0.40" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "xonsh" },
|
||||
{ name = "xonsh", git = "https://github.com/xonsh/xonsh.git?branch=main" },
|
||||
]
|
||||
testing = [{ name = "pytest" }]
|
||||
uis = [
|
||||
|
|
@ -1297,6 +1397,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptyprocess"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyarrow"
|
||||
version = "22.0.0"
|
||||
|
|
@ -1421,6 +1530,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyluach"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/11/42568c1568a75f8803c59f26d29af01a0890352b7a8e03d41ecda8bfb5dd/pyluach-2.3.0.tar.gz", hash = "sha256:ec6e30669d1df50c9ca160486da44a8195bb4c7a5d3d533990d0c5b03accd281", size = 26910, upload-time = "2025-09-09T20:24:39.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/c8/f96208ade3ca4c23b372497d0788bcf0f2e0ff4310e5ee693366bc33fdf0/pyluach-2.3.0-py3-none-any.whl", hash = "sha256:4497b731aef59508b079dbf5f00bc5bf4329ac45090a6cd37b5a83756f0e69ab", size = 25914, upload-time = "2025-09-09T20:24:37.831Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyperclip"
|
||||
version = "1.11.0"
|
||||
|
|
@ -1840,10 +1958,19 @@ name = "tomlkit"
|
|||
version = "0.11.8"
|
||||
source = { git = "https://github.com/pikers/tomlkit.git?branch=piker_pin#8e0239a766e96739da700cd87cc00b48dbe7451f" }
|
||||
|
||||
[[package]]
|
||||
name = "toolz"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tractor"
|
||||
version = "0.1.0a6.dev0"
|
||||
source = { git = "https://github.com/goodboy/tractor.git?branch=piker_pin#e232d9dd06f41b8dca997f0647f2083d27cc34f2" }
|
||||
source = { editable = "../tractor" }
|
||||
dependencies = [
|
||||
{ name = "bidict" },
|
||||
{ name = "cffi" },
|
||||
|
|
@ -1856,6 +1983,48 @@ dependencies = [
|
|||
{ name = "wrapt" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "bidict", specifier = ">=0.23.1" },
|
||||
{ name = "cffi", specifier = ">=1.17.1" },
|
||||
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||
{ name = "trio", specifier = ">0.27" },
|
||||
{ name = "wrapt", specifier = ">=1.16.0,<2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||
{ name = "xonsh", specifier = ">=0.19.2" },
|
||||
]
|
||||
devx = [
|
||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
||||
repl = [
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "xonsh", specifier = ">=0.19.2" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tricycle"
|
||||
version = "0.4.1"
|
||||
|
|
@ -2095,14 +2264,8 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "xonsh"
|
||||
version = "0.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/56/af/7e2ba3885da44cbe03c7ff46f90ea917ba10d91dc74d68604001ea28055f/xonsh-0.20.0.tar.gz", hash = "sha256:d44a50ee9f288ff96bd0456f0a38988ef6d4985637140ea793beeef5ec5d2d38", size = 811907, upload-time = "2025-11-24T07:50:50.847Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/db/1c5c057c0b2a89b8919477726558685720ae0849ea1a98a3803e93550824/xonsh-0.20.0-py311-none-any.whl", hash = "sha256:65d27ba31d558f79010d6c652751449fd3ed4df1f1eda78040a6427fa0a0f03e", size = 646312, upload-time = "2025-11-24T07:50:49.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/a2/d6f7534f31489a4b8b54bd2a2496248f86f7c21a6a6ce9bfdcdd389fe4e7/xonsh-0.20.0-py312-none-any.whl", hash = "sha256:3148900e67b9c2796bef6f2eda003b0a64d4c6f50a0db23324f786d9e1af9353", size = 646323, upload-time = "2025-11-24T07:50:43.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/48/bcb1e4d329c3d522bc29b066b0b6ee86938ec392376a29c36fac0ad1c586/xonsh-0.20.0-py313-none-any.whl", hash = "sha256:c83daaf6eb2960180fc5a507459dbdf6c0d6d63e1733c43f4e43db77255c7278", size = 646830, upload-time = "2025-11-24T07:50:45.078Z" },
|
||||
]
|
||||
version = "0.22.1"
|
||||
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#336658ff0919f8d7bb96d581136d37d470a8fe99" }
|
||||
|
||||
[[package]]
|
||||
name = "yapic-json"
|
||||
|
|
|
|||
Loading…
Reference in New Issue