release: v0.9.0

This commit is contained in:
github-actions[bot]
2026-07-07 23:40:23 +00:00
parent bb86eaf84c
commit a13b3f5104
5 changed files with 88 additions and 31 deletions
+22 -22
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Jimmy7892/manifoldbt/master/assets/logo.png" width="110" alt="ManifoldBT logo">
<img src="https://raw.githubusercontent.com/manifoldbt/manifoldbt/master/assets/logo.png" width="110" alt="ManifoldBT logo">
</p>
<p align="center">
@@ -14,7 +14,7 @@
<p align="center">
<a href="https://www.manifoldbt.com">Website</a> &middot;
<a href="https://www.manifoldbt.com/docs/documentation.html">Documentation</a> &middot;
<a href="https://github.com/Jimmy7892/manifoldbt/tree/master/examples">Examples</a>
<a href="https://github.com/manifoldbt/manifoldbt/tree/master/examples">Examples</a>
</p>
---
@@ -109,25 +109,25 @@ manifoldbt ingest --provider binance --symbol BTCUSDT --symbol-id 1 --start ...
| # | Example | What it shows |
|---|---------|---------------|
| 00 | [Template](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/00_template.py) | Minimal starting point |
| 01 | [Trend Following](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/01_trend_following.py) | EMA crossover, volume filter, stop-loss |
| 02 | [Mean Reversion](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/02_mean_reversion.py) | EMA crossover with parameter sweep |
| 03 | [Multi-Asset Momentum](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/03_multi_asset_momentum.py) | Cross-asset signals |
| 04 | [Linear Regression](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/04_linear_regression.py) | Regression-based signal |
| 05 | [Statistical Arbitrage](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/05_stat_arb.py) | Pairs trading, spread z-score |
| 06 | [Full Visualization](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/06_full_visualization.py) | Tearsheet and charts |
| 07 | [Walk-Forward](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/07_walk_forward.py) | Out-of-sample validation |
| 08 | [2D Sweep](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/08_sweep_2d_heatmap.py) | Parameter grid heatmap |
| 09 | [3D Surface](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/09_surface_3d.py) | Parameter surface plot |
| 10 | [Monte Carlo](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/10_monte_carlo.py) | Permutation-based robustness |
| 11 | [Portfolio](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/11_portfolio.py) | Multi-strategy portfolio |
| 12 | [Diagnostics](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/12_diagnostics.py) | Lookahead & exposure safety checks |
| 13 | [Stochastic Simulation](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/13_stochastic_simulation.py) | SDE path simulation (GBM, Heston, …) |
| 14 | [Multi-Timeframe](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/14_multi_timeframe.py) | Combining signals across timeframes |
| 15 | [Cross-Exchange](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/15_cross_exchange.py) | Signal on one venue, execute on another |
| 16 | [Exogenous Data](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/16_hashrate_exogene.py) | External series (e.g. hashrate) as a signal |
| 17 | [Per-Venue Fees](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/17_per_venue_fees.py) | Per-venue funding & borrow costs |
| 18 | [CSV Import](https://github.com/Jimmy7892/manifoldbt/blob/master/examples/18_csv_import.py) | Load OHLCV from CSV (standard / MT4 / MT5) |
| 00 | [Template](https://github.com/manifoldbt/manifoldbt/blob/master/examples/00_template.py) | Minimal starting point |
| 01 | [Trend Following](https://github.com/manifoldbt/manifoldbt/blob/master/examples/01_trend_following.py) | EMA crossover, volume filter, stop-loss |
| 02 | [Mean Reversion](https://github.com/manifoldbt/manifoldbt/blob/master/examples/02_mean_reversion.py) | EMA crossover with parameter sweep |
| 03 | [Multi-Asset Momentum](https://github.com/manifoldbt/manifoldbt/blob/master/examples/03_multi_asset_momentum.py) | Cross-asset signals |
| 04 | [Linear Regression](https://github.com/manifoldbt/manifoldbt/blob/master/examples/04_linear_regression.py) | Regression-based signal |
| 05 | [Statistical Arbitrage](https://github.com/manifoldbt/manifoldbt/blob/master/examples/05_stat_arb.py) | Pairs trading, spread z-score |
| 06 | [Full Visualization](https://github.com/manifoldbt/manifoldbt/blob/master/examples/06_full_visualization.py) | Tearsheet and charts |
| 07 | [Walk-Forward](https://github.com/manifoldbt/manifoldbt/blob/master/examples/07_walk_forward.py) | Out-of-sample validation |
| 08 | [2D Sweep](https://github.com/manifoldbt/manifoldbt/blob/master/examples/08_sweep_2d_heatmap.py) | Parameter grid heatmap |
| 09 | [3D Surface](https://github.com/manifoldbt/manifoldbt/blob/master/examples/09_surface_3d.py) | Parameter surface plot |
| 10 | [Monte Carlo](https://github.com/manifoldbt/manifoldbt/blob/master/examples/10_monte_carlo.py) | Permutation-based robustness |
| 11 | [Portfolio](https://github.com/manifoldbt/manifoldbt/blob/master/examples/11_portfolio.py) | Multi-strategy portfolio |
| 12 | [Diagnostics](https://github.com/manifoldbt/manifoldbt/blob/master/examples/12_diagnostics.py) | Lookahead & exposure safety checks |
| 13 | [Stochastic Simulation](https://github.com/manifoldbt/manifoldbt/blob/master/examples/13_stochastic_simulation.py) | SDE path simulation (GBM, Heston, …) |
| 14 | [Multi-Timeframe](https://github.com/manifoldbt/manifoldbt/blob/master/examples/14_multi_timeframe.py) | Combining signals across timeframes |
| 15 | [Cross-Exchange](https://github.com/manifoldbt/manifoldbt/blob/master/examples/15_cross_exchange.py) | Signal on one venue, execute on another |
| 16 | [Exogenous Data](https://github.com/manifoldbt/manifoldbt/blob/master/examples/16_hashrate_exogene.py) | External series (e.g. hashrate) as a signal |
| 17 | [Per-Venue Fees](https://github.com/manifoldbt/manifoldbt/blob/master/examples/17_per_venue_fees.py) | Per-venue funding & borrow costs |
| 18 | [CSV Import](https://github.com/manifoldbt/manifoldbt/blob/master/examples/18_csv_import.py) | Load OHLCV from CSV (standard / MT4 / MT5) |
## Performance
@@ -164,4 +164,4 @@ Full API reference, indicator list, configuration guide, and best practices:
Apache 2.0 with Commons Clause. The source is available, free to use,
modify and self-host. Reselling the software or offering it as a paid
hosted service is not permitted. See [LICENSE](https://github.com/Jimmy7892/manifoldbt/blob/master/LICENSE) for the full text.
hosted service is not permitted. See [LICENSE](https://github.com/manifoldbt/manifoldbt/blob/master/LICENSE) for the full text.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "manifoldbt"
version = "0.8.7"
version = "0.9.0"
description = "Rust-powered backtesting engine for quantitative research"
requires-python = ">=3.9"
license = { file = "LICENSE" }
+43 -6
View File
@@ -251,6 +251,43 @@ def _resolve_source_dict(source, store):
return None
# _prepare_config() deepcopies the user's config (so it is never mutated) and
# re-resolves every name on each call. Both are pure functions of the config
# CONTENT, the strategy's order overrides and the store's metadata DB, so the
# prepared JSON is memoised on that content fingerprint — same pattern as
# _RESOLVE_CACHE (content keys, never object identity/heap address). The
# deepcopy alone is ~75us per call, the dominant slice of the per-call Python
# floor on small backtests.
_PREPARED_CFG_CACHE: Dict[Tuple[str, str, Any], str] = {}
_PREPARED_CFG_CACHE_MAX = 256
def _prepared_config_json(config: BacktestConfig, strategy, store: DataStore) -> str:
"""Content-memoised equivalent of ``_prepare_config(...).to_json()``."""
try:
meta_db = store.metadata_db()
except Exception:
meta_db = None
if meta_db is None:
return _prepare_config(config, strategy, store).to_json()
orders = getattr(strategy, "_orders", None) if strategy is not None else None
try:
orders_key = json.dumps(orders, sort_keys=True, default=str) if orders else ""
key = (config.to_json(), orders_key, meta_db)
except (TypeError, ValueError):
# Unserialisable config content — skip memoisation, never fail.
return _prepare_config(config, strategy, store).to_json()
cached = _PREPARED_CFG_CACHE.get(key)
if cached is None:
cached = _prepare_config(config, strategy, store).to_json()
if len(_PREPARED_CFG_CACHE) >= _PREPARED_CFG_CACHE_MAX:
_PREPARED_CFG_CACHE.clear()
_PREPARED_CFG_CACHE[key] = cached
return cached
def _prepare_config(config: BacktestConfig, strategy, store: DataStore) -> BacktestConfig:
"""Prepare config for execution: resolve symbols, convert deprecated fields."""
cfg = copy.deepcopy(config)
@@ -642,8 +679,8 @@ def run(
try:
config = _cap_output_resolution(config)
store = _resolve_store(config, store)
cfg = _prepare_config(config, strategy, store)
raw = _run_native(strategy.to_json(), cfg.to_json(), store)
cfg_json = _prepared_config_json(config, strategy, store)
raw = _run_native(strategy.to_json(), cfg_json, store)
return Result(raw)
except (ValueError, RuntimeError) as exc:
raise _classify_error(exc) from exc
@@ -674,7 +711,7 @@ def run_sweep(
try:
config = _cap_output_resolution(config)
store = _resolve_store(config, store)
cfg = _prepare_config(config, strategy, store)
cfg_json = _prepared_config_json(config, strategy, store)
grid_json = json.dumps({
name: [scalar_value_to_json(v) for v in values]
for name, values in param_grid.items()
@@ -682,7 +719,7 @@ def run_sweep(
raw_results = _run_sweep_native(
strategy.to_json(),
grid_json,
cfg.to_json(),
cfg_json,
store,
max_parallelism,
)
@@ -800,7 +837,7 @@ def run_sweep_lite(
try:
config = _cap_output_resolution(config)
store = _resolve_store(config, store)
cfg = _prepare_config(config, strategy, store)
cfg_json = _prepared_config_json(config, strategy, store)
grid_json = json.dumps({
name: [scalar_value_to_json(v) for v in values]
for name, values in param_grid.items()
@@ -808,7 +845,7 @@ def run_sweep_lite(
return _run_sweep_lite_native(
strategy.to_json(),
grid_json,
cfg.to_json(),
cfg_json,
store,
max_parallelism,
device,
+5
View File
@@ -246,6 +246,10 @@ class BacktestConfig:
rng_seed: Optional[int] = None
trading_days_per_year: float = 365.25
"""Annualisation factor: 365.25 for crypto/futures, 252 for equities."""
risk_free_rate: float = 0.0
"""Annual risk-free rate used in Sharpe/Sortino (excess return). Default 0.0
= raw Sharpe (consistent with raptorbt/vectorbt and most reporting). Set a
non-zero rate (e.g. 0.025) for an excess-return Sharpe."""
output_resolution: Any = None
"""Downsample output timeseries (equity, positions).
None = auto (uses resample_to if set, else bar_interval; min 1h).
@@ -313,6 +317,7 @@ class BacktestConfig:
"data_version": self.data_version,
"rng_seed": self.rng_seed,
"trading_days_per_year": self.trading_days_per_year,
"risk_free_rate": self.risk_free_rate,
}
if self.output_resolution is not None:
d["output_resolution"] = self.output_resolution
+17 -2
View File
@@ -63,6 +63,8 @@ class Strategy:
self._constraints = constraints or []
self._description = description
self._orders: Optional[Dict[str, Any]] = None
# Memoised to_json() (invalidated by every builder mutation below).
self._json_cache: Optional[str] = None
# ------------------------------------------------------------------
# Fluent builder API
@@ -81,11 +83,13 @@ class Strategy:
def signal(self, name: str, expr: Expr) -> "Strategy":
"""Add a named signal expression (returns self for chaining)."""
self.signals[name] = expr
self._json_cache = None
return self
def size(self, expr: Expr) -> "Strategy":
"""Set the position sizing expression (returns self for chaining)."""
self.position_sizing = expr
self._json_cache = None
return self
def param(
@@ -104,6 +108,7 @@ class Strategy:
description: Human-readable description.
"""
self._parameters[name] = _param(name, default=default, range=range, description=description)
self._json_cache = None
return self
def stop_loss(self, pct: float) -> "Strategy":
@@ -115,6 +120,7 @@ class Strategy:
if self._orders is None:
self._orders = {}
self._orders["stop_loss"] = {"stop_pct": pct}
self._json_cache = None
return self
def take_profit(self, pct: float) -> "Strategy":
@@ -126,6 +132,7 @@ class Strategy:
if self._orders is None:
self._orders = {}
self._orders["take_profit"] = {"profit_pct": pct}
self._json_cache = None
return self
def trailing_stop(self, pct: float, use_high: bool = True) -> "Strategy":
@@ -138,11 +145,13 @@ class Strategy:
if self._orders is None:
self._orders = {}
self._orders["trailing_stop"] = {"trail_pct": pct, "use_high": use_high}
self._json_cache = None
return self
def describe(self, text: str) -> "Strategy":
"""Set strategy description (returns self for chaining)."""
self._description = text
self._json_cache = None
return self
@property
@@ -199,5 +208,11 @@ class Strategy:
}
def to_json(self) -> str:
"""Serialize to a JSON string matching Rust ``StrategyDef``."""
return json.dumps(self.to_json_dict())
"""Serialize to a JSON string matching Rust ``StrategyDef``.
Memoised: builder mutations reset the cache, so repeated runs of the
same strategy skip the (O(expression tree)) re-serialisation.
"""
if self._json_cache is None:
self._json_cache = json.dumps(self.to_json_dict())
return self._json_cache