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.
- {onesecondtrader-0.43.0 → onesecondtrader-0.44.0}/PKG-INFO +2 -2
- {onesecondtrader-0.43.0 → onesecondtrader-0.44.0}/README.md +1 -1
- {onesecondtrader-0.43.0 → onesecondtrader-0.44.0}/pyproject.toml +1 -1
- onesecondtrader-0.44.0/src/onesecondtrader/__init__.py +0 -0
- onesecondtrader-0.44.0/src/onesecondtrader/models/__init__.py +11 -0
- onesecondtrader-0.44.0/src/onesecondtrader/models/bar_fields.py +23 -0
- onesecondtrader-0.44.0/src/onesecondtrader/models/bar_period.py +21 -0
- onesecondtrader-0.44.0/src/onesecondtrader/models/order_types.py +21 -0
- onesecondtrader-0.44.0/src/onesecondtrader/models/trade_sides.py +20 -0
- onesecondtrader-0.43.0/src/onesecondtrader/__init__.py +0 -60
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/__init__.py +0 -3
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/brokers/__init__.py +0 -4
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/brokers/ib.py +0 -418
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/brokers/simulated.py +0 -349
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/datafeeds/__init__.py +0 -4
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/datafeeds/ib.py +0 -286
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/datafeeds/simulated.py +0 -198
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/gateways/__init__.py +0 -3
- onesecondtrader-0.43.0/src/onesecondtrader/connectors/gateways/ib.py +0 -314
- onesecondtrader-0.43.0/src/onesecondtrader/core/__init__.py +0 -7
- onesecondtrader-0.43.0/src/onesecondtrader/core/brokers/__init__.py +0 -3
- onesecondtrader-0.43.0/src/onesecondtrader/core/brokers/base.py +0 -46
- onesecondtrader-0.43.0/src/onesecondtrader/core/datafeeds/__init__.py +0 -3
- onesecondtrader-0.43.0/src/onesecondtrader/core/datafeeds/base.py +0 -32
- onesecondtrader-0.43.0/src/onesecondtrader/core/events/__init__.py +0 -33
- onesecondtrader-0.43.0/src/onesecondtrader/core/events/bases.py +0 -29
- onesecondtrader-0.43.0/src/onesecondtrader/core/events/market.py +0 -22
- onesecondtrader-0.43.0/src/onesecondtrader/core/events/requests.py +0 -33
- onesecondtrader-0.43.0/src/onesecondtrader/core/events/responses.py +0 -54
- onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/__init__.py +0 -13
- onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/averages.py +0 -56
- onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/bar.py +0 -47
- onesecondtrader-0.43.0/src/onesecondtrader/core/indicators/base.py +0 -60
- onesecondtrader-0.43.0/src/onesecondtrader/core/messaging/__init__.py +0 -7
- onesecondtrader-0.43.0/src/onesecondtrader/core/messaging/eventbus.py +0 -47
- onesecondtrader-0.43.0/src/onesecondtrader/core/messaging/subscriber.py +0 -69
- onesecondtrader-0.43.0/src/onesecondtrader/core/models/__init__.py +0 -15
- onesecondtrader-0.43.0/src/onesecondtrader/core/models/data.py +0 -18
- onesecondtrader-0.43.0/src/onesecondtrader/core/models/orders.py +0 -27
- onesecondtrader-0.43.0/src/onesecondtrader/core/models/params.py +0 -21
- onesecondtrader-0.43.0/src/onesecondtrader/core/models/records.py +0 -34
- onesecondtrader-0.43.0/src/onesecondtrader/core/strategies/__init__.py +0 -7
- onesecondtrader-0.43.0/src/onesecondtrader/core/strategies/base.py +0 -331
- onesecondtrader-0.43.0/src/onesecondtrader/core/strategies/examples.py +0 -47
- onesecondtrader-0.43.0/src/onesecondtrader/dashboard/__init__.py +0 -3
- onesecondtrader-0.43.0/src/onesecondtrader/dashboard/app.py +0 -2972
- onesecondtrader-0.43.0/src/onesecondtrader/dashboard/registry.py +0 -100
- onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/__init__.py +0 -7
- onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/orchestrator.py +0 -105
- onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/recorder.py +0 -199
- onesecondtrader-0.43.0/src/onesecondtrader/orchestrator/schema.sql +0 -212
- onesecondtrader-0.43.0/src/onesecondtrader/secmaster/__init__.py +0 -6
- onesecondtrader-0.43.0/src/onesecondtrader/secmaster/schema.sql +0 -740
- onesecondtrader-0.43.0/src/onesecondtrader/secmaster/utils.py +0 -737
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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,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)
|