2525 DepthMktDataDescription ,
2626 Dividends ,
2727 DOMLevel ,
28+ EfpData ,
2829 Execution ,
2930 FamilyCode ,
3031 Fill ,
101102 51 : "askYield" ,
102103 104 : "askYield" ,
103104 52 : "lastYield" ,
105+ 57 : "lastRthTrade" ,
106+ 78 : "creditmanMarkPrice" ,
107+ 79 : "creditmanSlowMarkPrice" ,
108+ 92 : "etfNavClose" ,
109+ 93 : "etfNavPriorClose" ,
110+ 94 : "etfNavBid" ,
111+ 95 : "etfNavAsk" ,
112+ 96 : "etfNavLast" ,
113+ 97 : "etfFrozenNavLast" ,
114+ 98 : "etfNavHigh" ,
115+ 99 : "etfNavLow" ,
116+ 101 : "estimatedIpoMidpoint" ,
117+ 102 : "finalIpoLast" ,
104118}
105119
106120
111125 64 : "volumeRate5Min" ,
112126 65 : "volumeRate10Min" ,
113127 21 : "avVolume" ,
128+ 22 : "openInterest" ,
114129 27 : "callOpenInterest" ,
115130 28 : "putOpenInterest" ,
116131 29 : "callVolume" ,
133148 55 : "tradeRate" ,
134149 56 : "volumeRate" ,
135150 58 : "rtHistVolatility" ,
151+ 60 : "bondFactorMultiplier" ,
152+ 90 : "delayedHalted" ,
136153}
137154
138155GREEKS_TICK_MAP : Final [TickDict ] = {
144161 82 : "lastGreeks" ,
145162 13 : "modelGreeks" ,
146163 83 : "modelGreeks" ,
164+ 53 : "custGreeks" ,
165+ }
166+
167+ EFP_TICK_MAP : Final [TickDict ] = {
168+ 38 : "bidEfp" ,
169+ 39 : "askEfp" ,
170+ 40 : "lastEfp" ,
171+ 41 : "openEfp" ,
172+ 42 : "highEfp" ,
173+ 43 : "lowEfp" ,
174+ 44 : "closeEfp" ,
175+ }
176+
177+ STRING_TICK_MAP : Final [TickDict ] = {
178+ 25 : "optionBidExch" ,
179+ 26 : "optionAskExch" ,
180+ 32 : "bidExchange" ,
181+ 33 : "askExchange" ,
182+ 84 : "lastExchange" ,
183+ 85 : "lastRegTime" ,
184+ 91 : "reutersMutualFunds" ,
185+ 100 : "socialMarketAnalytics" ,
186+ }
187+
188+ TIMESTAMP_TICK_MAP : Final [TickDict ] = {
189+ 45 : "lastTimestamp" ,
190+ 88 : "delayedLastTimestamp" ,
191+ }
192+
193+ RT_VOLUME_TICK_MAP : Final [TickDict ] = {
194+ 48 : "rtVolume" ,
195+ 77 : "rtTradeVolume" ,
147196}
148197
149198
@@ -418,6 +467,50 @@ def _setTimer(self, delay: float = 0):
418467 self .setTimeout (0 )
419468 self .ib .timeoutEvent .emit (diff )
420469
470+ # Helper methods for tick processing
471+
472+ def _processTimestampTick (self , ticker : Ticker , fieldName : str , value : str ):
473+ """Convert timestamp string to datetime and set on ticker field."""
474+ timestamp = int (value )
475+ # Only populate if timestamp isn't '0' (we don't want to report "last trade: 20,000 days ago")
476+ if timestamp :
477+ setattr (
478+ ticker ,
479+ fieldName ,
480+ datetime .fromtimestamp (timestamp , self .defaultTimezone ),
481+ )
482+
483+ def _processRtVolumeTick (
484+ self , ticker : Ticker , tickType : int , value : str
485+ ) -> tuple [float , float ] | None :
486+ """
487+ Parse RT Volume or RT Trade Volume tick.
488+
489+ Format: price;size;ms since epoch;total volume;VWAP;single trade
490+ Example: 701.28;1;1348075471534;67854;701.46918464;true
491+
492+ Returns (price, size) tuple if valid, None otherwise.
493+ """
494+ priceStr , sizeStr , rtTime , volume , vwap , _ = value .split (";" )
495+
496+ if volume :
497+ # Set volume field based on tick type
498+ volumeField = RT_VOLUME_TICK_MAP [tickType ]
499+ setattr (ticker , volumeField , float (volume ))
500+
501+ if vwap :
502+ ticker .vwap = float (vwap )
503+
504+ if rtTime :
505+ ticker .rtTime = datetime .fromtimestamp (
506+ int (rtTime ) / 1000 , self .defaultTimezone
507+ )
508+
509+ if priceStr == "" :
510+ return None
511+
512+ return (float (priceStr ), float (sizeStr ))
513+
421514 # wrapper methods
422515
423516 def connectAck (self ):
@@ -1107,20 +1200,12 @@ def tickString(self, reqId: int, tickType: int, value: str):
11071200 return
11081201
11091202 try :
1110- if tickType == 32 :
1111- ticker .bidExchange = value
1112- elif tickType == 33 :
1113- ticker .askExchange = value
1114- elif tickType == 84 :
1115- ticker .lastExchange = value
1116- elif tickType == 45 :
1117- timestamp = int (value )
1118-
1119- # only populate if timestamp isn't '0' (we don't want to report "last trade: 20,000 days ago")
1120- if timestamp :
1121- ticker .lastTimestamp = datetime .fromtimestamp (
1122- timestamp , self .defaultTimezone
1123- )
1203+ # Simple string assignments (O(1) dict lookup)
1204+ if tickType in STRING_TICK_MAP :
1205+ setattr (ticker , STRING_TICK_MAP [tickType ], value )
1206+ elif tickType in TIMESTAMP_TICK_MAP :
1207+ # Timestamp conversion (O(1) dict lookup)
1208+ self ._processTimestampTick (ticker , TIMESTAMP_TICK_MAP [tickType ], value )
11241209 elif tickType == 47 :
11251210 # https://web.archive.org/web/20200725010343/https://interactivebrokers.github.io/tws-api/fundamental_ratios_tags.html
11261211 d = dict (
@@ -1135,40 +1220,17 @@ def tickString(self, reqId: int, tickType: int, value: str):
11351220 d [k ] = float (v ) # type: ignore
11361221 d [k ] = int (v ) # type: ignore
11371222 ticker .fundamentalRatios = FundamentalRatios (** d )
1138- elif tickType in {48 , 77 }:
1139- # RT Volume or RT Trade Volume string format:
1140- # price;size;ms since epoch;total volume;VWAP;single trade
1141- # example:
1142- # 701.28;1;1348075471534;67854;701.46918464;true
1143- priceStr , sizeStr , rtTime , volume , vwap , _ = value .split (";" )
1144- if volume :
1145- if tickType == 48 :
1146- ticker .rtVolume = float (volume )
1147- elif tickType == 77 :
1148- ticker .rtTradeVolume = float (volume )
1149-
1150- if vwap :
1151- ticker .vwap = float (vwap )
1152-
1153- if rtTime :
1154- ticker .rtTime = datetime .fromtimestamp (
1155- int (rtTime ) / 1000 , self .defaultTimezone
1156- )
1157-
1158- if priceStr == "" :
1159- return
1160-
1161- price = float (priceStr )
1162- size = float (sizeStr )
1163-
1164- ticker .prevLast = ticker .last
1165- ticker .prevLastSize = ticker .lastSize
1166-
1167- ticker .last = price
1168- ticker .lastSize = size
1169-
1170- tick = TickData (self .lastTime , tickType , price , size )
1171- ticker .ticks .append (tick )
1223+ elif tickType in RT_VOLUME_TICK_MAP :
1224+ # RT Volume or RT Trade Volume (O(1) dict lookup + helper)
1225+ result = self ._processRtVolumeTick (ticker , tickType , value )
1226+ if result :
1227+ price , size = result
1228+ ticker .prevLast = ticker .last
1229+ ticker .prevLastSize = ticker .lastSize
1230+ ticker .last = price
1231+ ticker .lastSize = size
1232+ tick = TickData (self .lastTime , tickType , price , size )
1233+ ticker .ticks .append (tick )
11721234 elif tickType == 59 :
11731235 # Dividend tick:
11741236 # https://interactivebrokers.github.io/tws-api/tick_types.html#ib_dividends
@@ -1467,7 +1529,26 @@ def tickEFP(
14671529 dividendImpact : float ,
14681530 dividendsToLastTradeDate : float ,
14691531 ):
1470- pass
1532+ ticker = self .reqId2Ticker .get (reqId )
1533+ if not ticker :
1534+ return
1535+
1536+ # Create EFP data object with all available information
1537+ # Note: totalDividends parameter is actually the implied future price per IBKR docs
1538+ efpData = EfpData (
1539+ basisPoints = basisPoints ,
1540+ formattedBasisPoints = formattedBasisPoints ,
1541+ impliedFuture = totalDividends ,
1542+ holdDays = holdDays ,
1543+ futureLastTradeDate = futureLastTradeDate ,
1544+ dividendImpact = dividendImpact ,
1545+ dividendsToLastTradeDate = dividendsToLastTradeDate ,
1546+ )
1547+
1548+ # Store in appropriate field based on tick type (O(1) dict lookup)
1549+ if tickType in EFP_TICK_MAP :
1550+ setattr (ticker , EFP_TICK_MAP [tickType ], efpData )
1551+ self .pendingTickers .add (ticker )
14711552
14721553 def historicalSchedule (
14731554 self ,
0 commit comments