| # SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| """Self-test of tests/reference.py on synthetic bars (no broker data needed).
|
|
|
| python3 tests/check_reference.py [seeds] default 20 seeds, 3 years of H4 bars each
|
|
|
| For every random walk and each source (UT Bot, SuperTrend, Range Filter) it checks: the last bar never carries a signal (it
|
| is still forming); SuperTrend signals alternate buy, sell, buy; every trade enters at the open of the bar after a signal bar;
|
| the stop distance is stop x ATR(14) of the signal bar; trades never overlap; a reversal exits and re-enters at the same open on
|
| the other side; a stop exit is on the bar where the extreme touches the stop."""
|
| import sys
|
| from pathlib import Path
|
|
|
| import numpy as np
|
| import pandas as pd
|
|
|
| sys.path.insert(0, str(Path(__file__).parent))
|
| from reference import signals, run, atr14 # noqa: E402
|
|
|
| SOURCES = {"ut": dict(key=2.0, atr=10, ema=0), "st": dict(atr=10, mult=2.0, ema=0), "rf": dict(period=100, mult=3.0)}
|
|
|
|
|
| def synth(seed, years=3):
|
| rng = np.random.default_rng(seed)
|
| idx = pd.date_range("2021-01-01", periods=years * 365 * 6, freq="4h")
|
| idx = idx[idx.dayofweek < 5]
|
| drift = 0.15 * np.sin(np.arange(len(idx)) / 400.0)
|
| close = 1800 + np.cumsum(rng.normal(drift, 6.0))
|
| open_ = np.concatenate(([1800.0], close[:-1]))
|
| span = np.abs(rng.normal(0, 3.0, len(idx)))
|
| return pd.DataFrame({"open": open_, "high": np.maximum(open_, close) + span, "low": np.minimum(open_, close) - span,
|
| "close": close, "spread": rng.choice([0.2, 0.3, 0.5], len(idx))}, index=idx)
|
|
|
|
|
| def main(seeds):
|
| problems, n_sig, n_tr = [], 0, 0
|
| for seed in range(seeds):
|
| b = synth(seed)
|
| o, h, l, c = (b[k].to_numpy(float) for k in ("open", "high", "low", "close"))
|
| atr = atr14(h, l, c)
|
| for src, p in SOURCES.items():
|
| sig = signals(src, p, o, h, l, c)
|
| n_sig += int((sig != 0).sum())
|
| if sig[-1] != 0:
|
| problems.append(("signal on the last (forming) bar", src, seed))
|
| if src == "st":
|
| s = sig[sig != 0]
|
| if len(s) > 1 and (s[1:] == s[:-1]).any():
|
| problems.append(("SuperTrend signals do not alternate", src, seed))
|
| tr = run(b, sig, 3.0)
|
| n_tr += len(tr)
|
| pos_idx = {t: k for k, t in enumerate(b.index)}
|
| last_exit = None
|
| for i, r in enumerate(tr.itertuples()):
|
| k = pos_idx[r.entry_bar]
|
| if sig[k - 1] != r.side:
|
| problems.append(("entry not on the bar after a same-side signal", src, seed, str(r.entry_bar)))
|
| if abs(r.dist - 3.0 * atr[k - 1]) > 1e-9:
|
| problems.append(("stop distance is not 3 x ATR(14) of the signal bar", src, seed, str(r.entry_bar)))
|
| if last_exit is not None and r.entry_bar < last_exit:
|
| problems.append(("overlap", src, seed, str(r.entry_bar)))
|
| if i > 0 and tr.iloc[i - 1].why == "reverse":
|
| q = tr.iloc[i - 1]
|
| if not (q.exit_bar == r.entry_bar and q.side == -r.side):
|
| problems.append(("reversal not at the same open", src, seed, str(r.entry_bar)))
|
| if r.why == "stop":
|
| j = pos_idx[r.exit_bar]
|
| touched = l[j] <= r.exit if r.side > 0 else h[j] + b.spread.iloc[j] >= r.exit
|
| if not touched:
|
| problems.append(("stop exit without a touch", src, seed, str(r.exit_bar)))
|
| last_exit = r.exit_bar
|
| print(f"seeds: {seeds}, signals: {n_sig}, trades: {n_tr}")
|
| print(f"violations: {len(problems)}")
|
| for p in problems[:10]:
|
| print(" ", p)
|
| print("RESULT:", "PASS" if not problems else "FAIL")
|
| return 0 if not problems else 1
|
|
|
|
|
| if __name__ == "__main__":
|
| sys.exit(main(int(sys.argv[1]) if len(sys.argv) > 1 else 20))
|