mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-11-02 07:56:11 +00:00
release: v0.27.1
This commit is contained in:
committed by
Jimmy7892
parent
6de6e3ae2e
commit
8ebd171f71
@@ -0,0 +1,76 @@
|
||||
"""Les diagnostics doivent accepter les series EXOGENES, comme `run`.
|
||||
|
||||
`detect_lookahead` relancait la strategie avec des tables exo vides, et
|
||||
`AlignedData` (sur lequel `check_exposure_stability` relance ses runs) n'en
|
||||
portait aucune : une strategie lisant `exo.<nom>.<colonne>` levait
|
||||
"unknown input column" dans les deux, alors que `mbt.run` la faisait tourner.
|
||||
"""
|
||||
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):
|
||||
"""Serie horaire reguliere, plus une moyenne posee en serie exogene."""
|
||||
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")
|
||||
meta = str(tmp_path / "meta.sqlite")
|
||||
os.makedirs(racine, exist_ok=True)
|
||||
store = bt.import_dataframe(df.reset_index(), symbol="ZDEX", symbol_id=1,
|
||||
interval="1h", asset_class="equity",
|
||||
exchange="TEST", data_root=racine, metadata_db=meta)
|
||||
# Moyenne CAUSALE (fenetre glissante sur le passe) : aucune fuite.
|
||||
moy = pd.Series(px).rolling(24).mean().to_numpy()
|
||||
bt.register_exo("moyenne", pd.DataFrame({"timestamp": idx, "sma": moy}),
|
||||
store=store, data_root=racine, timeframe="1h")
|
||||
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={"ZDEX": 1})
|
||||
cfg.warmup_bars = 0
|
||||
cfg.exo_data = ["moyenne"]
|
||||
return store, cfg
|
||||
|
||||
|
||||
def _strategie():
|
||||
from manifoldbt.indicators import close, col
|
||||
m = col("exo.moyenne.sma")
|
||||
return (bt.Strategy.create("s")
|
||||
.signal("m", m)
|
||||
.size(bt.when(close > m, 1.0, 0.0)))
|
||||
|
||||
|
||||
def test_la_strategie_a_exo_trade_dans_run(tmp_path):
|
||||
store, cfg = _monte(tmp_path)
|
||||
r = bt.run(_strategie(), cfg, store)
|
||||
assert r.trade_count > 0
|
||||
|
||||
|
||||
def test_detect_lookahead_accepte_une_serie_exogene(tmp_path):
|
||||
store, cfg = _monte(tmp_path)
|
||||
rapport = bt.diagnostics.detect_lookahead(_strategie(), cfg, store, mode="all")
|
||||
# Une moyenne glissante sur le passe ne fuit pas : les re-runs tronques
|
||||
# doivent retrouver les trades du run complet.
|
||||
assert rapport.passed, rapport
|
||||
|
||||
|
||||
def test_check_exposure_stability_accepte_une_serie_exogene(tmp_path):
|
||||
store, cfg = _monte(tmp_path)
|
||||
rapport = bt.diagnostics.check_exposure_stability(_strategie(), cfg, store)
|
||||
assert rapport.passed, rapport
|
||||
@@ -149,6 +149,37 @@ def test_le_message_nomme_le_prix_et_la_sortie(store):
|
||||
assert "AtClose" in message
|
||||
|
||||
|
||||
@pytest.mark.parametrize("moyenne", ["TypicalPrice", "OhlcAverage"])
|
||||
def test_une_moyenne_intrabarre_sans_delai_est_refusee(store, moyenne):
|
||||
"""`fill_model.intra_bar_price` averages the execution bar whatever the
|
||||
price says: at delay 0, with the default close, it filled at (H+L+C)/3 or
|
||||
(O+H+L+C)/4 of the signal's own bar (Sharpe 37 and 65 on a driftless
|
||||
walk, `detect_lookahead` PASS). Refused like `AtVwap`."""
|
||||
with pytest.raises(ConfigError, match="needs signal_delay >= 1") as exc:
|
||||
bt.run(_reads_its_own_bar(),
|
||||
_config(signal_delay=0, fill_model={"intra_bar_price": moyenne}),
|
||||
store)
|
||||
assert moyenne in str(exc.value)
|
||||
assert "signal_delay=1" in str(exc.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("moyenne", ["TypicalPrice", "OhlcAverage"])
|
||||
def test_une_moyenne_intrabarre_avec_delai_reste_permise(store, moyenne):
|
||||
"""With a delay the averaged bar follows the signal: nothing to refuse."""
|
||||
result = bt.run(_reads_its_own_bar(),
|
||||
_config(signal_delay=1, fill_model={"intra_bar_price": moyenne}),
|
||||
store)
|
||||
assert abs(result.metrics["total_return"]) < 0.5
|
||||
|
||||
|
||||
def test_single_point_sans_delai_reste_permis(store):
|
||||
"""`SinglePoint` is the default: the price named by `execution_price`."""
|
||||
result = bt.run(_reads_its_own_bar(),
|
||||
_config(signal_delay=0, fill_model={"intra_bar_price": "SinglePoint"}),
|
||||
store)
|
||||
assert abs(result.metrics["total_return"]) < 0.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("price", ["AtClose", "NextBarClose"])
|
||||
def test_la_cloture_reste_permise_sans_delai(store, price):
|
||||
"""These two fill at the close that produced the signal: nothing to refuse.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,81 @@
|
||||
"""run() refuse un param() lu sans valeur, au lieu de ne rien trader.
|
||||
|
||||
Un param() sans defaut n'a de valeur que dans un balayage. Un run seul le
|
||||
lisait comme une serie vide : EMA sans periode, aucun signal, 0 trade, aucune
|
||||
erreur (la strategie de l'exemple 08 lancee seule). Les balayages, eux,
|
||||
fixent chaque nom balaye et ne passent pas par cette garde.
|
||||
"""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import manifoldbt as bt
|
||||
from manifoldbt.exceptions import StrategyError
|
||||
from manifoldbt.indicators import close, ema
|
||||
|
||||
pd = pytest.importorskip("pandas")
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def monte(tmp_path_factory):
|
||||
tmp = tmp_path_factory.mktemp("param")
|
||||
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 / "data")
|
||||
os.makedirs(racine, exist_ok=True)
|
||||
store = bt.import_dataframe(df.reset_index(), symbol="ZPSV", symbol_id=1,
|
||||
interval="1h", asset_class="equity",
|
||||
exchange="TEST", data_root=racine,
|
||||
metadata_db=str(tmp / "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={"ZPSV": 1})
|
||||
return store, cfg
|
||||
|
||||
|
||||
def _croisement(fast, slow):
|
||||
f, s = ema(close, fast), ema(close, slow)
|
||||
return (bt.Strategy.create("croisement").signal("f", f).signal("s", s)
|
||||
.size(bt.when(f > s, 0.25, bt.when(f < s, -0.25, 0.0))))
|
||||
|
||||
|
||||
def test_un_param_lu_sans_valeur_est_refuse_et_nomme(monte):
|
||||
store, cfg = monte
|
||||
with pytest.raises(StrategyError, match="has no value in a plain run") as exc:
|
||||
bt.run(_croisement(bt.param("fast"), bt.param("slow")), cfg, store)
|
||||
assert "'fast'" in str(exc.value) and "'slow'" in str(exc.value)
|
||||
|
||||
|
||||
def test_un_param_avec_defaut_tourne(monte):
|
||||
store, cfg = monte
|
||||
r = bt.run(_croisement(bt.param("fast", default=12), bt.param("slow", default=26)),
|
||||
cfg, store)
|
||||
assert r.trade_count > 0
|
||||
|
||||
|
||||
def test_une_declaration_strategy_param_donne_la_valeur(monte):
|
||||
store, cfg = monte
|
||||
s = (_croisement(bt.param("fast"), bt.param("slow"))
|
||||
.param("fast", default=12).param("slow", default=26))
|
||||
assert bt.run(s, cfg, store).trade_count > 0
|
||||
|
||||
|
||||
def test_un_param_declare_mais_lu_par_rien_ne_bloque_pas(monte):
|
||||
store, cfg = monte
|
||||
s = _croisement(12, 26).param("seuil")
|
||||
assert bt.run(s, cfg, store).trade_count > 0
|
||||
|
||||
|
||||
def test_un_balayage_fixe_les_valeurs_et_passe(monte):
|
||||
store, cfg = monte
|
||||
if bt.license_info()[0] != "Pro":
|
||||
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)
|
||||
assert len(b) == 2
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Le walk-forward doit accepter les sources exogenes PAR SYMBOLE (`exo_sources`).
|
||||
|
||||
Une strategie lisant `exo.<colonne>` d'une source par symbole tournait dans
|
||||
`run`, mais le walk-forward passait des sources vides a ses trois runs
|
||||
(selection, run d'apprentissage, run hors echantillon) et levait
|
||||
"unknown input column".
|
||||
"""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import manifoldbt as bt
|
||||
|
||||
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):
|
||||
"""Le titre trade, une seconde source porte une moyenne causale du titre."""
|
||||
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
|
||||
aux = pd.Series(px).rolling(24, min_periods=1).mean().to_numpy()
|
||||
|
||||
def barres(p):
|
||||
return pd.DataFrame({"open": p, "high": p * 1.004, "low": p * 0.996,
|
||||
"close": p, "volume": np.full(n, 1000.0)},
|
||||
index=idx).reset_index()
|
||||
|
||||
racine = str(tmp_path / "data")
|
||||
meta = str(tmp_path / "meta.sqlite")
|
||||
os.makedirs(racine, exist_ok=True)
|
||||
store = bt.import_dataframe(barres(px), symbol="ZESM", symbol_id=1, interval="1h",
|
||||
asset_class="equity", exchange="TEST",
|
||||
data_root=racine, metadata_db=meta)
|
||||
bt.import_dataframe(barres(aux), symbol="ZESA", symbol_id=2, interval="1h",
|
||||
asset_class="equity", exchange="AUX",
|
||||
data_root=racine, metadata_db=meta)
|
||||
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={"ZESM": 1})
|
||||
cfg.warmup_bars = 0
|
||||
cfg.exo_sources = {1: ("AUX", "ZESA")}
|
||||
return store, cfg
|
||||
|
||||
|
||||
def _strategie():
|
||||
from manifoldbt.indicators import close, col
|
||||
m = col("exo.close")
|
||||
bande = m * (bt.lit(1.0) - bt.param("dev", default=0.002))
|
||||
return (bt.Strategy.create("s")
|
||||
.signal("b", bande)
|
||||
.size(bt.when(close < bande, 1.0, bt.when(close > m, 0.0, bt.hold()))))
|
||||
|
||||
|
||||
WF = {"method": "Anchored", "n_splits": 3, "train_ratio": 0.5,
|
||||
"optimize_metric": "sharpe", "param_grid": {"dev": [0.002, 0.005, 0.01]}}
|
||||
|
||||
|
||||
def test_la_strategie_trade_dans_run(tmp_path):
|
||||
store, cfg = _monte(tmp_path)
|
||||
assert bt.run(_strategie(), cfg, store).trade_count > 0
|
||||
|
||||
|
||||
def test_walk_forward_accepte_une_source_par_symbole(tmp_path):
|
||||
store, cfg = _monte(tmp_path)
|
||||
r = bt.run_walk_forward(_strategie(), WF, cfg, store)
|
||||
assert r["n_folds"] == 3
|
||||
# La source est lue dans les trois runs : la selection (metriques IS) et
|
||||
# les deux courbes. Sans elle, aucun pli ne trade.
|
||||
assert any(f["oos_metrics"]["total_return"] != 0.0 for f in r["folds"])
|
||||
assert any(len(set(f["is_equity"])) > 1 for f in r["folds"])
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Walk-forward sur un signal plus grossier que le store.
|
||||
|
||||
Un signal 12h sur des barres 1h (mode hybride) rendait, en sortie
|
||||
automatique, une courbe OOS sous-echantillonnee au pas du signal. Sa longueur
|
||||
ne valait plus celle des barres natives, et le walk-forward retombait sur les
|
||||
metriques du run ENTIER, chauffe plate comprise : Sharpe hors echantillon
|
||||
dilue, WFE faux, sans un avertissement. Avec `output_resolution` a 1h, le meme
|
||||
walk-forward donnait le bon chiffre.
|
||||
|
||||
Le contrat : les metriques hors echantillon ne dependent pas de la resolution
|
||||
de sortie, et la courbe rendue suit celle-ci, avec autant d'horodatages que de
|
||||
points.
|
||||
"""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import manifoldbt as bt
|
||||
|
||||
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")
|
||||
|
||||
HEURE_NS = 3_600 * 1_000_000_000
|
||||
|
||||
|
||||
def _magasin_horaire(tmp_path):
|
||||
"""100 jours de barres horaires, prix cyclique pour garantir des trades."""
|
||||
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")
|
||||
meta = str(tmp_path / "meta.sqlite")
|
||||
os.makedirs(racine, exist_ok=True)
|
||||
store = bt.import_dataframe(df.reset_index(), symbol="ZWFR", symbol_id=1,
|
||||
interval="1h", asset_class="equity",
|
||||
exchange="TEST", data_root=racine, metadata_db=meta)
|
||||
return store
|
||||
|
||||
|
||||
def _config(signal):
|
||||
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=signal, symbol_names={"ZWFR": 1})
|
||||
cfg.warmup_bars = 0
|
||||
return cfg
|
||||
|
||||
|
||||
def _strategie():
|
||||
from manifoldbt.indicators import close, sma
|
||||
m = sma(close, bt.param("n"))
|
||||
return (bt.Strategy.create("s")
|
||||
.signal("m", m)
|
||||
.size(bt.when(close > m, 1.0, 0.0)))
|
||||
|
||||
|
||||
WF = {"geometry": "anchored", "n_splits": 2, "train_ratio": 0.6,
|
||||
"optimize_metric": "total_return", "param_grid": {"n": [4, 8]}}
|
||||
|
||||
|
||||
def test_les_metriques_oos_ne_dependent_pas_de_la_sortie(tmp_path):
|
||||
store = _magasin_horaire(tmp_path)
|
||||
auto = bt.run_walk_forward(_strategie(), WF, _config(bt.Interval.hours(12)), store)
|
||||
cfg_native = _config(bt.Interval.hours(12))
|
||||
cfg_native.output_resolution = bt.Interval.hours(1)
|
||||
native = bt.run_walk_forward(_strategie(), WF, cfg_native, store)
|
||||
|
||||
assert auto["n_folds"] == native["n_folds"] == 2
|
||||
for fa, fn in zip(auto["folds"], native["folds"]):
|
||||
assert fa["best_params"] == fn["best_params"]
|
||||
for k in ("sharpe", "cagr", "total_return", "volatility", "max_drawdown"):
|
||||
assert fa["oos_metrics"][k] == pytest.approx(fn["oos_metrics"][k], rel=1e-12), k
|
||||
assert fa["oos_metrics"]["sharpe"] != 0.0, "le pli doit trader"
|
||||
if fn["wfe"] is None:
|
||||
assert fa["wfe"] is None
|
||||
else:
|
||||
assert fa["wfe"] == pytest.approx(fn["wfe"], rel=1e-12)
|
||||
|
||||
|
||||
def test_la_courbe_oos_suit_la_resolution_de_sortie(tmp_path):
|
||||
store = _magasin_horaire(tmp_path)
|
||||
auto = bt.run_walk_forward(_strategie(), WF, _config(bt.Interval.hours(12)), store)
|
||||
cfg_native = _config(bt.Interval.hours(12))
|
||||
cfg_native.output_resolution = bt.Interval.hours(1)
|
||||
native = bt.run_walk_forward(_strategie(), WF, cfg_native, store)
|
||||
for fa, fn in zip(auto["folds"], native["folds"]):
|
||||
eq, ts = fa["oos_equity"], fa["oos_timestamps"]
|
||||
assert len(eq) == len(ts) > 0
|
||||
assert ts[0] >= fa["test_range"]["start"]
|
||||
assert ts[-1] < fa["test_range"]["end"]
|
||||
# Un point par seau de sortie, au plus 12h d'ecart...
|
||||
pas = np.diff(np.asarray(ts))
|
||||
assert pas.max() == 12 * HEURE_NS and pas.min() > 0
|
||||
# ... et chaque point est celui de la courbe native a cet instant.
|
||||
natif = dict(zip(fn["oos_timestamps"], fn["oos_equity"]))
|
||||
assert len(natif) == len(fn["oos_timestamps"]) > len(ts)
|
||||
for t, e in zip(ts, eq):
|
||||
assert natif[t] == e
|
||||
Reference in New Issue
Block a user