backtest: --record/--record-bars Parquet recorder + 2-year M5/M15 replay with indicator weights and calendar

Adds an opt-in recorder to the backtester so a replay keeps the evidence each
trade setup was decided on: every indicator's vote, value, confidence, base
weight, regime multiplier, effective weight and contribution on the primary and
both confirmation timeframes, penalties, the setup and outcome, and the Forex
Factory calendar events around the trade (4 h before entry to exit, all-day rows
on the New-York date). --record-bars adds one float32 row per analyzed bar with
the same votes plus 1 h/4 h/24 h forward moves in ATRs. Output is Parquet in a
folder per run with a manifest carrying the engine/gate fingerprints, the regime
weight table in force and the book MD5.

Nothing in the signal core changes: the trade book is byte-identical with
recording on or off (control-vs-recorded MD5 equal on both 2-year runs, M5
781 / +146.07R / PF 1.52, M15 793 / +117.62R / PF 1.41 at $0.65). Module 33
adds 12 tests (498 total): snapshot reproduces the engine's composite, absence
is no vote, calendar join edges/phases/holiday, write/load round-trip,
_replay_span parity with recording on, CLI flags, import and source guards.

Also: backtest/replay_scorecard.py (first-look scorecards, calendar buckets,
in-book and per-bar what-ifs, --check invariants), scripts/ff_calendar_scraper.py
restored from git history (the only way to regenerate the gitignored calendar
file), report in backtest/research/replay_recording_2026-09-09.md.

