mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 14:38:04 +00:00
release: v0.13.0
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "manifoldbt"
|
name = "manifoldbt"
|
||||||
version = "0.12.3"
|
version = "0.13.0"
|
||||||
description = "Rust-powered backtesting engine for quantitative research"
|
description = "Rust-powered backtesting engine for quantitative research"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""manifoldbt: Fast research backtesting with Rust core + Python DSL."""
|
"""manifoldbt: Fast research backtesting with Rust core + Python DSL."""
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import importlib as _importlib
|
import importlib as _importlib
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ from manifoldbt._native import (
|
|||||||
run_json,
|
run_json,
|
||||||
run_sweep as _run_sweep_native,
|
run_sweep as _run_sweep_native,
|
||||||
run_sweep_lite as _run_sweep_lite_native,
|
run_sweep_lite as _run_sweep_lite_native,
|
||||||
|
sweep_columns as _sweep_columns_native,
|
||||||
run_with_parquet,
|
run_with_parquet,
|
||||||
py_run_walk_forward as _run_walk_forward_native,
|
py_run_walk_forward as _run_walk_forward_native,
|
||||||
py_run_sweep_2d as _run_sweep_2d_native,
|
py_run_sweep_2d as _run_sweep_2d_native,
|
||||||
@@ -869,7 +870,8 @@ def run_sweep_lite(
|
|||||||
store: DataStore,
|
store: DataStore,
|
||||||
*,
|
*,
|
||||||
max_parallelism: int = 0,
|
max_parallelism: int = 0,
|
||||||
device: str = "cpu",
|
device: str = "auto",
|
||||||
|
precision: str = "fp64",
|
||||||
) -> List["BatchResultLite"]:
|
) -> List["BatchResultLite"]:
|
||||||
"""Run a parameter sweep returning only metrics (no Arrow output).
|
"""Run a parameter sweep returning only metrics (no Arrow output).
|
||||||
|
|
||||||
@@ -882,14 +884,47 @@ def run_sweep_lite(
|
|||||||
config: Backtest configuration.
|
config: Backtest configuration.
|
||||||
store: Data store.
|
store: Data store.
|
||||||
max_parallelism: Maximum threads. 0 = all available cores.
|
max_parallelism: Maximum threads. 0 = all available cores.
|
||||||
device: ``"cpu"`` (default) or ``"cuda"``/``"gpu"``. The GPU path
|
device: ``"auto"`` (default), ``"cpu"``, or ``"cuda"``/``"gpu"``.
|
||||||
accelerates single-asset, AtClose + FixedBps sweeps and produces
|
The GPU path produces results numerically identical to the CPU
|
||||||
results numerically identical to the CPU path. **Pro-only**: a
|
path. ``"auto"`` picks per sweep: small grids run on the CPU (the
|
||||||
Community license raises ``PermissionError`` for ``device="cuda"``
|
GPU has a ~50 ms fixed launch floor, so the CPU wins below ~1,000
|
||||||
(Community keeps the full-speed CPU sweep with no restriction).
|
combos -- override with ``MBT_GPU_AUTO_MIN_COMBOS``), large grids
|
||||||
Requires a build with ``--features cuda`` and a CUDA device; for any
|
run on the GPU when the build, a device, and a Pro license are
|
||||||
unsupported strategy/config (or when no GPU is present at runtime) it
|
available, and the CPU otherwise. This is the default because it is
|
||||||
silently falls back to the CPU sweep, so results are never affected.
|
never slower than the better of the two by more than the launch
|
||||||
|
floor and its results match the CPU bit-for-bit, so it is safe to
|
||||||
|
leave on: with no GPU, no Pro license, or a Community build it is
|
||||||
|
simply the CPU sweep. **Pro-only**: a Community license raises
|
||||||
|
``PermissionError`` for ``device="cuda"`` (``"auto"`` simply stays
|
||||||
|
on the CPU; Community keeps the full-speed CPU sweep with no
|
||||||
|
restriction). ``"cuda"`` requires a build with ``--features cuda``
|
||||||
|
and a CUDA device; for any unsupported strategy/config (or when no
|
||||||
|
GPU is present at runtime) it falls back to the CPU sweep with a
|
||||||
|
``UserWarning`` naming the reason, so results are never affected.
|
||||||
|
An unknown device string raises ``ValueError`` instead of silently
|
||||||
|
running on the CPU.
|
||||||
|
precision: ``"fp64"`` (default) runs the GPU sweep in double precision,
|
||||||
|
bit-identical to the CPU path. ``"fp32"`` runs the single-asset GPU
|
||||||
|
kernel in single precision at the cost of approximate results: a
|
||||||
|
signal within ~1e-7 relative of a decision threshold can flip vs f64,
|
||||||
|
so occasional combos diverge. Intended as a **scan-only** accelerator
|
||||||
|
(rank in fp32, re-run the winner in fp64 for an exact P&L). Note the
|
||||||
|
speedup is modest (~1.1x measured on an RTX 3090): the per-bar
|
||||||
|
capital/position recurrence is latency-bound, so fp32's throughput
|
||||||
|
advantage barely applies. ``"fp32"`` requires ``device="cuda"``.
|
||||||
|
|
||||||
|
Metric resolution:
|
||||||
|
The lite path computes risk metrics from one equity point per UTC day
|
||||||
|
(this is what makes it fast), whereas :func:`run` uses the full-resolution
|
||||||
|
curve. ``final_equity``, ``total_return``, ``sharpe``, ``sortino``,
|
||||||
|
``volatility`` and ``max_drawdown`` are unaffected -- they match ``run``
|
||||||
|
exactly. Three annualisation-sensitive metrics differ slightly because
|
||||||
|
they are derived from the daily series: ``cagr`` (it starts from the
|
||||||
|
first daily equity rather than initial capital), ``calmar`` and
|
||||||
|
``ulcer_index``. The gap is small (< ~0.4% relative on a multi-year daily
|
||||||
|
backtest) and is the same for every sweep regardless of orders. Sort and
|
||||||
|
rank on it freely; for an exact single-figure P&L, re-run the winning
|
||||||
|
combo through :func:`run`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
One :class:`BatchResultLite` per combo (Cartesian product order).
|
One :class:`BatchResultLite` per combo (Cartesian product order).
|
||||||
@@ -911,11 +946,58 @@ def run_sweep_lite(
|
|||||||
store,
|
store,
|
||||||
max_parallelism,
|
max_parallelism,
|
||||||
device,
|
device,
|
||||||
|
precision,
|
||||||
)
|
)
|
||||||
except (ValueError, RuntimeError) as exc:
|
except (ValueError, RuntimeError) as exc:
|
||||||
raise _classify_error(exc) from exc
|
raise _classify_error(exc) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def sweep_columns(
|
||||||
|
batch: List["BatchResultLite"],
|
||||||
|
names: Union[str, List[str]],
|
||||||
|
) -> Union["Any", Dict[str, "Any"]]:
|
||||||
|
"""Extract whole metric columns from a sweep as numpy arrays.
|
||||||
|
|
||||||
|
``result.metrics`` builds a 21-key dict per combo, so reading one metric off
|
||||||
|
a large sweep creates millions of throwaway floats. This walks the results
|
||||||
|
once and copies each requested column straight into a numpy array, which is
|
||||||
|
~20x faster: on a 1M-combo sweep, ~1.1s of extraction becomes ~0.05s.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
batch: The list returned by :func:`run_sweep_lite`.
|
||||||
|
names: One column name, or a list of them. Available: ``final_equity``,
|
||||||
|
``trade_count``, and every :class:`PerformanceMetrics` field
|
||||||
|
(``sharpe``, ``sortino``, ``calmar``, ``max_drawdown``, ``alpha``,
|
||||||
|
``beta``, ``tstat_alpha``, ``total_return``, ``cagr``,
|
||||||
|
``volatility``, ``skewness``, ``kurtosis``, ``tail_ratio``,
|
||||||
|
``omega_ratio``, ``ulcer_index``, ``best_day``, ``worst_day``,
|
||||||
|
``avg_daily_return``, ``pct_positive_days``,
|
||||||
|
``max_drawdown_duration_days``, ``tstat_sharpe``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A single ``np.ndarray`` if ``names`` is a string, else a dict mapping
|
||||||
|
each name to its array. Arrays are float64 and in combo order (the same
|
||||||
|
order as ``batch``), so ``np.argmax``/``argsort`` indices map straight
|
||||||
|
back onto it. ``trade_count`` comes back as float64 like the rest.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The arrays are read-only views over the returned buffers (no copy). Call
|
||||||
|
``.copy()`` if you need to mutate one.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> batch = mbt.run_sweep_lite(strategy, grid, config, store, device="cuda")
|
||||||
|
>>> sharpe = mbt.sweep_columns(batch, "sharpe")
|
||||||
|
>>> best = batch[int(sharpe.argmax())]
|
||||||
|
"""
|
||||||
|
import numpy as _np
|
||||||
|
|
||||||
|
single = isinstance(names, str)
|
||||||
|
wanted = [names] if single else list(names)
|
||||||
|
raw = _sweep_columns_native(batch, wanted)
|
||||||
|
out = {n: _np.frombuffer(raw[n], dtype=_np.float64) for n in wanted}
|
||||||
|
return out[names] if single else out
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Research API
|
# Research API
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -31,13 +31,23 @@ def _collect_params(expr: "Expr", out: Dict[str, Any]) -> None:
|
|||||||
if name not in out:
|
if name not in out:
|
||||||
out[name] = expr._param_meta
|
out[name] = expr._param_meta
|
||||||
for arg in expr._args:
|
for arg in expr._args:
|
||||||
if isinstance(arg, Expr):
|
_collect_params_arg(arg, out)
|
||||||
_collect_params(arg, out)
|
|
||||||
elif isinstance(arg, str):
|
|
||||||
# DynPeriod/DynFloat param name — check global registry
|
def _collect_params_arg(arg: Any, out: Dict[str, Any]) -> None:
|
||||||
from manifoldbt.expr import _param_registry
|
if isinstance(arg, Expr):
|
||||||
if arg in _param_registry and arg not in out:
|
_collect_params(arg, out)
|
||||||
out[arg] = _param_registry[arg]
|
elif isinstance(arg, (list, tuple)):
|
||||||
|
# Scan nodes carry their init/update expressions as LISTS of Exprs;
|
||||||
|
# skipping them silently dropped every param() used inside a scan
|
||||||
|
# ("strategy uses undefined parameters: q" on a swept Kalman).
|
||||||
|
for item in arg:
|
||||||
|
_collect_params_arg(item, out)
|
||||||
|
elif isinstance(arg, str):
|
||||||
|
# DynPeriod/DynFloat param name — check global registry
|
||||||
|
from manifoldbt.expr import _param_registry
|
||||||
|
if arg in _param_registry and arg not in out:
|
||||||
|
out[arg] = _param_registry[arg]
|
||||||
|
|
||||||
|
|
||||||
class Strategy:
|
class Strategy:
|
||||||
|
|||||||
Reference in New Issue
Block a user