File size: 6,855 Bytes
03f6e95 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """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()
|