onesecondtrader 0.15.0__py3-none-any.whl → 0.17.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,177 +0,0 @@
1
- import threading
2
-
3
- from onesecondtrader import messaging
4
- from onesecondtrader.messaging import events
5
- from onesecondtrader.strategies import base_strategy
6
- from onesecondtrader.monitoring import console
7
- from onesecondtrader.core import models
8
-
9
-
10
- class Portfolio:
11
- def __init__(self, event_bus: messaging.EventBus | None = None):
12
- """
13
- Initialize the Portfolio class and subscribe to events.
14
- Most importantly, the `symbol_to_strategy` registry is initialized,
15
- which keeps track of which symbols are currently assigned to which strategy
16
- in order to enforce exclusive symbol ownership.
17
-
18
- Args:
19
- event_bus (messaging.EventBus | None): Event bus to use; defaults to
20
- messaging.system_event_bus when None.
21
-
22
- Attributes:
23
- self._lock (threading.Lock): Lock for thread-safe operations.
24
- self.event_bus (messaging.EventBus): Event bus used for communication
25
- between the trading infrastructure's components.
26
- self.symbols_to_strategy (dict[str, base_strategy.Strategy]): Registry of
27
- symbols to strategies.
28
- """
29
- # ------------------------------------------------------------------------------
30
- # INITIALIZE LOCK FOR THREAD-SAFE OPERATIONS
31
- self._lock: threading.Lock = threading.Lock()
32
-
33
- # ------------------------------------------------------------------------------
34
- # INITIALIZE EVENT BUS AND SUBSCRIBE TO EVENTS
35
- self.event_bus: messaging.EventBus = (
36
- event_bus if event_bus else messaging.system_event_bus
37
- )
38
- self.event_bus.subscribe(events.Strategy.SymbolRelease, self.on_symbol_release)
39
-
40
- # ------------------------------------------------------------------------------
41
- # INITIALIZE SYMBOLS TO STRATEGY REGISTRY
42
- self.symbols_to_strategy: dict[str, base_strategy.Strategy] = {}
43
-
44
- def on_symbol_release(self, event: messaging.events.Base.Event) -> None:
45
- """
46
- Event handler for symbol release events (`events.Strategy.SymbolRelease`).
47
- The symbol is removed from the `symbols_to_strategy` registry.
48
-
49
- Args:
50
- event (messaging.events.Base.Event): Symbol release event.
51
- """
52
- # ------------------------------------------------------------------------------
53
- # IGNORE UNRELATED EVENT TYPES
54
- if not isinstance(event, events.Strategy.SymbolRelease):
55
- return
56
-
57
- # ------------------------------------------------------------------------------
58
- # RELEASE SYMBOL FROM STRATEGY
59
- symbol = event.symbol
60
- with self._lock:
61
- if symbol in self.symbols_to_strategy:
62
- del self.symbols_to_strategy[symbol]
63
- console.logger.info(
64
- f"on_symbol_release: symbol {symbol} released from "
65
- f"{getattr(event.strategy, 'name', type(event.strategy).__name__)}"
66
- )
67
- else:
68
- console.logger.warning(
69
- f"on_symbol_release: symbol {symbol} not owned by "
70
- f"{getattr(event.strategy, 'name', type(event.strategy).__name__)}"
71
- )
72
-
73
- def assign_symbols(
74
- self, strategy_instance: base_strategy.Strategy, symbols: list[str]
75
- ) -> bool:
76
- """
77
- Assign a list of symbols to a strategy if no conflicts exist and notify the
78
- strategy of the assignment.
79
-
80
- Args:
81
- strategy_instance (base_strategy.Strategy): Strategy instance to assign
82
- symbols to.
83
- symbols (list[str]): List of symbols to assign.
84
- """
85
- # ------------------------------------------------------------------------------
86
- # VALIDATE THAT INSTANCE IS A SUBCLASS OF base_strategy.Strategy
87
- if not isinstance(strategy_instance, base_strategy.Strategy):
88
- console.logger.error("assign_symbols: strategy must inherit from Strategy")
89
- return False
90
-
91
- # ------------------------------------------------------------------------------
92
- # CHECK FOR CONFLICTS
93
- non_conflicting: list[str] = []
94
- conflicting: list[str] = []
95
- with self._lock:
96
- for symbol in symbols:
97
- owner = self.symbols_to_strategy.get(symbol)
98
- if owner is None:
99
- non_conflicting.append(symbol)
100
- else:
101
- conflicting.append(symbol)
102
- if conflicting:
103
- console.logger.warning(
104
- "assign_symbols: symbols not assigned due to conflicts; "
105
- "use Portfolio.assign_symbols(...) after resolving. "
106
- f"non_conflicting={non_conflicting}, conflicts={conflicting}"
107
- )
108
- return False
109
- else:
110
- # --------------------------------------------------------------------------
111
- # ASSIGN SYMBOLS TO REGISTRY
112
- for symbol in symbols:
113
- self.symbols_to_strategy[symbol] = strategy_instance
114
-
115
- # --------------------------------------------------------------------------
116
- # PUBLISH SYMBOL ASSIGNMENT EVENT
117
- # noinspection PyArgumentList
118
- self.event_bus.publish(
119
- events.Strategy.SymbolAssignment(
120
- strategy=strategy_instance,
121
- symbol_list=symbols,
122
- )
123
- )
124
- return True
125
-
126
- def unassign_symbols(
127
- self,
128
- symbols: list[str],
129
- shutdown_mode: models.SymbolShutdownMode = models.SymbolShutdownMode.SOFT,
130
- ) -> bool:
131
- """
132
- Unassign a list of symbols from their owning strategy if all of them have
133
- previously been assigned to a strategy.
134
- Calling this methods will request the owning strategy to stop trading the symbol
135
- in the manner dictated via the `shutdown_mode` argument (default to soft
136
- shutdown, i.e. wait for open positions to close naturally and release symbols
137
- once they are flat).
138
- After the owning strategy has released the symbol, the symbol is unassigned from
139
- the portfolio via the `on_symbol_release` event handler.
140
-
141
- Args:
142
- symbols (list[str]): List of symbols to unassign.
143
- shutdown_mode (models.SymbolShutdownMode): Shutdown mode to use. Defaults
144
- to `models.SymbolShutdownMode.SOFT`.
145
- """
146
- # ------------------------------------------------------------------------------
147
- # CHECK THAT SYMBOLS ARE REGISTERED
148
- conflicting: list[str] = []
149
- with self._lock:
150
- for symbol in symbols:
151
- if symbol not in self.symbols_to_strategy:
152
- conflicting.append(symbol)
153
- if conflicting:
154
- console.logger.warning(
155
- "unassign_symbols: symbols not unassigned due to conflicts; "
156
- f"conflicts={conflicting}. "
157
- f"Use Portfolio.unassign_symbols(...) after resolving."
158
- )
159
- return False
160
- else:
161
- # ----------------------------------------------------------------------
162
- # PUBLISH STOP TRADING SYMBOL EVENT FOR EACH SYMBOL
163
- for symbol in symbols:
164
- # noinspection PyArgumentList
165
- self.event_bus.publish(
166
- events.Strategy.StopTradingSymbol(
167
- strategy=self.symbols_to_strategy[symbol],
168
- symbol=symbol,
169
- shutdown_mode=shutdown_mode,
170
- )
171
- )
172
- console.logger.info(
173
- f"unassign_symbols: trading stop for {symbol} trading strategy "
174
- f"{self.symbols_to_strategy[symbol]} requested with shutdown"
175
- f"mode {shutdown_mode.name}"
176
- )
177
- return True
File without changes
File without changes
@@ -1,54 +0,0 @@
1
- """
2
- This module provides the base class for all datafeeds.
3
- """
4
-
5
- import abc
6
- from onesecondtrader import messaging
7
- from onesecondtrader.core import models
8
-
9
-
10
- class BaseDatafeed(abc.ABC):
11
- """
12
- Base class for all datafeeds.
13
- """
14
-
15
- def __init__(self, event_bus: messaging.EventBus | None = None):
16
- self.event_bus: messaging.EventBus = (
17
- event_bus if event_bus else messaging.system_event_bus
18
- )
19
-
20
- @abc.abstractmethod
21
- def connect(self):
22
- """
23
- Connect to the datafeed.
24
- """
25
- pass
26
-
27
- @abc.abstractmethod
28
- def watch(self, symbols: list[tuple[str, models.RecordType]]):
29
- """
30
- Start watching symbols.
31
-
32
- Args:
33
- symbols (list[tuple[str, models.TimeFrame]]): List of symbols to watch with
34
- their respective timeframes.
35
- """
36
- pass
37
-
38
- @abc.abstractmethod
39
- def unwatch(self, symbols: list[tuple[str, models.RecordType]]):
40
- """
41
- Stop watching symbols.
42
-
43
- Args:
44
- symbols (list[tuple[str, models.TimeFrame]]): List of symbols to stop
45
- watching with their respective timeframes.
46
- """
47
- pass
48
-
49
- @abc.abstractmethod
50
- def disconnect(self):
51
- """
52
- Disconnect from the datafeed.
53
- """
54
- pass
@@ -1,297 +0,0 @@
1
- """
2
- This module provides a CSV-based simulated live datafeed.
3
- """
4
-
5
- import os
6
- import pandas as pd
7
- import threading
8
- import time
9
- from pathlib import Path
10
- from dotenv import load_dotenv
11
- from onesecondtrader.messaging import events, eventbus
12
- from onesecondtrader.core import models
13
- from onesecondtrader.monitoring import console
14
- from onesecondtrader.datafeeds import base_datafeed
15
- from pandas.io.parsers.readers import TextFileReader
16
-
17
-
18
- class CSVDatafeed(base_datafeed.BaseDatafeed):
19
- """
20
- CSV-based simulated live datafeed.
21
-
22
- Only one instance of any BaseDatafeed subclass can exist at a time.
23
- """
24
-
25
- _instance = None
26
-
27
- def __init__(
28
- self,
29
- event_bus: eventbus.EventBus,
30
- csv_path: str | Path | None = None,
31
- streaming_delay: float | None = None,
32
- ):
33
- """
34
- Initialize CSV datafeed.
35
-
36
- Args:
37
- event_bus: Event bus used to publish market data events.
38
- csv_path: Optional path to CSV file. Overrides CSV_PATH env var.
39
- streaming_delay: Optional delay in seconds between processing rows.
40
- Overrides CSV_STREAMING_DELAY env var.
41
-
42
- Attributes:
43
- self.csv_path (Path | None): Path to CSV file.
44
- self.data_iterator (TextFileReader | None): Iterator for reading CSV.
45
- self._watched_symbols (set[tuple[str, models.RecordType]]): Set of
46
- symbols and record types currently being watched.
47
- self._streaming_thread (threading.Thread | None): Background thread
48
- for streaming data.
49
- self._symbols_lock (threading.Lock): Lock to protect _watched_symbols
50
- from concurrent access.
51
- self._streaming_delay (float): Delay in seconds between processing
52
- CSV rows (from CSV_STREAMING_DELAY env var, set in connect()).
53
- self._init_csv_path (str | Path | None): CSV path provided during
54
- initialization.
55
- self._init_streaming_delay (float | None): Streaming delay provided
56
- during initialization.
57
- """
58
- if CSVDatafeed._instance is not None:
59
- console.logger.warning(
60
- f"Only one BaseDatafeed instance allowed. "
61
- f"Current: {type(CSVDatafeed._instance).__name__}. "
62
- f"Initialization failed."
63
- )
64
- return
65
-
66
- super().__init__(event_bus)
67
- CSVDatafeed._instance = self
68
-
69
- self.csv_path: Path | None = None
70
- self.data_iterator: TextFileReader | None = None
71
- self._watched_symbols: set[tuple[str, models.RecordType]] = set()
72
- self._stop_event = threading.Event()
73
- self._streaming_thread: threading.Thread | None = None
74
- self._symbols_lock: threading.Lock = threading.Lock()
75
- self._streaming_delay: float = 0.0
76
-
77
- self._init_csv_path: str | Path | None = csv_path
78
- self._init_streaming_delay: float | None = streaming_delay
79
-
80
- def connect(self):
81
- """
82
- Connect to CSV file specified in .env file (CSV_PATH variable) and
83
- create data iterator.
84
- """
85
- load_dotenv()
86
-
87
- if self._init_csv_path is not None:
88
- csv_path_str = str(self._init_csv_path)
89
- console.logger.info(f"Using CSV path from initialization: {csv_path_str}")
90
- else:
91
- csv_path_str = os.getenv("CSV_PATH")
92
- if not csv_path_str:
93
- console.logger.error(
94
- "CSV_PATH not found in environment variables and not "
95
- "provided in __init__. Either set CSV_PATH in .env file "
96
- "or pass csv_path to CSVDatafeed()"
97
- )
98
- return False
99
-
100
- if self._init_streaming_delay is not None:
101
- self._streaming_delay = self._init_streaming_delay
102
- if self._streaming_delay < 0:
103
- console.logger.warning(
104
- f"Streaming delay cannot be negative "
105
- f"({self._streaming_delay}), using default 0.0"
106
- )
107
- self._streaming_delay = 0.0
108
- else:
109
- console.logger.info(
110
- f"CSV streaming delay set from initialization: "
111
- f"{self._streaming_delay} seconds"
112
- )
113
- else:
114
- streaming_delay_str = os.getenv("CSV_STREAMING_DELAY", "0.0")
115
- try:
116
- self._streaming_delay = float(streaming_delay_str)
117
- if self._streaming_delay < 0:
118
- console.logger.warning(
119
- f"CSV_STREAMING_DELAY cannot be negative "
120
- f"({self._streaming_delay}), using default 0.0"
121
- )
122
- self._streaming_delay = 0.0
123
- else:
124
- console.logger.info(
125
- f"CSV streaming delay set from environment: "
126
- f"{self._streaming_delay} seconds"
127
- )
128
- except ValueError:
129
- console.logger.error(
130
- f"Invalid CSV_STREAMING_DELAY value "
131
- f"'{streaming_delay_str}', must be a number. "
132
- f"Using default 0.0"
133
- )
134
- self._streaming_delay = 0.0
135
-
136
- self.csv_path = Path(csv_path_str)
137
-
138
- try:
139
- self.data_iterator = pd.read_csv(
140
- self.csv_path,
141
- usecols=[
142
- "ts_event",
143
- "rtype",
144
- "open",
145
- "high",
146
- "low",
147
- "close",
148
- "volume",
149
- "symbol",
150
- ],
151
- dtype={
152
- "ts_event": int,
153
- "rtype": int,
154
- "open": int,
155
- "high": int,
156
- "low": int,
157
- "close": int,
158
- "volume": int,
159
- "symbol": str,
160
- },
161
- iterator=True,
162
- chunksize=1,
163
- )
164
- console.logger.info(f"CSV datafeed connected to: {self.csv_path}")
165
- self._stop_event.clear()
166
- return True
167
-
168
- except Exception as e:
169
- console.logger.error(f"Failed to connect to CSV file {self.csv_path}: {e}")
170
- return False
171
-
172
- def watch(self, symbols):
173
- """
174
- Start streaming data for specified symbols.
175
- Can be called multiple times to add more symbols.
176
-
177
- Args:
178
- symbols (list[tuple[str, models.RecordType]]): List of symbols to
179
- watch with their respective record types.
180
- """
181
- if not self.data_iterator:
182
- console.logger.error("Not connected. Call connect() first.")
183
- return
184
-
185
- with self._symbols_lock:
186
- new_symbols = set(symbols) - self._watched_symbols
187
- already_watched = set(symbols) & self._watched_symbols
188
-
189
- self._watched_symbols.update(new_symbols)
190
-
191
- if new_symbols:
192
- console.logger.info(f"Added new symbols: {new_symbols}")
193
- if already_watched:
194
- console.logger.info(f"Already watching: {already_watched}")
195
- console.logger.info(
196
- f"Currently watching: {len(self._watched_symbols)} symbols"
197
- )
198
-
199
- if self._streaming_thread is None or not self._streaming_thread.is_alive():
200
- self._streaming_thread = threading.Thread(
201
- target=self._stream, name="CSVDatafeedStreaming", daemon=True
202
- )
203
- self._streaming_thread.start()
204
- console.logger.info("Started CSV streaming thread")
205
-
206
- def _stream(self):
207
- """Internal method that runs in background thread to stream CSV data."""
208
- console.logger.info("CSV streaming thread started")
209
-
210
- should_delay = self._streaming_delay > 0
211
- delay_time = self._streaming_delay
212
-
213
- while not self._stop_event.is_set():
214
- try:
215
- chunk = next(self.data_iterator)
216
- row = chunk.iloc[0]
217
-
218
- symbol = row["symbol"]
219
- rtype = row["rtype"]
220
-
221
- with self._symbols_lock:
222
- symbol_key = (symbol, models.RecordType(rtype))
223
- if symbol_key not in self._watched_symbols:
224
- continue
225
-
226
- bar_event = events.Market.IncomingBar(
227
- ts_event=pd.Timestamp(row["ts_event"], unit="ns", tz="UTC"),
228
- symbol=symbol,
229
- bar=models.Bar(
230
- open=row["open"] / 1e9,
231
- high=row["high"] / 1e9,
232
- low=row["low"] / 1e9,
233
- close=row["close"] / 1e9,
234
- volume=int(row["volume"]),
235
- ),
236
- )
237
-
238
- self.event_bus.publish(bar_event)
239
-
240
- if should_delay:
241
- time.sleep(delay_time)
242
-
243
- except StopIteration:
244
- console.logger.info("CSV datafeed reached end of file")
245
- break
246
- except ValueError as e:
247
- console.logger.warning(f"Invalid rtype {row['rtype']} in CSV data: {e}")
248
- continue
249
- except Exception as e:
250
- console.logger.error(f"CSV datafeed error reading data: {e}")
251
- break
252
-
253
- console.logger.info("CSV streaming thread stopped")
254
-
255
- def unwatch(self, symbols):
256
- """
257
- Stop watching specific symbols.
258
-
259
- Args:
260
- symbols (list[tuple[str, models.RecordType]]): List of symbols to
261
- stop watching.
262
- """
263
- with self._symbols_lock:
264
- for symbol in symbols:
265
- self._watched_symbols.discard(symbol)
266
-
267
- console.logger.info(f"Stopped watching symbols: {symbols}")
268
- console.logger.info(f"Still watching: {self._watched_symbols}")
269
-
270
- def disconnect(self):
271
- """
272
- Disconnect from CSV datafeed.
273
- """
274
- self._stop_event.set()
275
-
276
- if self._streaming_thread and self._streaming_thread.is_alive():
277
- console.logger.info("Waiting for streaming thread to stop...")
278
- self._streaming_thread.join(timeout=5.0)
279
- if self._streaming_thread.is_alive():
280
- console.logger.warning("Streaming thread did not stop within timeout")
281
-
282
- with self._symbols_lock:
283
- self._watched_symbols.clear()
284
-
285
- if self.data_iterator is not None:
286
- try:
287
- self.data_iterator.close()
288
- console.logger.info("CSV iterator closed successfully")
289
- except Exception as e:
290
- console.logger.warning(f"Error closing CSV iterator: {e}")
291
- finally:
292
- self.data_iterator = None
293
-
294
- self.csv_path = None
295
- self._streaming_thread = None
296
-
297
- CSVDatafeed._instance = None
File without changes
@@ -1,136 +0,0 @@
1
- """
2
- This module provides the base class for all indicators.
3
- """
4
-
5
- import abc
6
- import collections
7
- import threading
8
-
9
- import numpy as np
10
- from onesecondtrader.core import models
11
- from onesecondtrader.monitoring import console
12
-
13
-
14
- class BaseIndicator(abc.ABC):
15
- """
16
- Base class for all indicators.
17
-
18
- If new market data is received, the indicator is updated by calling the
19
- `update(incoming_bar)` method.
20
- When programming a new indicator, only the `name` property and the
21
- `_compute_indicator()` method need to be implemented.
22
-
23
- Examples:
24
- >>> from onesecondtrader.indicators import base_indicator
25
- >>> from onesecondtrader.core import models
26
- >>> class DummyCloseIndicator(base_indicator.BaseIndicator):
27
- ... @property
28
- ... def name(self) -> str:
29
- ... return "dummy_close_indicator"
30
- ... def _compute_indicator(self, incoming_bar: models.Bar):
31
- ... return incoming_bar.close
32
- ...
33
- >>> dummy_close_indicator = DummyCloseIndicator(max_history=10)
34
- >>> incoming_bar = models.Bar(
35
- ... open=100.0, high=101.0, low=99.0, close=100.5, volume=10000
36
- ... )
37
- >>> dummy_close_indicator.update(incoming_bar)
38
- >>> dummy_close_indicator[0]
39
- 100.5
40
- >>> dummy_close_indicator[-1]
41
- nan
42
- >>> next_incoming_bar = models.Bar(
43
- ... open=100.0, high=101.0, low=99.0, close=101.0, volume=10000
44
- ... )
45
- >>> dummy_close_indicator.update(next_incoming_bar)
46
- >>> dummy_close_indicator[0]
47
- 101.0
48
- >>> dummy_close_indicator[-1]
49
- 100.5
50
- """
51
-
52
- def __init__(self, max_history: int = 100) -> None:
53
- """
54
- Initialize the indicator with a maximum lookback history length.
55
-
56
- Args:
57
- max_history (int): Maximum lookback history length as number of periods.
58
- Defaults to 100.
59
-
60
- Attributes:
61
- self._lock (threading.Lock): Lock to protect concurrent access to the
62
- indicator's state.
63
- self._history (collections.deque): Deque to store the lookback history.
64
- """
65
- if max_history < 1:
66
- console.logger.warning(
67
- f"max_history must be >= 1, got {max_history}; defaulting to 1"
68
- )
69
- max_history = 1
70
- self._lock: threading.Lock = threading.Lock()
71
-
72
- self._history: collections.deque[float] = collections.deque(maxlen=max_history)
73
-
74
- @property
75
- @abc.abstractmethod
76
- def name(self) -> str:
77
- """
78
- Name of the indicator.
79
- This property must be implemented by subclasses.
80
-
81
- Returns:
82
- str: Name of the indicator.
83
- """
84
- pass
85
-
86
- @property
87
- def latest(self) -> float:
88
- """
89
- The latest (most recent) indicator value.
90
-
91
- Equivalent to self[0]. Returns numpy.nan when no value is available yet.
92
- """
93
- return self[0]
94
-
95
- def update(self, incoming_bar: models.Bar) -> None:
96
- """
97
- Updates the indicator based on an incoming closed bar by calling
98
- `self._compute_indicator()`.
99
- """
100
- new_value = self._compute_indicator(incoming_bar)
101
- with self._lock:
102
- self._history.append(new_value)
103
-
104
- @abc.abstractmethod
105
- def _compute_indicator(self, incoming_bar: models.Bar) -> float:
106
- """
107
- Computes the new indicator value based on an incoming closed bar.
108
- This method must be implemented by subclasses.
109
- """
110
- pass
111
-
112
- def __getitem__(self, index: int) -> float:
113
- """
114
- Return the indicator value at the given index with tolerant indexing.
115
-
116
- Indexing rules:
117
-
118
- - `0` returns the current (most recent) value
119
- - `-1` returns the previous value, `-2` two periods back, and so on
120
- - For convenience, a positive `k` behaves like `-k` (e.g., `1 == -1`,
121
- `2 == -2`)
122
- - Out-of-range indices return `np.nan` instead of raising an `IndexError`.
123
- """
124
- normalized: int
125
- if index == 0:
126
- normalized = -1
127
- elif index > 0:
128
- normalized = -(index + 1)
129
- else:
130
- normalized = index - 1
131
-
132
- with self._lock:
133
- try:
134
- return self._history[normalized]
135
- except IndexError:
136
- return np.nan