// 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 });
});