Merge pull request 'ib_venue_closures: gap detection for "legacy mkts"' (#71)
Reviewed-on: https://www.pikers.dev/pikers/piker/pulls/71main
commit
9a720f8e21
|
|
@ -237,8 +237,8 @@ async def open_history_client(
|
||||||
|
|
||||||
async def get_ohlc(
|
async def get_ohlc(
|
||||||
timeframe: float,
|
timeframe: float,
|
||||||
end_dt: datetime | None = None,
|
end_dt: datetime|None = None,
|
||||||
start_dt: datetime | None = None,
|
start_dt: datetime|None = None,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
np.ndarray,
|
np.ndarray,
|
||||||
|
|
@ -297,7 +297,7 @@ async def open_history_client(
|
||||||
async def get_mkt_info(
|
async def get_mkt_info(
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
||||||
) -> tuple[MktPair, Pair] | None:
|
) -> tuple[MktPair, Pair]|None:
|
||||||
|
|
||||||
# uppercase since kraken bs_mktid is always upper
|
# uppercase since kraken bs_mktid is always upper
|
||||||
if 'binance' not in fqme.lower():
|
if 'binance' not in fqme.lower():
|
||||||
|
|
@ -374,7 +374,7 @@ async def get_mkt_info(
|
||||||
if 'futes' in mkt_mode:
|
if 'futes' in mkt_mode:
|
||||||
assert isinstance(pair, FutesPair)
|
assert isinstance(pair, FutesPair)
|
||||||
|
|
||||||
dst: Asset | None = assets.get(pair.bs_dst_asset)
|
dst: Asset|None = assets.get(pair.bs_dst_asset)
|
||||||
if (
|
if (
|
||||||
not dst
|
not dst
|
||||||
# TODO: a known asset DNE list?
|
# TODO: a known asset DNE list?
|
||||||
|
|
@ -433,7 +433,7 @@ async def subscribe(
|
||||||
# might get ack from ws server, or maybe some
|
# might get ack from ws server, or maybe some
|
||||||
# other msg still in transit..
|
# other msg still in transit..
|
||||||
res = await ws.recv_msg()
|
res = await ws.recv_msg()
|
||||||
subid: str | None = res.get('id')
|
subid: str|None = res.get('id')
|
||||||
if subid:
|
if subid:
|
||||||
assert res['id'] == subid
|
assert res['id'] == subid
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,6 @@ def i3ipc_fin_wins_titled(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def i3ipc_xdotool_manual_click_hack() -> None:
|
def i3ipc_xdotool_manual_click_hack() -> None:
|
||||||
'''
|
'''
|
||||||
Do the data reset hack but expecting a local X-window using `xdotool`.
|
Do the data reset hack but expecting a local X-window using `xdotool`.
|
||||||
|
|
@ -388,99 +387,3 @@ def i3ipc_xdotool_manual_click_hack() -> None:
|
||||||
])
|
])
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
log.exception('xdotool timed out?')
|
log.exception('xdotool timed out?')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def is_current_time_in_range(
|
|
||||||
start_dt: datetime,
|
|
||||||
end_dt: datetime,
|
|
||||||
) -> bool:
|
|
||||||
'''
|
|
||||||
Check if current time is within the datetime range.
|
|
||||||
|
|
||||||
Use any/the-same timezone as provided by `start_dt.tzinfo` value
|
|
||||||
in the range.
|
|
||||||
|
|
||||||
'''
|
|
||||||
now: datetime = datetime.now(start_dt.tzinfo)
|
|
||||||
return start_dt <= now <= end_dt
|
|
||||||
|
|
||||||
|
|
||||||
# TODO, put this into `._util` and call it from here!
|
|
||||||
#
|
|
||||||
# NOTE, this was generated by @guille from a gpt5 prompt
|
|
||||||
# and was originally thot to be needed before learning about
|
|
||||||
# `ib_insync.contract.ContractDetails._parseSessions()` and
|
|
||||||
# it's downstream meths..
|
|
||||||
#
|
|
||||||
# This is still likely useful to keep for now to parse the
|
|
||||||
# `.tradingHours: str` value manually if we ever decide
|
|
||||||
# to move off `ib_async` and implement our own `trio`/`anyio`
|
|
||||||
# based version Bp
|
|
||||||
#
|
|
||||||
# >attempt to parse the retarted ib "time stampy thing" they
|
|
||||||
# >do for "venue hours" with this.. written by
|
|
||||||
# >gpt5-"thinking",
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
def parse_trading_hours(
|
|
||||||
spec: str,
|
|
||||||
tz: TzInfo|None = None
|
|
||||||
) -> dict[
|
|
||||||
date,
|
|
||||||
tuple[datetime, datetime]
|
|
||||||
]|None:
|
|
||||||
'''
|
|
||||||
Parse venue hours like:
|
|
||||||
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
|
|
||||||
|
|
||||||
Returns `dict[date] = (open_dt, close_dt)` or `None` if
|
|
||||||
closed.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if (
|
|
||||||
not isinstance(spec, str)
|
|
||||||
or
|
|
||||||
not spec
|
|
||||||
):
|
|
||||||
raise ValueError('spec must be a non-empty string')
|
|
||||||
|
|
||||||
out: dict[
|
|
||||||
date,
|
|
||||||
tuple[datetime, datetime]
|
|
||||||
]|None = {}
|
|
||||||
|
|
||||||
for part in (p.strip() for p in spec.split(';') if p.strip()):
|
|
||||||
if part.endswith(':CLOSED'):
|
|
||||||
day_s, _ = part.split(':', 1)
|
|
||||||
d = datetime.strptime(day_s, '%Y%m%d').date()
|
|
||||||
out[d] = None
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
start_s, end_s = part.split('-', 1)
|
|
||||||
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
|
|
||||||
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(f'invalid segment: {part}') from exc
|
|
||||||
|
|
||||||
if tz is not None:
|
|
||||||
start_dt = start_dt.replace(tzinfo=tz)
|
|
||||||
end_dt = end_dt.replace(tzinfo=tz)
|
|
||||||
|
|
||||||
out[start_dt.date()] = (start_dt, end_dt)
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
# ORIG desired usage,
|
|
||||||
#
|
|
||||||
# TODO, for non-drunk tomorrow,
|
|
||||||
# - call above fn and check that `output[today] is not None`
|
|
||||||
# trading_hrs: dict = parse_trading_hours(
|
|
||||||
# details.tradingHours
|
|
||||||
# )
|
|
||||||
# liq_hrs: dict = parse_trading_hours(
|
|
||||||
# details.liquidHours
|
|
||||||
# )
|
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,11 @@ import tractor
|
||||||
from tractor import to_asyncio
|
from tractor import to_asyncio
|
||||||
from tractor import trionics
|
from tractor import trionics
|
||||||
from pendulum import (
|
from pendulum import (
|
||||||
from_timestamp,
|
|
||||||
DateTime,
|
DateTime,
|
||||||
Duration,
|
Duration,
|
||||||
duration as mk_duration,
|
duration as mk_duration,
|
||||||
|
from_timestamp,
|
||||||
|
Interval,
|
||||||
)
|
)
|
||||||
from eventkit import Event
|
from eventkit import Event
|
||||||
from ib_insync import (
|
from ib_insync import (
|
||||||
|
|
@ -91,10 +92,15 @@ from .symbols import (
|
||||||
_exch_skip_list,
|
_exch_skip_list,
|
||||||
_futes_venues,
|
_futes_venues,
|
||||||
)
|
)
|
||||||
from ._util import (
|
from ...log import get_logger
|
||||||
log,
|
from .venues import (
|
||||||
# only for the ib_sync internal logging
|
is_venue_open,
|
||||||
get_logger,
|
sesh_times,
|
||||||
|
is_venue_closure,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = get_logger(
|
||||||
|
name=__name__,
|
||||||
)
|
)
|
||||||
|
|
||||||
_bar_load_dtype: list[tuple[str, type]] = [
|
_bar_load_dtype: list[tuple[str, type]] = [
|
||||||
|
|
@ -180,7 +186,7 @@ class NonShittyIB(IB):
|
||||||
# override `ib_insync` internal loggers so we can see wtf
|
# override `ib_insync` internal loggers so we can see wtf
|
||||||
# it's doing..
|
# it's doing..
|
||||||
self._logger = get_logger(
|
self._logger = get_logger(
|
||||||
'ib_insync.ib',
|
name=__name__,
|
||||||
)
|
)
|
||||||
self._createEvents()
|
self._createEvents()
|
||||||
|
|
||||||
|
|
@ -188,7 +194,7 @@ class NonShittyIB(IB):
|
||||||
self.wrapper = NonShittyWrapper(self)
|
self.wrapper = NonShittyWrapper(self)
|
||||||
self.client = ib_client.Client(self.wrapper)
|
self.client = ib_client.Client(self.wrapper)
|
||||||
self.client._logger = get_logger(
|
self.client._logger = get_logger(
|
||||||
'ib_insync.client',
|
name='ib_insync.client',
|
||||||
)
|
)
|
||||||
|
|
||||||
# self.errorEvent += self._onError
|
# self.errorEvent += self._onError
|
||||||
|
|
@ -260,6 +266,16 @@ def remove_handler_on_err(
|
||||||
event.disconnect(handler)
|
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:
|
class Client:
|
||||||
'''
|
'''
|
||||||
IB wrapped for our broker backend API.
|
IB wrapped for our broker backend API.
|
||||||
|
|
@ -333,9 +349,11 @@ class Client:
|
||||||
self,
|
self,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
|
||||||
# EST in ISO 8601 format is required... below is EPOCH
|
# EST in ISO 8601 format is required..
|
||||||
start_dt: datetime|str = "1970-01-01T00:00:00.000000-05:00",
|
# XXX, see `ib_async.ib.IB.reqHistoricalDataAsync()`
|
||||||
end_dt: datetime|str = "",
|
# below is EPOCH.
|
||||||
|
start_dt: datetime|None = None, # _iso8601_epoch_in_est,
|
||||||
|
end_dt: datetime|None = None,
|
||||||
|
|
||||||
# ohlc sample period in seconds
|
# ohlc sample period in seconds
|
||||||
sample_period_s: int = 1,
|
sample_period_s: int = 1,
|
||||||
|
|
@ -346,9 +364,17 @@ class Client:
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> tuple[BarDataList, np.ndarray, Duration]:
|
) -> tuple[
|
||||||
|
BarDataList,
|
||||||
|
np.ndarray,
|
||||||
|
Duration,
|
||||||
|
]:
|
||||||
'''
|
'''
|
||||||
Retreive OHLCV bars for a fqme over a range to the present.
|
Retreive the `fqme`'s OHLCV-bars for the time-range "until `end_dt`".
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- IB's api doesn't support a `start_dt` (which is why default
|
||||||
|
is null) so we only use it for bar-frame duration checking.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# See API docs here:
|
# See API docs here:
|
||||||
|
|
@ -363,13 +389,19 @@ class Client:
|
||||||
|
|
||||||
dt_duration: Duration = (
|
dt_duration: Duration = (
|
||||||
duration
|
duration
|
||||||
or default_dt_duration
|
or
|
||||||
|
default_dt_duration
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: maybe remove all this?
|
# TODO: maybe remove all this?
|
||||||
global _enters
|
global _enters
|
||||||
if not end_dt:
|
if end_dt is None:
|
||||||
end_dt = ''
|
end_dt: str = ''
|
||||||
|
|
||||||
|
else:
|
||||||
|
est_end_dt = end_dt.in_tz('EST')
|
||||||
|
if est_end_dt != end_dt:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
_enters += 1
|
_enters += 1
|
||||||
|
|
||||||
|
|
@ -438,58 +470,116 @@ class Client:
|
||||||
+ query_info
|
+ 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?
|
# rewrite the method in the first case?
|
||||||
# right now there's no way to detect a timeout..
|
# right now there's no way to detect a timeout..
|
||||||
return [], np.empty(0), dt_duration
|
return [], np.empty(0), dt_duration
|
||||||
|
|
||||||
log.info(query_info)
|
log.info(query_info)
|
||||||
|
|
||||||
|
# ------ GAP-DETECTION ------
|
||||||
# NOTE XXX: ensure minimum duration in bars?
|
# NOTE XXX: ensure minimum duration in bars?
|
||||||
# => recursively call this method until we get at least as
|
# => recursively call this method until we get at least as
|
||||||
# many bars such that they sum in aggregate to the the
|
# many bars such that they sum in aggregate to the the
|
||||||
# desired total time (duration) at most.
|
# desired total time (duration) at most.
|
||||||
# - if you query over a gap and get no data
|
# - if you query over a gap and get no data
|
||||||
# that may short circuit the history
|
# that may short circuit the history
|
||||||
if (
|
if end_dt:
|
||||||
# XXX XXX XXX
|
|
||||||
# => WHY DID WE EVEN NEED THIS ORIGINALLY!? <=
|
|
||||||
# XXX XXX XXX
|
|
||||||
False
|
|
||||||
and end_dt
|
|
||||||
):
|
|
||||||
nparr: np.ndarray = bars_to_np(bars)
|
nparr: np.ndarray = bars_to_np(bars)
|
||||||
times: np.ndarray = nparr['time']
|
times: np.ndarray = nparr['time']
|
||||||
first: float = times[0]
|
first: float = times[0]
|
||||||
tdiff: float = times[-1] - first
|
last: float = times[-1]
|
||||||
|
# frame_dur: float = times[-1] - first
|
||||||
|
|
||||||
|
details: ContractDetails = (
|
||||||
|
await self.ib.reqContractDetailsAsync(contract)
|
||||||
|
)[0]
|
||||||
|
# convert to makt-native tz
|
||||||
|
tz: str = details.timeZoneId
|
||||||
|
end_dt = end_dt.in_tz(tz)
|
||||||
|
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
||||||
|
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
||||||
|
tdiff: int = (
|
||||||
|
last_dt
|
||||||
|
-
|
||||||
|
first_dt
|
||||||
|
).in_seconds() + sample_period_s
|
||||||
|
_open_now: bool = is_venue_open(
|
||||||
|
con_deats=details,
|
||||||
|
)
|
||||||
|
|
||||||
|
# XXX, do gap detections.
|
||||||
|
has_closure_gap: bool = False
|
||||||
|
if (
|
||||||
|
last_dt.add(seconds=sample_period_s)
|
||||||
|
<
|
||||||
|
end_dt
|
||||||
|
):
|
||||||
|
open_time, close_time = sesh_times(details)
|
||||||
|
# XXX, always calc gap in mkt-venue-local timezone
|
||||||
|
gap: Interval = end_dt - last_dt
|
||||||
|
if not (
|
||||||
|
has_closure_gap := is_venue_closure(
|
||||||
|
gap=gap,
|
||||||
|
con_deats=details,
|
||||||
|
time_step_s=sample_period_s,
|
||||||
|
)):
|
||||||
|
log.warning(
|
||||||
|
f'Invalid non-closure gap for {fqme!r} ?!?\n'
|
||||||
|
f'is-open-now: {_open_now}\n'
|
||||||
|
f'\n'
|
||||||
|
f'{gap}\n'
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
f'Detected NON venue-closure GAP ??\n'
|
||||||
|
f'{gap}\n'
|
||||||
|
)
|
||||||
|
breakpoint()
|
||||||
|
else:
|
||||||
|
assert has_closure_gap
|
||||||
|
log.debug(
|
||||||
|
f'Detected venue closure gap (weekend),\n'
|
||||||
|
f'{gap}\n'
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
# len(bars) * sample_period_s) < dt_duration.in_seconds()
|
start_dt is None
|
||||||
tdiff < dt_duration.in_seconds()
|
and (
|
||||||
# and False
|
tdiff
|
||||||
|
<
|
||||||
|
dt_duration.in_seconds()
|
||||||
|
)
|
||||||
|
and
|
||||||
|
not has_closure_gap
|
||||||
):
|
):
|
||||||
end_dt: DateTime = from_timestamp(first)
|
log.error(
|
||||||
log.warning(
|
|
||||||
f'Frame result was shorter then {dt_duration}!?\n'
|
f'Frame result was shorter then {dt_duration}!?\n'
|
||||||
'Recursing for more bars:\n'
|
|
||||||
f'end_dt: {end_dt}\n'
|
f'end_dt: {end_dt}\n'
|
||||||
f'dt_duration: {dt_duration}\n'
|
f'dt_duration: {dt_duration}\n'
|
||||||
|
# f'\n'
|
||||||
|
# f'Recursing for more bars:\n'
|
||||||
)
|
)
|
||||||
(
|
# XXX, debug!
|
||||||
r_bars,
|
breakpoint()
|
||||||
r_arr,
|
# XXX ? TODO? recursively try to re-request?
|
||||||
r_duration,
|
# => i think *NO* right?
|
||||||
) = await self.bars(
|
#
|
||||||
fqme,
|
# (
|
||||||
start_dt=start_dt,
|
# r_bars,
|
||||||
end_dt=end_dt,
|
# r_arr,
|
||||||
sample_period_s=sample_period_s,
|
# 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
|
# # TODO: make a table for Duration to
|
||||||
# the ib str values in order to use this?
|
# # the ib str values in order to use this?
|
||||||
# duration=duration,
|
# # duration=duration,
|
||||||
)
|
# )
|
||||||
r_bars.extend(bars)
|
# r_bars.extend(bars)
|
||||||
bars = r_bars
|
# bars = r_bars
|
||||||
|
|
||||||
nparr: np.ndarray = bars_to_np(bars)
|
nparr: np.ndarray = bars_to_np(bars)
|
||||||
|
|
||||||
|
|
@ -784,9 +874,16 @@ class Client:
|
||||||
# crypto$
|
# crypto$
|
||||||
elif exch == 'PAXOS': # btc.paxos
|
elif exch == 'PAXOS': # btc.paxos
|
||||||
con = Crypto(
|
con = Crypto(
|
||||||
symbol=symbol,
|
symbol=symbol.upper(),
|
||||||
currency=currency,
|
currency='USD',
|
||||||
|
exchange='PAXOS',
|
||||||
)
|
)
|
||||||
|
# XXX, on `ib_insync` when first tried this,
|
||||||
|
# > Error 10299, reqId 141: Expected what to show is
|
||||||
|
# > AGGTRADES, please use that instead of TRADES.,
|
||||||
|
# > contract: Crypto(conId=479624278, symbol='BTC',
|
||||||
|
# > exchange='PAXOS', currency='USD',
|
||||||
|
# > localSymbol='BTC.USD', tradingClass='BTC')
|
||||||
|
|
||||||
# stonks
|
# stonks
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,9 @@ from .api import (
|
||||||
Contract,
|
Contract,
|
||||||
RequestError,
|
RequestError,
|
||||||
)
|
)
|
||||||
|
from .venues import is_venue_open
|
||||||
from ._util import (
|
from ._util import (
|
||||||
data_reset_hack,
|
data_reset_hack,
|
||||||
is_current_time_in_range,
|
|
||||||
)
|
)
|
||||||
from .symbols import get_mkt_info
|
from .symbols import get_mkt_info
|
||||||
|
|
||||||
|
|
@ -206,7 +206,8 @@ async def open_history_client(
|
||||||
latency = time.time() - query_start
|
latency = time.time() - query_start
|
||||||
if (
|
if (
|
||||||
not timedout
|
not timedout
|
||||||
# and latency <= max_timeout
|
# and
|
||||||
|
# latency <= max_timeout
|
||||||
):
|
):
|
||||||
count += 1
|
count += 1
|
||||||
mean += latency / count
|
mean += latency / count
|
||||||
|
|
@ -222,8 +223,10 @@ async def open_history_client(
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
end_dt
|
end_dt
|
||||||
and head_dt
|
and
|
||||||
and end_dt <= head_dt
|
head_dt
|
||||||
|
and
|
||||||
|
end_dt <= head_dt
|
||||||
):
|
):
|
||||||
raise DataUnavailable(
|
raise DataUnavailable(
|
||||||
f'First timestamp is {head_dt}\n'
|
f'First timestamp is {head_dt}\n'
|
||||||
|
|
@ -281,7 +284,7 @@ async def open_history_client(
|
||||||
start_dt
|
start_dt
|
||||||
):
|
):
|
||||||
# TODO! rm this once we're more confident it never hits!
|
# TODO! rm this once we're more confident it never hits!
|
||||||
breakpoint()
|
# breakpoint()
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f'OHLC-bars array start is gt `start_dt` limit !!\n'
|
f'OHLC-bars array start is gt `start_dt` limit !!\n'
|
||||||
f'start_dt: {start_dt}\n'
|
f'start_dt: {start_dt}\n'
|
||||||
|
|
@ -301,7 +304,7 @@ async def open_history_client(
|
||||||
# TODO: it seems like we can do async queries for ohlc
|
# TODO: it seems like we can do async queries for ohlc
|
||||||
# but getting the order right still isn't working and I'm not
|
# but getting the order right still isn't working and I'm not
|
||||||
# quite sure why.. needs some tinkering and probably
|
# 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?
|
# we have to do the batch queries on the `asyncio` side?
|
||||||
yield (
|
yield (
|
||||||
get_hist,
|
get_hist,
|
||||||
|
|
@ -424,14 +427,13 @@ _failed_resets: int = 0
|
||||||
|
|
||||||
|
|
||||||
async def get_bars(
|
async def get_bars(
|
||||||
|
|
||||||
proxy: MethodProxy,
|
proxy: MethodProxy,
|
||||||
fqme: str,
|
fqme: str,
|
||||||
timeframe: int,
|
timeframe: int,
|
||||||
|
|
||||||
# blank to start which tells ib to look up the latest datum
|
# blank to start which tells ib to look up the latest datum
|
||||||
end_dt: str = '',
|
end_dt: datetime|None = None,
|
||||||
start_dt: str|None = '',
|
start_dt: datetime|None = None,
|
||||||
|
|
||||||
# TODO: make this more dynamic based on measured frame rx latency?
|
# TODO: make this more dynamic based on measured frame rx latency?
|
||||||
# how long before we trigger a feed reset (seconds)
|
# how long before we trigger a feed reset (seconds)
|
||||||
|
|
@ -485,7 +487,8 @@ async def get_bars(
|
||||||
dt_duration,
|
dt_duration,
|
||||||
) = await proxy.bars(
|
) = await proxy.bars(
|
||||||
fqme=fqme,
|
fqme=fqme,
|
||||||
# XXX TODO! lol we're not using this..
|
# XXX TODO! LOL we're not using this and IB dun
|
||||||
|
# support it anyway..
|
||||||
# start_dt=start_dt,
|
# start_dt=start_dt,
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
sample_period_s=timeframe,
|
sample_period_s=timeframe,
|
||||||
|
|
@ -737,7 +740,7 @@ async def _setup_quote_stream(
|
||||||
# '294', # Trade rate / minute
|
# '294', # Trade rate / minute
|
||||||
# '295', # Vlm rate / minute
|
# '295', # Vlm rate / minute
|
||||||
),
|
),
|
||||||
contract: Contract | None = None,
|
contract: Contract|None = None,
|
||||||
|
|
||||||
) -> trio.abc.ReceiveChannel:
|
) -> trio.abc.ReceiveChannel:
|
||||||
'''
|
'''
|
||||||
|
|
@ -759,7 +762,12 @@ async def _setup_quote_stream(
|
||||||
# XXX since this is an `asyncio.Task`, we must use
|
# XXX since this is an `asyncio.Task`, we must use
|
||||||
# tractor.pause_from_sync()
|
# tractor.pause_from_sync()
|
||||||
|
|
||||||
caccount_name, client = get_preferred_data_client(accts2clients)
|
(
|
||||||
|
_account_name,
|
||||||
|
client,
|
||||||
|
) = get_preferred_data_client(
|
||||||
|
accts2clients,
|
||||||
|
)
|
||||||
contract = (
|
contract = (
|
||||||
contract
|
contract
|
||||||
or
|
or
|
||||||
|
|
@ -1094,14 +1102,9 @@ async def stream_quotes(
|
||||||
)
|
)
|
||||||
|
|
||||||
# is venue active rn?
|
# is venue active rn?
|
||||||
venue_is_open: bool = any(
|
venue_is_open: bool = is_venue_open(
|
||||||
is_current_time_in_range(
|
con_deats=details,
|
||||||
start_dt=sesh.start,
|
|
||||||
end_dt=sesh.end,
|
|
||||||
)
|
|
||||||
for sesh in details.tradingSessions()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
init_msg = FeedInit(mkt_info=mkt)
|
init_msg = FeedInit(mkt_info=mkt)
|
||||||
|
|
||||||
# NOTE, tell sampler (via config) to skip vlm summing for dst
|
# NOTE, tell sampler (via config) to skip vlm summing for dst
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ _adhoc_fiat_set = set((
|
||||||
|
|
||||||
# manually discovered tick discrepancies,
|
# manually discovered tick discrepancies,
|
||||||
# onl god knows how or why they'd cuck these up..
|
# 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')},
|
'vtgn.nasdaq': {'price_tick': Decimal('0.01')},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,8 +489,7 @@ def con2fqme(
|
||||||
@async_lifo_cache()
|
@async_lifo_cache()
|
||||||
async def get_mkt_info(
|
async def get_mkt_info(
|
||||||
fqme: str,
|
fqme: str,
|
||||||
|
proxy: MethodProxy|None = None,
|
||||||
proxy: MethodProxy | None = None,
|
|
||||||
|
|
||||||
) -> tuple[MktPair, ibis.ContractDetails]:
|
) -> tuple[MktPair, ibis.ContractDetails]:
|
||||||
|
|
||||||
|
|
@ -551,7 +550,7 @@ async def get_mkt_info(
|
||||||
size_tick: Decimal = Decimal(
|
size_tick: Decimal = Decimal(
|
||||||
str(details.minSize).rstrip('0')
|
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()
|
# NOTE: this is duplicate from the .broker.norm_trade_records()
|
||||||
# routine, we should factor all this parsing somewhere..
|
# routine, we should factor all this parsing somewhere..
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
# piker: trading gear for hackers
|
||||||
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
(Multi-)venue mgmt helpers.
|
||||||
|
|
||||||
|
IB generally supports all "legacy" trading venues, those mostly owned
|
||||||
|
by ICE and friends.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
from datetime import ( # noqa
|
||||||
|
datetime,
|
||||||
|
date,
|
||||||
|
tzinfo as TzInfo,
|
||||||
|
)
|
||||||
|
from typing import (
|
||||||
|
Iterator,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
|
import exchange_calendars as xcals
|
||||||
|
from pendulum import (
|
||||||
|
now,
|
||||||
|
Duration,
|
||||||
|
Interval,
|
||||||
|
Time,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ib_insync import (
|
||||||
|
TradingSession,
|
||||||
|
ContractDetails,
|
||||||
|
)
|
||||||
|
from exchange_calendars.exchange_calendars import (
|
||||||
|
ExchangeCalendar,
|
||||||
|
)
|
||||||
|
from pandas import (
|
||||||
|
# DatetimeIndex,
|
||||||
|
TimeDelta,
|
||||||
|
Timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_weekend(
|
||||||
|
period: Interval,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Predicate to for a period being within
|
||||||
|
days 6->0 (sat->sun).
|
||||||
|
|
||||||
|
'''
|
||||||
|
has_weekend: bool = False
|
||||||
|
for dt in period:
|
||||||
|
if dt.day_of_week in [0, 6]: # 0=Sunday, 6=Saturday
|
||||||
|
has_weekend = True
|
||||||
|
break
|
||||||
|
|
||||||
|
return has_weekend
|
||||||
|
|
||||||
|
|
||||||
|
def has_holiday(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
period: Interval,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Using the `exchange_calendars` lib detect if a time-gap `period`
|
||||||
|
is contained in a known "cash hours" closure.
|
||||||
|
|
||||||
|
'''
|
||||||
|
tz: str = con_deats.timeZoneId
|
||||||
|
exch: str = con_deats.contract.primaryExchange
|
||||||
|
cal: ExchangeCalendar = xcals.get_calendar(exch)
|
||||||
|
end: datetime = period.end
|
||||||
|
# _start: datetime = period.start
|
||||||
|
# ?TODO, can rm ya?
|
||||||
|
# => not that useful?
|
||||||
|
# dti: DatetimeIndex = cal.sessions_in_range(
|
||||||
|
# _start.date(),
|
||||||
|
# end.date(),
|
||||||
|
# )
|
||||||
|
prev_close: Timestamp = cal.previous_close(
|
||||||
|
end.date()
|
||||||
|
).tz_convert(tz)
|
||||||
|
prev_open: Timestamp = cal.previous_open(
|
||||||
|
end.date()
|
||||||
|
).tz_convert(tz)
|
||||||
|
# now do relative from prev_ values ^
|
||||||
|
# to get the next open which should match
|
||||||
|
# "contain" the end of the gap.
|
||||||
|
next_open: Timestamp = cal.next_open(
|
||||||
|
prev_open,
|
||||||
|
).tz_convert(tz)
|
||||||
|
next_open: Timestamp = cal.next_open(
|
||||||
|
prev_open,
|
||||||
|
).tz_convert(tz)
|
||||||
|
_next_close: Timestamp = cal.next_close(
|
||||||
|
prev_close
|
||||||
|
).tz_convert(tz)
|
||||||
|
cash_gap: TimeDelta = next_open - prev_close
|
||||||
|
is_holiday_gap = (
|
||||||
|
cash_gap
|
||||||
|
>
|
||||||
|
period
|
||||||
|
)
|
||||||
|
# XXX, debug
|
||||||
|
# breakpoint()
|
||||||
|
return is_holiday_gap
|
||||||
|
|
||||||
|
|
||||||
|
def is_current_time_in_range(
|
||||||
|
sesh: Interval,
|
||||||
|
when: datetime|None = None,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Check if current time is within the datetime range.
|
||||||
|
|
||||||
|
Use any/the-same timezone as provided by `start_dt.tzinfo` value
|
||||||
|
in the range.
|
||||||
|
|
||||||
|
'''
|
||||||
|
when: datetime = when or now()
|
||||||
|
return when in sesh
|
||||||
|
|
||||||
|
|
||||||
|
def iter_sessions(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
) -> Iterator[Interval]:
|
||||||
|
'''
|
||||||
|
Yield `pendulum.Interval`s for all
|
||||||
|
`ibas.ContractDetails.tradingSessions() -> TradingSession`s.
|
||||||
|
|
||||||
|
'''
|
||||||
|
sesh: TradingSession
|
||||||
|
for sesh in con_deats.tradingSessions():
|
||||||
|
yield Interval(*sesh)
|
||||||
|
|
||||||
|
|
||||||
|
def sesh_times(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
) -> tuple[Time, Time]:
|
||||||
|
'''
|
||||||
|
Based on the earliest trading session provided by the IB API,
|
||||||
|
get the (day-agnostic) times for the start/end.
|
||||||
|
|
||||||
|
'''
|
||||||
|
earliest_sesh: Interval = next(iter_sessions(con_deats))
|
||||||
|
return (
|
||||||
|
earliest_sesh.start.time(),
|
||||||
|
earliest_sesh.end.time(),
|
||||||
|
)
|
||||||
|
# ^?TODO, use `.diff()` to get point-in-time-agnostic period?
|
||||||
|
# https://pendulum.eustace.io/docs/#difference
|
||||||
|
|
||||||
|
|
||||||
|
def is_venue_open(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
when: datetime|Duration|None = None,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Check if market-venue is open during `when`, which defaults to
|
||||||
|
"now".
|
||||||
|
|
||||||
|
'''
|
||||||
|
sesh: Interval
|
||||||
|
for sesh in iter_sessions(con_deats):
|
||||||
|
if is_current_time_in_range(
|
||||||
|
sesh=sesh,
|
||||||
|
when=when,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_venue_closure(
|
||||||
|
gap: Interval,
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
time_step_s: int,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Check if a provided time-`gap` is just an (expected) trading
|
||||||
|
venue closure period.
|
||||||
|
|
||||||
|
'''
|
||||||
|
open: Time
|
||||||
|
close: Time
|
||||||
|
open, close = sesh_times(con_deats)
|
||||||
|
|
||||||
|
# ensure times are in mkt-native timezone
|
||||||
|
tz: str = con_deats.timeZoneId
|
||||||
|
start = gap.start.in_tz(tz)
|
||||||
|
start_t = start.time()
|
||||||
|
end = gap.end.in_tz(tz)
|
||||||
|
end_t = end.time()
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
start_t in (
|
||||||
|
close,
|
||||||
|
close.subtract(seconds=time_step_s)
|
||||||
|
)
|
||||||
|
and
|
||||||
|
end_t in (
|
||||||
|
open,
|
||||||
|
open.add(seconds=time_step_s),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or
|
||||||
|
has_weekend(gap)
|
||||||
|
or
|
||||||
|
has_holiday(
|
||||||
|
con_deats=con_deats,
|
||||||
|
period=gap,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# breakpoint()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# TODO, put this into `._util` and call it from here!
|
||||||
|
#
|
||||||
|
# NOTE, this was generated by @guille from a gpt5 prompt
|
||||||
|
# and was originally thot to be needed before learning about
|
||||||
|
# `ib_insync.contract.ContractDetails._parseSessions()` and
|
||||||
|
# it's downstream meths..
|
||||||
|
#
|
||||||
|
# This is still likely useful to keep for now to parse the
|
||||||
|
# `.tradingHours: str` value manually if we ever decide
|
||||||
|
# to move off `ib_async` and implement our own `trio`/`anyio`
|
||||||
|
# based version Bp
|
||||||
|
#
|
||||||
|
# >attempt to parse the retarted ib "time stampy thing" they
|
||||||
|
# >do for "venue hours" with this.. written by
|
||||||
|
# >gpt5-"thinking",
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def parse_trading_hours(
|
||||||
|
spec: str,
|
||||||
|
tz: TzInfo|None = None
|
||||||
|
) -> dict[
|
||||||
|
date,
|
||||||
|
tuple[datetime, datetime]
|
||||||
|
]|None:
|
||||||
|
'''
|
||||||
|
Parse venue hours like:
|
||||||
|
'YYYYMMDD:HHMM-YYYYMMDD:HHMM;YYYYMMDD:CLOSED;...'
|
||||||
|
|
||||||
|
Returns `dict[date] = (open_dt, close_dt)` or `None` if
|
||||||
|
closed.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if (
|
||||||
|
not isinstance(spec, str)
|
||||||
|
or
|
||||||
|
not spec
|
||||||
|
):
|
||||||
|
raise ValueError('spec must be a non-empty string')
|
||||||
|
|
||||||
|
out: dict[
|
||||||
|
date,
|
||||||
|
tuple[datetime, datetime]
|
||||||
|
]|None = {}
|
||||||
|
|
||||||
|
for part in (p.strip() for p in spec.split(';') if p.strip()):
|
||||||
|
if part.endswith(':CLOSED'):
|
||||||
|
day_s, _ = part.split(':', 1)
|
||||||
|
d = datetime.strptime(day_s, '%Y%m%d').date()
|
||||||
|
out[d] = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_s, end_s = part.split('-', 1)
|
||||||
|
start_dt = datetime.strptime(start_s, '%Y%m%d:%H%M')
|
||||||
|
end_dt = datetime.strptime(end_s, '%Y%m%d:%H%M')
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f'invalid segment: {part}') from exc
|
||||||
|
|
||||||
|
if tz is not None:
|
||||||
|
start_dt = start_dt.replace(tzinfo=tz)
|
||||||
|
end_dt = end_dt.replace(tzinfo=tz)
|
||||||
|
|
||||||
|
out[start_dt.date()] = (start_dt, end_dt)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ORIG desired usage,
|
||||||
|
#
|
||||||
|
# TODO, for non-drunk tomorrow,
|
||||||
|
# - call above fn and check that `output[today] is not None`
|
||||||
|
# trading_hrs: dict = parse_trading_hours(
|
||||||
|
# details.tradingHours
|
||||||
|
# )
|
||||||
|
# liq_hrs: dict = parse_trading_hours(
|
||||||
|
# details.liquidHours
|
||||||
|
# )
|
||||||
|
|
@ -75,6 +75,7 @@ dependencies = [
|
||||||
"trio-typing>=0.10.0",
|
"trio-typing>=0.10.0",
|
||||||
"numba>=0.61.0",
|
"numba>=0.61.0",
|
||||||
"pyvnc",
|
"pyvnc",
|
||||||
|
"exchange-calendars>=4.13.1",
|
||||||
]
|
]
|
||||||
# ------ dependencies ------
|
# ------ dependencies ------
|
||||||
# NOTE, by default we ship only a "headless" deps set bc
|
# NOTE, by default we ship only a "headless" deps set bc
|
||||||
|
|
|
||||||
106
uv.lock
106
uv.lock
|
|
@ -2,8 +2,12 @@ version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.14'",
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||||
"python_full_version < '3.14'",
|
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version < '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -416,6 +420,23 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
{ 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]]
|
[[package]]
|
||||||
name = "frozenlist"
|
name = "frozenlist"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|
@ -659,6 +680,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/d3/c3db0b92a0ff39c3e08f168cd382c24bf021d4a96fc89b47a3e55294f883/keysymdef-1.2.0-py2.py3-none-any.whl", hash = "sha256:19a5c2263a861f3ff884a1f58e2b4f7efa319ffc9d11f9ba8e20129babc31a9e", size = 20146, upload-time = "2023-02-25T00:22:36.318Z" },
|
{ 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]]
|
[[package]]
|
||||||
name = "llvmlite"
|
name = "llvmlite"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
|
|
@ -953,6 +983,58 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pdbp"
|
name = "pdbp"
|
||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
|
|
@ -1023,6 +1105,7 @@ dependencies = [
|
||||||
{ name = "colorama" },
|
{ name = "colorama" },
|
||||||
{ name = "colorlog" },
|
{ name = "colorlog" },
|
||||||
{ name = "cryptofeed" },
|
{ name = "cryptofeed" },
|
||||||
|
{ name = "exchange-calendars" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "ib-insync" },
|
{ name = "ib-insync" },
|
||||||
{ name = "msgspec" },
|
{ name = "msgspec" },
|
||||||
|
|
@ -1098,6 +1181,7 @@ requires-dist = [
|
||||||
{ name = "colorama", specifier = ">=0.4.6,<0.5.0" },
|
{ name = "colorama", specifier = ">=0.4.6,<0.5.0" },
|
||||||
{ name = "colorlog", specifier = ">=6.7.0,<7.0.0" },
|
{ name = "colorlog", specifier = ">=6.7.0,<7.0.0" },
|
||||||
{ name = "cryptofeed", specifier = ">=2.4.0,<3.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 = "httpx", specifier = ">=0.27.0,<0.28.0" },
|
||||||
{ name = "ib-insync", specifier = ">=0.9.86,<0.10.0" },
|
{ name = "ib-insync", specifier = ">=0.9.86,<0.10.0" },
|
||||||
{ name = "msgspec", specifier = ">=0.19.0,<0.20" },
|
{ name = "msgspec", specifier = ">=0.19.0,<0.20" },
|
||||||
|
|
@ -1446,6 +1530,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pyperclip"
|
name = "pyperclip"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|
@ -1865,6 +1958,15 @@ name = "tomlkit"
|
||||||
version = "0.11.8"
|
version = "0.11.8"
|
||||||
source = { git = "https://github.com/pikers/tomlkit.git?branch=piker_pin#8e0239a766e96739da700cd87cc00b48dbe7451f" }
|
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]]
|
[[package]]
|
||||||
name = "tractor"
|
name = "tractor"
|
||||||
version = "0.1.0a6.dev0"
|
version = "0.1.0a6.dev0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue