@chart-engine/core
v0.1.13
Published
High-performance financial charting engine (WASM) — adapter-based, exchange-neutral. Candles, indicators, drawings, pan/zoom, scalper UI.
Maintainers
Readme
@chart-engine/core
High-performance financial charting engine for the browser. Exchange-neutral
and adapter-based: the chart renders on a canvas at 60 fps from a Rust
WASM core, and never talks to a broker or exchange directly — it only ever
sees an ExchangeAdapter.
Candles, 28+ indicators, drawing tools, pan/zoom, live streaming, and a scalper-friendly paper-trading surface — all in one dependency.
npm install @chart-engine/coreFeatures
- Rust/WASM core — chart state, scales and indicator math run in WebAssembly;
the JS layer only replays draw commands on a
<canvas>. - Adapter-based data — wire any venue through an
ExchangeAdapter, or start instantly with the built-in offlinemockfeed. - Chart types — candles, bars, line, area, Heikin-Ashi.
- 28+ indicators — overlays (SMA, EMA, Bollinger, VWAP, Ichimoku, SuperTrend, …) and oscillators (RSI, MACD, Stochastic, ATR, ADX, CCI, …) auto-routed to a sub-pane.
- Live streaming — upserted tick-to-candle aggregation (100+ updates/sec).
- Trading —
buy/sell/closePosition; real orders via order-capable adapters, paper fills everywhere else. - Dark / light themes with custom accent color.
Quick start
<div id="chart" style="width:100%; height:600px"></div>
<script type="module">
import { ChartEngine } from "@chart-engine/core";
const engine = new ChartEngine({
container: document.getElementById("chart"),
initialAdapter: "mock", // offline demo feed
initialSymbol: "RELIANCE",
initialTimeframe: "5m",
accent: "#60a5fa",
});
</script>That's it. The chart mounts, loads history, subscribes to live ticks, and
renders. Destroy it with engine.destroy() when you're done.

