onesecondtrader 0.18.0__py3-none-any.whl → 0.20.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/core.py CHANGED
@@ -5,6 +5,7 @@ Core module containing the backbone of OneSecondTrader's event-driven architectu
5
5
  import abc
6
6
  import dataclasses
7
7
  import enum
8
+ import logging
8
9
  import pandas as pd
9
10
  import queue
10
11
  import threading
@@ -13,6 +14,13 @@ import uuid
13
14
  from collections import defaultdict
14
15
 
15
16
 
17
+ logging.basicConfig(
18
+ level=logging.DEBUG,
19
+ format="%(asctime)s - %(levelname)s - %(threadName)s - %(message)s",
20
+ )
21
+ logger = logging.getLogger("onesecondtrader")
22
+
23
+
16
24
  class Models:
17
25
  """
18
26
  Namespace for all models.
@@ -34,12 +42,41 @@ class Models:
34
42
  STOP = enum.auto()
35
43
  STOP_LIMIT = enum.auto()
36
44
 
45
+ class OrderRejectionReason(enum.Enum):
46
+ INSUFFICIENT_FUNDS = enum.auto()
47
+ MARKET_CLOSED = enum.auto()
48
+ UNKNOWN = enum.auto()
49
+
50
+ class CancelRejectionReason(enum.Enum):
51
+ ORDER_ALREADY_FILLED = enum.auto()
52
+ ORDER_ALREADY_CANCELLED = enum.auto()
53
+ ORDER_PENDING_EXECUTION = enum.auto()
54
+ MARKET_CLOSED = enum.auto()
55
+ UNKNOWN = enum.auto()
56
+
57
+ class ModifyRejectionReason(enum.Enum):
58
+ ORDER_ALREADY_FILLED = enum.auto()
59
+ ORDER_ALREADY_CANCELLED = enum.auto()
60
+ ORDER_PENDING_EXECUTION = enum.auto()
61
+ ORDER_NOT_FOUND = enum.auto()
62
+ INVALID_PRICE = enum.auto()
63
+ INVALID_QUANTITY = enum.auto()
64
+ MARKET_CLOSED = enum.auto()
65
+ UNKNOWN = enum.auto()
66
+
67
+ class TimeInForce(enum.Enum):
68
+ GTC = enum.auto()
69
+ DAY = enum.auto()
70
+ IOC = enum.auto()
71
+ FOK = enum.auto()
72
+
37
73
 
38
74
  class Events:
39
75
  """
40
76
  Namespace for all events.
