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

# RSI PCA forecast

> RSI at 23 lengths reduced to 3 principal components, a regression on them fitted walk-forward, and trades at its extremes.

![RSI PCA forecast on a BTC chart](https://mintcdn.com/0xarchive-e895b8e7/vVPD5xxz36aXgMyV/images/terminal/examples/rsi-pca-forecast.webp?fit=max\&auto=format\&n=vVPD5xxz36aXgMyV\&q=85\&s=76cb7cc407697e5e7ef23228f90435b2)

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

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

RSI at 23 lengths, from 2 to 24, reduced to the 3 ways they move together, and a forecast of the 6-bar return from those 3. The forecast is refit every 100 bars on the 500 bars before, and frozen in between: a walk-forward test, a port of [neurotrader888's RSI-PCA](https://github.com/neurotrader888/RSI-PCA). The histogram is the forecast in basis points, the dashed lines the long and short thresholds, the shade the position, and a diamond under the candles marks each refit.

## How it works

`lengths.map((n) => ta.rsi(n))` builds the 23 indicators. `ml.forward` hands each bar's RSIs to `ml.pcr` only once the 6-bar return after them is known. Every 100 bars `ml.pcr` finds the principal components of the last 500 labelled bars, fits the return on the first 3, and freezes the components and the coefficients together, so the model never applies old coefficients to new components.

A forecast above the 99th percentile of the fit's own in-sample forecasts is a long signal, below the 1st a short one. `ta.sma` over 6 bars holds each signal for the horizon, as the original does, so the position runs from −1 to 1.

The original trains on 2 years of hourly bars and refits yearly. The chart loads far fewer bars, so the defaults are 500 and 100; both are inputs. The original also projects on rows of the eigenvector matrix, which are not principal components; this uses the columns.

## The script

```ts rsi-pca-forecast.ts theme={"theme":"github-dark"}
// RSI at 23 lengths moves in a few common ways. Every 100 bars the model takes the last 500 bars
// whose 6-bar return is known, finds the RSIs' principal components, fits the return on the first
// 3, and freezes all of it until the next refit: a walk-forward test, as in neurotrader888's
// RSI-PCA. A forecast above the 99th percentile of the fit's own in-sample forecasts is a long
// signal, below the 1st a short one; the shade is the position, each signal held for 6 bars.
indicator('RSI PCA forecast', { shortName: 'RSI PCA', precision: 1 });

const horizon = input.int('Bars ahead', 6, { min: 1, max: 200 });
const training = input.int('Training bars', 500, { min: 50, max: 5000 });
const refit = input.int('Refit every', 100, { min: 1, max: 5000 });
const components = input.int('Components', 3, { min: 1, max: 10 });
const lengths = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];

const chart = bars();
const rsis = lengths.map((n) => ta.rsi(n));
const labels = ml.forward(horizon, lengths.length);
const model = ml.pcr(lengths.length, { window: training, components, refit, intercept: false, quantiles: [0.01, 0.99] });
const position = ta.sma(horizon);

const forecast = plot.histogram('Forecast', { unit: ' bp' });
const long = plot.line('Long above', { color: color.up, style: 'dashed', unit: ' bp' });
const short = plot.line('Short below', { color: color.down, style: 'dashed', unit: ' bp' });
const held = plot.line('Position', { hidden: true, precision: 2 });
const shade = plot.background('Position');
const refits = plot.markers('Refit', { pane: 'price' });

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;
  const signal = guess > high ? 1 : guess < low ? -1 : 0;
  const now = position.update(signal);
  forecast.set(guess * 10_000, signal > 0 ? color.up : signal < 0 ? color.down : color.neutral);
  long.set(high * 10_000);
  short.set(low * 10_000);
  held.set(isNa(guess) ? na : now);
  shade.set(isNa(guess) || now === 0 ? null : color.withAlpha(now > 0 ? color.up : color.down, 0.08 + 0.17 * Math.abs(now)));
  if (fit.refitted) refits.add({ position: 'below', shape: 'diamond', color: color.neutral });
});
```

## Next step

See the next example, [RSI cross, pasted Pine](https://docs.0xarchive.io/terminal/examples/rsi-pine.md).


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