Files
tickreplay/tests/test_sweep.py
T
danielmjrr 160ed2f948 tickreplay 0.1.0: tick-accurate MT5 backtesting engine + research toolkit
Memory-mapped tickstores, a nogil numba replay kernel validated trade-for-trade
against an independent reference implementation, purged walk-forward CV, an
append-only FDR registry, tick-exact labels, and online changepoint detection.
129 tests, 4 runnable examples on synthetic data, CI for linux/macos.
2026-08-03 19:43:21 +02:00

75 lines
2.7 KiB
Python

"""Sweeps must fan out without scrambling which result belongs to which config."""
import time
import numpy as np
import pytest
import tickreplay as tr
def crossover_case():
time_ns, bid, ask = tr.synth.generate_ticks(days=3, ticks_per_day=8000, seed=5)
store = tr.TickStore(time_ns, bid, ask)
bars = store.bars("M5")
trend = np.sign(bars["close"].rolling(5).mean() - bars["close"].rolling(20).mean())
signal = trend.fillna(0).to_numpy().astype("int8")
signal[signal == np.roll(signal, 1)] = 0 # act on crosses only
return store, bars, signal
def test_sweep_exits_returns_one_row_per_config_matching_a_direct_replay():
store, bars, signal = crossover_case()
base = {"max_hold": "1h", "cost": 0.09}
grid = [{"sl": sl, "tp": tp} for sl in (1.0, 3.0) for tp in (2.0, 6.0)]
df = tr.sweep_exits(store, bars, signal, grid, base=base)
assert len(df) == len(grid)
assert df["n_trades"].min() > 0
assert df["total_net"].nunique() == len(grid), "grid must actually discriminate"
for row, overrides in zip(df.to_dict("records"), grid):
assert (row["sl"], row["tp"]) == (overrides["sl"], overrides["tp"])
expected = tr.replay(store, bars, signal,
tr.Config(**base, **overrides)).summary()
assert set(expected) <= set(row)
for key, value in expected.items():
assert row[key] == pytest.approx(value), f"{overrides}: {key}"
def test_sweep_exits_applies_base_under_the_grid():
store, bars, signal = crossover_case()
grid = [{"sl": 2.0}]
with_hold = tr.sweep_exits(store, bars, signal, grid, base={"max_hold": "15m"})
without_hold = tr.sweep_exits(store, bars, signal, grid)
assert with_hold.loc[0, "n_trades"] != without_hold.loc[0, "n_trades"]
def test_run_sweep_preserves_config_to_result_pairing():
# descending delays make workers finish in roughly reverse submission order
configs = [{"idx": i, "delay": (7 - i) * 0.02} for i in range(8)]
def evaluate(config):
time.sleep(config["delay"])
return {"echo": config["idx"] * 10}
df = tr.run_sweep(evaluate, configs, workers=4)
assert list(df["idx"]) == [c["idx"] for c in configs]
assert list(df["echo"]) == [c["idx"] * 10 for c in configs]
def test_run_sweep_runs_configs_concurrently():
def evaluate(config):
time.sleep(0.1)
return {"idx_seen": config["idx"]}
configs = [{"idx": i} for i in range(8)]
started = time.perf_counter()
df = tr.run_sweep(evaluate, configs, workers=8)
elapsed = time.perf_counter() - started
assert list(df["idx_seen"]) == list(range(8))
assert elapsed < 0.4, f"8 x 100ms across 8 workers took {elapsed:.2f}s -- serialized?"