A model that learns the next bars’ return from taker imbalance, open interest and funding, without seeing the future.
BTC on 1-hour bars in the 0xArchive Terminal, 2026-10-25.
Open on a chart
Load it on BTC 1h in the terminal, then change its settings or open it in the script editor.
A forecast of the return over the next Bars ahead, in basis points, from three readings only 0xArchive has together: the bar’s taker imbalance, its change in open interest, and the funding rate. Under it, Skill: the correlation of each forecast with the return that actually followed, over the last 500. Skill near 0 means the model knows nothing on this market and interval, and that is a real answer.
Each reading becomes a z-score over 200 bars (stats.zscore), so the three weigh alike. ml.forward holds each bar’s readings until the return after them is known, and only then hands them to ml.rls, which learns from every bar so far with older bars weighing less (Memory). Forecasts are graded the same way: a second ml.forward keeps each one until its outcome exists.
A model’s skill on the bars it was built and tuned on is not evidence. Change the inputs or the settings while watching Skill, and you will find settings that look good by chance.
// A model that learns, bar by bar, what the next bars' return has followed: taker imbalance, the// change in open interest and funding, all from 0xArchive's data. Each input is a z-score so the// three weigh alike, and a missing one counts as 0. `ml.forward` holds each bar's inputs until their return is known, so the// model never learns from the future. Skill is the correlation of each forecast with the return// that followed it, over the last 500: the honest test of whether it knows anything.indicator('Order-flow forecast', { shortName: 'OF forecast', precision: 1 });const horizon = input.int('Bars ahead', 12, { min: 1, max: 200 });const memory = input.float('Memory', 0.998, { min: 0.9, max: 1, step: 0.001, tooltip: 'Each older bar weighs this much less: 1 remembers every bar alike' });const chart = bars();const oi = openInterest();const fund = funding();const imbalance = stats.zscore(200);const oiChange = stats.zscore(200);const rate = stats.zscore(200);const labels = ml.forward(horizon, 3);const model = ml.rls(3, { forget: memory, ridge: 1 });const graded = ml.forward(horizon, 1);const skill = stats.corr(500);const forecast = plot.histogram('Forecast', { unit: ' bp' });const score = plot.line('Skill', { pane: 'skill', precision: 3 });plot.level('No skill', 0, { pane: 'skill', style: 'dotted' });chart.onClose(() => { const flow = chart.volume > 0 ? (chart.buyVolume - chart.sellVolume) / chart.volume : na; const built = oi.close({ usd: true }) > 0 ? oi.change({ usd: true }) / oi.close({ usd: true }) : na; // A missing reading (no funding on this venue, say) counts as usual: 0 standard deviations. const x = [nz(imbalance.update(flow)), nz(oiChange.update(built)), nz(rate.update(fund.rate({ hours: 8760 })))]; // A label arrives `horizon` bars after its inputs; until then the model learns nothing (y is na). const done = labels.update(x, chart.close); model.update(done === null ? na : done.ret, done === null ? x : done.x); const guess = model.predict(x); forecast.set(guess * 10_000, guess >= 0 ? color.up : color.down); const back = graded.update([guess], chart.close); score.set(skill.update(back === null ? na : back.x[0], back === null ? na : back.ret));});