Add `is_expired()` and harden `.ib.venues` helpers
Add an expiry-date predicate and guard venue session lookups against expired contracts and empty session lists in `.ib.venues`; use in `api.py` to skip gap detection for expired tracts. Deats, - add `is_expired()` predicate using `pendulum.parse()` on `realExpirationDate`. - `sesh_times()`: raise `ValueError` if contract is expired or has no session intervals (instead of `StopIteration` from `next(iter(...))`). - `is_venue_closure()`: handle `None` return from `sesh_times()` with guard + `breakpoint()`. Also in `api.py`, - import and call `is_expired()` from `.venues`. - gate gap-detection on `not _is_expired`. - default `timeZoneId` to `'EST'` when IB returns empty/`None`. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-coderepair_tests
parent
08d159e652
commit
c04cc0e87f
|
|
@ -95,6 +95,7 @@ from .symbols import (
|
||||||
)
|
)
|
||||||
from ...log import get_logger
|
from ...log import get_logger
|
||||||
from .venues import (
|
from .venues import (
|
||||||
|
is_expired,
|
||||||
is_venue_open,
|
is_venue_open,
|
||||||
sesh_times,
|
sesh_times,
|
||||||
is_venue_closure,
|
is_venue_closure,
|
||||||
|
|
@ -496,7 +497,7 @@ class Client:
|
||||||
await self.ib.reqContractDetailsAsync(contract)
|
await self.ib.reqContractDetailsAsync(contract)
|
||||||
)[0]
|
)[0]
|
||||||
# convert to makt-native tz
|
# convert to makt-native tz
|
||||||
tz: str = details.timeZoneId
|
tz: str = details.timeZoneId or 'EST'
|
||||||
end_dt = end_dt.in_tz(tz)
|
end_dt = end_dt.in_tz(tz)
|
||||||
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
||||||
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
||||||
|
|
@ -508,10 +509,18 @@ class Client:
|
||||||
_open_now: bool = is_venue_open(
|
_open_now: bool = is_venue_open(
|
||||||
con_deats=details,
|
con_deats=details,
|
||||||
)
|
)
|
||||||
|
_is_expired: bool = is_expired(
|
||||||
|
con_deats=details,
|
||||||
|
)
|
||||||
|
|
||||||
# XXX, do gap detections.
|
# XXX, do gap detections.
|
||||||
has_closure_gap: bool = False
|
has_closure_gap: bool = False
|
||||||
if (
|
if (
|
||||||
|
# XXX, expired tracts can't be introspected
|
||||||
|
# for open/closure intervals due to ib's chitty
|
||||||
|
# details seemingly..
|
||||||
|
not _is_expired
|
||||||
|
and
|
||||||
last_dt.add(seconds=sample_period_s)
|
last_dt.add(seconds=sample_period_s)
|
||||||
<
|
<
|
||||||
end_dt
|
end_dt
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ from typing import (
|
||||||
|
|
||||||
import exchange_calendars as xcals
|
import exchange_calendars as xcals
|
||||||
from pendulum import (
|
from pendulum import (
|
||||||
|
parse,
|
||||||
now,
|
now,
|
||||||
Duration,
|
Duration,
|
||||||
Interval,
|
Interval,
|
||||||
|
|
@ -56,6 +57,17 @@ if TYPE_CHECKING:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_expired(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Predicate
|
||||||
|
|
||||||
|
'''
|
||||||
|
expiry_dt: datetime = parse(con_deats.realExpirationDate)
|
||||||
|
return expiry_dt.date() >= now().date()
|
||||||
|
|
||||||
|
|
||||||
def has_weekend(
|
def has_weekend(
|
||||||
period: Interval,
|
period: Interval,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
@ -170,7 +182,22 @@ def sesh_times(
|
||||||
get the (day-agnostic) times for the start/end.
|
get the (day-agnostic) times for the start/end.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
earliest_sesh: Interval = next(iter_sessions(con_deats))
|
# ?TODO, lookup the next front contract instead?
|
||||||
|
if is_expired(con_deats):
|
||||||
|
raise ValueError(
|
||||||
|
f'Contract is already expired!\n'
|
||||||
|
f'Choose an active alt contract instead.\n'
|
||||||
|
f'con_deats: {con_deats!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
maybe_sessions: list[Interval] = list(iter_sessions(con_deats))
|
||||||
|
if not maybe_sessions:
|
||||||
|
raise ValueError(
|
||||||
|
f'Contract has no trading-session info?\n'
|
||||||
|
f'con_deats: {con_deats!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
earliest_sesh: Interval = maybe_sessions[0]
|
||||||
return (
|
return (
|
||||||
earliest_sesh.start.time(),
|
earliest_sesh.start.time(),
|
||||||
earliest_sesh.end.time(),
|
earliest_sesh.end.time(),
|
||||||
|
|
@ -211,7 +238,13 @@ def is_venue_closure(
|
||||||
'''
|
'''
|
||||||
open: Time
|
open: Time
|
||||||
close: Time
|
close: Time
|
||||||
open, close = sesh_times(con_deats)
|
maybe_oc: tuple|None = sesh_times(con_deats)
|
||||||
|
if maybe_oc is None:
|
||||||
|
# XXX, should never get here.
|
||||||
|
breakpoint()
|
||||||
|
return False
|
||||||
|
|
||||||
|
open, close = maybe_oc
|
||||||
|
|
||||||
# ensure times are in mkt-native timezone
|
# ensure times are in mkt-native timezone
|
||||||
tz: str = con_deats.timeZoneId
|
tz: str = con_deats.timeZoneId
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue