Compare commits

..

1 Commits

Author SHA1 Message Date
Gud Boi a15a4ec8cd Capture `cons` in `Client.get_fute()`
That is to be able to (eventually) introspect "ambiguous" contract sets
once we move to `ib_async` and its `returnAll: bool` now offered by
`IB.qualifyContractsAsync()`,

https://github.com/ib-api-reloaded/ib_async/blob/main/ib_async/ib.py#L2115

Also, tweak some type type annots to multline style in sibling mods.
2026-01-26 11:38:29 -05:00
23 changed files with 418 additions and 1570 deletions

View File

@ -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,15 +275,9 @@ 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 (
_time_step := (inow - times[-1])
>
timeframe * 2
):
if (inow - times[-1]) > 60:
await tractor.pause()
start_dt = from_timestamp(times[0])
@ -297,7 +291,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():
@ -374,7 +368,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?
@ -433,7 +427,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

View File

@ -250,9 +250,7 @@ async def vnc_click_hack(
'connection': 'r'
}[reset_type]
with tractor.devx.open_crash_handler(
ignore={TimeoutError,},
):
with tractor.devx.open_crash_handler():
client = await AsyncVNCClient.connect(
VNCConfig(
host=host,
@ -326,20 +324,14 @@ 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()
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
orig_win_id = focussed.window
try:
for name, con in matches:
print(f'Resetting data feed for {name}')
@ -387,3 +379,99 @@ 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
# )

View File

@ -50,11 +50,10 @@ 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 (
@ -92,15 +91,10 @@ from .symbols import (
_exch_skip_list,
_futes_venues,
)
from ...log import get_logger
from .venues import (
is_venue_open,
sesh_times,
is_venue_closure,
)
log = get_logger(
name=__name__,
from ._util import (
log,
# only for the ib_sync internal logging
get_logger,
)
_bar_load_dtype: list[tuple[str, type]] = [
@ -186,7 +180,7 @@ class NonShittyIB(IB):
# override `ib_insync` internal loggers so we can see wtf
# it's doing..
self._logger = get_logger(
name=__name__,
'ib_insync.ib',
)
self._createEvents()
@ -194,7 +188,7 @@ class NonShittyIB(IB):
self.wrapper = NonShittyWrapper(self)
self.client = ib_client.Client(self.wrapper)
self.client._logger = get_logger(
name='ib_insync.client',
'ib_insync.client',
)
# self.errorEvent += self._onError
@ -266,16 +260,6 @@ 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.
@ -349,11 +333,9 @@ class Client:
self,
fqme: 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,
# 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 = "",
# ohlc sample period in seconds
sample_period_s: int = 1,
@ -364,17 +346,9 @@ class Client:
**kwargs,
) -> tuple[
BarDataList,
np.ndarray,
Duration,
]:
) -> tuple[BarDataList, np.ndarray, Duration]:
'''
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.
Retreive OHLCV bars for a fqme over a range to the present.
'''
# See API docs here:
@ -389,19 +363,13 @@ class Client:
dt_duration: Duration = (
duration
or
default_dt_duration
or default_dt_duration
)
# TODO: maybe remove all this?
global _enters
if end_dt is None:
end_dt: str = ''
else:
est_end_dt = end_dt.in_tz('EST')
if est_end_dt != end_dt:
breakpoint()
if not end_dt:
end_dt = ''
_enters += 1
@ -470,116 +438,58 @@ 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 end_dt:
if (
# XXX XXX XXX
# => WHY DID WE EVEN NEED THIS ORIGINALLY!? <=
# XXX XXX XXX
False
and end_dt
):
nparr: np.ndarray = bars_to_np(bars)
times: np.ndarray = nparr['time']
first: float = times[0]
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'
)
tdiff: float = times[-1] - first
if (
start_dt is None
and (
tdiff
<
dt_duration.in_seconds()
)
and
not has_closure_gap
# len(bars) * sample_period_s) < dt_duration.in_seconds()
tdiff < dt_duration.in_seconds()
# and False
):
log.error(
end_dt: DateTime = from_timestamp(first)
log.warning(
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'
)
# 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,
(
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)
@ -875,16 +785,9 @@ class Client:
# crypto$
elif exch == 'PAXOS': # btc.paxos
con = Crypto(
symbol=symbol.upper(),
currency='USD',
exchange='PAXOS',
symbol=symbol,
currency=currency,
)
# 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:

View File

@ -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,8 +203,7 @@ 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
@ -220,10 +219,8 @@ 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'
@ -265,43 +262,12 @@ async def open_history_client(
vlm = bars_array['volume']
vlm[vlm < 0] = 0
# 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,
)
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,
@ -424,13 +390,14 @@ _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: datetime|None = None,
start_dt: datetime|None = None,
end_dt: str = '',
start_dt: str | None = '',
# TODO: make this more dynamic based on measured frame rx latency?
# how long before we trigger a feed reset (seconds)
@ -484,9 +451,6 @@ 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,
@ -737,7 +701,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:
'''
@ -759,12 +723,7 @@ async def _setup_quote_stream(
# XXX since this is an `asyncio.Task`, we must use
# tractor.pause_from_sync()
(
_account_name,
client,
) = get_preferred_data_client(
accts2clients,
)
caccount_name, client = get_preferred_data_client(accts2clients)
contract = (
contract
or
@ -1099,9 +1058,14 @@ async def stream_quotes(
)
# is venue active rn?
venue_is_open: bool = is_venue_open(
con_deats=details,
venue_is_open: bool = any(
is_current_time_in_range(
start_dt=sesh.start,
end_dt=sesh.end,
)
for sesh in details.tradingSessions()
)
init_msg = FeedInit(mkt_info=mkt)
# NOTE, tell sampler (via config) to skip vlm summing for dst
@ -1118,7 +1082,6 @@ 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:
@ -1171,14 +1134,15 @@ 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
@ -1202,9 +1166,6 @@ 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
@ -1252,12 +1213,55 @@ 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()
quote = normalize(ticker)
feed_is_live.set()
fqme: str = quote['fqme']
await send_chan.send({fqme: quote})

View File

@ -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,7 +488,8 @@ def con2fqme(
@async_lifo_cache()
async def get_mkt_info(
fqme: str,
proxy: MethodProxy|None = None,
proxy: MethodProxy | None = None,
) -> tuple[MktPair, ibis.ContractDetails]:
@ -553,7 +554,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..

View File

@ -1,312 +0,0 @@
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
(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
# )

View File

@ -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?
) -> set[int]:
) -> None:
get_console_log(tractor.current_actor().loglevel)
incr_was_started: bool = False
@ -362,12 +362,7 @@ 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
# and
# feed_is_live.is_set()
# ^TODO? pass it in instead?
):
if shms_by_period is not None:
from ._sharedmem import (
attach_shm_array,
_Token,
@ -381,17 +376,12 @@ 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:
@ -437,7 +427,7 @@ async def register_with_sampler(
async def spawn_samplerd(
loglevel: str|None = None,
loglevel: str | None = None,
**extra_tractor_kwargs
) -> bool:
@ -483,7 +473,7 @@ async def spawn_samplerd(
@acm
async def maybe_open_samplerd(
loglevel: str|None = None,
loglevel: str | None = None,
**pikerd_kwargs,
) -> tractor.Portal: # noqa
@ -508,11 +498,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,
@ -543,8 +533,6 @@ 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
@ -559,10 +547,10 @@ async def open_sample_stream(
'open_index_stream': open_index_stream,
'sub_for_broadcasts': sub_for_broadcasts,
},
) as (ctx, shm_periods)
) as (ctx, first)
):
if ensure_is_active:
assert len(shm_periods) > 1
assert len(first) > 1
async with (
ctx.open_stream(

View File

@ -520,12 +520,9 @@ def open_shm_array(
# "unlink" created shm on process teardown by
# pushing teardown calls onto actor context stack
stack = tractor.current_actor(
err_on_no_runtime=False,
).lifetime_stack
if stack:
stack.callback(shmarr.close)
stack.callback(shmarr.destroy)
stack = tractor.current_actor().lifetime_stack
stack.callback(shmarr.close)
stack.callback(shmarr.destroy)
return shmarr
@ -610,10 +607,7 @@ def attach_shm_array(
_known_tokens[key] = token
# "close" attached shm on actor teardown
if (actor := tractor.current_actor(
err_on_no_runtime=False,
)):
actor.lifetime_stack.callback(sha.close)
tractor.current_actor().lifetime_stack.callback(sha.close)
return sha

View File

@ -447,13 +447,7 @@ def ldshm(
)
# last chance manual overwrites in REPL
# await tractor.pause()
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"
)
assert aids
tf2aids[period_s] = aids
else:

View File

@ -275,18 +275,6 @@ 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.
@ -446,8 +434,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[
@ -499,8 +487,7 @@ 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]
@ -514,8 +501,8 @@ def iter_null_segs(
yield (
absi_start, absi_end, # abs indices
fi_start, fi_end, # relative "frame" indices
start_t, end_t, # epoch times
start_dt, end_dt, # dts
start_t, end_t,
start_dt, end_dt,
)

View File

@ -30,57 +30,11 @@ 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,
@ -88,25 +42,12 @@ 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]
@ -114,7 +55,6 @@ 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()
@ -181,34 +121,20 @@ 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]
# 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 + 1 - from_idx,
cls,
)
ro: tuple[float, float] = (
iend + from_idx,
iend + rect_gap + 1,
opn,
)
lc: tuple[float, float] = (
istart - rect_gap, # + 1 ,
cls,
)
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?
@ -223,80 +149,30 @@ 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|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
aid: int = await actl.add_rect(**rect_kwargs)
assert aid
aids[aid] = rect_kwargs
direction: str = (
'down' if down_gap
'down' if sgn == 1
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, # fallback if timestamp lookup fails
x=iend,
y=cls,
time=gap_time, # for server-side index lookup
color=color,
alpha=169,
alpha=160,
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(

View File

@ -49,7 +49,6 @@ from pendulum import (
Duration,
duration as mk_duration,
from_timestamp,
timezone,
)
import numpy as np
import polars as pl
@ -58,7 +57,9 @@ from piker.brokers import NoData
from piker.accounting import (
MktPair,
)
from piker.log import get_logger
from piker.data._util import (
log,
)
from ..data._sharedmem import (
maybe_open_shm_array,
ShmArray,
@ -96,9 +97,6 @@ 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
@ -244,18 +242,6 @@ 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
@ -403,9 +389,7 @@ async def start_backfill(
# based on the sample step size, maybe load a certain amount history
update_start_on_prepend: bool = False
if (
_until_was_none := (backfill_until_dt is None)
):
if backfill_until_dt is None:
# TODO: per-provider default history-durations?
# -[ ] inside the `open_history_client()` config allow
@ -439,8 +423,6 @@ 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'
@ -454,10 +436,9 @@ async def start_backfill(
next_end_dt,
) = await get_hist(
timeframe,
end_dt=(end_dt_param := last_start_dt),
end_dt=last_start_dt,
)
except NoData as nodata:
_nodata = nodata
except NoData as _daterr:
orig_last_start_dt: datetime = last_start_dt
gap_report: str = (
f'EMPTY FRAME for `end_dt: {last_start_dt}`?\n'
@ -525,32 +506,8 @@ async def start_backfill(
==
next_start_dt.timestamp()
)
assert (
(last_time := 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()
assert time[-1] == next_end_dt.timestamp()
expected_dur: Interval = (
last_start_dt.subtract(
@ -612,26 +569,6 @@ 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
@ -653,14 +590,6 @@ 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(
@ -687,7 +616,7 @@ async def start_backfill(
},
})
# XXX, can't push the entire frame? so
# can't push the entire frame? so
# push only the amount that can fit..
break
@ -745,16 +674,8 @@ async def start_backfill(
deduped,
diff,
) = dedupe(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,
)
# if diff:
# sort_diff(df)
else:
# finally filled gap
@ -1321,7 +1242,6 @@ 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]
@ -1470,26 +1390,12 @@ async def manage_history(
1: rt_shm,
60: hist_shm,
}
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,
shms_by_period={
1.: rt_shm.token,
60.: hist_shm.token,
},
# NOTE: we want to only open a stream for doing
# broadcasts on backfill operations, not receive the

View File

@ -27,15 +27,15 @@ import trio
from piker.ui.qt import (
QEvent,
)
from . import _chart
from . import _event
from . import _search
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 . 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 ..log import get_logger
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: _chart.GodWidget,
# implicit required argument provided by ``qtractor_run()``
main_widget: GodWidget,
syms: list[str],
brokers: dict[str, ModuleType],
@ -87,9 +87,6 @@ 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()
@ -204,6 +201,6 @@ def _main(
brokermods,
piker_loglevel,
),
main_widget_type=_chart.GodWidget,
main_widget_type=GodWidget,
tractor_kwargs=tractor_kwargs,
)

View File

@ -82,25 +82,6 @@ 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
@ -123,7 +104,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)
@ -142,9 +123,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
@ -388,9 +369,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,
@ -406,13 +387,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
@ -423,7 +404,7 @@ class ChartnPane(QFrame):
self._sidepane = sidepane
@property
def sidepane(self) -> FieldsForm|SearchWidget:
def sidepane(self) -> FieldsForm | SearchWidget:
return self._sidepane
@ -469,7 +450,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
@ -505,7 +486,7 @@ class LinkedSplits(QWidget):
def set_split_sizes(
self,
prop: float|None = None,
prop: float | None = None,
) -> None:
'''
@ -586,8 +567,8 @@ class LinkedSplits(QWidget):
# style?
self.chart.setFrameStyle(
QFrame.Shape.StyledPanel
|QFrame.Shadow.Plain
QFrame.Shape.StyledPanel |
QFrame.Shadow.Plain
)
return self.chart
@ -599,11 +580,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,
@ -706,7 +687,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.
@ -819,7 +800,7 @@ class LinkedSplits(QWidget):
def resize_sidepanes(
self,
from_linked: LinkedSplits|None = None,
from_linked: LinkedSplits | None = None,
) -> None:
'''
@ -893,7 +874,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,
@ -908,7 +889,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)
@ -942,7 +923,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
@ -953,7 +934,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
@ -1046,7 +1027,7 @@ class ChartPlotWidget(pg.PlotWidget):
def increment_view(
self,
datums: int = 1,
vb: ChartView|None = None,
vb: ChartView | None = None,
) -> None:
'''
@ -1077,8 +1058,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 = {},
@ -1166,14 +1147,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,
@ -1271,7 +1252,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
@ -1305,7 +1286,7 @@ class ChartPlotWidget(pg.PlotWidget):
shm: ShmArray,
flume: Flume,
array_key: str|None = None,
array_key: str | None = None,
**draw_curve_kwargs,
) -> Viz:

View File

@ -27,6 +27,7 @@ import pyqtgraph as pg
from piker.ui.qt import (
QtWidgets,
QGraphicsItem,
Qt,
QLineF,
QRectF,

View File

@ -94,11 +94,6 @@ 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:
'''
@ -114,15 +109,6 @@ 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)
@ -131,11 +117,10 @@ class ArrowEditor(Struct):
arrow = pg.ArrowItem(
angle=angle,
baseAngle=0,
headLen=headLen,
headWidth=headWidth,
tailLen=tailLen,
tailWidth=tailWidth,
pxMode=pxMode,
headLen=size,
headWidth=size/2,
tailLen=None,
pxMode=True,
# coloring
pen=pen,
brush=brush,
@ -169,10 +154,7 @@ class ArrowEditor(Struct):
f'{arrow!r}\n'
)
for linked in self.godw.iter_linked():
if not (chart := linked.chart):
continue
chart.plotItem.removeItem(arrow)
linked.chart.plotItem.removeItem(arrow)
try:
arrows.remove(arrow)
except ValueError:

View File

@ -91,10 +91,6 @@ 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()
@ -174,7 +170,7 @@ def run_qtractor(
# hook into app focus change events
app.focusChanged.connect(window.on_focus_change)
instance: GodWidget = main_widget_type()
instance = main_widget_type()
instance.window = window
# override tractor's defaults

View File

@ -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,11 +87,7 @@ def update_fsp_chart(
# guard against unreadable case
if not last_row:
log.warning(
f'Read-race on shm array,\n'
f'graphics_name: {graphics_name!r}\n'
f'shm.token: {shm.token}\n'
)
log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}')
return
# update graphics
@ -207,6 +203,7 @@ async def open_fsp_actor_cluster(
async def run_fsp_ui(
linkedsplits: LinkedSplits,
flume: Flume,
started: trio.Event,
@ -474,7 +471,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):
@ -626,10 +623,8 @@ 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,
@ -639,12 +634,12 @@ async def open_vlm_displays(
) -> None:
'''
Vlm (volume) subchart displays.
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

View File

@ -49,7 +49,6 @@ 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 (
@ -58,7 +57,6 @@ from ._editors import (
)
from ._chart import ChartPlotWidget
from ._dataviz import Viz
from ._style import hcolor
log = get_logger(__name__)
@ -95,7 +93,7 @@ _annots: AnnotsTable = {}
_editors: EditorsTable = {}
def rm_annot(
annot: ArrowEditor|SelectRect|pg.TextItem
annot: ArrowEditor|SelectRect
) -> bool:
global _editors
match annot:
@ -116,12 +114,6 @@ def rm_annot(
annot.delete()
return True
case pg.TextItem():
scene = annot.scene()
if scene:
scene.removeItem(annot)
return True
return False
@ -149,76 +141,12 @@ async def serve_rc_annots(
'kwargs': dict(kwargs),
}:
ds: DisplayState = _dss[fqme]
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
chart: ChartPlotWidget = {
60: ds.hist_chart,
1: ds.chart,
}[timeframe]
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,
@ -237,12 +165,6 @@ 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]
@ -261,52 +183,18 @@ 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]
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
chart: ChartPlotWidget = {
60: ds.hist_chart,
1: ds.chart,
}[timeframe]
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':
@ -325,109 +213,15 @@ 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)
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)
# TODO, use `pg.TextItem` to put a humaized
# time label beside the arrows
case {
'cmd': 'remove',
@ -468,38 +262,6 @@ 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'
@ -588,47 +350,36 @@ class AnnotCtl(Struct):
from_acm: bool = False,
# NEW: optional timestamps for server-side index lookup
start_time: float|None = None,
end_time: float|None = None,
) -> int|None:
) -> int:
'''
Add a `SelectRect` annotation to the target view, return
the instances `id(obj)` from the remote UI actor.
'''
ipc: MsgStream = self._get_ipc(fqme)
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,
)
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,
)
return aid
)
return aid
async def remove(
self,
@ -690,116 +441,41 @@ 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,
# NEW: optional timestamp for server-side index lookup
time: float|None = None,
) -> int|None:
) -> int:
'''
Add a `SelectRect` annotation to the target view, return
the instances `id(obj)` from the remote UI actor.
'''
ipc: MsgStream = self._get_ipc(fqme)
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,
)
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,
)
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
)
return aid
@acm

View File

@ -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,22 +107,7 @@ class DpiAwareFont:
@property
def px_size(self) -> int:
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
return self._qfont.pixelSize()
def configure_to_dpi(self, screen: QtGui.QScreen | None = None):
'''
@ -236,20 +221,6 @@ 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
@ -337,7 +308,6 @@ def hcolor(name: str) -> str:
'cool_green': '#33b864',
'dull_green': '#74a662',
'hedge_green': '#518360',
'lilypad_green': '#839c84',
# orders and alerts
'alert_yellow': '#e2d083',
@ -365,7 +335,6 @@ def hcolor(name: str) -> str:
'sell_red': '#b6003f',
# 'sell_red': '#d00048',
'sell_red_light': '#f85462',
'wine': '#69212d',
# 'sell_red': '#f85462',
# 'sell_red_light': '#ff4d5c',

View File

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

View File

@ -75,7 +75,6 @@ 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
@ -117,6 +116,7 @@ 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,10 +134,6 @@ 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
@ -195,15 +191,10 @@ 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
View File

@ -2,12 +2,8 @@ version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"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'",
"python_full_version >= '3.14'",
"python_full_version < '3.14'",
]
[[package]]
@ -420,23 +416,6 @@ 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"
@ -680,15 +659,6 @@ 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"
@ -983,58 +953,6 @@ 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"
@ -1082,18 +1000,6 @@ 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"
@ -1105,7 +1011,6 @@ dependencies = [
{ name = "colorama" },
{ name = "colorlog" },
{ name = "cryptofeed" },
{ name = "exchange-calendars" },
{ name = "httpx" },
{ name = "ib-insync" },
{ name = "msgspec" },
@ -1142,7 +1047,6 @@ dev = [
{ name = "greenback" },
{ name = "i3ipc" },
{ name = "pdbp" },
{ name = "pexpect" },
{ name = "prompt-toolkit" },
{ name = "pyperclip" },
{ name = "pyqt6" },
@ -1158,7 +1062,6 @@ lint = [
repl = [
{ name = "greenback" },
{ name = "pdbp" },
{ name = "pexpect" },
{ name = "prompt-toolkit" },
{ name = "pyperclip" },
{ name = "xonsh" },
@ -1181,7 +1084,6 @@ 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" },
@ -1197,7 +1099,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", editable = "../tractor" },
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=piker_pin" },
{ name = "trio", specifier = ">=0.27" },
{ name = "trio-typing", specifier = ">=0.10.0" },
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
@ -1214,7 +1116,6 @@ 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" },
@ -1222,16 +1123,15 @@ dev = [
{ name = "pytest" },
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
{ name = "xonsh", git = "https://github.com/xonsh/xonsh.git?branch=main" },
{ name = "xonsh" },
]
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", git = "https://github.com/xonsh/xonsh.git?branch=main" },
{ name = "xonsh" },
]
testing = [{ name = "pytest" }]
uis = [
@ -1397,15 +1297,6 @@ 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"
@ -1530,15 +1421,6 @@ 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"
@ -1958,19 +1840,10 @@ 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 = { editable = "../tractor" }
source = { git = "https://github.com/goodboy/tractor.git?branch=piker_pin#e232d9dd06f41b8dca997f0647f2083d27cc34f2" }
dependencies = [
{ name = "bidict" },
{ name = "cffi" },
@ -1983,48 +1856,6 @@ 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"
@ -2264,8 +2095,14 @@ wheels = [
[[package]]
name = "xonsh"
version = "0.22.1"
source = { git = "https://github.com/xonsh/xonsh.git?branch=main#336658ff0919f8d7bb96d581136d37d470a8fe99" }
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" },
]
[[package]]
name = "yapic-json"