> ## 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.

# Statistics and machine learning

> Rolling statistics, regression that learns bar by bar, Kalman filters, PCA, walk-forward PCA regression, matrices, and labels a model cannot see early.

Build signals the way a quant does, inside the script: rolling statistics, models that learn one bar at a time, principal components, and matrices. Each model is an instance, like an indicator: create it at the top, update it on every bar, and it rolls back with the forming bar.

## Statistics

`stats.*` are rolling statistics that match pandas, so a number checked in a notebook is the number on the chart. Variance divides by n − 1, as pandas does; `ta.stdev` keeps TradingView's n.

```ts theme={"theme":"github-dark"}
const z = stats.zscore(100);
const corr = stats.corr(50);
const vol = stats.vol(30, 'yangZhang', { annualize: true });

chart.onBar(() => {
  const reading = z.update(chart.close);
  const together = corr.update(chart.close, other.close);
  const annual = vol.update(chart);
});
```

`zscore`, `corr`, `cov`, `beta`, `skew`, `kurt`, `quantile`, `median` and `autocorr` work over the last `length` values. `ewmVar`, `ewmCov` and `ewmCorr` weigh by a half-life. `vol` estimates volatility from each bar's prices: from closes, or with Parkinson, Garman-Klass, Rogers-Satchell or Yang-Zhang, which use the high, the low and the gap from the previous close.

> [!NOTE]
>
> A bar with `na` in an input returns `na` and leaves the window as it was, so a window is the last `length` bars that had values. pandas would count the missing bar inside the window instead.

## Regression

`ml.ols(length, inputs)` fits a least-squares line over the last `length` bars; `ml.rls(inputs)` learns from every bar so far, each older bar weighing `forget` times less. Each update takes this bar's `y` and its list of `x`, and returns the coefficients, R², t-statistics and two honest numbers: `fitted`, the estimate of this bar's `y` from what the model knew before it, and `residual`, the miss.

```ts theme={"theme":"github-dark"}
const fit = ml.rls(2, { forget: 0.995, ridge: 1 });

chart.onBar(() => {
  const r = fit.update(chart.close, [other.close, third.close]);
  const ahead = fit.predict([other.close, third.close]);
});
```

`ridge` keeps the fit steady when inputs move together; it leaves the intercept alone, as scikit-learn's Ridge does. Give the inputs similar scales (z-scores, say) when you use it, so it weighs them alike.

> [!WARNING]
>
> The t-statistics assume each bar's error is independent. Labels that overlap, such as 12-bar forward returns sampled every bar, share 11 bars each, and the t-statistics overstate how significant a coefficient is.

## Labels a model cannot see early

To learn what came *after* a reading, a model needs the return that followed it, which does not exist until later. `ml.forward(horizon, inputs)` holds each bar's inputs and hands them back with the log return since, `horizon` bars later and never sooner. Train on what it returns, and the model cannot learn from the future.

```ts theme={"theme":"github-dark"}
const labels = ml.forward(12, 3);
const model = ml.rls(3, { forget: 0.998 });

chart.onClose(() => {
  const x = [a.value, b.value, c.value];
  const done = labels.update(x, chart.close);
  model.update(done === null ? na : done.ret, done === null ? x : done.x);
  forecast.set(model.predict(x));
});
```

A model updated with `na` as `y` learns nothing that bar, so it can be updated on every bar as the rules ask, even before the first label. Grade a forecast the same way: hold it in a second `ml.forward` and correlate it with the return that arrives, as [Order-flow forecast](https://docs.0xarchive.io/terminal/examples/order-flow-forecast.md) does.

## Kalman filters

`ml.kalman` smooths a value that moves as a random walk and is seen with noise: it returns the level, its uncertainty, and each bar's surprise. `ml.kalmanBeta` estimates `y = α + βx` with α and β drifting, the hedge ratio of a pairs trade, and returns the spread and its z-score. See [Kalman pairs spread](https://docs.0xarchive.io/terminal/examples/kalman-pairs.md).

## Principal components

`ml.pca(inputs)` finds the ways a list of numbers moves together over a window: RSI at many lengths, or many markets' returns. Each bar it returns the `scores` (where the inputs sit on each component, what a model learns from), the share each component explains, the `loadings`, and each input's `residual`, the part the components don't explain.

```ts theme={"theme":"github-dark"}
const rsis = [4, 8, 12, 16, 20, 24, 28, 32].map((n) => ta.rsi(n));
const pca = ml.pca(8, { window: 500, components: 3, standardize: true });

chart.onClose(() => {
  const p = pca.update(rsis.map((rsi) => rsi.update(chart.close)));
  const first = p.scores[0];
});
```

The components are computed exactly, from the window's covariance (or its correlations, with `standardize`), on closed bars. They are recomputed every `refit` bars and held in between, so a model fed the scores doesn't have its inputs shift under it every bar. `refit: 'auto'`, the default, recomputes every bar up to 20 inputs, then every 4 bars at 30 inputs, every 8 at 40 and every 16 at 50, to keep the cost near 20 inputs'. Each component's largest loading is positive, so a component never flips sign from one refit to the next.

## Forecasting from components, walk-forward

A model that keeps learning from `ml.pca` scores has the components move under it at every refit. When two smaller components swap places, its coefficients suddenly apply to the wrong one until it learns again. `ml.pcr(inputs)` avoids that. It fits the components and the regression on them together, on the same window, then freezes both, centre and scale included, until the next refit. That is a walk-forward test: each stretch is forecast by a fit that only saw the bars before it.

```ts theme={"theme":"github-dark"}
const rsis = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24].map((n) => ta.rsi(n));
const labels = ml.forward(6, 12);
const model = ml.pcr(12, { window: 500, components: 3, refit: 100, quantiles: [0.01, 0.99] });

chart.onClose(() => {
  const x = rsis.map((rsi) => rsi.update(chart.close));
  const done = labels.update(x, chart.close);
  const fit = model.update(done === null ? na : done.ret, done === null ? x : done.x);
  const guess = model.predict(x);
  const [low, high] = fit.quantiles;
});
```

Feed it the pairs `ml.forward` returns. Once `window` pairs are in, it fits on closed bars every `refit` pairs, and `fit.refitted` is true on that bar. `quantiles` gives thresholds from the fit's own in-sample forecasts, so "above the 99th percentile" means unusually strong by that fit's standard. `predict` and `scores` use the frozen fit, and return `na` before the first. See [RSI PCA forecast](https://docs.0xarchive.io/terminal/examples/rsi-pca-forecast.md).

## Matrices

`linalg` has matrices for scripts that build their own models: products, `solve`, `inv`, and the Cholesky, QR, symmetric eigen and singular value decompositions. A matrix made in a handler is a plain value; `state.mat(rows, cols)` keeps one across bars and rolls it back with the forming bar.

```ts theme={"theme":"github-dark"}
const a = linalg.mat(2, 2, [4, 1, 1, 3]);
const { values, vectors } = linalg.eigSym(a);
const x = linalg.solve(a, [1, 2]);
```

A singular `solve` or `inv`, or a `cholesky` of a matrix that is not positive definite, returns `na` throughout rather than stopping the script. Results are the same bits in every browser. A decomposition counts about n³ steps against the [step budget](https://docs.0xarchive.io/terminal/guides/limits.md).

## Next step

Read other markets, intervals and 0xArchive's data in [data and timeframes](https://docs.0xarchive.io/terminal/guides/data.md).


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