onesecondtrader 0.22.0__tar.gz → 0.32.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 (31) hide show
  1. {onesecondtrader-0.22.0 → onesecondtrader-0.32.0}/PKG-INFO +1 -1
  2. {onesecondtrader-0.22.0 → onesecondtrader-0.32.0}/pyproject.toml +1 -1
  3. onesecondtrader-0.32.0/src/onesecondtrader/__init__.py +5 -0
  4. onesecondtrader-0.32.0/src/onesecondtrader/brokers/__init__.py +7 -0
  5. onesecondtrader-0.32.0/src/onesecondtrader/brokers/base.py +39 -0
  6. onesecondtrader-0.32.0/src/onesecondtrader/brokers/simulated.py +346 -0
  7. onesecondtrader-0.32.0/src/onesecondtrader/events/__init__.py +33 -0
  8. onesecondtrader-0.32.0/src/onesecondtrader/events/bases.py +29 -0
  9. onesecondtrader-0.32.0/src/onesecondtrader/events/market.py +22 -0
  10. onesecondtrader-0.32.0/src/onesecondtrader/events/requests.py +31 -0
  11. onesecondtrader-0.32.0/src/onesecondtrader/events/responses.py +54 -0
  12. onesecondtrader-0.32.0/src/onesecondtrader/indicators/__init__.py +13 -0
  13. onesecondtrader-0.32.0/src/onesecondtrader/indicators/averages.py +56 -0
  14. onesecondtrader-0.32.0/src/onesecondtrader/indicators/bar.py +47 -0
  15. onesecondtrader-0.32.0/src/onesecondtrader/indicators/base.py +50 -0
  16. onesecondtrader-0.32.0/src/onesecondtrader/messaging/__init__.py +7 -0
  17. onesecondtrader-0.32.0/src/onesecondtrader/messaging/eventbus.py +47 -0
  18. onesecondtrader-0.32.0/src/onesecondtrader/messaging/subscriber.py +69 -0
  19. onesecondtrader-0.32.0/src/onesecondtrader/models/__init__.py +12 -0
  20. onesecondtrader-0.32.0/src/onesecondtrader/models/data.py +18 -0
  21. onesecondtrader-0.32.0/src/onesecondtrader/models/orders.py +15 -0
  22. onesecondtrader-0.32.0/src/onesecondtrader/models/records.py +32 -0
  23. onesecondtrader-0.32.0/src/onesecondtrader/strategies/__init__.py +5 -0
  24. onesecondtrader-0.32.0/src/onesecondtrader/strategies/base.py +318 -0
  25. onesecondtrader-0.22.0/src/onesecondtrader/__init__.py +0 -0
  26. onesecondtrader-0.22.0/src/onesecondtrader/brokers.py +0 -92
  27. onesecondtrader-0.22.0/src/onesecondtrader/core.py +0 -260
  28. onesecondtrader-0.22.0/src/onesecondtrader/datafeeds.py +0 -173
  29. onesecondtrader-0.22.0/src/onesecondtrader/indicators.py +0 -106
  30. {onesecondtrader-0.22.0 → onesecondtrader-0.32.0}/LICENSE +0 -0
  31. {onesecondtrader-0.22.0 → onesecondtrader-0.32.0}/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.22.0
