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
+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