1/* crypto.js â /crypto : niveaux dealer BTC/ETH calcules par nous depuis la chaine 2 d'options Deribit (publique). Trois lectures du worker, rien d'autre : 3 /public/crypto/levels (derniere photo, cron 10 min) 4 /public/crypto/candles (perp Deribit, 10 min) 5 /public/crypto/history (photos du jour -> murs/flip dans le temps) 6 CSP du site : connect-src = api.gex-levels.com uniquement, d'ou tout passe par le worker. */ 7(function () { 8 "use strict"; 9 var publicEnv = (window.__GEX_PUBLIC_ENV__ || {}); 10 var apiBase = window.GEX_LEVELS_API_BASE || publicEnv.apiBaseUrl || "https://api.gex-levels.com"; 11 var $ = function (s) { return document.querySelector(s); }; 12 // Tout ce qui vient du worker passe par esc() avant innerHTML â meme si le 13 // worker est a nous, une reponse alteree ne doit jamais devenir du HTML. 14 var esc = function (v) { return String(v == null ? "" : v).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[c]; }); }; 15 var currency = (location.hash || "").replace("#", "").toUpperCase() === "ETH" ? "ETH" : "BTC"; 16 var lwChart = null, candleSeries = null, priceLines = [], lineValues = []; 17 var REFRESH_MS = 60 * 1000; 18 var fmt = function (n, d) { return n == null || !isFinite(n) ? "â" : Number(n).toLocaleString("en-US", { maximumFractionDigits: d == null ? 0 : d, minimumFractionDigits: d == null ? 0 : d }); }; 19 var money = function (n) { var a = Math.abs(n); var s = a >= 1e9 ? (a / 1e9).toFixed(2) + "B" : a >= 1e6 ? (a / 1e6).toFixed(1) + "M" : a >= 1e3 ? (a / 1e3).toFixed(0) + "K" : String(Math.round(a)); return (n < 0 ? "â$" : "$") + s; }; 20 var pct = function (a, b) { return a == null || b == null || !b ? "" : ((a - b) / b * 100).toFixed(2) + "%"; }; 21 22 function setStatus(state, text) { var el = $("[data-cx-status]"); if (!el) return; el.setAttribute("data-state", state); el.querySelector("[data-cx-status-text]").textContent = text; } 23 24 function getJson(path) { 25 // credentials: la session compte (cookie) decide si les niveaux sont servis. 26 return fetch(apiBase + path, { headers: { Accept: "application/json" }, credentials: "include" }).then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { j.__status = r.status; return j; }); }); 27 } 28 29 // Niveaux verrouilles derriere l'abonnement (decision Marc 05/09) : le prix reste 30 // visible, tout ce qui est calcule par nous demande une session abonnee. 31 function renderLocked(status) { 32 var signedIn = status === 402 || status === 403; 33 var lock = $("[data-cx-lock]"); 34 if (lock) { 35 lock.hidden = false; 36 lock.querySelector("[data-cx-lock-text]").textContent = signedIn 37 ? "Your account is signed in but has no active Indicator subscription. The BTC and ETH maps are part of the Indicator plan." 38 : "BTC and ETH levels are part of the GEX Levels Indicator subscription. Sign in to see today's map, or start a free trial."; 39 lock.querySelector("[data-cx-lock-signin]").hidden = signedIn; 40 } 41 ["[data-cx-readings]", "[data-cx-ladder]", "[data-cx-profile]"].forEach(function (s) { var el = $(s); if (el) el.innerHTML = ""; }); 42 var hn = $("[data-cx-hist-note]"); if (hn) hn.textContent = "Subscribers see the day's structure over time here."; 43 var svg = $("[data-cx-hist]"); if (svg) svg.innerHTML = ""; 44 setStatus("stale", currency + " · levels locked · subscribe to unlock"); 45 } 46 47 function renderReadings(s) { 48 var regime = s.regime_by_model === "positive_gamma" ? "Positive gamma" : s.regime_by_model === "negative_gamma" ? "Negative gamma" : "â"; 49 var regimeNote = s.regime_by_model === "positive_gamma" ? "dealers dampen moves (model)" : s.regime_by_model === "negative_gamma" ? "dealers amplify moves (model)" : ""; 50 var rows = [ 51 ["Spot (index)", fmt(s.spot, s.currency === "ETH" ? 2 : 0), esc(s.currency) + "/USD · Deribit index"], 52 ["Regime", regime, regimeNote], 53 ["Net dealer gamma", money(s.net_gex_usd), "per 1% move · " + esc(s.expiry_scope)], 54 ["DVOL", s.dvol == null ? "â" : Number(s.dvol).toFixed(1), "Deribit implied vol index"], 55 ["Open interest", fmt(s.oi_total, 0) + " " + esc(s.currency), esc(s.contracts) + " usable contracts · " + esc(s.instruments) + " listed"] 56 ]; 57 $("[data-cx-readings]").innerHTML = rows.map(function (r) { return "<div class=\"cx-read\"><b>" + r[0] + "</b><span>" + r[1] + "</span><small>" + r[2] + "</small></div>"; }).join(""); 58 } 59 60 function renderLadder(s) { 61 var items = [ 62 { k: "call_wall", name: "Call Wall", note: "largest positive net gamma strike", color: "#7fb37a" }, 63 { k: "zero_gamma", name: "Zero Gamma (model)", note: s.zg_band_lo != null ? "band " + fmt(s.zg_band_lo) + " â " + fmt(s.zg_band_hi) + (s.zg_confidence ? " · confidence " + esc(s.zg_confidence) : "") : "repriced from the chain", color: "#d4b483" }, 64 { k: "gamma_flip", name: "Gamma Flip", note: "cumulative net gamma crosses zero", color: "#e8dcc2" }, 65 { k: "put_wall", name: "Put Wall", note: "largest negative net gamma strike", color: "#c86b5a" } 66 ].filter(function (i) { return s[i.k] != null; }); 67 items.push({ k: "spot", name: "Spot", note: "now", color: "#f5f1e9", spot: true }); 68 items.sort(function (a, b) { return s[b.k] - s[a.k]; }); 69 $("[data-cx-ladder]").innerHTML = items.map(function (i) { 70 var v = s[i.k]; 71 return "<li" + (i.spot ? " data-spot=\"1\"" : "") + "><span class=\"cx-mark cx-mark--" + i.k + "\"></span><span class=\"cx-name\">" + i.name + "<small>" + i.note + "</small></span><span class=\"cx-px\">" + fmt(v, s.currency === "ETH" ? 2 : 0) + (i.spot ? "" : "<small>" + pct(v, s.spot) + " from spot</small>") + "</span></li>"; 72 }).join(""); 73 } 74 75 function renderProfile(s) { 76 var prof = (s.profile || []).slice(); 77 if (!prof.length) { $("[data-cx-profile]").innerHTML = ""; return; } 78 var max = prof.reduce(function (m, r) { return Math.max(m, Math.abs(r[1])); }, 0) || 1; 79 // du plus haut strike au plus bas, comme une echelle de prix 80 prof.sort(function (a, b) { return b[0] - a[0]; }); 81 var spotDone = false;
82 var html = prof.map(function (r) { 83 var w = Math.abs(r[1]) / max * 50; 84 var row = "<span class=\"cx-k\">" + fmt(r[0]) + "</span><span class=\"cx-bar\"><i class=\"" + (r[1] >= 0 ? "pos" : "neg") + "\" data-w=\"" + w.toFixed(1) + "\"></i></span><span class=\"cx-v\">" + money(r[1]) + "</span>"; 85 if (!spotDone && r[0] <= s.spot) { spotDone = true; row = "<span class=\"cx-k cx-spot\">spot " + fmt(s.spot) + "</span><span class=\"cx-bar\"></span><span class=\"cx-v cx-spot\">â</span>" + row; } 86 return row; 87 }).join(""); 88 $("[data-cx-profile]").innerHTML = html; 89 // Largeurs via CSSOM : un attribut style inline serait bloque par la CSP. 90 $("[data-cx-profile]").querySelectorAll("i[data-w]").forEach(function (el) { el.style.width = el.getAttribute("data-w") + "%"; }); 91 } 92 93 function ensureChart() { 94 if (lwChart || !window.LightweightCharts) return; 95 var wrap = $("[data-cx-chart]"); 96 lwChart = LightweightCharts.createChart(wrap, { 97 layout: { background: { type: "solid", color: "transparent" }, textColor: "#8a8478", fontFamily: "JetBrains Mono, monospace", fontSize: 11 }, 98 grid: { vertLines: { color: "rgba(245,241,233,0.04)" }, horzLines: { color: "rgba(245,241,233,0.04)" } }, 99 rightPriceScale: { borderColor: "rgba(245,241,233,0.1)" }, 100 timeScale: { borderColor: "rgba(245,241,233,0.1)", timeVisible: true, secondsVisible: false }, 101 crosshair: { mode: 0 }, handleScroll: true, handleScale: true, autoSize: true 102 }); 103 candleSeries = lwChart.addCandlestickSeries({ upColor: "#7fb37a", downColor: "#c86b5a", borderVisible: false, wickUpColor: "#7fb37a", wickDownColor: "#c86b5a" }); 104 // Les lignes de niveaux hors de la plage des bougies seraient dessinees hors 105 // ecran : l'autoscale inclut les niveaux proches du spot (±12 %). 106 candleSeries.applyOptions({ autoscaleInfoProvider: function (orig) { 107 var r = orig(); 108 if (!r || !r.priceRange || !lineValues.length) return r; 109 var lo = r.priceRange.minValue, hi = r.priceRange.maxValue; 110 lineValues.forEach(function (v) { if (v < lo) lo = v; if (v > hi) hi = v; }); 111 return { priceRange: { minValue: lo, maxValue: hi }, margins: r.margins }; 112 } }); 113 } 114 115 function renderChart(s, candles) { 116 s = s || {}; 117 ensureChart(); 118 if (!candleSeries) return; 119 candleSeries.setData(candles.map(function (c) { return { time: c.t, open: c.o, high: c.h, low: c.l, close: c.c }; })); 120 priceLines.forEach(function (l) { try { candleSeries.removePriceLine(l); } catch (_) {} }); 121 priceLines = []; 122 lineValues = []; 123 var lines = [["call_wall", "Call Wall", "#7fb37a", 0], ["gamma_flip", "Gamma Flip", "#e8dcc2", 2], ["zero_gamma", "Zero Gamma", "#d4b483", 1], ["put_wall", "Put Wall", "#c86b5a", 0]]; 124 lines.forEach(function (l) { 125 if (s[l[0]] == null) return; 126 if (s.spot && Math.abs(s[l[0]] - s.spot) <= s.spot * 0.12) lineValues.push(s[l[0]]); 127 priceLines.push(candleSeries.createPriceLine({ price: s[l[0]], color: l[2], lineWidth: 1, lineStyle: l[3], axisLabelVisible: true, title: l[1] })); 128 }); 129 if (s.zg_band_lo != null && s.zg_band_hi != null) { 130 [s.zg_band_lo, s.zg_band_hi].forEach(function (p) { priceLines.push(candleSeries.createPriceLine({ price: p, color: "rgba(212,180,131,0.35)", lineWidth: 1, lineStyle: 3, axisLabelVisible: false, title: "" })); }); 131 } 132 lwChart.timeScale().fitContent(); 133 } 134 135 function renderHistory(rows, s) { 136 var svg = $("[data-cx-hist]"); 137 if (!svg) return; 138 var W = 600, H = 120, P = 6; 139 if (!rows || rows.length < 2) { svg.innerHTML = "<text x=\"8\" y=\"64\" fill=\"#8a8478\" font-size=\"11\" font-family=\"JetBrains Mono, monospace\">History fills in as the sampler runs (one point every 10 minutes).</text>"; return; } 140 var series = [["call_wall", "#7fb37a"], ["gamma_flip", "#e8dcc2"], ["zero_gamma", "#d4b483"], ["put_wall", "#c86b5a"], ["spot", "#f5f1e9"]]; 141 var vals = []; 142 rows.forEach(function (r) { series.forEach(function (sr) { if (r[sr[0]] != null) vals.push(r[sr[0]]); }); }); 143 var lo = Math.min.apply(null, vals), hi = Math.max.apply(null, vals); if (hi === lo) hi = lo + 1; 144 var t0 = rows[0].ts, t1 = rows[rows.length - 1].ts; if (t1 === t0) t1 = t0 + 1; 145 var x = function (t) { return P + (t - t0) / (t1 - t0) * (W - 2 * P); }, y = function (v) { return H - P - (v - lo) / (hi - lo) * (H - 2 * P); }; 146 svg.setAttribute("viewBox", "0 0 " + W + " " + H); 147 svg.innerHTML = series.map(function (sr) { 148 var pts = rows.filter(function (r) { return r[sr[0]] != null; }).map(function (r) { return x(r.ts).toFixed(1) + "," + y(r[sr[0]]).toFixed(1); }); 149 return pts.length ? "<polyline fill=\"none\" stroke=\"" + sr[1] + "\" stroke-width=\"" + (sr[0] === "spot" ? 1.6 : 1) + "\" points=\"" + pts.join(" ") + "\"/>" : ""; 150 }).join(""); 151 $("[data-cx-hist-note]").textContent = rows.length + " snapshots · " + new Date(t0).toUTCString().slice(17, 22) + " â " + new Date(t1).toUTCString().slice(17, 22) + " UTC"; 152 } 153 154 function load() { 155 var to = Date.now(), from = to - 24 * 3600 * 1000; 156 setStatus("live", "Loading " + currency + "â¦"); 157 return Promise.all([ 158 getJson("/public/crypto/levels?currency=" + currency), 159 getJson("/public/crypto/candles?currency=" + currency + "&resolution=10&hours=48"), 160 getJson("/public/crypto/history?currency=" + currency + "&from=" + from + "&to=" + to + "&limit=400") 161 ]).then(function (res) { 162 var s = res[0], c = res[1], h = res[2]; 163 if (c && c.ok) renderChart(s && s.ok ? s : {}, c.candles || []); 164 if (s && (s.__status === 401 || s.__status === 402 || s.__status === 403)) { renderLocked(s.__status); return; } 165 var lock = $("[data-cx-lock]"); if (lock) lock.hidden = true; 166 if (!s || !s.ok) { setStatus("error", s && s.error === "no_sample_yet" ? "First snapshot lands within 10 minutes." : "Levels unavailable right now."); return; } 167 var age = Math.round((Date.now() - s.ts) / 60000); 168 setStatus(age > 25 ? "stale" : "live", currency + " · computed " + age + " min ago from " + Number(s.instruments || 0) + " Deribit options · refreshes every 10 min"); 169 renderReadings(s); renderLadder(s); renderProfile(s); 170 renderHistory(h && h.ok ? h.rows : [], s); 171 document.title = currency + " gamma exposure levels â Call Wall " + fmt(s.call_wall) + " · Put Wall " + fmt(s.put_wall) + " â GEX Levels"; 172 }).catch(function () { setStatus("error", "Could not reach the data service."); }); 173 } 174
175 document.querySelectorAll("[data-cx-currency]").forEach(function (b) { 176 b.addEventListener("click", function () { 177 currency = b.getAttribute("data-cx-currency"); 178 document.querySelectorAll("[data-cx-currency]").forEach(function (x) { x.setAttribute("aria-pressed", x === b ? "true" : "false"); }); 179 history.replaceState(null, "", "#" + currency.toLowerCase()); 180 load(); 181 }); 182 b.setAttribute("aria-pressed", b.getAttribute("data-cx-currency") === currency ? "true" : "false"); 183 }); 184 load(); 185 setInterval(load, REFRESH_MS); 186})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.