From a13b3f51042a4bd162ef363b94a8e98ae11ece18 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Jul 2026 23:40:23 +0000 Subject: [PATCH] release: v0.9.0 --- README.md | 44 +++++++++++++++---------------- pyproject.toml | 2 +- python/manifoldbt/__init__.py | 49 ++++++++++++++++++++++++++++++----- python/manifoldbt/config.py | 5 ++++ python/manifoldbt/strategy.py | 19 ++++++++++++-- 5 files changed, 88 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 42ee185..64df418 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- ManifoldBT logo + ManifoldBT logo

@@ -14,7 +14,7 @@

Website · Documentation · - Examples + Examples

--- @@ -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. diff --git a/pyproject.toml b/pyproject.toml index 33fcf66..746ec9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/python/manifoldbt/__init__.py b/python/manifoldbt/__init__.py index bc2f85f..5ae4384 100644 --- a/python/manifoldbt/__init__.py +++ b/python/manifoldbt/__init__.py @@ -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, diff --git a/python/manifoldbt/config.py b/python/manifoldbt/config.py index 9b7b3ca..11439b9 100644 --- a/python/manifoldbt/config.py +++ b/python/manifoldbt/config.py @@ -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 diff --git a/python/manifoldbt/strategy.py b/python/manifoldbt/strategy.py index 8ecc090..cda9d83 100644 --- a/python/manifoldbt/strategy.py +++ b/python/manifoldbt/strategy.py @@ -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