onesecondtrader 0.53.0__py3-none-any.whl → 0.54.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,11 @@
1
+ """
2
+ Provides data feed components for ingesting market data into the system.
3
+ """
4
+
5
+ from .base import DatafeedBase
6
+ from .simulated import SimulatedDatafeed
7
+
8
+ __all__ = [
9
+ "DatafeedBase",
10
+ "SimulatedDatafeed",
11
+ ]
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+
5
+ from onesecondtrader import events, messaging, models
6
+
7
+
8
+ class DatafeedBase(abc.ABC):
9
+ """
10
+ Abstract base class for market data feed implementations.
11
+
12
+ A data feed is responsible for connecting to an external data source, managing symbol and bar-period subscriptions, and publishing market data events onto the system event bus.
13
+
14
+ Concrete subclasses implement the mechanics of connectivity, subscription handling, and lifecycle management for a specific data source.
15
+ """
16
+
17
+ def __init__(self, event_bus: messaging.EventBus) -> None:
18
+ """
19
+ Initialize the data feed with an event bus.
20
+
21
+ parameters:
22
+ event_bus:
23
+ Event bus used to publish market data events produced by this data feed.
24
+ """
25
+ self._event_bus = event_bus
26
+
27
+ def _publish(self, event: events.EventBase) -> None:
28
+ """
29
+ Publish a market data event to the event bus.
30
+
31
+ This method is intended for use by subclasses to forward incoming data from the external source into the internal event-driven system.
32
+
33
+ parameters:
34
+ event:
35
+ Event instance to be published.
36
+ """
37
+ self._event_bus.publish(event)
38
+
39
+ @abc.abstractmethod
40
+ def connect(self) -> None:
41
+ """
42
+ Establish a connection to the underlying data source.
43
+
44
+ Implementations should perform any required setup, authentication, or resource allocation needed before subscriptions can be registered.
45
+ """
46
+ pass
47
+
48
+ @abc.abstractmethod
49
+ def disconnect(self) -> None:
50
+ """
51
+ Terminate the connection to the underlying data source.
52
+
53
+ Implementations should release resources and ensure that no further events are published after disconnection.
54
+ """
55
+ pass
56
+
57
+ @abc.abstractmethod
58
+ def subscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
59
+ """
60
+ Subscribe to market data for one or more symbols at a given bar period.
61
+
62
+ parameters:
63
+ symbols:
64
+ Instrument symbols to subscribe to, interpreted according to the conventions of the underlying data source.
65
+ bar_period:
66
+ Bar aggregation period specifying the granularity of market data.
67
+ """
68
+ pass
69
+
70
+ @abc.abstractmethod
71
+ def unsubscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
72
+ """
73
+ Cancel existing subscriptions for one or more symbols at a given bar period.
74
+
75
+ parameters:
76
+ symbols:
77
+ Instrument symbols for which subscriptions should be removed.
78
+ bar_period:
79
+ Bar aggregation period associated with the subscriptions.
80
+ """
81
+ pass
82
+
83
+ def wait_until_complete(self) -> None:
84
+ """
85
+ Block until the data feed has completed all pending work.
86
+
87
+ This method may be overridden by subclasses that perform asynchronous ingestion or background processing.
88
+ The default implementation does nothing.
89
+ """
90
+ pass
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import os
5
+ import sqlite3
6
+ import threading
7
+
8
+ from onesecondtrader import events, messaging, models
9
+ from onesecondtrader.datafeeds.base import DatafeedBase
10
+
11
+ _RTYPE_MAP = {
12
+ models.BarPeriod.SECOND: 32,
13
+ models.BarPeriod.MINUTE: 33,
14
+ models.BarPeriod.HOUR: 34,
15
+ models.BarPeriod.DAY: 35,
16
+ }
17
+
18
+ _RTYPE_TO_BAR_PERIOD = {v: k for k, v in _RTYPE_MAP.items()}
19
+
20
+
21
+ class SimulatedDatafeed(DatafeedBase):
22
+ """
23
+ Simulated market data feed backed by a secmaster SQLite database.
24
+
25
+ This datafeed replays historical OHLCV bars from a secmaster database, resolving symbols
26
+ via time-bounded symbology mappings. Bars are delivered in timestamp order, with all bars
27
+ sharing the same timestamp published before calling `wait_until_system_idle`.
28
+
29
+ Subclasses must set `publisher_name`, `dataset`, and `symbol_type` as class attributes to
30
+ scope the feed to a specific data source. The database must contain publishers with numeric
31
+ `source_instrument_id` values; symbol-only publishers (e.g., yfinance) are not supported.
32
+ """
33
+
34
+ db_path: str = ""
35
+ publisher_name: str = ""
36
+ dataset: str = ""
37
+ symbol_type: str = ""
38
+ price_scale: float = 1e9
39
+ start_ts: int | None = None
40
+ end_ts: int | None = None
41
+
42
+ def __init__(self, event_bus: messaging.EventBus) -> None:
43
+ """
44
+ Parameters:
45
+ event_bus:
46
+ Event bus used to publish bar events and synchronize with subscribers.
47
+ """
48
+ super().__init__(event_bus)
49
+ self._db_path = self.db_path or os.environ.get(
50
+ "SECMASTER_DB_PATH", "secmaster.db"
51
+ )
52
+ if not self.publisher_name:
53
+ raise ValueError("publisher_name is required")
54
+ if not self.dataset:
55
+ raise ValueError("dataset is required")
56
+ if not self.symbol_type:
57
+ raise ValueError("symbol_type is required")
58
+ self._subscriptions: set[tuple[str, models.BarPeriod]] = set()
59
+ self._subscriptions_lock = threading.Lock()
60
+ self._connection: sqlite3.Connection | None = None
61
+ self._thread: threading.Thread | None = None
62
+ self._stop_event = threading.Event()
63
+ self._publisher_id: int | None = None
64
+
65
+ def connect(self) -> None:
66
+ """
67
+ Open a connection to the secmaster database and resolve the publisher.
68
+
69
+ If already connected, this method returns immediately.
70
+ """
71
+ if self._connection:
72
+ return
73
+ self._connection = sqlite3.connect(self._db_path, check_same_thread=False)
74
+ self._connection.execute("PRAGMA foreign_keys = ON")
75
+ self._connection.execute("PRAGMA journal_mode = WAL")
76
+ row = self._connection.execute(
77
+ "SELECT publisher_id FROM publishers WHERE name = ? AND dataset = ?",
78
+ (self.publisher_name, self.dataset),
79
+ ).fetchone()
80
+ if row is None:
81
+ raise ValueError(
82
+ f"Publisher not found: {self.publisher_name}/{self.dataset}"
83
+ )
84
+ self._publisher_id = row[0]
85
+
86
+ def disconnect(self) -> None:
87
+ """
88
+ Close the database connection and stop any active streaming.
89
+
90
+ If not connected, this method returns immediately.
91
+ """
92
+ if not self._connection:
93
+ return
94
+ self._stop_event.set()
95
+ if self._thread and self._thread.is_alive():
96
+ self._thread.join()
97
+ self._connection.close()
98
+ self._connection = None
99
+ self._publisher_id = None
100
+
101
+ def subscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
102
+ """
103
+ Register symbols for bar delivery at the specified period.
104
+
105
+ Parameters:
106
+ symbols:
107
+ List of ticker symbols to subscribe.
108
+ bar_period:
109
+ Bar aggregation period for the subscription.
110
+ """
111
+ with self._subscriptions_lock:
112
+ self._subscriptions.update((s, bar_period) for s in symbols)
113
+
114
+ def unsubscribe(self, symbols: list[str], bar_period: models.BarPeriod) -> None:
115
+ """
116
+ Remove symbols from bar delivery at the specified period.
117
+
118
+ Parameters:
119
+ symbols:
120
+ List of ticker symbols to unsubscribe.
121
+ bar_period:
122
+ Bar aggregation period for the subscription.
123
+ """
124
+ with self._subscriptions_lock:
125
+ self._subscriptions.difference_update((s, bar_period) for s in symbols)
126
+
127
+ def wait_until_complete(self) -> None:
128
+ """
129
+ Stream all subscribed bars and block until delivery is complete.
130
+
131
+ Bars are published in timestamp order. After each timestamp batch, the method
132
+ waits for all event bus subscribers to become idle before proceeding.
133
+ """
134
+ with self._subscriptions_lock:
135
+ has_subscriptions = bool(self._subscriptions)
136
+ if not has_subscriptions:
137
+ return
138
+ if self._thread is None or not self._thread.is_alive():
139
+ self._stop_event.clear()
140
+ self._thread = threading.Thread(
141
+ target=self._stream,
142
+ name=self.__class__.__name__,
143
+ daemon=False,
144
+ )
145
+ self._thread.start()
146
+ self._thread.join()
147
+
148
+ def _stream(self) -> None:
149
+ if not self._connection or self._publisher_id is None:
150
+ return
151
+
152
+ with self._subscriptions_lock:
153
+ subscriptions = list(self._subscriptions)
154
+ if not subscriptions:
155
+ return
156
+
157
+ symbols = list({symbol for symbol, _ in subscriptions})
158
+ rtypes = list({_RTYPE_MAP[bp] for _, bp in subscriptions})
159
+ subscription_set = {(symbol, _RTYPE_MAP[bp]) for symbol, bp in subscriptions}
160
+
161
+ params: list = [self._publisher_id, self.symbol_type]
162
+ params.extend(symbols)
163
+ params.extend(rtypes)
164
+ if self.start_ts is not None:
165
+ params.append(self.start_ts)
166
+ if self.end_ts is not None:
167
+ params.append(self.end_ts)
168
+
169
+ query = f"""
170
+ SELECT s.symbol, o.rtype, o.ts_event, o.open, o.high, o.low, o.close, o.volume
171
+ FROM ohlcv o
172
+ JOIN instruments i ON i.instrument_id = o.instrument_id
173
+ JOIN symbology s
174
+ ON s.publisher_ref = i.publisher_ref
175
+ AND s.source_instrument_id = i.source_instrument_id
176
+ AND date(o.ts_event / 1000000000, 'unixepoch') >= s.start_date
177
+ AND date(o.ts_event / 1000000000, 'unixepoch') <= s.end_date
178
+ WHERE i.publisher_ref = ?
179
+ AND s.symbol_type = ?
180
+ AND s.symbol IN ({",".join("?" * len(symbols))})
181
+ AND o.rtype IN ({",".join("?" * len(rtypes))})
182
+ {"AND o.ts_event >= ?" if self.start_ts is not None else ""}
183
+ {"AND o.ts_event <= ?" if self.end_ts is not None else ""}
184
+ ORDER BY o.ts_event, s.symbol
185
+ """
186
+
187
+ rows = self._connection.execute(query, params)
188
+
189
+ def to_bar(row):
190
+ symbol, rtype, ts_event, open_, high, low, close, volume = row
191
+ if (symbol, rtype) not in subscription_set:
192
+ return None
193
+ return events.market.BarReceived(
194
+ ts_event_ns=ts_event,
195
+ symbol=symbol,
196
+ bar_period=_RTYPE_TO_BAR_PERIOD[rtype],
197
+ open=open_ / self.price_scale,
198
+ high=high / self.price_scale,
199
+ low=low / self.price_scale,
200
+ close=close / self.price_scale,
201
+ volume=volume,
202
+ )
203
+
204
+ for _, group in itertools.groupby(rows, key=lambda r: r[2]):
205
+ if self._stop_event.is_set():
206
+ return
207
+ for bar in filter(None, map(to_bar, group)):
208
+ self._publish(bar)
209
+ self._event_bus.wait_until_system_idle()
@@ -559,6 +559,8 @@ def _ingest_symbology(
559
559
  batch,
560
560
  )
