onesecondtrader 0.43.0__tar.gz → 0.44.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {onesecondtrader-0.43.0 → onesecondtrader-0.44.0}/PKG-INFO +2 -2
  2. {onesecondtrader-0.43.0 → onesecondtrader-0.44.0}/README.md +1 -1
  3. {onesecondtrader-0.43.0 → onesecondtrader-0.44.0}/pyproject.toml +1 -1
  4. onesecondtrader-0.44.0/src/onesecondtrader/__init__.py +0 -0
  5. onesecondtrader-0.44.0/src/onesecondtrader/models/__init__.py +11 -0
  6. onesecondtrader-0.44.0/src/onesecondtrader/models/bar_fields.py +23 -0
  7. onesecondtrader-0.44.0/src/onesecondtrader/models/bar_period.py +21 -0
  8. onesecondtrader-0.44.0/src/onesecondtrader/models/order_types.py +21 -0
  9. onesecondtrader-0.44.0/src/onesecondtrader/models/trade_sides.py +20 -0
  10. onesecondtrader-0.43.0/src/onesecondtrader/__init__.py +0 -60
  11. onesecondtrader-0.43.0/src/onesecondtrader/connectors/__init__.py +0 -3
  12. onesecondtrader-0.43.0/src/onesecondtrader/connectors/brokers/__init__.py +0 -4
  13. onesecondtrader-0.43.0/src/onesecondtrader/connectors/brokers/ib.py +0 -418
  14. onesecondtrader-0.43.0/src/onesecondtrader/connectors/brokers/simulated.py +0 -349
  15. onesecondtrader-0.43.0/src/onesecondtrader/connectors/datafeeds/__init__.py +0 -4
  16. onesecondtrader-0.43.0/src/onesecondtrader/connectors/datafeeds/ib.py +0 -286
  17. onesecondtrader-0.43.0/src/onesecondtrader/connectors/datafeeds/simulated.py +0 -198
  18. onesecondtrader-0.43.0/src/onesecondtrader/connectors/gateways/__init__.py +0 -3
  19. onesecondtrader-0.43.0/src/onesecondtrader/connectors/gateways/ib.py +0 -314
  20. onesecondtrader-0.43.0/src/onesecondtrader/core/__init__.py +0 -7
  21. onesecondtrader-0.43.0/src/onesecondtrader/core/brokers/__init__.py +0 -3
  22. onesecondtrader-0.43.0/src/onesecondtrader/core/brokers/base.py +0 -46
  23. onesecondtrader-0.43.0/src/onesecondtrader/core/datafeeds/__init__.py +0 -3
  24. onesecondtrader-0.43.0/src/onesecondtrader/core/datafeeds/base.py +0 -32
  25. onesecondtrader-0.43.0/src/onesecondtrader/core/events/__init__.py +0 -33
  26. onesecondtrader-0.43.0/src/onesecondtrader/core/events/bases.py +0 -29
  27. onesecondtrader-0.43.0/src/onesecondtrader/core/events/market.py +0 -22
  28. onesecondtrader-0.43.0/src/onesecondtrader/core/events/requests.py +0 -33
  29. onesecondtrader-0.43.0/src/onesecondtrader/core/events/responses.py +0 -54
  30. onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/__init__.py +0 -13
  31. onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/averages.py +0 -56
  32. onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/bar.py +0 -47
  33. onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/base.py +0 -60
  34. onesecondtrader-0.43.0/src/onesecondtrader/core/messaging/__init__.py +0 -7
  35. onesecondtrader-0.43.0/src/onesecondtrader/core/messaging/eventbus.py +0 -47
  36. onesecondtrader-0.43.0/src/onesecondtrader/core/messaging/subscriber.py +0 -69
  37. onesecondtrader-0.43.0/src/onesecondtrader/core/models/__init__.py +0 -15
  38. onesecondtrader-0.43.0/src/onesecondtrader/core/models/data.py +0 -18
  39. onesecondtrader-0.43.0/src/onesecondtrader/core/models/orders.py +0 -27
  40. onesecondtrader-0.43.0/src/onesecondtrader/core/models/params.py +0 -21
  41. onesecondtrader-0.43.0/src/onesecondtrader/core/models/records.py +0 -34
  42. onesecondtrader-0.43.0/src/onesecondtrader/core/strategies/__init__.py +0 -7
  43. onesecondtrader-0.43.0/src/onesecondtrader/core/strategies/base.py +0 -331
  44. onesecondtrader-0.43.0/src/onesecondtrader/core/strategies/examples.py +0 -47
  45. onesecondtrader-0.43.0/src/onesecondtrader/dashboard/__init__.py +0 -3
  46. onesecondtrader-0.43.0/src/onesecondtrader/dashboard/app.py +0 -2972
  47. onesecondtrader-0.43.0/src/onesecondtrader/dashboard/registry.py +0 -100
  48. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/__init__.py +0 -7
  49. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/orchestrator.py +0 -105
  50. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/recorder.py +0 -199
  51. onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/schema.sql +0 -212
  52. onesecondtrader-0.43.0/src/onesecondtrader/secmaster/__init__.py +0 -6
  53. onesecondtrader-0.43.0/src/onesecondtrader/secmaster/schema.sql +0 -740
  54. onesecondtrader-0.43.0/src/onesecondtrader/secmaster/utils.py +0 -737
  55. {onesecondtrader-0.43.0 → onesecondtrader-0.44.0}/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.43.0
