ib-api-reloaded/ib_async

ContractDetails.tradingSessions throws exception if timeZoneId is empty

skister opened this issue · 3 comments

Steps to reproduce:
Find a contract with timeZoneId='', the following contract currently has that
ContractDetails(contract=Contract(secType='WAR', conId=528943602, symbol='NFYS', lastTradeDateOrContractMonth='20240715', strike=11.5, right='C', multiplier='1', exchange='SMART', primaryExchange='NYSE', currency='USD', localSymbol='NFYS WS', tradingClass='NFYS'), marketName='NFYS', minTick=0.0001, orderTypes='ACTIVETIM,AD,ADJUST,ALERT,ALGO,ALLOC,AVGCOST,BASKET,COND,CONDORDER,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,GAT,GTC,GTD,GTT,HID,ICE,IMB,IOC,LIT,LMT,LOC,MIT,MKT,MOC,MTL,NGCOMB,NONALGO,OCA,OPG,OPGREROUT,POSTONLY,PREOPGRTH,REL,RELPCTOFS,RPI,RTH,RTHIGNOPG,SCALE,SCALERST,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF', validExchanges='SMART,NYSE,ISE,CHX,ARCA,NASDAQ,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,PEARL,NYSENAT,LTSE,MEMX,PSX', priceMagnifier=1, underConId=527039910, longName='ENPHYS ACQUISITION CORP', contractMonth='202407', industry='Diversified', category='Holding Companies-Divers', subcategory='Specified Purpose Acquis', timeZoneId='', tradingHours='', liquidHours='', evRule='', evMultiplier=0, mdSizeMultiplier=1, aggGroup=13, underSymbol='NFYS', underSecType='STK', marketRuleIds='557,557,557,557,557,557,557,557,557,557,557,557,557,557,557,557,557', secIdList=[TagValue(tag='ISIN', value='KYG3167L1178')], realExpirationDate='20240715', lastTradeTime='', stockType='', minSize=1.0, sizeIncrement=1.0, suggestedSizeIncrement=100.0, cusip='', ratings='', descAppend='', bondType='', couponType='', callable=False, putable=False, coupon=0, convertible=False, maturity='', issueDate='', nextOptionDate='', nextOptionType='', nextOptionPartial=False, notes='')

Call ContractDetails.liquidSession() with this contract detail object

