Skip to content

Commit f9c5e57

Browse files
committed
Implement missing tick types
Also refactors some handling to use the more modern O(1) setting patterns. Adds missing tick types present in the current IBKR ibapi client we hadn't updated internally in a while. Currently untested, but looks good so far. Some specific data types or data extraction formats may need to be adjusted when we test against live data. Fixes #182 Fixes #182
1 parent 2afdae4 commit f9c5e57

File tree

3 files changed

+208
-49
lines changed

3 files changed

+208
-49
lines changed

ib_async/objects.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,37 @@ class Fill(NamedTuple):
323323
time: datetime
324324

325325

326+
@dataclass(slots=True, frozen=True)
327+
class EfpData:
328+
"""
329+
Exchange for Physical (EFP) futures data.
330+
331+
EFP allows trading a position in a single stock for a position
332+
in the corresponding single stock future.
333+
"""
334+
335+
# Annualized basis points (financing rate comparable to broker rates)
336+
basisPoints: float
337+
338+
# Basis points formatted as percentage string
339+
formattedBasisPoints: str
340+
341+
# The implied Futures price
342+
impliedFuture: float
343+
344+
# Number of days until the future's last trade date
345+
holdDays: int
346+
347+
# Expiration date of the single stock future
348+
futureLastTradeDate: str
349+
350+
# Dividend impact on the annualized basis points interest rate
351+
dividendImpact: float
352+
353+
# Expected dividends until future expiration
354+
dividendsToLastTradeDate: float
355+
356+
326357
@dataclass(slots=True, frozen=True)
327358
class OptionComputation:
328359
tickAttrib: int

ib_async/ticker.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ib_async.objects import (
1111
Dividends,
1212
DOMLevel,
13+
EfpData,
1314
FundamentalRatios,
1415
IBDefaults,
1516
MktDepthData,
@@ -109,6 +110,28 @@ class Ticker:
109110
avOptionVolume: float = nan
110111
histVolatility: float = nan
111112
impliedVolatility: float = nan
113+
openInterest: float = nan
114+
lastRthTrade: float = nan
115+
lastRegTime: str = ""
116+
optionBidExch: str = ""
117+
optionAskExch: str = ""
118+
bondFactorMultiplier: float = nan
119+
creditmanMarkPrice: float = nan
120+
creditmanSlowMarkPrice: float = nan
121+
delayedLastTimestamp: datetime | None = None
122+
delayedHalted: float = nan
123+
reutersMutualFunds: str = ""
124+
etfNavClose: float = nan
125+
etfNavPriorClose: float = nan
126+
etfNavBid: float = nan
127+
etfNavAsk: float = nan
128+
etfNavLast: float = nan
129+
etfFrozenNavLast: float = nan
130+
etfNavHigh: float = nan
131+
etfNavLow: float = nan
132+
socialMarketAnalytics: str = ""
133+
estimatedIpoMidpoint: float = nan
134+
finalIpoLast: float = nan
112135
dividends: Optional[Dividends] = None
113136
fundamentalRatios: Optional[FundamentalRatios] = None
114137
ticks: list[TickData] = field(default_factory=list)
@@ -124,6 +147,14 @@ class Ticker:
124147
askGreeks: Optional[OptionComputation] = None
125148
lastGreeks: Optional[OptionComputation] = None
126149
modelGreeks: Optional[OptionComputation] = None
150+
custGreeks: Optional[OptionComputation] = None
151+
bidEfp: Optional[EfpData] = None
152+
askEfp: Optional[EfpData] = None
153+
lastEfp: Optional[EfpData] = None
154+
openEfp: Optional[EfpData] = None
155+
highEfp: Optional[EfpData] = None
156+
lowEfp: Optional[EfpData] = None
157+
closeEfp: Optional[EfpData] = None
127158
auctionVolume: float = nan
128159
auctionPrice: float = nan
129160
auctionImbalance: float = nan
@@ -195,6 +226,22 @@ def __post_init__(self):
195226
self.auctionPrice = self.defaults.unset
196227
self.auctionImbalance = self.defaults.unset
197228
self.regulatoryImbalance = self.defaults.unset
229+
self.openInterest = self.defaults.unset
230+
self.lastRthTrade = self.defaults.unset
231+
self.bondFactorMultiplier = self.defaults.unset
232+
self.creditmanMarkPrice = self.defaults.unset
233+
self.creditmanSlowMarkPrice = self.defaults.unset
234+
self.delayedHalted = self.defaults.unset
235+
self.etfNavClose = self.defaults.unset
236+
self.etfNavPriorClose = self.defaults.unset
237+
self.etfNavBid = self.defaults.unset
238+
self.etfNavAsk = self.defaults.unset
239+
self.etfNavLast = self.defaults.unset
240+
self.etfFrozenNavLast = self.defaults.unset
241+
self.etfNavHigh = self.defaults.unset
242+
self.etfNavLow = self.defaults.unset
243+
self.estimatedIpoMidpoint = self.defaults.unset
244+
self.finalIpoLast = self.defaults.unset
198245

199246
self.created = True
200247

ib_async/wrapper.py

Lines changed: 130 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
DepthMktDataDescription,
2626
Dividends,
2727
DOMLevel,
28+
EfpData,
2829
Execution,
2930
FamilyCode,
3031
Fill,
@@ -101,6 +102,19 @@
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

@@ -111,6 +125,7 @@
111125
64: "volumeRate5Min",
112126
65: "volumeRate10Min",
113127
21: "avVolume",
128+
22: "openInterest",
114129
27: "callOpenInterest",
115130
28: "putOpenInterest",
116131
29: "callVolume",
@@ -133,6 +148,8 @@
133148
55: "tradeRate",
134149
56: "volumeRate",
135150
58: "rtHistVolatility",
151+
60: "bondFactorMultiplier",
152+
90: "delayedHalted",
136153
}
137154

138155
GREEKS_TICK_MAP: Final[TickDict] = {
@@ -144,6 +161,38 @@
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

Comments
 (0)