BackcastPro 0.0.2__py3-none-any.whl → 0.0.3__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.

Potentially problematic release.


This version of BackcastPro might be problematic. Click here for more details.

BackcastPro/_util.py DELETED
@@ -1,337 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import sys
5
- import warnings
6
- from contextlib import contextmanager
7
- from functools import partial
8
- from itertools import chain
9
- from multiprocessing import resource_tracker as _mprt
10
- from multiprocessing import shared_memory as _mpshm
11
- from numbers import Number
12
- from threading import Lock
13
- from typing import Dict, List, Optional, Sequence, Union, cast
14
-
15
- import numpy as np
16
- import pandas as pd
17
-
18
- try:
19
- from tqdm.auto import tqdm as _tqdm
20
- _tqdm = partial(_tqdm, leave=False)
21
- except ImportError:
22
- def _tqdm(seq, **_):
23
- return seq
24
-
25
-
26
- def try_(lazy_func, default=None, exception=Exception):
27
- try:
28
- return lazy_func()
29
- except exception:
30
- return default
31
-
32
-
33
- @contextmanager
34
- def patch(obj, attr, newvalue):
35
- had_attr = hasattr(obj, attr)
36
- orig_value = getattr(obj, attr, None)
37
- setattr(obj, attr, newvalue)
38
- try:
39
- yield
40
- finally:
41
- if had_attr:
42
- setattr(obj, attr, orig_value)
43
- else:
44
- delattr(obj, attr)
45
-
46
-
47
- def _as_str(value) -> str:
48
- if isinstance(value, (Number, str)):
49
- return str(value)
50
- if isinstance(value, pd.DataFrame):
51
- return 'df'
52
- name = str(getattr(value, 'name', '') or '')
53
- if name in ('Open', 'High', 'Low', 'Close', 'Volume'):
54
- return name[:1]
55
- if callable(value):
56
- name = getattr(value, '__name__', value.__class__.__name__).replace('<lambda>', 'λ')
57
- if len(name) > 10:
58
- name = name[:9] + '…'
59
- return name
60
-
61
-
62
- def _as_list(value) -> List:
63
- if isinstance(value, Sequence) and not isinstance(value, str):
64
- return list(value)
65
- return [value]
66
-
67
-
68
- def _batch(seq):
69
- # XXX: Replace with itertools.batched
70
- n = np.clip(int(len(seq) // (os.cpu_count() or 1)), 1, 300)
71
- for i in range(0, len(seq), n):
72
- yield seq[i:i + n]
73
-
74
-
75
- def _data_period(index) -> Union[pd.Timedelta, Number]:
76
- """Return data index period as pd.Timedelta"""
77
- values = pd.Series(index[-100:])
78
- return values.diff().dropna().median()
79
-
80
-
81
- def _strategy_indicators(strategy):
82
- return {attr: indicator
83
- for attr, indicator in strategy.__dict__.items()
84
- if isinstance(indicator, _Indicator)}.items()
85
-
86
-
87
- def _indicator_warmup_nbars(strategy):
88
- if strategy is None:
89
- return 0
90
- nbars = max((np.isnan(indicator.astype(float)).argmin(axis=-1).max()
91
- for _, indicator in _strategy_indicators(strategy)
92
- if not indicator._opts['scatter']), default=0)
93
- return nbars
94
-
95
-
96
- class _Array(np.ndarray):
97
- """
98
- ndarray extended to supply .name and other arbitrary properties
99
- in ._opts dict.
100
- """
101
- def __new__(cls, array, *, name=None, **kwargs):
102
- obj = np.asarray(array).view(cls)
103
- obj.name = name or array.name
104
- obj._opts = kwargs
105
- return obj
106
-
107
- def __array_finalize__(self, obj):
108
- if obj is not None:
109
- self.name = getattr(obj, 'name', '')
110
- self._opts = getattr(obj, '_opts', {})
111
-
112
- # Make sure properties name and _opts are carried over
113
- # when (un-)pickling.
114
- def __reduce__(self):
115
- value = super().__reduce__()
116
- return value[:2] + (value[2] + (self.__dict__,),)
117
-
118
- def __setstate__(self, state):
119
- self.__dict__.update(state[-1])
120
- super().__setstate__(state[:-1])
121
-
122
- def __bool__(self):
123
- try:
124
- return bool(self[-1])
125
- except IndexError:
126
- return super().__bool__()
127
-
128
- def __float__(self):
129
- try:
130
- return float(self[-1])
131
- except IndexError:
132
- return super().__float__()
133
-
134
- def to_series(self):
135
- warnings.warn("`.to_series()` is deprecated. For pd.Series conversion, use accessor `.s`")
136
- return self.s
137
-
138
- @property
139
- def s(self) -> pd.Series:
140
- values = np.atleast_2d(self)
141
- index = self._opts['index'][:values.shape[1]]
142
- return pd.Series(values[0], index=index, name=self.name)
143
-
144
- @property
145
- def df(self) -> pd.DataFrame:
146
- values = np.atleast_2d(np.asarray(self))
147
- index = self._opts['index'][:values.shape[1]]
148
- df = pd.DataFrame(values.T, index=index, columns=[self.name] * len(values))
149
- return df
150
-
151
-
152
- class _Indicator(_Array):
153
- pass
154
-
155
-
156
- class _Data:
157
- """
158
- A data array accessor. Provides access to OHLCV "columns"
159
- as a standard `pd.DataFrame` would, except it's not a DataFrame
160
- and the returned "series" are _not_ `pd.Series` but `np.ndarray`
161
- for performance reasons.
162
- """
163
- def __init__(self, df: pd.DataFrame):
164
- self.__df = df
165
- self.__len = len(df) # Current length
166
- self.__pip: Optional[float] = None
167
- self.__cache: Dict[str, _Array] = {}
168
- self.__arrays: Dict[str, _Array] = {}
169
- self._update()
170
-
171
- def __getitem__(self, item):
172
- return self.__get_array(item)
173
-
174
- def __getattr__(self, item):
175
- try:
176
- return self.__get_array(item)
177
- except KeyError:
178
- raise AttributeError(f"Column '{item}' not in data") from None
179
-
180
- def _set_length(self, length):
181
- self.__len = length
182
- self.__cache.clear()
183
-
184
- def _update(self):
185
- index = self.__df.index.copy()
186
- self.__arrays = {col: _Array(arr, index=index)
187
- for col, arr in self.__df.items()}
188
- # Leave index as Series because pd.Timestamp nicer API to work with
189
- self.__arrays['__index'] = index
190
-
191
- def __repr__(self):
192
- i = min(self.__len, len(self.__df)) - 1
193
- index = self.__arrays['__index'][i]
194
- items = ', '.join(f'{k}={v}' for k, v in self.__df.iloc[i].items())
195
- return f'<Data i={i} ({index}) {items}>'
196
-
197
- def __len__(self):
198
- return self.__len
199
-
200
- @property
201
- def df(self) -> pd.DataFrame:
202
- return (self.__df.iloc[:self.__len]
203
- if self.__len < len(self.__df)
204
- else self.__df)
205
-
206
- @property
207
- def pip(self) -> float:
208
- if self.__pip is None:
209
- self.__pip = float(10**-np.median([len(s.partition('.')[-1])
210
- for s in self.__arrays['Close'].astype(str)]))
211
- return self.__pip
212
-
213
- def __get_array(self, key) -> _Array:
214
- arr = self.__cache.get(key)
215
- if arr is None:
216
- arr = self.__cache[key] = cast(_Array, self.__arrays[key][:self.__len])
217
- return arr
218
-
219
- @property
220
- def Open(self) -> _Array:
221
- return self.__get_array('Open')
222
-
223
- @property
224
- def High(self) -> _Array:
225
- return self.__get_array('High')
226
-
227
- @property
228
- def Low(self) -> _Array:
229
- return self.__get_array('Low')
230
-
231
- @property
232
- def Close(self) -> _Array:
233
- return self.__get_array('Close')
234
-
235
- @property
236
- def Volume(self) -> _Array:
237
- return self.__get_array('Volume')
238
-
239
- @property
240
- def index(self) -> pd.DatetimeIndex:
241
- return self.__get_array('__index')
242
-
243
- # Make pickling in Backtest.optimize() work with our catch-all __getattr__
244
- def __getstate__(self):
245
- return self.__dict__
246
-
247
- def __setstate__(self, state):
248
- self.__dict__ = state
249
-
250
-
251
- if sys.version_info >= (3, 13):
252
- SharedMemory = _mpshm.SharedMemory
253
- else:
254
- class SharedMemory(_mpshm.SharedMemory):
255
- # From https://github.com/python/cpython/issues/82300#issuecomment-2169035092
256
- __lock = Lock()
257
-
258
- def __init__(self, *args, track: bool = True, **kwargs):
259
- self._track = track
260
- if track:
261
- return super().__init__(*args, **kwargs)
262
- with self.__lock:
263
- with patch(_mprt, 'register', lambda *a, **kw: None):
264
- super().__init__(*args, **kwargs)
265
-
266
- def unlink(self):
267
- if _mpshm._USE_POSIX and self._name:
268
- _mpshm._posixshmem.shm_unlink(self._name)
269
- if self._track:
270
- _mprt.unregister(self._name, "shared_memory")
271
-
272
-
273
- class SharedMemoryManager:
274
- """
275
- A simple shared memory contextmanager based on
276
- https://docs.python.org/3/library/multiprocessing.shared_memory.html#multiprocessing.shared_memory.SharedMemory
277
- """
278
- def __init__(self, create=False) -> None:
279
- self._shms: list[SharedMemory] = []
280
- self.__create = create
281
-
282
- def SharedMemory(self, *, name=None, create=False, size=0, track=True):
283
- shm = SharedMemory(name=name, create=create, size=size, track=track)
284
- shm._create = create
285
- # Essential to keep refs on Windows
286
- # https://stackoverflow.com/questions/74193377/filenotfounderror-when-passing-a-shared-memory-to-a-new-process#comment130999060_74194875 # noqa: E501
287
- self._shms.append(shm)
288
- return shm
289
-
290
- def __enter__(self):
291
- return self
292
-
293
- def __exit__(self, *args, **kwargs):
294
- for shm in self._shms:
295
- try:
296
- shm.close()
297
- if shm._create:
298
- shm.unlink()
299
- except Exception:
300
- warnings.warn(f'Failed to unlink shared memory {shm.name!r}',
301
- category=ResourceWarning, stacklevel=2)
302
- raise
303
-
304
- def arr2shm(self, vals):
305
- """Array to shared memory. Returns (shm_name, shape, dtype) used for restore."""
306
- assert vals.ndim == 1, (vals.ndim, vals.shape, vals)
307
- shm = self.SharedMemory(size=vals.nbytes, create=True)
308
- # np.array can't handle pandas' tz-aware datetimes
309
- # https://github.com/numpy/numpy/issues/18279
310
- buf = np.ndarray(vals.shape, dtype=vals.dtype.base, buffer=shm.buf)
311
- has_tz = getattr(vals.dtype, 'tz', None)
312
- buf[:] = vals.tz_localize(None) if has_tz else vals # Copy into shared memory
313
- return shm.name, vals.shape, vals.dtype
314
-
315
- def df2shm(self, df):
316
- return tuple((
317
- (column, *self.arr2shm(values))
318
- for column, values in chain([(self._DF_INDEX_COL, df.index)], df.items())
319
- ))
320
-
321
- @staticmethod
322
- def shm2s(shm, shape, dtype) -> pd.Series:
323
- arr = np.ndarray(shape, dtype=dtype.base, buffer=shm.buf)
324
- arr.setflags(write=False)
325
- return pd.Series(arr, dtype=dtype)
326
-
327
- _DF_INDEX_COL = '__bt_index'
328
-
329
- @staticmethod
330
- def shm2df(data_shm):
331
- shm = [SharedMemory(name=name, create=False, track=False) for _, name, _, _ in data_shm]
332
- df = pd.DataFrame({
333
- col: SharedMemoryManager.shm2s(shm, shape, dtype)
334
- for shm, (col, _, shape, dtype) in zip(shm, data_shm)})
335
- df.set_index(SharedMemoryManager._DF_INDEX_COL, drop=True, inplace=True)
336
- df.index.name = None
337
- return df, shm