PyAlgoEngine 0.8.0a13__tar.gz → 0.8.0a16__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 (58) hide show
  1. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/PKG-INFO +1 -1
  2. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
  3. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/__init__.py +1 -1
  4. pyalgoengine-0.8.0a16/algo_engine/backtest/__init__.py +19 -0
  5. pyalgoengine-0.8.0a16/algo_engine/backtest/replay.py +439 -0
  6. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/market_data_buffer.pyi +2 -0
  7. pyalgoengine-0.8.0a13/algo_engine/backtest/__init__.py +0 -19
  8. pyalgoengine-0.8.0a13/algo_engine/backtest/replay.py +0 -292
  9. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/LICENSE +0 -0
  10. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/PyAlgoEngine.egg-info/SOURCES.txt +0 -0
  11. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  12. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/PyAlgoEngine.egg-info/requires.txt +0 -0
  13. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  14. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/README.md +0 -0
  15. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/__init__.py +0 -0
  16. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/backtest/__init__.py +0 -0
  17. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/backtest/doc_server.py +0 -0
  18. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/backtest/tester.py +0 -0
  19. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/backtest/web_app.py +0 -0
  20. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/bokeh_server.py +0 -0
  21. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/demo/__init__.py +0 -0
  22. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/demo/test.py +0 -0
  23. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/sim_input/__init__.py +0 -0
  24. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/sim_input/client.py +0 -0
  25. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/sim_input/sim_keyboard.py +0 -0
  26. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/sim_input/sim_mouse.py +0 -0
  27. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/apps/sim_input/window.py +0 -0
  28. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/backtest/__main__.py +0 -0
  29. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/backtest/metrics.py +0 -0
  30. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/backtest/sim_match.py +0 -0
  31. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/__init__.py +0 -0
  32. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/candlestick.pyi +0 -0
  33. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/console_utils.py +0 -0
  34. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/finance_decimal.py +0 -0
  35. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/market_data.pyi +0 -0
  36. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/market_utils_nt.py +0 -0
  37. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/market_utils_posix.py +0 -0
  38. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/technical_analysis.py +0 -0
  39. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/telemetrics.py +0 -0
  40. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/tick.pyi +0 -0
  41. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/trade_utils.pyi +0 -0
  42. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/base/transaction.pyi +0 -0
  43. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/engine/__init__.py +0 -0
  44. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/engine/algo_engine.py +0 -0
  45. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/engine/event_engine.py +0 -0
  46. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/engine/market_engine.py +0 -0
  47. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/engine/trade_engine.py +0 -0
  48. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/monitor/__init__.py +0 -0
  49. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/monitor/advanced_data_interface.py +0 -0
  50. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/profile/__init__.py +0 -0
  51. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/profile/cn.py +0 -0
  52. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/strategy/__init__.py +0 -0
  53. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/strategy/strategy_engine.py +0 -0
  54. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/utils/__init__.py +0 -0
  55. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/utils/commit_regularizer.py +0 -0
  56. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/algo_engine/utils/data_utils.py +0 -0
  57. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/setup.cfg +0 -0
  58. {pyalgoengine-0.8.0a13 → pyalgoengine-0.8.0a16}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAlgoEngine
3
- Version: 0.8.0a13
3
+ Version: 0.8.0a16
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyAlgoEngine
3
- Version: 0.8.0a13
3
+ Version: 0.8.0a16
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,4 +1,4 @@
1
- __version__ = "0.8.0.alpha13"
1
+ __version__ = "0.8.0.alpha16"
2
2
 
3
3
  import logging
4
4
  import os
@@ -0,0 +1,19 @@
1
+ import logging
2
+
3
+ from .. import LOGGER
4
+
5
+ LOGGER = LOGGER.getChild('BackTest')
6
+
7
+
8
+ def set_logger(logger: logging.Logger):
9
+ global LOGGER
10
+ LOGGER = logger
11
+
12
+ replay.LOGGER = LOGGER.getChild('Replay')
13
+ sim_match.LOGGER = LOGGER.getChild('SimMatch')
14
+
15
+
16
+ from .replay import MarketDateCallable, MarketDataLoader, MarketDataBulkLoader, Replay, SimpleReplay, ProgressReplay, ProgressiveReplay
17
+ from .sim_match import SimMatch
18
+
19
+ __all__ = ['MarketDateCallable', 'MarketDataLoader', 'MarketDataBulkLoader', 'Replay', 'SimpleReplay', 'ProgressReplay', 'ProgressiveReplay', 'SimMatch']
@@ -0,0 +1,439 @@
1
+ import abc
2
+ import datetime
3
+ import inspect
4
+ import operator
5
+ import warnings
6
+ from collections.abc import Sequence, Mapping, Iterable, Callable
7
+ from typing import Literal, Protocol, runtime_checkable, get_type_hints
8
+
9
+ from . import LOGGER
10
+ from ..base import MarketData, DataType, MarketDataBuffer
11
+
12
+ LOGGER = LOGGER.getChild('Replay')
13
+ __all__ = ['MarketDateCallable', 'MarketDataLoader', 'MarketDataBulkLoader', 'Replay', 'SimpleReplay', 'ProgressReplay', 'ProgressiveReplay']
14
+
15
+
16
+ @runtime_checkable
17
+ class MarketDateCallable(Protocol):
18
+ def __call__(self, market_date: datetime.date) -> None:
19
+ ...
20
+
21
+
22
+ @runtime_checkable
23
+ class MarketDataLoader(Protocol):
24
+ def __call__(self, market_date: datetime.date, ticker: str, dtype: str | DataType) -> Sequence[MarketData] | Mapping[float, MarketData]:
25
+ pass
26
+
27
+
28
+ @runtime_checkable
29
+ class MarketDataBulkLoader(Protocol):
30
+ def __call__(self, market_date: datetime.date, tickers: Sequence[str], dtypes: Sequence[str | DataType]) -> Sequence[MarketData] | Mapping[float, MarketData] | MarketDataBuffer:
31
+ pass
32
+
33
+
34
+ def check_protocol_signature(func: Callable, protocol: type) -> bool:
35
+ if not callable(func):
36
+ raise TypeError(f"{func} is not callable")
37
+
38
+ proto_sig = inspect.signature(protocol.__call__)
39
+ func_sig = inspect.signature(func)
40
+
41
+ proto_params = list(proto_sig.parameters.values())[1:] # Skip 'self'
42
+ func_params = list(func_sig.parameters.values())
43
+
44
+ # Check for *args (VAR_POSITIONAL) — not allowed
45
+ for p in func_params:
46
+ if p.kind == inspect.Parameter.VAR_POSITIONAL:
47
+ raise TypeError(f"{func.__name__} uses *args, which is not allowed")
48
+
49
+ # Check number of required positional args (not counting **kwargs)
50
+ proto_arg_names = [p.name for p in proto_params if p.kind in (
51
+ inspect.Parameter.POSITIONAL_ONLY,
52
+ inspect.Parameter.POSITIONAL_OR_KEYWORD
53
+ )]
54
+
55
+ func_arg_names = [p.name for p in func_params if p.kind in (
56
+ inspect.Parameter.POSITIONAL_ONLY,
57
+ inspect.Parameter.POSITIONAL_OR_KEYWORD
58
+ )]
59
+
60
+ if proto_arg_names != func_arg_names:
61
+ raise TypeError(f"{func.__name__} argument names {func_arg_names} do not match protocol {proto_arg_names}")
62
+
63
+ # Type hint comparison (warn if mismatched, but allow)
64
+ proto_hints = get_type_hints(protocol.__call__)
65
+ func_hints = get_type_hints(func)
66
+
67
+ for pname in proto_arg_names:
68
+ expected = proto_hints.get(pname)
69
+ actual = func_hints.get(pname)
70
+ if expected and actual and expected != actual:
71
+ warnings.warn(
72
+ f"Type hint mismatch for parameter '{pname}': expected {expected}, got {actual}",
73
+ stacklevel=2
74
+ )
75
+
76
+ # Optional: check return type
77
+ expected_ret = proto_hints.get("return")
78
+ actual_ret = func_hints.get("return")
79
+ if expected_ret and actual_ret and expected_ret != actual_ret:
80
+ warnings.warn(
81
+ f"Return type mismatch: expected {expected_ret}, got {actual_ret}",
82
+ stacklevel=2
83
+ )
84
+
85
+ return True
86
+
87
+
88
+ class Replay(object, metaclass=abc.ABCMeta):
89
+ # __slots__ = ('start_date', 'end_date', 'market_date', 'calendar', 'bod', 'eod', 'subscription', '_calendar', '_market_date', '_status', '_progress')
90
+
91
+ def __init__(self, start_date: datetime.date = None, end_date: datetime.date = None, market_date: datetime.date = None, calendar: Sequence[datetime.date] = None, bod: MarketDateCallable = None, eod: MarketDateCallable = None) -> None:
92
+ self.start_date = start_date or market_date or calendar[0]
93
+ self.end_date = end_date or calendar[-1]
94
+ self.market_date = market_date or start_date
95
+ self.calendar = calendar or []
96
+
97
+ self.bod = []
98
+ self.eod = []
99
+ self.subscription = {}
100
+
101
+ if bod is not None:
102
+ self.add_bod(bod)
103
+
104
+ if eod is not None:
105
+ self.add_eod(eod)
106
+
107
+ def add_bod(self, func: MarketDateCallable):
108
+ self.bod.append(func)
109
+
110
+ def add_eod(self, func: MarketDateCallable):
111
+ self.eod.append(func)
112
+
113
+ @classmethod
114
+ def get_dtype(cls, dtype: DataType | str) -> str | Literal['TickData', 'TickDataLite', 'OrderData', 'TransactionData']:
115
+ match dtype:
116
+ case 'TickData' | 'TickDataLite' | 'OrderData' | 'TransactionData':
117
+ return str(dtype)
118
+ case DataType.DTYPE_TICK | DataType.DTYPE_ORDER | DataType.DTYPE_TRANSACTION:
119
+ return DataType(dtype).name.removeprefix('DTYPE_').capitalize() + 'Data'
120
+ case DataType.DTYPE_TICK_LITE:
121
+ return 'Data'.join(_.capitalize() for _ in DataType(dtype).name.removeprefix('DTYPE_').split('_'))
122
+ case _:
123
+ raise ValueError(f'Invalid dtype {dtype}, expect str or int.')
124
+
125
+ def add_subscription(self, ticker: str, dtype: DataType | str):
126
+ dtype = self.get_dtype(dtype)
127
+ topic = f'{ticker}.{dtype}'
128
+
129
+ self.subscription[topic] = (ticker, dtype)
130
+
131
+ def remove_subscription(self, ticker: str, dtype: DataType | str):
132
+ dtype = self.get_dtype(dtype)
133
+ topic = f'{ticker}.{dtype}'
134
+
135
+ try:
136
+ self.subscription.pop(topic)
137
+ except KeyError as _:
138
+ LOGGER.info(f'{topic} not in {self.subscription}')
139
+
140
+ @abc.abstractmethod
141
+ def __next__(self):
142
+ ...
143
+
144
+ @abc.abstractmethod
145
+ def __iter__(self):
146
+ ...
147
+
148
+
149
+ class SimpleReplay(Replay):
150
+ def __init__(
151
+ self,
152
+ loader: MarketDataBulkLoader | MarketDataLoader = None,
153
+ market_date: datetime.date = None,
154
+ start_date: datetime.date = None,
155
+ end_date: datetime.date = None,
156
+ calendar: Sequence[datetime.date] = None,
157
+ bod: MarketDateCallable = None,
158
+ eod: MarketDateCallable = None
159
+ ):
160
+ super().__init__(market_date=market_date, start_date=start_date, end_date=end_date, calendar=calendar, bod=bod, eod=eod)
161
+ self.loader = loader
162
+
163
+ def __iter__(self):
164
+ self._calendar = self.calendar or [self.start_date + datetime.timedelta(days=i) for i in range((self.end_date - self.start_date).days + 1)]
165
+ self._market_date = self.market_date or sorted(_ for _ in self._calendar if _ >= self.market_date)[0]
166
+ self._status = {market_date: 'skipped' if market_date < self.market_date else 'idle' for market_date in self._calendar}
167
+ self._idx_buffer = 0
168
+ self._idx_date = sum([1 for _ in self._calendar if _ < self.market_date])
169
+
170
+ for func in self.bod:
171
+ func(self._market_date)
172
+
173
+ self._safe_load()
174
+
175
+ return self
176
+
177
+ def __next__(self) -> MarketData:
178
+ if self._idx_buffer < self._buffer_size:
179
+ self._idx_buffer += 1
180
+ return next(self._buffer)
181
+
182
+ for func in self.eod:
183
+ func(self._market_date)
184
+
185
+ self._idx_buffer = 0
186
+ self._idx_date += 1
187
+
188
+ if self._idx_date >= len(self._calendar):
189
+ self._calendar.clear()
190
+ del self._calendar
191
+ del self._market_date
192
+ del self._status
193
+ del self._idx_buffer
194
+ del self._idx_date
195
+ del self._buffer
196
+ del self._buffer_size
197
+ raise StopIteration()
198
+
199
+ self._market_date = self._calendar[self._idx_date]
200
+
201
+ for func in self.bod:
202
+ func(self._market_date)
203
+
204
+ self._safe_load()
205
+ return self.__next__()
206
+
207
+ def __repr__(self):
208
+ return f'{self.__class__.__name__}{{id={id(self)}, from={self.start_date}, to={self.end_date}}}'
209
+
210
+ def _bulk_load_protocol(self):
211
+ buffer = self.loader(market_date=self._market_date, tickers=self.tickers, dtypes=self.dtypes)
212
+ buffer.sort()
213
+
214
+ if isinstance(buffer, MarketDataBuffer):
215
+ self._buffer = buffer
216
+ self._buffer_size = len(self._buffer)
217
+ elif isinstance(buffer, Sequence):
218
+ self._buffer = iter(buffer)
219
+ self._buffer_size = len(buffer)
220
+ elif isinstance(buffer, Mapping):
221
+ self._buffer = iter(buffer.values())
222
+ self._buffer_size = len(buffer)
223
+
224
+ def _individual_load_protocol(self):
225
+ buffer = []
226
+ for topic, (_ticker, _dtype) in self.subscription.items():
227
+ LOGGER.info(f'{self} loading {self._market_date} {_ticker} {_dtype}')
228
+ data = self.loader(market_date=self._market_date, ticker=_ticker, dtype=_dtype)
229
+ if isinstance(data, Mapping):
230
+ buffer.extend(list(data.values()))
231
+ elif isinstance(data, Sequence):
232
+ buffer.extend(data)
233
+ else:
234
+ raise TypeError(f'The loader {self.loader} returned {type(data)}. Expect a sequence or mapping of MarketData')
235
+ buffer.sort(key=operator.attrgetter('timestamp', 'ticker', '_dtype'))
236
+ self._buffer = iter(buffer)
237
+ self._buffer_size = len(buffer)
238
+
239
+ def _safe_load(self):
240
+ if self.loader is None:
241
+ assert hasattr(self, '_buffer') and isinstance(self._buffer, Iterable), f'Without assigning a data loader, the _buffer of {self.__class__.__name__} should be set in bod process.'
242
+ return
243
+
244
+ is_bulk_loader = check_protocol_signature(self.loader, MarketDataBulkLoader)
245
+ is_individual_loader = check_protocol_signature(self.loader, MarketDataLoader)
246
+
247
+ if (is_bulk_loader and is_individual_loader) or (not is_bulk_loader and not is_individual_loader):
248
+ try:
249
+ return self._bulk_load_protocol()
250
+ except Exception as e:
251
+ LOGGER.info('Failed to load data using MarketDataBulkLoader protocol!')
252
+
253
+ try:
254
+ return self._individual_load_protocol()
255
+ except Exception as e:
256
+ LOGGER.info('Failed to load data using MarketDataLoader protocol!')
257
+ raise
258
+
259
+ if is_bulk_loader:
260
+ return self._bulk_load_protocol()
261
+
262
+ return self._individual_load_protocol()
263
+
264
+ @property
265
+ def progress(self) -> float:
266
+ if not hasattr(self, '_buffer'):
267
+ raise RuntimeError(f'{self.__class__.__name__} not started yet.')
268
+
269
+ return (self._idx_date + (self._idx_buffer / self._buffer_size - 1)) / len(self._calendar)
270
+
271
+ @property
272
+ def tickers(self) -> list[str]:
273
+ tickers = set()
274
+ for _, (ticker, dtype) in self.subscription.items():
275
+ tickers.add(ticker)
276
+ return list(tickers)
277
+
278
+ @property
279
+ def dtypes(self) -> list[str]:
280
+ dtypes = set()
281
+ for _, (ticker, dtype) in self.subscription.items():
282
+ dtypes.add(dtype)
283
+ return list(dtypes)
284
+
285
+ @property
286
+ def status(self) -> dict[datetime.date, str]:
287
+ if not hasattr(self, '_status'):
288
+ raise RuntimeError(f'{self.__class__.__name__} not started yet.')
289
+
290
+ return self._status
291
+
292
+
293
+ class ProgressReplay(SimpleReplay):
294
+ def __init__(
295
+ self,
296
+ loader: MarketDataBulkLoader | MarketDataLoader = None,
297
+ market_date: datetime.date = None,
298
+ start_date: datetime.date = None,
299
+ end_date: datetime.date = None,
300
+ calendar: Sequence[datetime.date] = None,
301
+ bod: MarketDateCallable = None,
302
+ eod: MarketDateCallable = None,
303
+ **tqdm_kwargs
304
+ ):
305
+ super().__init__(
306
+ loader=loader,
307
+ market_date=market_date,
308
+ start_date=start_date,
309
+ end_date=end_date,
310
+ calendar=calendar,
311
+ bod=bod,
312
+ eod=eod
313
+ )
314
+
315
+ self._tqdm_kwargs = {
316
+ 'total': 1,
317
+ 'unit_scale': True,
318
+ 'unit': 'percent',
319
+ 'mininterval': 0.1,
320
+ 'miniters': 0.001,
321
+ **tqdm_kwargs
322
+ }
323
+ self.add_bod(self._update_progress_bar)
324
+
325
+ def __iter__(self):
326
+ from tqdm.auto import tqdm
327
+ self._pbar = tqdm(**self._tqdm_kwargs)
328
+ iterator = super().__iter__()
329
+
330
+ try:
331
+ while True:
332
+ try:
333
+ result = next(iterator)
334
+ if self._pbar:
335
+ self._pbar.update(self.progress)
336
+ yield result
337
+ except StopIteration:
338
+ break
339
+ finally:
340
+ if self._pbar is not None:
341
+ self._pbar.close()
342
+ self._pbar = None
343
+
344
+ def __next__(self) -> MarketData:
345
+ raise RuntimeError("MarketDataBufferReplay should be used as an iterator context")
346
+
347
+ def _update_progress_bar(self, market_date: datetime.date):
348
+ if self._pbar:
349
+ self._pbar.set_description(f'Replay {market_date:%Y-%m-%d} ({self._idx_date + 1} / {len(self._calendar)})')
350
+ self._pbar.refresh()
351
+
352
+
353
+ class ProgressiveReplay(SimpleReplay):
354
+ """
355
+ progressively loading and replaying market data
356
+
357
+ requires arguments
358
+ loader: a data loading function. Expect loader = Callable(market_date: datetime.date, ticker: str, dtype: str| type) -> dict[any, MarketData]
359
+ start_date & end_date: the given replay period
360
+ or calendar: the given replay calendar.
361
+
362
+ accepts kwargs:
363
+ ticker / tickers: the given symbols to replay, expect a str| list[str]
364
+ dtype / dtypes: the given dtype(s) of symbol to replay, expect a str | type, list[str | type]. default = all, which is (TradeData, TickData, OrderBook)
365
+ subscription / subscribe: the given ticker-dtype pair to replay, expect a list[dict[str, str | type]]
366
+ """
367
+
368
+ def __init__(
369
+ self,
370
+ loader: MarketDataLoader,
371
+ tickers: str | Sequence[str] = None,
372
+ dtypes: str | DataType | Sequence[str] | Sequence[DataType] = None,
373
+ market_date: datetime.date = None,
374
+ start_date: datetime.date = None,
375
+ end_date: datetime.date = None,
376
+ calendar: Sequence[datetime.date] = None,
377
+ bod: MarketDateCallable = None,
378
+ eod: MarketDateCallable = None,
379
+ **progress_config
380
+ ) -> None:
381
+ warnings.deprecated('User ProgressReplay instead!')
382
+ self.loader = loader
383
+ super().__init__(loader=loader, market_date=market_date, start_date=start_date, end_date=end_date, calendar=calendar, bod=bod, eod=eod)
384
+
385
+ tickers = tickers or []
386
+ dtypes = dtypes or ['TransactionData', 'TickData', 'OrderData']
387
+
388
+ if not isinstance(loader, MarketDataLoader):
389
+ raise TypeError('loader function has 3 requires args, market_date, ticker and dtype.')
390
+
391
+ if isinstance(tickers, str):
392
+ tickers = [tickers]
393
+ elif isinstance(tickers, Iterable):
394
+ tickers = list(tickers)
395
+ else:
396
+ raise TypeError(f'Invalid ticker {tickers}, expect str or list[str]')
397
+
398
+ if isinstance(dtypes, (str, int, DataType)):
399
+ dtypes = [dtypes]
400
+ elif isinstance(dtypes, Iterable):
401
+ dtypes = list(dtypes)
402
+ else:
403
+ raise TypeError(f'Invalid dtype {dtypes}, expect str or list[str]')
404
+
405
+ for ticker in tickers:
406
+ for dtype in dtypes:
407
+ self.add_subscription(ticker=ticker, dtype=dtype)
408
+
409
+ self.progress_config = dict(
410
+ tasks=1,
411
+ **progress_config
412
+ )
413
+
414
+ def __iter__(self):
415
+ from ..base import Progress
416
+ self._pbar = Progress(**self.progress_config)
417
+ return super().__iter__()
418
+
419
+ def __next__(self) -> MarketData:
420
+ try:
421
+ result = super().__next__()
422
+ self._pbar.done_tasks = self.progress
423
+
424
+ if (not self._pbar.tick_size) \
425
+ or self._pbar.progress >= self._pbar.tick_size + self._pbar.last_output \
426
+ or self._pbar.is_done:
427
+ self._pbar.output()
428
+
429
+ return result
430
+ except StopIteration:
431
+ if not self._pbar.is_done:
432
+ self.progress.done_tasks = 1
433
+ self._pbar.output()
434
+ raise
435
+
436
+ def _update_progress_bar(self, market_date: datetime.date):
437
+ if self._pbar:
438
+ self.progress.prompt = f'Replay {market_date:%Y-%m-%d} ({self._idx_date + 1} / {len(self._calendar)}):'
439
+ self._pbar.output()
@@ -33,6 +33,8 @@ class MarketDataBuffer:
33
33
 
