From 2930c079a039b964ff031c9f767d95f0b0179c38 Mon Sep 17 00:00:00 2001 From: gonzo Date: Wed, 12 Mar 2025 17:49:02 +0100 Subject: [PATCH 1/2] WAP support on tickfilter bars Align `ib_async.ticker.Ticker.Bar` with TWS API `IBApi.Bar` class Reference -> https://interactivebrokers.github.io/tws-api/classIBApi_1_1Bar.html Change summary - add `wap` column - sort columns aligned with `IBApi.Bar` . - set Bar class `slots=True` - using pympler asizeof.asizeof(bar)=736 - slot version asizeof.asizeof(bar)=152 - include wap calculation on: - TimeBars - TickBars - VolumeBars - set `ib_async.ticker.Ticker` dataclasse as slots=True - according to pympler asizeof(Ticker) = 7200 - slotted version asizeof(Ticker) = 3000 --- ib_async/ticker.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/ib_async/ticker.py b/ib_async/ticker.py index bf799ed..c74a3f8 100644 --- a/ib_async/ticker.py +++ b/ib_async/ticker.py @@ -23,7 +23,7 @@ nan = float("nan") -@dataclass +@dataclass(slots=True) class Ticker: """ Current market data such as bid, ask, last price, etc. for a contract. @@ -123,6 +123,7 @@ class Ticker: regulatoryImbalance: float = nan bboExchange: str = "" snapshotPermissions: int = 0 + updateEvent: Event = field(init=False) def __post_init__(self): self.updateEvent = TickerUpdateEvent("updateEvent") @@ -258,7 +259,7 @@ def on_source(self, ticker): self.emit(ticker.time, ticker.midpoint(), 0) -@dataclass +@dataclass(slots=True) class Bar: time: Optional[datetime] open: float = nan @@ -266,6 +267,7 @@ class Bar: low: float = nan close: float = nan volume: int = 0 + wap: float = 0 count: int = 0 @@ -302,6 +304,11 @@ def on_source(self, time, price, size): bar.high = max(bar.high, price) bar.low = min(bar.low, price) bar.close = price + # wap + if (bar.volume + size) == 0: + bar.wap = bar.wap + else: + bar.wap = ((bar.wap * bar.volume) + (price * size)) / (bar.volume + size) bar.volume += size bar.count += 1 self.bars.updateEvent.emit(self.bars, False) @@ -310,7 +317,9 @@ def _on_timer(self, time): if self.bars: bar = self.bars[-1] if isNan(bar.close) and len(self.bars) > 1: - bar.open = bar.high = bar.low = bar.close = self.bars[-2].close + bar.open = bar.high = bar.low = bar.close = bar.wap = self.bars[ + -2 + ].close self.bars.updateEvent.emit(self.bars, True) self.emit(bar) self.bars.append(Bar(time)) @@ -333,16 +342,25 @@ def __init__(self, count, source=None): def on_source(self, time, price, size): if not self.bars or self.bars[-1].count == self._count: - bar = Bar(time, price, price, price, price, size, 1) + bar = Bar(time, price, price, price, price, size, price, 1) self.bars.append(bar) else: bar = self.bars[-1] bar.high = max(bar.high, price) bar.low = min(bar.low, price) bar.close = price + # wap + if (bar.volume + size) == 0: + bar.wap = bar.wap + else: + bar.wap = ((bar.wap * bar.volume) + (price * size)) / ( + bar.volume + size + ) bar.volume += size bar.count += 1 if bar.count == self._count: + if bar.wap == 0: + bar.wap = bar.close self.bars.updateEvent.emit(self.bars, True) self.emit(self.bars) @@ -360,15 +378,24 @@ def __init__(self, volume, source=None): def on_source(self, time, price, size): if not self.bars or self.bars[-1].volume >= self._volume: - bar = Bar(time, price, price, price, price, size, 1) + bar = Bar(time, price, price, price, price, size, price, 1) self.bars.append(bar) else: bar = self.bars[-1] bar.high = max(bar.high, price) bar.low = min(bar.low, price) + # wap bar.close = price + if (bar.volume + size) == 0: + bar.wap = bar.wap + else: + bar.wap = ((bar.wap * bar.volume) + (price * size)) / ( + bar.volume + size + ) bar.volume += size bar.count += 1 if bar.volume >= self._volume: + if bar.wap == 0: + bar.wap = bar.close self.bars.updateEvent.emit(self.bars, True) self.emit(self.bars) From ba42d9fe0530be5d2eb81dd240b02115172c5b2b Mon Sep 17 00:00:00 2001 From: gonzo Date: Tue, 18 Mar 2025 12:02:16 +0100 Subject: [PATCH 2/2] Updates - set wap default value to `nan` - manage first bar to avoit operations with `nan` - align functionality within `Timebars`, `TickBars` and `VolumeBars`. They all emit on new ticks `self.bars.updateEvent.emit(self.bars, False)` with parameter `hasNewBar` set `False`. Aligned with `reqHistoricalData` and `reqRealTimeBars` --- ib_async/ticker.py | 51 +++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/ib_async/ticker.py b/ib_async/ticker.py index c74a3f8..d1b795d 100644 --- a/ib_async/ticker.py +++ b/ib_async/ticker.py @@ -8,8 +8,8 @@ from ib_async.contract import Contract from ib_async.objects import ( - DOMLevel, Dividends, + DOMLevel, FundamentalRatios, MktDepthData, OptionComputation, @@ -267,7 +267,7 @@ class Bar: low: float = nan close: float = nan volume: int = 0 - wap: float = 0 + wap: float = nan count: int = 0 @@ -305,10 +305,10 @@ def on_source(self, time, price, size): bar.low = min(bar.low, price) bar.close = price # wap - if (bar.volume + size) == 0: - bar.wap = bar.wap - else: - bar.wap = ((bar.wap * bar.volume) + (price * size)) / (bar.volume + size) + if (bar.volume + size) != 0: # Prevent division by zero on empty bar + bar.wap = ( + ((bar.wap if not isNan(bar.wap) else 0) * bar.volume) + (price * size) + ) / (bar.volume + size) bar.volume += size bar.count += 1 self.bars.updateEvent.emit(self.bars, False) @@ -350,19 +350,22 @@ def on_source(self, time, price, size): bar.low = min(bar.low, price) bar.close = price # wap - if (bar.volume + size) == 0: - bar.wap = bar.wap - else: - bar.wap = ((bar.wap * bar.volume) + (price * size)) / ( - bar.volume + size - ) + if (bar.volume + size) != 0: # Prevent division by zero on empty bar + bar.wap = ( + ((bar.wap if not isNan(bar.wap) else 0) * bar.volume) + + (price * size) + ) / (bar.volume + size) bar.volume += size bar.count += 1 - if bar.count == self._count: - if bar.wap == 0: + if bar.count == self._count: # full bar + if isNan(bar.wap): bar.wap = bar.close self.bars.updateEvent.emit(self.bars, True) self.emit(self.bars) + else: # partial bar + if isNan(bar.wap): + bar.wap = bar.close + self.bars.updateEvent.emit(self.bars, False) class VolumeBars(Op): @@ -385,17 +388,19 @@ def on_source(self, time, price, size): bar.high = max(bar.high, price) bar.low = min(bar.low, price) # wap - bar.close = price - if (bar.volume + size) == 0: - bar.wap = bar.wap - else: - bar.wap = ((bar.wap * bar.volume) + (price * size)) / ( - bar.volume + size - ) + if (bar.volume + size) != 0: # Prevent division by zero on empty bar + bar.wap = ( + ((bar.wap if not isNan(bar.wap) else 0) * bar.volume) + + (price * size) + ) / (bar.volume + size) bar.volume += size bar.count += 1 - if bar.volume >= self._volume: - if bar.wap == 0: + if bar.volume >= self._volume: # full bar + if isNan(bar.wap): bar.wap = bar.close self.bars.updateEvent.emit(self.bars, True) self.emit(self.bars) + else: # partial bar + if isNan(bar.wap): + bar.wap = bar.close + self.bars.updateEvent.emit(self.bars, False)