Skip to content

Keep built-in indicators on their candles while history loads - #294

Merged
alexgrover merged 1 commit into
devfrom
fix/native-indicators-history-load
Oct 16, 2026
Merged

alexgrover merged 1 commit into
devfrom
fix/native-indicators-history-load

Conversation

@alexgrover

@alexgrover alexgrover commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Background

On production, a built-in EMA on AAPL 15m could vanish when zooming out, or sit shifted left of its candles and stop well before the last one. Both come from history arriving in stages: the data server answers a cold symbol with growing snapshots while it completes the history, and the chart paints each one.

The renderer paints an indicator's points index-aligned from the bar its computation started on (the model's anchorTime), bar 0 when absent. Script indicators follow two rules that keep them correct across a staged load; native indicators followed neither:

  • They stamp anchorTime. Natives did not, so the renderer pinned their first point to whatever the chart's first bar was at paint time. A native computed over the first snapshot was painted from the first bar of the next, deeper one: the whole plot slid back in time by the number of bars that landed in front. A first paint can be as small as 20 bars, so a deep history pushed the plot off-screen entirely: the "disappeared" report. A depth trim shifted natives the other way, because that path's completion fires while the switch still gates every poke.
  • They hold their first run until the history completes. Natives computed immediately over the partial history. The progressive snapshots never notified anyone (only the chunked backfill does), so natives realigned only at completion or on the next live tick. The tick hid the bug on busy markets; on a quiet one it stayed until the load completed.

Summary

  • Every native emit is anchored to the chart's first bar at the time of the call (buildNativeModel), so an output stays on its candles across prepends and trims until its next compute. This covers every native, built-in or plugin, with no change on their side.
  • New optional descriptor flag waitsForHistory. Such a native starts only once the history is complete: it gets a loading legend row meanwhile (the same placeholder path script indicators use, so ready / indicator:added wait and inspect() skips it), then one compute over every candle. A later backfill (a deeper depth) holds onBars(), live ticks included, while the previous output stays aligned through its anchor, and re-runs at completion. A depth trim during an in-flight load releases held natives. The classic studies opt in; volume and VPVR do not. Absent flag = today's behavior.
  • Progressive snapshots poke consumers with 'backfill', the same as a chunked backfill chunk. Script engines ignore it; the footprint engine and the volume layer treat it as a no-op on a live chart.
  • A price-style transform change (Heikin Ashi) re-runs started natives over the new candles instead of waiting for the next tick: ctx.bars() is the view, so the synthetic candles are the studies' source.
  • Blanked models drop their anchor. On a market switch, the old market's anchor matched no bar on the new axis and the renderer warned on every switch (already true for script indicators, and natives would have joined them).
  • Docs: docs/contributing/plugin-sdk.md describes staged history and the new flag. CHANGELOG.md has two fixes and one addition under [Unreleased].

Public API: one additive, optional field on NativeIndicatorDescriptor (@luxalgo/vela/plugin). Non-breaking. Contract comments on emit and onBars now state the alignment rule and the price-style poke.

End-to-end verification

Exercised in the Vela playground (Chromium) against the real renderer. A stand-in progressive provider registered from the console served real BTCUSDT 1h bars from Binance, released snapshots on command, and streamed no live ticks (a quiet market). A probe read the renderer's scene: each indicator's points, its anchor offset, and the bar each point is painted on.

  • Before (dev): after a 1,500-bar first paint and a 2,000-bar snapshot, all 1,500 EMA and Bollinger points sat on the wrong candles and the newest candle had no value. In the recent view the studies were gone; zoomed out they ran 500 bars early, matching the production screenshot.
  • After: during the load, EMA and BB show the loading state and paint nothing, while volume paints from the first candles. On completion both cover all 2,000 bars, 0 misplaced, up to the newest candle. After switching to Heikin Ashi through the style menu, the EMA's last value equals an EMA over the Heikin Ashi closes (82,093.77; over the regular closes it would be 82,072.69).
  • Unit tests: test/native-history-load.test.ts, 10 tests: progressive load, the hold and its loading row, volume not held, ticks during the hold, a default-policy native following snapshots, depth trim, trim during an in-flight load, a deeper depth held then recomputed (sampled at each chunk), market-switch blanking, and Heikin Ashi both ways. All 10 fail on dev and pass here.
  • Gate: typecheck, lint, test (2,192 passed) and build all pass. One timing-based replay test (play() pace, 100 ms wall-clock sleep) failed once under full-suite load, then passed on the rerun and three times alone.
  • Workspace oracle suite (Vela): 25/25 deterministic. Those scenarios exercise script features, so this is a regression check, not proof of this fix.
  • Not verified: the real data server through the Pro playground, and Pro's natives or the toolkits against this build.

Checklist

  • This PR does one thing (one fix, one feature, one refactor, or one docs change)
  • npm run typecheck, npm run lint, npm test, and npm run build all pass
  • Tests have been added / updated (for bug fixes / features)
  • Documentation in docs/ has been added / updated (for public interface or feature changes)
  • CHANGELOG.md has an entry under [Unreleased] (for user-visible changes)
  • I have signed the CLA (the bot asks in this PR)
  • I have reviewed this pull request myself (self-review)

Future work

  • Plugin natives whose values depend on the whole history (the toolkits, some of Pro's order-flow studies) can opt into waitsForHistory once this ships.
  • Seen in the code, not verified: picking a shallower depth while the initial history is still loading fires the completion while the switch gates every poke, so held script sessions may never be released. Natives are covered here; script indicators would need the same release.
  • The progressive path does not emit history:progress the way the chunked backfill does.

Note

Medium Risk
Touches core engine history/backfill and native indicator lifecycle paths that affect every on-chart study; behavior change is intentional but broad across progressive loads, depth changes, and price styles.

Overview
Fixes native indicators sliding or vanishing while history loads in chunks, and aligns built-in studies with script-indicator behavior.

Native emits are now anchored via anchorTime on each model (first chart bar at emit time), so plots stay on the correct candles when older bars prepend or depth trims until the next compute. Optional waitsForHistory on NativeIndicatorDescriptor defers start until history is complete (loading legend row, ready waits); during backfill or deeper loads, onBars() is held and prior output stays anchored. Classic built-in studies opt in; volume/VPVR still paint from the first candles.

Orchestrator changes: progressive snapshot repaints notify sessions with 'backfill'; pokeNative / releaseHeldNatives implement the hold policy; Heikin Ashi (and other bar transforms) immediately re-run started natives over ctx.bars(); blanked models drop anchorTime on market switch. Plugin SDK and CHANGELOG document staged history and the flag. New test/native-history-load.test.ts covers progressive load, depth changes, and price-style switches.

Reviewed by Cursor Bugbot for commit 25fd936. Bugbot is set up for automated code reviews on this repo. Configure here.

The renderer paints an indicator's points index-aligned from the bar its
computation started on (the model's anchorTime), bar 0 when absent. Script
indicators stamp that anchor and hold their first run until the history
completes; native indicators did neither. A native computed over the first
part of a progressive load was painted from the chart's first bar once older
history landed: the whole plot slid back in time, and a deep prepend pushed
it off-screen entirely. A depth trim shifted it the other way, and
progressive snapshots never re-ran natives at all. A live tick recomputed
them, which hid the shift on busy markets; on a quiet one it stayed until
the load completed.

- Every native emit is anchored to the chart's first bar at the call, so an
  output stays on its candles across prepends and trims until its next
  compute.
- New optional waitsForHistory descriptor flag: such a native starts only
  once the history is complete (a loading legend row meanwhile, ready and
  indicator:added deferred, inspect() skipping it) and holds onBars()
  during a later backfill. The classic studies opt in; volume and VPVR do
  not.
- Progressive snapshots poke consumers with 'backfill', like the chunked
  backfill does.
- A price-style transform change (Heikin Ashi) re-runs started natives
  over the new candles instead of waiting for the next tick.
- Blanked models drop their anchor, so a market switch no longer warns that
  the old anchor matches no bar on the new axis.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alexgrover
alexgrover marked this pull request as ready for review October 16, 2026 20:24
@cursor

cursor Bot commented Oct 16, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: 08a29fd4-7337-4a8e-a284-8955bba1ae72)

@alexgrover
alexgrover merged commit 9151539 into dev Oct 16, 2026
3 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant