onesecondtrader 0.36.0__py3-none-any.whl → 0.37.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.
@@ -1,318 +0,0 @@
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
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
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,35 +0,0 @@
1
- from onesecondtrader import events, indicators, models
2
- from .base import StrategyBase
3
-
4
-
5
- class SMACrossover(StrategyBase):
6
- fast_period: int = 20
7
- slow_period: int = 100
8
- quantity: float = 1.0
9
-
10
- def setup(self) -> None:
11
- self.fast_sma = self.add_indicator(
12
- indicators.SimpleMovingAverage(period=self.fast_period)
13
- )
14
- self.slow_sma = self.add_indicator(
15
- indicators.SimpleMovingAverage(period=self.slow_period)
16
- )
17
-
18
- def on_bar(self, event: events.BarReceived) -> None:
19
- if (
20
- self.fast_sma[-2] <= self.slow_sma[-2]
21
- and self.fast_sma.latest > self.slow_sma.latest
22
- and self.position <= 0
23
- ):
24
- self.submit_order(
25
- models.OrderType.MARKET, models.OrderSide.BUY, self.quantity
26
- )
27
-
28
- if (
29
- self.fast_sma[-2] >= self.slow_sma[-2]
30
- and self.fast_sma.latest < self.slow_sma.latest
31
- and self.position >= 0
32
- ):
33
- self.submit_order(
34
- models.OrderType.MARKET, models.OrderSide.SELL, self.quantity
35
- )