34
34
  def update(self, dtype: int, **kwargs: dict[str, Any]) -> None: ...
35
35
 
36
+ def __getitem__(self, idx: int) -> MarketData | TransactionData | OrderData | TickDataLite | TickData | BarData: ...
37
+
36
38
  def __iter__(self) -> MarketDataBuffer: ...
37
39
 
38
40
  def __next__(self) -> MarketData | TransactionData | OrderData | TickDataLite | TickData | BarData: ...
@@ -1,19 +0,0 @@
1
- import logging
2
-
3
- from .. import LOGGER
4
-
5
- LOGGER = LOGGER.getChild('BackTest')
6
-
7
-
8
- def set_logger(logger: logging.Logger):
9
- global LOGGER
10
- LOGGER = logger
11
-
12
- replay.LOGGER = LOGGER.getChild('Replay')
13
- sim_match.LOGGER = LOGGER.getChild('SimMatch')
14
-
15
-
16
- from .replay import Replay, SimpleReplay, ProgressiveReplay
17
- from .sim_match import SimMatch
18
-
19
- __all__ = ['Replay', 'SimpleReplay', 'ProgressiveReplay', 'SimMatch']
@@ -1,292 +0,0 @@
1
- import abc
2
- import datetime
3
- import inspect
4
- import operator
5
- from collections.abc import Mapping, Sequence, Iterator
6
- from typing import Iterable, Protocol
7
-
8
- from . import LOGGER
9
- from ..base import Progress, TickData, TransactionData, TradeData, OrderData, MarketData, MarketDataBuffer
10
-
11
- LOGGER = LOGGER.getChild('Replay')
12
-
13
-
14
- class Replay(object, metaclass=abc.ABCMeta):
15
- @abc.abstractmethod
16
- def __next__(self): ...
17
-
18
- @abc.abstractmethod
19
- def __iter__(self): ...
20
-
21
-
22
- class SimpleReplay(Replay):
23
- def __init__(self, **kwargs):
24
- self.eod = kwargs.pop('eod', None)
25
- self.bod = kwargs.pop('bod', None)
26
-
27
- self.replay_task = []
28
- self.task_progress = 0
29
- self.task_date = None
30
- self.progress = Progress(tasks=1, **kwargs)
31
-
32
- def load(self, data):
33
- if isinstance(data, dict):
34
- self.replay_task.extend(list(data.values()))
35
- else:
36
- self.replay_task.extend(data)
37
-
38
- def reset(self):
39
- self.replay_task.clear()
40
- self.task_progress = 0
41
- self.task_date = None
42
- self.progress.reset()
43
-
44
- def next_task(self):
45
- if self.task_progress < len(self.replay_task):
46
- market_data = self.replay_task[self.task_progress]
47
- market_time = market_data.market_time
48
-
49
- if isinstance(market_time, datetime.datetime):
50
- market_date = market_time.date()
51
- else:
52
- market_date = market_time
53
-
54
- if market_date != self.task_date:
55
- if callable(self.eod) and self.task_date:
56
- self.eod(self.task_date)
57
-
58
- self.task_date = market_date
59
- self.progress.prompt = f'Replay {market_date:%Y-%m-%d}:'
60
-
61
- if callable(self.bod):
62
- self.bod(market_date)
63
-
64
- self.progress.done_tasks = self.task_progress / len(self.replay_task)
65
-
66
- if (not self.progress.tick_size) or self.progress.progress >= self.progress.tick_size + self.progress.last_output:
67
- self.progress.output()
68
-
69
- self.task_progress += 1
70
- else:
71
- raise StopIteration()
72
-
73
- return market_data
74
-
75
- def __next__(self):
76
- try:
77
- return self.next_task()
78
- except StopIteration:
79
- if not self.progress.is_done:
80
- self.progress.done_tasks = 1
81
- self.progress.output()
82
-
83
- self.reset()
84
- raise StopIteration()
85
-
86
- def __iter__(self):
87
- return self
88
-
89
-
90
- class DataLoader(Protocol):
91
- def __call__(self, market_date: datetime.date, ticker: str, dtype: str) -> Mapping[float, MarketData] | Sequence[MarketData] | MarketDataBuffer:
92
- ...
93
-
94
-
95
- class ProgressiveReplay(Replay):
96
- """
97
- progressively loading and replaying market data
98
-
99
- requires arguments
100
- loader: a data loading function. Expect loader = Callable(market_date: datetime.date, ticker: str, dtype: str| type) -> dict[any, MarketData]
101
- start_date & end_date: the given replay period
102
- or calendar: the given replay calendar.
103
-
104
- accepts kwargs:
105
- ticker / tickers: the given symbols to replay, expect a str| list[str]
106
- dtype / dtypes: the given dtype(s) of symbol to replay, expect a str | type, list[str | type]. default = all, which is (TradeData, TickData, OrderBook)
107
- subscription / subscribe: the given ticker-dtype pair to replay, expect a list[dict[str, str | type]]
108
- """
109
-
110
- def __init__(
111
- self,
112
- loader: DataLoader,
113
- **kwargs
114
- ):
115
- self.loader = loader
116
- self.market_date: datetime.date | None = kwargs.pop('market_date', None)
117
- self.start_date: datetime.date | None = kwargs.pop('start_date', None)
118
- self.end_date: datetime.date | None = kwargs.pop('end_date', None)
119
- self.calendar: list[datetime.date] | None = kwargs.pop('calendar', None)
120
-
121
- self.eod = kwargs.pop('eod', None)
122
- self.bod = kwargs.pop('bod', None)
123
-
124
- self.replay_subscription = {}
125
- self.replay_calendar = []
126
- self.replay_task: Iterator | None = None
127
- self.replay_task_length: int = 0
128
- self.replay_status = {}
129
-
130
- self.date_progress = 0
131
- self.task_progress = 0
132
- self.progress = Progress(tasks=1, **kwargs)
133
-
134
- tickers: list[str] = kwargs.pop('ticker', kwargs.pop('tickers', []))
135
- dtypes: list[str | type] = kwargs.pop('dtype', kwargs.pop('dtypes', [TradeData, TransactionData, OrderData, TickData]))
136
-
137
- if not all([arg_name in inspect.getfullargspec(loader).args for arg_name in ['market_date', 'ticker', 'dtype']]):
138
- raise TypeError('loader function has 3 requires args, market_date, ticker and dtype.')
139
-
140
- if isinstance(tickers, str):
141
- tickers = [tickers]
142
- elif isinstance(tickers, Iterable):
143
- tickers = list(tickers)
144
- else:
145
- raise TypeError(f'Invalid ticker {tickers}, expect str or list[str]')
146
-
147
- if isinstance(dtypes, str) or inspect.isclass(dtypes):
148
- dtypes = [dtypes]
149
- elif isinstance(dtypes, Iterable):
150
- dtypes = list(dtypes)
151
- else:
152
- raise TypeError(f'Invalid dtype {dtypes}, expect str or list[str]')
153
-
154
- for ticker in tickers:
155
- for dtype in dtypes:
156
- self.add_subscription(ticker=ticker, dtype=dtype)
157
-
158
- subscription = kwargs.pop('subscription', kwargs.pop('subscribe', []))
159
-
160
- if isinstance(subscription, dict):
161
- subscription = [subscription]
162
-
163
- for sub in subscription:
164
- self.add_subscription(**sub)
165
-
166
- self.reset()
167
-
168
- def add_subscription(self, ticker: str, dtype: type | str):
169
- if isinstance(dtype, str):
170
- pass
171
- elif inspect.isclass(dtype):
172
- dtype = dtype.__name__
173
- else:
174
- raise ValueError(f'Invalid dtype {dtype}, expect str or class.')
175
-
176
- topic = f'{ticker}.{dtype}'
177
- self.replay_subscription[topic] = (ticker, dtype)
178
-
179
- def remove_subscription(self, ticker: str, dtype: type | str):
180
- if isinstance(dtype, str):
181
- pass
182
- else:
183
- dtype = dtype.__name__
184
-
185
- topic = f'{ticker}.{dtype}'
186
- self.replay_subscription.pop(topic, None)
187
-
188
- def reset(self):
189
- if self.calendar is None:
190
- self.replay_calendar = [self.start_date + datetime.timedelta(days=i) for i in range((self.end_date - self.start_date).days + 1)]
191
- else:
192
- self.replay_calendar = self.calendar
193
-
194
- if self.market_date is None:
195
- self.market_date = self.replay_calendar[0] if self.replay_calendar else self.start_date
196
- else:
197
- date_to_replay = [_ for _ in self.replay_calendar if _ >= self.market_date]
198
- self.market_date = date_to_replay[0] if date_to_replay else self.end_date
199
-
200
- self.replay_status = {market_date: 'skipped' if market_date < self.market_date else 'idle' for market_date in self.replay_calendar}
201
-
202
- self.task_progress = 0
203
- self.replay_task_length = 0
204
- self.replay_task = None
205
- self.date_progress = sum([1 for _ in self.replay_calendar if _ < self.market_date])
206
- self.progress.reset()
207
-
208
- if self.date_progress:
209
- self.progress.done_tasks = self.date_progress / len(self.replay_calendar)
210
-
211
- def next_trade_day(self):
212
- if self.date_progress >= len(self.replay_calendar):
213
- raise StopIteration()
214
-
215
- self.market_date = market_date = self.replay_calendar[self.date_progress]
216
- self.replay_status[market_date] = 'started'
217
- self.progress.prompt = f'Replay {market_date:%Y-%m-%d} ({self.date_progress + 1} / {len(self.replay_calendar)}):'
218
-
219
- for topic in self.replay_subscription:
220
- ticker, dtype = self.replay_subscription[topic]
221
- LOGGER.info(f'{self} loading {market_date} {ticker} {dtype}...')
222
- data = self.loader(market_date=market_date, ticker=ticker, dtype=dtype)
223
- if isinstance(data, Mapping):
224
- data = [data[ts] for ts in sorted(data)] # expect to be a mapping of ts and data
225
- self.replay_task = iter(data)
226
- self.replay_task_length = len(data)
227
- elif isinstance(data, Sequence):
228
- data = sorted(data, key=operator.attrgetter('timestamp', 'ticker', '__class__.__name__'))
229
- self.replay_task = iter(data)
230
- self.replay_task_length = len(data)
231
- elif isinstance(data, MarketDataBuffer):
232
- data.sort()
233
- self.replay_task = iter(data)
234
- self.replay_task_length = len(data)
235
- else:
236
- raise TypeError(f'Invalid return type of dataloader, expect list, tuple, dict or MarketDataBuffer, got {type(data)}.')
237
-
238
- LOGGER.info(f'{market_date} data loaded! {self.replay_task_length:,} entries.')
239
- self.date_progress += 1
240
-
241
- def next_task(self):
242
- try:
243
- data = next(self.replay_task)
244
- self.task_progress += 1
245
- except StopIteration:
246
- if self.eod is not None and self.replay_status[self.market_date] == 'started':
247
- self.eod(market_date=self.market_date, replay=self)
248
- self.replay_status[self.market_date] = 'done'
249
-
250
- self.replay_task = None
251
- self.task_progress = 0
252
-
253
- if self.bod is not None and self.date_progress < len(self.replay_calendar):
254
- self.bod(market_date=self.replay_calendar[self.date_progress], replay=self)
255
-
256
- # this is by designed, to load the new data after the bod is done.
257
- self.next_trade_day()
258
-
259
- # the bod process should be moved here!
260
-
261
- data = self.next_task()
262
-
263
- if self.replay_task_length and self.replay_calendar:
264
- current_progress = (self.date_progress - 1 + (self.task_progress / self.replay_task_length)) / len(self.replay_calendar)
265
- self.progress.done_tasks = current_progress
266
- else:
267
- self.progress.done_tasks = 1
268
-
269
- if (not self.progress.tick_size) \
270
- or self.progress.progress >= self.progress.tick_size + self.progress.last_output \
271
- or self.progress.is_done:
272
- self.progress.output()
273
-
274
- return data
275
-
276
- def __next__(self) -> MarketData:
277
- try:
278
- return self.next_task()
279
- except StopIteration:
280
- if not self.progress.is_done:
281
- self.progress.done_tasks = 1
282
- self.progress.output()
283
-
284
- self.reset()
285
- raise StopIteration()
286
-
287
- def __iter__(self):
288
- self.reset()
289
- return self
290
-
291
- def __repr__(self):
292
- return f'{self.__class__.__name__}{{id={id(self)}, from={self.start_date}, to={self.end_date}}}'
File without changes