onesecondtrader 0.41.0__py3-none-any.whl → 0.44.0__py3-none-any.whl
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/__init__.py +0 -58
- onesecondtrader/models/__init__.py +11 -0
- onesecondtrader/models/bar_fields.py +23 -0
- onesecondtrader/models/bar_period.py +21 -0
- onesecondtrader/models/order_types.py +21 -0
- onesecondtrader/models/trade_sides.py +20 -0
- {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.44.0.dist-info}/METADATA +2 -2
- onesecondtrader-0.44.0.dist-info/RECORD +10 -0
- onesecondtrader/connectors/__init__.py +0 -3
- onesecondtrader/connectors/brokers/__init__.py +0 -4
- onesecondtrader/connectors/brokers/ib.py +0 -418
- onesecondtrader/connectors/brokers/simulated.py +0 -349
- onesecondtrader/connectors/datafeeds/__init__.py +0 -4
- onesecondtrader/connectors/datafeeds/ib.py +0 -286
- onesecondtrader/connectors/datafeeds/simulated.py +0 -167
- onesecondtrader/connectors/gateways/__init__.py +0 -3
- onesecondtrader/connectors/gateways/ib.py +0 -314
- onesecondtrader/core/__init__.py +0 -7
- onesecondtrader/core/brokers/__init__.py +0 -3
- onesecondtrader/core/brokers/base.py +0 -46
- onesecondtrader/core/datafeeds/__init__.py +0 -3
- onesecondtrader/core/datafeeds/base.py +0 -32
- onesecondtrader/core/events/__init__.py +0 -33
- onesecondtrader/core/events/bases.py +0 -29
- onesecondtrader/core/events/market.py +0 -22
- onesecondtrader/core/events/requests.py +0 -31
- onesecondtrader/core/events/responses.py +0 -54
- onesecondtrader/core/indicators/__init__.py +0 -13
- onesecondtrader/core/indicators/averages.py +0 -56
- onesecondtrader/core/indicators/bar.py +0 -47
- onesecondtrader/core/indicators/base.py +0 -60
- onesecondtrader/core/messaging/__init__.py +0 -7
- onesecondtrader/core/messaging/eventbus.py +0 -47
- onesecondtrader/core/messaging/subscriber.py +0 -69
- onesecondtrader/core/models/__init__.py +0 -14
- onesecondtrader/core/models/data.py +0 -18
- onesecondtrader/core/models/orders.py +0 -15
- onesecondtrader/core/models/params.py +0 -21
- onesecondtrader/core/models/records.py +0 -32
- onesecondtrader/core/strategies/__init__.py +0 -7
- onesecondtrader/core/strategies/base.py +0 -324
- onesecondtrader/core/strategies/examples.py +0 -43
- onesecondtrader/dashboard/__init__.py +0 -3
- onesecondtrader/dashboard/app.py +0 -1677
- onesecondtrader/dashboard/registry.py +0 -100
- onesecondtrader/orchestrator/__init__.py +0 -7
- onesecondtrader/orchestrator/orchestrator.py +0 -105
- onesecondtrader/orchestrator/recorder.py +0 -196
- onesecondtrader/orchestrator/schema.sql +0 -208
- onesecondtrader/secmaster/__init__.py +0 -6
- onesecondtrader/secmaster/schema.sql +0 -740
- onesecondtrader/secmaster/utils.py +0 -737
- onesecondtrader-0.41.0.dist-info/RECORD +0 -49
- {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.44.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.41.0.dist-info → onesecondtrader-0.44.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sqlite3
|
|
5
|
-
import threading
|
|
6
|
-
|
|
7
|
-
import pandas as pd
|
|
8
|
-
|
|
9
|
-
from onesecondtrader.core import events, messaging, models
|
|
10
|
-
from onesecondtrader.core.datafeeds import DatafeedBase
|
|
11
|
-
|
|
12
|
-
_RTYPE_MAP = {
|
|
13
|
-
models.BarPeriod.SECOND: 32,
|
|
14
|
-
models.BarPeriod.MINUTE: 33,
|
|
15
|
-
models.BarPeriod.HOUR: 34,
|
|
16
|
-
models.BarPeriod.DAY: 35,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
_PRICE_SCALE = 1e9
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class SimulatedDatafeed(DatafeedBase):
|
|
23
|
-
db_path: str = ""
|
|
24
|
-
start_ts: int | None = None
|
|
25
|
-
end_ts: int | None = None
|
|
26
|
-
|
|
27
|
-
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
28
|
-
super().__init__(event_bus)
|
|
29
|
-
self._db_path = self.db_path or os.environ.get(
|
|
30
|
-
"SECMASTER_DB_PATH", "secmaster.db"
|
|
31
|
-
)
|
|
32
|
-
self._connected = False
|
|
33
|
-
self._subscriptions: set[tuple[str, models.BarPeriod]] = set()
|
|
34
|
-
self._connection: sqlite3.Connection | None = None
|
|
35
|
-
self._thread: threading.Thread | None = None
|
|
36
|
-
self._stop_event = threading.Event()
|
|
37
|
-
|
|
38
|
-
def connect(self) -> None:
|
|
39
|
-
if self._connected:
|
|
40
|
-
return
|
|
41
|
-
self._connection = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
42
|
-
self._connected = True
|
|
43
|
-
|
|
44
|
-
def disconnect(self) -> None:
|
|
45
|
-
if not self._connected:
|
|
46
|
-
return
|
|
47
|
-
self._stop_event.set()
|
|
48
|
-
if self._thread and self._thread.is_alive():
|
|
49
|
-
self._thread.join()
|
|
50
|
-
if self._connection:
|
|
51
|
-
self._connection.close()
|
|
52
|
-
self._connection = None
|
|
53
|
-
self._connected = False
|
|
54
|
-
|
|
55
|
-
def subscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
56
|
-
self._subscriptions.add((symbol, bar_period))
|
|
57
|
-
|
|
58
|
-
def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
59
|
-
self._subscriptions.discard((symbol, bar_period))
|
|
60
|
-
|
|
61
|
-
def wait_until_complete(self) -> None:
|
|
62
|
-
if not self._subscriptions:
|
|
63
|
-
return
|
|
64
|
-
if self._thread is None or not self._thread.is_alive():
|
|
65
|
-
self._stop_event.clear()
|
|
66
|
-
self._thread = threading.Thread(
|
|
67
|
-
target=self._stream,
|
|
68
|
-
name=self.__class__.__name__,
|
|
69
|
-
daemon=False,
|
|
70
|
-
)
|
|
71
|
-
self._thread.start()
|
|
72
|
-
self._thread.join()
|
|
73
|
-
|
|
74
|
-
def _stream(self) -> None:
|
|
75
|
-
if not self._connection:
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
subscriptions = list(self._subscriptions)
|
|
79
|
-
if not subscriptions:
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
symbol_rtype_pairs = [
|
|
83
|
-
(symbol, _RTYPE_MAP[bar_period]) for symbol, bar_period in subscriptions
|
|
84
|
-
]
|
|
85
|
-
rtype_by_symbol = {symbol: rtype for symbol, rtype in symbol_rtype_pairs}
|
|
86
|
-
bar_period_by_symbol = {symbol: bp for symbol, bp in subscriptions}
|
|
87
|
-
symbols = list(rtype_by_symbol.keys())
|
|
88
|
-
|
|
89
|
-
cursor = self._connection.cursor()
|
|
90
|
-
|
|
91
|
-
instrument_ids = self._resolve_instrument_ids(cursor, symbols)
|
|
92
|
-
if not instrument_ids:
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
instrument_to_symbol = {v: k for k, v in instrument_ids.items()}
|
|
96
|
-
id_list = list(instrument_ids.values())
|
|
97
|
-
rtype_list = list(set(rtype_by_symbol.values()))
|
|
98
|
-
|
|
99
|
-
placeholders_ids = ",".join("?" * len(id_list))
|
|
100
|
-
placeholders_rtypes = ",".join("?" * len(rtype_list))
|
|
101
|
-
|
|
102
|
-
date_filter = ""
|
|
103
|
-
params = id_list + rtype_list
|
|
104
|
-
if self.start_ts is not None:
|
|
105
|
-
date_filter += " AND ts_event >= ?"
|
|
106
|
-
params.append(self.start_ts)
|
|
107
|
-
if self.end_ts is not None:
|
|
108
|
-
date_filter += " AND ts_event <= ?"
|
|
109
|
-
params.append(self.end_ts)
|
|
110
|
-
|
|
111
|
-
query = f"""
|
|
112
|
-
SELECT instrument_id, rtype, ts_event, open, high, low, close, volume
|
|
113
|
-
FROM ohlcv
|
|
114
|
-
WHERE instrument_id IN ({placeholders_ids})
|
|
115
|
-
AND rtype IN ({placeholders_rtypes})
|
|
116
|
-
{date_filter}
|
|
117
|
-
ORDER BY ts_event
|
|
118
|
-
"""
|
|
119
|
-
|
|
120
|
-
cursor.execute(query, params)
|
|
121
|
-
|
|
122
|
-
while True:
|
|
123
|
-
if self._stop_event.is_set():
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
row = cursor.fetchone()
|
|
127
|
-
if row is None:
|
|
128
|
-
break
|
|
129
|
-
|
|
130
|
-
instrument_id, rtype, ts_event, open_, high, low, close, volume = row
|
|
131
|
-
symbol = instrument_to_symbol.get(instrument_id)
|
|
132
|
-
if symbol is None:
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
|
-
expected_rtype = rtype_by_symbol.get(symbol)
|
|
136
|
-
if rtype != expected_rtype:
|
|
137
|
-
continue
|
|
138
|
-
|
|
139
|
-
bar_period = bar_period_by_symbol[symbol]
|
|
140
|
-
|
|
141
|
-
self._publish(
|
|
142
|
-
events.BarReceived(
|
|
143
|
-
ts_event=pd.Timestamp(ts_event, unit="ns", tz="UTC"),
|
|
144
|
-
symbol=symbol,
|
|
145
|
-
bar_period=bar_period,
|
|
146
|
-
open=open_ / _PRICE_SCALE,
|
|
147
|
-
high=high / _PRICE_SCALE,
|
|
148
|
-
low=low / _PRICE_SCALE,
|
|
149
|
-
close=close / _PRICE_SCALE,
|
|
150
|
-
volume=volume,
|
|
151
|
-
)
|
|
152
|
-
)
|
|
153
|
-
self._event_bus.wait_until_system_idle()
|
|
154
|
-
|
|
155
|
-
def _resolve_instrument_ids(
|
|
156
|
-
self, cursor: sqlite3.Cursor, symbols: list[str]
|
|
157
|
-
) -> dict[str, int]:
|
|
158
|
-
placeholders = ",".join("?" * len(symbols))
|
|
159
|
-
query = f"""
|
|
160
|
-
SELECT symbol, instrument_id
|
|
161
|
-
FROM symbology
|
|
162
|
-
WHERE symbol IN ({placeholders})
|
|
163
|
-
GROUP BY symbol
|
|
164
|
-
HAVING start_date = MAX(start_date)
|
|
165
|
-
"""
|
|
166
|
-
cursor.execute(query, symbols)
|
|
167
|
-
return {row[0]: row[1] for row in cursor.fetchall()}
|
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import sqlite3
|
|
7
|
-
import threading
|
|
8
|
-
import time
|
|
9
|
-
|
|
10
|
-
from ib_async import Contract, Forex, Future, IB, Option, Stock
|
|
11
|
-
|
|
12
|
-
_logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
_INSTRUMENT_CLASS_MAP = {
|
|
15
|
-
"K": "STK",
|
|
16
|
-
"F": "FUT",
|
|
17
|
-
"C": "OPT",
|
|
18
|
-
"P": "OPT",
|
|
19
|
-
"X": "CASH",
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def make_contract(
|
|
24
|
-
symbol: str, db_connection: sqlite3.Connection | None = None
|
|
25
|
-
) -> Contract:
|
|
26
|
-
"""
|
|
27
|
-
Resolve a symbol string to an IB Contract.
|
|
28
|
-
|
|
29
|
-
Resolution priority:
|
|
30
|
-
1. Explicit format with colons (e.g., ``"AAPL:STK:USD:SMART"``)
|
|
31
|
-
2. Secmaster database lookup (if db_connection provided)
|
|
32
|
-
3. Default: US stock on SMART routing
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
symbol: Symbol string, either simple (``"AAPL"``) or qualified
|
|
36
|
-
(``"AAPL:STK:USD:SMART"``).
|
|
37
|
-
db_connection: Optional SQLite connection to secmaster database.
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
An ib_async Contract object.
|
|
41
|
-
"""
|
|
42
|
-
if ":" in symbol:
|
|
43
|
-
return _parse_qualified_symbol(symbol)
|
|
44
|
-
|
|
45
|
-
if db_connection:
|
|
46
|
-
row = _query_instrument(symbol, db_connection)
|
|
47
|
-
if row:
|
|
48
|
-
return _row_to_contract(row)
|
|
49
|
-
|
|
50
|
-
return Stock(symbol, "SMART", "USD")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _parse_qualified_symbol(symbol: str) -> Contract:
|
|
54
|
-
parts = symbol.split(":")
|
|
55
|
-
sec_type = parts[1].upper() if len(parts) > 1 else "STK"
|
|
56
|
-
currency = parts[2] if len(parts) > 2 else "USD"
|
|
57
|
-
exchange = parts[3] if len(parts) > 3 else "SMART"
|
|
58
|
-
|
|
59
|
-
if sec_type == "STK":
|
|
60
|
-
return Stock(parts[0], exchange, currency)
|
|
61
|
-
elif sec_type == "CASH":
|
|
62
|
-
return Forex(pair=f"{parts[0]}{currency}")
|
|
63
|
-
elif sec_type == "FUT":
|
|
64
|
-
expiry = parts[4] if len(parts) > 4 else ""
|
|
65
|
-
return Future(parts[0], expiry, exchange, currency)
|
|
66
|
-
elif sec_type == "OPT":
|
|
67
|
-
expiry = parts[4] if len(parts) > 4 else ""
|
|
68
|
-
strike = float(parts[5]) if len(parts) > 5 else 0.0
|
|
69
|
-
right = parts[6] if len(parts) > 6 else "C"
|
|
70
|
-
return Option(parts[0], expiry, strike, right, exchange, currency)
|
|
71
|
-
else:
|
|
72
|
-
return Stock(parts[0], exchange, currency)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _query_instrument(symbol: str, db_connection: sqlite3.Connection) -> tuple | None:
|
|
76
|
-
cursor = db_connection.cursor()
|
|
77
|
-
cursor.execute(
|
|
78
|
-
"""
|
|
79
|
-
SELECT raw_symbol, instrument_class, currency, exchange,
|
|
80
|
-
expiration, strike_price
|
|
81
|
-
FROM instruments
|
|
82
|
-
WHERE raw_symbol = ?
|
|
83
|
-
ORDER BY expiration DESC
|
|
84
|
-
LIMIT 1
|
|
85
|
-
""",
|
|
86
|
-
(symbol,),
|
|
87
|
-
)
|
|
88
|
-
return cursor.fetchone()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _row_to_contract(row: tuple) -> Contract:
|
|
92
|
-
raw_symbol, instrument_class, currency, exchange, expiration, strike = row
|
|
93
|
-
currency = currency or "USD"
|
|
94
|
-
exchange = exchange or "SMART"
|
|
95
|
-
sec_type = _INSTRUMENT_CLASS_MAP.get(instrument_class, "STK")
|
|
96
|
-
|
|
97
|
-
if sec_type == "STK":
|
|
98
|
-
return Stock(raw_symbol, exchange, currency)
|
|
99
|
-
elif sec_type == "CASH":
|
|
100
|
-
return Forex(pair=f"{raw_symbol}{currency}")
|
|
101
|
-
elif sec_type == "FUT":
|
|
102
|
-
expiry = str(expiration) if expiration else ""
|
|
103
|
-
return Future(raw_symbol, expiry, exchange, currency)
|
|
104
|
-
elif sec_type == "OPT":
|
|
105
|
-
expiry = str(expiration) if expiration else ""
|
|
106
|
-
strike_val = float(strike) / 1e9 if strike else 0.0
|
|
107
|
-
right = "C" if instrument_class == "C" else "P"
|
|
108
|
-
return Option(raw_symbol, expiry, strike_val, right, exchange, currency)
|
|
109
|
-
else:
|
|
110
|
-
return Stock(raw_symbol, exchange, currency)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class IBGateway:
|
|
114
|
-
"""
|
|
115
|
-
Shared gateway for IB connectivity with automatic reconnection.
|
|
116
|
-
|
|
117
|
-
The gateway manages a single IB connection shared by multiple components
|
|
118
|
-
(datafeed, broker). It handles:
|
|
119
|
-
|
|
120
|
-
- Reference counting for connect/disconnect
|
|
121
|
-
- Automatic reconnection on disconnect
|
|
122
|
-
- Running an asyncio event loop in a background thread
|
|
123
|
-
|
|
124
|
-
Reconnection Behavior:
|
|
125
|
-
When the connection is lost, the gateway will automatically attempt
|
|
126
|
-
to reconnect after a configurable delay. Components that registered
|
|
127
|
-
reconnect callbacks will be notified after successful reconnection
|
|
128
|
-
so they can restore their subscriptions.
|
|
129
|
-
|
|
130
|
-
Environment Variables:
|
|
131
|
-
- ``IB_HOST``: Host address (default: 127.0.0.1)
|
|
132
|
-
- ``IB_PORT``: Port number (default: 4001 for gateway, 7497 for TWS)
|
|
133
|
-
- ``IB_CLIENT_ID``: Client ID (default: 1)
|
|
134
|
-
- ``IB_RECONNECT_DELAY``: Seconds to wait before reconnecting (default: 5)
|
|
135
|
-
- ``IB_MAX_RECONNECT_ATTEMPTS``: Max reconnect attempts, 0=infinite (default: 0)
|
|
136
|
-
"""
|
|
137
|
-
|
|
138
|
-
def __init__(self) -> None:
|
|
139
|
-
self._host = os.environ.get("IB_HOST", "127.0.0.1")
|
|
140
|
-
self._port = int(os.environ.get("IB_PORT", "4001"))
|
|
141
|
-
self._client_id = int(os.environ.get("IB_CLIENT_ID", "1"))
|
|
142
|
-
self._reconnect_delay = float(os.environ.get("IB_RECONNECT_DELAY", "5"))
|
|
143
|
-
self._max_reconnect_attempts = int(
|
|
144
|
-
os.environ.get("IB_MAX_RECONNECT_ATTEMPTS", "0")
|
|
145
|
-
)
|
|
146
|
-
self._ib = IB()
|
|
147
|
-
self._ref_count = 0
|
|
148
|
-
self._lock = threading.Lock()
|
|
149
|
-
self._loop: asyncio.AbstractEventLoop | None = None
|
|
150
|
-
self._loop_thread: threading.Thread | None = None
|
|
151
|
-
self._reconnect_callbacks: list = []
|
|
152
|
-
self._disconnect_callbacks: list = []
|
|
153
|
-
self._reconnecting = False
|
|
154
|
-
self._should_reconnect = True
|
|
155
|
-
self._reconnect_attempts = 0
|
|
156
|
-
|
|
157
|
-
def _run_loop(self) -> None:
|
|
158
|
-
self._loop = asyncio.new_event_loop()
|
|
159
|
-
asyncio.set_event_loop(self._loop)
|
|
160
|
-
self._loop.run_forever()
|
|
161
|
-
|
|
162
|
-
def run_coro(self, coro):
|
|
163
|
-
if self._loop is None:
|
|
164
|
-
raise RuntimeError("Gateway not connected")
|
|
165
|
-
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
166
|
-
return future.result()
|
|
167
|
-
|
|
168
|
-
def acquire(self) -> None:
|
|
169
|
-
with self._lock:
|
|
170
|
-
if self._ref_count == 0:
|
|
171
|
-
_logger.info("Connecting to IB at %s:%d", self._host, self._port)
|
|
172
|
-
self._should_reconnect = True
|
|
173
|
-
self._reconnect_attempts = 0
|
|
174
|
-
self._loop_thread = threading.Thread(
|
|
175
|
-
target=self._run_loop, daemon=True, name="IBGatewayLoop"
|
|
176
|
-
)
|
|
177
|
-
self._loop_thread.start()
|
|
178
|
-
while self._loop is None:
|
|
179
|
-
time.sleep(0.01)
|
|
180
|
-
self._ib.disconnectedEvent += self._on_disconnected
|
|
181
|
-
self.run_coro(
|
|
182
|
-
self._ib.connectAsync(
|
|
183
|
-
self._host, self._port, clientId=self._client_id
|
|
184
|
-
)
|
|
185
|
-
)
|
|
186
|
-
_logger.info("Connected to IB")
|
|
187
|
-
self._ref_count += 1
|
|
188
|
-
|
|
189
|
-
def release(self) -> None:
|
|
190
|
-
with self._lock:
|
|
191
|
-
self._ref_count -= 1
|
|
192
|
-
if self._ref_count == 0:
|
|
193
|
-
_logger.info("Disconnecting from IB")
|
|
194
|
-
self._should_reconnect = False
|
|
195
|
-
self._ib.disconnectedEvent -= self._on_disconnected
|
|
196
|
-
self._ib.disconnect()
|
|
197
|
-
if self._loop:
|
|
198
|
-
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
199
|
-
if self._loop_thread:
|
|
200
|
-
self._loop_thread.join()
|
|
201
|
-
self._loop = None
|
|
202
|
-
self._loop_thread = None
|
|
203
|
-
self._reconnect_callbacks.clear()
|
|
204
|
-
self._disconnect_callbacks.clear()
|
|
205
|
-
_logger.info("Disconnected from IB")
|
|
206
|
-
|
|
207
|
-
def register_reconnect_callback(self, callback) -> None:
|
|
208
|
-
"""
|
|
209
|
-
Register a callback to be called after successful reconnection.
|
|
210
|
-
|
|
211
|
-
The callback should restore any subscriptions or state that was lost
|
|
212
|
-
during the disconnect.
|
|
213
|
-
"""
|
|
214
|
-
if callback not in self._reconnect_callbacks:
|
|
215
|
-
self._reconnect_callbacks.append(callback)
|
|
216
|
-
|
|
217
|
-
def unregister_reconnect_callback(self, callback) -> None:
|
|
218
|
-
if callback in self._reconnect_callbacks:
|
|
219
|
-
self._reconnect_callbacks.remove(callback)
|
|
220
|
-
|
|
221
|
-
def register_disconnect_callback(self, callback) -> None:
|
|
222
|
-
"""
|
|
223
|
-
Register a callback to be called when disconnection is detected.
|
|
224
|
-
|
|
225
|
-
The callback can be used to pause operations or notify the system.
|
|
226
|
-
"""
|
|
227
|
-
if callback not in self._disconnect_callbacks:
|
|
228
|
-
self._disconnect_callbacks.append(callback)
|
|
229
|
-
|
|
230
|
-
def unregister_disconnect_callback(self, callback) -> None:
|
|
231
|
-
if callback in self._disconnect_callbacks:
|
|
232
|
-
self._disconnect_callbacks.remove(callback)
|
|
233
|
-
|
|
234
|
-
def _on_disconnected(self) -> None:
|
|
235
|
-
if not self._should_reconnect or self._reconnecting:
|
|
236
|
-
return
|
|
237
|
-
|
|
238
|
-
_logger.warning("IB connection lost")
|
|
239
|
-
for callback in self._disconnect_callbacks:
|
|
240
|
-
try:
|
|
241
|
-
callback()
|
|
242
|
-
except Exception:
|
|
243
|
-
_logger.exception("Error in disconnect callback")
|
|
244
|
-
|
|
245
|
-
if self._loop:
|
|
246
|
-
asyncio.run_coroutine_threadsafe(self._reconnect_async(), self._loop)
|
|
247
|
-
|
|
248
|
-
async def _reconnect_async(self) -> None:
|
|
249
|
-
if self._reconnecting:
|
|
250
|
-
return
|
|
251
|
-
self._reconnecting = True
|
|
252
|
-
|
|
253
|
-
while self._should_reconnect:
|
|
254
|
-
self._reconnect_attempts += 1
|
|
255
|
-
if (
|
|
256
|
-
self._max_reconnect_attempts > 0
|
|
257
|
-
and self._reconnect_attempts > self._max_reconnect_attempts
|
|
258
|
-
):
|
|
259
|
-
_logger.error(
|
|
260
|
-
"Max reconnect attempts (%d) reached, giving up",
|
|
261
|
-
self._max_reconnect_attempts,
|
|
262
|
-
)
|
|
263
|
-
self._reconnecting = False
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
_logger.info(
|
|
267
|
-
"Reconnecting to IB (attempt %d) in %.1fs",
|
|
268
|
-
self._reconnect_attempts,
|
|
269
|
-
self._reconnect_delay,
|
|
270
|
-
)
|
|
271
|
-
await asyncio.sleep(self._reconnect_delay)
|
|
272
|
-
|
|
273
|
-
if not self._should_reconnect:
|
|
274
|
-
break
|
|
275
|
-
|
|
276
|
-
try:
|
|
277
|
-
await self._ib.connectAsync(
|
|
278
|
-
self._host, self._port, clientId=self._client_id
|
|
279
|
-
)
|
|
280
|
-
_logger.info("Reconnected to IB")
|
|
281
|
-
self._reconnect_attempts = 0
|
|
282
|
-
self._reconnecting = False
|
|
283
|
-
|
|
284
|
-
for callback in self._reconnect_callbacks:
|
|
285
|
-
try:
|
|
286
|
-
callback()
|
|
287
|
-
except Exception:
|
|
288
|
-
_logger.exception("Error in reconnect callback")
|
|
289
|
-
return
|
|
290
|
-
except Exception as e:
|
|
291
|
-
_logger.warning("Reconnect failed: %s", e)
|
|
292
|
-
continue
|
|
293
|
-
|
|
294
|
-
self._reconnecting = False
|
|
295
|
-
|
|
296
|
-
@property
|
|
297
|
-
def ib(self) -> IB:
|
|
298
|
-
return self._ib
|
|
299
|
-
|
|
300
|
-
@property
|
|
301
|
-
def is_connected(self) -> bool:
|
|
302
|
-
return self._ib.isConnected()
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
_gateway: IBGateway | None = None
|
|
306
|
-
_gateway_lock = threading.Lock()
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _get_gateway() -> IBGateway:
|
|
310
|
-
global _gateway
|
|
311
|
-
with _gateway_lock:
|
|
312
|
-
if _gateway is None:
|
|
313
|
-
_gateway = IBGateway()
|
|
314
|
-
return _gateway
|
onesecondtrader/core/__init__.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
from onesecondtrader.core import brokers as brokers
|
|
2
|
-
from onesecondtrader.core import datafeeds as datafeeds
|
|
3
|
-
from onesecondtrader.core import events as events
|
|
4
|
-
from onesecondtrader.core import indicators as indicators
|
|
5
|
-
from onesecondtrader.core import messaging as messaging
|
|
6
|
-
from onesecondtrader.core import models as models
|
|
7
|
-
from onesecondtrader.core import strategies as strategies
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
|
|
3
|
-
from onesecondtrader.core import events, messaging
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class BrokerBase(messaging.Subscriber):
|
|
7
|
-
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
8
|
-
super().__init__(event_bus)
|
|
9
|
-
self._subscribe(
|
|
10
|
-
events.requests.OrderSubmission,
|
|
11
|
-
events.requests.OrderCancellation,
|
|
12
|
-
events.requests.OrderModification,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
@abc.abstractmethod
|
|
16
|
-
def connect(self) -> None:
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
def disconnect(self) -> None:
|
|
20
|
-
self.shutdown()
|
|
21
|
-
|
|
22
|
-
def _on_event(self, event: events.bases.EventBase) -> None:
|
|
23
|
-
match event:
|
|
24
|
-
case events.requests.OrderSubmission() as submit_order:
|
|
25
|
-
self._on_submit_order(submit_order)
|
|
26
|
-
case events.requests.OrderCancellation() as cancel_order:
|
|
27
|
-
self._on_cancel_order(cancel_order)
|
|
28
|
-
case events.requests.OrderModification() as modify_order:
|
|
29
|
-
self._on_modify_order(modify_order)
|
|
30
|
-
case _:
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
@abc.abstractmethod
|
|
34
|
-
def _on_submit_order(self, event: events.requests.OrderSubmission) -> None:
|
|
35
|
-
pass
|
|
36
|
-
|
|
37
|
-
@abc.abstractmethod
|
|
38
|
-
def _on_cancel_order(self, event: events.requests.OrderCancellation) -> None:
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
@abc.abstractmethod
|
|
42
|
-
def _on_modify_order(self, event: events.requests.OrderModification) -> None:
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
def _respond(self, response_event: events.bases.BrokerResponseEvent) -> None:
|
|
46
|
-
self._publish(response_event)
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import abc
|
|
4
|
-
|
|
5
|
-
from onesecondtrader.core import events, messaging, models
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class DatafeedBase(abc.ABC):
|
|
9
|
-
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
10
|
-
self._event_bus = event_bus
|
|
11
|
-
|
|
12
|
-
def _publish(self, event: events.EventBase) -> None:
|
|
13
|
-
self._event_bus.publish(event)
|
|
14
|
-
|
|
15
|
-
@abc.abstractmethod
|
|
16
|
-
def connect(self) -> None:
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
@abc.abstractmethod
|
|
20
|
-
def disconnect(self) -> None:
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
@abc.abstractmethod
|
|
24
|
-
def subscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
@abc.abstractmethod
|
|
28
|
-
def unsubscribe(self, symbol: str, bar_period: models.BarPeriod) -> None:
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
def wait_until_complete(self) -> None:
|
|
32
|
-
pass
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
__all__ = [
|
|
2
|
-
"EventBase",
|
|
3
|
-
"MarketEvent",
|
|
4
|
-
"BrokerRequestEvent",
|
|
5
|
-
"BrokerResponseEvent",
|
|
6
|
-
"BarReceived",
|
|
7
|
-
"BarProcessed",
|
|
8
|
-
"OrderSubmission",
|
|
9
|
-
"OrderModification",
|
|
10
|
-
"OrderCancellation",
|
|
11
|
-
"OrderSubmissionAccepted",
|
|
12
|
-
"OrderSubmissionRejected",
|
|
13
|
-
"OrderModificationAccepted",
|
|
14
|
-
"OrderModificationRejected",
|
|
15
|
-
"OrderCancellationAccepted",
|
|
16
|
-
"OrderCancellationRejected",
|
|
17
|
-
"OrderFilled",
|
|
18
|
-
"OrderExpired",
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
from .bases import EventBase, MarketEvent, BrokerRequestEvent, BrokerResponseEvent
|
|
22
|
-
from .market import BarReceived, BarProcessed
|
|
23
|
-
from .requests import OrderSubmission, OrderModification, OrderCancellation
|
|
24
|
-
from .responses import (
|
|
25
|
-
OrderSubmissionAccepted,
|
|
26
|
-
OrderSubmissionRejected,
|
|
27
|
-
OrderModificationAccepted,
|
|
28
|
-
OrderModificationRejected,
|
|
29
|
-
OrderCancellationAccepted,
|
|
30
|
-
OrderCancellationRejected,
|
|
31
|
-
OrderFilled,
|
|
32
|
-
OrderExpired,
|
|
33
|
-
)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import dataclasses
|
|
4
|
-
import pandas as pd
|
|
5
|
-
import uuid
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
9
|
-
class EventBase:
|
|
10
|
-
ts_event: pd.Timestamp
|
|
11
|
-
ts_created: pd.Timestamp = dataclasses.field(
|
|
12
|
-
default_factory=lambda: pd.Timestamp.now(tz="UTC")
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
17
|
-
class MarketEvent(EventBase):
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
22
|
-
class BrokerRequestEvent(EventBase):
|
|
23
|
-
system_order_id: uuid.UUID
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
27
|
-
class BrokerResponseEvent(EventBase):
|
|
28
|
-
ts_broker: pd.Timestamp
|
|
29
|
-
associated_order_id: uuid.UUID
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import dataclasses
|
|
4
|
-
|
|
5
|
-
from onesecondtrader.core import models
|
|
6
|
-
from . import bases
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
10
|
-
class BarReceived(bases.MarketEvent):
|
|
11
|
-
symbol: str
|
|
12
|
-
bar_period: models.data.BarPeriod
|
|
13
|
-
open: float
|
|
14
|
-
high: float
|
|
15
|
-
low: float
|
|
16
|
-
close: float
|
|
17
|
-
volume: int | None = None
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
21
|
-
class BarProcessed(BarReceived):
|
|
22
|
-
indicators: dict[str, float] = dataclasses.field(default_factory=dict)
|