Spaces:
Running
Running
site update
Browse files- README.md +7 -5
- index.html +361 -17
README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
---
|
| 2 |
-
title: Cascade Lab
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Cascade Research Lab
|
| 3 |
+
colorFrom: blue
|
| 4 |
+
colorTo: gray
|
|
|
|
| 5 |
sdk: static
|
| 6 |
pinned: false
|
| 7 |
+
short_description: Live crypto liquidations and an open lab notebook
|
| 8 |
---
|
| 9 |
|
| 10 |
+
Static website. It reads the public ledger dataset
|
| 11 |
+
(`latest.json`, `journey/lab.json`) and shows the live recorder, the studies
|
| 12 |
+
and the lab notebook. All data comes from the dataset; this page holds none.
|
index.html
CHANGED
|
@@ -1,19 +1,363 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
| 6 |
+
<title>Cascade Research Lab</title>
|
| 7 |
+
<meta name="description" content="A live crypto liquidation recorder and an open research notebook on liquidation cascades.">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans+Condensed:wght@500;600&family=IBM+Plex+Sans:wght@400;500&display=swap">
|
| 11 |
+
<style>
|
| 12 |
+
/* One reading column: live recorder first (what is running now), then the
|
| 13 |
+
studies and their gates, then the dated notebook. */
|
| 14 |
+
:root {
|
| 15 |
+
--paper: #f6f7f9; --panel: #ffffff; --ink: #141820; --ink-2: #4f5866; --rule: #dde1e8;
|
| 16 |
+
--accent: #2147c4; --accent-soft: #e6ecfb;
|
| 17 |
+
--long: #c2412d; --short: #14806b; --ok: #18684a; --warn: #8a5300;
|
| 18 |
+
--k-pre: #2147c4; --k-pre-bg: #e6ecfb; --k-caught: #8a5300; --k-caught-bg: #fbefd9;
|
| 19 |
+
--k-null: #5b3fa6; --k-null-bg: #efe9fb; --k-find: #18684a; --k-find-bg: #e2f3ea;
|
| 20 |
+
--k-plain: #4f5866; --k-plain-bg: #eceef2;
|
| 21 |
+
--display: "IBM Plex Sans Condensed", "Arial Narrow", system-ui, sans-serif;
|
| 22 |
+
--body: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 23 |
+
--mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Consolas, monospace;
|
| 24 |
+
color-scheme: light;
|
| 25 |
+
}
|
| 26 |
+
@media (prefers-color-scheme: dark) {
|
| 27 |
+
:root {
|
| 28 |
+
--paper: #0f1217; --panel: #161b22; --ink: #e6e9ee; --ink-2: #9ba4b1; --rule: #2a313b;
|
| 29 |
+
--accent: #8aa6ff; --accent-soft: #1d2840; --long: #f08a70; --short: #5fd1b5; --ok: #6fd1a2; --warn: #f0b75c;
|
| 30 |
+
--k-pre: #8aa6ff; --k-pre-bg: #1d2840; --k-caught: #f0b75c; --k-caught-bg: #3a2c12;
|
| 31 |
+
--k-null: #b9a4f5; --k-null-bg: #2a2340; --k-find: #6fd1a2; --k-find-bg: #15302a;
|
| 32 |
+
--k-plain: #9ba4b1; --k-plain-bg: #222831;
|
| 33 |
+
color-scheme: dark;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
* { box-sizing: border-box; }
|
| 37 |
+
body { margin: 0; background: var(--paper); color: var(--ink); font: 400 16px/1.6 var(--body); }
|
| 38 |
+
.page { max-width: 56rem; margin: 0 auto; padding-block: 48px 64px; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); display: grid; gap: 56px; }
|
| 39 |
+
.mono { font-family: var(--mono); font-size: 13px; }
|
| 40 |
+
h1, h2, h3 { text-wrap: balance; margin: 0; }
|
| 41 |
+
h2 { font: 600 24px/1.2 var(--display); letter-spacing: .01em; }
|
| 42 |
+
h3 { font: 500 17px/1.35 var(--body); }
|
| 43 |
+
p { margin: 0; }
|
| 44 |
+
a { color: var(--accent); text-underline-offset: 3px; }
|
| 45 |
+
a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
| 46 |
+
.note { color: var(--ink-2); max-width: 64ch; }
|
| 47 |
+
.section-head { display: grid; gap: 8px; margin-bottom: 20px; }
|
| 48 |
+
|
| 49 |
+
.hero { display: grid; gap: 16px; }
|
| 50 |
+
.eyebrow { color: var(--ink-2); text-transform: uppercase; letter-spacing: .08em; font-size: 12px; }
|
| 51 |
+
.hero h1 { font: 600 clamp(30px, 5.2vw, 46px)/1.08 var(--display); max-width: 22ch; }
|
| 52 |
+
.summary { font-size: 18px; color: var(--ink-2); max-width: 60ch; }
|
| 53 |
+
|
| 54 |
+
/* live panel */
|
| 55 |
+
.live { background: var(--panel); border: 1px solid var(--rule); border-radius: 6px; padding: 24px; display: grid; gap: 20px; }
|
| 56 |
+
.live-head { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; justify-content: space-between; }
|
| 57 |
+
.pill { display: inline-flex; align-items: center; gap: 8px; font: 500 13px/1 var(--mono); padding: 6px 10px; border-radius: 999px; border: 1px solid var(--rule); }
|
| 58 |
+
.pill i { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-2); }
|
| 59 |
+
.pill.on i { background: var(--ok); }
|
| 60 |
+
.pill.stale i { background: var(--warn); }
|
| 61 |
+
.figs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 9.5rem), 1fr)); gap: 16px; }
|
| 62 |
+
.fig { display: grid; gap: 2px; align-content: start; }
|
| 63 |
+
.fig .lab { font-size: 13px; color: var(--ink-2); }
|
| 64 |
+
.fig .val { font: 500 26px/1.15 var(--display); font-variant-numeric: tabular-nums; }
|
| 65 |
+
.fig .sub { font: 400 12px/1.3 var(--mono); color: var(--ink-2); }
|
| 66 |
+
.val.long { color: var(--long); } .val.short { color: var(--short); }
|
| 67 |
+
.chart { position: relative; min-height: 220px; }
|
| 68 |
+
.chart svg { display: block; width: 100%; height: auto; }
|
| 69 |
+
.legend { display: flex; flex-wrap: wrap; gap: 8px 20px; font-size: 13px; color: var(--ink-2); }
|
| 70 |
+
.sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: -1px; }
|
| 71 |
+
.tip { position: absolute; pointer-events: none; background: var(--panel); border: 1px solid var(--rule); border-radius: 4px; padding: 6px 10px; font-size: 12px; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,.12); }
|
| 72 |
+
.two { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 24px; }
|
| 73 |
+
table { width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: tabular-nums; }
|
| 74 |
+
th, td { padding: 6px 8px; text-align: right; border-bottom: 1px solid var(--rule); white-space: nowrap; }
|
| 75 |
+
th { color: var(--ink-2); font-weight: 500; font-size: 13px; }
|
| 76 |
+
th:first-child, td:first-child { text-align: left; }
|
| 77 |
+
.scroll { overflow-x: auto; }
|
| 78 |
+
.caveat { font-size: 13px; color: var(--ink-2); }
|
| 79 |
+
.empty { color: var(--ink-2); font-size: 14px; }
|
| 80 |
+
|
| 81 |
+
/* studies */
|
| 82 |
+
.studies { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
|
| 83 |
+
.study { background: var(--panel); border: 1px solid var(--rule); border-radius: 6px; padding: 20px; display: grid; grid-template-columns: 3.25rem minmax(0, 1fr); gap: 4px 16px; }
|
| 84 |
+
.study.is-planned { background: transparent; }
|
| 85 |
+
.study-id { font: 500 22px/1.2 var(--mono); color: var(--ink-2); }
|
| 86 |
+
.study.is-active .study-id { color: var(--accent); }
|
| 87 |
+
.study-main { display: grid; gap: 12px; min-width: 0; }
|
| 88 |
+
.study .q { color: var(--ink-2); max-width: 66ch; }
|
| 89 |
+
.track { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 4px; }
|
| 90 |
+
.seg { display: grid; gap: 6px; }
|
| 91 |
+
.seg i { display: block; height: 4px; border-radius: 2px; background: var(--rule); }
|
| 92 |
+
.seg.done i { background: var(--accent); }
|
| 93 |
+
.seg.next i { background: transparent; box-shadow: inset 0 0 0 1px var(--accent); }
|
| 94 |
+
.seg span { font: 400 11px/1.3 var(--mono); color: var(--ink-2); overflow-wrap: anywhere; }
|
| 95 |
+
.seg.done span, .seg.next span { color: var(--ink); }
|
| 96 |
+
@media (max-width: 34rem) { .seg span { display: none; } .seg.next span { display: block; } }
|
| 97 |
+
.headline { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
|
| 98 |
+
.headline li { padding-left: 16px; position: relative; }
|
| 99 |
+
.headline li::before { content: ""; position: absolute; left: 0; top: .62em; width: 7px; height: 7px; border-radius: 50%; background: var(--k-find); }
|
| 100 |
+
.study-foot { display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: baseline; }
|
| 101 |
+
.status { color: var(--ink-2); font-size: 14px; }
|
| 102 |
+
.study-links { display: flex; flex-wrap: wrap; gap: 8px 16px; font-size: 14px; }
|
| 103 |
+
|
| 104 |
+
/* notebook */
|
| 105 |
+
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
| 106 |
+
.chip { font: 400 13px/1 var(--mono); padding: 7px 12px; border-radius: 999px; border: 1px solid var(--rule); background: transparent; color: var(--ink); cursor: pointer; }
|
| 107 |
+
.chip[aria-pressed="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
| 108 |
+
.log { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
|
| 109 |
+
.entry { display: grid; grid-template-columns: 11rem minmax(0, 1fr); gap: 4px 24px; padding-block: 18px; border-bottom: 1px solid var(--rule); }
|
| 110 |
+
.entry time { color: var(--ink-2); padding-top: 2px; }
|
| 111 |
+
.entry-main { display: grid; gap: 6px; min-width: 0; }
|
| 112 |
+
.meta { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; }
|
| 113 |
+
.kind { font: 500 12px/1 var(--mono); padding: 5px 8px; border-radius: 4px; color: var(--k-plain); background: var(--k-plain-bg); }
|
| 114 |
+
.kind[data-k="Pre-registration"] { color: var(--k-pre); background: var(--k-pre-bg); }
|
| 115 |
+
.kind[data-k="Caught"] { color: var(--k-caught); background: var(--k-caught-bg); }
|
| 116 |
+
.kind[data-k="Null result"] { color: var(--k-null); background: var(--k-null-bg); }
|
| 117 |
+
.kind[data-k="Finding"] { color: var(--k-find); background: var(--k-find-bg); }
|
| 118 |
+
.meta .mono { color: var(--ink-2); }
|
| 119 |
+
.entry p { color: var(--ink-2); max-width: 66ch; }
|
| 120 |
+
@media (max-width: 40rem) { .entry { grid-template-columns: minmax(0, 1fr); } }
|
| 121 |
+
.foot { border-top: 1px solid var(--rule); padding-top: 16px; color: var(--ink-2); font-size: 14px; display: grid; gap: 4px; }
|
| 122 |
+
</style>
|
| 123 |
+
</head>
|
| 124 |
+
<body>
|
| 125 |
+
<main class="page">
|
| 126 |
+
<header class="hero">
|
| 127 |
+
<p class="eyebrow mono">Open research notebook</p>
|
| 128 |
+
<h1 id="focus">Liquidation cascades in crypto</h1>
|
| 129 |
+
<p class="summary" id="summary"></p>
|
| 130 |
+
</header>
|
| 131 |
+
|
| 132 |
+
<section id="live" aria-labelledby="live-h">
|
| 133 |
+
<div class="section-head">
|
| 134 |
+
<h2 id="live-h">Live: liquidation recorder</h2>
|
| 135 |
+
<p class="note">Every liquidation order Binance pushes on its USD-M perpetual futures, all symbols, plus open interest for five large coins every minute. Aggregated each hour and published to a public dataset.</p>
|
| 136 |
+
</div>
|
| 137 |
+
<div class="live">
|
| 138 |
+
<div class="live-head">
|
| 139 |
+
<span class="pill" id="pill"><i></i><span id="pill-text">Loading</span></span>
|
| 140 |
+
<span class="mono" id="updated"></span>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="figs">
|
| 143 |
+
<div class="fig"><span class="lab">Liquidations, last 24 h</span><span class="val" id="f-n">—</span><span class="sub" id="f-n-sub"></span></div>
|
| 144 |
+
<div class="fig"><span class="lab">Longs liquidated</span><span class="val long" id="f-long">—</span><span class="sub">sell orders</span></div>
|
| 145 |
+
<div class="fig"><span class="lab">Shorts liquidated</span><span class="val short" id="f-short">—</span><span class="sub">buy orders</span></div>
|
| 146 |
+
<div class="fig"><span class="lab">Largest single</span><span class="val" id="f-big">—</span><span class="sub" id="f-big-sub"></span></div>
|
| 147 |
+
<div class="fig"><span class="lab">Recorder coverage, 24 h</span><span class="val" id="f-cov">—</span><span class="sub" id="f-cov-sub"></span></div>
|
| 148 |
+
</div>
|
| 149 |
+
<div>
|
| 150 |
+
<div class="chart" id="chart"></div>
|
| 151 |
+
<div class="legend"><span><span class="sw" style="background: var(--long)"></span>Longs liquidated (USD, up)</span><span><span class="sw" style="background: var(--short)"></span>Shorts liquidated (USD, down)</span><span>Faded bars: the recorder was connected for less than the full hour</span></div>
|
| 152 |
+
</div>
|
| 153 |
+
<div class="two">
|
| 154 |
+
<div>
|
| 155 |
+
<h3>Most liquidated, last 24 h</h3>
|
| 156 |
+
<div class="scroll"><table><thead><tr><th>Symbol</th><th>Orders</th><th>USD</th><th>Longs</th></tr></thead><tbody id="top"></tbody></table></div>
|
| 157 |
+
</div>
|
| 158 |
+
<div>
|
| 159 |
+
<h3>Open interest, change over 24 h</h3>
|
| 160 |
+
<div class="scroll"><table><thead><tr><th>Coin</th><th>Change</th></tr></thead><tbody id="oi"></tbody></table></div>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
<p class="caveat">Binance pushes at most one liquidation per symbol per second, so counts and USD values are lower bounds when liquidations cluster. Research only; nothing here is investment advice.</p>
|
| 164 |
+
</div>
|
| 165 |
+
</section>
|
| 166 |
+
|
| 167 |
+
<section id="studies" aria-labelledby="studies-h">
|
| 168 |
+
<div class="section-head">
|
| 169 |
+
<h2 id="studies-h">Studies</h2>
|
| 170 |
+
<p class="note">Six studies, worked through in order, from what happens in a cascade to a paper-trading forward test. Each must pass its gate before the next step; failures are published too.</p>
|
| 171 |
+
</div>
|
| 172 |
+
<ol class="studies" id="study-list"><li class="empty">Loading the studies…</li></ol>
|
| 173 |
+
</section>
|
| 174 |
+
|
| 175 |
+
<section id="notebook" aria-labelledby="notebook-h">
|
| 176 |
+
<div class="section-head">
|
| 177 |
+
<h2 id="notebook-h">Lab notebook</h2>
|
| 178 |
+
<p class="note">Dated entries as the work happens, including the bugs caught and the tests that failed.</p>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="filters" id="filters" role="group" aria-label="Filter by study"></div>
|
| 181 |
+
<ol class="log" id="log"></ol>
|
| 182 |
+
</section>
|
| 183 |
+
|
| 184 |
+
<footer class="foot">
|
| 185 |
+
<span>Every number on this page is read from the public ledger, <a id="ledger-link" href="#" target="_blank" rel="noopener">a Hugging Face dataset</a>. Each hourly update is a commit, so its history is the project's timestamped record.</span>
|
| 186 |
+
<span>Data: Binance USD-M futures public market data. Charts and code are the project's own.</span>
|
| 187 |
+
</footer>
|
| 188 |
+
</main>
|
| 189 |
+
|
| 190 |
+
<script>
|
| 191 |
+
(() => {
|
| 192 |
+
const DATASET = 'vin268/cascade-lab-ledger';
|
| 193 |
+
const onHub = /(\.hf\.space|huggingface\.co)$/.test(location.hostname);
|
| 194 |
+
const BASE = onHub ? `https://huggingface.co/datasets/${DATASET}/resolve/main` : './data';
|
| 195 |
+
document.getElementById('ledger-link').href = `https://huggingface.co/datasets/${DATASET}`;
|
| 196 |
+
|
| 197 |
+
const $ = (id) => document.getElementById(id);
|
| 198 |
+
const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
| 199 |
+
const link = (href, text) => { const a = el('a', null, text); a.href = href; a.target = '_blank'; a.rel = 'noopener'; return a; };
|
| 200 |
+
const usd = (v) => v >= 1e6 ? '$' + (v / 1e6).toFixed(2) + 'm' : v >= 1e3 ? '$' + (v / 1e3).toFixed(1) + 'k' : '$' + Math.round(v);
|
| 201 |
+
const pct = (v) => (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(2) + '%';
|
| 202 |
+
const fmtDay = (iso) => new Date(iso.length === 10 ? iso + 'T00:00:00Z' : iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
|
| 203 |
+
const fmtTime = (iso) => new Date(iso).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', timeZone: 'UTC' });
|
| 204 |
+
const fetchJson = (path) => fetch(`${BASE}/${path}?t=${Date.now()}`).then((r) => { if (!r.ok) throw new Error(r.status); return r.json(); });
|
| 205 |
+
|
| 206 |
+
// ---------- live panel ----------
|
| 207 |
+
function renderLive(d) {
|
| 208 |
+
const ageMin = (Date.now() - Date.parse(d.updated)) / 60000;
|
| 209 |
+
const stale = ageMin > 120 || (d.recorder && d.recorder.connected === false);
|
| 210 |
+
$('pill').className = 'pill ' + (stale ? 'stale' : 'on');
|
| 211 |
+
$('pill-text').textContent = stale ? 'Not updating' : 'Recording';
|
| 212 |
+
$('updated').textContent = `Updated ${fmtDay(d.updated)} ${fmtTime(d.updated)} UTC` + (d.recording_since ? ` · recording since ${fmtDay(d.recording_since)}` : '');
|
| 213 |
+
$('f-n').textContent = d.n_liquidations.toLocaleString('en-GB');
|
| 214 |
+
$('f-n-sub').textContent = `${d.n_symbols} symbols`;
|
| 215 |
+
$('f-long').textContent = usd(d.usd_long_liquidated);
|
| 216 |
+
$('f-short').textContent = usd(d.usd_short_liquidated);
|
| 217 |
+
if (d.largest) {
|
| 218 |
+
$('f-big').textContent = usd(d.largest.usd);
|
| 219 |
+
$('f-big-sub').textContent = `${d.largest.symbol} ${d.largest.side === 'SELL' ? 'long' : 'short'} · ${fmtTime(d.largest.time)} UTC`;
|
| 220 |
+
}
|
| 221 |
+
$('f-cov').textContent = (100 * d.coverage_24h).toFixed(1) + '%';
|
| 222 |
+
$('f-cov-sub').textContent = 'of the last 24 h connected';
|
| 223 |
+
|
| 224 |
+
const top = $('top'); top.replaceChildren();
|
| 225 |
+
if (!d.top_symbols.length) { const tr = el('tr'); const td = el('td', 'empty', 'No liquidations yet'); td.colSpan = 4; tr.appendChild(td); top.appendChild(tr); }
|
| 226 |
+
d.top_symbols.forEach((s) => {
|
| 227 |
+
const tr = el('tr');
|
| 228 |
+
[s.symbol, s.n.toLocaleString('en-GB'), usd(s.usd), s.long_share == null ? '—' : Math.round(100 * s.long_share) + '%'].forEach((v) => tr.appendChild(el('td', null, v)));
|
| 229 |
+
top.appendChild(tr);
|
| 230 |
+
});
|
| 231 |
+
const oi = $('oi'); oi.replaceChildren();
|
| 232 |
+
const entries = Object.entries(d.oi_change_24h_pct || {});
|
| 233 |
+
if (!entries.length) { const tr = el('tr'); const td = el('td', 'empty', 'Not enough polls yet'); td.colSpan = 2; tr.appendChild(td); oi.appendChild(tr); }
|
| 234 |
+
entries.forEach(([c, v]) => { const tr = el('tr'); tr.appendChild(el('td', null, c)); tr.appendChild(el('td', null, pct(v))); oi.appendChild(tr); });
|
| 235 |
+
drawChart(d.hourly_7d || []);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
function drawChart(rows) {
|
| 239 |
+
const box = $('chart'); box.replaceChildren();
|
| 240 |
+
if (!rows.length) { box.appendChild(el('p', 'empty', 'The hourly chart appears after the first full hour of recording.')); return; }
|
| 241 |
+
const W = Math.max(320, box.clientWidth), H = 220, M = { t: 10, r: 8, b: 24, l: 64 };
|
| 242 |
+
const max = Math.max(1, ...rows.map((r) => Math.max(r.usd_long_liquidated, r.usd_short_liquidated)));
|
| 243 |
+
const mid = M.t + (H - M.t - M.b) / 2, half = (H - M.t - M.b) / 2;
|
| 244 |
+
// fixed 7-day axis, one slot per hour, ending at the latest hour
|
| 245 |
+
const SLOTS = 168, HOUR = 3600e3;
|
| 246 |
+
const endMs = Date.parse(rows[rows.length - 1].hour);
|
| 247 |
+
const startMs = endMs - (SLOTS - 1) * HOUR;
|
| 248 |
+
const bw = (W - M.l - M.r) / SLOTS;
|
| 249 |
+
const slot = (iso) => Math.round((Date.parse(iso) - startMs) / HOUR);
|
| 250 |
+
rows = rows.filter((r) => slot(r.hour) >= 0);
|
| 251 |
+
const y = (v) => (v / max) * half;
|
| 252 |
+
const ns = 'http://www.w3.org/2000/svg';
|
| 253 |
+
const svg = document.createElementNS(ns, 'svg');
|
| 254 |
+
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
| 255 |
+
svg.setAttribute('role', 'img');
|
| 256 |
+
svg.setAttribute('aria-label', 'Hourly USD of long and short liquidations');
|
| 257 |
+
const add = (tag, attrs, text) => { const e = document.createElementNS(ns, tag); Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v)); if (text != null) e.textContent = text; svg.appendChild(e); return e; };
|
| 258 |
+
const ink2 = getComputedStyle(document.body).getPropertyValue('--ink-2');
|
| 259 |
+
const rule = getComputedStyle(document.body).getPropertyValue('--rule');
|
| 260 |
+
add('line', { x1: M.l, x2: W - M.r, y1: mid, y2: mid, stroke: rule });
|
| 261 |
+
[['↑ ' + usd(max), mid - half + 8], ['0', mid + 4], ['↓ ' + usd(max), mid + half]].forEach(([t, yy]) => add('text', { x: M.l - 6, y: yy, 'text-anchor': 'end', 'font-size': 11, fill: ink2, 'font-family': 'IBM Plex Mono, monospace' }, t));
|
| 262 |
+
const tip = el('div', 'tip'); tip.hidden = true; box.appendChild(tip);
|
| 263 |
+
rows.forEach((r) => {
|
| 264 |
+
const i = slot(r.hour);
|
| 265 |
+
const x = M.l + i * bw + bw * 0.12, w = Math.max(1.5, bw * 0.76);
|
| 266 |
+
const op = r.coverage >= 0.999 ? 1 : 0.35;
|
| 267 |
+
add('rect', { x, y: mid - y(r.usd_long_liquidated), width: w, height: y(r.usd_long_liquidated), fill: 'var(--long)', opacity: op, rx: 1 });
|
| 268 |
+
add('rect', { x, y: mid, width: w, height: y(r.usd_short_liquidated), fill: 'var(--short)', opacity: op, rx: 1 });
|
| 269 |
+
const hit = add('rect', { x: M.l + i * bw, y: M.t, width: bw, height: H - M.t - M.b, fill: 'transparent' });
|
| 270 |
+
hit.addEventListener('pointerenter', () => {
|
| 271 |
+
tip.replaceChildren(el('div', 'mono', `${fmtDay(r.hour)} ${fmtTime(r.hour)} UTC`), el('div', null, `Longs ${usd(r.usd_long_liquidated)} (${r.n_long_liquidated})`), el('div', null, `Shorts ${usd(r.usd_short_liquidated)} (${r.n_short_liquidated})`), el('div', null, `Coverage ${Math.round(100 * r.coverage)}%`));
|
| 272 |
+
tip.hidden = false;
|
| 273 |
+
const px = (M.l + i * bw) * (box.clientWidth / W);
|
| 274 |
+
tip.style.left = Math.min(box.clientWidth - tip.offsetWidth, Math.max(0, px + 12)) + 'px';
|
| 275 |
+
tip.style.top = '0px';
|
| 276 |
+
});
|
| 277 |
+
hit.addEventListener('pointerleave', () => { tip.hidden = true; });
|
| 278 |
+
});
|
| 279 |
+
// day ticks at UTC midnight
|
| 280 |
+
for (let i = 0; i < SLOTS; i++) {
|
| 281 |
+
const t = new Date(startMs + i * HOUR);
|
| 282 |
+
if (t.getUTCHours() !== 0) continue;
|
| 283 |
+
add('line', { x1: M.l + i * bw, x2: M.l + i * bw, y1: M.t, y2: H - M.b, stroke: rule });
|
| 284 |
+
add('text', { x: M.l + i * bw + 3, y: H - 6, 'font-size': 11, fill: ink2, 'font-family': 'IBM Plex Mono, monospace' }, t.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }));
|
| 285 |
+
}
|
| 286 |
+
box.insertBefore(svg, tip);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
// ---------- journey ----------
|
| 290 |
+
function fmtAt(iso) { return fmtDay(iso.slice(0, 10)) + ' · ' + fmtTime(iso) + ' UTC'; }
|
| 291 |
+
function renderJourney(lab) {
|
| 292 |
+
$('focus').textContent = lab.lab.focus;
|
| 293 |
+
$('summary').textContent = lab.lab.summary;
|
| 294 |
+
const repo = lab.lab.repo;
|
| 295 |
+
const list = $('study-list'); list.replaceChildren();
|
| 296 |
+
const LINK = { dashboard: 'Interactive dashboard', findings: 'Findings', code: 'Code' };
|
| 297 |
+
lab.studies.forEach((s) => {
|
| 298 |
+
const li = el('li', 'study ' + (s.stage > 0 ? 'is-active' : 'is-planned'));
|
| 299 |
+
li.appendChild(el('div', 'study-id', s.id));
|
| 300 |
+
const main = el('div', 'study-main');
|
| 301 |
+
main.appendChild(el('h3', null, s.title));
|
| 302 |
+
main.appendChild(el('p', 'q', s.question));
|
| 303 |
+
const track = el('div', 'track');
|
| 304 |
+
track.setAttribute('role', 'img');
|
| 305 |
+
track.setAttribute('aria-label', `${s.stage} of ${lab.stages.length} stages done`);
|
| 306 |
+
lab.stages.forEach((name, i) => {
|
| 307 |
+
const seg = el('div', 'seg' + (i < s.stage ? ' done' : i === s.stage ? ' next' : ''));
|
| 308 |
+
seg.appendChild(el('i'));
|
| 309 |
+
seg.appendChild(el('span', null, i === s.stage ? 'Next: ' + name : name));
|
| 310 |
+
track.appendChild(seg);
|
| 311 |
+
});
|
| 312 |
+
main.appendChild(track);
|
| 313 |
+
if (s.headline && s.headline.length) { const ul = el('ul', 'headline'); s.headline.forEach((h) => ul.appendChild(el('li', null, h))); main.appendChild(ul); }
|
| 314 |
+
const foot = el('div', 'study-foot');
|
| 315 |
+
foot.appendChild(el('span', 'status', s.status));
|
| 316 |
+
const sl = el('div', 'study-links');
|
| 317 |
+
Object.entries(s.links || {}).forEach(([k, v]) => { if (v && !v.includes('claude.ai')) sl.appendChild(link(v, LINK[k] || k)); });
|
| 318 |
+
if (sl.children.length) foot.appendChild(sl);
|
| 319 |
+
main.appendChild(foot);
|
| 320 |
+
li.appendChild(main);
|
| 321 |
+
list.appendChild(li);
|
| 322 |
+
});
|
| 323 |
+
|
| 324 |
+
const log = $('log'); log.replaceChildren();
|
| 325 |
+
const entries = [...lab.notebook].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
|
| 326 |
+
entries.forEach((n) => {
|
| 327 |
+
const li = el('li', 'entry'); li.dataset.study = n.study;
|
| 328 |
+
const t = el('time', 'mono', fmtAt(n.at)); t.dateTime = n.at; li.appendChild(t);
|
| 329 |
+
const main = el('div', 'entry-main');
|
| 330 |
+
const meta = el('p', 'meta');
|
| 331 |
+
const k = el('span', 'kind', n.kind); k.dataset.k = n.kind; meta.appendChild(k);
|
| 332 |
+
meta.appendChild(el('span', 'mono', n.study === 'live' ? 'Live system' : 'Study ' + n.study));
|
| 333 |
+
if (n.ref) meta.appendChild(repo ? link(repo.replace(/\/$/, '') + '/commit/' + n.ref, n.ref) : el('span', 'mono', n.ref));
|
| 334 |
+
main.appendChild(meta);
|
| 335 |
+
main.appendChild(el('h3', null, n.title));
|
| 336 |
+
main.appendChild(el('p', null, n.body));
|
| 337 |
+
li.appendChild(main);
|
| 338 |
+
log.appendChild(li);
|
| 339 |
+
});
|
| 340 |
+
const filters = $('filters'); filters.replaceChildren();
|
| 341 |
+
const ids = [...new Set(entries.map((n) => n.study))].sort();
|
| 342 |
+
let picked = 'all';
|
| 343 |
+
const apply = () => {
|
| 344 |
+
filters.querySelectorAll('.chip').forEach((c) => c.setAttribute('aria-pressed', String(c.dataset.v === picked)));
|
| 345 |
+
log.querySelectorAll('.entry').forEach((e) => { e.hidden = picked !== 'all' && e.dataset.study !== picked; });
|
| 346 |
+
};
|
| 347 |
+
[['all', 'All']].concat(ids.map((i) => [i, i === 'live' ? 'Live system' : 'Study ' + i])).forEach(([v, label]) => {
|
| 348 |
+
const b = el('button', 'chip', label); b.type = 'button'; b.dataset.v = v;
|
| 349 |
+
b.addEventListener('click', () => { picked = v; apply(); });
|
| 350 |
+
filters.appendChild(b);
|
| 351 |
+
});
|
| 352 |
+
filters.hidden = ids.length < 2;
|
| 353 |
+
apply();
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
fetchJson('latest.json').then(renderLive).catch(() => { $('pill').className = 'pill stale'; $('pill-text').textContent = 'Ledger not reachable'; });
|
| 357 |
+
fetchJson('journey/lab.json').then(renderJourney).catch(() => { $('study-list').replaceChildren(el('li', 'empty', 'Could not load the studies.')); });
|
| 358 |
+
let lastW = 0;
|
| 359 |
+
window.addEventListener('resize', () => { const w = $('chart').clientWidth; if (Math.abs(w - lastW) > 40) { lastW = w; fetchJson('latest.json').then(renderLive).catch(() => {}); } });
|
| 360 |
+
})();
|
| 361 |
+
</script>
|
| 362 |
+
</body>
|
| 363 |
</html>
|