41
77
  """
42
78
 
79
+ # BASE EVENT
43
80
  @dataclasses.dataclass(kw_only=True, frozen=True)
44
81
  class BaseEvent:
45
82
  ts_event: pd.Timestamp = dataclasses.field(
@@ -48,12 +85,20 @@ class Events:
48
85
 
49
86
  # SYSTEM EVENTS
50
87
  @dataclasses.dataclass(kw_only=True, frozen=True)
51
- class SystemShutdown(BaseEvent):
88
+ class SystemEvent(BaseEvent):
89
+ pass
90
+
91
+ @dataclasses.dataclass(kw_only=True, frozen=True)
92
+ class SystemShutdown(SystemEvent):
52
93
  pass
53
94
 
54
95
  # MARKET EVENTS
55
96
  @dataclasses.dataclass(kw_only=True, frozen=True)
56
- class IncomingBar(BaseEvent):
97
+ class MarketEvent(BaseEvent):
98
+ pass
99
+
100
+ @dataclasses.dataclass(kw_only=True, frozen=True)
101
+ class IncomingBar(MarketEvent):
57
102
  ts_event: pd.Timestamp
58
103
  symbol: str
59
104
  record_type: Models.RecordType
@@ -63,29 +108,83 @@ class Events:
63
108
  close: float
64
109
  volume: int | None = None
65
110
 
66
- # BROKER REQUEST EVENTS
111
+ # BROKER REQUESTS EVENTS
67
112
  @dataclasses.dataclass(kw_only=True, frozen=True)
68
- class Order(BaseEvent):
69
- order_id: uuid.UUID = dataclasses.field(default_factory=lambda: uuid.uuid4())
113
+ class BrokerRequestEvent(BaseEvent):
114
+ pass
115
+
116
+ @dataclasses.dataclass(kw_only=True, frozen=True)
117
+ class SubmitOrder(BrokerRequestEvent):
118
+ order_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
70
119
  symbol: str
71
120
  order_type: Models.OrderType
72
121
  side: Models.OrderSide
73
122
  quantity: float
74
123
  limit_price: float | None = None
75
124
  stop_price: float | None = None
125
+ time_in_force: Models.TimeInForce = Models.TimeInForce.GTC
126
+
127
+ @dataclasses.dataclass(kw_only=True, frozen=True)
128
+ class ModifyOrder(BrokerRequestEvent):
129
+ order_id: uuid.UUID
130
+ quantity: float | None = None
131
+ limit_price: float | None = None
132
+ stop_price: float | None = None
133
+
134
+ @dataclasses.dataclass(kw_only=True, frozen=True)
135
+ class CancelOrder(BrokerRequestEvent):
136
+ order_id: uuid.UUID
76
137
 
77
138
  # BROKER RESPONSE EVENTS
78
139
  @dataclasses.dataclass(kw_only=True, frozen=True)
79
- class Fill(BaseEvent):
140
+ class BrokerResponseEvent(BaseEvent):
141
+ ts_broker: pd.Timestamp
142
+
143
+ @dataclasses.dataclass(kw_only=True, frozen=True)
144
+ class OrderSubmitted(BrokerResponseEvent):
145
+ order_id: uuid.UUID
146
+ broker_order_id: str | None = None
147
+
148
+ @dataclasses.dataclass(kw_only=True, frozen=True)
149
+ class OrderModified(BrokerResponseEvent):
150
+ order_id: uuid.UUID
151
+ broker_order_id: str | None = None
152
+
153
+ @dataclasses.dataclass(kw_only=True, frozen=True)
154
+ class Fill(BrokerResponseEvent):
80
155
  fill_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
81
156
  broker_fill_id: str | None = None
82
157
  associated_order_id: uuid.UUID
158
+ symbol: str
83
159
  side: Models.OrderSide
84
160
  quantity_filled: float
85
161
  fill_price: float
86
162
  commission: float
87
163
  exchange: str = "SIMULATED"
88
164
 
165
+ @dataclasses.dataclass(kw_only=True, frozen=True)
166
+ class OrderRejected(BrokerResponseEvent):
167
+ order_id: uuid.UUID
168
+ reason: Models.OrderRejectionReason
169
+
170
+ @dataclasses.dataclass(kw_only=True, frozen=True)
171
+ class OrderCancelled(BrokerResponseEvent):
172
+ order_id: uuid.UUID
173
+
174
+ @dataclasses.dataclass(kw_only=True, frozen=True)
175
+ class OrderExpired(BrokerResponseEvent):
176
+ order_id: uuid.UUID
177
+
178
+ @dataclasses.dataclass(kw_only=True, frozen=True)
179
+ class CancelRejected(BrokerResponseEvent):
180
+ order_id: uuid.UUID
181
+ reason: Models.CancelRejectionReason
182
+
183
+ @dataclasses.dataclass(kw_only=True, frozen=True)
184
+ class ModifyRejected(BrokerResponseEvent):
185
+ order_id: uuid.UUID
186
+ reason: Models.ModifyRejectionReason
187
+
89
188
 
90
189
  class BaseConsumer(abc.ABC):
91
190
  """
@@ -94,7 +193,9 @@ class BaseConsumer(abc.ABC):
94
193
 
95
194
  def __init__(self) -> None:
96
195
  self._queue: queue.Queue[Events.BaseEvent] = queue.Queue()
97
- self._thread = threading.Thread(target=self._consume, daemon=True)
196
+ self._thread = threading.Thread(
197
+ target=self._consume, name=self.__class__.__name__, daemon=True
198
+ )
98
199
  self._thread.start()