3
+ Version: 0.32.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "onesecondtrader"
3
- version = "0.22.0"
3
+ version = "0.32.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"}
@@ -0,0 +1,5 @@
1
+ from onesecondtrader import brokers as brokers
2
+ from onesecondtrader import events as events
3
+ from onesecondtrader import indicators as indicators
4
+ from onesecondtrader import messaging as messaging
5
+ from onesecondtrader import models as models
@@ -0,0 +1,7 @@
1
+ __all__ = [
2
+ "BrokerBase",
3
+ "SimulatedBroker",
4
+ ]
5
+
6
+ from .base import BrokerBase
7
+ from .simulated import SimulatedBroker
@@ -0,0 +1,39 @@
1
+ import abc
2
+
3
+ from onesecondtrader 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
+ def _on_event(self, event: events.bases.EventBase) -> None:
16
+ match event:
17
+ case events.requests.OrderSubmission() as submit_order:
18
+ self._on_submit_order(submit_order)
19
+ case events.requests.OrderCancellation() as cancel_order:
20
+ self._on_cancel_order(cancel_order)
21
+ case events.requests.OrderModification() as modify_order:
22
+ self._on_modify_order(modify_order)
23
+ case _:
24
+ return
25
+
26
+ @abc.abstractmethod
27
+ def _on_submit_order(self, event: events.requests.OrderSubmission) -> None:
28
+ pass
29
+
30
+ @abc.abstractmethod
31
+ def _on_cancel_order(self, event: events.requests.OrderCancellation) -> None:
32
+ pass
33
+
34
+ @abc.abstractmethod
35
+ def _on_modify_order(self, event: events.requests.OrderModification) -> None:
36
+ pass
37
+
38
+ def _respond(self, response_event: events.bases.BrokerResponseEvent) -> None:
39
+ self._publish(response_event)
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import uuid
5
+
6
+ from onesecondtrader import events, messaging, models
7
+ from .base import BrokerBase
8
+
9
+
10
+ @dataclasses.dataclass
11
+ class _PendingOrder:
12
+ # Order state tracked by the broker, distinct from the OrderSubmission event
13
+ order_id: uuid.UUID
14
+ symbol: str
15
+ order_type: models.OrderType
16
+ side: models.OrderSide
17
+ quantity: float
18
+ limit_price: float | None = None
19
+ stop_price: float | None = None
20
+
21
+
22
+ class SimulatedBroker(BrokerBase):
23
+ commission_per_unit: float = 0.0
24
+ minimum_commission_per_order: float = 0.0
25
+
26
+ def __init__(self, event_bus: messaging.EventBus) -> None:
27
+ self._pending_market_orders: dict[uuid.UUID, _PendingOrder] = {}
28
+ self._pending_limit_orders: dict[uuid.UUID, _PendingOrder] = {}
29
+ self._pending_stop_orders: dict[uuid.UUID, _PendingOrder] = {}
30
+ self._pending_stop_limit_orders: dict[uuid.UUID, _PendingOrder] = {}
31
+
32
+ super().__init__(event_bus)
33
+ self._subscribe(events.BarReceived)
34
+
35
+ def _on_event(self, event: events.EventBase) -> None:
36
+ match event:
37
+ case events.BarReceived() as bar:
38
+ self._on_bar(bar)
39
+ case _:
40
+ super()._on_event(event)
41
+
42
+ def _on_bar(self, event: events.BarReceived) -> None:
43
+ self._process_market_orders(event)
44
+ self._process_stop_orders(event)
45
+ self._process_stop_limit_orders(event)
46
+ self._process_limit_orders(event)
47
+
48
+ def _process_market_orders(self, event: events.BarReceived) -> None:
49
+ for order_id, order in list(self._pending_market_orders.items()):
50
+ if order.symbol != event.symbol:
51
+ continue
52
+
53
+ self._publish(
54
+ events.OrderFilled(
55
+ ts_event=event.ts_event,
56
+ ts_broker=event.ts_event,
57
+ associated_order_id=order.order_id,
58
+ symbol=order.symbol,
59
+ side=order.side,
60
+ quantity_filled=order.quantity,
61
+ fill_price=event.open,
62
+ commission=max(
63
+ order.quantity * self.commission_per_unit,
64
+ self.minimum_commission_per_order,
65
+ ),
66
+ )
67
+ )
68
+ del self._pending_market_orders[order_id]
69
+
70
+ def _process_stop_orders(self, event: events.BarReceived) -> None:
71
+ for order_id, order in list(self._pending_stop_orders.items()):
72
+ if order.symbol != event.symbol:
73
+ continue
74
+
75
+ # This is for mypy, it has already been validated on submission
76
+ assert order.stop_price is not None
77
+
78
+ triggered = False
79
+ match order.side:
80
+ case models.OrderSide.BUY:
81
+ triggered = event.high >= order.stop_price
82
+ case models.OrderSide.SELL:
83
+ triggered = event.low <= order.stop_price
84
+
85
+ if not triggered:
86
+ continue
87
+
88
+ fill_price = 0.0
89
+ match order.side:
90
+ case models.OrderSide.BUY:
91
+ fill_price = max(order.stop_price, event.open)
92
+ case models.OrderSide.SELL:
93
+ fill_price = min(order.stop_price, event.open)
94
+
95
+ self._publish(
96
+ events.OrderFilled(
97
+ ts_event=event.ts_event,
98
+ ts_broker=event.ts_event,
99
+ associated_order_id=order.order_id,
100
+ symbol=order.symbol,
101
+ side=order.side,
102
+ quantity_filled=order.quantity,
103
+ fill_price=fill_price,
104
+ commission=max(
105
+ order.quantity * self.commission_per_unit,
106
+ self.minimum_commission_per_order,
107
+ ),
108
+ )
109
+ )
110
+ del self._pending_stop_orders[order_id]
111
+
112
+ def _process_stop_limit_orders(self, event: events.BarReceived) -> None:
113
+ for order_id, order in list(self._pending_stop_limit_orders.items()):
114
+ if order.symbol != event.symbol:
115
+ continue
116
+
117
+ # This is for mypy, it has already been validated on submission
118
+ assert order.stop_price is not None
119
+
120
+ triggered = False
121
+ match order.side:
122
+ case models.OrderSide.BUY:
123
+ triggered = event.high >= order.stop_price
124
+ case models.OrderSide.SELL:
125
+ triggered = event.low <= order.stop_price
126
+
127
+ if not triggered:
128
+ continue
129
+
130
+ limit_order = dataclasses.replace(order, order_type=models.OrderType.LIMIT)
131
+ self._pending_limit_orders[order_id] = limit_order
132
+ del self._pending_stop_limit_orders[order_id]
133
+
134
+ def _process_limit_orders(self, event: events.BarReceived) -> None:
135
+ for order_id, order in list(self._pending_limit_orders.items()):
136
+ if order.symbol != event.symbol:
137
+ continue
138
+
139
+ # This is for mypy, it has already been validated on submission
140
+ assert order.limit_price is not None
141
+
142
+ triggered = False
143
+ match order.side:
144
+ case models.OrderSide.BUY:
145
+ triggered = event.low <= order.limit_price
146
+ case models.OrderSide.SELL:
147
+ triggered = event.high >= order.limit_price
148
+
149
+ if not triggered:
150
+ continue
151
+
152
+ fill_price = 0.0
153
+ match order.side:
154
+ case models.OrderSide.BUY:
155
+ fill_price = min(order.limit_price, event.open)
156
+ case models.OrderSide.SELL:
157
+ fill_price = max(order.limit_price, event.open)
158
+
159
+ self._publish(
160
+ events.OrderFilled(
161
+ ts_event=event.ts_event,
162
+ ts_broker=event.ts_event,
163
+ associated_order_id=order.order_id,
164
+ symbol=order.symbol,
165
+ side=order.side,
166
+ quantity_filled=order.quantity,
167
+ fill_price=fill_price,
168
+ commission=max(
169
+ order.quantity * self.commission_per_unit,
170
+ self.minimum_commission_per_order,
171
+ ),
172
+ )
173
+ )
174
+ del self._pending_limit_orders[order_id]
175
+
176
+ def _reject_if_invalid_submission(self, event: events.OrderSubmission) -> bool:
177
+ is_invalid = event.quantity <= 0
178
+
179
+ match event.order_type:
180
+ case models.OrderType.LIMIT:
181
+ is_invalid = (
182
+ is_invalid or event.limit_price is None or event.limit_price <= 0
183
+ )
184
+ case models.OrderType.STOP:
185
+ is_invalid = (
186
+ is_invalid or event.stop_price is None or event.stop_price <= 0
187
+ )
188
+ case models.OrderType.STOP_LIMIT:
189
+ is_invalid = is_invalid or (
190
+ event.limit_price is None
191
+ or event.limit_price <= 0
192
+ or event.stop_price is None
193
+ or event.stop_price <= 0
194
+ )
195
+
196
+ if is_invalid:
197
+ # Use event timestamp to maintain simulated time consistency in backtesting
198
+ self._publish(
199
+ events.OrderSubmissionRejected(
200
+ ts_event=event.ts_event,
201
+ ts_broker=event.ts_event,
202
+ associated_order_id=event.system_order_id,
203
+ )
204
+ )
205
+
206
+ return is_invalid
207
+
208
+ def _on_submit_order(self, event: events.OrderSubmission) -> None:
209
+ if self._reject_if_invalid_submission(event):
210
+ return
211
+
212
+ order = _PendingOrder(
213
+ order_id=event.system_order_id,
214
+ symbol=event.symbol,
215
+ order_type=event.order_type,
216
+ side=event.side,
217
+ quantity=event.quantity,
218
+ limit_price=event.limit_price,
219
+ stop_price=event.stop_price,
220
+ )
221
+
222
+ match order.order_type:
223
+ case models.OrderType.MARKET:
224
+ self._pending_market_orders[order.order_id] = order
225
+ case models.OrderType.LIMIT:
226
+ self._pending_limit_orders[order.order_id] = order
227
+ case models.OrderType.STOP:
228
+ self._pending_stop_orders[order.order_id] = order
229
+ case models.OrderType.STOP_LIMIT:
230
+ self._pending_stop_limit_orders[order.order_id] = order
231
+
232
+ # Use event timestamp to maintain simulated time consistency in backtesting
233
+ self._publish(
234
+ events.OrderSubmissionAccepted(
235
+ ts_event=event.ts_event,
236
+ ts_broker=event.ts_event,
237
+ associated_order_id=order.order_id,
238
+ )
239
+ )
240
+
241
+ def _on_cancel_order(self, event: events.OrderCancellation) -> None:
242
+ order_id = event.system_order_id
243
+
244
+ removed = False
245
+ for pending_orders in (
246
+ self._pending_market_orders,
247
+ self._pending_limit_orders,
248
+ self._pending_stop_orders,
249
+ self._pending_stop_limit_orders,
250
+ ):
251
+ if order_id in pending_orders:
252
+ del pending_orders[order_id]
253
+ removed = True
254
+ break
255
+
256
+ # Use event timestamp to maintain simulated time consistency in backtesting
257
+ if removed:
258
+ self._publish(
259
+ events.OrderCancellationAccepted(
260
+ ts_event=event.ts_event,
261
+ ts_broker=event.ts_event,
262
+ associated_order_id=order_id,
263
+ )
264
+ )
265
+ else:
266
+ self._publish(
267
+ events.OrderCancellationRejected(
268
+ ts_event=event.ts_event,
269
+ ts_broker=event.ts_event,
270
+ associated_order_id=order_id,
271
+ )
272
+ )
273
+
274
+ def _reject_if_invalid_modification(self, event: events.OrderModification) -> bool:
275
+ is_invalid = (
276
+ (event.quantity is not None and event.quantity <= 0)
277
+ or (event.limit_price is not None and event.limit_price <= 0)
278
+ or (event.stop_price is not None and event.stop_price <= 0)
279
+ )
280
+
281
+ if is_invalid:
282
+ # Use event timestamp to maintain simulated time consistency in backtesting
283
+ self._publish(
284
+ events.OrderModificationRejected(
285
+ ts_event=event.ts_event,
286
+ ts_broker=event.ts_event,
287
+ associated_order_id=event.system_order_id,
288
+ )
289
+ )
290
+
291
+ return is_invalid
292
+
293
+ def _on_modify_order(self, event: events.OrderModification) -> None:
294
+ if self._reject_if_invalid_modification(event):
295
+ return
296
+
297
+ order_id = event.system_order_id
298
+
299
+ for pending_orders in (
300
+ self._pending_market_orders,
301
+ self._pending_limit_orders,
302
+ self._pending_stop_orders,
303
+ self._pending_stop_limit_orders,
304
+ ):
305
+ if order_id in pending_orders:
306
+ order = pending_orders[order_id]
307
+
308
+ new_quantity = (
309
+ event.quantity if event.quantity is not None else order.quantity
310
+ )
311
+ new_limit_price = (
312
+ event.limit_price
313
+ if event.limit_price is not None
314
+ else order.limit_price
315
+ )
316
+ new_stop_price = (
317
+ event.stop_price
318
+ if event.stop_price is not None
319
+ else order.stop_price
320
+ )
321
+
322
+ pending_orders[order_id] = dataclasses.replace(
323
+ order,
324
+ quantity=new_quantity,
325
+ limit_price=new_limit_price,
326
+ stop_price=new_stop_price,
327
+ )
328
+
329
+ # Use event timestamp to maintain simulated time consistency in backtesting
330
+ self._publish(
331
+ events.OrderModificationAccepted(
332
+ ts_event=event.ts_event,
333
+ ts_broker=event.ts_event,
334
+ associated_order_id=order_id,
335
+ )
336
+ )
337
+ return
338
+
339
+ # Use event timestamp to maintain simulated time consistency in backtesting
340
+ self._publish(
341
+ events.OrderModificationRejected(
342
+ ts_event=event.ts_event,
343
+ ts_broker=event.ts_event,
344
+ associated_order_id=order_id,
345
+ )
346
+ )
@@ -0,0 +1,33 @@
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
+ )
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+
5
+ from onesecondtrader 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)
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import uuid
5
+
6
+ from onesecondtrader import models
7
+ from . import bases
8
+
9
+
10
+ @dataclasses.dataclass(kw_only=True, frozen=True)
11
+ class OrderSubmission(bases.BrokerRequestEvent):
12
+ system_order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
13
+ symbol: str
14
+ order_type: models.orders.OrderType
15
+ side: models.orders.OrderSide
16
+ quantity: float
17
+ limit_price: float | None = None
18
+ stop_price: float | None = None
19
+
20
+
21
+ @dataclasses.dataclass(kw_only=True, frozen=True)
22
+ class OrderCancellation(bases.BrokerRequestEvent):
23
+ symbol: str
24
+
25
+
26
+ @dataclasses.dataclass(kw_only=True, frozen=True)
27
+ class OrderModification(bases.BrokerRequestEvent):
28
+ symbol: str
29
+ quantity: float | None = None
30
+ limit_price: float | None = None
31
+ stop_price: float | None = None
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import uuid
5
+
6
+ from onesecondtrader import models
7
+ from . import bases
8
+
9
+
10
+ @dataclasses.dataclass(kw_only=True, frozen=True)
11
+ class OrderSubmissionAccepted(bases.BrokerResponseEvent):
12
+ broker_order_id: str | None = None
13
+
14
+
15
+ @dataclasses.dataclass(kw_only=True, frozen=True)
16
+ class OrderSubmissionRejected(bases.BrokerResponseEvent):
17
+ reason: str | None = None
18
+
19
+
20
+ @dataclasses.dataclass(kw_only=True, frozen=True)
21
+ class OrderModificationAccepted(bases.BrokerResponseEvent):
22
+ broker_order_id: str | None = None
23
+
24
+
25
+ @dataclasses.dataclass(kw_only=True, frozen=True)
26
+ class OrderModificationRejected(bases.BrokerResponseEvent):
27
+ reason: str | None = None
28
+
29
+
30
+ @dataclasses.dataclass(kw_only=True, frozen=True)
31
+ class OrderCancellationAccepted(bases.BrokerResponseEvent):
32
+ pass
33
+
34
+
35
+ @dataclasses.dataclass(kw_only=True, frozen=True)
36
+ class OrderCancellationRejected(bases.BrokerResponseEvent):
37
+ reason: str | None = None
38
+
39
+
40
+ @dataclasses.dataclass(kw_only=True, frozen=True)
41
+ class OrderFilled(bases.BrokerResponseEvent):
42
+ fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
43
+ broker_fill_id: str | None = None
44
+ symbol: str
45
+ side: models.orders.OrderSide
46
+ quantity_filled: float
47
+ fill_price: float
48
+ commission: float
49
+ exchange: str = "SIMULATED"
50
+
51
+
52
+ @dataclasses.dataclass(kw_only=True, frozen=True)
53
+ class OrderExpired(bases.BrokerResponseEvent):
54
+ pass
@@ -0,0 +1,13 @@
1
+ __all__ = [
2
+ "Indicator",
3
+ "Open",
4
+ "High",
5
+ "Low",
6
+ "Close",
7
+ "Volume",
8
+ "SimpleMovingAverage",
9
+ ]
10
+
11
+ from .base import Indicator
12
+ from .bar import Open, High, Low, Close, Volume
13
+ from .averages import SimpleMovingAverage
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import collections
4
+
5
+ import numpy as np
6
+
7
+ from onesecondtrader import events, models
8
+ from .base import Indicator
9
+
10
+
11
+ class SimpleMovingAverage(Indicator):
12
+ def __init__(
13
+ self,
14
+ period: int = 200,
15
+ max_history: int = 100,
16
+ input_source: models.InputSource = models.InputSource.CLOSE,
17
+ plot_at: int = 0,
18
+ ) -> None:
19
+ super().__init__(max_history=max_history, plot_at=plot_at)
20
+ self.period: int = max(1, int(period))
21
+ self.input_source: models.InputSource = input_source
22
+ self._window: dict[str, collections.deque[float]] = {}
23
+
24
+ @property
25
+ def name(self) -> str:
26
+ return f"SMA_{self.period}_{self.input_source.name}"
27
+
28
+ def _compute_indicator(self, incoming_bar: events.BarReceived) -> float:
29
+ symbol = incoming_bar.symbol
30
+ if symbol not in self._window:
31
+ self._window[symbol] = collections.deque(maxlen=self.period)
32
+ window = self._window[symbol]
33
+ value = self._extract_input(incoming_bar)
34
+ window.append(value)
35
+ if len(window) < self.period:
36
+ return np.nan
37
+ return sum(window) / self.period
38
+
39
+ def _extract_input(self, incoming_bar: events.BarReceived) -> float:
40
+ match self.input_source:
41
+ case models.InputSource.OPEN:
42
+ return incoming_bar.open
43
+ case models.InputSource.HIGH:
44
+ return incoming_bar.high
45
+ case models.InputSource.LOW:
46
+ return incoming_bar.low
47
+ case models.InputSource.CLOSE:
48
+ return incoming_bar.close
49
+ case models.InputSource.VOLUME:
50
+ return (
51
+ float(incoming_bar.volume)
52
+ if incoming_bar.volume is not None
53
+ else np.nan
54
+ )
55
+ case _:
56
+ return incoming_bar.close