ebt-backtest 0.1.0__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.
- ebt_backtest-0.1.0/LICENSE +21 -0
- ebt_backtest-0.1.0/MANIFEST.in +4 -0
- ebt_backtest-0.1.0/PKG-INFO +208 -0
- ebt_backtest-0.1.0/README.md +168 -0
- ebt_backtest-0.1.0/ebt/__init__.py +10 -0
- ebt_backtest-0.1.0/ebt/data/__init__.py +2 -0
- ebt_backtest-0.1.0/ebt/data/outlier.py +146 -0
- ebt_backtest-0.1.0/ebt/data/types.py +69 -0
- ebt_backtest-0.1.0/ebt/features/__init__.py +1 -0
- ebt_backtest-0.1.0/ebt/features/core.py +257 -0
- ebt_backtest-0.1.0/ebt/futures.py +1000 -0
- ebt_backtest-0.1.0/ebt/parallel.py +676 -0
- ebt_backtest-0.1.0/ebt/simulate/__init__.py +13 -0
- ebt_backtest-0.1.0/ebt/simulate/costaware.py +572 -0
- ebt_backtest-0.1.0/ebt/simulate/execution.py +468 -0
- ebt_backtest-0.1.0/ebt/simulate/features.py +2 -0
- ebt_backtest-0.1.0/ebt/simulate/gate.py +166 -0
- ebt_backtest-0.1.0/ebt/simulate/grid.py +432 -0
- ebt_backtest-0.1.0/ebt/simulate/kf.py +1767 -0
- ebt_backtest-0.1.0/ebt/simulate/linear.py +230 -0
- ebt_backtest-0.1.0/ebt/simulate/model.py +96 -0
- ebt_backtest-0.1.0/ebt/simulate/nn/__init__.py +1 -0
- ebt_backtest-0.1.0/ebt/simulate/nn/loader.py +37 -0
- ebt_backtest-0.1.0/ebt/simulate/nn/model.py +402 -0
- ebt_backtest-0.1.0/ebt/simulate/nn/nn_loss.py +38 -0
- ebt_backtest-0.1.0/ebt/simulate/nn/util.py +82 -0
- ebt_backtest-0.1.0/ebt/simulate/optimize.py +577 -0
- ebt_backtest-0.1.0/ebt/simulate/plot.py +3338 -0
- ebt_backtest-0.1.0/ebt/simulate/postprocessing.py +175 -0
- ebt_backtest-0.1.0/ebt/simulate/predict.py +368 -0
- ebt_backtest-0.1.0/ebt/simulate/spread.py +330 -0
- ebt_backtest-0.1.0/ebt/simulate/stats.py +127 -0
- ebt_backtest-0.1.0/ebt/simulate/timeseries.py +90 -0
- ebt_backtest-0.1.0/ebt/simulate/tricks.py +239 -0
- ebt_backtest-0.1.0/ebt_backtest.egg-info/PKG-INFO +208 -0
- ebt_backtest-0.1.0/ebt_backtest.egg-info/SOURCES.txt +52 -0
- ebt_backtest-0.1.0/ebt_backtest.egg-info/dependency_links.txt +1 -0
- ebt_backtest-0.1.0/ebt_backtest.egg-info/requires.txt +29 -0
- ebt_backtest-0.1.0/ebt_backtest.egg-info/top_level.txt +1 -0
- ebt_backtest-0.1.0/ebt_rust/Cargo.lock +400 -0
- ebt_backtest-0.1.0/ebt_rust/Cargo.toml +27 -0
- ebt_backtest-0.1.0/ebt_rust/src/backtest.rs +2001 -0
- ebt_backtest-0.1.0/ebt_rust/src/combine_positions.rs +185 -0
- ebt_backtest-0.1.0/ebt_rust/src/execution.rs +303 -0
- ebt_backtest-0.1.0/ebt_rust/src/lib.rs +906 -0
- ebt_backtest-0.1.0/ebt_rust/src/outlier.rs +3060 -0
- ebt_backtest-0.1.0/ebt_rust/src/rows.rs +63 -0
- ebt_backtest-0.1.0/pyproject.toml +47 -0
- ebt_backtest-0.1.0/setup.cfg +4 -0
- ebt_backtest-0.1.0/tests/test_backtest.py +542 -0
- ebt_backtest-0.1.0/tests/test_futures.py +401 -0
- ebt_backtest-0.1.0/tests/test_kernels.py +371 -0
- ebt_backtest-0.1.0/tests/test_outlier.py +209 -0
- ebt_backtest-0.1.0/tests/test_simulate.py +874 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ebt contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ebt-backtest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Backtesting and evaluation toolkit for position-based trading strategies
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Rust
|
|
9
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
10
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: pandas
|
|
16
|
+
Requires-Dist: scipy
|
|
17
|
+
Requires-Dist: scikit-learn
|
|
18
|
+
Requires-Dist: numba
|
|
19
|
+
Requires-Dist: matplotlib
|
|
20
|
+
Requires-Dist: seaborn
|
|
21
|
+
Requires-Dist: Pillow
|
|
22
|
+
Requires-Dist: tqdm
|
|
23
|
+
Requires-Dist: psutil
|
|
24
|
+
Requires-Dist: pyarrow
|
|
25
|
+
Requires-Dist: dask[distributed]
|
|
26
|
+
Requires-Dist: polars
|
|
27
|
+
Requires-Dist: torch
|
|
28
|
+
Requires-Dist: torcheval
|
|
29
|
+
Requires-Dist: torch-lr-finder
|
|
30
|
+
Requires-Dist: tensorboard
|
|
31
|
+
Provides-Extra: nn
|
|
32
|
+
Provides-Extra: notebook
|
|
33
|
+
Requires-Dist: ipython; extra == "notebook"
|
|
34
|
+
Requires-Dist: ipywidgets; extra == "notebook"
|
|
35
|
+
Provides-Extra: optimize
|
|
36
|
+
Requires-Dist: cvxpy; extra == "optimize"
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: pytest; extra == "test"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# ebt
|
|
42
|
+
|
|
43
|
+
Backtesting and evaluation toolkit for position-based trading strategies.
|
|
44
|
+
|
|
45
|
+
You bring a price frame (DatetimeIndex × assets) and a position frame on the same assets.
|
|
46
|
+
ebt fits models walk-forward, turns predictions into positions, backtests them with an
|
|
47
|
+
execution lag, reports key figures, and sweeps strategy variants into sub-portfolios.
|
|
48
|
+
The hot loops (backtest, execution simulation, position combination, outlier detection)
|
|
49
|
+
are Rust kernels built into the package as `ebt_rust`.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- **Walk-forward prediction** (`ebt.predict`): retrain any `fit`/`predict` model on a
|
|
54
|
+
rolling window with an embargo gap, for sklearn-style linear models and PyTorch nets.
|
|
55
|
+
- **Feature and label helpers** (`ebt.features`): multi-scale EMA filter banks, forward
|
|
56
|
+
labels, OHLC resampling with causal timestamps, cross-sectional ranks.
|
|
57
|
+
- **Backtest and key figures** (`ebt.simulate.kf`): PnL from positions and prices with an
|
|
58
|
+
integer or timedelta execution lag, Sharpe decomposition, turnover, holding time,
|
|
59
|
+
Sharpe-by-lag, trend correlation. Wide (one column per asset) and stacked/long input.
|
|
60
|
+
- **One-pager and ensemble sweeps** (`ebt.simulate.plot`): `one_pager` evaluates one book
|
|
61
|
+
and draws the standard figure; `plot_ensemble` evaluates a list of variants, splits them
|
|
62
|
+
into sub-portfolios by config variable and summarises the sweep.
|
|
63
|
+
- **Risk sizing and sanity checks**: `risk_adjust` to a target annualised risk,
|
|
64
|
+
`risk_to_lots`, cost-aware sizing (`ebt.simulate.costaware`), and a
|
|
65
|
+
`position_sanity_gate` that flags broken books before anyone reads their Sharpe.
|
|
66
|
+
- **Execution models**: VWAP-ramped fills, spread-aware timing, trade suppression below
|
|
67
|
+
a risk threshold; effective-spread estimators from daily OHLC.
|
|
68
|
+
- **Futures tooling** (`ebt.futures`): contract selection, rolls, Panama adjustment,
|
|
69
|
+
blended rolls, interpolated and open-interest-weighted continuous series, position
|
|
70
|
+
decomposition back into contracts.
|
|
71
|
+
- **Streaming outlier detection** (`ebt.data.outlier`): point-in-time detectors for
|
|
72
|
+
trades, quotes and OHLCV bars.
|
|
73
|
+
- **Parallel map** (`ebt.parallel`): fork or lean-subprocess workers with zero-copy
|
|
74
|
+
shared frames.
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
Requires Python 3.9+ and a Rust toolchain (https://rustup.rs) to build the extension.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install ebt-backtest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Wheels are published for Linux x86_64 and Windows (Python 3.9+); other platforms, including macOS, build from the sdist with a Rust toolchain. From a checkout:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install .
|
|
88
|
+
# editable install (the top-level ebt_rust extension needs compat mode):
|
|
89
|
+
pip install -e . --config-settings editable_mode=compat
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Extras: `ebt[notebook]` (IPython widgets for the tabbed sweep views), `ebt[optimize]`
|
|
93
|
+
(cvxpy for the allocation optimiser), `ebt[test]`.
|
|
94
|
+
|
|
95
|
+
## Quickstart
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import numpy as np, pandas as pd
|
|
99
|
+
import ebt
|
|
100
|
+
from ebt.simulate.linear import Ridge
|
|
101
|
+
from ebt.simulate.plot import one_pager, plot_ensemble
|
|
102
|
+
from ebt.simulate.tricks import risk_adjust
|
|
103
|
+
|
|
104
|
+
# synthetic 5-minute prices for four assets
|
|
105
|
+
rng = np.random.default_rng(0)
|
|
106
|
+
idx = pd.date_range('2022-01-03', '2023-12-29', freq='5min')
|
|
107
|
+
rets = 1e-3 * rng.standard_normal((len(idx), 4))
|
|
108
|
+
prices = pd.DataFrame(100 * np.exp(np.cumsum(rets, axis=0)), index=idx, columns=list('ABCD'))
|
|
109
|
+
|
|
110
|
+
# daily features and forward labels; a bar's timestamp is when its price is known
|
|
111
|
+
daily_ret = prices.resample('1D', label='right').last().dropna().pct_change()
|
|
112
|
+
features = ebt.apply_filters(daily_ret, lengths=[4, 16, 64]).replace([np.inf, -np.inf], np.nan).dropna()
|
|
113
|
+
labels = ebt.make_forward_labels(daily_ret, smoothing_length=5).reindex(features.index)
|
|
114
|
+
|
|
115
|
+
# walk-forward fit: retrain every 60 days on an expanding window, 10-day embargo
|
|
116
|
+
def walk_forward(model):
|
|
117
|
+
out = ebt.predict(features, labels, models=[model], min_lookback='120D',
|
|
118
|
+
retrain_interval='60D', embargo='10D', progressbar=False)
|
|
119
|
+
return risk_adjust(out[model], prices, shift='2min') # scale to annualised risk 1.0
|
|
120
|
+
|
|
121
|
+
pos = walk_forward(Ridge(alpha=1.0))
|
|
122
|
+
|
|
123
|
+
# evaluate against the raw prices with a 2-minute execution lag
|
|
124
|
+
res = one_pager(pos, prices, shift='2min')
|
|
125
|
+
print(res['kf']) # sharpe, sharpe exbias, turnover, holding(days), lptp, ...
|
|
126
|
+
|
|
127
|
+
# sweep variants and compare sub-portfolios per config variable
|
|
128
|
+
configs = [{'alpha': a} for a in (0.1, 1.0, 10.0)]
|
|
129
|
+
positions = [walk_forward(Ridge(alpha=c['alpha'])) for c in configs]
|
|
130
|
+
ens = plot_ensemble(positions, prices, configs_df=pd.DataFrame(configs), shift='2min')
|
|
131
|
+
print(ens['kfs_df']) # one row per variant: config columns + key figures
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Neural-net models work the same way: `ebt.simulate.nn.NeuralNetModel(create_net=FeedForwardNet,
|
|
135
|
+
n_hidden=[16], ...)` trains with a masked MSE loss by default; pass `create_loss_f` for your own
|
|
136
|
+
objective.
|
|
137
|
+
|
|
138
|
+
## Conventions
|
|
139
|
+
|
|
140
|
+
- **Timestamps mark when a value is known.** Resample prices with `label='right'`; the bar
|
|
141
|
+
stamped `T` holds prints in `(T - freq, T]`. `make_ohlc` and `make_forward_labels` follow this.
|
|
142
|
+
- **Positions** are frames on the same assets as the prices. `NaN` means "carry the previous
|
|
143
|
+
position", `0` means flat. After `risk_adjust` a book has annualised risk 1.0, so key figures
|
|
144
|
+
across books are comparable.
|
|
145
|
+
- **Execution lag** `shift` is an integer number of price bars or a timedelta (`'2min'`,
|
|
146
|
+
`pd.Timedelta('5m')`). A timedelta fills at the first price print at or after
|
|
147
|
+
`stamp + shift`, so pass raw prices at a resolution finer than the signal.
|
|
148
|
+
- **Stacked (long) input**: `backtest_stacked` and friends accept one row per `(time, contract)`
|
|
149
|
+
in pandas or Polars. Column names are auto-detected (`ts` / `time` / `timestamp` for time,
|
|
150
|
+
`series` / `symbol` / `contract` for the contract, `price` / `close` / `return` for the value)
|
|
151
|
+
and can be given explicitly with `time_col=`, `contract_col=`, `pos_value_col=`, `price_value_col=`.
|
|
152
|
+
- **Futures tools** take a `columns={...}` mapping (`time`, `contract`, `expiry`, `price`, `weight`)
|
|
153
|
+
so any source schema works without renaming; outputs add a `series` column naming the
|
|
154
|
+
continuous series (`'FUT(front)'`, `'FUT(30d)'`, `'FUT(top3)'`, ...).
|
|
155
|
+
|
|
156
|
+
## Key figures
|
|
157
|
+
|
|
158
|
+
Computed by `one_pager` and `plot_ensemble` ("kf" = key figure). Sharpe and risk use daily PnL.
|
|
159
|
+
|
|
160
|
+
| Key figure | Meaning |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `sharpe` | Annualised Sharpe of the book's daily PnL. |
|
|
163
|
+
| `sharpe exbias` / `sharpe bias` | Sharpe of the timing component and of the static component. Each asset's position (in risk units) is split into its time-mean and the remainder; `bias` is the PnL of the constant mean position, `exbias` the rest. Slow books can earn most of their raw Sharpe from `bias`. |
|
|
164
|
+
| `sharpe long` / `sharpe short` | Sharpe of the long and short legs. |
|
|
165
|
+
| `sharpe w costs` | Sharpe after spread costs when `spreads=` is given. |
|
|
166
|
+
| `turnover` | Daily `sum(|Δpos|) / mean(|pos|)`, frequency-independent. |
|
|
167
|
+
| `holding(days)` | Average holding period implied by the turnover. |
|
|
168
|
+
| `lptp` | Long-position-to-total-position: `0.5 + sum(pos) / (2 sum(|pos|))`. 1 long-only, 0 short-only, 0.5 dollar-neutral. |
|
|
169
|
+
| `up_days` | Fraction of days with positive PnL. |
|
|
170
|
+
| `risk` | Annualised std of daily PnL (1.0 after `risk_adjust`). |
|
|
171
|
+
| `ncvar` | Normalised covariance ratio: the book's variance under an EWMA correlation model divided by its variance if the assets were uncorrelated. 1 for uncorrelated positions, above 1 when positions load on correlated assets, below 1 when they hedge each other. |
|
|
172
|
+
| `sharpe_decay` | Log-log slope of Sharpe against holding time across smoothed versions of the book; near 0 the signal persists, around -0.5 it decays like noise. |
|
|
173
|
+
| `skew`, `kurtosis` | Of the daily PnL. |
|
|
174
|
+
|
|
175
|
+
## Package map
|
|
176
|
+
|
|
177
|
+
| Module | Contents |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `ebt.features.core` | `apply_filters`, `make_forward_labels`, `make_ohlc`, `make_daily_from_intraday`, `normalized_rank`, `ffill_timeseries` |
|
|
180
|
+
| `ebt.simulate.predict` | `predict` walk-forward driver, `generator`, `enough_data` |
|
|
181
|
+
| `ebt.simulate.model`, `.linear`, `.nn` | `Model` base class, sklearn wrappers (`Ridge`, `LassoLars`, `SparseMarkowitz`, ...), `NeuralNetModel` / `LSTMNeuralNetModel` |
|
|
182
|
+
| `ebt.simulate.kf` | `backtest`, `backtest_stacked`, `sharpe`, `turnover`, `holding_time`, `lptp`, `ncvar`, `sharpe_per_lag`, `holding_curve`, `sharpe_decay`, `correlation_to_trend` |
|
|
183
|
+
| `ebt.simulate.plot` | `one_pager`, `plot_ensemble`, `per_year_table`, figure building blocks |
|
|
184
|
+
| `ebt.simulate.postprocessing`, `.tricks` | `risk_adjust`, `risk_to_lots`, `combine_positions`, `split_portfolios`, `nanagg` |
|
|
185
|
+
| `ebt.simulate.gate` | `position_sanity_gate` |
|
|
186
|
+
| `ebt.simulate.grid` | `ReducedGrid`: evaluate sparse signals on the price rows that matter |
|
|
187
|
+
| `ebt.simulate.execution`, `.spread`, `.costaware` | execution simulators, spread estimators, cost-aware sizing and turnover budgets |
|
|
188
|
+
| `ebt.simulate.optimize` | ensemble weight optimisation |
|
|
189
|
+
| `ebt.futures` | contract schedules, rolls, continuous prices and weights, position decomposition |
|
|
190
|
+
| `ebt.data.outlier` | `detect_outliers` for trades, quotes and bars |
|
|
191
|
+
| `ebt.parallel` | `pmap` |
|
|
192
|
+
| `ebt_rust` | the compiled kernels (not a public API) |
|
|
193
|
+
|
|
194
|
+
## Tests
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install -e . --config-settings editable_mode=compat
|
|
198
|
+
python -m pytest tests
|
|
199
|
+
cd ebt_rust && cargo test --profile perf
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Releasing
|
|
203
|
+
|
|
204
|
+
`scripts/release.sh X.Y.Z` tags a release and the GitHub workflow publishes it; see `RELEASING.md`.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT, see `LICENSE`.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# ebt
|
|
2
|
+
|
|
3
|
+
Backtesting and evaluation toolkit for position-based trading strategies.
|
|
4
|
+
|
|
5
|
+
You bring a price frame (DatetimeIndex × assets) and a position frame on the same assets.
|
|
6
|
+
ebt fits models walk-forward, turns predictions into positions, backtests them with an
|
|
7
|
+
execution lag, reports key figures, and sweeps strategy variants into sub-portfolios.
|
|
8
|
+
The hot loops (backtest, execution simulation, position combination, outlier detection)
|
|
9
|
+
are Rust kernels built into the package as `ebt_rust`.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Walk-forward prediction** (`ebt.predict`): retrain any `fit`/`predict` model on a
|
|
14
|
+
rolling window with an embargo gap, for sklearn-style linear models and PyTorch nets.
|
|
15
|
+
- **Feature and label helpers** (`ebt.features`): multi-scale EMA filter banks, forward
|
|
16
|
+
labels, OHLC resampling with causal timestamps, cross-sectional ranks.
|
|
17
|
+
- **Backtest and key figures** (`ebt.simulate.kf`): PnL from positions and prices with an
|
|
18
|
+
integer or timedelta execution lag, Sharpe decomposition, turnover, holding time,
|
|
19
|
+
Sharpe-by-lag, trend correlation. Wide (one column per asset) and stacked/long input.
|
|
20
|
+
- **One-pager and ensemble sweeps** (`ebt.simulate.plot`): `one_pager` evaluates one book
|
|
21
|
+
and draws the standard figure; `plot_ensemble` evaluates a list of variants, splits them
|
|
22
|
+
into sub-portfolios by config variable and summarises the sweep.
|
|
23
|
+
- **Risk sizing and sanity checks**: `risk_adjust` to a target annualised risk,
|
|
24
|
+
`risk_to_lots`, cost-aware sizing (`ebt.simulate.costaware`), and a
|
|
25
|
+
`position_sanity_gate` that flags broken books before anyone reads their Sharpe.
|
|
26
|
+
- **Execution models**: VWAP-ramped fills, spread-aware timing, trade suppression below
|
|
27
|
+
a risk threshold; effective-spread estimators from daily OHLC.
|
|
28
|
+
- **Futures tooling** (`ebt.futures`): contract selection, rolls, Panama adjustment,
|
|
29
|
+
blended rolls, interpolated and open-interest-weighted continuous series, position
|
|
30
|
+
decomposition back into contracts.
|
|
31
|
+
- **Streaming outlier detection** (`ebt.data.outlier`): point-in-time detectors for
|
|
32
|
+
trades, quotes and OHLCV bars.
|
|
33
|
+
- **Parallel map** (`ebt.parallel`): fork or lean-subprocess workers with zero-copy
|
|
34
|
+
shared frames.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Requires Python 3.9+ and a Rust toolchain (https://rustup.rs) to build the extension.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install ebt-backtest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Wheels are published for Linux x86_64 and Windows (Python 3.9+); other platforms, including macOS, build from the sdist with a Rust toolchain. From a checkout:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install .
|
|
48
|
+
# editable install (the top-level ebt_rust extension needs compat mode):
|
|
49
|
+
pip install -e . --config-settings editable_mode=compat
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Extras: `ebt[notebook]` (IPython widgets for the tabbed sweep views), `ebt[optimize]`
|
|
53
|
+
(cvxpy for the allocation optimiser), `ebt[test]`.
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import numpy as np, pandas as pd
|
|
59
|
+
import ebt
|
|
60
|
+
from ebt.simulate.linear import Ridge
|
|
61
|
+
from ebt.simulate.plot import one_pager, plot_ensemble
|
|
62
|
+
from ebt.simulate.tricks import risk_adjust
|
|
63
|
+
|
|
64
|
+
# synthetic 5-minute prices for four assets
|
|
65
|
+
rng = np.random.default_rng(0)
|
|
66
|
+
idx = pd.date_range('2022-01-03', '2023-12-29', freq='5min')
|
|
67
|
+
rets = 1e-3 * rng.standard_normal((len(idx), 4))
|
|
68
|
+
prices = pd.DataFrame(100 * np.exp(np.cumsum(rets, axis=0)), index=idx, columns=list('ABCD'))
|
|
69
|
+
|
|
70
|
+
# daily features and forward labels; a bar's timestamp is when its price is known
|
|
71
|
+
daily_ret = prices.resample('1D', label='right').last().dropna().pct_change()
|
|
72
|
+
features = ebt.apply_filters(daily_ret, lengths=[4, 16, 64]).replace([np.inf, -np.inf], np.nan).dropna()
|
|
73
|
+
labels = ebt.make_forward_labels(daily_ret, smoothing_length=5).reindex(features.index)
|
|
74
|
+
|
|
75
|
+
# walk-forward fit: retrain every 60 days on an expanding window, 10-day embargo
|
|
76
|
+
def walk_forward(model):
|
|
77
|
+
out = ebt.predict(features, labels, models=[model], min_lookback='120D',
|
|
78
|
+
retrain_interval='60D', embargo='10D', progressbar=False)
|
|
79
|
+
return risk_adjust(out[model], prices, shift='2min') # scale to annualised risk 1.0
|
|
80
|
+
|
|
81
|
+
pos = walk_forward(Ridge(alpha=1.0))
|
|
82
|
+
|
|
83
|
+
# evaluate against the raw prices with a 2-minute execution lag
|
|
84
|
+
res = one_pager(pos, prices, shift='2min')
|
|
85
|
+
print(res['kf']) # sharpe, sharpe exbias, turnover, holding(days), lptp, ...
|
|
86
|
+
|
|
87
|
+
# sweep variants and compare sub-portfolios per config variable
|
|
88
|
+
configs = [{'alpha': a} for a in (0.1, 1.0, 10.0)]
|
|
89
|
+
positions = [walk_forward(Ridge(alpha=c['alpha'])) for c in configs]
|
|
90
|
+
ens = plot_ensemble(positions, prices, configs_df=pd.DataFrame(configs), shift='2min')
|
|
91
|
+
print(ens['kfs_df']) # one row per variant: config columns + key figures
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Neural-net models work the same way: `ebt.simulate.nn.NeuralNetModel(create_net=FeedForwardNet,
|
|
95
|
+
n_hidden=[16], ...)` trains with a masked MSE loss by default; pass `create_loss_f` for your own
|
|
96
|
+
objective.
|
|
97
|
+
|
|
98
|
+
## Conventions
|
|
99
|
+
|
|
100
|
+
- **Timestamps mark when a value is known.** Resample prices with `label='right'`; the bar
|
|
101
|
+
stamped `T` holds prints in `(T - freq, T]`. `make_ohlc` and `make_forward_labels` follow this.
|
|
102
|
+
- **Positions** are frames on the same assets as the prices. `NaN` means "carry the previous
|
|
103
|
+
position", `0` means flat. After `risk_adjust` a book has annualised risk 1.0, so key figures
|
|
104
|
+
across books are comparable.
|
|
105
|
+
- **Execution lag** `shift` is an integer number of price bars or a timedelta (`'2min'`,
|
|
106
|
+
`pd.Timedelta('5m')`). A timedelta fills at the first price print at or after
|
|
107
|
+
`stamp + shift`, so pass raw prices at a resolution finer than the signal.
|
|
108
|
+
- **Stacked (long) input**: `backtest_stacked` and friends accept one row per `(time, contract)`
|
|
109
|
+
in pandas or Polars. Column names are auto-detected (`ts` / `time` / `timestamp` for time,
|
|
110
|
+
`series` / `symbol` / `contract` for the contract, `price` / `close` / `return` for the value)
|
|
111
|
+
and can be given explicitly with `time_col=`, `contract_col=`, `pos_value_col=`, `price_value_col=`.
|
|
112
|
+
- **Futures tools** take a `columns={...}` mapping (`time`, `contract`, `expiry`, `price`, `weight`)
|
|
113
|
+
so any source schema works without renaming; outputs add a `series` column naming the
|
|
114
|
+
continuous series (`'FUT(front)'`, `'FUT(30d)'`, `'FUT(top3)'`, ...).
|
|
115
|
+
|
|
116
|
+
## Key figures
|
|
117
|
+
|
|
118
|
+
Computed by `one_pager` and `plot_ensemble` ("kf" = key figure). Sharpe and risk use daily PnL.
|
|
119
|
+
|
|
120
|
+
| Key figure | Meaning |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `sharpe` | Annualised Sharpe of the book's daily PnL. |
|
|
123
|
+
| `sharpe exbias` / `sharpe bias` | Sharpe of the timing component and of the static component. Each asset's position (in risk units) is split into its time-mean and the remainder; `bias` is the PnL of the constant mean position, `exbias` the rest. Slow books can earn most of their raw Sharpe from `bias`. |
|
|
124
|
+
| `sharpe long` / `sharpe short` | Sharpe of the long and short legs. |
|
|
125
|
+
| `sharpe w costs` | Sharpe after spread costs when `spreads=` is given. |
|
|
126
|
+
| `turnover` | Daily `sum(|Δpos|) / mean(|pos|)`, frequency-independent. |
|
|
127
|
+
| `holding(days)` | Average holding period implied by the turnover. |
|
|
128
|
+
| `lptp` | Long-position-to-total-position: `0.5 + sum(pos) / (2 sum(|pos|))`. 1 long-only, 0 short-only, 0.5 dollar-neutral. |
|
|
129
|
+
| `up_days` | Fraction of days with positive PnL. |
|
|
130
|
+
| `risk` | Annualised std of daily PnL (1.0 after `risk_adjust`). |
|
|
131
|
+
| `ncvar` | Normalised covariance ratio: the book's variance under an EWMA correlation model divided by its variance if the assets were uncorrelated. 1 for uncorrelated positions, above 1 when positions load on correlated assets, below 1 when they hedge each other. |
|
|
132
|
+
| `sharpe_decay` | Log-log slope of Sharpe against holding time across smoothed versions of the book; near 0 the signal persists, around -0.5 it decays like noise. |
|
|
133
|
+
| `skew`, `kurtosis` | Of the daily PnL. |
|
|
134
|
+
|
|
135
|
+
## Package map
|
|
136
|
+
|
|
137
|
+
| Module | Contents |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `ebt.features.core` | `apply_filters`, `make_forward_labels`, `make_ohlc`, `make_daily_from_intraday`, `normalized_rank`, `ffill_timeseries` |
|
|
140
|
+
| `ebt.simulate.predict` | `predict` walk-forward driver, `generator`, `enough_data` |
|
|
141
|
+
| `ebt.simulate.model`, `.linear`, `.nn` | `Model` base class, sklearn wrappers (`Ridge`, `LassoLars`, `SparseMarkowitz`, ...), `NeuralNetModel` / `LSTMNeuralNetModel` |
|
|
142
|
+
| `ebt.simulate.kf` | `backtest`, `backtest_stacked`, `sharpe`, `turnover`, `holding_time`, `lptp`, `ncvar`, `sharpe_per_lag`, `holding_curve`, `sharpe_decay`, `correlation_to_trend` |
|
|
143
|
+
| `ebt.simulate.plot` | `one_pager`, `plot_ensemble`, `per_year_table`, figure building blocks |
|
|
144
|
+
| `ebt.simulate.postprocessing`, `.tricks` | `risk_adjust`, `risk_to_lots`, `combine_positions`, `split_portfolios`, `nanagg` |
|
|
145
|
+
| `ebt.simulate.gate` | `position_sanity_gate` |
|
|
146
|
+
| `ebt.simulate.grid` | `ReducedGrid`: evaluate sparse signals on the price rows that matter |
|
|
147
|
+
| `ebt.simulate.execution`, `.spread`, `.costaware` | execution simulators, spread estimators, cost-aware sizing and turnover budgets |
|
|
148
|
+
| `ebt.simulate.optimize` | ensemble weight optimisation |
|
|
149
|
+
| `ebt.futures` | contract schedules, rolls, continuous prices and weights, position decomposition |
|
|
150
|
+
| `ebt.data.outlier` | `detect_outliers` for trades, quotes and bars |
|
|
151
|
+
| `ebt.parallel` | `pmap` |
|
|
152
|
+
| `ebt_rust` | the compiled kernels (not a public API) |
|
|
153
|
+
|
|
154
|
+
## Tests
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pip install -e . --config-settings editable_mode=compat
|
|
158
|
+
python -m pytest tests
|
|
159
|
+
cd ebt_rust && cargo test --profile perf
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Releasing
|
|
163
|
+
|
|
164
|
+
`scripts/release.sh X.Y.Z` tags a release and the GitHub workflow publishes it; see `RELEASING.md`.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT, see `LICENSE`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from ebt.simulate.timeseries import *
|
|
2
|
+
from ebt.simulate.stats import *
|
|
3
|
+
from ebt.simulate.predict import *
|
|
4
|
+
from ebt.simulate.kf import *
|
|
5
|
+
from ebt.simulate.plot import *
|
|
6
|
+
from ebt.data import *
|
|
7
|
+
from ebt.simulate.features import *
|
|
8
|
+
from ebt.simulate.tricks import *
|
|
9
|
+
from ebt import simulate
|
|
10
|
+
from ebt.simulate.optimize import *
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Streaming outlier detection for trades, quotes and OHLCV bars (Rust kernels).
|
|
2
|
+
|
|
3
|
+
The detectors are sequential and point-in-time: the decision for a row depends only
|
|
4
|
+
on the rows before it. See ``detect_outliers``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional, Dict, Literal
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Kind = Literal["auto", "bars", "trades", "quotes"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def detect_kind(df: pd.DataFrame) -> Kind:
|
|
19
|
+
cols = {c.lower() for c in df.columns}
|
|
20
|
+
if {"open", "high", "low", "close"}.issubset(cols):
|
|
21
|
+
return "bars"
|
|
22
|
+
if {"bid", "ask"}.issubset(cols) or {"bid_price", "ask_price"}.issubset(cols):
|
|
23
|
+
return "quotes"
|
|
24
|
+
if "price" in cols:
|
|
25
|
+
return "trades"
|
|
26
|
+
raise ValueError("Could not detect kind. Provide kind='bars'|'trades'|'quotes'.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_RUST_DEFAULT_COLUMNS = {
|
|
30
|
+
"trades": {"time": None, "price": "price", "volume": "volume"},
|
|
31
|
+
"quotes": {"time": None, "bid": "bid_price", "ask": "ask_price"},
|
|
32
|
+
"bars": {"time": None, "open": "open", "high": "high", "low": "low", "close": "close", "volume": "volume"},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def detect_outliers(df, kind: Kind = "auto", columns: Optional[Dict[str, str]] = None,
|
|
37
|
+
return_debug: bool = False, **config):
|
|
38
|
+
"""Streaming outlier detection with the Rust detectors.
|
|
39
|
+
|
|
40
|
+
trades: ensemble of an adaptive level filter, rolling z-score, IQR, price
|
|
41
|
+
jump, time gap and volume checks; rejected when enough methods vote against.
|
|
42
|
+
quotes: mid-price level filter cascaded with a spread upper-bound filter.
|
|
43
|
+
bars: bar consistency checks (non-finite prices, high < low), then the trade
|
|
44
|
+
ensemble on the close.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
df : pandas or polars DataFrame, sorted by time. A pandas DatetimeIndex is used
|
|
49
|
+
as the time column when ``columns`` doesn't name one.
|
|
50
|
+
kind : "auto" | "trades" | "quotes" | "bars"
|
|
51
|
+
columns : maps roles to column names, e.g. ``{"time": "ts", "price": "px"}``.
|
|
52
|
+
Roles: trades — time, price, volume; quotes — time, bid, ask;
|
|
53
|
+
bars — time, open, high, low, close, volume. Without a time column the
|
|
54
|
+
frame's single datetime column is used. A missing volume column counts as 0.
|
|
55
|
+
return_debug : return a DataFrame (same library as the input) of
|
|
56
|
+
``accepted`` plus the detector's internal state per row.
|
|
57
|
+
**config : detector settings.
|
|
58
|
+
trades/bars: ``zscore_threshold``, ``iqr_multiplier``, ``min_votes_to_reject``,
|
|
59
|
+
``window_size``, ``merge_stat_votes``, ``gap_widen_threshold_sec``,
|
|
60
|
+
``warmup_threshold``, ``min_scale``, ``min_scale_ticks``, ``n_switch``,
|
|
61
|
+
``min_switch_seconds``, ``warmup_hard_limit``.
|
|
62
|
+
quotes: ``mid_tau_min``, ``mid_tau_max``, ``mid_n_switch``,
|
|
63
|
+
``spread_max_consecutive_reject``, ``max_spread``, ``warmup_count``,
|
|
64
|
+
``allow_crossed``, ``mid_min_scale``, ``mid_min_scale_ticks``,
|
|
65
|
+
``mid_min_switch_seconds``, ``spread_min_scale``, ``spread_min_scale_ticks``,
|
|
66
|
+
``warmup_hard_limit``.
|
|
67
|
+
The level filter's acceptance band is the learned variance floored at
|
|
68
|
+
``min_scale_ticks`` learned ticks (not a fixed percentage); a regime switch needs
|
|
69
|
+
``n_switch`` confirmations spanning ``min_switch_seconds``; during warm-up a
|
|
70
|
+
price further than ``warmup_hard_limit`` from the last accepted one is rejected
|
|
71
|
+
outright; crossed quotes are invalid unless ``allow_crossed``.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
numpy bool array (True = accepted), or the debug DataFrame. Rows with a missing
|
|
76
|
+
value are not accepted and don't update the detector.
|
|
77
|
+
"""
|
|
78
|
+
import ebt_rust
|
|
79
|
+
|
|
80
|
+
is_polars = type(df).__module__.startswith("polars")
|
|
81
|
+
pdf = df.to_pandas() if is_polars else df
|
|
82
|
+
if kind == "auto":
|
|
83
|
+
kind = detect_kind(pdf)
|
|
84
|
+
if kind == "quotes" and "bid_price" not in pdf.columns:
|
|
85
|
+
kind_cols = {"bid": "bid", "ask": "ask"}
|
|
86
|
+
else:
|
|
87
|
+
kind_cols = {}
|
|
88
|
+
else:
|
|
89
|
+
kind_cols = {}
|
|
90
|
+
if kind not in _RUST_DEFAULT_COLUMNS:
|
|
91
|
+
raise ValueError(f"Unknown kind: {kind}")
|
|
92
|
+
cols = {**_RUST_DEFAULT_COLUMNS[kind], **kind_cols, **(columns or {})}
|
|
93
|
+
|
|
94
|
+
if cols["time"] is not None:
|
|
95
|
+
t = pdf[cols["time"]]
|
|
96
|
+
elif pd.api.types.is_datetime64_any_dtype(pdf.index):
|
|
97
|
+
t = pdf.index.to_series()
|
|
98
|
+
else:
|
|
99
|
+
candidates = [c for c in pdf.columns if pd.api.types.is_datetime64_any_dtype(pdf[c])]
|
|
100
|
+
if len(candidates) != 1:
|
|
101
|
+
raise ValueError(f"Pass columns={{'time': ...}}: found {len(candidates)} datetime columns {candidates}")
|
|
102
|
+
t = pdf[candidates[0]]
|
|
103
|
+
if pd.api.types.is_datetime64_any_dtype(t):
|
|
104
|
+
t = t.dt.tz_convert("UTC").dt.tz_localize(None) if getattr(t.dt, "tz", None) is not None else t
|
|
105
|
+
time_ns = t.to_numpy(dtype="datetime64[ns]").view("int64")
|
|
106
|
+
valid = ~t.isna().to_numpy()
|
|
107
|
+
else:
|
|
108
|
+
valid = ~t.isna().to_numpy()
|
|
109
|
+
time_ns = t.fillna(0).to_numpy(dtype="int64")
|
|
110
|
+
|
|
111
|
+
def col(role, default_zero=False):
|
|
112
|
+
name = cols[role]
|
|
113
|
+
if name not in pdf.columns:
|
|
114
|
+
if default_zero:
|
|
115
|
+
return np.zeros(len(pdf))
|
|
116
|
+
raise KeyError(f"Column {name!r} for {role!r} not found; pass columns={{'{role}': ...}}")
|
|
117
|
+
s = pdf[name]
|
|
118
|
+
return s.to_numpy(dtype="float64", na_value=np.nan), ~s.isna().to_numpy()
|
|
119
|
+
|
|
120
|
+
def values(roles, zero_ok=()):
|
|
121
|
+
nonlocal valid
|
|
122
|
+
out = []
|
|
123
|
+
for r in roles:
|
|
124
|
+
v = col(r, default_zero=r in zero_ok)
|
|
125
|
+
if isinstance(v, tuple):
|
|
126
|
+
v, ok = v
|
|
127
|
+
valid = valid & ok
|
|
128
|
+
out.append(np.ascontiguousarray(v))
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
if kind == "trades":
|
|
132
|
+
args = values(["price", "volume"], zero_ok=("volume",))
|
|
133
|
+
res = ebt_rust.detect_outliers(time_ns, *args, valid=valid, return_debug=return_debug, **config)
|
|
134
|
+
elif kind == "quotes":
|
|
135
|
+
args = values(["bid", "ask"])
|
|
136
|
+
res = ebt_rust.detect_outliers_quotes(time_ns, *args, valid=valid, return_debug=return_debug, **config)
|
|
137
|
+
else:
|
|
138
|
+
args = values(["open", "high", "low", "close", "volume"], zero_ok=("volume",))
|
|
139
|
+
res = ebt_rust.detect_outliers_ohlcv(time_ns, *args, valid=valid, return_debug=return_debug, **config)
|
|
140
|
+
|
|
141
|
+
if not return_debug:
|
|
142
|
+
return res
|
|
143
|
+
if is_polars:
|
|
144
|
+
import polars as pl
|
|
145
|
+
return pl.DataFrame(res)
|
|
146
|
+
return pd.DataFrame(res, index=pdf.index)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pickle
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import MutableMapping
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
from ebt.simulate.tricks import is_dask_process
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LazyDiskDict(MutableMapping):
|
|
12
|
+
"""A dict whose values live on disk (one pickle per key) and are loaded on access.
|
|
13
|
+
|
|
14
|
+
Each instance owns a private directory ``<folder>/lazy_disk_dict_<uuid>/`` that is
|
|
15
|
+
removed when the instance is garbage collected (unless ``monitor_dask`` is set and the
|
|
16
|
+
instance lives inside a dask worker, where the files may be shared). Keys are
|
|
17
|
+
``str()``-ed and percent-encoded for the file name, so any key is safe ('a/b' does not
|
|
18
|
+
become a sub-directory) and distinct keys never collide. The directory name is a uuid,
|
|
19
|
+
not ``id(self)``: ``id`` is recycled by CPython, so a later instance could otherwise
|
|
20
|
+
read or overwrite the files an earlier (undeleted) one left behind.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, d=None, folder=None, monitor_dask=False):
|
|
24
|
+
self._folder = folder or tempfile.gettempdir()
|
|
25
|
+
self._dir = os.path.join(self._folder, f'lazy_disk_dict_{uuid.uuid4().hex}')
|
|
26
|
+
os.makedirs(self._dir, exist_ok=True)
|
|
27
|
+
self._keys = set() # Keep track of keys explicitly
|
|
28
|
+
self.is_dask = is_dask_process() if monitor_dask else False
|
|
29
|
+
if d is not None:
|
|
30
|
+
for k, v in d.items():
|
|
31
|
+
self[k] = v
|
|
32
|
+
|
|
33
|
+
def _filepath(self, key):
|
|
34
|
+
"""A safe file name for a given key (percent-encoded, no path separators)."""
|
|
35
|
+
return os.path.join(self._dir, f'{quote(str(key), safe="")}.pkl')
|
|
36
|
+
|
|
37
|
+
def __getitem__(self, key):
|
|
38
|
+
if key not in self._keys:
|
|
39
|
+
raise KeyError(key)
|
|
40
|
+
with open(self._filepath(key), "rb") as f:
|
|
41
|
+
return pickle.load(f)
|
|
42
|
+
|
|
43
|
+
def __setitem__(self, key, value):
|
|
44
|
+
os.makedirs(self._dir, exist_ok=True)
|
|
45
|
+
with open(self._filepath(key), "wb") as f:
|
|
46
|
+
pickle.dump(value, f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
47
|
+
self._keys.add(key)
|
|
48
|
+
|
|
49
|
+
def __delitem__(self, key):
|
|
50
|
+
if key not in self._keys:
|
|
51
|
+
raise KeyError(key)
|
|
52
|
+
os.remove(self._filepath(key))
|
|
53
|
+
self._keys.remove(key)
|
|
54
|
+
|
|
55
|
+
def __del__(self):
|
|
56
|
+
if getattr(self, 'is_dask', False):
|
|
57
|
+
return # files may be shared with dask workers; leave them
|
|
58
|
+
d = getattr(self, '_dir', None)
|
|
59
|
+
if d is not None:
|
|
60
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
61
|
+
|
|
62
|
+
def __iter__(self):
|
|
63
|
+
return iter(self._keys)
|
|
64
|
+
|
|
65
|
+
def __len__(self):
|
|
66
|
+
return len(self._keys)
|
|
67
|
+
|
|
68
|
+
def __repr__(self):
|
|
69
|
+
return f"{self.__class__.__name__}({list(self._keys)})"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ebt.features.core import *
|