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.
Files changed (54) hide show
  1. ebt_backtest-0.1.0/LICENSE +21 -0
  2. ebt_backtest-0.1.0/MANIFEST.in +4 -0
  3. ebt_backtest-0.1.0/PKG-INFO +208 -0
  4. ebt_backtest-0.1.0/README.md +168 -0
  5. ebt_backtest-0.1.0/ebt/__init__.py +10 -0
  6. ebt_backtest-0.1.0/ebt/data/__init__.py +2 -0
  7. ebt_backtest-0.1.0/ebt/data/outlier.py +146 -0
  8. ebt_backtest-0.1.0/ebt/data/types.py +69 -0
  9. ebt_backtest-0.1.0/ebt/features/__init__.py +1 -0
  10. ebt_backtest-0.1.0/ebt/features/core.py +257 -0
  11. ebt_backtest-0.1.0/ebt/futures.py +1000 -0
  12. ebt_backtest-0.1.0/ebt/parallel.py +676 -0
  13. ebt_backtest-0.1.0/ebt/simulate/__init__.py +13 -0
  14. ebt_backtest-0.1.0/ebt/simulate/costaware.py +572 -0
  15. ebt_backtest-0.1.0/ebt/simulate/execution.py +468 -0
  16. ebt_backtest-0.1.0/ebt/simulate/features.py +2 -0
  17. ebt_backtest-0.1.0/ebt/simulate/gate.py +166 -0
  18. ebt_backtest-0.1.0/ebt/simulate/grid.py +432 -0
  19. ebt_backtest-0.1.0/ebt/simulate/kf.py +1767 -0
  20. ebt_backtest-0.1.0/ebt/simulate/linear.py +230 -0
  21. ebt_backtest-0.1.0/ebt/simulate/model.py +96 -0
  22. ebt_backtest-0.1.0/ebt/simulate/nn/__init__.py +1 -0
  23. ebt_backtest-0.1.0/ebt/simulate/nn/loader.py +37 -0
  24. ebt_backtest-0.1.0/ebt/simulate/nn/model.py +402 -0
  25. ebt_backtest-0.1.0/ebt/simulate/nn/nn_loss.py +38 -0
  26. ebt_backtest-0.1.0/ebt/simulate/nn/util.py +82 -0
  27. ebt_backtest-0.1.0/ebt/simulate/optimize.py +577 -0
  28. ebt_backtest-0.1.0/ebt/simulate/plot.py +3338 -0
  29. ebt_backtest-0.1.0/ebt/simulate/postprocessing.py +175 -0
  30. ebt_backtest-0.1.0/ebt/simulate/predict.py +368 -0
  31. ebt_backtest-0.1.0/ebt/simulate/spread.py +330 -0
  32. ebt_backtest-0.1.0/ebt/simulate/stats.py +127 -0
  33. ebt_backtest-0.1.0/ebt/simulate/timeseries.py +90 -0
  34. ebt_backtest-0.1.0/ebt/simulate/tricks.py +239 -0
  35. ebt_backtest-0.1.0/ebt_backtest.egg-info/PKG-INFO +208 -0
  36. ebt_backtest-0.1.0/ebt_backtest.egg-info/SOURCES.txt +52 -0
  37. ebt_backtest-0.1.0/ebt_backtest.egg-info/dependency_links.txt +1 -0
  38. ebt_backtest-0.1.0/ebt_backtest.egg-info/requires.txt +29 -0
  39. ebt_backtest-0.1.0/ebt_backtest.egg-info/top_level.txt +1 -0
  40. ebt_backtest-0.1.0/ebt_rust/Cargo.lock +400 -0
  41. ebt_backtest-0.1.0/ebt_rust/Cargo.toml +27 -0
  42. ebt_backtest-0.1.0/ebt_rust/src/backtest.rs +2001 -0
  43. ebt_backtest-0.1.0/ebt_rust/src/combine_positions.rs +185 -0
  44. ebt_backtest-0.1.0/ebt_rust/src/execution.rs +303 -0
  45. ebt_backtest-0.1.0/ebt_rust/src/lib.rs +906 -0
  46. ebt_backtest-0.1.0/ebt_rust/src/outlier.rs +3060 -0
  47. ebt_backtest-0.1.0/ebt_rust/src/rows.rs +63 -0
  48. ebt_backtest-0.1.0/pyproject.toml +47 -0
  49. ebt_backtest-0.1.0/setup.cfg +4 -0
  50. ebt_backtest-0.1.0/tests/test_backtest.py +542 -0
  51. ebt_backtest-0.1.0/tests/test_futures.py +401 -0
  52. ebt_backtest-0.1.0/tests/test_kernels.py +371 -0
  53. ebt_backtest-0.1.0/tests/test_outlier.py +209 -0
  54. 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,4 @@
1
+ include LICENSE README.md
2
+ include ebt_rust/Cargo.toml ebt_rust/Cargo.lock
3
+ recursive-include ebt_rust/src *.rs
4
+ recursive-include tests *.py
@@ -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,2 @@
1
+ from ebt.data.types import *
2
+ from ebt.data.outlier 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 *