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()