release: v0.8.6

This commit is contained in:
github-actions[bot]
2026-06-29 21:23:38 +00:00
parent 46bfb70666
commit e231751013
9 changed files with 239 additions and 158 deletions
+108
View File
@@ -0,0 +1,108 @@
"""Regression tests for the diagnostics config-preparation path.
Guards the fix for the bug where ``detect_lookahead`` / ``check_exposure_stability``
crashed with a dict ``universe`` (e.g. ``{"binance": ["BTC-USDT:perp"]}``):
they serialized the config without resolving the universe, so ``config.to_json()``
emitted a JSON *map* while the Rust loader expects a *sequence*
(``ValueError: invalid type: map, expected a sequence``).
The fix routes diagnostics through the same preparation as ``run()`` via
``_prepare_for_diagnostics``. These tests assert that helper resolves a dict
universe into a list of integer SymbolIds (so serialization is a JSON array),
without needing a Pro license or real market data.
"""
import json
import sqlite3
import manifoldbt as bt
from manifoldbt.diagnostics import _prepare_for_diagnostics
def _make_metadata_db(path):
"""Create a minimal metadata sqlite with one resolvable symbol (id=1)."""
conn = sqlite3.connect(path)
conn.execute(
"CREATE TABLE symbols ("
"id INTEGER PRIMARY KEY, base_currency TEXT, quote_currency TEXT, "
"asset_class TEXT, exchange TEXT, ticker TEXT)"
)
conn.execute(
"INSERT INTO symbols VALUES (1, 'BTC', 'USDT', 'CryptoPerpetual', "
"'BINANCE', 'BTC-USDT:perp')"
)
conn.commit()
conn.close()
return str(path)
class _StubStore:
"""Minimal DataStore stand-in.
``_resolve_normalized`` only needs ``metadata_db()`` (+ ``resolve_symbol``
as a fallback). ``dataset()`` raises so ``_resolve_store`` returns the store
unchanged instead of trying to swap datasets on disk.
"""
def __init__(self, db_path):
self._db = db_path
def metadata_db(self):
return self._db
def dataset(self):
raise NotImplementedError
def resolve_symbol(self, name): # fallback, not expected to be hit here
return 1
def _simple_strategy():
return (
bt.Strategy.create("regression")
.signal("s", bt.lit(1.0))
.size(bt.col("s"))
)
def test_prepare_for_diagnostics_resolves_dict_universe(tmp_path):
"""A dict universe must become a list of ints before serialization."""
db = _make_metadata_db(tmp_path / "metadata.sqlite")
store = _StubStore(db)
config = bt.BacktestConfig(
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=0,
time_range_end=4_000_000_000,
bar_interval={"Hours": 1},
initial_capital=1000.0,
)
prepared, _ = _prepare_for_diagnostics(config, _simple_strategy(), store)
# Core invariant: universe is a list of ints, never a dict.
assert isinstance(prepared.universe, list)
assert prepared.universe == [1]
# And the JSON the Rust loader sees is an array, not a map (the crash cause).
universe_json = json.loads(prepared.to_json())["universe"]
assert isinstance(universe_json, list)
assert universe_json == [1]
def test_prepare_for_diagnostics_passes_through_list_universe(tmp_path):
"""An already-resolved list universe is left intact."""
db = _make_metadata_db(tmp_path / "metadata.sqlite")
store = _StubStore(db)
config = bt.BacktestConfig(
universe=[1],
time_range_start=0,
time_range_end=4_000_000_000,
bar_interval={"Hours": 1},
initial_capital=1000.0,
)
prepared, _ = _prepare_for_diagnostics(config, _simple_strategy(), store)
assert prepared.universe == [1]
assert json.loads(prepared.to_json())["universe"] == [1]
+60
View File
@@ -0,0 +1,60 @@
"""Doc <-> code signature contract.
These assertions encode the public signatures and helper outputs that the
online documentation and the interactive notebook rely on. They are cheap,
IO-free, and Pro-free, and exist to catch *doc drift*: if a documented kwarg,
preset, or helper shape changes in the code, a doc snippet silently breaks.
This guards, among others:
* ``plot.monte_carlo`` exposing ``n_simulations`` (NOT ``n_paths``) -- the
notebook bug where ``n_paths=`` raised TypeError.
* ``Slippage.volume_impact`` emitting ``impact_coeff``/``exponent`` -- the
notebook bug where ``{"coefficient": ...}`` failed Rust deserialization.
* ``DataStore`` accepting ``mega``/``arrow_dir`` -- the doc signature that
omitted them.
"""
import inspect
import manifoldbt as bt
def test_monte_carlo_uses_n_simulations_not_n_paths():
params = inspect.signature(bt.plot.monte_carlo).parameters
assert "n_simulations" in params
assert "n_paths" not in params # the notebook snippet bug
def test_slippage_helper_shapes_match_serde():
# Keys must match the Rust SlippageConfig serde variants exactly.
assert bt.Slippage.volume_impact(0.1) == {
"VolumeImpact": {"impact_coeff": 0.1, "exponent": 1.5}
}
assert bt.Slippage.fixed_bps(2.0) == {"FixedBps": {"bps": 2.0}}
def test_interval_helper_shapes():
assert bt.Interval.seconds(1) == {"Seconds": 1}
assert bt.Interval.minutes(1) == {"Minutes": 1}
assert bt.Interval.hours(12) == {"Hours": 12}
assert bt.Interval.days(1) == {"Days": 1}
def test_fee_presets_match_documented_values():
# Documented under #configuration > FeeConfig Presets.
perps = bt.FeeConfig.binance_perps()
assert (perps.maker_fee_bps, perps.taker_fee_bps) == (2.0, 5.0)
spot = bt.FeeConfig.binance_spot()
assert (spot.maker_fee_bps, spot.taker_fee_bps) == (10.0, 10.0)
def test_datastore_accepts_mega_and_arrow_dir_kwargs(tmp_path):
# The real signature is (data_root, metadata_db, dataset, mega, arrow_dir).
# We only assert the kwargs are *accepted* (no TypeError for unknown kwarg);
# any runtime/IO error from opening an empty dir is fine for this contract.
for kw in ("mega", "arrow_dir"):
try:
bt.DataStore(str(tmp_path), dataset="bars_1m", **{kw: str(tmp_path)})
except TypeError as exc: # unexpected keyword argument -> contract broken
raise AssertionError(f"DataStore rejected kwarg {kw!r}: {exc}")
except Exception:
pass # non-TypeError (e.g. cannot open store) -> kwarg was accepted