Documentation
VOLTERRA is a live predator and prey monitor for HYPE and SOL. It reads two public price series, fits the hundred year old Lotka and Volterra model with a lagged cross correlation, and reports which asset is currently following the other and feeding on its momentum. It runs entirely in your browser, with no backend. This is the full source and the documentation. Everything here is verifiable.
01 Overview
In nature, a predator's numbers always rise a beat behind its prey's, because a hunter has to eat before it can multiply. Two tightly coupled assets behave the same way: one leads, the other copies the move a little later. VOLTERRA measures that lag and names the predator.
The whole read is four steps, recomputed every few seconds:
- Ingest · pull the last 240 one minute closes for HYPE and SOL from the public feed.
- Detrend · strip each series of its slow trend, leaving only the swing around its baseline.
- Correlate · slide one series against the other across an hour each way and find the best offset.
- Resolve · the sign of that offset names the predator; its height is the pressure.
02 Quick start
The site is fully static: three files (index.html, styles.css, main.js) plus this page. Serve them from any static host and it works. There is nothing to build and nothing to run on a server.
If all you want is the reading, the estimator is one function call:
const H = detrendZ(await fetchSeries('HYPE')); const S = detrendZ(await fetchSeries('SOL')); const read = resolveRole(crossCorr(H, S, 60)); // → { predator: 'SOL', pressure: 0.71, lag: 7 }
The four functions are defined in full under function reference.
03 Architecture
There is no backend, no database, and no build step. Everything happens client side, on every visit.
browser
└─ fetch HYPE 1m ┐
└─ fetch SOL 1m ┘→ detrend ×2 → cross correlate → resolve → render
└→ redraw every 5s
File map:
| index.html | page structure and copy |
| styles.css | the monochrome terminal styling |
| main.js | feed, estimator, charts, and all live updates |
| source.html | this documentation |
The token, $VOLTERRA, is not part of this. It shares the project's name and idea but is not wired to the code in any way. The page never connects a wallet and never touches the chain.
04 Data feed
Prices come from one open endpoint, Hyperliquid's info API. No key, no account. For each coin the browser requests the last 240 one minute candles and keeps the closes. The request is identical to one a trading bot would make, and the endpoint allows it directly from the browser.
// POST https://api.hyperliquid.xyz/info { "type": "candleSnapshot", "req": { "coin": "HYPE", // or "SOL", or any HL market "interval": "1m", "startTime": now - 240*60*1000, "endTime": now } }
Each candle is an object; VOLTERRA reads the close field c.
// one candle { t, T, s, i, o, h, l, c, v, n } // c = close (the only field used)
The fetch itself, in full:
async function fetchSeries(coin) { const now = Date.now(); const r = await fetch('https://api.hyperliquid.xyz/info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'candleSnapshot', req: { coin, interval: '1m', startTime: now - 240*60*1000, endTime: now } }), }); return (await r.json()).map(c => Number(c.c)); }
The BTC value in the top ticker is a separate convenience call to Binance's public 24hr ticker; it is display only and not part of the estimator.
05 Detrend
Raw price is dominated by trend, and two trending assets correlate just because they both went up. To measure who leads rather than who rose, each series is stripped of its slow trend first.
Take the log price, subtract an exponential moving average with a 60 minute half life, then z score the remainder to zero mean and unit variance. What is left is the pure oscillation around each coin's own baseline, on a common scale so the two can be compared directly.
function detrendZ(arr, halflife = 60) { const log = arr.map(p => Math.log(p)); const k = Math.exp(-Math.log(2) / halflife); // EMA decay from half life const ema = [log[0]]; for (let i = 1; i < log.length; i++) ema.push(k*ema[i-1] + (1-k)*log[i]); const dev = log.map((x,i) => x - ema[i]); // remove the trend const m = dev.reduce((a,b)=>a+b,0)/dev.length; const s = Math.sqrt(dev.reduce((a,b)=>a+(b-m)**2,0)/dev.length) || 1e-9; return dev.map(x => (x-m)/s); // z score }
06 Cross correlation
With both series detrended, slide one past the other. For every offset tau from minus 60 to plus 60 minutes, compute the mean product of the two over their overlap. Because both are z scored, this is their correlation at that offset.
// C(tau) = mean over t of H[t] * S[t - tau] function crossCorr(h, s, maxLag) { const out = []; for (let tau = -maxLag; tau <= maxLag; tau++) { let num = 0, n = 0; for (let t = 0; t < h.length; t++) { const j = t - tau; if (j >= 0 && j < s.length) { num += h[t]*s[j]; n++; } } out.push({ tau, c: n ? num / n : 0 }); } return out; }
The offset with the strongest match is where the two line up best. Its sign says who is ahead: tau > 0 means SOL's past predicts HYPE's present, so SOL leads and HYPE copies.
07 Resolve
Pick the offset with the highest positive correlation. That single point gives all three outputs.
function resolveRole(spec) { let peak = spec[0]; for (const p of spec) if (p.c > peak.c) peak = p; const pressure = Math.max(0, Math.min(1, peak.c)); // 0..1 → shown as 0..100 let predator = null, prey = null; if (peak.tau > 0) { predator = 'HYPE'; prey = 'SOL'; } // HYPE lags → feeds else if (peak.tau < 0) { predator = 'SOL'; prey = 'HYPE'; } // SOL lags → feeds // peak.tau === 0 → in sync, no predator return { peak, pressure, predator, prey, lag: Math.abs(peak.tau) }; }
08 Cycle period
Optionally, VOLTERRA estimates how long one full loop of the chase takes by reading the dominant period of the leader's swing. It walks the autocorrelation of the leader and takes the first clear positive peak after the curve crosses zero. When no clean period stands out, it returns nothing rather than inventing one, so the cycle field is shown only when it is genuinely measurable.
function estimateCycle(x) { const acf = lag => { let s=0,c=0; for(let t=lag;t<x.length;t++){s+=x[t]*x[t-lag];c++;} return c?s/c:0; }; const a0 = acf(0) || 1; let crossed=false, best=-1, bestVal=-Infinity; for (let lag=2; lag<=120; lag++) { const v = acf(lag)/a0; if (!crossed && v<0) crossed=true; if (crossed && v>bestVal) { bestVal=v; best=lag; } } return (crossed && bestVal>0.12) ? best : null; // minutes, or null }
09 Parameters
| name | value | meaning & tuning |
|---|---|---|
| N | 240 | window length in 1m candles (4h). Larger = longer memory, slower to react. Smaller = twitchier. |
| MAXLAG | ±60 | how far the series are slid, in minutes. Widen to catch slower lead times, narrow to focus on fast ones. |
| HALFLIFE | 60 | EMA half life for detrending, in minutes. Shorter strips more trend; longer keeps more of the slow move. |
| interval | 1m | candle size pulled from the feed. |
| refresh | 5s | how often the page re fetches and recomputes. |
10 Output
| field | type | meaning |
|---|---|---|
| predator | HYPE / SOL / null | the coin that lags and feeds. null when the two are in sync. |
| prey | HYPE / SOL | the coin that leads, out of its own momentum. |
| pressure | 0 – 100 | strength of the best match. How tightly the pair is coupled right now. |
| lag | minutes | how far behind the predator runs. |
| cycle | minutes / none | length of one loop, when a clear period exists. |
11 Function reference
| signature | returns |
|---|---|
| fetchSeries(coin) | Promise<number[]> · 240 closes |
| detrendZ(arr, halflife=60) | number[] · z scored, detrended |
| crossCorr(h, s, maxLag) | {tau, c}[] · correlation at each offset |
| resolveRole(spec) | {peak, pressure, predator, prey, lag} |
| estimateCycle(x) | number | null · cycle in minutes |
All five are plain functions with no dependencies. Copy them into any project as is.
12 Run it yourself
Open a browser console on any page and paste the four functions plus the compose block from quick start. The feed allows the request directly from the browser, so it runs with no server, no key, and no install. The number you get is the number this site shows, give or take the seconds between your call and ours.
To watch it live without the UI, wrap it in an interval:
setInterval(async () => { const H = detrendZ(await fetchSeries('HYPE')); const S = detrendZ(await fetchSeries('SOL')); console.log(resolveRole(crossCorr(H, S, 60))); }, 5000);
13 Fork & adapt
The model does not care that the inputs are HYPE and SOL. Anything you can express as two time series works.
- New pair · change the two coin arguments to any two Hyperliquid markets.
- New source · replace fetchSeries with any function that returns an array of closes.
- New horizon · raise N for memory, move MAXLAG for the lead range.
- New venue · the same four steps run on equities, FX, or any correlated pair, on or off chain.
// example: point it at a different pair const A = detrendZ(await fetchSeries('BTC')); const B = detrendZ(await fetchSeries('ETH')); const read = resolveRole(crossCorr(A, B, 90)); // wider 90m lead search
14 Caveats & limits
- Not a forecast. It reports who has been leading, not where price goes next.
- High correlation inflates pressure. HYPE and SOL often move together, so pressure runs high; that is real, not a bug.
- Boundary lags. When the best match sits at the edge of the search range, the lead is at or beyond what the window can resolve; read it loosely.
- In sync. A best match at zero offset means neither clearly leads, and no predator is named.
- Feed outage. If Hyperliquid is unreachable, the chart shows a simulated reference cycle and labels it as such. It never passes simulation off as live.
- One minute resolution. The smallest lead the model can see is one minute.
15 License & disclaimer
- The code here is free to read, copy, modify, and ship, with no warranty of any kind.
- The reading is a statistical estimate over a fixed window. It is not a price prediction, a signal, or financial advice.
- $VOLTERRA is an experimental token sharing this project's name and idea. It is not wired to this code, and holding it grants no rights in HYPE, SOL, or Hyperliquid.
- Crypto is volatile and carries real risk. Never put in more than you are comfortable losing.