@hypelens/hypelens-agent-rail 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore +3 -0
- package/README.md +71 -0
- package/ROLLOUT.md +67 -0
- package/bin/hypelens-rail-mcp.js +3 -0
- package/package-lock.json +1182 -0
- package/package.json +48 -0
- package/scripts/bundle-vendor.mjs +13 -0
- package/server.json +35 -0
- package/smithery.yaml +23 -0
- package/src/core.js +178 -0
- package/src/exchange.js +88 -0
- package/src/index.js +2 -0
- package/src/load.js +39 -0
- package/src/mcp.js +63 -0
- package/test/exchange.test.mjs +74 -0
- package/test/risk.test.mjs +86 -0
- package/vendor/hl-actions.js +116 -0
- package/vendor/hl-sdk.js +9215 -0
- package/vendor/hl-signer.js +68 -0
- package/vendor/viewmodel.js +344 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// HypeLens Module 3 — SIGNER ADAPTER over the vendored SDK (window.HLSDK).
|
|
2
|
+
// -----------------------------------------------------------------------------
|
|
3
|
+
// This is the ONLY place that produces signatures, and it does so ONLY through
|
|
4
|
+
// the vendored @nktkas/hyperliquid signing subset. It hand-rolls NOTHING. If
|
|
5
|
+
// the SDK is absent it FAILS CLOSED. Before every L1 send it re-derives the
|
|
6
|
+
// action hash TWICE via the SDK and asserts equality (guards against accidental
|
|
7
|
+
// action mutation / non-deterministic key order). Exposes window.HLX3.signer.
|
|
8
|
+
(function (g) {
|
|
9
|
+
'use strict';
|
|
10
|
+
const X3 = g.HLX3 = g.HLX3 || {};
|
|
11
|
+
|
|
12
|
+
const SDK_METHODS = ['randomPrivateKey', 'addressFromPrivateKey', 'hashL1Action', 'signL1Action', 'userSignedTypedData', 'orderToWire'];
|
|
13
|
+
function sdk() { const s = g.HLSDK; if (!s) throw new Error('signing SDK not vendored — placement disabled'); return s; }
|
|
14
|
+
|
|
15
|
+
// SELF-TEST (runs at load): all 6 adapter methods present + hashL1Action is
|
|
16
|
+
// deterministic (sync string). Fail-closed — cached so ready() reflects it.
|
|
17
|
+
let _selfTest = null;
|
|
18
|
+
function selfTest() {
|
|
19
|
+
try {
|
|
20
|
+
const s = g.HLSDK;
|
|
21
|
+
if (!s) return (_selfTest = { ok: false, error: 'window.HLSDK is null — signing SDK not vendored' });
|
|
22
|
+
for (const m of SDK_METHODS) if (typeof s[m] !== 'function') return (_selfTest = { ok: false, error: 'HLSDK missing method: ' + m });
|
|
23
|
+
const action = { type: 'order', orders: [{ a: 0, b: true, p: '1', s: '1', r: false, t: { limit: { tif: 'Gtc' } } }], grouping: 'na' };
|
|
24
|
+
const h1 = s.hashL1Action(action, 1700000000000, true, null);
|
|
25
|
+
const h2 = s.hashL1Action(action, 1700000000000, true, null);
|
|
26
|
+
if (typeof h1 !== 'string' || !h1 || h1 !== h2) return (_selfTest = { ok: false, error: 'hashL1Action is not deterministic (or not a string) — refusing to enable placement' });
|
|
27
|
+
return (_selfTest = { ok: true, hash: h1 });
|
|
28
|
+
} catch (e) { return (_selfTest = { ok: false, error: 'self-test threw: ' + (e && e.message ? e.message : e) }); }
|
|
29
|
+
}
|
|
30
|
+
function ready() { return (_selfTest || selfTest()).ok; }
|
|
31
|
+
function lastError() { return (_selfTest || selfTest()).error || null; }
|
|
32
|
+
|
|
33
|
+
// Deterministic-hash gate: the SAME action + nonce MUST hash identically twice.
|
|
34
|
+
function assertDeterministicHash(action, nonce, isTestnet, vaultAddress) {
|
|
35
|
+
const s = sdk();
|
|
36
|
+
const h1 = s.hashL1Action(action, nonce, isTestnet, vaultAddress || null);
|
|
37
|
+
const h2 = s.hashL1Action(action, nonce, isTestnet, vaultAddress || null);
|
|
38
|
+
if (!h1 || h1 !== h2) throw new Error('action-hash verification FAILED (non-deterministic) — refusing to sign');
|
|
39
|
+
return h1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Sign an L1 (agent) action. `privateKey` comes from the vault at sign time.
|
|
43
|
+
// viem signs ASYNCHRONOUSLY → signL1Action returns a Promise: await it.
|
|
44
|
+
// Returns { signature, action, nonce, hash }.
|
|
45
|
+
async function signL1(privateKey, action, nonce, isTestnet, vaultAddress) {
|
|
46
|
+
const s = sdk();
|
|
47
|
+
const hash = assertDeterministicHash(action, nonce, isTestnet, vaultAddress);
|
|
48
|
+
const signature = await s.signL1Action(privateKey, action, nonce, isTestnet, vaultAddress || null);
|
|
49
|
+
if (!signature || signature.r == null || signature.s == null || signature.v == null) throw new Error('SDK returned an invalid signature');
|
|
50
|
+
return { signature, action, nonce, hash };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Build the exact EIP-712 typed-data payload for a user-signed action (the
|
|
54
|
+
// MASTER wallet signs this via the page's window.ethereum bridge — the
|
|
55
|
+
// extension never sees the master key). Returns { domain, types, primaryType, message }.
|
|
56
|
+
function userTypedData(built) {
|
|
57
|
+
const s = sdk();
|
|
58
|
+
if (typeof s.userSignedTypedData === 'function') return s.userSignedTypedData(built.action, built.action.signatureChainId);
|
|
59
|
+
// Fallback to the exact payload the builder already assembled (types + domain
|
|
60
|
+
// are pinned in hl-actions.js). The SDK path is preferred when present.
|
|
61
|
+
return { domain: built.domain, types: built.types, primaryType: built.primaryType, message: built.action };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
X3.signer = { ready, lastError, selfTest, signL1, userTypedData, assertDeterministicHash, addressFromPrivateKey: (pk) => sdk().addressFromPrivateKey(pk) };
|
|
65
|
+
// Run the self-test once at load and surface the result in the console so a
|
|
66
|
+
// broken/absent SDK is obvious. Placement stays fail-closed on failure.
|
|
67
|
+
try { const r = selfTest(); if (r.ok) console.log('[HypeLens] signing SDK self-test PASSED (hash', r.hash.slice(0, 10) + '…)'); else console.warn('[HypeLens] signing SDK self-test FAILED —', r.error); } catch (e) {}
|
|
68
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// HypeLens view-model — the DATA-SHAPE CONTRACT + shared math the whole UI
|
|
2
|
+
// consumes. Loaded into BOTH the content-script world and the popup (as a
|
|
3
|
+
// plain <script> before their own JS), exposing `window.HLVM`.
|
|
4
|
+
// ---------------------------------------------------------------------
|
|
5
|
+
// Hero = LIQUIDATION INTELLIGENCE + SMART-MONEY POSITIONING + the
|
|
6
|
+
// LIQ-AWARE LEVERAGE tool (keep your liq price clear of the walls big
|
|
7
|
+
// books target). Funding is a one-line footnote.
|
|
8
|
+
//
|
|
9
|
+
// smartMoney + liqClusters come from a backend-precomputed JSON (see
|
|
10
|
+
// worker/aggregate-intel.mjs) fetched by background.js. When that JSON is
|
|
11
|
+
// unavailable the UI falls back to clearly-labelled PLACEHOLDER data.
|
|
12
|
+
// Funding is always live from the HL info row.
|
|
13
|
+
//
|
|
14
|
+
// ┌── CONTRACT (HLVM.buildViewModel -> this shape) ────────────────────┐
|
|
15
|
+
// │ { │
|
|
16
|
+
// │ coin, markPx, maxLeverage, mmf, // mmf = maint margin frac │
|
|
17
|
+
// │ smartMoney:{ side, pctShort, netUsd, nWallets, nProfitable, │
|
|
18
|
+
// │ recentEntries:[{addr,side,sizeUsd,liqPx,roi, │
|
|
19
|
+
// │ pnlLabel,agoMin}], source }, │
|
|
20
|
+
// │ liq:{ clusters:[{price,sizeUsd,side,distPct}], nearest, │
|
|
21
|
+
// │ totalBelowUsd, totalAboveUsd, source }, │
|
|
22
|
+
// │ funding:{ apr, perDayPer1k, fundingHr, side, premiumPct, source},│
|
|
23
|
+
// │ isHyperp, placeholder │
|
|
24
|
+
// │ } │
|
|
25
|
+
// │ Backend JSON per coin: { markPx, smartMoney:{side,pctShort,netUsd, │
|
|
26
|
+
// │ nWallets,nProfitable,topEntries:[...]}, liqClusters:[{price, │
|
|
27
|
+
// │ sizeUsd,side}] } — see worker/aggregate-intel.mjs. │
|
|
28
|
+
// └────────────────────────────────────────────────────────────────────┘
|
|
29
|
+
|
|
30
|
+
(function (g) {
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
// ---- formatting ----
|
|
34
|
+
function moneyPerDayPer1k(fundingHr) { return fundingHr == null ? null : fundingHr * 24 * 1000; }
|
|
35
|
+
function fmtMoney(n) {
|
|
36
|
+
if (n == null || isNaN(n)) return '—';
|
|
37
|
+
const v = Math.abs(n);
|
|
38
|
+
if (v >= 100) return '$' + v.toFixed(0);
|
|
39
|
+
if (v >= 10) return '$' + v.toFixed(1);
|
|
40
|
+
return '$' + v.toFixed(2);
|
|
41
|
+
}
|
|
42
|
+
function fmtUsd(n) {
|
|
43
|
+
if (n == null || isNaN(n)) return '—';
|
|
44
|
+
const abs = Math.abs(n), s = n < 0 ? '-' : '';
|
|
45
|
+
if (abs >= 1e9) return s + '$' + (abs / 1e9).toFixed(2) + 'B';
|
|
46
|
+
if (abs >= 1e6) return s + '$' + (abs / 1e6).toFixed(1) + 'M';
|
|
47
|
+
if (abs >= 1e3) return s + '$' + (abs / 1e3).toFixed(1) + 'K';
|
|
48
|
+
return s + '$' + abs.toFixed(0);
|
|
49
|
+
}
|
|
50
|
+
function fmtApr(n) { return n == null || isNaN(n) ? '—' : (n > 0 ? '+' : '') + Number(n).toFixed(1) + '%'; }
|
|
51
|
+
function fmtPrice(p) {
|
|
52
|
+
if (p == null || isNaN(p)) return '—';
|
|
53
|
+
const v = Math.abs(p);
|
|
54
|
+
if (v >= 1000) return '$' + p.toFixed(0);
|
|
55
|
+
if (v >= 1) return '$' + p.toFixed(2);
|
|
56
|
+
return '$' + p.toFixed(4);
|
|
57
|
+
}
|
|
58
|
+
function fmtPrem(n) { return n == null ? '—' : (n > 0 ? '+' : '') + n.toFixed(3) + '%'; }
|
|
59
|
+
function signClass(n) { return n == null ? '' : n > 0 ? 'pos' : n < 0 ? 'neg' : ''; }
|
|
60
|
+
function shortAddr(a) { if (!a) return '0x…'; return a.length > 12 ? a.slice(0, 6) + '…' + a.slice(-4) : a; }
|
|
61
|
+
function agoLabel(min) {
|
|
62
|
+
if (min == null) return '';
|
|
63
|
+
if (min < 60) return Math.round(min) + 'm ago';
|
|
64
|
+
const h = min / 60;
|
|
65
|
+
if (h < 24) return h.toFixed(h < 10 ? 1 : 0) + 'h ago';
|
|
66
|
+
return Math.round(h / 24) + 'd ago';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ========================================================================
|
|
70
|
+
// LIQ-AWARE LEVERAGE math (the killer feature). Pure functions, shared.
|
|
71
|
+
// ========================================================================
|
|
72
|
+
// Maintenance-margin fraction. HL doesn't cheaply expose the tiered mmf,
|
|
73
|
+
// so we approximate mmf ≈ 1/(2·maxLeverage) (a standard first-pass; refine
|
|
74
|
+
// when meta exposes the margin table). Used only to place the liq marker,
|
|
75
|
+
// never to execute anything.
|
|
76
|
+
function maintMarginFraction(maxLeverage) {
|
|
77
|
+
return maxLeverage && maxLeverage > 0 ? 1 / (2 * maxLeverage) : 0.05;
|
|
78
|
+
}
|
|
79
|
+
// Liquidation price at entry E, leverage L, direction, maint-margin mmf.
|
|
80
|
+
// long ≈ E·(1 − 1/L + mmf) short ≈ E·(1 + 1/L − mmf)
|
|
81
|
+
// (operator-specified approximation; mark used as entry proxy.)
|
|
82
|
+
function liqPrice(entry, leverage, dir, mmf) {
|
|
83
|
+
if (!entry || !leverage) return null;
|
|
84
|
+
return dir === 'short'
|
|
85
|
+
? entry * (1 + 1 / leverage - mmf)
|
|
86
|
+
: entry * (1 - 1 / leverage + mmf);
|
|
87
|
+
}
|
|
88
|
+
// A long's liq sits BELOW mark → it can be hunted into LONG-liq walls;
|
|
89
|
+
// a short's liq sits ABOVE mark → hunted into SHORT-liq walls. Return the
|
|
90
|
+
// biggest cluster within `band` (default ±1.5%) of the liq price, else null.
|
|
91
|
+
function huntRiskCluster(liqPx, clusters, dir, band) {
|
|
92
|
+
band = band || 0.015;
|
|
93
|
+
if (liqPx == null || !clusters) return null;
|
|
94
|
+
const rel = clusters.filter((c) => (dir === 'long' ? c.side === 'long' : c.side === 'short'));
|
|
95
|
+
let hit = null;
|
|
96
|
+
for (const c of rel) {
|
|
97
|
+
if (Math.abs(c.price - liqPx) / liqPx <= band) {
|
|
98
|
+
if (!hit || c.sizeUsd > hit.sizeUsd) hit = c;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return hit;
|
|
102
|
+
}
|
|
103
|
+
// Descriptive helper: the highest leverage AT OR BELOW the user's current
|
|
104
|
+
// at which the computed liq price sits clear of every cluster. This DESCRIBES
|
|
105
|
+
// the data ("leverage where your liq sits clear of walls: ≤Nx") — it is NOT a
|
|
106
|
+
// recommendation to trade at that leverage. Scans currentL → 1.
|
|
107
|
+
function suggestClearLeverage(entry, dir, mmf, clusters, currentL, maxL) {
|
|
108
|
+
// contract: AT OR BELOW current. currentL 0/negative means "nothing below" —
|
|
109
|
+
// NOT "scan from max" (`0` is falsy; `currentL || maxL` violated the contract).
|
|
110
|
+
if (currentL != null && currentL < 1) return null;
|
|
111
|
+
const top = Math.min(Math.floor(currentL != null ? currentL : (maxL || 1)), Math.floor(maxL || 50));
|
|
112
|
+
for (let L = top; L >= 1; L--) {
|
|
113
|
+
const lp = liqPrice(entry, L, dir, mmf);
|
|
114
|
+
if (!huntRiskCluster(lp, clusters, dir)) return { lev: L, liqPx: lp };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// Full evaluation for a given user input.
|
|
119
|
+
function evalLeverage(vm, input) {
|
|
120
|
+
if (!vm || !vm.markPx) return null;
|
|
121
|
+
const dir = input.dir === 'short' ? 'short' : 'long';
|
|
122
|
+
const L = Math.max(1, Math.min(Number(input.leverage) || 1, vm.maxLeverage || 50));
|
|
123
|
+
const entry = vm.markPx;
|
|
124
|
+
const lp = liqPrice(entry, L, dir, vm.mmf);
|
|
125
|
+
const hit = huntRiskCluster(lp, vm.liq.clusters, dir);
|
|
126
|
+
const clear = hit ? suggestClearLeverage(entry, dir, vm.mmf, vm.liq.clusters, L - 1, vm.maxLeverage) : null;
|
|
127
|
+
return {
|
|
128
|
+
dir, leverage: L, sizeUsd: Number(input.sizeUsd) || 1000,
|
|
129
|
+
margin: input.margin === 'cross' ? 'cross' : 'isolated',
|
|
130
|
+
liqPx: lp,
|
|
131
|
+
liqDistPct: entry ? ((lp - entry) / entry) * 100 : null,
|
|
132
|
+
cluster: hit, // cluster the liq sits inside, or null
|
|
133
|
+
inWall: Boolean(hit), // liq lands inside a crowded cluster
|
|
134
|
+
suggest: clear // { lev, liqPx } at-or-below current that sits clear, or null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---- funding leg (LIVE) ----
|
|
139
|
+
function fundingLeg(row) {
|
|
140
|
+
if (!row) return null;
|
|
141
|
+
return {
|
|
142
|
+
apr: row.aprPct,
|
|
143
|
+
perDayPer1k: moneyPerDayPer1k(row.fundingHr),
|
|
144
|
+
fundingHr: row.fundingHr,
|
|
145
|
+
side: row.fundingHr == null || row.fundingHr === 0 ? 'funding flat'
|
|
146
|
+
: row.fundingHr > 0 ? 'longs pay shorts' : 'shorts pay longs',
|
|
147
|
+
premiumPct: row.premiumPct,
|
|
148
|
+
source: 'live'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- normalize REAL whale intel (from background getCoinIntel) ----
|
|
153
|
+
function normSmart(sm) {
|
|
154
|
+
return {
|
|
155
|
+
side: sm.side || (sm.pctShort >= 55 ? 'short' : sm.pctShort <= 45 ? 'long' : 'mixed'),
|
|
156
|
+
pctShort: sm.pctShort, netUsd: sm.netUsd,
|
|
157
|
+
nWallets: sm.nWallets, nProfitable: sm.nProfitable != null ? sm.nProfitable : null,
|
|
158
|
+
source: sm.source || 'live' // 'live' (crawl) or 'sample' (bundled snapshot)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function normLiq(wallsRaw, markPx) {
|
|
162
|
+
const clusters = (wallsRaw || []).map((c) => ({
|
|
163
|
+
price: c.price, sizeUsd: c.sizeUsd, side: c.side,
|
|
164
|
+
distPct: markPx ? ((c.price - markPx) / markPx) * 100 : null
|
|
165
|
+
})).sort((a, b) => b.price - a.price);
|
|
166
|
+
return finishLiq(clusters);
|
|
167
|
+
}
|
|
168
|
+
function finishLiq(clusters) {
|
|
169
|
+
const nearest = clusters.slice().sort((a, b) => Math.abs(a.distPct || 1e9) - Math.abs(b.distPct || 1e9))[0] || null;
|
|
170
|
+
const totalBelowUsd = clusters.filter((c) => c.side === 'long').reduce((a, c) => a + c.sizeUsd, 0);
|
|
171
|
+
const totalAboveUsd = clusters.filter((c) => c.side === 'short').reduce((a, c) => a + c.sizeUsd, 0);
|
|
172
|
+
return { clusters, nearest, totalBelowUsd, totalAboveUsd, source: 'live' };
|
|
173
|
+
}
|
|
174
|
+
const LOADING_LIQ = { clusters: [], nearest: null, totalBelowUsd: 0, totalAboveUsd: 0, source: 'loading' };
|
|
175
|
+
const LOADING_SM = { side: 'mixed', pctShort: 50, netUsd: 0, nWallets: 0, nProfitable: 0, source: 'loading' };
|
|
176
|
+
|
|
177
|
+
// ---- the one function the UI calls ----
|
|
178
|
+
// opts: { coin, row (HL info row), intel (REAL whale intel from
|
|
179
|
+
// getCoinIntel: { loading, walls:[{price,sizeUsd,side}], smartMoney }) }
|
|
180
|
+
function buildViewModel(opts) {
|
|
181
|
+
const coin = (opts.coin || '').toUpperCase();
|
|
182
|
+
const row = opts.row || null;
|
|
183
|
+
const intel = opts.intel || null;
|
|
184
|
+
const markPx = row ? row.markPx : null;
|
|
185
|
+
const maxLeverage = row ? row.maxLeverage : 50;
|
|
186
|
+
let smartMoney = LOADING_SM, liq = LOADING_LIQ, positions = [];
|
|
187
|
+
if (intel && !intel.loading && intel.smartMoney) {
|
|
188
|
+
smartMoney = normSmart(intel.smartMoney);
|
|
189
|
+
liq = normLiq(intel.walls || [], markPx);
|
|
190
|
+
positions = (intel.positions || []).map((p) => ({ price: p.price, sizeUsd: p.sizeUsd, side: p.side, addr: p.addr, pnl: p.pnl }));
|
|
191
|
+
}
|
|
192
|
+
// REAL liquidation LEVELS for the VPVR-style profile — populated even while
|
|
193
|
+
// the live whale crawl is still running (bundled snapshot fallback), so the
|
|
194
|
+
// profile renders instantly. Each: { price(=liqPx), sizeUsd(=notional), side }.
|
|
195
|
+
const liqLevels = (intel && Array.isArray(intel.levels))
|
|
196
|
+
? intel.levels.map((l) => ({ price: l.price, sizeUsd: l.sizeUsd, side: l.side === 'long' ? 'long' : 'short' }))
|
|
197
|
+
: [];
|
|
198
|
+
const funding = fundingLeg(row);
|
|
199
|
+
return {
|
|
200
|
+
coin, markPx, maxLeverage, mmf: maintMarginFraction(maxLeverage),
|
|
201
|
+
dataAsOf: Date.now(), smartMoney, liq, positions, liqLevels,
|
|
202
|
+
liqLevelsSource: intel ? (intel.levelsSource || null) : null, funding,
|
|
203
|
+
// STALENESS HONESTY (v0.21.1): where the levels came from + how fresh —
|
|
204
|
+
// drives the chart-foot badge and verdict-confidence degradation.
|
|
205
|
+
levelsMeta: intel ? {
|
|
206
|
+
source: intel.levelsSource || null,
|
|
207
|
+
bundleUpdated: intel.bundleUpdated || null,
|
|
208
|
+
bundleStale: Boolean(intel.bundleStale),
|
|
209
|
+
coveragePct: intel.coveragePct != null ? intel.coveragePct : null, // feed: REAL per-coin % of OI
|
|
210
|
+
feedUpdated: intel.feedUpdated || null,
|
|
211
|
+
crawl: intel.crawl || null
|
|
212
|
+
} : null,
|
|
213
|
+
oiNtl: row ? row.oiNtl : null, dayNtlVlm: row ? row.dayNtlVlm : null,
|
|
214
|
+
isHyperp: row ? Boolean(row.isHyperp) : false, loading: liq.source === 'loading'
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ========================================================================
|
|
219
|
+
// VOLATILITY (honest, computed from candles — never a fake "% liquidation").
|
|
220
|
+
// ========================================================================
|
|
221
|
+
// Typical 1-day move as a fraction (0.0136 = 1.36%) from candle log returns.
|
|
222
|
+
function dailyMovePct(candles, interval) {
|
|
223
|
+
if (!candles || candles.length < 3) return null;
|
|
224
|
+
const rets = [];
|
|
225
|
+
for (let i = 1; i < candles.length; i++) { const a = candles[i - 1].c, b = candles[i].c; if (a > 0 && b > 0) rets.push(Math.log(b / a)); }
|
|
226
|
+
if (rets.length < 2) return null;
|
|
227
|
+
const mu = rets.reduce((x, y) => x + y, 0) / rets.length;
|
|
228
|
+
const sd = Math.sqrt(rets.reduce((x, y) => x + (y - mu) * (y - mu), 0) / rets.length);
|
|
229
|
+
const perDay = interval === '15m' ? 96 : interval === '4h' ? 6 : interval === '1d' ? 1 : 24; // default 1h
|
|
230
|
+
return sd * Math.sqrt(perDay);
|
|
231
|
+
}
|
|
232
|
+
// How many typical daily moves a price level sits from mark.
|
|
233
|
+
function volDistance(level, mark, dmp) { if (!level || !mark || !dmp) return null; return Math.abs(level - mark) / mark / dmp; }
|
|
234
|
+
// Risk color from vol-distance: RED ≤1 move · ORANGE ≤2.5 · GREEN beyond.
|
|
235
|
+
function volColor(d) { if (d == null) return 'green'; return d <= 1.0 ? 'red' : d <= 2.5 ? 'orange' : 'green'; }
|
|
236
|
+
function erf(x) { const t = 1 / (1 + 0.3275911 * Math.abs(x)); const y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x); return x >= 0 ? y : -y; }
|
|
237
|
+
// Barrier-touch approximation ~2·(1−Φ(d)) — VOLATILITY ESTIMATE, NOT a
|
|
238
|
+
// prediction, and NEVER to be labelled "liquidation chance" as fact.
|
|
239
|
+
function reachEstimate(d) { if (d == null) return null; const Phi = (x) => 0.5 * (1 + erf(x / Math.SQRT2)); return Math.max(0, Math.min(1, 2 * (1 - Phi(d)))); }
|
|
240
|
+
|
|
241
|
+
// ========================================================================
|
|
242
|
+
// LIQUIDATION CASCADE ("gravity") — the PREDICTIVE layer. Only possible
|
|
243
|
+
// because HL is on-chain and we have REAL per-wallet liq prices + notionals:
|
|
244
|
+
// price entering a cluster forces those liquidations → forced market orders
|
|
245
|
+
// push price further → can reach the NEXT cluster → chain reaction. This is a
|
|
246
|
+
// MODEL: the impact coefficient is an estimate, never present terminalPx as
|
|
247
|
+
// certain. Coinglass/Hyblock can't do this from estimated data.
|
|
248
|
+
// ========================================================================
|
|
249
|
+
const CASCADE_K = 0.6; // impact coefficient (TUNABLE): dumping N notional
|
|
250
|
+
// moves price ~ k·N / marketDepth. Conservative.
|
|
251
|
+
const CASCADE_MAX_STEP = 0.06; // clamp any single cluster's impact to ≤6% (a lone
|
|
252
|
+
// huge wall can't teleport price across the book).
|
|
253
|
+
const CASCADE_BAND = 0.35; // only consider clusters within ±35% of mark.
|
|
254
|
+
function cascadeDepth(vm) {
|
|
255
|
+
if (vm.oiNtl && vm.oiNtl > 0) return { depth: vm.oiNtl, source: 'oi' }; // open interest USD — best proxy
|
|
256
|
+
if (vm.dayNtlVlm && vm.dayNtlVlm > 0) return { depth: vm.dayNtlVlm, source: 'vlm' }; // 24h volume USD fallback
|
|
257
|
+
// last-resort proxy: 4×Σ(tracked liq). OVERSTATES impact by 1/(4·coverage) when
|
|
258
|
+
// tracked liqs are a thin slice of true OI (exactly when oi/vlm are missing) —
|
|
259
|
+
// callers must treat source 'proxy' as LOW-CONFIDENCE: no red alarms off it.
|
|
260
|
+
const t = (vm.liqLevels || []).reduce((s, l) => s + (l.sizeUsd || 0), 0);
|
|
261
|
+
return { depth: t > 0 ? t * 4 : 0, source: 'proxy' };
|
|
262
|
+
}
|
|
263
|
+
// dir: 'down' = long-liq cascade below mark; 'up' = short-squeeze above mark.
|
|
264
|
+
function computeCascade(vm, dir, opts) {
|
|
265
|
+
opts = opts || {};
|
|
266
|
+
const mark = vm && vm.markPx;
|
|
267
|
+
if (!mark || !Array.isArray(vm.liqLevels) || !vm.liqLevels.length) return null;
|
|
268
|
+
const down = dir !== 'up';
|
|
269
|
+
const k = opts.k != null ? opts.k : CASCADE_K;
|
|
270
|
+
const maxStep = opts.maxStep != null ? opts.maxStep : CASCADE_MAX_STEP;
|
|
271
|
+
const band = opts.band != null ? opts.band : CASCADE_BAND;
|
|
272
|
+
const dd = opts.depth != null ? { depth: opts.depth, source: opts.depthSource || 'oi' } : cascadeDepth(vm);
|
|
273
|
+
const depth = dd.depth, depthSource = dd.source;
|
|
274
|
+
if (!depth || depth <= 0) return null;
|
|
275
|
+
// bucket the real liq levels on the relevant side into clusters
|
|
276
|
+
const bw = mark * (opts.bucketFrac || 0.0025);
|
|
277
|
+
const bins = new Map();
|
|
278
|
+
for (const l of vm.liqLevels) {
|
|
279
|
+
const p = l.price, n = l.sizeUsd || 0;
|
|
280
|
+
if (p == null || n <= 0) continue;
|
|
281
|
+
if (down ? !(p < mark) : !(p > mark)) continue;
|
|
282
|
+
if (Math.abs(p - mark) / mark > band) continue;
|
|
283
|
+
const key = Math.round(p / bw), b = bins.get(key) || { wpx: 0, usd: 0 };
|
|
284
|
+
b.usd += n; b.wpx += p * n; bins.set(key, b);
|
|
285
|
+
}
|
|
286
|
+
let clusters = [];
|
|
287
|
+
for (const b of bins.values()) clusters.push({ price: b.wpx / b.usd, usd: b.usd });
|
|
288
|
+
if (!clusters.length) return null;
|
|
289
|
+
// nearest → farthest from mark (down: highest price first; up: lowest first)
|
|
290
|
+
clusters.sort((a, b) => down ? b.price - a.price : a.price - b.price);
|
|
291
|
+
const biggest = clusters.slice().sort((a, b) => b.usd - a.usd)[0];
|
|
292
|
+
// walk price from mark into the side; each fired cluster's impact may reach
|
|
293
|
+
// the next → self-sustaining chain.
|
|
294
|
+
const hops = [];
|
|
295
|
+
let price = mark, total = 0;
|
|
296
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
297
|
+
const c = clusters[i];
|
|
298
|
+
if (i > 0) {
|
|
299
|
+
const reached = down ? price <= c.price : price >= c.price;
|
|
300
|
+
if (!reached) break; // chain stalls: prior impact didn't reach this wall
|
|
301
|
+
}
|
|
302
|
+
hops.push({ price: c.price, usd: c.usd });
|
|
303
|
+
total += c.usd;
|
|
304
|
+
const impact = Math.min(maxStep, k * c.usd / depth);
|
|
305
|
+
price = down ? c.price * (1 - impact) : c.price * (1 + impact);
|
|
306
|
+
}
|
|
307
|
+
const chain = hops.length >= 2; // self-sustaining = ≥2 walls fired in sequence
|
|
308
|
+
if (chain) {
|
|
309
|
+
const triggerPx = hops[0].price, terminalPx = price;
|
|
310
|
+
return {
|
|
311
|
+
dir: down ? 'down' : 'up', chain: true, isolated: false,
|
|
312
|
+
triggerPx, terminalPx, totalLiqUsd: total, hops,
|
|
313
|
+
dropFrac: Math.abs(terminalPx - mark) / mark,
|
|
314
|
+
biggestWall: biggest, depth, depthSource, k
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// no chain — report the single biggest wall + its ISOLATED impact
|
|
318
|
+
const impact = Math.min(maxStep, k * biggest.usd / depth);
|
|
319
|
+
const terminalPx = down ? biggest.price * (1 - impact) : biggest.price * (1 + impact);
|
|
320
|
+
return {
|
|
321
|
+
dir: down ? 'down' : 'up', chain: false, isolated: true,
|
|
322
|
+
triggerPx: biggest.price, terminalPx, totalLiqUsd: biggest.usd,
|
|
323
|
+
hops: [{ price: biggest.price, usd: biggest.usd }],
|
|
324
|
+
dropFrac: Math.abs(terminalPx - mark) / mark,
|
|
325
|
+
biggestWall: biggest, depth, depthSource, k
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// Does a cascade sweep THROUGH a given price (e.g. the user's liq)? True when
|
|
329
|
+
// liqPx lies between triggerPx and terminalPx inclusive — the chain blows past it.
|
|
330
|
+
function cascadeHitsPrice(cascade, liqPx) {
|
|
331
|
+
if (!cascade || liqPx == null) return false;
|
|
332
|
+
const a = Math.min(cascade.triggerPx, cascade.terminalPx), b = Math.max(cascade.triggerPx, cascade.terminalPx);
|
|
333
|
+
return liqPx >= a && liqPx <= b;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
g.HLVM = {
|
|
337
|
+
CONTRACT_VERSION: '0.5',
|
|
338
|
+
dailyMovePct, volDistance, volColor, reachEstimate,
|
|
339
|
+
moneyPerDayPer1k, fmtMoney, fmtUsd, fmtApr, fmtPrice, fmtPrem, signClass, shortAddr, agoLabel,
|
|
340
|
+
maintMarginFraction, liqPrice, huntRiskCluster, suggestClearLeverage, evalLeverage,
|
|
341
|
+
computeCascade, cascadeHitsPrice, cascadeDepth, CASCADE_K,
|
|
342
|
+
fundingLeg, buildViewModel
|
|
343
|
+
};
|
|
344
|
+
})(typeof window !== 'undefined' ? window : this);
|