Files
tickreplay/examples/sweep_and_validate.py
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

60 lines
2.5 KiB
Python

"""Sweep exit rules, then test the winner honestly.
Picking the best cell of a grid and reporting its p-value as-is is the standard way
to manufacture an edge. This example sweeps, then thins overlapping trades before
testing, and reads the result against a selection-adjusted threshold.
Run: python examples/sweep_and_validate.py
"""
import numpy as np
import tickreplay as tr
HOUR_NS = 3_600 * 10**9
def main():
time_ns, bid, ask = tr.synth.generate_ticks(days=60, seed=3)
store = tr.TickStore(time_ns, bid, ask)
bars = store.bars("M5")
fast = bars["close"].rolling(10).mean()
slow = bars["close"].rolling(50).mean()
regime = np.sign(fast - slow).fillna(0.0).to_numpy()
signal = np.where(regime != np.r_[0.0, regime[:-1]], regime, 0).astype("int8")
base = {"max_hold": "4h", "cost": 0.05}
grid = [{"sl": sl, "tp": tp} for sl in (2.0, 3.0, 4.0) for tp in (4.0, 6.0, 8.0)]
table = tr.sweep_exits(store, bars, signal, grid, base=base)
table = table.sort_values("total_net", ascending=False).reset_index(drop=True)
print(f"{len(grid)} exit configs over {len(store):,} ticks\n")
print(table[["sl", "tp", "n_trades", "total_net", "hit_rate",
"max_drawdown"]].to_string(index=False, float_format="%.3f"))
best = table.iloc[0]
result = tr.replay(store, bars, signal,
tr.Config(sl=best["sl"], tp=best["tp"], **base))
pnl = result.arrays["pnl_net"]
# Trades whose holding windows overlap are correlated; a t-stat over all of them
# is inflated. Test a non-overlapping subset spaced by the max hold time.
keep = tr.research.stats.thin_by_gap(result.arrays["entry_time_ns"], 4 * HOUR_NS)
t_stat, p_value = tr.research.stats.one_sided_positive(pnl[keep])
threshold = 0.05 / len(grid)
print(f"\nbest by total_net: sl={best['sl']} tp={best['tp']}")
print(f" trades {len(pnl):>8,} -> {int(keep.sum()):,} after thinning")
print(f" one-sided t {t_stat:>8.3f}")
print(f" p (mean > 0) {p_value:>8.4f}")
print(f" threshold (0.05/{len(grid)}) {threshold:>8.4f}")
print(f"\n verdict: {'NOT significant' if p_value > threshold else 'passes'}"
" once selection over the grid is accounted for.")
print(" Expected: this is a synthetic random walk with costs, so there is no edge"
"\n to find. A pass here would mean the procedure is broken, not that the"
"\n strategy works.")
if __name__ == "__main__":
main()