BackcastPro 0.0.2__py3-none-any.whl → 0.0.4__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/__init__.py +10 -85
- BackcastPro/_broker.py +415 -0
- BackcastPro/_stats.py +169 -212
- BackcastPro/backtest.py +293 -0
- BackcastPro/data/JapanStock.py +171 -0
- BackcastPro/data/__init__.py +7 -0
- BackcastPro/order.py +151 -0
- BackcastPro/position.py +61 -0
- BackcastPro/strategy.py +174 -0
- BackcastPro/trade.py +195 -0
- backcastpro-0.0.4.dist-info/METADATA +69 -0
- backcastpro-0.0.4.dist-info/RECORD +14 -0
- BackcastPro/_plotting.py +0 -785
- BackcastPro/_util.py +0 -337
- BackcastPro/backtesting.py +0 -1763
- BackcastPro/lib.py +0 -646
- BackcastPro/test/__init__.py +0 -29
- BackcastPro/test/__main__.py +0 -7
- BackcastPro/test/_test.py +0 -1174
- backcastpro-0.0.2.dist-info/METADATA +0 -53
- backcastpro-0.0.2.dist-info/RECORD +0 -13
- {backcastpro-0.0.2.dist-info → backcastpro-0.0.4.dist-info}/WHEEL +0 -0
- {backcastpro-0.0.2.dist-info → backcastpro-0.0.4.dist-info}/top_level.txt +0 -0
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
|