mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-11-02 07:56:11 +00:00
release: v0.29.0
This commit is contained in:
@@ -40,8 +40,7 @@ included. Python stays out of the hot loop.
|
||||
vectorbt in CI, on the wheel published to PyPI. A benchmark workload the
|
||||
engines disagree on gets no timing at all.
|
||||
- **Explicit.** Every fill rule is [written down](#execution-semantics),
|
||||
pessimistic wherever a choice existed, and so is
|
||||
[what the engine does not model](#what-is-not-covered).
|
||||
pessimistic wherever a choice existed.
|
||||
|
||||
```bash
|
||||
pip install manifoldbt
|
||||
@@ -273,32 +272,17 @@ was available, the pessimistic one was taken.
|
||||
| Trailing stops | Never trigger on the bar they ratchet on, which would require reading the intra-bar path. |
|
||||
| An execution-price expression evaluates to NaN | Falls back to the close and warns, rather than dropping the trade silently. |
|
||||
| A short option's maintenance margin exceeds equity | The position is force-closed on that bar. |
|
||||
| A leveraged position's equity reaches its maintenance margin (`cfg.liquidation`) | The book is liquidated inside the bar, at the liquidation price; a stop placed before that price fires first. |
|
||||
| Fees, funding and borrow rates | Resolved per symbol, so venues inside one portfolio keep their own rates. |
|
||||
|
||||
### What is not covered
|
||||
**Prices and margin.** Prices are used as the data source supplies them: Yahoo
|
||||
defaults to dividend-adjusted (`dataset="raw"` opts out) and splits follow the
|
||||
provider. Margin force-close applies to short options; leveraged spot and
|
||||
perpetual positions are held as sized, so leverage limits belong in the
|
||||
strategy.
|
||||
|
||||
The edges, stated rather than left to be discovered:
|
||||
|
||||
- **No general liquidation model.** Margin force-close exists for short options
|
||||
only. A leveraged spot or perpetual position is not liquidated by the engine.
|
||||
- **No corporate actions.** Yahoo prices arrive dividend-adjusted from the
|
||||
source (`dataset="raw"` opts out) and splits are whatever the provider
|
||||
returns. Nothing in the engine reconstructs either.
|
||||
- The cross-engine scenarios run on short synthetic series, each built to
|
||||
isolate one behaviour. They are not a long backtest over market data compared
|
||||
trade for trade.
|
||||
- Perpetual funding is exercised by the engine's own tests, but has no
|
||||
cross-engine parity test.
|
||||
- The largest universe under test is a handful of instruments. Cross-sectional
|
||||
research across thousands of assets is exercised by the sweep benchmarks, not
|
||||
by the correctness suite.
|
||||
- Look-ahead coverage is a set of regression tests and one worked example, not
|
||||
a systematic battery of leak archetypes.
|
||||
|
||||
The engine's own Rust suite is not published, which is why the rules above are
|
||||
written out rather than linked. Independent verification is welcome, and a
|
||||
reproduction showing a fill this engine gets wrong is the most useful report
|
||||
this project can receive.
|
||||
Independent verification is welcome, and a reproduction showing a fill this
|
||||
engine gets wrong is the most useful report this project can receive.
|
||||
|
||||
## Look-ahead
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
[project]
|
||||
name = "manifoldbt"
|
||||
version = "0.28.1"
|
||||
version = "0.29.0"
|
||||
description = "Rust-powered backtesting engine for quantitative research"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -80,6 +80,7 @@ from manifoldbt.config import (
|
||||
BacktestConfig,
|
||||
ExecutionConfig,
|
||||
FeeConfig,
|
||||
Liquidation,
|
||||
OrderConfig,
|
||||
VenueFees,
|
||||
account_sessions,
|
||||
@@ -128,10 +129,15 @@ __version__ = _update.installed_version() or "0.1.0"
|
||||
# ---------------------------------------------------------------------------
|
||||
# License banner
|
||||
# ---------------------------------------------------------------------------
|
||||
# Every tier that includes Pro. Researcher carries all of Pro, so a Pro gate
|
||||
# opens on either. Defined before the banner, which reads it at import.
|
||||
_PAID_TIERS = ("Pro", "Researcher")
|
||||
|
||||
|
||||
def _print_banner():
|
||||
try:
|
||||
tier, email = _license_info()
|
||||
if tier == "Pro" and email:
|
||||
if tier in _PAID_TIERS:
|
||||
# A trial shows its end date up front. Without this the banner is
|
||||
# identical to a paid licence, and the expiry is discovered the day
|
||||
# everything stops working, mid-session.
|
||||
@@ -142,7 +148,8 @@ def _print_banner():
|
||||
trial = f" (trial ends {expiry[:10]})"
|
||||
except Exception:
|
||||
pass
|
||||
print(f"manifoldbt v{__version__} | \033[38;5;214mPro\033[0m{trial} | {email}")
|
||||
who = f" | {email}" if email else ""
|
||||
print(f"manifoldbt v{__version__} | \033[38;5;214m{tier}\033[0m{trial}{who}")
|
||||
else:
|
||||
print(f"manifoldbt v{__version__} | \033[36mCommunity\033[0m | upgrade: www.manifoldbt.com")
|
||||
except Exception:
|
||||
@@ -206,15 +213,19 @@ def check_for_update() -> Optional[str]:
|
||||
|
||||
|
||||
def license_info() -> tuple:
|
||||
"""Get license info: (tier, email). tier is "Pro" or "Community", email is str or None."""
|
||||
"""Get license info: ``(tier, email)``.
|
||||
|
||||
``tier`` is ``"Researcher"``, ``"Pro"`` or ``"Community"``; ``email`` is a
|
||||
str, or ``None`` on Community.
|
||||
"""
|
||||
return _license_info()
|
||||
|
||||
|
||||
def _is_pro() -> bool:
|
||||
"""Check if current license is Pro."""
|
||||
"""Check if the current license includes Pro (Pro or Researcher)."""
|
||||
try:
|
||||
tier, _ = _license_info()
|
||||
return tier == "Pro"
|
||||
return tier in _PAID_TIERS
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -242,10 +253,9 @@ def _require_grant_for_gpu(device, feature: str) -> None:
|
||||
``LicenseError``, instead of each surfacing its own error type from deeper in
|
||||
the run. No-op for CPU, and no-op for a licence that carries it.
|
||||
|
||||
GPU acceleration is a Researcher feature. Note that
|
||||
``license_info()`` reports ``"Pro"`` for a Researcher licence, because
|
||||
Researcher includes everything Pro has: the tier string is not the way to
|
||||
tell whether a given feature is covered.
|
||||
GPU acceleration is a Researcher feature. The grant is asked directly
|
||||
rather than read from the tier: a Pro licence may carry the GPU grant on
|
||||
its own.
|
||||
"""
|
||||
if isinstance(device, str) and device.lower() in ("cuda", "gpu"):
|
||||
if _grant_couvre("gpu_sweep"):
|
||||
@@ -669,6 +679,19 @@ def _prepare_config(config: BacktestConfig, strategy, store: DataStore) -> Backt
|
||||
resolved_sv[int(store.resolve_symbol(key))] = venue
|
||||
fees.symbol_venue = resolved_sv
|
||||
|
||||
# --- Liquidation overrides may be keyed by symbol name, like symbol_venue ---
|
||||
liq = getattr(cfg, "liquidation", None)
|
||||
if liq is not None and getattr(liq, "per_symbol", None):
|
||||
resolved_liq = {}
|
||||
for key, rate in liq.per_symbol.items():
|
||||
if isinstance(key, int):
|
||||
resolved_liq[key] = rate
|
||||
elif cfg.symbol_names and key in cfg.symbol_names:
|
||||
resolved_liq[int(cfg.symbol_names[key])] = rate
|
||||
else:
|
||||
resolved_liq[int(store.resolve_symbol(key))] = rate
|
||||
liq.per_symbol = resolved_liq
|
||||
|
||||
# Per-strategy SL/TP/trailing orders are NOT merged into the config anymore:
|
||||
# they travel inside the strategy JSON (Strategy.to_json -> StrategyDef.orders)
|
||||
# so the engine applies them per-strategy. This lets one batch/sweep call run
|
||||
@@ -2579,15 +2602,15 @@ def run_sweep_lite(
|
||||
path. ``"auto"`` picks per sweep: small grids run on the CPU (the
|
||||
GPU has a ~50 ms fixed launch floor, so the CPU wins below ~1,000
|
||||
combos -- override with ``MBT_GPU_AUTO_MIN_COMBOS``), large grids
|
||||
run on the GPU when the build, a device, and a Pro license are
|
||||
available, and the CPU otherwise. This is the default because it is
|
||||
never slower than the better of the two by more than the launch
|
||||
run on the GPU when the build, a device, and a Researcher licence
|
||||
are available, and the CPU otherwise. This is the default because it
|
||||
is 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``
|
||||
leave on: with no GPU or a CPU-only build it is simply the CPU
|
||||
sweep. **Researcher**: without it, ``device="cuda"`` raises
|
||||
``LicenseError``, and ``"auto"`` runs a large grid on the CPU with a
|
||||
``UserWarning`` saying so when a GPU is present; the CPU sweep
|
||||
itself is full speed on every tier. ``"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.
|
||||
@@ -3380,6 +3403,7 @@ __all__ = [
|
||||
"calibrate_gpu",
|
||||
"AccountPhase",
|
||||
"AccountRules",
|
||||
"Liquidation",
|
||||
"account_sessions",
|
||||
# Core types
|
||||
"BacktestResult",
|
||||
|
||||
@@ -28,6 +28,7 @@ EXIT_REASON_LABELS: Dict[int, str] = {
|
||||
4: "limit expiry",
|
||||
5: "option expiry",
|
||||
6: "margin call",
|
||||
7: "liquidation",
|
||||
}
|
||||
|
||||
# ``exit_reason`` of a round trip still open at the end of the run.
|
||||
|
||||
@@ -87,14 +87,14 @@ def _cmd_activate(args: argparse.Namespace) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
tier, email = bt.license_info()
|
||||
if tier == "Pro":
|
||||
if tier in ("Pro", "Researcher"):
|
||||
print(f"Activated: {tier}" + (f" ({email})" if email else ""))
|
||||
# A trial activation is the one moment the user is certainly reading:
|
||||
# say when it ends here, not only in the import banner.
|
||||
try:
|
||||
expiry = bt._license_expiry()
|
||||
if expiry:
|
||||
print(f"Trial license: Pro features stop on {expiry[:10]}.")
|
||||
print(f"Trial license: {tier} features stop on {expiry[:10]}.")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -1073,6 +1073,66 @@ class AccountRules:
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Liquidation:
|
||||
"""How the venue liquidates a leveraged position, enforced DURING the run.
|
||||
|
||||
The account is cross-margined: one balance carries every position, and
|
||||
the book is closed the moment equity falls to the maintenance margin,
|
||||
``rate * |position| * price`` summed over the symbols held. Without it a
|
||||
3x long survives every wick that would have closed it on the venue, and
|
||||
the curve reads a recovery the account never traded.
|
||||
|
||||
import manifoldbt as mbt
|
||||
cfg.execution.max_position_pct = 3.0 # 3x
|
||||
cfg.liquidation = mbt.Liquidation(
|
||||
maintenance_margin_rate=0.005, # 0.5 % of notional
|
||||
fee_bps=50, # liquidation fee
|
||||
)
|
||||
|
||||
Inside a bar the rule walks every position from its open toward its
|
||||
adverse extreme (the low for a long, the high for a short) and closes the
|
||||
book where equity meets the margin: on one symbol, at the venue's
|
||||
liquidation price exactly; on several, as if their adverse extremes
|
||||
coincided. A bar that opens past that point fills at the open. A stop
|
||||
placed before the liquidation price fires first; one placed beyond it
|
||||
never does. A gap through the bankruptcy price leaves the account at zero
|
||||
rather than in debt, as a venue's insurance fund does.
|
||||
|
||||
Liquidations land in the trade log with ``exit_reason == 7``
|
||||
("liquidation") and are counted in ``result.warnings``. The full run and
|
||||
the lite sweep apply the same rule; the fast kernels and the GPU sweep
|
||||
refuse the configuration by name, as does ``fill_resolution="ticks"``.
|
||||
Option positions keep their own margin call (``option_margin_model``).
|
||||
"""
|
||||
|
||||
maintenance_margin_rate: float = 0.005
|
||||
"""Maintenance margin as a fraction of notional, in ``[0, 1)``."""
|
||||
per_symbol: Dict[Any, float] = field(default_factory=dict)
|
||||
"""Per-symbol overrides of ``maintenance_margin_rate``, keyed by symbol
|
||||
name or integer id: ``{"BTCUSDT": 0.004, "DOGEUSDT": 0.01}``."""
|
||||
fee_bps: float = 0.0
|
||||
"""Liquidation fee in basis points of the notional closed. It replaces the
|
||||
trading fee on a liquidation, as venues charge it."""
|
||||
|
||||
def to_json_dict(self) -> dict:
|
||||
d: dict = {"maintenance_margin_rate": float(self.maintenance_margin_rate)}
|
||||
if self.per_symbol:
|
||||
for key in self.per_symbol:
|
||||
if not isinstance(key, int):
|
||||
raise ValueError(
|
||||
f"Liquidation.per_symbol key {key!r} is not a symbol id: "
|
||||
"names are resolved by mbt.run against the store; pass "
|
||||
"integer ids when serialising the config yourself."
|
||||
)
|
||||
d["per_symbol"] = {
|
||||
str(sid): float(r) for sid, r in self.per_symbol.items()
|
||||
}
|
||||
if self.fee_bps:
|
||||
d["fee_bps"] = float(self.fee_bps)
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class BacktestConfig:
|
||||
universe: List[int] = field(default_factory=lambda: [1])
|
||||
@@ -1161,6 +1221,9 @@ class BacktestConfig:
|
||||
sells an option. ``"deribit"`` applies the venue's published per-contract
|
||||
formula, refuses a short that does not fit initial margin, and force-closes
|
||||
the book when maintenance margin passes equity."""
|
||||
liquidation: Any = None
|
||||
"""Liquidation of leveraged positions (see :class:`Liquidation`).
|
||||
``None`` == nothing is ever liquidated, the default."""
|
||||
account_rules: Any = None
|
||||
"""Prop-firm limits enforced *during* the run (see :class:`AccountRules`).
|
||||
``None`` == no limits.
|
||||
@@ -1266,6 +1329,11 @@ class BacktestConfig:
|
||||
d["account_rules"] = (
|
||||
rules.to_json_dict() if hasattr(rules, "to_json_dict") else rules
|
||||
)
|
||||
if self.liquidation is not None:
|
||||
liq = self.liquidation
|
||||
d["liquidation"] = (
|
||||
liq.to_json_dict() if hasattr(liq, "to_json_dict") else liq
|
||||
)
|
||||
if self.book_levels is not None:
|
||||
d["book_levels"] = int(self.book_levels)
|
||||
# Deprecated fields (backward compat)
|
||||
|
||||
@@ -13,7 +13,7 @@ import manifoldbt as bt
|
||||
|
||||
# Safety diagnostics are a Pro feature.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: skipped on a Community wheel",
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""license_info() names the tier actually granted.
|
||||
|
||||
A Researcher licence used to report "Pro", so the one call a user makes to
|
||||
check their tier could not tell the two apart. These hold on any machine,
|
||||
whatever licence it carries: the tier is read against the gates themselves.
|
||||
"""
|
||||
import manifoldbt as bt
|
||||
from manifoldbt import _grant_couvre
|
||||
|
||||
|
||||
def test_the_tier_is_one_of_the_three():
|
||||
tier, email = bt.license_info()
|
||||
assert tier in ("Community", "Pro", "Researcher")
|
||||
if tier == "Community":
|
||||
assert email is None
|
||||
|
||||
|
||||
def test_an_open_tick_layer_reports_researcher():
|
||||
# The tick layer opens on the Researcher grant alone: no Pro licence and
|
||||
# no a-la-carte grant carries it. When it is open, the tier has to say so.
|
||||
if _grant_couvre("ticks"):
|
||||
assert bt.license_info()[0] == "Researcher"
|
||||
else:
|
||||
assert bt.license_info()[0] in ("Community", "Pro")
|
||||
|
||||
|
||||
def test_the_banner_names_the_tier():
|
||||
# The banner swallows every exception and falls back to "Community": a
|
||||
# name it cannot resolve at import used to print Community on a paid
|
||||
# licence. It runs once, at import, so it is read from a fresh process.
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
out = subprocess.run(
|
||||
[sys.executable, "-c", "import manifoldbt"],
|
||||
capture_output=True, text=True, check=True,
|
||||
).stdout
|
||||
assert bt.license_info()[0] in out, out
|
||||
|
||||
|
||||
def test_pro_gates_open_for_every_paid_tier():
|
||||
# Researcher carries all of Pro: a Pro gate that read the tier string
|
||||
# would have shut on the higher tier.
|
||||
assert bt._is_pro() == (bt.license_info()[0] in ("Pro", "Researcher"))
|
||||
@@ -0,0 +1,97 @@
|
||||
"""cfg.liquidation from Python: the config reaches the engine, symbol names
|
||||
resolve, and a leveraged position is closed where the venue would close it."""
|
||||
import pytest
|
||||
|
||||
import manifoldbt as bt
|
||||
from manifoldbt.expr import lit
|
||||
from manifoldbt.helpers import Interval, Slippage
|
||||
|
||||
pd = pytest.importorskip("pandas")
|
||||
|
||||
LIQUIDATION = 7
|
||||
|
||||
|
||||
def _frame():
|
||||
# Flat at 100, then one bar wicks to 60 and closes at 95.
|
||||
n = 48
|
||||
ts = pd.date_range("2022-01-01", periods=n, freq="1h", tz="UTC")
|
||||
o = [100.0] * n
|
||||
h = [100.0] * n
|
||||
lo = [100.0] * n
|
||||
c = [100.0] * n
|
||||
o[30], h[30], lo[30], c[30] = 98.0, 99.0, 60.0, 95.0
|
||||
for i in range(31, n):
|
||||
o[i] = h[i] = lo[i] = c[i] = 95.0
|
||||
return pd.DataFrame({"timestamp": ts, "open": o, "high": h, "low": lo,
|
||||
"close": c, "volume": [1e9] * n})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path):
|
||||
return bt.import_dataframe(_frame(), symbol="AAA", symbol_id=1, interval="1h",
|
||||
data_root=str(tmp_path / "data"),
|
||||
metadata_db=str(tmp_path / "meta.sqlite"))
|
||||
|
||||
|
||||
def _cfg(liquidation=None):
|
||||
cfg = bt.BacktestConfig(
|
||||
universe=["AAA"], time_range_start=0, time_range_end=4_102_444_800_000_000_000,
|
||||
bar_interval=Interval.hours(1), initial_capital=1000.0,
|
||||
execution=bt.ExecutionConfig(execution_price="AtClose", max_position_pct=3.0),
|
||||
slippage=Slippage.fixed_bps(0.0),
|
||||
)
|
||||
cfg.liquidation = liquidation
|
||||
return cfg
|
||||
|
||||
|
||||
def _hold_3x():
|
||||
return bt.Strategy.create("hold3x").signal("w", lit(3.0)).size(lit(3.0))
|
||||
|
||||
|
||||
def _reasons(result):
|
||||
return list(result.trades_df(backend="pandas")["exit_reason"])
|
||||
|
||||
|
||||
def test_a_3x_long_is_liquidated_on_the_wick(store):
|
||||
result = bt.run(_hold_3x(), _cfg(bt.Liquidation(maintenance_margin_rate=0.005)), store)
|
||||
trades = result.trades_df(backend="pandas")
|
||||
liq = trades[trades["exit_reason"] == LIQUIDATION]
|
||||
assert len(liq) == 1
|
||||
entry = trades.iloc[0]
|
||||
# The engine's liquidation price for the position it actually opened.
|
||||
cash = 1000.0 - entry["quantity"] * entry["fill_price"] - entry["fees"]
|
||||
expected = -cash / (entry["quantity"] * (1.0 - 0.005))
|
||||
assert liq["fill_price"].iloc[0] == pytest.approx(expected, rel=1e-9)
|
||||
assert any("liquidated" in w for w in result.warnings)
|
||||
|
||||
|
||||
def test_without_liquidation_the_wick_is_survived(store):
|
||||
result = bt.run(_hold_3x(), _cfg(None), store)
|
||||
assert LIQUIDATION not in _reasons(result)
|
||||
|
||||
|
||||
def test_per_symbol_rates_accept_symbol_names(store):
|
||||
# 30 % maintenance margin on AAA alone: liquidated far above the 0.5 % price.
|
||||
liq = bt.Liquidation(maintenance_margin_rate=0.005, per_symbol={"AAA": 0.3})
|
||||
result = bt.run(_hold_3x(), _cfg(liq), store)
|
||||
trades = result.trades_df(backend="pandas")
|
||||
price = trades[trades["exit_reason"] == LIQUIDATION]["fill_price"].iloc[0]
|
||||
assert price > 90.0
|
||||
|
||||
|
||||
def test_the_lite_sweep_agrees_with_the_run(store):
|
||||
cfg = _cfg(bt.Liquidation(maintenance_margin_rate=0.005, fee_bps=20))
|
||||
full = bt.run(_hold_3x(), cfg, store)
|
||||
strat = bt.Strategy.create("sw").signal("w", lit(3.0)).size(lit(3.0) * bt.param("k"))
|
||||
lite = bt.run_sweep_lite(strat, {"k": [1.0]}, cfg, store, device="cpu")
|
||||
assert lite[0].final_equity == pytest.approx(full.equity_df(backend="pandas").iloc[-1, -1], rel=1e-12)
|
||||
|
||||
|
||||
def test_an_impossible_rate_is_refused(store):
|
||||
with pytest.raises(Exception, match="maintenance_margin_rate"):
|
||||
bt.run(_hold_3x(), _cfg(bt.Liquidation(maintenance_margin_rate=1.5)), store)
|
||||
|
||||
|
||||
def test_liquidation_is_labelled_in_round_trips():
|
||||
from manifoldbt._trades import EXIT_REASON_LABELS
|
||||
assert EXIT_REASON_LABELS[LIQUIDATION] == "liquidation"
|
||||
@@ -120,7 +120,7 @@ def test_a_parameter_baked_at_research_time_flatters_the_result(tmp_path):
|
||||
# two run plain backtests on daily data and measure the same leak without
|
||||
# it, which is exactly what a Community wheel can still verify.
|
||||
@pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: the look-ahead detector is gated",
|
||||
)
|
||||
def test_re_running_alone_returns_a_clean_verdict(tmp_path):
|
||||
|
||||
@@ -34,7 +34,7 @@ from manifoldbt.helpers import ExecutionPrice, Interval, Slippage # noqa: E402
|
||||
# assertions count sub-daily bars: coarsened to the Community daily
|
||||
# floor they compare 4 bars and conclude nothing.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: skipped on a Community wheel",
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import manifoldbt as bt
|
||||
|
||||
# Safety diagnostics are a Pro feature.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: skipped on a Community wheel",
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from manifoldbt.diagnostics import detect_lookahead
|
||||
from manifoldbt.indicators import close
|
||||
|
||||
pro_only = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: the detector is a Pro feature",
|
||||
)
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_un_param_declare_mais_lu_par_rien_ne_bloque_pas(monte):
|
||||
|
||||
def test_un_balayage_fixe_les_valeurs_et_passe(monte):
|
||||
store, cfg = monte
|
||||
if bt.license_info()[0] != "Pro":
|
||||
if bt.license_info()[0] not in ("Pro", "Researcher"):
|
||||
pytest.skip("requires Pro beyond the Community combination limit")
|
||||
b = bt.run_sweep_lite(_croisement(bt.param("fast"), bt.param("slow")),
|
||||
{"fast": [8, 12], "slow": [26]}, cfg, store)
|
||||
|
||||
@@ -20,7 +20,7 @@ import pytest
|
||||
|
||||
import manifoldbt as bt
|
||||
|
||||
IS_PRO = bt.license_info()[0] == "Pro"
|
||||
IS_PRO = bt.license_info()[0] in ("Pro", "Researcher")
|
||||
N_BARS = 3 * 3600 # three hours of one-second bars
|
||||
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ def _wf_config(metric, train_days, test_days):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(bt.license_info()[0] != "Pro",
|
||||
@pytest.mark.skipif(bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="walk-forward: skipped on a Community wheel")
|
||||
def test_walk_forward_on_windows_under_two_days(tmp_path):
|
||||
# Four days of minutes; one-day training windows, one-day tests.
|
||||
|
||||
@@ -13,7 +13,7 @@ from manifoldbt import run_sweep, run_with_parquet
|
||||
# comparison passes vacuously. Sub-daily output is a Pro feature, so the suite
|
||||
# skips rather than assert on an empty result.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: these assertions are only meaningful at sub-daily resolution",
|
||||
)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import manifoldbt as bt
|
||||
from manifoldbt.expr import col
|
||||
from manifoldbt.helpers import Interval, Slippage
|
||||
|
||||
IS_PRO = bt.license_info()[0] == "Pro"
|
||||
IS_PRO = bt.license_info()[0] in ("Pro", "Researcher")
|
||||
|
||||
|
||||
def _locked():
|
||||
|
||||
@@ -22,7 +22,7 @@ import manifoldbt as bt
|
||||
# Walk-forward optimization is a Pro feature: on a Community wheel the
|
||||
# gate raises before the exogenous columns are ever loaded.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: skipped on a Community wheel",
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import pytest
|
||||
import manifoldbt as bt
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: skipped on a Community wheel",
|
||||
)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import manifoldbt as bt
|
||||
# every assertion below about folds and warmup would fail on the gate,
|
||||
# not on what it tests. The suite skips, and names why.
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: skipped on a Community wheel",
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import pytest
|
||||
import manifoldbt as bt
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
bt.license_info()[0] != "Pro",
|
||||
bt.license_info()[0] not in ("Pro", "Researcher"),
|
||||
reason="requires Pro: skipped on a Community wheel",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user