> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xarchive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Market Read

> Who is opening and who is closing across 20 perps, as a map of price against open interest with tails, a table, a stacked history and how much the market moves as one.

![Market Read in a Script panel, clocked by BTC](https://mintcdn.com/0xarchive-e895b8e7/hbSnH0kvgsdLDrH8/images/terminal/examples/market-read.webp?fit=max\&auto=format\&n=hbSnH0kvgsdLDrH8\&q=85\&s=0d604ffb4489b3cbc6ab02f6ed3204b4)

BTC on 1-hour bars in the 0xArchive Terminal, 2026-10-26.

> **[Open on a chart](https://terminal.0xarchive.io/?symbol=BTC\&interval=1h\&scripts=example%3Amarket-read)**
>
> Load it on BTC 1h in the terminal, then change its settings or open it in the script editor.

Who is opening positions and who is closing them, across 20 of Hyperliquid's largest perps at once. It is a panel script: it runs in a **Script panel** of its own, clocked by BTC's hourly bars, rather than on a chart. Adding it from a chart's **Indicators** opens one. The panel's settings change the market and timeframe it runs on.

* **Positioning**, the map: each market by its price change and its open interest change over the last `Change bars` bars. Price and open interest both up is new longs; price up on falling open interest is shorts covering; price down on rising open interest is new shorts; both down is longs exiting. Each point trails where it was, as a relative rotation graph does, so you can see which way it is turning.
* **Markets**, the table: each market's quadrant, its two changes, its own move (its return less what its beta to the market explains) and that beta. Sort by any column; a market's name shows it on the linked charts.
* **Positioning history**: how many of the 20 sat in each quadrant on every bar, stacked.
* The tiles: the quadrant most markets are in, the equal-weighted market's move, the average change in open interest, and **Absorption**.

## How it works

Open interest is counted in coins, so a change in it is positions opened or closed, not price revaluing the old ones. [`panel.scatter`](https://docs.0xarchive.io/terminal/reference/panels) places each market, with its earlier positions as `tail`; [`panel.table`](https://docs.0xarchive.io/terminal/reference/panels) and [`panel.stats`](https://docs.0xarchive.io/terminal/reference/panels) hold the rest, and a [`graph`](https://docs.0xarchive.io/terminal/guides/outputs#graphs) with `stack: true` the history.

Absorption is a rolling [PCA](https://docs.0xarchive.io/terminal/reference/machine-learning.md) of the 20 markets' hourly returns (Kritzman, Li, Page and Rigobon, 2010): the share of their variance the top 4 components explain. Over 75% the market is one trade, and a market's own move matters less than the beta it carries. Each market's beta is a rolling [`stats.beta`](https://docs.0xarchive.io/terminal/reference/machine-learning) against the equal-weighted market, fitted on bars before the one it is used on.

## The script

```ts market-read.ts theme={"theme":"github-dark"}
// Market Read: who is opening and who is closing across 20 of Hyperliquid's largest perps, in a
// panel of its own. BTC's bars clock it; every market is read the same way.
//   Map        each market by its price change and its open interest change over `momBars` bars,
//              in four quadrants: price and OI up is new longs, price up on falling OI is shorts
//              covering, price down on rising OI is new shorts, both down is longs exiting. Each
//              point trails where it was, as a relative rotation graph does, so the way it is
//              turning shows.
//   History    how many of the 20 sat in each quadrant on every bar, stacked.
//   Absorption a rolling PCA of the 20 markets' returns (Kritzman, Li, Page and Rigobon, 2010): the
//              share of their variance the top 4 components (a fifth of the assets) explain. High
//              means everything is one trade.
//   Own move   residual momentum: each market's return less its beta to the equal-weighted market
//              times the market's return, what it did on its own.
// Open interest is in coins, so it counts positions opened and closed, not price moving the old ones.
// Every value is as of a closed bar; the PCA and betas are fitted on bars before.
indicator('Market Read', { panel: { market: 'BTC', interval: '1h' } });

const momBars = input.int('Change bars', 42, { min: 2, max: 500, group: 'Model', tooltip: 'Bars the price and open interest changes are measured over (42 on 4h is 7 days)' });
const winLen = input.int('PCA window', 120, { min: 30, max: 1000, group: 'Model', tooltip: 'Bars of returns the components and betas are fitted on' });
const refit = input.int('Refit every', 6, { min: 1, max: 100, group: 'Model' });
const tailPoints = input.int('Tail points', 4, { min: 0, max: 30, group: 'Display', tooltip: 'Earlier positions each market trails on the map' });
const tailStep = input.int('Tail step', 6, { min: 1, max: 50, group: 'Display', tooltip: 'Bars between a tail\'s points' });
const historyBars = input.int('History bars', 240, { min: 20, max: 1000, group: 'Display' });

const UNIVERSE = ['BTC', 'ETH', 'SOL', 'HYPE', 'XRP', 'DOGE', 'SUI', 'AVAX', 'LINK', 'ENA', 'AAVE', 'TAO', 'ZEC', 'BNB', 'LTC', 'ADA', 'WLD', 'FARTCOIN', 'kPEPE', 'WIF'];
const N = UNIVERSE.length;
const K = 4;

const LONGS = color.up;
// The theme's own blue and amber (its study palette's slots 12 and 0), so they read on light themes too.
const COVER = 'theme:study:12';
const EXIT = 'theme:study:0';
const SHORTS = color.down;
// Quadrants by index: 0 new longs, 1 shorts covering, 2 longs exiting, 3 new shorts.
const QUAD = [LONGS, COVER, EXIT, SHORTS];
const QNAME = ['New longs', 'Shorts covering', 'Longs exiting', 'New shorts'];

const chart = bars();
const markets = UNIVERSE.map((m) => bars({ market: m }));
const ois = UNIVERSE.map((m) => openInterest({ market: m }));
const prev = UNIVERSE.map(() => series(2));
const logPx = UNIVERSE.map(() => series(600));
const logOi = UNIVERSE.map(() => series(600));
// Each market's two changes per bar, kept for its tail.
const chg = UNIVERSE.map(() => series(1600));
const oiChg = UNIVERSE.map(() => series(1600));
const times = series(1001);
const mkt = series(600);
const betas = UNIVERSE.map(() => stats.beta(winLen));
const pca = ml.pca(N, { window: winLen, components: K, standardize: true, refit });
const mktLevel = state.bar(0);

const tiles = panel.stats('', { format: 'percent', precision: 1 });
const map = panel.scatter('Positioning', {
  x: { title: 'Price change', format: 'percent', precision: 1, scale: 'sqrt' },
  y: { title: 'OI change', format: 'percent', precision: 1, scale: 'sqrt' },
  quadrants: [{ label: QNAME[3], color: SHORTS }, { label: QNAME[0], color: LONGS }, { label: QNAME[2], color: EXIT }, { label: QNAME[1], color: COVER }],
});
const table = panel.table('Markets', {
  columns: [
    { title: 'Market', format: 'market' },
    { title: 'Positioning', format: 'text' },
    { title: 'Price', format: 'percent', precision: 1, signed: true },
    { title: 'OI', format: 'percent', precision: 1, signed: true },
    { title: 'Own move', format: 'percent', precision: 1, signed: true },
    { title: 'Beta', format: 'number', precision: 2 },
  ],
  sort: 'Own move',
});
const history = graph('Positioning history', { axis: 'time', stack: true, format: 'percent', precision: 0 });
const stack = QUAD.map((hue, q) => history.bars(QNAME[q], { color: hue }));

function quadrant(ret: number, oi: number): number {
  return ret >= 0 ? (oi >= 0 ? 0 : 1) : oi >= 0 ? 3 : 2;
}

const pct = (logChange: number) => (isNa(logChange) ? na : (Math.exp(logChange) - 1) * 100);

chart.onBar(() => {
  const r: number[] = [];
  let sum = 0;
  let have = 0;
  for (let i = 0; i < N; i += 1) {
    const c = markets[i].close;
    const p = prev[i].ago(1);
    prev[i].set(c);
    const x = isNa(c) || isNa(p) || p <= 0 ? na : Math.log(c / p);
    r.push(x);
    if (!isNa(x)) {
      sum += x;
      have += 1;
    }
    logPx[i].set(isNa(c) || c <= 0 ? na : Math.log(c));
    // A market's newest hour can have no open interest yet (its minutes land a poll behind): it holds the last it had.
    const o = ois[i].available ? ois[i].close() : na;
    logOi[i].set(isNa(o) || o <= 0 ? logOi[i].ago(1) : Math.log(o));
  }
  const m = have >= N - 2 ? sum / have : na;
  if (!isNa(m)) mktLevel.value += m;
  mkt.set(mktLevel.value);
  times.set(chart.time);
  const pc = pca.update(r.map((x) => (isNa(x) ? nz(m) : x)));
  let ar = 0;
  for (let k = 0; k < K; k += 1) ar += nz(pc.explained[k]);
  ar = isNa(pc.explained[0]) ? na : ar;

  const counts = [0, 0, 0, 0];
  let n = 0;
  const mk = mkt.value - mkt.ago(momBars);
  const resid: number[] = [];
  const betaOf: number[] = [];
  for (let i = 0; i < N; i += 1) {
    const b = betas[i].update(r[i], m);
    const pr = logPx[i].value - logPx[i].ago(momBars);
    const oc = logOi[i].value - logOi[i].ago(momBars);
    chg[i].set(pr);
    oiChg[i].set(oc);
    betaOf.push(b);
    resid.push(isNa(b) || isNa(pr) || isNa(mk) ? na : pr - b * mk);
    if (isNa(pr) || isNa(oc)) continue;
    counts[quadrant(pr, oc)] += 1;
    n += 1;
  }

  // The stacked history: each quadrant's share of the markets with data, kept for `historyBars` bars.
  for (let q = 0; q < 4; q += 1) stack[q].set(chart.time, n < N - 2 ? na : (counts[q] / n) * 100);
  const old = times.ago(historyBars);
  if (!isNa(old)) for (let q = 0; q < 4; q += 1) stack[q].delete(old);

  if (!chart.isLast) return;

  for (let i = 0; i < N; i += 1) {
    const pr = chg[i].value;
    const oc = oiChg[i].value;
    const q = isNa(pr) || isNa(oc) ? -1 : quadrant(pr, oc);
    const tail: [number, number][] = [];
    for (let k = tailPoints; k >= 1; k -= 1) {
      const tx = pct(chg[i].ago(k * tailStep));
      const ty = pct(oiChg[i].ago(k * tailStep));
      if (!isNa(tx) && !isNa(ty)) tail.push([tx, ty]);
    }
    map.point(UNIVERSE[i], pct(pr), pct(oc), { label: UNIVERSE[i], color: q < 0 ? null : QUAD[q], tail, note: q < 0 ? undefined : QNAME[q] });
    table.row(UNIVERSE[i], [UNIVERSE[i], q < 0 ? '—' : QNAME[q], pct(pr), pct(oc), pct(resid[i]), betaOf[i]], { colors: [null, q < 0 ? null : QUAD[q]] });
  }

  let lead = 0;
  for (let q = 1; q < 4; q += 1) if (counts[q] > counts[lead]) lead = q;
  let oiSum = 0;
  for (let i = 0; i < N; i += 1) if (!isNa(oiChg[i].value)) oiSum += oiChg[i].value;
  const hours = chart.interval.isTime ? (momBars * chart.interval.ms) / 3600000 : na;
  const span = isNa(hours) ? `${fmt.num(momBars, 0)} bars` : hours >= 48 && hours % 24 === 0 ? `${fmt.num(hours / 24, 0)}d` : `${fmt.num(hours, 0)}h`;
  const regime = isNa(ar) ? '' : ar > 0.75 ? 'One trade' : ar > 0.6 ? 'Coupled' : 'Dispersed';
  tiles.set('Positioning', QNAME[lead], { color: QUAD[lead], note: `${fmt.num(counts[lead], 0)} of ${fmt.num(n, 0)} markets` });
  tiles.set('Market', pct(mk), { signed: true, note: `equal-weighted, ${span}` });
  tiles.set('Open interest', n > 0 ? pct(oiSum / n) : na, { signed: true, note: `average, ${span}` });
  tiles.set('Absorption', isNa(ar) ? na : ar * 100, { note: isNa(ar) ? 'fitting' : `${regime} · top ${K} components` });
});
```

## Next step

See the next example, [Funding carry](https://docs.0xarchive.io/terminal/examples/funding-carry.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.