File size: 5,571 Bytes
b55a7c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Exchange inflows and outflows and Bitcoin: the labelled version of the whale test.

The whale study (fig15) could not tell a sale from an exchange shuffling its
own coins. Here every transaction touching 22 publicly labelled exchange
wallets (outputs/onchain/exchange_addresses.csv: Binance, OKX, Bitfinex,
Robinhood, Crypto.com, Coincheck, Bitbank, Gate, BitMEX, Huobi) is split by
the sending side:

  inflow    BTC received by an exchange from addresses not known to belong to
            that exchange: coins arriving, possibly to be sold
  outflow   BTC leaving a known exchange address for anywhere else
  internal  an exchange paying itself; dropped

The wallets are mostly cold storage, so "inflow" still includes an exchange
topping up cold storage from its own unlabelled hot wallets. That noise only
dilutes a real effect; it cannot create one.

Data: runtime/cache/exchange_flows.csv, written by the Colab cell
notebooks/colab_exchange_flows_cell.py from Google's public BigQuery dataset.
Method: whale_event_study.event_study and lead_lag, unchanged.

Usage:
    python scripts/exchange_flow_study.py
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent))
from trump_post_event_study import bars                                          # noqa: E402
from whale_event_study import PLACEBO, SEED, draw, event_study, first_in_burst, lead_lag  # noqa: E402

ROOT = Path(__file__).resolve().parents[1]
FLOWS = ROOT / "runtime" / "cache" / "exchange_flows.csv"
GROUPS = {   # name: (direction, min BTC, max BTC)
    "Inflow 100 - 1,000 BTC": ("inflow", 100, 1_000),
    "Inflow 1,000 BTC or more": ("inflow", 1_000, np.inf),
    "Outflow 100 - 1,000 BTC": ("outflow", 100, 1_000),
    "Outflow 1,000 BTC or more": ("outflow", 1_000, np.inf),
}
COLORS = {"Inflow 100 - 1,000 BTC": "#f2a37f", "Inflow 1,000 BTC or more": "#c24a12",
          "Outflow 100 - 1,000 BTC": "#8fb8ea", "Outflow 1,000 BTC or more": "#0b3d91",
          PLACEBO: "#8b8a85"}


def flows() -> pd.DataFrame:
    if not FLOWS.exists():
        raise SystemExit(f"missing {FLOWS.relative_to(ROOT)}: run the Colab cell "
                         "notebooks/colab_exchange_flows_cell.py and copy its CSV here")
    d = pd.read_csv(FLOWS, keep_default_na=False)
    d["ts"] = pd.to_datetime(d["block_timestamp"], utc=True)

    def received(s: str) -> dict:
        return {k: float(v) for k, v in (x.rsplit(":", 1) for x in s.split(";"))} if s else {}

    out = d["out_exchanges"].map(received)
    senders = d["in_exchanges"].map(lambda s: set(s.split(";")) if s else set())
    d["inflow"] = [sum(v for k, v in o.items() if k not in s) for o, s in zip(out, senders)]
    d["internal"] = [sum(v for k, v in o.items() if k in s) for o, s in zip(out, senders)]
    # Leaving: what a known exchange sent that did not land on a known exchange address.
    d["outflow"] = np.where(senders.map(len) > 0,
                            (d["total_btc"] - out.map(lambda o: sum(o.values()))).clip(lower=0), 0.0)
    return d.drop_duplicates("hash").sort_values("ts").reset_index(drop=True)


def main() -> None:
    sys.stdout.reconfigure(encoding="utf-8")
    rng = np.random.default_rng(SEED)
    d = flows()
    close = bars()
    start = max(close.index.min(), d["ts"].min()) + pd.Timedelta(days=2)
    end = min(close.index.max(), d["ts"].max()) - pd.Timedelta(days=22)
    print(f"{len(d):,} transactions: inflow {d['inflow'].sum():,.0f} BTC, outflow "
          f"{d['outflow'].sum():,.0f} BTC, internal {d['internal'].sum():,.0f} BTC (dropped)")

    groups = []
    for name, (side, lo, hi) in GROUPS.items():
        hit = d[(d[side] >= lo) & (d[side] < hi)]
        groups.append((name, first_in_burst(hit["ts"]), d.loc[d[side] >= 100, "ts"]))
    results, curves, rel = event_study(close, groups, rng, start, end)
    lead = lead_lag(close, {side: d.loc[d[side] > 0].set_index("ts")[side]
                            for side in ("inflow", "outflow")}, start, end)

    (ROOT / "outputs" / "exchange_flow_study.json").write_text(json.dumps({
        "method": ("transactions touching 22 labelled exchange wallets (BigQuery crypto_bitcoin); "
                   "inflow = BTC received from addresses not labelled as the same exchange, outflow = "
                   "BTC a labelled exchange sent elsewhere; event study and lead-lag as "
                   "whale_event_study.py"),
        "transactions": int(len(d)), "span": [str(d["ts"].min()), str(d["ts"].max())],
        "btc_totals": {k: float(d[k].sum()) for k in ("inflow", "outflow", "internal")},
        "groups": results, "lead_lag_hourly": lead}, indent=2))

    fi = lead["inflow"]["hour_of_week_removed"]["flow_predicts_volatility"]
    fo = lead["outflow"]["hour_of_week_removed"]["flow_predicts_volatility"]
    draw(results, curves, rel, COLORS, "flow (events)",
         "Exchange inflows and outflows on the Bitcoin blockchain and the BTC price",
         "Transactions touching 22 publicly labelled exchange wallets (Binance, OKX, Bitfinex, Robinhood, "
         "Crypto.com, …), timed by block; 5-minute BTC bars; exchange-to-itself moves dropped.\n"
         f"Hourly lead-lag, weekly pattern removed: inflow → next hour's volatility t = {fi['t']:+.1f} "
         f"(+{fi['r2_gain_pts']:.2f} pts R²);  outflow → next hour's volatility t = {fo['t']:+.1f} "
         f"(+{fo['r2_gain_pts']:.2f} pts R²).",
         "fig16_exchange_flows.png")


if __name__ == "__main__":
    main()