onesecondtrader 0.31.0__py3-none-any.whl → 0.33.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,5 +1,49 @@
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
1
+ __all__ = [
2
+ "BarPeriod",
3
+ "BarProcessed",
4
+ "BarReceived",
5
+ "BrokerBase",
6
+ "Close",
7
+ "FillRecord",
8
+ "High",
9
+ "Indicator",
10
+ "InputSource",
11
+ "Low",
12
+ "Open",
13
+ "OrderFilled",
14
+ "OrderRecord",
15
+ "OrderSide",
16
+ "OrderSubmission",
17
+ "OrderType",
18
+ "SimpleMovingAverage",
19
+ "SimulatedBroker",
20
+ "SMACrossover",
21
+ "StrategyBase",
22
+ "Volume",
23
+ ]
24
+
25
+ from onesecondtrader.brokers import BrokerBase, SimulatedBroker
26
+ from onesecondtrader.events import (
27
+ BarProcessed,
28
+ BarReceived,
29
+ OrderFilled,
30
+ OrderSubmission,
31
+ )
32
+ from onesecondtrader.indicators import (
33
+ Close,
34
+ High,
35
+ Indicator,
36
+ Low,
37
+ Open,
38
+ SimpleMovingAverage,
39
+ Volume,
40
+ )
41
+ from onesecondtrader.models import (
42
+ BarPeriod,
43
+ FillRecord,
44
+ InputSource,
45
+ OrderRecord,
46
+ OrderSide,
47
+ OrderType,
48
+ )
49
+ from onesecondtrader.strategies import SMACrossover, StrategyBase
@@ -13,9 +13,8 @@ class Indicator(abc.ABC):
13
13
  def __init__(self, max_history: int = 100, plot_at: int = 99) -> None:
14
14
  self._lock = threading.Lock()
15
15
  self._max_history = max(1, int(max_history))
16
- # Keyed by symbol only - each strategy subscribes to one timeframe, so the indicator only sees bars from that timeframe.
17
- self._history: dict[str, collections.deque[float]] = {}
18
- # 0 = main price chart, 1-98 = subcharts, 99 = no plot
16
+ self._current_symbol: str = ""
17
+ self._history_data: dict[str, collections.deque[float]] = {}
19
18
  self._plot_at = plot_at
20
19
 
21
20
  @property
@@ -29,22 +28,33 @@ class Indicator(abc.ABC):
29
28
 
30
29
  def update(self, incoming_bar: events.BarReceived) -> None:
31
30
  symbol = incoming_bar.symbol
31
+ self._current_symbol = symbol
32
32
  value = self._compute_indicator(incoming_bar)
33
33
  with self._lock:
34
- if symbol not in self._history:
35
- self._history[symbol] = collections.deque(maxlen=self._max_history)
36
- self._history[symbol].append(value)
34
+ if symbol not in self._history_data:
35
+ self._history_data[symbol] = collections.deque(maxlen=self._max_history)
36
+ self._history_data[symbol].append(value)
37
37
 
38
- def latest(self, symbol: str) -> float:
38
+ @property
39
+ def latest(self) -> float:
39
40
  with self._lock:
40
- history = self._history.get(symbol, collections.deque())
41
- return history[-1] if history else np.nan
41
+ h = self._history_data.get(self._current_symbol, collections.deque())
42
+ return h[-1] if h else np.nan
42
43
 
43
- def history(self, symbol: str) -> collections.deque[float]:
44
+ @property
45
+ def history(self) -> collections.deque[float]:
44
46
  with self._lock:
45
- h = self._history.get(symbol, collections.deque())
47
+ h = self._history_data.get(self._current_symbol, collections.deque())
46
48
  return collections.deque(h, maxlen=self._max_history)
47
49
 
50
+ def __getitem__(self, index: int) -> float:
51
+ # Returns np.nan on out-of-bounds access. Since np.nan comparisons always
52
+ # return False, strategies can skip explicit length checks.
53
+ try:
54
+ return self.history[index]
55
+ except IndexError:
56
+ return np.nan
57
+
48
58
  @property
49
59
  def plot_at(self) -> int:
50
60
  return self._plot_at
@@ -3,7 +3,10 @@ __all__ = [
3
3
  "InputSource",
4
4
  "OrderSide",
5
5
  "OrderType",
6
+ "OrderRecord",
7
+ "FillRecord",
6
8
  ]
7
9
 
