mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 14:38:04 +00:00
release: v0.9.0
This commit is contained in:
@@ -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> ·
|
||||
<a href="https://www.manifoldbt.com/docs/documentation.html">Documentation</a> ·
|
||||
<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
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user