Try the interactive playground: cd examples/basic && npm run dev — exchange
switcher, symbol search, timeframes, indicators, and one-click buy/sell/close.
Built-in adapters
| key | venue | live | orders | notes |
| ------ | --------- | ---- | ------ | -------------------------------------- |
| mock | Synthetic | ✅ | paper | deterministic demo feed, no network |
The package also ships real-venue adapters (enabled via registerAdapter) so
you can connect to a live exchange without writing any glue code.
API
new ChartEngine(options)
| option | type | description |
| ------------------ | ------------------------ | ------------------------------------------------- |
| container | HTMLElement \| string | Mount element or its id (required). |
| initialAdapter | string | Adapter key (default: first registered). |
| initialSymbol | string | Default symbol (default "RELIANCE"). |
| initialTimeframe | string | Default timeframe (default "1m"). |
| adapters | ExchangeAdapter[] | Extra adapters to register for this instance. |
| theme | "dark" \| "light" | Default "dark". |
| accent | string | Hex accent color (default "#60a5fa"). |
| onReady | (engine) => void | Fires after WASM init and first data load. |
Methods
| method | description |
| --------------------------- | ------------------------------------------------------------------ |
| setExchange(key) | Switch data source and reload, re-subscribing to the live feed. |
| setSymbol(sym) | Load a new symbol. |
| setTimeframe(tf) | Switch timeframe. |
| addIndicator(key, params?)| Add an indicator — see table below. |
| removeIndicator(index) | Remove an indicator by index. |
| setChartType(type) | "Candles", "Line", "Area", … |
| setTheme(t) / setAccent(hex) | Appearance. |
| fitAll() | Fit all visible candles. |
| buy(qty?) / sell(qty?) | Place a market order (real or paper, per adapter). |
| closePosition() | Close the current position. |
| getPositions() | Current positions. |
| currentSymbol() / currentTimeframe() / currentAdapter() | State getters. |
| destroy() | Tear down canvas, event listeners and live subscription. |
Indicators
Overlays render on the main pane; oscillators auto-route to a sub-pane.
| key | params (defaults) |
| --- | ----------------- |
| sma, ema | { period: 20 } |
| vwap, obv, pivot_points | — |
| bollinger_bands | { period: 20, std_dev: 2 } |
| keltner | { period: 14, multiplier: 2 } |
| donchian | { period: 20 } |
| super_trend | { period: 10, multiplier: 3 } |
| parabolic_sar | { step: 0.02, max_step: 0.2 } |
| ichimoku | { conversion: 9, base: 26, span_b: 52, displacement: 26 } |
| ema_ribbon, ma_ribbon | — |
| rsi (sub) | { period: 14 } |
| atr (sub) | { period: 14 } |
| macd (sub) | { fast: 12, slow: 26, signal: 9 } |
| stochastic (sub) | { k_period: 14, d_period: 3, smooth: 3 } |
| stoch_rsi (sub) | { rsi_period: 14, stoch_period: 14, k_smooth: 3, d_smooth: 3 } |
| adx, aroon, vortex, trix, mfi, williams_r, cci (sub) | { period: 14…20 } |
| momentum (sub) | { period: 10 } |
| roc (sub) | { period: 12 } |
| std_dev (sub) | { period: 14 } |
| volume_profile | { bins: 24 } |
| zig_zag | { pct: 5 } |
engine.addIndicator("sma", { period: 20 }); // overlay
engine.addIndicator("macd"); // defaults: 12/26/9
engine.removeIndicator(0);Bringing your own exchange
The engine only speaks to ExchangeAdapter. Implement it and register it:
import { ChartEngine, registerAdapter, type ExchangeAdapter } from "@chart-engine/core";
const myAdapter: ExchangeAdapter = {
key: "myexchange",
label: "My Exchange",
capabilities: {
historical: true,
live: true,
intradayTimeframes: ["1m", "5m"],
orders: "real",
},
async searchSymbols(query) { /* ... */ },
async fetchHistory({ symbol, timeframe, limit }) { /* candles */ },
subscribe(symbol, timeframe, onCandle) {
const ws = new WebSocket("wss://…");
ws.onmessage = (e) => onCandle(parseTick(e.data));
return { unsubscribe: () => ws.close() };
},
submitOrder(order) { /* place on your broker */ },
cancelOrder(id) { /* ... */ },
getPositions() { /* ... */ },
};
registerAdapter(myAdapter);Built-in adapters self-register on import; registerAdapter/unregisterAdapter
swap them at runtime. listAdapters() and getAdapter(key) inspect the
registry.
Adapter contract
fetchHistoryreturns{ time, open, high, low, close, volume }candles.subscribemust callonCandlefor both the in-progress candle and completed candles — the engine merges them viaupsert_candle. Return anunsubscribe().session.sessionsis an array of[openMinute, closeMinute]market sessions that drive the trading-hours time axis; setsession.aroundTheClockfor 24/7 venues.capabilities.orders: "real"→submitOrderactually places; anything else fills paper on the last price.
Consuming in frameworks
The package is plain ESM with no framework dependency — it mounts on any
<div>. Call destroy() on unmount to tear down the canvas, event listeners
and live feed.
React
import { useEffect, useRef } from "react";
import { ChartEngine } from "@chart-engine/core";
export function Chart() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const engine = new ChartEngine({ container: ref.current!, initialAdapter: "mock" });
return () => engine.destroy();
}, []);
return <div ref={ref} style={{ height: 500 }} />;
}Vue
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { ChartEngine } from "@chart-engine/core";
const el = ref();
let engine;
onMounted(() => { engine = new ChartEngine({ container: el.value }); });
onUnmounted(() => engine?.destroy());
</script>
<template><div ref="el" style="height:500px"></div></template>Svelte
<script>
import { onMount, onDestroy } from "svelte";
import { ChartEngine } from "@chart-engine/core";
let el, engine;
onMount(() => { engine = new ChartEngine({ container: el }); });
onDestroy(() => engine.destroy());
</script>
<div bind:this={el} style="height:500px"></div>Platforms: Flutter, iOS, Android
@chart-engine/core renders to an HTML5 canvas, so it runs anywhere a browser
(or WebView) does. It cannot run directly inside a Flutter/native app — the
chart engine is Rust/WASM, not a Flutter widget or a native library. Three
ways to consume it:
1. Browser apps (any web framework)
npm install @chart-engine/core — see the sections above. Vite, webpack,
Rollup, esbuild, or a bare <script type="module"> from esm.sh all work with
zero WASM configuration.
2. Flutter / React Native / native mobile (WebView)
Embed the engine's hosted web build in a WebView and drive it over
postMessage. This is exactly what the hosted build is for — a standalone
page (apps/web) that owns the chart and answers history/streaming via the
ce:* protocol (ce:init, ce:ready, ce:request, ce:candles, ce:tick).
- Flutter:
webview_flutterorflutter_inappwebview, bridge viapostMessage. - iOS:
WKWebView+WKScriptMessageHandler. - Android:
WebView+@JavascriptInterface. - React Native:
react-native-webview.
The host page feeds candles and ticks over the message bridge; the chart stays inside the WebView and never touches your native code.
A hosted build is already deployed — point your WebView at
https://chart-engine-ruby.vercel.app/index.html (live postMessage host
demo at /example-host.html).
3. True native rendering (C FFI)
The same Rust core ships a C ABI (chart_engine_create, chart_engine_set_candles,
chart_engine_render, …) that returns serialized draw commands. Compile the
crates to a static library and bind them from Swift/Kotlin/Dart FFI, then replay
the commands on a native canvas (Metal, UIKit, Skia). Heavier lift — intended
for embedders that must avoid any WebView.
Embedding & passing your data
The hosted build (https://chart-engine-ruby.vercel.app/index.html) is a dumb
rendering surface — it owns no market data. Your host page (or Flutter/native
WebView bridge) feeds it candles and ticks over window.postMessage.
Data flow
host (parent window) chart (iframe)
───────────────── ─────────────────
listen for ce:ready ◀─────────────────────── ce:ready {version} (chart booted)
send ce:init ──────────────────────────────▶ adopt symbol/timeframe/theme
send ce:pong ◀────────────────────────────── ce:ping (liveness)
send ce:candles ◀─────────────────────────── ce:request-candles {requestId, symbol, timeframe, bars}
│ (your history for that requestId)
send ce:tick {price, time, volume} ────────▶ live ticks → in-progress candle
send ce:candle {candle} ───────────────────▶ pre-aggregated candle (optional)Minimal host page
<iframe id="chart" src="https://chart-engine-ruby.vercel.app/index.html"></iframe>
<script>
const iframe = document.getElementById("chart");
const ORIGIN = "https://chart-engine-ruby.vercel.app";
// Listen first, before the iframe boots, so no ce:ready/ce:ping is dropped.
window.addEventListener("message", (e) => {
if (e.source !== iframe.contentWindow) return;
const m = e.data;
switch (m.type) {
case "ce:ready":
iframe.contentWindow.postMessage(
{ type: "ce:init", payload: { symbol: "RELIANCE", timeframe: "5m", theme: "dark" } },
ORIGIN,
);
break;
case "ce:ping":
iframe.contentWindow.postMessage({ type: "ce:pong", payload: {} }, ORIGIN);
break;
case "ce:request-candles": {
const { requestId, symbol, timeframe, bars } = m.payload;
const candles = await myApi.candles(symbol, timeframe, bars); // your data source
iframe.contentWindow.postMessage(
{ type: "ce:candles", payload: { requestId, symbol, timeframe, candles } },
ORIGIN,
);
break;
}
case "ce:notify":
// chart now shows symbol/timeframe — start/stop your live feed here
break;
}
});
// Push live prices as they arrive from your websocket/REST:
function onTick(price, time) {
iframe.contentWindow.postMessage(
{ type: "ce:tick", payload: { price, time, volume: 0 } },
ORIGIN,
);
}
</script>Candle shape
candles is an array of { time, open, high, low, close, volume } where
time is epoch ms. Ticks use { price, time, volume? } (epoch ms or seconds).
Full message reference
Host → chart: ce:init, ce:pong, ce:candles, ce:tick, ce:candle,
ce:set-symbol, ce:set-timeframe, ce:set-theme, ce:set-chart-type,
ce:indicator, ce:search-results, ce:drawings.
Chart → host: ce:ready, ce:ping, ce:request-candles, ce:request-search,
ce:request-drawings, ce:notify, ce:drawings.
Every message is { type, payload }. Wire contract in
apps/shared/src/protocol.ts; a working host demo is live at
https://chart-engine-ruby.vercel.app/example-host.html.
Performance notes
- Ingest and paint are decoupled. The WASM core mutates candle state freely;
rendering is gated by a rAF loop and
needs_render(). - Upsert, don't append. Live feeds merge candles, so 100+ updates/sec cost only a JSON stringify per bar.
- DPR-capped canvas at 2× for mobile perf.
Development
npm install
npm run build # vite lib build + tsc declarations
npm run typecheck
npm run test # vitest
cd examples/basic && npm run dev # interactive playgroundLicense
MIT