561
561
 
562
+ _validate_no_overlapping_symbology(con, publisher_id, symbol_type)
563
+
562
564
  logger.info(
563
565
  "Completed symbology ingest from %s (%d mappings)", json_path.name, count
564
566
  )
@@ -566,6 +568,37 @@ def _ingest_symbology(
566
568
  return count
567
569
 
568
570
 
571
+ def _validate_no_overlapping_symbology(
572
+ con: sqlite3.Connection,
573
+ publisher_id: int,
574
+ symbol_type: str,
575
+ ) -> None:
576
+ query = """
577
+ WITH ordered AS (
578
+ SELECT
579
+ symbol,
580
+ start_date,
581
+ end_date,
582
+ LEAD(start_date) OVER (
583
+ PARTITION BY symbol ORDER BY start_date
584
+ ) AS next_start
585
+ FROM symbology
586
+ WHERE publisher_ref = ? AND symbol_type = ?
587
+ )
588
+ SELECT symbol, start_date, end_date, next_start
589
+ FROM ordered
590
+ WHERE next_start IS NOT NULL AND end_date > next_start
591
+ LIMIT 1
592
+ """
593
+ row = con.execute(query, (publisher_id, symbol_type)).fetchone()
594
+ if row:
595
+ symbol, start, end, next_start = row
596
+ raise ValueError(
597
+ f"Overlapping symbology detected for symbol={symbol!r}: "
598
+ f"segment [{start}, {end}] overlaps with next segment starting {next_start}"
599
+ )
600
+
601
+
569
602
  def _enable_bulk_loading(con: sqlite3.Connection) -> None:
570
603
  con.execute("PRAGMA journal_mode = WAL")
571
604
  con.execute("PRAGMA synchronous = NORMAL")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onesecondtrader
3
- Version: 0.53.0
3
+ Version: 0.54.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
@@ -2,6 +2,9 @@ onesecondtrader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  onesecondtrader/brokers/__init__.py,sha256=CmOhwKOayuYCeg5KRiTp4fc8nSDnsLzIBkUNWhUevlo,271
3
3
  onesecondtrader/brokers/base.py,sha256=I4tQFr7P1DF5QAWb3I9tHz5D_zTleH8vEXS2WsH55DE,3531
4
4
  onesecondtrader/brokers/simulated.py,sha256=ZY39a84J2BmC2ADMkrSRzNBumPXudVBz2eUSnnHb0LM,17930
5
+ onesecondtrader/datafeeds/__init__.py,sha256=uu67phyj4ruIlMqrsbFQ_mP5u2we881WgQoOhuN_KvU,214
6
+ onesecondtrader/datafeeds/base.py,sha256=MOSUCuVfzPpqGf_T7O2aW64m9H2J4oa5u3VClQwZASE,3089
7
+ onesecondtrader/datafeeds/simulated.py,sha256=LjPJU6RTt7i9e0pE0YSrtjmGLAQOGmmIpWSQMS9Dh6g,7840
5
8
  onesecondtrader/events/__init__.py,sha256=1T7hJA6afxClEXvvnbXtHu9iMyhduRdJZWlg4ObWaKE,222
6
9
  onesecondtrader/events/base.py,sha256=WpLo1bSKJe7Poh2IuDDCiBYZo9vE8mkq3cQlUpyTXsY,850
7
10
  onesecondtrader/events/market/__init__.py,sha256=49z6maexBIDkAjIfkLbYzSZWEbyTpQ_HEEgT0eacrDo,132
@@ -37,8 +40,8 @@ onesecondtrader/models/trade_sides.py,sha256=Pf9BpxoUxqgKC_EKAExfSqgfIIK9NW-RpJE
37
40
  onesecondtrader/secmaster/__init__.py,sha256=XAouFrbRTpWWp8U43LQUkj8EZvJR9ydlI9fVdJjH1BY,294
38
41
  onesecondtrader/secmaster/schema_versions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
42
  onesecondtrader/secmaster/schema_versions/secmaster_schema_v1.sql,sha256=E41rVhpYlXiC_GR4cw1bNQW_8Fdy8d-s1RJASIUCijM,12974
40
- onesecondtrader/secmaster/utils.py,sha256=SHOOq79hbDlCg907oA88DPMDwCyQ4iTH3w3y2TrbJ8Y,18539
41
- onesecondtrader-0.53.0.dist-info/METADATA,sha256=n6hiAetcj2QUwOCeu5BUvoe-TSUmzZ5Xei5uPAUJlFE,9951
42
- onesecondtrader-0.53.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
43
- onesecondtrader-0.53.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
44
- onesecondtrader-0.53.0.dist-info/RECORD,,
43
+ onesecondtrader/secmaster/utils.py,sha256=d8PMSNLWVr10G0CSdL9vF-j_9jTfvOLxC-6k42x6LRU,19587
44
+ onesecondtrader-0.54.0.dist-info/METADATA,sha256=iVpo6U1CIhN_NOYh1yzyqq25GVcadtP26VL93M6NehY,9951
45
+ onesecondtrader-0.54.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
46
+ onesecondtrader-0.54.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
47
+ onesecondtrader-0.54.0.dist-info/RECORD,,