|
4 | 4 | import json |
5 | 5 | from pathlib import Path |
6 | 6 |
|
7 | | -from rlm.v100.backtesting import bars_from_source, simulate |
| 7 | +from rlm.v100.backtesting import bars_from_source, simulate, source_quality |
8 | 8 | from rlm.v100.paper import sha, timestamp |
9 | 9 |
|
10 | 10 |
|
11 | 11 | def verified(path: Path) -> dict: |
| 12 | + if path.is_symlink() or path.stat().st_size > 2 * 2**20: |
| 13 | + raise ValueError("Unsafe or oversized historical report") |
12 | 14 | report = json.loads(path.read_text()) |
13 | 15 | bodies = [] |
14 | 16 | for index, source in enumerate(report["sources"]): |
15 | | - body = (path.parent / f"source-{index}.json").read_text() |
| 17 | + archive = path.parent / f"source-{index}.json" |
| 18 | + if archive.is_symlink() or archive.stat().st_size > 2 * 2**20: |
| 19 | + raise ValueError("Unsafe or oversized archived source") |
| 20 | + body = archive.read_text() |
16 | 21 | if hashlib.sha256(body.encode()).hexdigest() != source["sha256"]: |
17 | 22 | raise ValueError("Backtest archived source changed") |
18 | 23 | bodies.append(body) |
19 | 24 | bars = bars_from_source(report["sources"][0]["url"], bodies[0], timestamp(report["fetched_at"])) |
| 25 | + quality = source_quality(bars) |
| 26 | + if quality["gaps"] or quality["large_opening_jumps"]: |
| 27 | + raise ValueError("Archived bars require source review; excluded from postmortem training") |
20 | 28 | events = None |
21 | 29 | if len(bodies) > 1: |
22 | 30 | events = [] |
@@ -65,19 +73,36 @@ def verified(path: Path) -> dict: |
65 | 73 | bars, split, len(bars), "buy_hold", chosen, arguments["fee_bps"], arguments["slippage_bps"] |
66 | 74 | ) |
67 | 75 | if ( |
68 | | - report["selected_lookback_on_training_only"] != chosen |
| 76 | + report["bars"] != len(bars) |
| 77 | + or report["split_index"] != split |
| 78 | + or report["selected_lookback_on_training_only"] != chosen |
69 | 79 | or report["development_test"] != test |
70 | 80 | or report.get("double_cost_stress") != stress |
71 | 81 | or report.get("buy_hold_baseline") != baseline |
72 | 82 | ): |
73 | 83 | raise ValueError("Historical report differs from independently rerun simulator") |
74 | 84 | outcome = test["net_return"] |
| 85 | + for value in (test, stress, baseline): |
| 86 | + peak, drawdown = 1.0, 0.0 |
| 87 | + for point in value["trace"]: |
| 88 | + equity = point["marked_equity"] |
| 89 | + if point["signal_available_at"] > point["fill_at"]: |
| 90 | + raise ValueError("Future information in historical trace") |
| 91 | + peak = max(peak, equity) |
| 92 | + drawdown = max(drawdown, 1 - equity / peak) |
| 93 | + if ( |
| 94 | + abs(value["net_return"] - (value["trace"][-1]["marked_equity"] - 1)) > 1e-10 |
| 95 | + or abs(value["max_sampled_drawdown"] - drawdown) > 1e-10 |
| 96 | + ): |
| 97 | + raise ValueError("Historical equity trace does not reproduce reported metrics") |
75 | 98 | label = { |
76 | 99 | "sample_result": "profit" if outcome > 0 else "loss" if outcome < 0 else "flat", |
77 | 100 | "beats_cash_in_sample": outcome > 0, |
78 | 101 | "beats_buy_hold_in_sample": outcome > baseline["net_return"], |
79 | 102 | "positive_at_double_assumed_cost": stress["net_return"] > 0, |
80 | 103 | "repeatable_income_established": False, |
| 104 | + "drawdown_is_standard_deviation": False, |
| 105 | + "negative_return_proves_data_corruption": False, |
81 | 106 | "next_evidence": "independent forward paper observations with documented costs", |
82 | 107 | } |
83 | 108 |
|
@@ -111,7 +136,7 @@ def records(root: Path) -> list[dict]: |
111 | 136 | for path in sorted((root / "research/backtests").glob("run-*/report.json")): |
112 | 137 | try: |
113 | 138 | row = verified(path) |
114 | | - except (ValueError, KeyError, OSError, TypeError): |
| 139 | + except (ValueError, KeyError, OSError, TypeError, IndexError, ZeroDivisionError): |
115 | 140 | continue # Older reports without complete artifacts are not training evidence. |
116 | 141 | result.append(row) |
117 | 142 | return result |
0 commit comments