8
10
  from .data import BarPeriod, InputSource
9
11
  from .orders import OrderSide, OrderType
12
+ from .records import OrderRecord, FillRecord
@@ -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,7 @@
1
+ __all__ = [
2
+ "StrategyBase",
3
+ "SMACrossover",
4
+ ]
5
+
6
+ from .base import StrategyBase
7
+ from .sma_crossover import SMACrossover
@@ -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
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
@@ -0,0 +1,35 @@
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.31.0
3
+ Version: 0.33.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=edb0kipMTx0p-utX0tc4J8IbhFrZprX200oxhX8LzH4,241
1
+ onesecondtrader/__init__.py,sha256=CLUi62mHp8tBzp-58mig3M4SEhPELPihHfQQzcD9Nz8,901
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
@@ -10,14 +10,18 @@ onesecondtrader/events/responses.py,sha256=w_BH1nkkPyxQjh30EXEVFcUGDoMprFc2PuAaq
10
10
  onesecondtrader/indicators/__init__.py,sha256=hRg3FCP1FT7LYOLzztybWn48gTR5QvewzzdELPYbdoY,239
11
11
  onesecondtrader/indicators/averages.py,sha256=DpRRdY5G5ze3jwNOV19PPjV6slA0IEeOOla67yChi2Y,1900
12
12
  onesecondtrader/indicators/bar.py,sha256=0H07mKNiUx5cE1fQvx1oPVY0R_MXcmAAgsLek175vTk,1115
13
- onesecondtrader/indicators/base.py,sha256=WGqjp9mmkR2PZ5ZC8zL9ddeTfUrB0hX3bjnq1W4HKuI,1658
13
+ onesecondtrader/indicators/base.py,sha256=64HJD8JWBnUdR7PLd02N3hbNrRfKMjWHEKGeSyMRms8,1889
14
14
  onesecondtrader/messaging/__init__.py,sha256=vMRDabHBgse_vZRTRFtnU8M8v2sY_o4pHjGzgu3hp3E,115
15
15
  onesecondtrader/messaging/eventbus.py,sha256=Y8VbDZlEz8Q6KcCkfXRKsVIixsctBMRW1a5ANw297Ls,1576
16
16
  onesecondtrader/messaging/subscriber.py,sha256=ImpFmu5IstLXLoKVMaebmLp5MXN6225vHLdTL1ZOPvw,2106
17
- onesecondtrader/models/__init__.py,sha256=0J91ixFxYRaZ9t-dm_LVh7xh1ukb3G7H1GgelZg68as,167
17
+ onesecondtrader/models/__init__.py,sha256=7amHCQ6BAhHKps0ke63E-zh8IJNmkdDogZq-PfBukMs,249
18
18
  onesecondtrader/models/data.py,sha256=fBmddVl6EXYC5u2UnvQ59DXAXeZeIb48KP1ZdeTL52A,322
19
19
  onesecondtrader/models/orders.py,sha256=y6Ar-6fMqaOd_hRnRGvfWUF0Z13H_2hfTOW3ROOk0A8,254
20
- onesecondtrader-0.31.0.dist-info/METADATA,sha256=OnASDmSzTXcJ2jmviGqe8qkF3xp8g3B4fAmicO3rDF0,9682
21
- onesecondtrader-0.31.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
22
- onesecondtrader-0.31.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
23
- onesecondtrader-0.31.0.dist-info/RECORD,,
20
+ onesecondtrader/models/records.py,sha256=vdCWBtoDQs5R4iB_8_3fXkxWEvoCxOssk9XBnS4l7Vk,599
21
+ onesecondtrader/strategies/__init__.py,sha256=5TlEckz3RnwZTs1Isj0wJ_9Og5R9MMXBL90Vu9b45io,126
22
+ onesecondtrader/strategies/base.py,sha256=kIi6by4Y8YuB9gPMPMr2Unm5_i9SGAANyiW2UaHiRO0,11206
23
+ onesecondtrader/strategies/sma_crossover.py,sha256=s2u_uL_D5CrZTACiAbojnrLrLf4jqIPdfOCiNDEIsDA,1119
24
+ onesecondtrader-0.33.0.dist-info/METADATA,sha256=U06iSqkTLBBhLzZCiIl5r-f4-FjpVPF6Qimcgq_kunk,9682
25
+ onesecondtrader-0.33.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
26
+ onesecondtrader-0.33.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
27
+ onesecondtrader-0.33.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