crypto-forecasting-ai / scripts /make_method_diagram.py
chonthichar's picture
Claude Opus 5.5
Add Fed/CPI calendar features to the volatility regime model; new studies
03f6e95
Raw History Blame Contribute Delete
6.86 kB
"""Method diagram: the live price-range forecaster (model B) next to the
volatility-regime classifier, step by step, with how each was tested.
Numbers are read from the deployed artifacts where they exist, so the figure
matches what is live: btc_deployment_models/price_range/params.json and
outputs/price_backtest_fixes.json.
Usage:
python scripts/make_method_diagram.py
"""
from __future__ import annotations
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
ROOT = Path(__file__).resolve().parents[1]
SURFACE, INK, INK_2, GRID = "#fcfcfb", "#0b0b0b", "#52514e", "#e6e5e1"
BLUE, ORANGE = "#2a78d6", "#eb6834"
FILL = {BLUE: "#eef3fb", ORANGE: "#fdf0ea"}
def main() -> None:
p = json.loads((ROOT / "btc_deployment_models" / "price_range" / "params.json").read_text())
bt = json.loads((ROOT / "outputs" / "price_backtest_fixes.json").read_text())["walk_forward"]
hw = p["hour_of_week_variance"]
cov = bt["24h | + Fed/CPI calendar"]["coverage"]
n = bt["24h | + Fed/CPI calendar"]["n"]
q80 = p["quantiles"]["24"]["0.8"]
left = [
("1 Recent volatility",
"Last 168 hourly BTC prices \u2192 \u03c3 = how much\nthe price typically moves per hour"),
("2 Weekly pattern",
f"Each of the 168 hours of the week gets a factor:\nweekends {p['weekend_factor']:.2f}\u00d7, "
f"range {min(hw):.2f}\u00d7 \u2013 {max(hw):.2f}\u00d7 normal"),
("3 Fed / CPI calendar",
f"Release hour {p['k_release']:.1f}\u00d7 variance, next 2 hours "
f"{p['k_after']:.2f}\u00d7\n(dates are published months ahead: no look-ahead)"),
("4 Combine over the horizon",
"\u03c3\u2095 = \u03c3 \u00d7 \u221a(sum of the factors of the next h hours)"),
("5 Fat tails \u2192 range",
f"price \u00d7 exp(\u00b1 q \u00d7 \u03c3\u2095), q learned from 2 years of\nreal outcomes "
f"(80%: q = {q80:.2f} instead of the normal 1.28)"),
]
vt = json.loads((ROOT / "btc_deployment_models" / "volatility"
/ "volatility_btc_24h_vaetransformer.json").read_text())
right = [
("1 Recent market data",
"Last 168 hours (+ 90 days for the reference level)\nof hourly price, volume, trades, high/low"),
("2 Features relative to normal",
f"{len(vt['features'])} features per hour (volatility 6\u2013168h, ATR, volume \u2026),\n"
"volatility ones divided by the 90-day median"),
("3 Question (target)",
"Will the next 24h be MORE volatile than\nthe 90-day median? yes / no"),
("4 Model: VAE + Transformer",
f"VAE encodes each hour, Transformer reads the {vt['sequence_length']}h\nsequence "
f"\u2192 P(high); {vt['seeds']}-seed average, cut-off {vt['decision_threshold']:.2f}"),
("5 Answer",
"\u201cHIGH\u201d or \u201cLOW\u201d volatility for the next 24h"),
]
# Both examples are the live Space's own answers for the 17 Oct 2026 12:00 UTC candle.
outputs = [
(BLUE, "Output: a price range",
"live, from \\$81,520, next 24h:\n80% chance between \\$80,519 and \\$82,534"),
(ORANGE, "Output: a yes/no regime",
"live, next 24h: LOW volatility\n(probability of high: 0.31)"),
]
tests = [
(BLUE, "Tested walk-forward 2023 \u2013 2026",
f"Refit every quarter on the previous 2 years,\n{n:,} unseen forecasts per horizon\n\n"
f"50 / 80 / 95% ranges held\n{cov['50%'] * 100:.1f} / {cov['80%'] * 100:.1f} / "
f"{cov['95%'] * 100:.1f}% of the time (24h)\n"
"3\u20137% better than plain volatility (1\u201324h)"),
# Persistence on the same held-out span, recomputed from btc_1h.csv
# (which ends 2026-09-27, a few days before the model's holdout does).
(ORANGE, "Tested on unseen hours, Mar \u2013 Sep 2026",
f"Last {vt['holdout_rows']:,} hours held out, trained on the {vt['trained_rows']:,} before\n\n"
f"Accuracy {vt['holdout_accuracy'] * 100:.1f}% vs \u2248 64.5% for \u201csame as now\u201d\n"
f"ROC-AUC {vt['holdout_auc_ensemble']:.2f}\n"
"(earlier tree model, 5 walk-forward folds 2022\u201326:\n71.3% \u00b1 2.2 vs 65.6% persistence)"),
]
plt.rcParams.update({"font.family": "DejaVu Sans", "text.color": INK})
fig = plt.figure(figsize=(14, 13.2), facecolor=SURFACE)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_xlim(0, 14); ax.set_ylim(0, 13.2); ax.axis("off")
def box(x, y, w, h, title, body, color, strong=False):
ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.02,rounding_size=0.12",
facecolor=color if strong else FILL[color],
edgecolor=color, linewidth=1.6 if strong else 1.1))
ink = "white" if strong else INK
ax.text(x + 0.22, y + h - 0.2, title, fontsize=11, fontweight="bold", va="top", color=ink)
ax.text(x + 0.22, y + h - 0.58, body, fontsize=9.6, va="top", color="white" if strong else INK_2,
linespacing=1.4)
def arrow(x, y0, y1, color):
ax.add_patch(FancyArrowPatch((x, y0), (x, y1), arrowstyle="-|>", mutation_scale=13,
color=color, linewidth=1.3))
ax.text(0.5, 12.85, "Two forecasts from the same idea: volatility clusters and is predictable",
fontsize=15, fontweight="bold", va="top")
ax.text(0.5, 12.38, "Price direction is a coin flip in every test we ran. How much the price "
"will move is not, and both models below forecast that, in two different forms.",
fontsize=9.8, color=INK_2, va="top")
cols = [(0.5, BLUE, "A Price range (live: \u201cWhere will the price be?\u201d)", left),
(7.2, ORANGE, "B Volatility regime (live: 24h volatility panel)", right)]
w, h, gap, top = 6.3, 1.12, 0.32, 11.55
for x, color, head, steps in cols:
ax.text(x, top + 0.42, head, fontsize=12, fontweight="bold", color=color, va="center")
y = top
for i, (title, body) in enumerate(steps):
box(x, y - h, w, h, title, body, color)
arrow(x + w / 2, y - h, y - h - gap, color)
y -= h + gap
k = 0 if color == BLUE else 1
oc, ot, ob = outputs[k]
box(x, y - 1.05, w, 1.05, ot, ob, oc, strong=True)
arrow(x + w / 2, y - 1.05, y - 1.05 - gap, color)
y -= 1.05 + gap
tc, tt, tb = tests[k]
box(x, y - 2.15, w, 2.15, tt, tb, tc)
fig.savefig(ROOT / "outputs" / "figures" / "fig14_method_diagram.png", dpi=150, facecolor=SURFACE)
plt.close(fig)
print("written: outputs/figures/fig14_method_diagram.png")
if __name__ == "__main__":
main()