3
+ Version: 0.44.0
4
4
  Summary: The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place.
5
5
  License-File: LICENSE
6
6
  Author: Nils P. Kujath
@@ -117,7 +117,7 @@ graph TD
117
117
  B["<b>Code Quality Checks</b><br/>• Ruff Check & Format<br/>• MyPy Type Checking<br/>• Tests & Doctests"]
118
118
  C["<b>Security Checks</b><br/>• Gitleaks Secret Detection"]
119
119
  D["<b>File Validation</b><br/>• YAML/TOML/JSON Check<br/>• End-of-file Fixer<br/>• Large Files Check<br/>• Merge Conflict Check<br/>• Debug Statements Check"]
120
- E["<b>Generate API Documentation</b> via <kbd>scripts/generate_api_docs.py</kbd><br/>• Auto-generate docs<br/>• Stage changes"]
120
+ E["<b>Generate Reference Documentation</b> via <kbd>scripts/generate_reference_docs.py</kbd><br/>• Auto-generate docs<br/>• Stage changes"]
121
121
  end
122
122
  B --> C --> D --> E
123
123
 
@@ -93,7 +93,7 @@ graph TD
93
93
  B["<b>Code Quality Checks</b><br/>• Ruff Check & Format<br/>• MyPy Type Checking<br/>• Tests & Doctests"]
94
94
  C["<b>Security Checks</b><br/>• Gitleaks Secret Detection"]
95
95
  D["<b>File Validation</b><br/>• YAML/TOML/JSON Check<br/>• End-of-file Fixer<br/>• Large Files Check<br/>• Merge Conflict Check<br/>• Debug Statements Check"]
96
- E["<b>Generate API Documentation</b> via <kbd>scripts/generate_api_docs.py</kbd><br/>• Auto-generate docs<br/>• Stage changes"]
96
+ E["<b>Generate Reference Documentation</b> via <kbd>scripts/generate_reference_docs.py</kbd><br/>• Auto-generate docs<br/>• Stage changes"]
97
97
  end
98
98
  B --> C --> D --> E
99
99
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "onesecondtrader"
3
- version = "0.43.0"
3
+ version = "0.44.0"
4
4
  description = "The Trading Infrastructure Toolkit for Python. Research, simulate, and deploy algorithmic trading strategies — all in one place."
