onesecondtrader 0.30.0__py3-none-any.whl → 0.32.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.
@@ -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
@@ -5,7 +5,9 @@ __all__ = [
5
5
  "Low",
6
6
  "Close",
7
7
  "Volume",
8
+ "SimpleMovingAverage",
8
9
  ]
9
10
 
10
11
  from .base import Indicator
11
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
@@ -1,8 +1,12 @@
1
1
  __all__ = [
2
2
  "BarPeriod",
3
+ "InputSource",
3
4
  "OrderSide",
4
5
  "OrderType",
6
+ "OrderRecord",
7
+ "FillRecord",
5
8
  ]
6
9
 
7
- from .data import BarPeriod
10
+ from .data import BarPeriod, InputSource
8
11
  from .orders import OrderSide, OrderType
12
+ from .records import OrderRecord, FillRecord
@@ -8,3 +8,11 @@ class BarPeriod(enum.Enum):
8
8
  MINUTE = enum.auto()
9
9
  HOUR = enum.auto()
10
10
  DAY = enum.auto()
11
+
12
+
13
+ class InputSource(enum.Enum):
14
+ OPEN = enum.auto()
15
+ HIGH = enum.auto()
16
+ LOW = enum.auto()
17
+ CLOSE = enum.auto()
18
+ VOLUME = enum.auto()
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import uuid
5
+
6
+ import pandas as pd
7
+
8
+ from . import orders
9
+
10
+
11
+ @dataclasses.dataclass
12
+ class OrderRecord:
13
+ order_id: uuid.UUID
14
+ symbol: str
15
+ order_type: orders.OrderType
16
+ side: orders.OrderSide
17
+ quantity: float
18
+ limit_price: float | None = None
19
+ stop_price: float | None = None
20
+ filled_quantity: float = 0.0
21
+
22
+
23
+ @dataclasses.dataclass
24
+ class FillRecord:
25
+ fill_id: uuid.UUID
26
+ order_id: uuid.UUID
27
+ symbol: str
28
+ side: orders.OrderSide
29
+ quantity: float
30
+ price: float
31
+ commission: float
32
+ ts_event: pd.Timestamp
@@ -0,0 +1,5 @@
1
+ __all__ = [
2
+ "StrategyBase",
3
+ ]
4
+
5
+ from .base import StrategyBase
@@ -0,0 +1,318 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import uuid
5
+ from types import SimpleNamespace
6
+
7
+ import pandas as pd
8
+
9
+ from onesecondtrader import events, indicators, messaging, models
10
+
11
+
12
+ class StrategyBase(messaging.Subscriber, abc.ABC):
13
+ symbols: list[str] = []
14
+ bar_period: models.BarPeriod = models.BarPeriod.SECOND
15
+
16
+ def __init__(self, event_bus: messaging.EventBus) -> None:
17
+ super().__init__(event_bus)
18
+ self._subscribe(
19
+ events.BarReceived,
20
+ events.OrderSubmissionAccepted,
21
+ events.OrderModificationAccepted,
22
+ events.OrderCancellationAccepted,
23
+ events.OrderSubmissionRejected,
24
+ events.OrderModificationRejected,
25
+ events.OrderCancellationRejected,
26
+ events.OrderFilled,
27
+ events.OrderExpired,
28
+ )
29
+
30
+ self._current_symbol: str = ""
31
+ self._current_ts: pd.Timestamp = pd.Timestamp.now(tz="UTC")
32
+ self._indicators: list[indicators.Indicator] = []
33
+
34
+ self._fills: dict[str, list[models.FillRecord]] = {}
35
+ self._positions: dict[str, float] = {}
36
+ self._avg_prices: dict[str, float] = {}
37
+ self._pending_orders: dict[uuid.UUID, models.OrderRecord] = {}
38
+ self._submitted_orders: dict[uuid.UUID, models.OrderRecord] = {}
39
+ self._submitted_modifications: dict[uuid.UUID, models.OrderRecord] = {}
40
+ self._submitted_cancellations: dict[uuid.UUID, models.OrderRecord] = {}
41
+
42
+ # OHLCV as indicators for history access: self.bar.close.history(symbol)
43
+ self.bar = SimpleNamespace(
44
+ open=self.add_indicator(indicators.Open()),
45
+ high=self.add_indicator(indicators.High()),
46
+ low=self.add_indicator(indicators.Low()),
47
+ close=self.add_indicator(indicators.Close()),
48
+ volume=self.add_indicator(indicators.Volume()),
49
+ )
50
+
51
+ # Hook for subclasses to register indicators without overriding __init__
52
+ self.setup()
53
+
54
+ def add_indicator(self, ind: indicators.Indicator) -> indicators.Indicator:
55
+ self._indicators.append(ind)
56
+ return ind
57
+
58
+ @property
59
+ def position(self) -> float:
60
+ return self._positions.get(self._current_symbol, 0.0)
61
+
62
+ @property
63
+ def avg_price(self) -> float:
64
+ return self._avg_prices.get(self._current_symbol, 0.0)
65
+
66
+ def submit_order(
67
+ self,
68
+ order_type: models.OrderType,
69
+ side: models.OrderSide,
70
+ quantity: float,
71
+ limit_price: float | None = None,
72
+ stop_price: float | None = None,
73
+ ) -> uuid.UUID:
74
+ # Uses bar timestamp for backtest compatibility; ts_created tracks real wall-clock time
75
+ order_id = uuid.uuid4()
76
+
77
+ event = events.OrderSubmission(
78
+ ts_event=self._current_ts,
79
+ system_order_id=order_id,
80
+ symbol=self._current_symbol,
81
+ order_type=order_type,
82
+ side=side,
83
+ quantity=quantity,
84
+ limit_price=limit_price,
85
+ stop_price=stop_price,
86
+ )
87
+
88
+ order = models.OrderRecord(
89
+ order_id=order_id,
90
+ symbol=self._current_symbol,
91
+ order_type=order_type,
92
+ side=side,
93
+ quantity=quantity,
94
+ limit_price=limit_price,
95
+ stop_price=stop_price,
96
+ )
97
+
98
+ self._submitted_orders[order_id] = order
99
+ self._publish(event)
100
+ return order_id
101
+
102
+ def submit_modification(
103
+ self,
104
+ order_id: uuid.UUID,
105
+ quantity: float | None = None,
106
+ limit_price: float | None = None,
107
+ stop_price: float | None = None,
108
+ ) -> bool:
109
+ original_order = self._pending_orders.get(order_id)
110
+ if original_order is None:
111
+ return False
112
+
113
+ event = events.OrderModification(
114
+ ts_event=self._current_ts,
115
+ system_order_id=order_id,
116
+ symbol=original_order.symbol,
117
+ quantity=quantity,
118
+ limit_price=limit_price,
119
+ stop_price=stop_price,
120
+ )
121
+
122
+ modified_order = models.OrderRecord(
123
+ order_id=order_id,
124
+ symbol=original_order.symbol,
125
+ order_type=original_order.order_type,
126
+ side=original_order.side,
127
+ quantity=quantity if quantity is not None else original_order.quantity,
128
+ limit_price=(
129
+ limit_price if limit_price is not None else original_order.limit_price
130
+ ),
131
+ stop_price=(
132
+ stop_price if stop_price is not None else original_order.stop_price
133
+ ),
134
+ filled_quantity=original_order.filled_quantity,
135
+ )
136
+
137
+ self._submitted_modifications[order_id] = modified_order
138
+ self._publish(event)
139
+ return True
140
+
141
+ def submit_cancellation(self, order_id: uuid.UUID) -> bool:
142
+ original_order = self._pending_orders.get(order_id)
143
+ if original_order is None:
144
+ return False
145
+
146
+ event = events.OrderCancellation(
147
+ ts_event=self._current_ts,
148
+ system_order_id=order_id,
149
+ symbol=original_order.symbol,
150
+ )
151
+
152
+ self._submitted_cancellations[order_id] = original_order
153
+ self._publish(event)
154
+ return True
155
+
156
+ def _on_event(self, event: events.EventBase) -> None:
157
+ match event:
158
+ case events.BarReceived() as bar_event:
159
+ self._on_bar_received(bar_event)
160
+ case events.OrderSubmissionAccepted() as accepted:
161
+ self._on_order_submission_accepted(accepted)
162
+ case events.OrderModificationAccepted() as accepted:
163
+ self._on_order_modification_accepted(accepted)
164
+ case events.OrderCancellationAccepted() as accepted:
165
+ self._on_order_cancellation_accepted(accepted)
166
+ case events.OrderSubmissionRejected() as rejected:
167
+ self._on_order_submission_rejected(rejected)
168
+ case events.OrderModificationRejected() as rejected:
169
+ self._on_order_modification_rejected(rejected)
170
+ case events.OrderCancellationRejected() as rejected:
171
+ self._on_order_cancellation_rejected(rejected)
172
+ case events.OrderFilled() as filled:
173
+ self._on_order_filled(filled)
174
+ case events.OrderExpired() as expired:
175
+ self._on_order_expired(expired)
176
+ case _:
177
+ return
178
+
179
+ def _on_bar_received(self, event: events.BarReceived) -> None:
180
+ if event.symbol not in self.symbols:
181
+ return
182
+ if event.bar_period != self.bar_period:
183
+ return
184
+
185
+ self._current_symbol = event.symbol
186
+ self._current_ts = event.ts_event
187
+
188
+ for ind in self._indicators:
189
+ ind.update(event)
190
+
191
+ self._emit_processed_bar(event)
192
+ self.on_bar(event)
193
+
194
+ def _emit_processed_bar(self, event: events.BarReceived) -> None:
195
+ ohlcv_names = {"OPEN", "HIGH", "LOW", "CLOSE", "VOLUME"}
196
+
197
+ indicator_values = {
198
+ f"{ind.plot_at:02d}_{ind.name}": ind.latest(event.symbol)
199
+ for ind in self._indicators
200
+ if ind.name not in ohlcv_names
201
+ }
202
+
203
+ processed_bar = events.BarProcessed(
204
+ ts_event=event.ts_event,
205
+ symbol=event.symbol,
206
+ bar_period=event.bar_period,
207
+ open=event.open,
208
+ high=event.high,
209
+ low=event.low,
210
+ close=event.close,
211
+ volume=event.volume,
212
+ indicators=indicator_values,
213
+ )
214
+
215
+ self._publish(processed_bar)
216
+
217
+ def _on_order_submission_accepted(
218
+ self, event: events.OrderSubmissionAccepted
219
+ ) -> None:
220
+ order = self._submitted_orders.pop(event.associated_order_id, None)
221
+ if order is not None:
222
+ self._pending_orders[event.associated_order_id] = order
223
+
224
+ def _on_order_modification_accepted(
225
+ self, event: events.OrderModificationAccepted
226
+ ) -> None:
227
+ modified_order = self._submitted_modifications.pop(
228
+ event.associated_order_id, None
229
+ )
230
+ if modified_order is not None:
231
+ self._pending_orders[event.associated_order_id] = modified_order
232
+
233
+ def _on_order_cancellation_accepted(
234
+ self, event: events.OrderCancellationAccepted
235
+ ) -> None:
236
+ self._submitted_cancellations.pop(event.associated_order_id, None)
237
+ self._pending_orders.pop(event.associated_order_id, None)
238
+
239
+ def _on_order_submission_rejected(
240
+ self, event: events.OrderSubmissionRejected
241
+ ) -> None:
242
+ self._submitted_orders.pop(event.associated_order_id, None)
243
+
244
+ def _on_order_modification_rejected(
245
+ self, event: events.OrderModificationRejected
246
+ ) -> None:
247
+ self._submitted_modifications.pop(event.associated_order_id, None)
248
+
249
+ def _on_order_cancellation_rejected(
250
+ self, event: events.OrderCancellationRejected
251
+ ) -> None:
252
+ self._submitted_cancellations.pop(event.associated_order_id, None)
253
+
254
+ def _on_order_filled(self, event: events.OrderFilled) -> None:
255
+ # Track partial fills: only remove order when fully filled
256
+ order = self._pending_orders.get(event.associated_order_id)
257
+ if order:
258
+ order.filled_quantity += event.quantity_filled
259
+ if order.filled_quantity >= order.quantity:
260
+ self._pending_orders.pop(event.associated_order_id)
261
+
262
+ fill = models.FillRecord(
263
+ fill_id=event.fill_id,
264
+ order_id=event.associated_order_id,
265
+ symbol=event.symbol,
266
+ side=event.side,
267
+ quantity=event.quantity_filled,
268
+ price=event.fill_price,
269
+ commission=event.commission,
270
+ ts_event=event.ts_event,
271
+ )
272
+
273
+ self._fills.setdefault(event.symbol, []).append(fill)
274
+ self._update_position(event)
275
+
276
+ def _update_position(self, event: events.OrderFilled) -> None:
277
+ symbol = event.symbol
278
+ fill_qty = event.quantity_filled
279
+ fill_price = event.fill_price
280
+
281
+ signed_qty = 0.0
282
+ match event.side:
283
+ case models.OrderSide.BUY:
284
+ signed_qty = fill_qty
285
+ case models.OrderSide.SELL:
286
+ signed_qty = -fill_qty
287
+
288
+ old_pos = self._positions.get(symbol, 0.0)
289
+ old_avg = self._avg_prices.get(symbol, 0.0)
290
+ new_pos = old_pos + signed_qty
291
+
292
+ if new_pos == 0.0:
293
+ new_avg = 0.0
294
+ elif old_pos == 0.0:
295
+ new_avg = fill_price
296
+ elif (old_pos > 0 and signed_qty > 0) or (old_pos < 0 and signed_qty < 0):
297
+ new_avg = (old_avg * abs(old_pos) + fill_price * abs(signed_qty)) / abs(
298
+ new_pos
299
+ )
300
+ else:
301
+ if abs(new_pos) <= abs(old_pos):
302
+ new_avg = old_avg
303
+ else:
304
+ new_avg = fill_price
305
+
306
+ self._positions[symbol] = new_pos
307
+ self._avg_prices[symbol] = new_avg
308
+
309
+ def _on_order_expired(self, event: events.OrderExpired) -> None:
310
+ self._pending_orders.pop(event.associated_order_id, None)
311
+
312
+ # Override to register indicators. Called at end of __init__.
313
+ def setup(self) -> None:
314
+ pass
315
+
316
+ @abc.abstractmethod
317
+ def on_bar(self, event: events.BarReceived) -> None:
318
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.30.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,4 +1,4 @@
1
- onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1
+ onesecondtrader/__init__.py,sha256=edb0kipMTx0p-utX0tc4J8IbhFrZprX200oxhX8LzH4,241
2
2
  onesecondtrader/brokers/__init__.py,sha256=YofzD0qlrfo_BzL6gwiHb9by7Webp36TQ_1O5EV0uzY,124
