mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-11-02 07:56:11 +00:00
80 lines
3.1 KiB
Python
80 lines
3.1 KiB
Python
"""Un sous-test de re-run qui n'a rien compare ne rend pas PASS.
|
|
|
|
detect_lookahead rendait PASS sur une strategie qui ne trade jamais (un
|
|
param() sans valeur suffit), ou quand le run tronque echouait : un verdict
|
|
vide lu comme un feu vert. Il rend maintenant INCONCLUSIVE, `.passed` vaut
|
|
False et `assert_clean()` leve, en disant pourquoi.
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
import manifoldbt as bt
|
|
|
|
# Safety diagnostics are a Pro feature.
|
|
pytestmark = pytest.mark.skipif(
|
|
bt.license_info()[0] != "Pro",
|
|
reason="requires Pro: skipped on a Community wheel",
|
|
)
|
|
|
|
pd = pytest.importorskip("pandas")
|
|
np = pytest.importorskip("numpy")
|
|
|
|
|
|
def _monte(tmp_path):
|
|
n = 2400
|
|
idx = pd.date_range("2021-01-01", periods=n, freq="1h", tz="UTC", name="timestamp")
|
|
pas = np.sin(np.arange(n) / 37.0) * 2.0 + np.cos(np.arange(n) / 11.0)
|
|
px = 100.0 + np.cumsum(pas) * 0.05
|
|
df = pd.DataFrame({"open": px, "high": px * 1.004, "low": px * 0.996,
|
|
"close": px, "volume": np.full(n, 1000.0)}, index=idx)
|
|
racine = str(tmp_path / "data")
|
|
os.makedirs(racine, exist_ok=True)
|
|
store = bt.import_dataframe(df.reset_index(), symbol="ZLSV", symbol_id=1,
|
|
interval="1h", asset_class="equity",
|
|
exchange="TEST", data_root=racine,
|
|
metadata_db=str(tmp_path / "meta.sqlite"))
|
|
tr0, tr1 = bt.time_range("2021-01-01", "2021-04-11")
|
|
cfg = bt.BacktestConfig(universe=[1], time_range_start=tr0, time_range_end=tr1,
|
|
initial_capital=1000.0, provider="TEST",
|
|
bar_interval=bt.Interval.hours(1), symbol_names={"ZLSV": 1})
|
|
cfg.warmup_bars = 0
|
|
return store, cfg
|
|
|
|
|
|
def _jamais():
|
|
from manifoldbt.indicators import close
|
|
return (bt.Strategy.create("jamais")
|
|
.signal("c", close)
|
|
.size(bt.lit(0.0)))
|
|
|
|
|
|
def _croise():
|
|
from manifoldbt.indicators import close, sma
|
|
m = sma(close, 24)
|
|
return bt.Strategy.create("croise").signal("m", m).size(bt.when(close > m, 1.0, 0.0))
|
|
|
|
|
|
def test_une_strategie_qui_ne_trade_pas_est_inconclusive(tmp_path):
|
|
store, cfg = _monte(tmp_path)
|
|
rapport = bt.diagnostics.detect_lookahead(_jamais(), cfg, store, mode="all")
|
|
assert not rapport.passed
|
|
re_runs = [r for r in rapport.reports if r.method != "static"]
|
|
assert {r.method for r in re_runs} == {"extension", "truncation"}
|
|
for r in re_runs:
|
|
assert r.inconclusive and "made no trade" in r.inconclusive
|
|
assert "INCONCLUSIVE" in str(r)
|
|
statique = [r for r in rapport.reports if r.method == "static"]
|
|
assert statique and statique[0].passed
|
|
assert str(rapport).startswith("Lookahead diagnostics: INCONCLUSIVE")
|
|
with pytest.raises(AssertionError, match="inconclusive"):
|
|
rapport.assert_clean()
|
|
|
|
|
|
def test_une_strategie_causale_qui_trade_reste_pass(tmp_path):
|
|
store, cfg = _monte(tmp_path)
|
|
rapport = bt.diagnostics.detect_lookahead(_croise(), cfg, store, mode="all")
|
|
assert rapport.passed, rapport
|
|
assert all(r.inconclusive is None for r in rapport.reports)
|
|
assert str(rapport).startswith("Lookahead diagnostics: PASS")
|