| # 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, 4 years of hourly bars each
|
|
|
| Per random walk, on the trades the reference returns: one position at a time (no overlap); every entry is at an H4 bar
|
| open; a stop exit is at or beyond the stop and never inside the bar's range the wrong way; an entry's stop distance is
|
| 3 x ATR(14) of the signal bar; a reversal exits and re-enters at the same open on the other side; every signal that is
|
| not ignored (same side as the open trade) while flat or opposite leads to a trade; and the signal rule never fires when
|
| Supertrend or the daily EMA disagree."""
|
| 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 run, signals, rma, true_range, supertrend_dir, daily_ema_dir, ut_line # noqa: E402
|
|
|
|
|
| def synth(seed, years=4):
|
| rng = np.random.default_rng(seed)
|
| idx = pd.date_range("2020-01-01", periods=years * 365 * 24, freq="h")
|
| idx = idx[idx.dayofweek < 5]
|
| drift = 0.02 * np.sin(np.arange(len(idx)) / 900.0)
|
| close = 1800 + np.cumsum(rng.normal(drift, 2.0))
|
| open_ = np.concatenate(([1800.0], close[:-1]))
|
| span = np.abs(rng.normal(0, 1.2, len(idx)))
|
| h1 = pd.DataFrame({"open": open_, "high": np.maximum(open_, close) + span, "low": np.minimum(open_, close) - span,
|
| "close": close}, index=idx)
|
| spread = pd.Series(rng.choice([0.15, 0.25, 0.4], len(idx)), index=idx)
|
| return h1, spread
|
|
|
|
|
| def bars(h1, rule):
|
| g = h1.resample(rule, label="left", closed="left")
|
| return pd.DataFrame({"open": g.open.first(), "high": g.high.max(), "low": g.low.min(), "close": g.close.last()}).dropna()
|
|
|
|
|
| def main(seeds):
|
| problems, n_trades, n_signals = [], 0, 0
|
| for seed in range(seeds):
|
| h1, spread = synth(seed)
|
| h4, d1 = bars(h1, "4h"), bars(h1, "1D")
|
| sp4 = spread.resample("4h", label="left", closed="left").mean().reindex(h4.index).ffill()
|
| sig, atr = signals(h4, d1)
|
| n_signals += int((sig != 0).sum())
|
| # the signal rule never fires against Supertrend or the daily EMA
|
| o, h, l, c = (h4[k].to_numpy(float) for k in ("open", "high", "low", "close"))
|
| st = supertrend_dir(h, l, c, rma(true_range(h, l, c), 10), 3.0)
|
| for j in np.flatnonzero(sig.to_numpy() != 0):
|
| if st[j] != sig.iloc[j]:
|
| problems.append(("signal against Supertrend", seed, str(h4.index[j])))
|
| tr = run(h4, d1, sp4)
|
| n_trades += len(tr)
|
| if tr.empty:
|
| continue
|
| opens = set(h4.index)
|
| last_exit = None
|
| for i, r in enumerate(tr.itertuples()):
|
| if r.entry_time not in opens:
|
| problems.append(("entry not at a bar open", seed, str(r.entry_time)))
|
| if last_exit is not None and r.entry_time < last_exit:
|
| problems.append(("overlap", seed, str(r.entry_time)))
|
| if r.exit_time < r.entry_time:
|
| problems.append(("exit before entry", seed, str(r.entry_time)))
|
| if i > 0 and tr.iloc[i - 1].why == "reversed":
|
| p = tr.iloc[i - 1]
|
| if not (p.exit_time == r.entry_time and p["dir"] == -r.dir):
|
| problems.append(("reversal not at the same open on the other side", seed, str(r.entry_time)))
|
| if r.why == "stop":
|
| bar = h4.loc[r.exit_time]
|
| if r.dir > 0 and not (bar.low <= r.exit + 1e-9 or r.exit <= bar.open + 1e-9):
|
| problems.append(("buy stop exit outside the bar", seed, str(r.exit_time)))
|
| last_exit = r.exit_time
|
| # entry stop distance = 3 x ATR(14) of the signal bar
|
| for r in tr.itertuples():
|
| j = h4.index.get_loc(r.entry_time) - 1
|
| if abs(r.dist - 3.0 * atr.iloc[j]) > 1e-9:
|
| problems.append(("entry stop distance is not 3 x ATR(14) of the signal bar", seed, str(r.entry_time)))
|
| print(f"seeds: {seeds}, signals: {n_signals}, trades: {n_trades}")
|
| 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))
|