onesecondtrader 0.54.0__tar.gz → 0.55.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 (52) hide show
  1. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/PKG-INFO +1 -1
  2. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/pyproject.toml +1 -1
  3. onesecondtrader-0.55.0/src/onesecondtrader/events/requests/order_submission.py +39 -0
  4. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/__init__.py +2 -0
  5. onesecondtrader-0.55.0/src/onesecondtrader/models/action_types.py +34 -0
  6. onesecondtrader-0.55.0/src/onesecondtrader/strategies/__init__.py +11 -0
  7. onesecondtrader-0.55.0/src/onesecondtrader/strategies/base.py +539 -0
  8. onesecondtrader-0.55.0/src/onesecondtrader/strategies/examples.py +48 -0
  9. onesecondtrader-0.54.0/src/onesecondtrader/events/requests/order_submission.py +0 -35
  10. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/LICENSE +0 -0
  11. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/README.md +0 -0
  12. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/__init__.py +0 -0
  13. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/brokers/__init__.py +0 -0
  14. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/brokers/base.py +0 -0
  15. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/brokers/simulated.py +0 -0
  16. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/datafeeds/__init__.py +0 -0
  17. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/datafeeds/base.py +0 -0
  18. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/datafeeds/simulated.py +0 -0
  19. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/__init__.py +0 -0
  20. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/base.py +0 -0
  21. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/__init__.py +0 -0
  22. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/bar_processed.py +0 -0
  23. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/market/bar_received.py +0 -0
  24. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/__init__.py +0 -0
  25. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/base.py +0 -0
  26. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/expirations.py +0 -0
  27. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/orders/fills.py +0 -0
  28. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/__init__.py +0 -0
  29. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/base.py +0 -0
  30. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/order_cancellation.py +0 -0
  31. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/requests/order_modification.py +0 -0
  32. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/__init__.py +0 -0
  33. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/base.py +0 -0
  34. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/cancellations.py +0 -0
  35. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/modifications.py +0 -0
  36. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/events/responses/orders.py +0 -0
  37. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/__init__.py +0 -0
  38. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/base.py +0 -0
  39. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/market_fields.py +0 -0
  40. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/indicators/moving_averages.py +0 -0
  41. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/messaging/__init__.py +0 -0
  42. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/messaging/eventbus.py +0 -0
  43. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/messaging/subscriber.py +0 -0
  44. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/bar_fields.py +0 -0
  45. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/bar_period.py +0 -0
  46. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/order_types.py +0 -0
  47. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/rejection_reasons.py +0 -0
  48. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/models/trade_sides.py +0 -0
  49. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/__init__.py +0 -0
  50. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/schema_versions/__init__.py +0 -0
  51. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/schema_versions/secmaster_schema_v1.sql +0 -0
  52. {onesecondtrader-0.54.0 → onesecondtrader-0.55.0}/src/onesecondtrader/secmaster/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.54.0
3
+ Version: 0.55.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.54.0"
3
+ version = "0.55.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,39 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import uuid
5
+
6
+ from onesecondtrader import models
7
+ from onesecondtrader.events.requests.base import RequestBase
8
+
9
+
10
+ @dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
11
+ class OrderSubmissionRequest(RequestBase):
12
+ """
13
+ Event representing a request to submit a new order to a brokers.
14
+
15
+ The `system_order_id` is a unique identifier assigned by the system to the order submission request by default at object creation.
16
+
17
+ | Field | Type | Semantics |
18
+ |-------------------|----------------------------|----------------------------------------------------------------------------|
19
+ | `ts_event_ns` | `int` | Time at which the submission request was issued, as UTC epoch nanoseconds. |
20
+ | `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
21
+ | `system_order_id` | `uuid.UUID` | System-assigned unique identifier for the order submission. |
22
+ | `symbol` | `str` | Identifier of the traded instrument. |
23
+ | `order_type` | `models.OrderType` | Execution constraint of the order. |
24
+ | `side` | `models.TradeSide` | Direction of the trade. |
25
+ | `quantity` | `float` | Requested order quantity. |
26
+ | `limit_price` | `float` or `None` | Limit price, if applicable to the order type. |
27
+ | `stop_price` | `float` or `None` | Stop price, if applicable to the order type. |
28
+ | `action` | `models.ActionType` or `None` | Intent of the order from the strategy's perspective (e.g., entry, exit). |
29
+ | `signal` | `str` or `None` | Optional signal name or identifier that triggered this order. |
30
+ """
31
+
32
+ system_order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
33
+ order_type: models.OrderType
34
+ side: models.TradeSide
35
+ quantity: float
36
+ limit_price: float | None = None
37
+ stop_price: float | None = None
38
+ action: models.ActionType | None = None
39
+ signal: str | None = None
@@ -2,6 +2,7 @@
2
2
  Defines the fundamental domain concepts used throughout the trading system.
