diff --git a/piker/brokers/ib/api.py b/piker/brokers/ib/api.py index 1cc5e4e0..1ba5eedf 100644 --- a/piker/brokers/ib/api.py +++ b/piker/brokers/ib/api.py @@ -95,6 +95,7 @@ from .symbols import ( ) from ...log import get_logger from .venues import ( + is_expired, is_venue_open, sesh_times, is_venue_closure, @@ -496,7 +497,7 @@ class Client: await self.ib.reqContractDetailsAsync(contract) )[0] # convert to makt-native tz - tz: str = details.timeZoneId + tz: str = details.timeZoneId or 'EST' 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) @@ -508,10 +509,18 @@ class Client: _open_now: bool = is_venue_open( con_deats=details, ) + _is_expired: bool = is_expired( + con_deats=details, + ) # XXX, do gap detections. has_closure_gap: bool = False 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) < end_dt diff --git a/piker/brokers/ib/venues.py b/piker/brokers/ib/venues.py index a24635bd..75981619 100644 --- a/piker/brokers/ib/venues.py +++ b/piker/brokers/ib/venues.py @@ -34,6 +34,7 @@ from typing import ( import exchange_calendars as xcals from pendulum import ( + parse, now, Duration, 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( period: Interval, ) -> bool: @@ -170,7 +182,22 @@ def sesh_times( 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 ( earliest_sesh.start.time(), earliest_sesh.end.time(), @@ -211,7 +238,13 @@ def is_venue_closure( ''' open: 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 tz: str = con_deats.timeZoneId