3
3
  onesecondtrader/brokers/base.py,sha256=b6Xq2gBUdy3RTpnUfpUiNSXwbuq_bRIjXJ-s89Xu7nE,1345
4
4
  onesecondtrader/brokers/simulated.py,sha256=pJvZ7b76xAs-NBbOX_v78IJgVrdnuTLCadqj8kYQ5sg,12694
@@ -7,16 +7,20 @@ onesecondtrader/events/bases.py,sha256=g-ykq2jgcitIAueRurUlqAq0jINQwuhSWi_khAniP
7
7
  onesecondtrader/events/market.py,sha256=IfHuIGfp_IUiw-dFay4c4RYmkoNzshxbhuWTglBqfN0,509
8
8
  onesecondtrader/events/requests.py,sha256=2KXwSckiar9-fy8wkN3vcSIeOkeBfeo_XhUhrNKEd2Q,831
9
9
  onesecondtrader/events/responses.py,sha256=w_BH1nkkPyxQjh30EXEVFcUGDoMprFc2PuAaqpVrQ8A,1436
10
- onesecondtrader/indicators/__init__.py,sha256=sGRS3cpN2yLBYws66eVeqY-bAIHg0o6pgFvrkzZtm0Q,170
10
+ onesecondtrader/indicators/__init__.py,sha256=hRg3FCP1FT7LYOLzztybWn48gTR5QvewzzdELPYbdoY,239
11
+ onesecondtrader/indicators/averages.py,sha256=DpRRdY5G5ze3jwNOV19PPjV6slA0IEeOOla67yChi2Y,1900
11
12
  onesecondtrader/indicators/bar.py,sha256=0H07mKNiUx5cE1fQvx1oPVY0R_MXcmAAgsLek175vTk,1115
12
13
  onesecondtrader/indicators/base.py,sha256=WGqjp9mmkR2PZ5ZC8zL9ddeTfUrB0hX3bjnq1W4HKuI,1658
13
14
  onesecondtrader/messaging/__init__.py,sha256=vMRDabHBgse_vZRTRFtnU8M8v2sY_o4pHjGzgu3hp3E,115
14
15
  onesecondtrader/messaging/eventbus.py,sha256=Y8VbDZlEz8Q6KcCkfXRKsVIixsctBMRW1a5ANw297Ls,1576
15
16
  onesecondtrader/messaging/subscriber.py,sha256=ImpFmu5IstLXLoKVMaebmLp5MXN6225vHLdTL1ZOPvw,2106
16
- onesecondtrader/models/__init__.py,sha256=cH5xyniz78MQjM9_-fFdP1ZW6FFLTmayMwQauFO23bU,135
17
- onesecondtrader/models/data.py,sha256=TqUvTtjpmzTJT8ZdTYnlVoyI7Qck2IsseCazWZxJgD0,173
17
+ onesecondtrader/models/__init__.py,sha256=7amHCQ6BAhHKps0ke63E-zh8IJNmkdDogZq-PfBukMs,249
18
+ onesecondtrader/models/data.py,sha256=fBmddVl6EXYC5u2UnvQ59DXAXeZeIb48KP1ZdeTL52A,322
18
19
  onesecondtrader/models/orders.py,sha256=y6Ar-6fMqaOd_hRnRGvfWUF0Z13H_2hfTOW3ROOk0A8,254
19
- onesecondtrader-0.30.0.dist-info/METADATA,sha256=WBtjr3Eh-kk7ZK4Q7MCF9Dv_UDhJgbGAeMeW_8il5FI,9682
20
- onesecondtrader-0.30.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
21
- onesecondtrader-0.30.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
22
- onesecondtrader-0.30.0.dist-info/RECORD,,
20
+ onesecondtrader/models/records.py,sha256=vdCWBtoDQs5R4iB_8_3fXkxWEvoCxOssk9XBnS4l7Vk,599
21
+ onesecondtrader/strategies/__init__.py,sha256=V1xobVVGzKWTORK63vTfuS4a-9_8Qtfcnlek1da7uAM,66
22
+ onesecondtrader/strategies/base.py,sha256=qE78AfPjOC8iWbfWSZfc2uQ2FqKuvBJJ-ce5p-kNAYo,11228
23
+ onesecondtrader-0.32.0.dist-info/METADATA,sha256=3L-rFM2jJ6R54W2AHucu4lGIhMkf1YyyLK67lhu20lI,9682
24
+ onesecondtrader-0.32.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
25
+ onesecondtrader-0.32.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
26
+ onesecondtrader-0.32.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: poetry-core 2.3.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any