99
200
 
100
201
  @abc.abstractmethod
@@ -0,0 +1,172 @@
1
+ import abc
2
+ import pandas as pd
3
+ import threading
4
+
5
+ from pathlib import Path
6
+ from onesecondtrader.core import Events, Models, event_bus, logger
7
+
8
+
9
+ class DatafeedBase(abc.ABC):
10
+ """
11
+ Base class for all datafeeds.
12
+ """
13
+
14
+ def __init__(self) -> None:
15
+ self._is_connected: bool = False
16
+ self._watched_symbols: set[tuple[str, Models.RecordType]] = set()
17
+ self._lock: threading.Lock = threading.Lock()
18
+
19
+ @abc.abstractmethod
20
+ def watch(self, symbols: list[tuple[str, Models.RecordType]]) -> bool:
21
+ pass
22
+
23
+ @abc.abstractmethod
24
+ def unwatch(self, symbols: list[str]) -> None:
25
+ pass
26
+
27
+
28
+ class SimulatedDatafeedCSV(DatafeedBase):
29
+ """
30
+ CSV-based simulated datafeed for backtesting.
31
+ """
32
+
33
+ csv_path: str | Path = ""
34
+ artificial_delay: float = 0.0
35
+
36
+ def __init__(self) -> None:
37
+ super().__init__()
38
+ self._stop_event = threading.Event()
39
+ self._streaming_thread: threading.Thread | None = None
40
+ self._data_iterator: pd.io.parsers.readers.TextFileReader | None = None
41
+ self._connected_path: str | Path = ""
42
+
43
+ def watch(self, symbols: list[tuple[str, Models.RecordType]]) -> bool:
44
+ with self._lock:
45
+ if not self._is_connected:
46
+ try:
47
+ self._data_iterator = pd.read_csv(
48
+ Path(self.csv_path),
49
+ usecols=[
50
+ "ts_event",
51
+ "rtype",
52
+ "open",
53
+ "high",
54
+ "low",
55
+ "close",
56
+ "volume",
57
+ "symbol",
58
+ ],
59
+ dtype={
60
+ "ts_event": int,
61
+ "rtype": int,
62
+ "open": int,
63
+ "high": int,
64
+ "low": int,
65
+ "close": int,
66
+ "volume": int,
67
+ "symbol": str,
68
+ },
69
+ chunksize=1,
70
+ )
71
+ self._is_connected = True
72
+ self._connected_path = self.csv_path
73
+ logger.info(
74
+ f"{self.__class__.__name__} connected to {self.csv_path}"
75
+ )
76
+ except Exception as e:
77
+ logger.error(f"{self.__class__.__name__} failed to connect: {e}")
78
+ self._data_iterator = None
79
+ self._is_connected = False
80
+ return False
81
+ elif self._connected_path != self.csv_path:
82
+ logger.warning(
83
+ "csv_path changed while connected; unwatch all symbols first"
84
+ )
85
+
86
+ self._watched_symbols.update(symbols)
87
+ formatted = ", ".join(f"{s} ({r.name})" for s, r in symbols)
88
+ logger.info(f"{self.__class__.__name__} watching {formatted}")
89
+
90
+ if not self._streaming_thread or not self._streaming_thread.is_alive():
91
+ self._stop_event.clear()
92
+ self._streaming_thread = threading.Thread(
93
+ target=self._stream, name="CSVDatafeedStreaming", daemon=False
94
+ )
95
+ self._streaming_thread.start()
96
+
97
+ return True
98
+
99
+ def unwatch(self, symbols: list[str]) -> None:
100
+ thread_to_join = None
101
+ with self._lock:
102
+ symbols_set = set(symbols)
103
+ self._watched_symbols.difference_update(
104
+ {
105
+ (symbol, rtype)
106
+ for (symbol, rtype) in self._watched_symbols
107
+ if symbol in symbols_set
108
+ }
109
+ )
110
+ logger.info(f"{self.__class__.__name__} unwatched {', '.join(symbols)}")
111
+ if not self._watched_symbols:
112
+ self._stop_event.set()
113
+ thread_to_join = self._streaming_thread
114
+ self._streaming_thread = None
115
+
116
+ if thread_to_join and thread_to_join.is_alive():
117
+ thread_to_join.join(timeout=5.0)
118
+ if thread_to_join.is_alive():
119
+ logger.warning("Streaming thread did not terminate within timeout")
120
+ else:
121
+ logger.info(f"{self.__class__.__name__} disconnected")
122
+
123
+ def _stream(self) -> None:
124
+ if self._data_iterator is None:
125
+ logger.error("_stream called with no data iterator")
126
+ return
127
+ should_delay = self.artificial_delay > 0
128
+ delay_time = self.artificial_delay
129
+ while not self._stop_event.is_set():
130
+ try:
131
+ chunk = next(self._data_iterator)
132
+ row = chunk.iloc[0]
133
+
134
+ symbol = row["symbol"]
135
+ record_type = Models.RecordType(row["rtype"])
136
+ symbol_key = (symbol, record_type)
137
+
138
+ with self._lock:
139
+ if symbol_key not in self._watched_symbols:
140
+ continue
141
+
142
+ bar_event = Events.IncomingBar(
143
+ ts_event=pd.Timestamp(row["ts_event"], unit="ns", tz="UTC"),
144
+ symbol=symbol,
145
+ record_type=record_type,
146
+ open=row["open"] / 1e9,
147
+ high=row["high"] / 1e9,
148
+ low=row["low"] / 1e9,
149
+ close=row["close"] / 1e9,
150
+ volume=row["volume"],
151
+ )
152
+
153
+ event_bus.publish(bar_event)
154
+
155
+ if should_delay and self._stop_event.wait(delay_time):
156
+ break
157
+ except StopIteration:
158
+ logger.info("CSV datafeed reached end of file")
159
+ break
160
+ except Exception as e:
161
+ logger.error(f"CSV datafeed error reading data: {e}")
162
+ break
163
+
164
+ with self._lock:
165
+ self._data_iterator = None
166
+ self._is_connected = False
167
+
168
+
169
+ simulated_datafeed_csv = SimulatedDatafeedCSV()
170
+ """
171
+ Global instance of SimulatedDatafeedCSV.
172
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.18.0
3
+ Version: 0.20.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
@@ -0,0 +1,8 @@
1
+ onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ onesecondtrader/core.py,sha256=UH8nVc_tZX-xHEaCp1YCyMeHDOtQTbP6sr8a9bqwSyw,7115
3
+ onesecondtrader/datafeeds.py,sha256=KXK2duFRFPo7KuNbG7AqIu-LGXtuuDj20aTOGBE4Cm4,6103
4
+ onesecondtrader/indicators.py,sha256=wGn-5v8L1gepMP45KcVrEo-f2ReOCD3r8lva9aEIUnY,3199
5
+ onesecondtrader-0.20.0.dist-info/METADATA,sha256=6FN1iee59eZiuQz1Y8NsY4EeOhayXxx6xGsmKt3UdOg,9682
6
+ onesecondtrader-0.20.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
7
+ onesecondtrader-0.20.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
8
+ onesecondtrader-0.20.0.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- onesecondtrader/core.py,sha256=uMy2eYLtzfSPC_99kjft7L3DL4SRi1ZBnnSYyd_XrIo,3921
3
- onesecondtrader/indicators.py,sha256=wGn-5v8L1gepMP45KcVrEo-f2ReOCD3r8lva9aEIUnY,3199
4
- onesecondtrader-0.18.0.dist-info/METADATA,sha256=48Xk2OPiF57MkcpqFnyAG328SWljqyTEO5XmFS0VDrI,9682
5
- onesecondtrader-0.18.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
6
- onesecondtrader-0.18.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
- onesecondtrader-0.18.0.dist-info/RECORD,,