Claude-Session: https://claude.ai/code/session_01Nc9Ninzyi18QkWezrxeBvB
This commit is contained in:
Harwinder Sekhon
2026-09-09 16:41:49 -04:00
parent d05348d1e4
commit daa2369ccb
9 changed files with 2965 additions and 4 deletions
+580
View File
@@ -11281,6 +11281,586 @@ def _():
assert name not in req, f"{name} must not enter requirements.txt"
# ══════════════════════════════════════════════════════════════════════
# MODULE 33: backtest replay recording (--record, research-only)
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 33: backtest replay recording ═══")
import contextlib as _ctx33
import dataclasses as _dc33
import hashlib as _hashlib33
import pickle as _pickle33
import types as _types33
from backtest import gpu_backtest as _gb33
from backtest import replay_record as _rr
_M33_CACHE = {}
def _m33_report():
"""One REAL M5 analyze_from_dataframes run + a hand-built setup.
Cached — the engine replay is the slow part and two tests need the
same report. Returns (cfg, aggregator, report, setup)."""
if "report" not in _M33_CACHE:
from pair_signals.xauusd import make_config as _mk33
cfg = _mk33(Timeframe.M5, [Timeframe.M15, Timeframe.H1])
dfs = {
Timeframe.M5: make_xau_bars(n=400, freq="5min",
end="2026-07-08 16:55"),
Timeframe.M15: make_xau_bars(n=300, freq="15min",
end="2026-07-08 16:45", seed=8),
Timeframe.H1: make_xau_bars(n=300, freq="60min",
end="2026-07-08 16:00", seed=9),
}
agg = SignalAggregator(cfg)
report = agg.analyze_from_dataframes(XAUUSD, dfs, use_news=False)
px = report.current_price
# Direction only drives the `agrees` label; this fixture's score
# is bearish, so a SELL keeps the labels meaningful.
setup = TradeSetup(
pair=XAUUSD, display_name="XAU/USD", direction="SELL",
entry_price=px, stop_loss=px + 6.0, take_profit_1=px - 12.0,
take_profit_2=px - 20.0, risk_reward=2.0, risk_pips=60.0,
reward_pips=120.0, pip_value=0.10, strategy="XAU_ATR")
_M33_CACHE["report"] = (cfg, agg, report, setup)
return _M33_CACHE["report"]
def _m33_snapshot():
"""snapshot_report() of the cached real report."""
cfg, _agg, report, setup = _m33_report()
return _rr.snapshot_report(
report, setup, pd.Timestamp("2026-07-08 17:00", tz="UTC"),
Timeframe.M5, [Timeframe.M15, Timeframe.H1])
def _m33_hand_analysis():
"""A TechnicalAnalysis with exactly 4 VOTING indicators + a weight-0
ATR, pushed through the real _compute_composite so the effective
weight / weighted score stamps are the engine's own."""
cfg, _agg, _rep, _setup = _m33_report()
result = TechnicalAnalysis(
symbol=XAUUSD, timeframe=Timeframe.M5.value,
timestamp="2026-07-08 16:55:00+00:00",
indicators={
"ema_trend": IndicatorSignal("EMA", 1.0, SignalStrength.BUY,
60.0, "ema up", 1.0),
"rsi": IndicatorSignal("RSI", 55.0, SignalStrength.NEUTRAL,
50.0, "rsi mid", 1.0),
"macd": IndicatorSignal("MACD", 0.4, SignalStrength.BUY,
70.0, "macd up", 1.5),
"keltner": IndicatorSignal("KC", 2.0, SignalStrength.SELL,
55.0, "kc high", 1.5),
# Context only (weight 0) — the composite skips it, so the
# recorder must too.
"atr": IndicatorSignal("ATR", 5.0, SignalStrength.NEUTRAL,
50.0, "atr", 0.0),
},
market_regime="ranging", volatility_regime="normal",
key_levels={"atr": 5.0, "atr_pct": 0.15})
TechnicalEngine(cfg.technical)._compute_composite(result)
return result
def _m33_duck_report(analysis):
"""Minimal duck-typed report carrying every attribute snapshot_report
and add_bar_row read (they never touch anything else)."""
return _types33.SimpleNamespace(
timestamp="2026-07-08 16:55:00+00:00",
primary_analysis=analysis, confirmation_analyses=[],
final_signal=SignalStrength.BUY, final_score=22.0,
final_confidence=58.0, signal_quality="B",
mtf_agreement=False, mtf_signals={}, mtf_scores={},
penalty_tags={}, penalty_total=0.0, momentum_fading=False,
exhaustion_count=0, spread_estimate=0.30, current_price=3300.0,
warnings=["a warning"], cleaned_warnings=[],
bullish_reasons=["b"], bearish_reasons=[], trade_setup=None)
def _m33_cal_row(row_id, ts, name, currency="USD", impact="high",
masked=False, label="1:30pm"):
"""One Forex-Factory calendar row in the parquet's own schema."""
return {
"id": row_id, "dateline": int(pd.Timestamp(ts).timestamp()),
"name": name, "country": currency[:2], "currency": currency,
"impactName": impact, "timeLabel": label, "timeMasked": masked,
"actual": "1.0", "forecast": "0.9", "previous": "0.8",
"revision": "", "actualBetterWorse": 1,
}
def _m33_span_frames(drift=0.8):
"""600 M5 bars + long-enough M15/H1 frames that produce candidates."""
return {
Timeframe.M5: make_xau_bars(n=600, freq="5min",
end="2026-07-08 16:55", drift=drift, seed=7),
Timeframe.M15: make_xau_bars(n=300, freq="15min",
end="2026-07-08 16:45", drift=drift, seed=8),
Timeframe.H1: make_xau_bars(n=300, freq="60min",
end="2026-07-08 16:00", drift=drift, seed=9),
}
@test("replay record: snapshot reproduces the engine's weights and composite on a real report")
def _():
cfg, _agg, report, setup = _m33_report()
snap = _m33_snapshot()
assert [b["role"] for b in snap["tfs"]] == ["primary", "c1", "c2"], \
[b["role"] for b in snap["tfs"]]
assert [b["tf"] for b in snap["tfs"]] == ["5min", "15min", "1h"], \
[b["tf"] for b in snap["tfs"]]
table = regime_weight_table(cfg.technical)
analyses = [report.primary_analysis] + list(report.confirmation_analyses)
for block, analysis in zip(snap["tfs"], analyses):
# The stored set is exactly the composite's voting set.
voting = {name for name, ind in analysis.indicators.items()
if not name.startswith("_") and ind.weight != 0}
assert set(block["indicators"]) == voting, \
(block["role"], sorted(block["indicators"]), sorted(voting))
mults = table.get(analysis.market_regime, {})
for name, stored in block["indicators"].items():
ind = analysis.indicators[name]
expect_eff = stored["base_weight"] * mults.get(name, 1.0)
assert abs(stored["eff_weight"] - expect_eff) < 1e-9, \
(name, stored["eff_weight"], expect_eff)
expect_ws = (stored["signal"] * stored["eff_weight"]
* stored["confidence"] / 100.0)
assert abs(stored["wscore"] - expect_ws) < 1e-9, \
(name, stored["wscore"], expect_ws)
assert abs(stored["regime_mult"] * stored["base_weight"]
- stored["eff_weight"]) < 1e-9, name
assert stored["signal"] == ind.signal.value
# Σ wscore / Σ eff_weight reproduces the composite exactly.
effs = [v["eff_weight"] for v in block["indicators"].values()]
wscores = [v["wscore"] for v in block["indicators"].values()]
score, sig = _rr.composite_from_rows(effs, wscores)
assert abs(score - analysis.composite_score) < 1e-9, \
(block["role"], score, analysis.composite_score)
assert sig == analysis.composite_signal.value, \
(block["role"], sig, analysis.composite_signal.value)
# Plain data only: it must survive JSON and the worker pickle boundary.
json.dumps(snap)
assert _pickle33.loads(_pickle33.dumps(snap)) == snap
# bar_ts is the bar LABEL (what signal_log stores), not wall clock.
assert snap["bar_ts"] == str(report.primary_analysis.timestamp), \
(snap["bar_ts"], report.primary_analysis.timestamp)
assert snap["setup"]["strategy"] == "XAU_ATR"
@test("replay record: an indicator that did not fire is no vote, not a zero")
def _():
# Divergence and candlestick only exist when they fire. A zero row
# would read as a NEUTRAL vote and drag every study's average.
analysis = _m33_hand_analysis()
report = _m33_duck_report(analysis)
setup = _types33.SimpleNamespace(
direction="BUY", strategy="XAU_ATR", setup_pattern=None,
round_trip_cost=0.30, trail_arm_r=0.6, trail_dist_r=0.8,
entry_price=3300.0, stop_loss=3294.0, take_profit_1=3312.0,
take_profit_2=3320.0, risk_reward=2.0)
snap = _rr.snapshot_report(
report, setup, pd.Timestamp("2026-07-08 17:00", tz="UTC"),
Timeframe.M5, [Timeframe.M15, Timeframe.H1])
assert set(snap["tfs"][0]["indicators"]) == {
"ema_trend", "rsi", "macd", "keltner"}, sorted(snap["tfs"][0]["indicators"])
trade = _m29_trade()
trade.snapshot = snap
ind_df = _rr.build_indicators_table([trade])
got = set(ind_df["indicator"])
assert got == {"ema_trend", "rsi", "macd", "keltner"}, sorted(got)
for absent in ("candlestick", "divergence", "atr"):
assert absent not in got, f"{absent} must have NO row at all"
# The per-bar float32 row: absent indicator / absent TF stays NaN.
buf = _rr.new_bar_buffer()
_rr.add_bar_row(buf, 5, pd.Timestamp("2026-07-08 16:55", tz="UTC"),
report, Timeframe.M5, [Timeframe.M15, Timeframe.H1])
mat, ts_ns, pos, warn0 = _rr.finish_bar_rows(buf)
bars = pd.DataFrame(mat, columns=list(_rr.BAR_COLS))
cs_cols = [c for c in _rr.BAR_COLS if c.startswith("primary_candlestick_")]
assert len(cs_cols) == 5, cs_cols
assert bars[cs_cols].isna().all(axis=None), bars[cs_cols].iloc[0].tolist()
conf_cols = [c for c in _rr.BAR_COLS
if c.startswith("c1_") or c.startswith("c2_")]
assert bars[conf_cols].isna().all(axis=None), \
"a missing confirmation block must be NaN, never 0"
assert warn0 == ["a warning"] and list(pos) == [5]
effs = [v["eff_weight"] for v in snap["tfs"][0]["indicators"].values()]
wscores = [v["wscore"] for v in snap["tfs"][0]["indicators"].values()]
score, sig = _rr.composite_from_rows(effs, wscores)
assert abs(score - analysis.composite_score) < 1e-9, \
(score, analysis.composite_score)
assert sig == analysis.composite_signal.value
@test("replay record: Trade.snapshot defaults empty, is not shared, and pickles")
def _():
t1, t2 = _m29_trade(), _m29_trade()
assert t1.snapshot == {}, t1.snapshot
assert t2.snapshot == {}, t2.snapshot
assert t1.snapshot is not t2.snapshot, "mutable default trap"
t1.snapshot = {"tfs": [{"role": "primary", "indicators": {"rsi": {"wscore": 1.0}}}]}
back = _pickle33.loads(_pickle33.dumps(t1))
assert back.snapshot == t1.snapshot, back.snapshot
assert t2.snapshot == {}, "a snapshot on one trade must not reach another"
@test("replay record: trades_to_dataframe ignores the snapshot field (63 columns, identical frame)")
def _():
# The pinned books must stay byte-identical with recording on.
plain = _m29_trade(r_multiple=1.5)
recorded = _m29_trade(r_multiple=1.5)
recorded.snapshot = _m33_snapshot()
off = _gb33.trades_to_dataframe([plain])
on = _gb33.trades_to_dataframe([recorded])
assert len(off.columns) == 63, len(off.columns)
assert "snapshot" not in off.columns and "snapshot" not in on.columns
pd.testing.assert_frame_equal(off, on)
@test("replay record: composite thresholds match the engine")
def _():
# One indicator, base weight 1.0, regime "unknown" (no multiplier
# table entry -> x1.0), so composite = signal x confidence / 2.
tech = TechnicalConfig()
for target in (39.99, 40.0, 14.99, 15.0, -14.99, -15.0, -39.99, -40.0, 0.0):
signal = SignalStrength.BUY if target >= 0 else SignalStrength.SELL
result = TechnicalAnalysis(
symbol=XAUUSD, timeframe=Timeframe.M5.value, timestamp="t",
indicators={"rsi": IndicatorSignal(
"RSI", 50.0, signal, abs(target) * 2.0, "", 1.0)},
market_regime="unknown")
TechnicalEngine(tech)._compute_composite(result)
assert abs(result.composite_score - target) < 1e-9, \
(target, result.composite_score)
ind = result.indicators["rsi"]
assert abs(ind.effective_weight - 1.0) < 1e-9, ind.effective_weight
score, sig = _rr.composite_from_rows(
[ind.effective_weight], [ind.weighted_score])
assert abs(score - result.composite_score) < 1e-9, (score, target)
assert sig == result.composite_signal.value, \
(target, sig, result.composite_signal.value)
# Both sides of both thresholds really were exercised.
assert _rr.COMPOSITE_THRESHOLDS == {"strong": 40.0, "weak": 15.0}
@test("replay record: calendar join — window edges, phases, all-day, holiday, offsets")
def _():
entry = pd.Timestamp("2026-09-07 13:00", tz="UTC") # Monday
exit_ts = entry + pd.Timedelta(hours=1)
# Window = [entry-4h, max(exit, entry+4h)] = [09:00, 17:00], inclusive.
rows = {
"A": _m33_cal_row(1, entry - pd.Timedelta(hours=4), "A prev release"),
"B": _m33_cal_row(2, entry - pd.Timedelta(hours=4, seconds=60),
"B outside window"),
"C": _m33_cal_row(3, entry + pd.Timedelta(minutes=30), "C in hold"),
# medium impact: in `events` as after_exit, but never counted high.
"D": _m33_cal_row(4, exit_ts + pd.Timedelta(hours=2), "D after exit",
impact="medium"),
"E": _m33_cal_row(5, entry + pd.Timedelta(hours=25), "E far ahead"),
"F": _m33_cal_row(6, entry + pd.Timedelta(minutes=45), "F eur low",
currency="EUR", impact="low"),
# All-day US holiday: its dateline is 2026-09-08 00:30 UTC =
# 2026-09-07 20:30 ET, so it joins on the entry's ET date.
"G": _m33_cal_row(7, pd.Timestamp("2026-09-08 00:30", tz="UTC"),
"Bank Holiday", impact="holiday", masked=True,
label="All Day"),
# A TIMED holiday row is not an all-day closure and must not
# flag us_holiday for a trade entered that day.
"H": _m33_cal_row(8, pd.Timestamp("2026-11-02 14:00", tz="UTC"),
"Daylight Saving Time Shift", impact="holiday"),
"I": _m33_cal_row(9, entry + pd.Timedelta(minutes=20), "I imminent"),
}
trades_df = pd.DataFrame({
"trade_id": [0, 1],
"entry_time": pd.to_datetime(
[entry, pd.Timestamp("2026-11-02 12:00", tz="UTC")], utc=True),
# Trade 1 is still open: exit NaT must not raise.
"exit_time": pd.to_datetime([exit_ts, pd.NaT], utc=True),
})
with tempfile.TemporaryDirectory() as d:
full_path = os.path.join(d, "cal_full.parquet")
pd.DataFrame([rows[k] for k in "ABCDEFGHI"]).to_parquet(
full_path, index=False)
# Same calendar minus every USD-high release inside 24h AFTER the
# entry, so the "no next release" branch is reachable.
quiet_path = os.path.join(d, "cal_quiet.parquet")
pd.DataFrame([rows[k] for k in "ABDEFGH"]).to_parquet(
quiet_path, index=False)
cal = _rr.load_calendar(full_path)
events = _rr.build_events_table(trades_df, cal)
assert str(events["ts_utc"].dtype) == "datetime64[ns, UTC]", \
events["ts_utc"].dtype
t0 = events[events["trade_id"] == 0]
phases = dict(zip(t0["name"], t0["phase"]))
assert phases == {
"A prev release": "before", "I imminent": "during_hold",
"C in hold": "during_hold", "F eur low": "during_hold",
"D after exit": "after_exit", "Bank Holiday": "all_day",
}, phases
assert "B outside window" not in phases, "entry-4h-60s is outside"
assert "E far ahead" not in phases, "entry+25h is outside"
offsets = dict(zip(t0["name"], t0["offset_min"]))
assert offsets["A prev release"] == -240.0, offsets["A prev release"]
assert offsets["I imminent"] == 20.0
assert offsets["D after exit"] == 180.0
assert np.isnan(offsets["Bank Holiday"]), "all-day rows have no offset"
timed = dict(zip(t0["name"], t0["timed"]))
assert timed["Bank Holiday"] is np.False_ or timed["Bank Holiday"] == False
assert timed["A prev release"] == True
scal = _rr.calendar_scalars(trades_df, cal)
assert scal["ev_prev_hi_usd_min"].iloc[0] == 240.0
assert scal["ev_prev_hi_usd_name"].iloc[0] == "A prev release"
assert scal["ev_next_hi_usd_min"].iloc[0] == 20.0
assert scal["ev_next_hi_usd_name"].iloc[0] == "I imminent"
assert bool(scal["ev_entry_within_30m_hi_usd"].iloc[0]) is True
# I and C are USD-high inside the hold; F (EUR low) and D
# (USD medium) are not.
assert scal["ev_hi_usd_during_hold"].iloc[0] == 2, \
scal["ev_hi_usd_during_hold"].iloc[0]
assert scal["ev_hi_usd_during_hold_names"].iloc[0] == \
"I imminent|C in hold"
assert scal["ev_any_hi_during_hold"].iloc[0] == 2
assert bool(scal["us_holiday"].iloc[0]) is True, \
"the all-day USD holiday shares the entry's ET date"
# Open trade: no exit -> no hold window, and no crash.
assert scal["ev_hi_usd_during_hold"].iloc[1] == 0
assert bool(scal["us_holiday"].iloc[1]) is False, \
"a TIMED holiday row must not flag us_holiday"
# Quiet calendar: the nearest USD-high after entry is 25h out,
# beyond NEAR_RELEASE_MAX_MIN, so both branches read empty.
quiet = _rr.load_calendar(quiet_path)
scal_q = _rr.calendar_scalars(trades_df, quiet)
assert scal_q["ev_prev_hi_usd_min"].iloc[0] == 240.0
assert np.isnan(scal_q["ev_next_hi_usd_min"].iloc[0]), \
scal_q["ev_next_hi_usd_min"].iloc[0]
assert scal_q["ev_next_hi_usd_name"].iloc[0] == ""
assert bool(scal_q["ev_entry_within_30m_hi_usd"].iloc[0]) is False
assert scal_q["ev_hi_usd_during_hold"].iloc[0] == 0
@test("replay record: write_run + load_run round-trip with a full manifest")
def _():
cfg, agg, _report, _setup = _m33_report()
snap = _m33_snapshot()
trades = [_m29_trade(r_multiple=1.5), _m29_trade(r_multiple=-1.0)]
for t in trades:
t.snapshot = copy.deepcopy(snap)
book_df = _gb33.trades_to_dataframe(trades)
assert len(book_df.columns) == 63, len(book_df.columns)
run_info = dict(first_pos=250, last_pos=400, n_primary_bars=400,
n_bars_in_window=150, skipped={"SESSION_WINDOW": 10},
n_candidates=3, workers=1, backend="NumPy CPU fallback",
cache_files={"5min": "x.parquet"}, replay_seconds=0.1)
with tempfile.TemporaryDirectory() as d:
# NEVER read backtest/cache/calendar.parquet from a test.
cal_path = os.path.join(d, "cal.parquet")
pd.DataFrame([_m33_cal_row(
1, pd.Timestamp("2026-01-05 13:30", tz="UTC"),
"Non-Farm Employment Change")]).to_parquet(cal_path, index=False)
rec_dir = os.path.join(d, "run")
bt_cfg = _gb33.BacktestConfig(
pair=XAUUSD, start=datetime(2026, 7, 1), end=datetime(2026, 7, 8),
primary_tf=Timeframe.M5,
confirmation_tfs=[Timeframe.M15, Timeframe.H1],
spread=0.65, record_dir=rec_dir)
saved_cal = _rr.CALENDAR_PARQUET
try:
_rr.CALENDAR_PARQUET = cal_path
_rr.write_run(rec_dir, trades=trades, book_df=book_df,
bt_cfg=bt_cfg, platform_cfg=cfg, aggregator=agg,
run_info=run_info)
finally:
_rr.CALENDAR_PARQUET = saved_cal
run = _rr.load_run(rec_dir)
man = run["manifest"]
assert man["engine_fingerprint"] == agg.engine_config_fingerprint()
assert man["gate_fingerprint"] == agg.gate_config_fingerprint()
assert man["regime_weight_table"] == regime_weight_table(cfg.technical)
assert man["base_weights_observed"], "base weights must be recorded"
for name, seen in man["base_weights_observed"].items():
assert set(seen) <= {0.5, 1.0, 1.5}, (name, seen)
assert man["calendar"]["md5"] == _hashlib33.md5(
open(cal_path, "rb").read()).hexdigest()
expect_md5 = _hashlib33.md5(
book_df.to_csv(index=False).encode()).hexdigest()
assert man["book_csv_md5"] == expect_md5, man["book_csv_md5"]
csv_path = os.path.join(d, "book.csv")
book_df.to_csv(csv_path, index=False)
assert expect_md5 == _hashlib33.md5(
open(csv_path, "rb").read()).hexdigest(), \
"the manifest md5 must match the exported CSV byte-for-byte"
assert man["n_trades"] == 2, man["n_trades"]
assert man["spread"] == 0.65
assert man["tables"]["bars"] is None, "no --record-bars in this run"
trades_tbl = run["trades"]
assert trades_tbl.columns[0] == "trade_id", trades_tbl.columns[0]
cols = list(trades_tbl.columns)
book_cols = list(book_df.columns)
start = cols.index(book_cols[0])
assert cols[start:start + len(book_cols)] == book_cols, \
"the 63 book columns must stay a contiguous, ordered slice"
assert str(run["indicators"]["signal"].dtype) == "int8", \
run["indicators"]["signal"].dtype
assert run["indicators"]["signal"].dtype == np.int8
assert str(trades_tbl["entry_time"].dtype) == "datetime64[ns, UTC]", \
trades_tbl["entry_time"].dtype
assert isinstance(run["events"], pd.DataFrame), type(run["events"])
assert run["bars"] is None, "bars.parquet must not exist"
@test("replay record: _replay_span with --record returns the same candidates plus snapshots")
def _():
from pair_signals.xauusd import make_config as _mk33
cfg = _mk33(Timeframe.M5, [Timeframe.M15, Timeframe.H1])
dfs_full = _m33_span_frames()
caches = {tf.value: _gb33.precompute_indicators(df, cfg.technical)
for tf, df in dfs_full.items()}
agg = SignalAggregator(cfg)
agg.tech_engine = _gb33.GPUTechnicalEngine(cfg.technical, caches)
bt_off = _gb33.BacktestConfig(
pair=XAUUSD, start=datetime(2026, 7, 6), end=datetime(2026, 7, 9),
primary_tf=Timeframe.M5,
confirmation_tfs=[Timeframe.M15, Timeframe.H1])
bt_on = _dc33.replace(bt_off, record_dir="unused", record_bars=True)
span = (300, 600)
try:
_gb33._REPLAY_CTX.update(
primary_df=dfs_full[Timeframe.M5], dfs_full=dfs_full,
timeframes=[Timeframe.M5, Timeframe.M15, Timeframe.H1],
aggregator=agg, platform_cfg=cfg, bt_cfg=bt_off,
primary_dur=pd.Timedelta(minutes=5))
skip_off, cands_off, bars_off = _gb33._replay_span(span)
_gb33._REPLAY_CTX["bt_cfg"] = bt_on
skip_on, cands_on, bars_on = _gb33._replay_span(span)
finally:
_gb33._REPLAY_CTX.clear()
assert len(cands_off) > 0, "the fixture must produce candidates"
assert skip_off == skip_on, (skip_off, skip_on)
assert [p for p, _ in cands_off] == [p for p, _ in cands_on], \
"recording must not move a single entry"
for (_p_off, t_off), (_p_on, t_on) in zip(cands_off, cands_on):
d_off = _dc33.asdict(t_off)
d_on = _dc33.asdict(t_on)
d_off.pop("snapshot")
snap = d_on.pop("snapshot")
differing = [k for k in d_off if d_off[k] != d_on[k]]
assert not differing, differing
assert t_off.snapshot == {}, "recording off must leave the field empty"
assert snap and len(snap["tfs"]) == 3, \
"a recorded trade carries primary + both confirmation blocks"
assert bars_off is None, "no bar chunk without --record-bars"
mat, ts_ns, pos, warn0 = bars_on
assert mat.dtype == np.float32, mat.dtype
assert mat.shape[1] == len(_rr.BAR_COLS), mat.shape
n_analyzed = (span[1] - span[0]) - sum(skip_off.values())
assert mat.shape[0] == n_analyzed, (mat.shape[0], n_analyzed)
assert len(cands_on) <= mat.shape[0] <= (span[1] - span[0])
assert len(ts_ns) == len(pos) == len(warn0) == mat.shape[0]
@test("replay record: forward moves on a hand-built series")
def _():
close = np.array([10.0, 11.0, 13.0, 12.0, 15.0, 14.0])
high, low = close + 1.0, close - 1.0
atr = np.array([1.0, 1.0, 1.0, 1.0, 1.0, 0.0])
mv = _rr.forward_moves(close, high, low, atr, 2)
assert mv["fwd"][0] == 3.0, mv["fwd"][0]
assert mv["mfe"][0] == 4.0, mv["mfe"][0] # max(11,13)+1 - 10
assert mv["mae"][0] == 0.0, mv["mae"][0] # 10 - (min(11,13)-1)
assert mv["fwd"][3] == 2.0, mv["fwd"][3]
for i in (4, 5):
for key in ("fwd", "mfe", "mae"):
assert np.isnan(mv[key][i]), (key, i, mv[key][i])
# A zero ATR blanks its own row even with a full horizon ahead.
mv2 = _rr.forward_moves(close, high, low,
np.array([1.0, 0.0, 1.0, 1.0, 1.0, 1.0]), 2)
assert np.isnan(mv2["fwd"][1]) and np.isnan(mv2["mfe"][1]) \
and np.isnan(mv2["mae"][1]), mv2["fwd"][1]
assert mv2["fwd"][0] == 3.0 and mv2["fwd"][2] == 2.0
@test("replay record: --record/--record-bars parse, default None/False, bars need record")
def _():
args = _gb33._parse_args(["--pair", "XAUUSD"])
assert args.record is None, args.record
assert args.record_bars is False, args.record_bars
assert _gb33._parse_args(
["--pair", "XAUUSD", "--record", "/tmp/x"]).record == "/tmp/x"
try:
with open(os.devnull, "w") as _null, _ctx33.redirect_stderr(_null):
_gb33._parse_args(["--pair", "XAUUSD", "--record-bars"])
raise AssertionError("--record-bars without --record must exit")
except SystemExit:
pass
bare = _gb33.BacktestConfig(pair=XAUUSD, start=datetime(2026, 7, 1),
end=datetime(2026, 7, 8))
assert bare.record_dir is None and bare.record_bars is False
@test("replay record: replay_record imports none of the live writers or the backtester")
def _():
# The recorder runs inside replay workers and must stay a leaf: no
# live writer, no news, no provider, and no import cycle back into
# the backtester.
banned = {"signal_log", "news_engine", "providers", "telegram_bot",
"news_archive", "agent_panel", "violent_watch",
"market_outlook", "gpu_backtest", "sklearn", "scipy",
"lightgbm", "torch"}
path = Path("backtest/replay_record.py")
tree = ast.parse(path.read_text(encoding="utf-8"), str(path))
seen, offenders = set(), []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
names = [a.name for a in node.names]
elif isinstance(node, ast.ImportFrom):
names = [node.module or ""]
else:
continue
for name in names:
root = name.split(".")[0]
seen.add(root)
if root in banned:
offenders.append(f"{path}:{node.lineno} {name}")
assert not offenders, f"replay_record must stay a leaf module: {offenders}"
# ...and the guard is not vacuous — it really does import these two.
assert "engine" in seen and "config" in seen, sorted(seen)
@test("replay record: the backtester still satisfies its source guards with the recorder wired in")
def _():
src = open(_gb33.__file__).read()
for banned in ("import signal_log", "scan_outcomes", "label_scan_outcomes",
"update_watch", "log_watch_update", "NewsEngine(",
"news_archive", "import violent_watch", "from violent_watch"):
assert banned not in src, f"backtests must stay write-free: {banned}"
for required in ("use_news=False",
"_apply_regime_filter_live(report, slice_dfs)",
"from backtest import replay_record", "snapshot=snap"):
assert required in src, f"missing: {required}"
assert "_compute_composite" not in _gb33.GPUTechnicalEngine.__dict__, \
"the GPU engine must inherit the live composite, never shadow it"
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════