5
5
  authors = [
6
6
  {name = "Nils P. Kujath",email = "63961429+NilsKujath@users.noreply.github.com"}
File without changes
@@ -0,0 +1,11 @@
1
+ """
2
+ The `models` package defines the fundamental domain concepts used throughout the trading system.
3
+ It establishes a shared vocabulary for representing domain-specific structures.
4
+ """
5
+
6
+ from .bar_fields import BarField
7
+ from .bar_period import BarPeriod
8
+ from .order_types import OrderType
9
+ from .trade_sides import TradeSide
10
+
11
+ __all__ = ["BarField", "BarPeriod", "OrderType", "TradeSide"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+
6
+ class BarField(enum.Enum):
7
+ """
8
+ Enumeration of bar fields used as indicator inputs.
9
+
10
+ | Value | Semantics |
11
+ |--------|------------------------------------|
12
+ | OPEN | Bar's opening value. |
13
+ | HIGH | Bar's highest value. |
14
+ | LOW | Bar's lowest value. |
15
+ | CLOSE | Bar's closing value. |
16
+ | VOLUME | Bar's traded volume. |
17
+ """
18
+
19
+ OPEN = enum.auto()
20
+ HIGH = enum.auto()
21
+ LOW = enum.auto()
22
+ CLOSE = enum.auto()
23
+ VOLUME = enum.auto()
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+
6
+ class BarPeriod(enum.Enum):
7
+ """
8
+ Enumeration of bar aggregation periods.
9
+
10
+ | Value | Semantics |
11
+ |--------|----------------------|
12
+ | SECOND | Duration of 1 second.|
13
+ | MINUTE | Duration of 1 minute.|
14
+ | HOUR | Duration of 1 hour. |
15
+ | DAY | Duration of 1 day. |
16
+ """
17
+
18
+ SECOND = enum.auto()
19
+ MINUTE = enum.auto()
20
+ HOUR = enum.auto()
21
+ DAY = enum.auto()
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+
6
+ class OrderType(enum.Enum):
7
+ """
8
+ Enumeration of order execution types.
9
+
10
+ | Value | Semantics |
11
+ |-------------|-------------------------------------------------------------|
12
+ | LIMIT | Executable only at the specified limit price or better. |
13
+ | MARKET | Executable immediately at the best available market price. |
14
+ | STOP | Becomes a market order once the stop price is reached. |
15
+ | STOP_LIMIT | Becomes a limit order once the stop price is reached. |
16
+ """
17
+
18
+ LIMIT = enum.auto()
19
+ MARKET = enum.auto()
20
+ STOP = enum.auto()
21
+ STOP_LIMIT = enum.auto()
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+
6
+ class TradeSide(enum.Enum):
7
+ """
8
+ Enumeration of trade direction.
9
+
10
+ `OrderSide` specifies the direction of change applied to the (net) signed position
11
+ quantity from the perspective of the trading account.
12
+
13
+ | Value | Semantics |
14
+ |-------|------------------------------------------------|
15
+ | BUY | Increases the signed position quantity. |
16
+ | SELL | Decreases the signed position quantity. |
17
+ """
18
+
19
+ BUY = enum.auto()
20
+ SELL = enum.auto()
@@ -1,60 +0,0 @@
1
- __all__ = [
2
- "ActionType",
3
- "BarPeriod",
4
- "BarProcessed",
5
- "BarReceived",
6
- "BrokerBase",
7
- "Close",
8
- "DatafeedBase",
9
- "FillRecord",
10
- "High",
11
- "IBBroker",
12
- "IBDatafeed",
13
- "Indicator",
14
- "InputSource",
15
- "Low",
16
- "Open",
17
- "OrderFilled",
18
- "OrderRecord",
19
- "OrderSide",
20
- "OrderSubmission",
21
- "OrderType",
22
- "ParamSpec",
23
- "SimulatedBroker",
24
- "SimulatedDatafeed",
25
- "SimpleMovingAverage",
26
- "SMACrossover",
27
- "StrategyBase",
28
- "Volume",
29
- ]
30
-
31
- from onesecondtrader.core.brokers import BrokerBase
32
- from onesecondtrader.connectors.brokers import IBBroker, SimulatedBroker
33
- from onesecondtrader.core.datafeeds import DatafeedBase
34
- from onesecondtrader.connectors.datafeeds import IBDatafeed, SimulatedDatafeed
35
- from onesecondtrader.core.events import (
36
- BarProcessed,
37
- BarReceived,
38
- OrderFilled,
39
- OrderSubmission,
40
- )
41
- from onesecondtrader.core.indicators import (
42
- Close,
43
- High,
44
- Indicator,
45
- Low,
46
- Open,
47
- SimpleMovingAverage,
48
- Volume,
49
- )
50
- from onesecondtrader.core.models import (
51
- ActionType,
52
- BarPeriod,
53
- FillRecord,
54
- InputSource,
55
- OrderRecord,
56
- OrderSide,
57
- OrderType,
58
- ParamSpec,
59
- )
60
- from onesecondtrader.core.strategies import SMACrossover, StrategyBase
@@ -1,3 +0,0 @@
1
- from onesecondtrader.connectors import brokers as brokers
2
- from onesecondtrader.connectors import datafeeds as datafeeds
3
- from onesecondtrader.connectors import gateways as gateways
@@ -1,4 +0,0 @@
1
- __all__ = ["IBBroker", "SimulatedBroker"]
2
-
3
- from .ib import IBBroker
4
- from .simulated import SimulatedBroker
@@ -1,418 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import dataclasses
4
- import logging
5
- import os
6
- import sqlite3
7
- import uuid
8
-
9
- import pandas as pd
10
- from ib_async import LimitOrder, MarketOrder, StopLimitOrder, StopOrder, Trade
11
-
12
- from onesecondtrader.connectors.gateways.ib import _get_gateway, make_contract
13
- from onesecondtrader.core import events, messaging, models
14
- from onesecondtrader.core.brokers import BrokerBase
15
-
16
- _logger = logging.getLogger(__name__)
17
-
18
-
19
- @dataclasses.dataclass
20
- class _OrderMapping:
21
- system_order_id: uuid.UUID
22
- symbol: str
23
- side: models.OrderSide
24
- trade: Trade
25
-
26
-
27
- class IBBroker(BrokerBase):
28
- """
29
- Live order execution broker for Interactive Brokers.
30
-
31
- Submits orders to IB and translates IB order events back to system events.
32
-
33
- Symbol Resolution:
34
- Uses the same resolution logic as IBDatafeed:
35
-
36
- 1. Explicit format: ``SYMBOL:SECTYPE:CURRENCY:EXCHANGE[:EXPIRY[:STRIKE[:RIGHT]]]``
37
- 2. Secmaster lookup: If ``db_path`` is configured
38
- 3. Default: US stock on SMART routing
39
-
40
- Order Types:
41
- - ``OrderType.MARKET``: Market order
42
- - ``OrderType.LIMIT``: Limit order
43
- - ``OrderType.STOP``: Stop order (becomes market when triggered)
44
- - ``OrderType.STOP_LIMIT``: Stop-limit order
45
-
46
- Attributes:
47
- db_path: Optional path to secmaster database for symbol resolution.
48
- """
49
-
50
- db_path: str = ""
51
-
52
- def __init__(self, event_bus: messaging.EventBus) -> None:
53
- super().__init__(event_bus)
54
- self._gateway = _get_gateway()
55
- self._connected = False
56
- self._db_connection: sqlite3.Connection | None = None
57
- self._order_mappings: dict[uuid.UUID, _OrderMapping] = {}
58
- self._ib_to_system_id: dict[int, uuid.UUID] = {}
59
- self._pending_cancellations: set[uuid.UUID] = set()
60
- self._pending_modifications: dict[uuid.UUID, events.OrderModification] = {}
61
- self._filled_quantities: dict[uuid.UUID, float] = {}
62
-
63
- def connect(self) -> None:
64
- if self._connected:
65
- return
66
- _logger.info("Connecting to IB")
67
- self._gateway.acquire()
68
- self._gateway.register_reconnect_callback(self._on_reconnect)
69
- self._gateway.register_disconnect_callback(self._on_disconnect)
70
- db_path = self.db_path or os.environ.get("SECMASTER_DB_PATH", "")
71
- if db_path and os.path.exists(db_path):
72
- self._db_connection = sqlite3.connect(db_path, check_same_thread=False)
73
-
74
- self._register_ib_events()
75
- self._connected = True
76
- _logger.info("Connected to IB")
77
-
78
- def disconnect(self) -> None:
79
- if not self._connected:
80
- return
81
- _logger.info("Disconnecting from IB")
82
-
83
- self._gateway.unregister_reconnect_callback(self._on_reconnect)
84
- self._gateway.unregister_disconnect_callback(self._on_disconnect)
85
- self._unregister_ib_events()
86
-
87
- if self._db_connection:
88
- self._db_connection.close()
89
- self._db_connection = None
90
- self._gateway.release()
91
- self._connected = False
92
- self.shutdown()
93
- _logger.info("Disconnected from IB")
94
-
95
- def _register_ib_events(self) -> None:
96
- ib = self._gateway.ib
97
- ib.orderStatusEvent += self._on_order_status
98
- ib.execDetailsEvent += self._on_exec_details
99
- ib.errorEvent += self._on_error
100
-
101
- def _unregister_ib_events(self) -> None:
102
- ib = self._gateway.ib
103
- ib.orderStatusEvent -= self._on_order_status
104
- ib.execDetailsEvent -= self._on_exec_details
105
- ib.errorEvent -= self._on_error
106
-
107
- def _on_disconnect(self) -> None:
108
- pending_count = len(self._order_mappings)
109
- _logger.warning("IB disconnected, expiring %d pending orders", pending_count)
110
- now = pd.Timestamp.now(tz="UTC")
111
- for order_id in list(self._order_mappings.keys()):
112
- self._publish(
113
- events.OrderExpired(
114
- ts_event=now,
115
- ts_broker=now,
116
- associated_order_id=order_id,
117
- )
118
- )
119
- self._order_mappings.clear()
120
- self._ib_to_system_id.clear()
121
- self._pending_cancellations.clear()
122
- self._pending_modifications.clear()
123
- self._filled_quantities.clear()
124
-
125
- def _on_reconnect(self) -> None:
126
- _logger.info("IB reconnected, re-registering event handlers")
127
- self._register_ib_events()
128
-
129
- def _on_submit_order(self, event: events.OrderSubmission) -> None:
130
- _logger.info(
131
- "Submitting order: %s %s %s %s",
132
- event.side.name,
133
- event.quantity,
134
- event.symbol,
135
- event.order_type.name,
136
- )
137
- contract = self._make_contract(event.symbol)
138
- action = "BUY" if event.side == models.OrderSide.BUY else "SELL"
139
-
140
- order = self._create_ib_order(event, action)
141
- if order is None:
142
- _logger.warning(
143
- "Order rejected (invalid params): %s", event.system_order_id
144
- )
145
- self._publish(
146
- events.OrderSubmissionRejected(
147
- ts_event=pd.Timestamp.now(tz="UTC"),
148
- ts_broker=pd.Timestamp.now(tz="UTC"),
149
- associated_order_id=event.system_order_id,
150
- reason="Invalid order parameters",
151
- )
152
- )
153
- return
154
-
155
- async def _place():
156
- return self._gateway.ib.placeOrder(contract, order)
157
-
158
- trade = self._gateway.run_coro(_place())
159
-
160
- mapping = _OrderMapping(
161
- system_order_id=event.system_order_id,
162
- symbol=event.symbol,
163
- side=event.side,
164
- trade=trade,
165
- )
166
- self._order_mappings[event.system_order_id] = mapping
167
- self._ib_to_system_id[trade.order.orderId] = event.system_order_id
168
- self._filled_quantities[event.system_order_id] = 0.0
169
-
170
- _logger.info(
171
- "Order accepted: %s -> IB order %d",
172
- event.system_order_id,
173
- trade.order.orderId,
174
- )
175
- self._publish(
176
- events.OrderSubmissionAccepted(
177
- ts_event=pd.Timestamp.now(tz="UTC"),
178
- ts_broker=pd.Timestamp.now(tz="UTC"),
179
- associated_order_id=event.system_order_id,
180
- broker_order_id=str(trade.order.orderId),
181
- )
182
- )
183
-
184
- def _on_cancel_order(self, event: events.OrderCancellation) -> None:
185
- order_id = event.system_order_id
186
- _logger.info("Cancelling order: %s", order_id)
187
- mapping = self._order_mappings.get(order_id)
188
-
189
- if mapping is None:
190
- _logger.warning("Cancel rejected (not found): %s", order_id)
191
- self._publish(
192
- events.OrderCancellationRejected(
193
- ts_event=pd.Timestamp.now(tz="UTC"),
194
- ts_broker=pd.Timestamp.now(tz="UTC"),
195
- associated_order_id=order_id,
196
- reason="Order not found",
197
- )
198
- )
199
- return
200
-
201
- self._pending_cancellations.add(order_id)
202
-
203
- async def _cancel():
204
- self._gateway.ib.cancelOrder(mapping.trade.order)
205
-
206
- self._gateway.run_coro(_cancel())
207
-
208
- def _on_modify_order(self, event: events.OrderModification) -> None:
209
- order_id = event.system_order_id
210
- _logger.info("Modifying order: %s", order_id)
211
- mapping = self._order_mappings.get(order_id)
212
-
213
- if mapping is None:
214
- _logger.warning("Modify rejected (not found): %s", order_id)
215
- self._publish(
216
- events.OrderModificationRejected(
217
- ts_event=pd.Timestamp.now(tz="UTC"),
218
- ts_broker=pd.Timestamp.now(tz="UTC"),
219
- associated_order_id=order_id,
220
- reason="Order not found",
221
- )
222
- )
223
- return
224
-
225
- trade = mapping.trade
226
- order = trade.order
227
-
228
- if event.quantity is not None:
229
- order.totalQuantity = event.quantity
230
- if event.limit_price is not None:
231
- order.lmtPrice = event.limit_price
232
- if event.stop_price is not None:
233
- order.auxPrice = event.stop_price
234
-
235
- self._pending_modifications[order_id] = event
236
-
237
- async def _modify():
238
- self._gateway.ib.placeOrder(trade.contract, order)
239
-
240
- self._gateway.run_coro(_modify())
241
-
242
- def _on_order_status(self, trade: Trade) -> None:
243
- ib_order_id = trade.order.orderId
244
- system_order_id = self._ib_to_system_id.get(ib_order_id)
245
- if system_order_id is None:
246
- return
247
-
248
- status = trade.orderStatus.status
249
- now = pd.Timestamp.now(tz="UTC")
250
-
251
- if system_order_id in self._pending_cancellations:
252
- if status == "Cancelled":
253
- _logger.info("Order cancelled: %s", system_order_id)
254
- self._pending_cancellations.discard(system_order_id)
255
- self._publish(
256
- events.OrderCancellationAccepted(
257
- ts_event=now,
258
- ts_broker=now,
259
- associated_order_id=system_order_id,
260
- )
261
- )
262
- self._cleanup_order(system_order_id)
263
- return
264
-
265
- if system_order_id in self._pending_modifications:
266
- if status in ("PreSubmitted", "Submitted"):
267
- _logger.info("Order modified: %s", system_order_id)
268
- self._pending_modifications.pop(system_order_id, None)
269
- self._publish(
270
- events.OrderModificationAccepted(
271
- ts_event=now,
272
- ts_broker=now,
273
- associated_order_id=system_order_id,
274
- broker_order_id=str(ib_order_id),
275
- )
276
- )
277
- return
278
-
279
- if status == "Inactive":
280
- mapping = self._order_mappings.get(system_order_id)
281
- if mapping:
282
- _logger.warning("Order expired (inactive): %s", system_order_id)
283
- self._publish(
284
- events.OrderExpired(
285
- ts_event=now,
286
- ts_broker=now,
287
- associated_order_id=system_order_id,
288
- )
289
- )
290
- self._cleanup_order(system_order_id)
291
-
292
- def _on_exec_details(self, trade: Trade, fill) -> None:
293
- ib_order_id = trade.order.orderId
294
- system_order_id = self._ib_to_system_id.get(ib_order_id)
295
- if system_order_id is None:
296
- return
297
-
298
- mapping = self._order_mappings.get(system_order_id)
299
- if mapping is None:
300
- return
301
-
302
- execution = fill.execution
303
- now = pd.Timestamp.now(tz="UTC")
304
- exec_time = pd.Timestamp(execution.time, tz="UTC") if execution.time else now
305
-
306
- commission = 0.0
307
- if fill.commissionReport and fill.commissionReport.commission:
308
- commission = fill.commissionReport.commission
309
-
310
- _logger.info(
311
- "Order filled: %s %s %s @ %.4f (qty=%.2f)",
312
- system_order_id,
313
- mapping.side.name,
314
- mapping.symbol,
315
- execution.price,
316
- execution.shares,
317
- )
318
- self._publish(
319
- events.OrderFilled(
320
- ts_event=now,
321
- ts_broker=exec_time,
322
- associated_order_id=system_order_id,
323
- broker_fill_id=execution.execId,
324
- symbol=mapping.symbol,
325
- side=mapping.side,
326
- quantity_filled=execution.shares,
327
- fill_price=execution.price,
328
- commission=commission,
329
- exchange=execution.exchange or "IB",
330
- )
331
- )
332
-
333
- self._filled_quantities[system_order_id] = (
334
- self._filled_quantities.get(system_order_id, 0.0) + execution.shares
335
- )
336
-
337
- if trade.orderStatus.status == "Filled":
338
- self._cleanup_order(system_order_id)
339
-
340
- def _on_error(self, reqId: int, errorCode: int, errorString: str, contract) -> None:
341
- system_order_id = self._ib_to_system_id.get(reqId)
342
- if system_order_id is None:
343
- return
344
-
345
- _logger.error(
346
- "IB error %d for order %s: %s", errorCode, system_order_id, errorString
347
- )
348
- now = pd.Timestamp.now(tz="UTC")
349
-
350
- if system_order_id in self._pending_cancellations:
351
- _logger.warning("Cancel rejected: %s - %s", system_order_id, errorString)
352
- self._pending_cancellations.discard(system_order_id)
353
- self._publish(
354
- events.OrderCancellationRejected(
355
- ts_event=now,
356
- ts_broker=now,
357
- associated_order_id=system_order_id,
358
- reason=errorString,
359
- )
360
- )
361
- return
362
-
363
- if system_order_id in self._pending_modifications:
364
- _logger.warning("Modify rejected: %s - %s", system_order_id, errorString)
365
- self._pending_modifications.pop(system_order_id, None)
366
- self._publish(
367
- events.OrderModificationRejected(
368
- ts_event=now,
369
- ts_broker=now,
370
- associated_order_id=system_order_id,
371
- reason=errorString,
372
- )
373
- )
374
- return
375
-
376
- if errorCode in (201, 202, 203, 321, 322):
377
- _logger.warning("Order rejected: %s - %s", system_order_id, errorString)
378
- self._publish(
379
- events.OrderSubmissionRejected(
380
- ts_event=now,
381
- ts_broker=now,
382
- associated_order_id=system_order_id,
383
- reason=errorString,
384
- )
385
- )
386
- self._cleanup_order(system_order_id)
387
-
388
- def _cleanup_order(self, system_order_id: uuid.UUID) -> None:
389
- mapping = self._order_mappings.pop(system_order_id, None)
390
- if mapping:
391
- self._ib_to_system_id.pop(mapping.trade.order.orderId, None)
392
- self._filled_quantities.pop(system_order_id, None)
393
- self._pending_cancellations.discard(system_order_id)
394
- self._pending_modifications.pop(system_order_id, None)
395
-
396
- def _create_ib_order(self, event: events.OrderSubmission, action: str):
397
- match event.order_type:
398
- case models.OrderType.MARKET:
399
- return MarketOrder(action, event.quantity)
400
- case models.OrderType.LIMIT:
401
- if event.limit_price is None:
402
- return None
403
- return LimitOrder(action, event.quantity, event.limit_price)
404
- case models.OrderType.STOP:
405
- if event.stop_price is None:
406
- return None
407
- return StopOrder(action, event.quantity, event.stop_price)
408
- case models.OrderType.STOP_LIMIT:
409
- if event.limit_price is None or event.stop_price is None:
410
- return None
411
- return StopLimitOrder(
412
- action, event.quantity, event.limit_price, event.stop_price
413
- )
414
- case _:
415
- return None
416
-
417
- def _make_contract(self, symbol: str):
418
- return make_contract(symbol, self._db_connection)