3
3
  """
4
4
 
5
+ from .action_types import ActionType
5
6
  from .bar_fields import BarField
6
7
  from .bar_period import BarPeriod
7
8
  from .order_types import OrderType
@@ -13,6 +14,7 @@ from .rejection_reasons import (
13
14
  from .trade_sides import TradeSide
14
15
 
15
16
  __all__ = [
17
+ "ActionType",
16
18
  "BarField",
17
19
  "BarPeriod",
18
20
  "OrderType",
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+
6
+ class ActionType(enum.Enum):
7
+ """
8
+ Enumeration of trading action types.
9
+
10
+ `ActionType` specifies the intent or purpose of an order from the strategy's perspective,
11
+ describing what the order is meant to accomplish in terms of position management.
12
+
13
+ | Value | Semantics |
14
+ |---------------|----------------------------------------------------------------------------|
15
+ | `ENTRY` | Opens a new position (direction-agnostic). |
16
+ | `ENTRY_LONG` | Opens a new long position. |
17
+ | `ENTRY_SHORT` | Opens a new short position. |
18
+ | `EXIT` | Closes an existing position (direction-agnostic). |
19
+ | `EXIT_LONG` | Closes an existing long position. |
20
+ | `EXIT_SHORT` | Closes an existing short position. |
21
+ | `ADD` | Increases the size of an existing position. |
22
+ | `REDUCE` | Decreases the size of an existing position without fully closing it. |
23
+ | `REVERSE` | Closes the current position and opens a new one in the opposite direction. |
24
+ """
25
+
26
+ ENTRY = enum.auto()
27
+ ENTRY_LONG = enum.auto()
28
+ ENTRY_SHORT = enum.auto()
29
+ EXIT = enum.auto()
30
+ EXIT_LONG = enum.auto()
31
+ EXIT_SHORT = enum.auto()
32
+ ADD = enum.auto()
33
+ REDUCE = enum.auto()
34
+ REVERSE = enum.auto()
@@ -0,0 +1,11 @@
1
+ """
2
+ Provides a base class for creating custom trading strategies and provides example strategies.
3
+ """
4
+
5
+ from .base import StrategyBase
6
+ from .examples import SMACrossover
7
+
8
+ __all__ = [
9
+ "StrategyBase",
10
+ "SMACrossover",
11
+ ]
@@ -0,0 +1,539 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import dataclasses
5
+ import enum
6
+ import uuid
7
+ from types import SimpleNamespace
8
+
9
+ import pandas as pd
10
+
11
+ from onesecondtrader import events, indicators, messaging, models
12
+
13
+
14
+ @dataclasses.dataclass
15
+ class ParamSpec:
16
+ """
17
+ Specification for a strategy parameter.
18
+
19
+ Defines the default value and optional constraints for a configurable strategy parameter.
20
+ Used to declare tunable parameters that can be overridden at strategy instantiation.
21
+
22
+ | Field | Type | Semantics |
23
+ |-----------|-------------------------------------------|--------------------------------------------------------------|
24
+ | `default` | `int`, `float`, `str`, `bool`, or `Enum` | Default value of the parameter. |
25
+ | `min` | `int`, `float`, or `None` | Minimum allowed value, if applicable. |
26
+ | `max` | `int`, `float`, or `None` | Maximum allowed value, if applicable. |
27
+ | `step` | `int`, `float`, or `None` | Step size for parameter sweeps, if applicable. |
28
+ | `choices` | `list` or `None` | Explicit list of allowed values, if applicable. |
29
+ """
30
+
31
+ default: int | float | str | bool | enum.Enum
32
+ min: int | float | None = None
33
+ max: int | float | None = None
34
+ step: int | float | None = None
35
+ choices: list | None = None
36
+
37
+ @property
38
+ def resolved_choices(self) -> list | None:
39
+ """
40
+ Return the effective list of allowed values for this parameter.
41
+
42
+ If `choices` is explicitly set, returns that list.
43
+ If `default` is an enum member, returns all members of that enum type.
44
+ Otherwise, returns `None`.
45
+ """
46
+ if self.choices is not None:
47
+ return self.choices
48
+ if isinstance(self.default, enum.Enum):
49
+ return list(type(self.default))
50
+ return None
51
+
52
+
53
+ @dataclasses.dataclass
54
+ class OrderRecord:
55
+ """
56
+ Internal record of an order submitted by a strategy.
57
+
58
+ Tracks the state of an order from submission through fill or cancellation.
59
+
60
+ | Field | Type | Semantics |
61
+ |-------------------|--------------------|-----------------------------------------------------|
62
+ | `order_id` | `uuid.UUID` | System-assigned unique identifier for the order. |
63
+ | `symbol` | `str` | Identifier of the traded instrument. |
64
+ | `order_type` | `models.OrderType` | Execution constraint of the order. |
65
+ | `side` | `models.TradeSide` | Direction of the trade. |
66
+ | `quantity` | `float` | Requested order quantity. |
67
+ | `limit_price` | `float` or `None` | Limit price, if applicable to the order type. |
68
+ | `stop_price` | `float` or `None` | Stop price, if applicable to the order type. |
69
+ | `signal` | `str` or `None` | Optional signal name associated with the order. |
70
+ | `filled_quantity` | `float` | Cumulative quantity filled for this order. |
71
+ """
72
+
73
+ order_id: uuid.UUID
74
+ symbol: str
75
+ order_type: models.OrderType
76
+ side: models.TradeSide
77
+ quantity: float
78
+ limit_price: float | None = None
79
+ stop_price: float | None = None
80
+ signal: str | None = None
81
+ filled_quantity: float = 0.0
82
+
83
+
84
+ @dataclasses.dataclass
85
+ class FillRecord:
86
+ """
87
+ Internal record of a fill received by a strategy.
88
+
89
+ Captures execution details for a single fill event.
90
+
91
+ | Field | Type | Semantics |
92
+ |--------------|--------------------|-----------------------------------------------------------|
93
+ | `fill_id` | `uuid.UUID` | System-assigned unique identifier for the fill. |
94
+ | `order_id` | `uuid.UUID` | Identifier of the order associated with the fill. |
95
+ | `symbol` | `str` | Identifier of the traded instrument. |
96
+ | `side` | `models.TradeSide` | Trade direction of the executed quantity. |
97
+ | `quantity` | `float` | Quantity executed in this fill. |
98
+ | `price` | `float` | Execution price of the fill. |
99
+ | `commission` | `float` | Commission or fee associated with the fill. |
100
+ | `ts_event` | `pd.Timestamp` | Timestamp at which the fill was observed by the strategy. |
101
+ """
102
+
103
+ fill_id: uuid.UUID
104
+ order_id: uuid.UUID
105
+ symbol: str
106
+ side: models.TradeSide
107
+ quantity: float
108
+ price: float
109
+ commission: float
110
+ ts_event: pd.Timestamp
111
+
112
+
113
+ class StrategyBase(messaging.Subscriber, abc.ABC):
114
+ """
115
+ Abstract base class for trading strategies.
116
+
117
+ A strategy subscribes to market data and order events, maintains position state,
118
+ and submits orders through the event bus. Subclasses implement `on_bar` to define
119
+ trading logic and optionally override `setup` to register indicators.
120
+
121
+ Class Attributes:
122
+ name:
123
+ Human-readable name of the strategy.
124
+ symbols:
125
+ List of instrument symbols the strategy trades.
126
+ parameters:
127
+ Dictionary mapping parameter names to their specifications.
128
+ """
129
+
130
+ name: str = ""
131
+ symbols: list[str] = []
132
+ parameters: dict[str, ParamSpec] = {}
133
+
134
+ def __init__(self, event_bus: messaging.EventBus, **overrides) -> None:
135
+ """
136
+ Initialize the strategy and start event processing.
137
+
138
+ Parameters:
139
+ event_bus:
140
+ Event bus used for subscribing to and publishing events.
141
+ **overrides:
142
+ Parameter values to override defaults defined in `parameters`.
143
+ """
144
+ super().__init__(event_bus)
145
+
146
+ for name, spec in self.parameters.items():
147
+ value = overrides.get(name, spec.default)
148
+ setattr(self, name, value)
149
+
150
+ self._subscribe(
151
+ events.market.BarReceived,
152
+ events.responses.OrderAccepted,
153
+ events.responses.ModificationAccepted,
154
+ events.responses.CancellationAccepted,
155
+ events.responses.OrderRejected,
156
+ events.responses.ModificationRejected,
157
+ events.responses.CancellationRejected,
158
+ events.orders.FillEvent,
159
+ events.orders.OrderExpired,
160
+ )
161
+
162
+ self._current_symbol: str = ""
163
+ self._current_ts: pd.Timestamp = pd.Timestamp.now(tz="UTC")
164
+ self._indicators: list[indicators.IndicatorBase] = []
165
+
166
+ self._fills: dict[str, list[FillRecord]] = {}
167
+ self._positions: dict[str, float] = {}
168
+ self._avg_prices: dict[str, float] = {}
169
+ self._pending_orders: dict[uuid.UUID, OrderRecord] = {}
170
+ self._submitted_orders: dict[uuid.UUID, OrderRecord] = {}
171
+ self._submitted_modifications: dict[uuid.UUID, OrderRecord] = {}
172
+ self._submitted_cancellations: dict[uuid.UUID, OrderRecord] = {}
173
+
174
+ # OHLCV as indicators for history access: self.bar.close.history
175
+ self.bar = SimpleNamespace(
176
+ open=self.add_indicator(indicators.Open()),
177
+ high=self.add_indicator(indicators.High()),
178
+ low=self.add_indicator(indicators.Low()),
179
+ close=self.add_indicator(indicators.Close()),
180
+ volume=self.add_indicator(indicators.Volume()),
181
+ )
182
+
183
+ # Hook for subclasses to register indicators without overriding __init__
184
+ self.setup()
185
+
186
+ def add_indicator(self, ind: indicators.IndicatorBase) -> indicators.IndicatorBase:
187
+ """
188
+ Register an indicator with the strategy.
189
+
190
+ Registered indicators are automatically updated on each bar event.
191
+
192
+ Parameters:
193
+ ind:
194
+ Indicator instance to register.
195
+
196
+ Returns:
197
+ The registered indicator instance.
198
+ """
199
+ self._indicators.append(ind)
200
+ return ind
201
+
202
+ @property
203
+ def position(self) -> float:
204
+ """
205
+ Return the current position for the active symbol.
206
+
207
+ The active symbol is set by the most recently processed bar event.
208
+ """
209
+ return self._positions.get(self._current_symbol, 0.0)
210
+
211
+ @property
212
+ def avg_price(self) -> float:
213
+ """
214
+ Return the average entry price for the current position on the active symbol.
215
+
216
+ Returns zero if there is no open position.
217
+ """
218
+ return self._avg_prices.get(self._current_symbol, 0.0)
219
+
220
+ def submit_order(
221
+ self,
222
+ order_type: models.OrderType,
223
+ side: models.TradeSide,
224
+ quantity: float,
225
+ limit_price: float | None = None,
226
+ stop_price: float | None = None,
227
+ action: models.ActionType | None = None,
228
+ signal: str | None = None,
229
+ ) -> uuid.UUID:
230
+ """
231
+ Submit a new order for the active symbol.
232
+
233
+ Parameters:
234
+ order_type:
235
+ Execution constraint of the order.
236
+ side:
237
+ Direction of the trade.
238
+ quantity:
239
+ Requested order quantity.
240
+ limit_price:
241
+ Limit price, if applicable to the order type.
242
+ stop_price:
243
+ Stop price, if applicable to the order type.
244
+ action:
245
+ Intent of the order from the strategy's perspective (e.g., entry, exit).
246
+ signal:
247
+ Optional signal name associated with the order.
248
+
249
+ Returns:
250
+ System-assigned unique identifier for the submitted order.
251
+ """
252
+ order_id = uuid.uuid4()
253
+
254
+ event = events.requests.OrderSubmissionRequest(
255
+ ts_event_ns=int(self._current_ts.value),
256
+ system_order_id=order_id,
257
+ symbol=self._current_symbol,
258
+ order_type=order_type,
259
+ side=side,
260
+ quantity=quantity,
261
+ limit_price=limit_price,
262
+ stop_price=stop_price,
263
+ action=action,
264
+ signal=signal,
265
+ )
266
+
267
+ order = OrderRecord(
268
+ order_id=order_id,
269
+ symbol=self._current_symbol,
270
+ order_type=order_type,
271
+ side=side,
272
+ quantity=quantity,
273
+ limit_price=limit_price,
274
+ stop_price=stop_price,
275
+ signal=signal,
276
+ )
277
+
278
+ self._submitted_orders[order_id] = order
279
+ self._publish(event)
280
+ return order_id
281
+
282
+ def submit_modification(
283
+ self,
284
+ order_id: uuid.UUID,
285
+ quantity: float | None = None,
286
+ limit_price: float | None = None,
287
+ stop_price: float | None = None,
288
+ ) -> bool:
289
+ """
290
+ Submit a modification request for a pending order.
291
+
292
+ Parameters:
293
+ order_id:
294
+ Identifier of the order to modify.
295
+ quantity:
296
+ Updated order quantity, or `None` to keep unchanged.
297
+ limit_price:
298
+ Updated limit price, or `None` to keep unchanged.
299
+ stop_price:
300
+ Updated stop price, or `None` to keep unchanged.
301
+
302
+ Returns:
303
+ `True` if the modification request was submitted, `False` if the order was not found.
304
+ """
305
+ original_order = self._pending_orders.get(order_id)
306
+ if original_order is None:
307
+ return False
308
+
309
+ event = events.requests.OrderModificationRequest(
310
+ ts_event_ns=int(self._current_ts.value),
311
+ system_order_id=order_id,
312
+ symbol=original_order.symbol,
313
+ quantity=quantity,
314
+ limit_price=limit_price,
315
+ stop_price=stop_price,
316
+ )
317
+
318
+ modified_order = OrderRecord(
319
+ order_id=order_id,
320
+ symbol=original_order.symbol,
321
+ order_type=original_order.order_type,
322
+ side=original_order.side,
323
+ quantity=quantity if quantity is not None else original_order.quantity,
324
+ limit_price=(
325
+ limit_price if limit_price is not None else original_order.limit_price
326
+ ),
327
+ stop_price=(
328
+ stop_price if stop_price is not None else original_order.stop_price
329
+ ),
330
+ signal=original_order.signal,
331
+ filled_quantity=original_order.filled_quantity,
332
+ )
333
+
334
+ self._submitted_modifications[order_id] = modified_order
335
+ self._publish(event)
336
+ return True
337
+
338
+ def submit_cancellation(self, order_id: uuid.UUID) -> bool:
339
+ """
340
+ Submit a cancellation request for a pending order.
341
+
342
+ Parameters:
343
+ order_id:
344
+ Identifier of the order to cancel.
345
+
346
+ Returns:
347
+ `True` if the cancellation request was submitted, `False` if the order was not found.
348
+ """
349
+ original_order = self._pending_orders.get(order_id)
350
+ if original_order is None:
351
+ return False
352
+
353
+ event = events.requests.OrderCancellationRequest(
354
+ ts_event_ns=int(self._current_ts.value),
355
+ system_order_id=order_id,
356
+ symbol=original_order.symbol,
357
+ )
358
+
359
+ self._submitted_cancellations[order_id] = original_order
360
+ self._publish(event)
361
+ return True
362
+
363
+ def _on_event(self, event: events.EventBase) -> None:
364
+ match event:
365
+ case events.market.BarReceived() as bar_event:
366
+ self._on_bar_received(bar_event)
367
+ case events.responses.OrderAccepted() as accepted:
368
+ self._on_order_submission_accepted(accepted)
369
+ case events.responses.ModificationAccepted() as accepted:
370
+ self._on_order_modification_accepted(accepted)
371
+ case events.responses.CancellationAccepted() as accepted:
372
+ self._on_order_cancellation_accepted(accepted)
373
+ case events.responses.OrderRejected() as rejected:
374
+ self._on_order_submission_rejected(rejected)
375
+ case events.responses.ModificationRejected() as rejected:
376
+ self._on_order_modification_rejected(rejected)
377
+ case events.responses.CancellationRejected() as rejected:
378
+ self._on_order_cancellation_rejected(rejected)
379
+ case events.orders.FillEvent() as filled:
380
+ self._on_order_filled(filled)
381
+ case events.orders.OrderExpired() as expired:
382
+ self._on_order_expired(expired)
383
+ case _:
384
+ return
385
+
386
+ def _on_bar_received(self, event: events.market.BarReceived) -> None:
387
+ if event.symbol not in self.symbols:
388
+ return
389
+ if event.bar_period != self.bar_period: # type: ignore[attr-defined]
390
+ return
391
+
392
+ self._current_symbol = event.symbol
393
+ self._current_ts = pd.Timestamp(event.ts_event_ns, tz="UTC")
394
+
395
+ for ind in self._indicators:
396
+ ind.update(event)
397
+
398
+ self._emit_processed_bar(event)
399
+ self.on_bar(event)
400
+
401
+ def _emit_processed_bar(self, event: events.market.BarReceived) -> None:
402
+ ohlcv_names = {"OPEN", "HIGH", "LOW", "CLOSE", "VOLUME"}
403
+
404
+ indicator_values = {
405
+ f"{ind.plot_at:02d}_{ind.name}": ind.latest(event.symbol)
406
+ for ind in self._indicators
407
+ if ind.name not in ohlcv_names
408
+ }
409
+
410
+ processed_bar = events.market.BarProcessed(
411
+ ts_event_ns=event.ts_event_ns,
412
+ symbol=event.symbol,
413
+ bar_period=event.bar_period,
414
+ open=event.open,
415
+ high=event.high,
416
+ low=event.low,
417
+ close=event.close,
418
+ volume=event.volume,
419
+ indicators=indicator_values,
420
+ )
421
+
422
+ self._publish(processed_bar)
423
+
424
+ def _on_order_submission_accepted(
425
+ self, event: events.responses.OrderAccepted
426
+ ) -> None:
427
+ order = self._submitted_orders.pop(event.associated_order_id, None)
428
+ if order is not None:
429
+ self._pending_orders[event.associated_order_id] = order
430
+
431
+ def _on_order_modification_accepted(
432
+ self, event: events.responses.ModificationAccepted
433
+ ) -> None:
434
+ modified_order = self._submitted_modifications.pop(
435
+ event.associated_order_id, None
436
+ )
437
+ if modified_order is not None:
438
+ self._pending_orders[event.associated_order_id] = modified_order
439
+
440
+ def _on_order_cancellation_accepted(
441
+ self, event: events.responses.CancellationAccepted
442
+ ) -> None:
443
+ self._submitted_cancellations.pop(event.associated_order_id, None)
444
+ self._pending_orders.pop(event.associated_order_id, None)
445
+
446
+ def _on_order_submission_rejected(
447
+ self, event: events.responses.OrderRejected
448
+ ) -> None:
449
+ self._submitted_orders.pop(event.associated_order_id, None)
450
+
451
+ def _on_order_modification_rejected(
452
+ self, event: events.responses.ModificationRejected
453
+ ) -> None:
454
+ self._submitted_modifications.pop(event.associated_order_id, None)
455
+
456
+ def _on_order_cancellation_rejected(
457
+ self, event: events.responses.CancellationRejected
458
+ ) -> None:
459
+ self._submitted_cancellations.pop(event.associated_order_id, None)
460
+
461
+ def _on_order_filled(self, event: events.orders.FillEvent) -> None:
462
+ order = self._pending_orders.get(event.associated_order_id)
463
+ if order:
464
+ order.filled_quantity += event.quantity_filled
465
+ if order.filled_quantity >= order.quantity:
466
+ self._pending_orders.pop(event.associated_order_id)
467
+
468
+ fill = FillRecord(
469
+ fill_id=event.fill_id,
470
+ order_id=event.associated_order_id,
471
+ symbol=event.symbol,
472
+ side=event.side,
473
+ quantity=event.quantity_filled,
474
+ price=event.fill_price,
475
+ commission=event.commission,
476
+ ts_event=pd.Timestamp(event.ts_event_ns, tz="UTC"),
477
+ )
478
+
479
+ self._fills.setdefault(event.symbol, []).append(fill)
480
+ self._update_position(event)
481
+
482
+ def _update_position(self, event: events.orders.FillEvent) -> None:
483
+ symbol = event.symbol
484
+ fill_qty = event.quantity_filled
485
+ fill_price = event.fill_price
486
+
487
+ signed_qty = 0.0
488
+ match event.side:
489
+ case models.TradeSide.BUY:
490
+ signed_qty = fill_qty
491
+ case models.TradeSide.SELL:
492
+ signed_qty = -fill_qty
493
+
494
+ old_pos = self._positions.get(symbol, 0.0)
495
+ old_avg = self._avg_prices.get(symbol, 0.0)
496
+ new_pos = old_pos + signed_qty
497
+
498
+ if new_pos == 0.0:
499
+ new_avg = 0.0
500
+ elif old_pos == 0.0:
501
+ new_avg = fill_price
502
+ elif (old_pos > 0 and signed_qty > 0) or (old_pos < 0 and signed_qty < 0):
503
+ new_avg = (old_avg * abs(old_pos) + fill_price * abs(signed_qty)) / abs(
504
+ new_pos
505
+ )
506
+ else:
507
+ if abs(new_pos) <= abs(old_pos):
508
+ new_avg = old_avg
509
+ else:
510
+ new_avg = fill_price
511
+
512
+ self._positions[symbol] = new_pos
513
+ self._avg_prices[symbol] = new_avg
514
+
515
+ def _on_order_expired(self, event: events.orders.OrderExpired) -> None:
516
+ self._pending_orders.pop(event.associated_order_id, None)
517
+
518
+ def setup(self) -> None:
519
+ """
520
+ Hook for subclasses to register indicators and perform initialization.
521
+
522
+ Called at the end of `__init__`. Override this method to register indicators
523
+ using `add_indicator` without needing to override `__init__`.
524
+ """
525
+ pass
526
+
527
+ @abc.abstractmethod
528
+ def on_bar(self, event: events.market.BarReceived) -> None:
529
+ """
530
+ Handle a bar event for a subscribed symbol.
531
+
532
+ Called after all registered indicators have been updated. Subclasses implement
533
+ this method to define trading logic.
534
+
535
+ Parameters:
536
+ event:
537
+ Bar event containing OHLCV data for the current bar.
538
+ """
539
+ pass
@@ -0,0 +1,48 @@
1
+ from onesecondtrader import events, indicators, models
2
+ from .base import StrategyBase, ParamSpec
3
+
4
+
5
+ class SMACrossover(StrategyBase):
6
+ name = "SMA Crossover"
7
+ parameters = {
8
+ "bar_period": ParamSpec(default=models.BarPeriod.SECOND),
9
+ "fast_period": ParamSpec(default=20, min=5, max=100, step=1),
10
+ "slow_period": ParamSpec(default=100, min=10, max=500, step=1),
11
+ "quantity": ParamSpec(default=1.0, min=0.1, max=100.0, step=0.1),
12
+ }
13
+
14
+ def setup(self) -> None:
15
+ self.fast_sma = self.add_indicator(
16
+ indicators.SimpleMovingAverage(period=self.fast_period) # type: ignore[attr-defined]
17
+ )
18
+ self.slow_sma = self.add_indicator(
19
+ indicators.SimpleMovingAverage(period=self.slow_period) # type: ignore[attr-defined]
20
+ )
21
+
22
+ def on_bar(self, event: events.market.BarReceived) -> None:
23
+ sym = event.symbol
24
+ if (
25
+ self.fast_sma[sym, -2] <= self.slow_sma[sym, -2]
26
+ and self.fast_sma.latest(sym) > self.slow_sma.latest(sym)
27
+ and self.position <= 0
28
+ ):
29
+ self.submit_order(
30
+ models.OrderType.MARKET,
31
+ models.TradeSide.BUY,
32
+ self.quantity, # type: ignore[attr-defined]
33
+ action=models.ActionType.ENTRY,
34
+ signal="sma_crossover_up",
35
+ )
36
+
37
+ if (
38
+ self.fast_sma[sym, -2] >= self.slow_sma[sym, -2]
39
+ and self.fast_sma.latest(sym) < self.slow_sma.latest(sym)
40
+ and self.position >= 0
41
+ ):
42
+ self.submit_order(
43
+ models.OrderType.MARKET,
44
+ models.TradeSide.SELL,
45
+ self.quantity, # type: ignore[attr-defined]
46
+ action=models.ActionType.EXIT,
47
+ signal="sma_crossover_down",
48
+ )
@@ -1,35 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import dataclasses
4
- import uuid
5
-
6
- from onesecondtrader import models
7
- from onesecondtrader.events.requests.base import RequestBase
8
-
9
-
10
- @dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
11
- class OrderSubmissionRequest(RequestBase):
12
- """
13
- Event representing a request to submit a new order to a brokers.
14
-
15
- The `system_order_id` is a unique identifier assigned by the system to the order submission request by default at object creation.
16
-
17
- | Field | Type | Semantics |
18
- |-------------------|--------------------------|----------------------------------------------------------------------------|
19
- | `ts_event_ns` | `int` | Time at which the submission request was issued, as UTC epoch nanoseconds. |
20
- | `ts_created_ns` | `int` | Time at which the event object was created, as UTC epoch nanoseconds. |
21
- | `system_order_id` | `uuid.UUID` | System-assigned unique identifier for the order submission. |
22
- | `symbol` | `str` | Identifier of the traded instrument. |
23
- | `order_type` | `models.OrderType` | Execution constraint of the order. |
24
- | `side` | `models.TradeSide` | Direction of the trade. |
25
- | `quantity` | `float` | Requested order quantity. |
26
- | `limit_price` | `float` or `None` | Limit price, if applicable to the order type. |
27
- | `stop_price` | `float` or `None` | Stop price, if applicable to the order type. |
28
- """
29
-
30
- system_order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
31
- order_type: models.OrderType
32
- side: models.TradeSide
33
- quantity: float
34
- limit_price: float | None = None
35
- stop_price: float | None = None