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.
Nothing is stored, cached, or signed by a server. If you do not trust a number, you can recompute it yourself from the same public data in under a minute. The reading is the product.

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.htmlpage structure and copy
styles.cssthe monochrome terminal styling
main.jsfeed, estimator, charts, and all live updates
source.htmlthis 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

namevaluemeaning & tuning
N240window length in 1m candles (4h). Larger = longer memory, slower to react. Smaller = twitchier.
MAXLAG±60how far the series are slid, in minutes. Widen to catch slower lead times, narrow to focus on fast ones.
HALFLIFE60EMA half life for detrending, in minutes. Shorter strips more trend; longer keeps more of the slow move.
interval1mcandle size pulled from the feed.
refresh5show often the page re fetches and recomputes.

10 Output

fieldtypemeaning
predatorHYPE / SOL / nullthe coin that lags and feeds. null when the two are in sync.
preyHYPE / SOLthe coin that leads, out of its own momentum.
pressure0 – 100strength of the best match. How tightly the pair is coupled right now.
lagminuteshow far behind the predator runs.
cycleminutes / nonelength of one loop, when a clear period exists.
None of these is a forecast. They describe the recent past over a fixed window, not the next move.

11 Function reference

signaturereturns
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
If you build on it, the one ask is to keep it honest: show your work, and label any simulated or filled in value as exactly that.

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.
VOLTERRA · docs · 2026 ← back to the monitor · @volterrarun