WebGPU Trading Charts With SciChart.js v6: A Per-User Renderer Rollout, When wasm64 Pays, And Profiling Dashboards By Counts
SciChart published the first non-alpha SciChart.js v6 on 24 September 2026, with a WebGPU renderer it selects automatically on Apple Silicon Macs and wasm64 binaries that lift the 4GB memory ceiling. For broker and quant teams running multi-chart dashboards, this guide sets the renderer per user with a WebGL control arm and engine telemetry, keeps wasm64 for deep tick replay, and streams candles into FIFO series safely. It then profiles both arms by counts per frame, using an unvetted third-party review of 6.0.6 as the checklist.
AlchmAI Engineering17 min read
6.0.1 to 6.0.8
SciChart.js v6 versions published to npm between 24 September and 13 October 2026, 19 days from the first non-alpha build
29,108
npm downloads of the scichart package in the week of 6 to 12 October 2026
4GB
WebAssembly memory ceiling that the new wasm64 (Memory64) binaries lift, per the SciChart.js v6 README
94
Issues in an unvetted third-party review of scichart 6.0.6 published on 3 October 2026: 28 high, 51 medium, 15 low
WebGPU trading charts became a commercial, npm-installable option on 24 September 2026, when SciChart published scichart 6.0.1, the first non-alpha release of SciChart.js v6. By 13 October 2026 it had reached 6.0.8. The package README calls v6 'the first release supporting WebGPU', describes a brand new renderer that the library selects automatically on Apple Silicon Macs while WebGL stays in use everywhere else, and says WebGPU significantly improves multi-chart scenarios such as dashboards. It also ships wasm64 (Memory64) binaries that 'lift the 4GB memory ceiling'. For a broker or quant team whose traders keep eight or twelve live charts on one screen, that is precisely the case the release is aimed at.
The uncomfortable part is the pace. The version number went from 6.0.1 to 6.0.8 in 19 days, and a third-party review of 6.0.6, published on GitHub on 3 October 2026, lists 94 issues, 28 of them rated high. Nobody has vetted that review, including us, but it is a good list of things to measure. Demand is not in doubt: ChartGPU's Show HN reached 670 points in January 2026, and TradingView's Lightweight Charts had not published to npm since 5.2.1 on 12 August 2026. So the question for a CTO is not whether GPU charting arrives, but how to adopt a renderer first released on 24 September 2026 without a trading desk finding its bugs first. This guide covers it in TypeScript against scichart 6.0.8: a per-user renderer flag with engine telemetry and a WebGL control arm, a rule for wasm64, a candlestick dashboard that streams into FIFO series without growing, and a profiling harness that counts work per frame instead of timing it.
1. How Do You Roll Out WebGPU Trading Charts Without Betting The Desk On Them?
Start with how the 6.0.8 build picks a renderer, because that decides where your flag has to live. The package source reads one localStorage key, IS_WEB_GPU: unset means auto, '1' forces WebGPU and '0' forces WebGL. It reads the key once, when the scichart module is first evaluated, not when a chart is created. In auto mode it attempts WebGPU on Macs and lets WebGpuHelper finish the decision from the GPU adapter information, which is how it separates Apple Silicon from Intel machines. Two consequences follow. The flag must be written before the first import of scichart, so chart code loads through a dynamic import behind your feature-flag call. And a change of arm takes effect on the next page load, which is what you want: a trader's renderer should never switch mid-session.
That gives a three-arm rollout: auto, webgpu and webgl, assigned per user and sticky, plus a server-side kill switch that writes '0' for everyone. Keep a WebGL control arm on Macs from day one, because auto mode moves Apple Silicon users to WebGPU the moment you upgrade, whether or not you planned an experiment. Then record what actually loaded. SciChartSurfaceBase.debugWasmWebGPU() returns { wasm64, simd, webGpu, details } from the same functions the loader uses, so it reports the engine in use rather than the one requested. Each surface's isMasterCanvasRenderTarget is false under WebGPU, and WebGpuHelper.getWebGpuSupported() says whether the device could run it. A user in the webgpu arm whose report says webGpu: false has fallen back, and the fallback rate by browser and GPU is the first number to watch.
The upgrade itself is small. The README's steps are to copy the single _wasm directory to your static assets and delete any SciChart3DSurface.configure() call; teams with custom indicator panes should also note that XyNDataSeries is now TableDataSeries. Pin the exact version in package.json. A library moving this quickly should change only when you decide it does.
// scichart 6.0.8, browser only. The renderer flag is read once, when the scichart module is evaluated,
// so nothing may import "scichart" as a value before bootCharts() runs.
import type * as SciChart from "scichart";
export type Sc = typeof SciChart;
export type RendererArm = "auto" | "webgpu" | "webgl";
export interface RolloutDecision {
experiment: string; // e.g. "scichart6-renderer-2026-10"
userId: string;
renderer: RendererArm; // from your feature-flag service, sticky per user
wasm64: boolean; // separate flag, replay users only: never mixed into the renderer test
forceWebGl: boolean; // server-side kill switch for every user
}
export interface EngineReport {
experiment: string; userId: string; arm: RendererArm; scichartVersion: string;
flagWritten: "0" | "1" | null; storageBlocked: boolean;
webGpu: boolean; wasm64: boolean; simd: boolean; webGpuSupported: boolean;
surfaces: number; surfacesOnOwnSwapchain: number;
details: Record<string, unknown>; userAgent: string; at: string;
}
const FLAG_KEY = "IS_WEB_GPU"; // unset = auto, "1" = WebGPU, "0" = WebGL
const SCICHART_VERSION = "6.0.8"; // pinned exactly in package.json
function writeRendererFlag(d: RolloutDecision): { value: "0" | "1" | null; blocked: boolean } {
const value = d.forceWebGl || d.renderer === "webgl" ? "0" : d.renderer === "webgpu" ? "1" : null;
try {
if (value === null) localStorage.removeItem(FLAG_KEY);
else localStorage.setItem(FLAG_KEY, value);
return { value, blocked: false };
} catch {
return { value: null, blocked: true }; // site data blocked: the library falls back to auto
}
}
export async function bootCharts(d: RolloutDecision, send: (r: EngineReport) => void) {
const flag = writeRendererFlag(d);
const sc: Sc = await import("scichart"); // first evaluation happens here, after the flag is written
sc.SciChartDefaults.useWasm64 = d.wasm64 ? sc.EUseWasm64.Auto : sc.EUseWasm64.Never;
// Served from the package's _wasm directory; wasm64Url defaults to scichart-64.wasm beside it.
sc.SciChartSurface.configure({ wasmUrl: "/_wasm/scichart.wasm" });
const surfaces: SciChart.SciChartSurface[] = [];
const firstFrames: Promise<void>[] = [];
/** Register every dashboard surface as it is created. */
function track(surface: SciChart.SciChartSurface): void {
surfaces.push(surface);
firstFrames.push(new Promise<void>((resolve) => {
const once = () => { surface.rendered.unsubscribe(once); resolve(); };
surface.rendered.subscribe(once);
}));
}
/** Report after every surface has drawn once, when auto mode has finished deciding. */
async function reportEngine(): Promise<void> {
await Promise.all(firstFrames);
const engine = sc.SciChartSurfaceBase.debugWasmWebGPU();
send({
experiment: d.experiment, userId: d.userId, arm: d.forceWebGl ? "webgl" : d.renderer,
scichartVersion: SCICHART_VERSION, flagWritten: flag.value, storageBlocked: flag.blocked,
webGpu: engine.webGpu, wasm64: engine.wasm64, simd: engine.simd,
webGpuSupported: sc.WebGpuHelper.getWebGpuSupported(),
surfaces: surfaces.length,
surfacesOnOwnSwapchain: surfaces.filter((s) => !s.isMasterCanvasRenderTarget).length,
details: engine.details, userAgent: navigator.userAgent, at: new Date().toISOString(),
});
}
return { sc, track, reportEngine };
}Note that the module imports scichart only as a type, and every other chart module in this guide takes the namespace from bootCharts as a parameter. That keeps a stray static import from evaluating the library before the flag exists, which would silently put a webgl-arm user on auto.
When Is wasm64 Worth It For Market Data Infrastructure?
Turn it on only where one tab must hold more than 32-bit WebAssembly can address. In trading that means deep replay: a quant stepping through a full session of tick data across a basket of instruments, a surveillance analyst replaying an order book reconstruction, or a research tool loading months of one-minute bars for many symbols at once. Stored as two 64-bit doubles, one tick costs 16 bytes, so 100 million ticks is 1.6GB of raw values before the library's working buffers and the copies your feed handler keeps. That is close enough to 4GB to matter. A live dashboard with a few thousand bars per panel is nowhere near it, and enabling wasm64 there adds a second binary to serve, a browser split to support and another variable in every performance comparison, for nothing.
The browser split is the other constraint. With SciChartDefaults.useWasm64 set to EUseWasm64.Auto, browsers with Memory64 and SIMD load scichart-64.wasm and the rest fall back to the 32-bit build. Never is the default. Since the type documentation says Safari did not support Memory64 as of 2025, a replay tool that needs more than 4GB should check debugWasmWebGPU().wasm64 and tell a Safari user the session is too large, rather than fail at the allocation that crosses the line.
2. What Changes For A Multi-Chart Dashboard, And How Do You Stream Candles Safely?
The dashboard is where the renderer change shows. In the 6.0.8 type definitions, WebGL surfaces draw into a shared master canvas, while under WebGPU each surface renders into its own canvas swapchain. With twelve panels on screen that means twelve independent render targets instead of one shared one, which is the scenario the README says improves. Nothing in your chart code changes: SciChartSurface.create, OhlcDataSeries and FastCandlestickRenderableSeries are the same calls in both arms, so the A/B test compares renderers, not implementations.
A faster renderer does not fix the data path into it. Four rules keep a streaming candlestick panel flat over a trading day:
- Aggregate ticks into bars outside the chart and append closed bars once per animation frame with appendRange, so a burst at the London open becomes one call per panel per frame, not hundreds.
- Give the history series a fifoCapacity sized to the visible window plus scroll-back headroom. Memory is fixed when the panel is created, and the oldest bars drop off as new ones arrive.
- Draw the forming bar from its own one-point series, updated in place at index 0, so the FIFO history is only ever appended to.
- Compute derived lines such as a moving average in your own code, into their own FIFO series with the same capacity, instead of attaching a filter to the FIFO source. The review's issue #018 says that pattern misbehaves; right or wrong about 6.0.8, a series you append to yourself has a capacity you can see.
// scichart 6.0.8. Takes the namespace from bootCharts() so the renderer flag is always set first.
import type * as SciChart from "scichart";
import type { Sc } from "./rendererRollout";
/** t is the bar open time in UTC seconds, as DateTimeNumericAxis expects. */
export interface Bar { t: number; o: number; h: number; l: number; c: number }
export interface PanelOptions {
symbol: string; // e.g. "EURUSD"
digits: number; // price precision on the axis
visibleBars: number; // e.g. 300 one-minute bars
maLength: number; // e.g. 20
}
export interface CandlePanel {
surface: SciChart.SciChartSurface;
history: SciChart.OhlcDataSeries;
ma: SciChart.XyDataSeries;
closeBar(bar: Bar): void;
updateForming(bar: Bar): void;
dispose(): void;
}
const UP = "#26a69a";
const DOWN = "#ef5350";
export async function createCandlePanel(sc: Sc, el: HTMLDivElement, opts: PanelOptions): Promise<CandlePanel> {
const { sciChartSurface: surface, wasmContext } = await sc.SciChartSurface.create(el);
const capacity = opts.visibleBars * 2; // memory is fixed from the moment the panel exists
const common = { isSorted: true, containsNaN: false };
surface.xAxes.add(new sc.DateTimeNumericAxis(wasmContext, { autoRange: sc.EAutoRange.Always }));
surface.yAxes.add(new sc.NumericAxis(wasmContext, { autoRange: sc.EAutoRange.Always, labelPrecision: opts.digits }));
surface.chartModifiers.add(new sc.CursorModifier()); // its axis labels are the pointer-move path to count
const history = new sc.OhlcDataSeries(wasmContext, { ...common, dataSeriesName: opts.symbol, fifoCapacity: capacity });
const live = new sc.OhlcDataSeries(wasmContext, { ...common, dataSeriesName: `${opts.symbol} forming` });
// Derived line in its own FIFO series, fed by our code: no filter attached to a FIFO source.
const ma = new sc.XyDataSeries(wasmContext, { ...common, dataSeriesName: `${opts.symbol} SMA ${opts.maLength}`, fifoCapacity: capacity });
surface.renderableSeries.add(
new sc.FastCandlestickRenderableSeries(wasmContext, {
dataSeries: history, dataPointWidth: 0.7, brushUp: UP, brushDown: DOWN, strokeUp: UP, strokeDown: DOWN,
}),
new sc.FastCandlestickRenderableSeries(wasmContext, {
dataSeries: live, dataPointWidth: 0.7, brushUp: UP + "80", brushDown: DOWN + "80", strokeUp: UP, strokeDown: DOWN,
}),
new sc.FastLineRenderableSeries(wasmContext, { dataSeries: ma, stroke: "#94a3b8", strokeThickness: 1 }),
);
const pending: Bar[] = [];
const closes: number[] = [];
let sum = 0;
let forming: Bar | null = null;
let liveT = Number.NaN;
let frame = 0;
function flush(): void {
frame = 0;
if (pending.length > 0) {
// One appendRange per series per frame, however many bars closed since the last one.
history.appendRange(
pending.map((b) => b.t), pending.map((b) => b.o), pending.map((b) => b.h),
pending.map((b) => b.l), pending.map((b) => b.c),
);
const maX: number[] = [];
const maY: number[] = [];
for (const b of pending) {
closes.push(b.c);
sum += b.c;
if (closes.length > opts.maLength) sum -= closes.shift() as number;
if (closes.length === opts.maLength) { maX.push(b.t); maY.push(sum / opts.maLength); }
}
if (maX.length > 0) ma.appendRange(maX, maY);
if (pending.some((b) => b.t === liveT)) { live.clear(); liveT = Number.NaN; }
pending.length = 0;
}
if (forming) {
const b = forming;
if (b.t === liveT) live.update(0, b.o, b.h, b.l, b.c); // same bar: overwrite in place
else { live.clear(); live.append(b.t, b.o, b.h, b.l, b.c); liveT = b.t; }
forming = null;
}
}
const schedule = () => { if (frame === 0) frame = requestAnimationFrame(flush); };
return {
surface, history, ma,
closeBar(bar) { // from your bar builder, never per tick
if (forming?.t === bar.t) forming = null;
pending.push(bar);
schedule();
},
updateForming(bar) { forming = bar; schedule(); }, // only the latest value per frame reaches the chart
dispose() {
if (frame !== 0) cancelAnimationFrame(frame);
surface.delete();
[history, live, ma].forEach((s) => s.delete());
},
};
}Each panel is created with SciChartSurface.create, which shares one WebAssembly context across the dashboard, and passed to track() from the rollout module so the engine report covers every surface. On unmount, dispose() deletes the surface and its data series explicitly; the harness below checks that nothing survives.
3. Why Profile Trading Charts By Counts, Not Timings?
Frame times are the wrong first metric for this rollout. They vary with the trader's machine, other tabs, thermal state and power settings, so a WebGL versus WebGPU comparison drowns in noise. Counts do not. For the same feed and the same panels, the GPU calls per frame, the bytes uploaded per frame and the native objects created and not yet deleted should be the same on a trader's workstation and a virtual desktop, and they move when the code changes. A count that grows with session length is a leak. A count that jumps when the pointer moves is work in the wrong place.
The third-party review of 6.0.6 is a useful list of what to count, as long as it is treated as unvetted. It was published by the GitHub user agorodnichev on 3 October 2026, ten days before 6.0.8. Issue #010 says toLocaleDateString 'builds a new ICU formatter on every call, on every pointer move'. The harness counts Date locale-formatting calls per frame, so sweeping the cursor across a DateTimeNumericAxis shows whether your build does that. Issue #018 says filters over a FIFO source 'grow without bound'. The harness samples count() on any series you register, so a filter that keeps growing while its source sits at fifoCapacity shows within minutes of a soak run. Setting MemoryUsageHelper.isMemoryUsageDebugEnabled before the first chart lets it read how many SciChart objects are registered but not yet deleted. Run it in canary builds and soak tests, not for every user: it wraps browser prototypes.
// Canary and soak builds only: wraps browser prototypes. scichart 6.0.8, @webgpu/types for GPU* globals.
// Install after bootCharts() and before the first panel is created.
import type { Sc } from "./rendererRollout";
type Args = unknown[];
interface FrameCounts { calls: Record<string, number>; bytes: number; locale: number; renders: number }
export interface FrameSample extends FrameCounts { t: number; gauges: Record<string, number> }
const fresh = (): FrameCounts => ({ calls: {}, bytes: 0, locale: 0, renders: 0 });
let current = fresh();
const samples: FrameSample[] = [];
const gauges = new Map<string, () => number>();
/** GPUQueue.writeBuffer: dataOffset and size are in elements for typed arrays, bytes otherwise. */
function writeBufferBytes([, , data, dataOffset = 0, size]: Args): number {
const view = data as ArrayBufferView | ArrayBuffer;
const unit = (view as Partial<{ BYTES_PER_ELEMENT: number }>).BYTES_PER_ELEMENT ?? 1;
return size !== undefined ? (size as number) * unit : view.byteLength - (dataOffset as number) * unit;
}
const glBytes = (d: unknown) => (typeof d === "number" ? d : (d as ArrayBufferView | null)?.byteLength ?? 0);
function wrap(proto: object, method: string, label: string, onCall?: (args: Args) => void): void {
const target = proto as Record<string, unknown>;
const original = target[method];
if (typeof original !== "function") return;
target[method] = function (this: unknown, ...args: Args) {
current.calls[label] = (current.calls[label] ?? 0) + 1;
onCall?.(args);
return (original as (...a: Args) => unknown).apply(this, args);
};
}
export function installFrameCounters(sc: Sc, maxFrames = 36_000): void {
sc.MemoryUsageHelper.isMemoryUsageDebugEnabled = true; // must precede every surface
gauges.set("scichart.undeleted", () => sc.MemoryUsageHelper.objectRegistry.undeletedObjectsMap.size);
if (typeof GPUQueue !== "undefined") {
wrap(GPUQueue.prototype, "writeBuffer", "gpu.writeBuffer", (a) => { current.bytes += writeBufferBytes(a); });
wrap(GPUQueue.prototype, "writeTexture", "gpu.writeTexture");
wrap(GPUQueue.prototype, "submit", "gpu.submit");
wrap(GPUDevice.prototype, "createBuffer", "gpu.createBuffer");
wrap(GPURenderPassEncoder.prototype, "draw", "gpu.draw");
wrap(GPURenderPassEncoder.prototype, "drawIndexed", "gpu.drawIndexed");
}
for (const proto of [WebGLRenderingContext.prototype, WebGL2RenderingContext.prototype]) {
wrap(proto, "bufferData", "gl.bufferData", (a) => { current.bytes += glBytes(a[1]); });
wrap(proto, "bufferSubData", "gl.bufferSubData", (a) => { current.bytes += glBytes(a[2]); });
wrap(proto, "createBuffer", "gl.createBuffer");
wrap(proto, "drawArrays", "gl.drawArrays");
wrap(proto, "drawElements", "gl.drawElements");
}
wrap(WebGL2RenderingContext.prototype, "drawArraysInstanced", "gl.drawArraysInstanced");
wrap(WebGL2RenderingContext.prototype, "drawElementsInstanced", "gl.drawElementsInstanced");
for (const m of ["toLocaleDateString", "toLocaleTimeString", "toLocaleString"]) {
wrap(Date.prototype, m, `date.${m}`, () => { current.locale += 1; });
}
const tick = (t: number) => {
const g: Record<string, number> = {};
gauges.forEach((read, name) => { g[name] = read(); });
samples.push({ t, ...current, gauges: g });
if (samples.length > maxFrames) samples.shift();
current = fresh();
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
export function watchSurface(surface: { rendered: { subscribe(h: () => void): void } }): void {
surface.rendered.subscribe(() => { current.renders += 1; });
}
/** Watch a FIFO source and anything derived from it, e.g. a filter. Both should plateau. */
export function watchCount(name: string, series: { count(): number }): void {
gauges.set(name, () => series.count());
}
const pct = (xs: number[], q: number) => {
const s = [...xs].sort((a, b) => a - b);
return s[Math.min(s.length - 1, Math.floor(q * s.length))] ?? 0;
};
/** Summarise frames since fromMs (a performance.now() time): run once per arm and scenario. */
export function summarise(fromMs: number) {
const s = samples.filter((x) => x.t >= fromMs);
const p99: Record<string, number> = {};
for (const label of new Set(s.flatMap((x) => Object.keys(x.calls)))) {
p99[label] = pct(s.map((x) => x.calls[label] ?? 0), 0.99);
}
const gaugeDrift: Record<string, [number, number]> = {};
for (const name of gauges.keys()) gaugeDrift[name] = [s[0]?.gauges[name] ?? 0, s.at(-1)?.gauges[name] ?? 0];
return {
frames: s.length, callsP99: p99, gaugeDrift,
bytesP99: pct(s.map((x) => x.bytes), 0.99),
localeP99: pct(s.map((x) => x.locale), 0.99),
rendersP99: pct(s.map((x) => x.renders), 0.99),
};
}
/** Soak gate: fails when a count grows with session length or pointer movement instead of with the feed. */
export function assertSteady(
sum: ReturnType<typeof summarise>,
limits: { maxLocalePerFrame: number; maxBytesPerFrame: number; maxGaugeGrowth: Record<string, number> },
): string[] {
const failures: string[] = [];
if (sum.localeP99 > limits.maxLocalePerFrame) failures.push(`locale formatting p99 ${sum.localeP99} per frame`);
if (sum.bytesP99 > limits.maxBytesPerFrame) failures.push(`upload p99 ${sum.bytesP99} bytes per frame`);
for (const [name, [start, end]] of Object.entries(sum.gaugeDrift)) {
if (end - start > (limits.maxGaugeGrowth[name] ?? 0)) failures.push(`${name} grew from ${start} to ${end}`);
}
return failures;
}Run the same scripted session in each arm: a recorded feed replayed at market speed, a cursor sweep across every panel, then mounting and unmounting the dashboard ten times. Pass criteria worth writing down before the first run:
- Locale formatting per frame while the pointer moves: zero from your own code, and a figure you can report upstream if the library's is not.
- Bytes uploaded per frame at a steady feed: proportional to bars appended, not to bars held.
- Undeleted SciChart objects after every unmount: back to the baseline taken before the first mount.
- Every FIFO series and anything derived from it: count() flat at or below fifoCapacity.
“A renderer first published on 24 September 2026 earns its place on a trading desk the way any infrastructure change does: behind a flag, against a control arm, measured in counts.”
Where This Belongs: Brokers, Prop Firms, Quant Teams And Startups
- 01Brokers and CFD providers: if your web platform offers multi-chart workspaces, ship v6 behind the renderer flag with a WebGL control arm before any Mac user is moved by auto mode, and put the engine report next to client-side error rates for each arm.
- 02Prop trading firms: traders run dense multi-monitor layouts for whole sessions, so the soak test matters more than the demo. Replay a recorded London open and New York open through the harness before each upgrade.
- 03Research and quant teams: keep wasm64 for replay tools that genuinely exceed 4GB, on Chromium and Firefox, with an explicit message for Safari users. It belongs to your market data infrastructure, not to every chart.
- 04Fintech and trading startups: trading charts AI features, such as descriptive annotations computed by code and labelled by a model, add series and labels to every panel. Put them inside the same counts budget, and keep them descriptive, not advisory.
The Bottom Line
WebGPU trading charts are now a shipping option rather than a demo: SciChart.js v6 reached npm on 24 September 2026 with a WebGPU renderer that it selects automatically on Apple Silicon Macs, and wasm64 binaries for data beyond the 4GB ceiling. Adopt it the way you would any change to market data infrastructure. Pin the version, write the IS_WEB_GPU flag per user before the library loads, record what debugWasmWebGPU() says actually ran, keep a WebGL control arm, reserve wasm64 for deep replay, and judge both arms on counts per frame rather than milliseconds. That is the trading AI architecture AlchmAI, a London trading technology and AI studio, builds for brokers and quant teams: charts that are measured first, with AI chart analysis layered on top only once the rendering budget is known.
References & Further Reading
- npm - scichart (SciChart.js) package and v6 README. npmjs.com/package/scichart
- npm - scichart registry metadata, release dates for 6.0.1, 6.0.6 and 6.0.8. registry.npmjs.org/scichart
- npm - Download counts for scichart, 6 to 12 October 2026. api.npmjs.org/downloads/point/2026-10-06:2026-10-12/scichart
- GitHub - agorodnichev/scichart-v6-performance-review, review of scichart 6.0.6 (3 October 2026). github.com/agorodnichev/scichart-v6-performance-review
- npm - lightweight-charts (TradingView Lightweight Charts). npmjs.com/package/lightweight-charts
- Hacker News - ChartGPU Show HN (January 2026), via HN search. hn.algolia.com/?q=ChartGPU
- W3C - WebGPU specification. w3.org/TR/webgpu
- WebAssembly - Memory64 proposal. github.com/WebAssembly/memory64
- AlchmAI - Market data infrastructure. alchmai.com/solutions/market-data-infrastructure
- AlchmAI - Trading charts and AI chart analysis. alchmai.com/charting
- AlchmAI - Brokers and CFD providers. alchmai.com/solutions/broker-trading-platforms
AlchmAI Engineering
Engineering, London
Written by the AlchmAI engineering team in Mayfair, London. We build trading platforms, real-time charts, market data pipelines and AI features for brokers, prop firms and fintech teams. The Playbook is where we explain how we approach these systems, with code you can run and sources you can check.
Code in this guide is illustrative and supplied without warranty. Review and test it before production use. Nothing here is investment advice. Important information