onesecondtrader 0.39.0__py3-none-any.whl → 0.41.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 +8 -3
- onesecondtrader/connectors/__init__.py +1 -0
- onesecondtrader/connectors/brokers/__init__.py +2 -1
- onesecondtrader/connectors/brokers/ib.py +418 -0
- onesecondtrader/connectors/brokers/simulated.py +3 -0
- onesecondtrader/connectors/datafeeds/__init__.py +2 -2
- onesecondtrader/connectors/datafeeds/ib.py +286 -0
- onesecondtrader/connectors/datafeeds/simulated.py +141 -73
- onesecondtrader/connectors/gateways/__init__.py +3 -0
- onesecondtrader/connectors/gateways/ib.py +314 -0
- onesecondtrader/core/__init__.py +1 -0
- onesecondtrader/core/brokers/base.py +7 -0
- onesecondtrader/core/datafeeds/__init__.py +3 -0
- onesecondtrader/core/datafeeds/base.py +32 -0
- onesecondtrader/core/models/__init__.py +2 -0
- onesecondtrader/core/models/params.py +21 -0
- onesecondtrader/core/strategies/base.py +9 -3
- onesecondtrader/core/strategies/examples.py +15 -7
- onesecondtrader/dashboard/__init__.py +3 -0
- onesecondtrader/dashboard/app.py +1677 -0
- onesecondtrader/dashboard/registry.py +100 -0
- onesecondtrader/orchestrator/__init__.py +7 -0
- onesecondtrader/orchestrator/orchestrator.py +105 -0
- onesecondtrader/orchestrator/recorder.py +196 -0
- onesecondtrader/orchestrator/schema.sql +208 -0
- onesecondtrader/secmaster/schema.sql +48 -0
- onesecondtrader/secmaster/utils.py +90 -0
- {onesecondtrader-0.39.0.dist-info → onesecondtrader-0.41.0.dist-info}/METADATA +4 -1
- onesecondtrader-0.41.0.dist-info/RECORD +49 -0
- onesecondtrader/connectors/datafeeds/base.py +0 -19
- onesecondtrader-0.39.0.dist-info/RECORD +0 -37
- {onesecondtrader-0.39.0.dist-info → onesecondtrader-0.41.0.dist-info}/WHEEL +0 -0
- {onesecondtrader-0.39.0.dist-info → onesecondtrader-0.41.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
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
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from onesecondtrader.core import brokers as brokers
|
|
2
|
+
from onesecondtrader.core import datafeeds as datafeeds
|
|
2
3
|
from onesecondtrader.core import events as events
|
|
3
4
|
from onesecondtrader.core import indicators as indicators
|
|
4
5
|
from onesecondtrader.core import messaging as messaging
|
|
@@ -12,6 +12,13 @@ class BrokerBase(messaging.Subscriber):
|
|
|
12
12
|
events.requests.OrderModification,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
+
@abc.abstractmethod
|
|
16
|
+
def connect(self) -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def disconnect(self) -> None:
|
|
20
|
+
self.shutdown()
|
|
21
|
+
|
|
15
22
|
def _on_event(self, event: events.bases.EventBase) -> None:
|
|
16
23
|
match event:
|
|
17
24
|
case events.requests.OrderSubmission() as submit_order:
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclasses.dataclass
|
|
8
|
+
class ParamSpec:
|
|
9
|
+
default: int | float | str | bool | enum.Enum
|
|
10
|
+
min: int | float | None = None
|
|
11
|
+
max: int | float | None = None
|
|
12
|
+
step: int | float | None = None
|
|
13
|
+
choices: list | None = None
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def resolved_choices(self) -> list | None:
|
|
17
|
+
if self.choices is not None:
|
|
18
|
+
return self.choices
|
|
19
|
+
if isinstance(self.default, enum.Enum):
|
|
20
|
+
return list(type(self.default))
|
|
21
|
+
return None
|
|
@@ -10,11 +10,17 @@ from onesecondtrader.core import events, indicators, messaging, models
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
13
|
+
name: str = ""
|
|
13
14
|
symbols: list[str] = []
|
|
14
|
-
|
|
15
|
+
parameters: dict[str, models.ParamSpec] = {}
|
|
15
16
|
|
|
16
|
-
def __init__(self, event_bus: messaging.EventBus) -> None:
|
|
17
|
+
def __init__(self, event_bus: messaging.EventBus, **overrides) -> None:
|
|
17
18
|
super().__init__(event_bus)
|
|
19
|
+
|
|
20
|
+
for name, spec in self.parameters.items():
|
|
21
|
+
value = overrides.get(name, spec.default)
|
|
22
|
+
setattr(self, name, value)
|
|
23
|
+
|
|
18
24
|
self._subscribe(
|
|
19
25
|
events.BarReceived,
|
|
20
26
|
events.OrderSubmissionAccepted,
|
|
@@ -179,7 +185,7 @@ class StrategyBase(messaging.Subscriber, abc.ABC):
|
|
|
179
185
|
def _on_bar_received(self, event: events.BarReceived) -> None:
|
|
180
186
|
if event.symbol not in self.symbols:
|
|
181
187
|
return
|
|
182
|
-
if event.bar_period != self.bar_period:
|
|
188
|
+
if event.bar_period != self.bar_period: # type: ignore[attr-defined]
|
|
183
189
|
return
|
|
184
190
|
|
|
185
191
|
self._current_symbol = event.symbol
|
|
@@ -3,16 +3,20 @@ from .base import StrategyBase
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class SMACrossover(StrategyBase):
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
name = "SMA Crossover"
|
|
7
|
+
parameters = {
|
|
8
|
+
"bar_period": models.ParamSpec(default=models.BarPeriod.SECOND),
|
|
9
|
+
"fast_period": models.ParamSpec(default=20, min=5, max=100, step=1),
|
|
10
|
+
"slow_period": models.ParamSpec(default=100, min=10, max=500, step=1),
|
|
11
|
+
"quantity": models.ParamSpec(default=1.0, min=0.1, max=100.0, step=0.1),
|
|
12
|
+
}
|
|
9
13
|
|
|
10
14
|
def setup(self) -> None:
|
|
11
15
|
self.fast_sma = self.add_indicator(
|
|
12
|
-
indicators.SimpleMovingAverage(period=self.fast_period)
|
|
16
|
+
indicators.SimpleMovingAverage(period=self.fast_period) # type: ignore[attr-defined]
|
|
13
17
|
)
|
|
14
18
|
self.slow_sma = self.add_indicator(
|
|
15
|
-
indicators.SimpleMovingAverage(period=self.slow_period)
|
|
19
|
+
indicators.SimpleMovingAverage(period=self.slow_period) # type: ignore[attr-defined]
|
|
16
20
|
)
|
|
17
21
|
|
|
18
22
|
def on_bar(self, event: events.BarReceived) -> None:
|
|
@@ -22,7 +26,9 @@ class SMACrossover(StrategyBase):
|
|
|
22
26
|
and self.position <= 0
|
|
23
27
|
):
|
|
24
28
|
self.submit_order(
|
|
25
|
-
models.OrderType.MARKET,
|
|
29
|
+
models.OrderType.MARKET,
|
|
30
|
+
models.OrderSide.BUY,
|
|
31
|
+
self.quantity, # type: ignore[attr-defined]
|
|
26
32
|
)
|
|
27
33
|
|
|
28
34
|
if (
|
|
@@ -31,5 +37,7 @@ class SMACrossover(StrategyBase):
|
|
|
31
37
|
and self.position >= 0
|
|
32
38
|
):
|
|
33
39
|
self.submit_order(
|
|
34
|
-
models.OrderType.MARKET,
|
|
40
|
+
models.OrderType.MARKET,
|
|
41
|
+
models.OrderSide.SELL,
|
|
42
|
+
self.quantity, # type: ignore[attr-defined]
|
|
35
43
|
)
|