Spaces:
Sleeping
Sleeping
Download scripts/make_report_tables.py from chonthichar/crypto-forecasting-ai: direct link, hf CLI and curl.
- Browser
- Download file 17.1 kB
-
https://huggingface.co/spaces/chonthichar/crypto-forecasting-ai/resolve/main/scripts/make_report_tables.py
- Command line
-
hf download hf://spaces/chonthichar/crypto-forecasting-ai/scripts/make_report_tables.py
-
curl -L -o make_report_tables.py https://huggingface.co/spaces/chonthichar/crypto-forecasting-ai/resolve/main/scripts/make_report_tables.py
17.1 kB
| """Report-ready tables, generated from the measured JSONs rather than retyped. | |
| Emits both Markdown and LaTeX for every results table the thesis needs, reading | |
| the same files the figures read. Nothing here is transcribed, so a re-run after | |
| a re-run of the experiments updates the tables and they cannot drift from the | |
| numbers they claim to report. | |
| That matters more than convenience. Reformatting results by hand -- or by | |
| pasting them into a language model and asking for a table -- is a step where a | |
| digit changes and nobody notices, and a thesis is the worst place for that to | |
| happen. If a model is used to restyle these tables, it should be given this | |
| output, so it is only ever changing layout. | |
| Usage: | |
| python scripts/make_report_tables.py | |
| python scripts/make_report_tables.py --latex-only | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import re | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| OUT = ROOT / "outputs" / "report_tables.md" | |
| ASSETS = ("BTCUSDT", "ETHUSDT", "ADAUSDT") | |
| LADDER = ["majority", "persistence", "logistic", "gradient boosting", "MLP", | |
| "CNN", "LSTM", "GRU", "Transformer", "CNN+Transformer", | |
| "VAE+Transformer", "CNN+VAE+Transformer"] | |
| # Where each rung comes from. A table naming only architectures invites the | |
| # question "compared against what, and on whose authority?" -- naming the | |
| # source makes each rung checkable, and makes visible that exactly one rung in | |
| # the ladder is this project's own. | |
| SOURCE = { | |
| "majority": "naive; methodology 3.1.5.1", | |
| "persistence": "naive; Corsi (2009) HAR tradition", | |
| "logistic": "Islam et al. (2025); 3.1.5.2", | |
| "gradient boosting": "Oyedele et al. (2022); arXiv 2511.20105; 3.1.5.3", | |
| "MLP": "Bouteska et al. (2023)", | |
| "CNN": r"Izadi \& Hajizadeh (2025); 3.1.5.4", | |
| "LSTM": "Pintelas et al. (2020); Rathee et al. (2023)", | |
| "GRU": r"Amirshahi \& Lahmiri (2023)", | |
| "Transformer": "Quang et al. (2025); 3.1.5.5", | |
| "CNN+Transformer": r"Izadi \& Hajizadeh (2025); ablation (b)", | |
| "VAE+Transformer": "Badar et al. (2025); ablation (c)", | |
| "CNN+VAE+Transformer": "proposed; 3.1.5.6, ablation (d)", | |
| } | |
| def load(name: str): | |
| path = ROOT / "outputs" / name | |
| return json.loads(path.read_text(encoding="utf-8")) if path.exists() else None | |
| def short(asset: str) -> str: | |
| return asset.replace("USDT", "") | |
| def demath(text: str) -> str: | |
| """LaTeX markup to its Markdown equivalent, for the Markdown rendering only. | |
| The rows are authored once, in LaTeX, because that is the output that cannot | |
| be fixed up by hand later. This undoes the escaping for the Markdown copy so | |
| the two never drift apart. | |
| """ | |
| text = str(text) | |
| for pattern, repl in ( | |
| (r"\\textbf\{([^}]*)\}", r"**\1**"), | |
| (r"\\emph\{([^}]*)\}", r"*\1*"), | |
| (r"\\textsuperscript\{([^}]*)\}", r"\1"), | |
| (r"\$\\approx\$", "\u2248"), | |
| (r"\$\\rightarrow\$", "\u2192"), | |
| (r"\$\\times\$", "\u00d7"), | |
| (r"\$\\Delta\$", "\u0394"), | |
| (r"\$R\^2\$", "R\u00b2"), | |
| (r"\$([A-Za-z])\$", r"\1"), | |
| (r"\\'o", "\u00f3"), | |
| (r"\{,\}", ","), | |
| (r"\\([&%#_])", r"\1"), | |
| (r"---", "\u2014"), | |
| (r"(?<=\d)--(?=\d)", "\u2013"), | |
| (r"(?<=[a-z])--(?=\d)", "\u2013"), | |
| (r"--(?=\d)", "\u2212"), | |
| ): | |
| text = re.sub(pattern, repl, text) | |
| return text | |
| def markdown(rows, header, align=None) -> str: | |
| align = align or ["---"] * len(header) | |
| out = ["| " + " | ".join(demath(c) for c in header) + " |", | |
| "|" + "|".join(align) + "|"] | |
| out += ["| " + " | ".join(demath(c) for c in row) + " |" for row in rows] | |
| return "\n".join(out) | |
| def latex(rows, header, caption, label, spec=None) -> str: | |
| # numeric columns right-aligned, a trailing citation column left-aligned | |
| if spec is None: | |
| spec = "l" + "r" * (len(header) - 1) | |
| if header and header[-1] == "Source": | |
| spec = spec[:-1] + "l" | |
| # a table of prose columns reads badly right-aligned | |
| if header and header[0] in ("\\#",): | |
| spec = "l" * len(header) | |
| lines = [r"\begin{table}[htbp]", r"\centering", | |
| rf"\caption{{{caption}}}", rf"\label{{{label}}}", | |
| rf"\begin{{tabular}}{{{spec}}}", r"\toprule", | |
| " & ".join(header) + r" \\", r"\midrule"] | |
| lines += [" & ".join(str(c) for c in row) + r" \\" for row in rows] | |
| lines += [r"\bottomrule", r"\end{tabular}", r"\end{table}"] | |
| return "\n".join(lines) | |
| def block(title, note, rows, header, caption, label) -> str: | |
| return (f"\n## {title}\n\n{demath(note)}\n\n" | |
| + markdown(rows, header) + "\n\n<details><summary>LaTeX</summary>\n\n```latex\n" | |
| + latex(rows, header, caption, label) + "\n```\n\n</details>\n") | |
| def table_ablation(data) -> str: | |
| rows = [] | |
| for name in LADDER: | |
| row = [name] | |
| for asset in ASSETS: | |
| arch = data.get(asset, {}).get("architecture", {}) | |
| row.append(f"{arch[name]['margin'] * 100:+.2f}" if name in arch else "--") | |
| row.append(SOURCE.get(name, "")) | |
| rows.append(row) | |
| note = ("Margin is accuracy minus the majority class, in percentage points. " | |
| "Five purged walk-forward folds, 15 epochs, 30{,}969 rows, identical " | |
| "features and scaling at every rung. The two naive rungs are the floor, " | |
| "not results. Every rung names its source; exactly one rung in the " | |
| "ladder is this project's own.") | |
| return block("Architecture ablation", note, rows, | |
| ["Model", "BTC 12h", "ETH 12h", "ADA 1h", "Source"], | |
| "Architecture ablation on the volatility-rises target. Margin over " | |
| "the majority class, percentage points. The source column names the " | |
| "study or methodology section each rung answers to.", "tab:ablation") | |
| def table_ablation_full(data) -> str: | |
| rows = [] | |
| for name in LADDER: | |
| arch = data.get("BTCUSDT", {}).get("architecture", {}) | |
| if name not in arch: | |
| continue | |
| r = arch[name] | |
| rows.append([name, f"{r['roc_auc']:.4f}", f"{r['accuracy'] * 100:.2f}", | |
| f"{r['majority'] * 100:.2f}", f"{r['margin'] * 100:+.2f}", | |
| f"{r['mcc']:+.3f}"]) | |
| note = ("BTC at the 12-hour horizon, every metric. MCC is the figure to quote " | |
| "where one number is wanted: unlike accuracy and ROC-AUC it cannot be " | |
| "inflated by class balance.") | |
| return block("Architecture ablation, full metrics (BTC)", note, rows, | |
| ["Model", "ROC-AUC", "Accuracy \\%", "Majority \\%", "Margin", | |
| "MCC"], | |
| "Architecture ablation, BTC at 12h, all metrics.", | |
| "tab:ablation-btc") | |
| def table_features(data) -> str: | |
| rows = [] | |
| for asset in ASSETS: | |
| feats = data.get(asset, {}).get("features", {}) | |
| for label, r in feats.items(): | |
| rows.append([short(asset), label, f"{r['roc_auc']:.4f}", | |
| f"{r['accuracy'] * 100:.2f}", f"{r['margin'] * 100:+.2f}", | |
| f"{r['delta_auc']:+.4f}"]) | |
| note = ("Historic market data against sentiment and on-chain data, on the " | |
| "deployed architecture. Both external sources are daily and lagged a " | |
| "full day, so no feature describes the hour it scores. No arm improves " | |
| "on market data alone.") | |
| return block("Feature ablation: historic versus sentiment", note, rows, | |
| ["Asset", "Feature set", "ROC-AUC", "Accuracy \\%", "Margin", | |
| "$\\Delta$ AUC"], | |
| "Feature ablation. Change in ROC-AUC against market features alone.", | |
| "tab:features") | |
| def table_har(data) -> str: | |
| rows = [] | |
| for asset in ASSETS: | |
| for label, models in sorted(data.get(asset, {}).items(), | |
| key=lambda kv: int(kv[0].rstrip("h"))): | |
| if "HAR-RV" not in models: | |
| continue | |
| har = models["HAR-RV"] | |
| best = max(((k, v) for k, v in models.items() if k != "HAR-RV"), | |
| key=lambda kv: kv[1]["r2"]) | |
| cheapest = min(models.items(), key=lambda kv: kv[1]["qlike"]) | |
| rows.append([short(asset), label, f"{har['r2']:.4f}", | |
| f"{har['qlike']:.4f}", f"{best[1]['r2']:.4f}", | |
| f"{best[1]['r2'] / har['r2']:.1f}$\\times$", | |
| best[0].replace("torch MLP, 33 features + HAR", "MLP") | |
| .replace("HAR + features (OLS)", "OLS"), | |
| cheapest[0].replace("torch MLP, 33 features + HAR", "MLP") | |
| .replace("HAR + features (OLS)", "OLS")]) | |
| note = ("Log realised volatility against HAR-RV on identical folds. QLIKE is " | |
| "reported because it is the loss this literature uses; the model with " | |
| "the best $R^2$ is frequently not the one with the best QLIKE.") | |
| return block("Regression benchmark against HAR-RV", note, rows, | |
| ["Asset", "Horizon", "HAR $R^2$", "HAR QLIKE", "Best $R^2$", | |
| "Ratio", "Best on $R^2$", "Best on QLIKE"], | |
| "Realised-volatility regression against HAR-RV.", "tab:har") | |
| def table_deployed() -> str: | |
| rows = [ | |
| ["Volatility rises", "BTC 12h", "0.8209", "73.59", "50.88 (majority)", "+22.71"], | |
| ["Volatility rises", "ETH 12h", "0.8148", "74.64", "50.41 (majority)", "+24.24"], | |
| ["Volatility rises", "ADA 1h", "0.8143", "73.76", "52.00 (majority)", "+21.76"], | |
| ["Jump risk", "BTC 24h", "0.7648", "70.08", "55.32 (persistence)", "+14.76"], | |
| ["Jump risk", "ETH 24h", "0.6801", "65.04", "54.80 (persistence)", "+10.23"], | |
| ["Jump risk", "ADA 24h", "0.7053", "62.86", "57.73 (persistence)", "+5.13"], | |
| ["Volatility regime", "BTC 24h", "0.7761", "74.37", "64.60 (persistence)", "+9.77"], | |
| ["Volatility regime", "ETH 24h", "0.7729", "72.69", "--", "--"], | |
| ["Volatility regime", "ADA 24h", "0.8145", "72.26", "--", "--"], | |
| ] | |
| note = ("The deployed VAE+Transformer artifacts, five seeds each, scored once " | |
| "on a held-out block. The baseline column names which baseline binds: " | |
| "volatility-rises is measured against the majority class because its " | |
| "persistence baseline sits below chance, jump risk against persistence " | |
| "because that is the harder of the two.") | |
| return block("Deployed models", note, rows, | |
| ["Model", "Asset", "ROC-AUC", "Accuracy \\%", "Baseline \\%", | |
| "Margin"], | |
| "Deployed models, held-out performance against the binding baseline.", | |
| "tab:deployed") | |
| def table_direction() -> str: | |
| rows = [ | |
| ["Deployed direction model", "1h", "0.503", "49.4", | |
| r"$\approx$56\%\textsuperscript{*}", "Jaquart et al. (2021), D1"], | |
| ["Deployed direction model", "6h", "0.474", "50.3", | |
| r"61\% train, test flat", "Kraehenbuehl \\& Osterrieder (2022), D3"], | |
| ["Deployed direction model", "24h", "0.490", "50.3", | |
| r"\textbf{79.50\%}", "Seabe et al. (2025), C1"], | |
| ["Triple barrier 1\\%", "48h", "0.4989", "50.15", | |
| "method, no figure", r"L\'opez de Prado (2018), D4"], | |
| ["Triple barrier 2.5\\%", "48h", "0.5167", "51.31", | |
| "method, no figure", r"L\'opez de Prado (2018), D4"], | |
| ["Triple barrier 5\\%", "48h", "0.5265", "52.27", | |
| "method, no figure", r"L\'opez de Prado (2018), D4"], | |
| ["\\emph{Volatility rises (control)}", "12h", "\\emph{0.8286}", "\\emph{75.31}", | |
| "\\emph{n/a}", "\\emph{positive control}"], | |
| ] | |
| note = ("The directional negative result, BTC. ROC-AUC needs no baseline: 0.5000 " | |
| "is chance by construction, and every directional row sits on it. " | |
| "Accuracy does, and each row was measured against its own majority " | |
| "class, computed from the same test rows: 51.0\\% for the three " | |
| "deployed horizons, 51.1\\%, 52.0\\% and 53.9\\% for the barriers at " | |
| "1\\%, 2.5\\% and 5\\%, and 50.54\\% for the control. Every " | |
| "configuration therefore loses to a guess, by 0.7 to 1.7 points, while " | |
| "the control beats it by 24.77. The final row is the positive control: " | |
| "the same features, " | |
| "pipeline and folds detect a large effect when one is present, which is " | |
| "what makes the negative result informative rather than inconclusive. " | |
| "`Reported` is the figure the source itself publishes for the same kind " | |
| "of target, placed beside the measurement so the disagreement is visible " | |
| "on one line. None of those sources reports a baseline alongside it. " | |
| "\\textsuperscript{*}at 1--60 minute horizons, and its authors state the " | |
| "60-minute signal is not exploitable after transaction costs.") | |
| return block("Directional predictability", note, rows, | |
| ["Target", "Horizon", "ROC-AUC", "Accuracy \\%", "Reported \\%", | |
| "Source"], | |
| "Directional predictability, with volatility-rises as a positive " | |
| "control.", "tab:direction") | |
| def table_direction_literature() -> str: | |
| """The five studies the directional result answers to. | |
| Transcribed from the sources rather than measured here, so it carries the | |
| verification state in the row: D1 and D2 are summarised from abstracts and | |
| need confirming against the PDFs. | |
| """ | |
| rows = [ | |
| ["D1", "Jaquart et al. (2021)", "1--60 min, ordered split", | |
| r"$\approx$56\%, not exploitable after costs", "horizon below 1h"], | |
| ["D2", "Jaquart et al. (2022)", "cross-section, costs modelled", | |
| r"39\%/month before costs, negative after", "costs remove the edge"], | |
| ["D3", r"Kraehenbuehl \& Osterrieder (2022)", "train/test separation", | |
| r"train 54.6\%$\rightarrow$61\%, test flat", "agrees: no detectable edge"], | |
| ["D4", r"L\'opez de Prado (2018)", "purged $k$-fold, embargo, CPCV", | |
| "methodological source", "protocol adopted here"], | |
| ["D5", r"Kapoor \& Narayanan (2023)", "294 papers, 17 fields", | |
| "leakage inflates reported gains", "explains the gap"], | |
| ["C1", "Seabe et al. (2025)", "not stated", | |
| r"79.50\% directional accuracy", "no baseline reported"], | |
| # An em dash in the key column renders as an empty cell to a reader | |
| # skimming the table, which is the wrong impression for the one row | |
| # that is this project's own measurement. | |
| [r"\textbf{Ours}", r"\textbf{Measured here}", "purged walk-forward, 6 configs", | |
| r"\textbf{ROC-AUC 0.47--0.53}", "control at 0.8286"], | |
| ] | |
| note = ("The directional claim set against the literature it contradicts. The " | |
| "gap between a reported 79.50\\% and a measured 0.49 is a difference " | |
| "of protocol, not of architecture: D4 names the mechanism and D5 " | |
| "quantifies how often it is skipped. D1 and D2 report positive results " | |
| "that are consistent with this one, at horizons below an hour or before " | |
| "transaction costs. D1 and D2 are summarised from abstracts and are " | |
| "pending verification against the PDFs.") | |
| return block("Directional predictability against the literature", note, rows, | |
| ["\\#", "Study", "Protocol", "Reported", "Relation to this result"], | |
| "Directional predictability: five published studies against this " | |
| "project's measurement under purged walk-forward validation.", | |
| "tab:direction-literature") | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--latex-only", action="store_true") | |
| args = ap.parse_args() | |
| ablation = load("ablation_study.json") | |
| har = load("har_benchmark.json") | |
| parts = ["# Results tables\n", | |
| "Generated by `scripts/make_report_tables.py` from the experiment " | |
| "JSONs. Nothing here is transcribed by hand, so re-running the " | |
| "experiments and this script keeps the tables and the measurements in " | |
| "step. Each table carries its LaTeX in a collapsed block.\n"] | |
| if ablation: | |
| parts += [table_ablation(ablation), table_ablation_full(ablation), | |
| table_features(ablation)] | |
| if har: | |
| parts.append(table_har(har)) | |
| parts += [table_deployed(), table_direction(), table_direction_literature()] | |
| OUT.write_text("\n".join(parts), encoding="utf-8") | |
| print(f"written: {OUT.relative_to(ROOT)}") | |
| for name, present in (("ablation_study.json", ablation), ("har_benchmark.json", har)): | |
| print(f" {name}: {'read' if present else 'MISSING, tables omitted'}") | |
| if __name__ == "__main__": | |
| main() | |