Result:
File "/usr/local/lib/python3.12/site-packages/ib_async/contract.py", line 582, in liquidSessions
return self._parseSessions(self.liquidHours)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ib_async/contract.py", line 585, in _parseSessions
tz = util.ZoneInfo(self.timeZoneId)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/zoneinfo/_tzpath.py", line 73, in find_tzfile
_validate_tzfile_path(key)
File "/usr/local/lib/python3.12/zoneinfo/_tzpath.py", line 97, in _validate_tzfile_path
raise ValueError(
ValueError: ZoneInfo keys must be normalized relative paths, got:

Expected result:
[]

Thanks for providing the exact contract details so we can try to investigate.

I can't seem to reproduce this though?

Your paste result has empty fields for tradingHours and liquidHours (tradingHours='', liquidHours='',) while it populated fine for me (and yours has empty timeZoneId='' as you noted).

I checked the contract and the returned details from reqContractDetailsAsync(contract) give me:

Details: ib_async.contract.ContractDetails(
    contract=ib_async.contract.Contract(
        secType='WAR',
        conId=528943602,
        symbol='NFYS',
        lastTradeDateOrContractMonth='20261006',
        strike=11.5,
        right='C',
        multiplier='1',
        exchange='SMART',
        primaryExchange='NYSE',
        currency='USD',
        localSymbol='NFYS WS',
        tradingClass='NFYS'
    ),
    marketName='NFYS',
    minTick=0.0001,
    orderTypes='ACTIVETIM,AD,ADJUST,ALERT,ALGO,ALLOC,AVGCOST,BASKET,COND,CONDORDER,'
        'DAY,DEACT,DEACTDIS,DEACTEOD,DIS,GAT,GTC,GTD,GTT,HID,ICE,IMB,IOC,LIT,'
        'LMT,LOC,MIT,MKT,MOC,MTL,NGCOMB,NONALGO,OCA,OPG,OPGREROUT,POSTONLY,'
        'PREOPGRTH,REL,RELPCTOFS,RPI,RTH,RTHIGNOPG,SCALE,SCALERST,SMARTSTG,'
        'SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,'
        'TRAILMIT,WHATIF',
    validExchanges='SMART,NYSE,ISE,CHX,ARCA,ISLAND,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,PEARL,'
        'NYSENAT,LTSE,MEMX,PSX',
    priceMagnifier=1,
    underConId=527039910,
    longName='ENPHYS ACQUISITION CORP',
    contractMonth='202610',
    industry='Diversified',
    category='Holding Companies-Divers',
    subcategory='Specified Purpose Acquis',
    timeZoneId='US/Eastern',
    tradingHours='20240721:CLOSED;20240722:0400-20240722:2000;20240723:0400-20240723:'
        '2000;20240724:0400-20240724:2000;20240725:0400-20240725:2000;'
        '20240726:0400-20240726:2000',
    liquidHours='20240721:CLOSED;20240722:0930-20240722:1600;20240723:0930-20240723:'
        '1600;20240724:0930-20240724:1600;20240725:0930-20240725:1600;'
        '20240726:0930-20240726:1600',
    aggGroup=13,
    underSymbol='NFYS',
    underSecType='STK',
    marketRuleIds='557,557,557,557,557,557,557,557,557,557,557,557,557,557,557,557,557',
    secIdList=[
        ib_async.contract.TagValue(tag='ISIN', value='KYG3167L1178')
    ],
    realExpirationDate='20261006',
    minSize=1.0,
    sizeIncrement=1.0,
    suggestedSizeIncrement=100.0
)

Then after calling details.liquidSessions() I get:

Trading Sessions: [
    ib_async.contract.TradingSession(
        start=datetime.datetime(
            year=2024,
            month=7,
            day=22,
            hour=4,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        ),
        end=datetime.datetime(
            year=2024,
            month=7,
            day=22,
            hour=20,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        )
    ),
    ib_async.contract.TradingSession(
        start=datetime.datetime(
            year=2024,
            month=7,
            day=23,
            hour=4,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        ),
        end=datetime.datetime(
            year=2024,
            month=7,
            day=23,
            hour=20,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        )
    ),
    ib_async.contract.TradingSession(
        start=datetime.datetime(
            year=2024,
            month=7,
            day=24,
            hour=4,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        ),
        end=datetime.datetime(
            year=2024,
            month=7,
            day=24,
            hour=20,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        )
    ),
    ib_async.contract.TradingSession(
        start=datetime.datetime(
            year=2024,
            month=7,
            day=25,
            hour=4,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        ),
        end=datetime.datetime(
            year=2024,
            month=7,
            day=25,
            hour=20,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        )
    ),
    ib_async.contract.TradingSession(
        start=datetime.datetime(
            year=2024,
            month=7,
            day=26,
            hour=4,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        ),
        end=datetime.datetime(
            year=2024,
            month=7,
            day=26,
            hour=20,
            tzinfo=zoneinfo.ZoneInfo(key='US/Eastern')
        )
    )
]

Perhaps we just need a crash check to return nothing if the underlying time details aren't available.

I think this was a rare instance where IB had incorrect data for the contract details and that caused empty TimeZoneId and hours fields.

When I hit the bug, IB had lastTradeDateOrContractMonth='20240715', so according to their data, it shouldn't be trading on July 16th, which is likely why the tradingHours and liquidHours were empty. In your post it looks like they have corrected the data to lastTradeDateOrContractMonth='20261006',

While this is likely a rare occurrence, I think it would be better to return an empty list instead of throwing an exception. I think returning [] if any of the time details are missing is the correct response.

Yeah, sounds about right. IBKR data does weird things especially around expirations or overnight/weekends (it's fun working on code that can often only be tested during live market hours!).

Simplest fix is just "don't crash" if we don't have the data, so that'll be done for this case going forward 🙃