An open-source trading terminal that runs entirely in your browser — no backend to run, no signup, no API keys.
Advanced charting · full drawing-tool suite · 8 indicators · session replay · watchlist · depth-of-market · order ticket · built-in paper-trading engine, seeded with real market history.
- What is OpenCharts?
- Features
- Quick start
- What's real and what's simulated
- How it works
- Built on Lightweight Charts
- Project structure
- Building on OpenCharts
- Refreshing the bundled market data
- Integrating OpenCharts
- Adding instruments
- Configuration
- Scripts
- Tech stack
- Known limitations
- Contributing
- Acknowledgements
- License
OpenCharts is a self-contained trading terminal UI. Open it and you land straight in the terminal: a candlestick chart with a TradingView-style drawing toolbar, indicators, a watchlist, a depth-of-market ladder and an order ticket — all wired to an in-browser paper-trading engine funded with $100,000.
There is no server to run. The session is seeded with genuine historical OHLC pulled from Binance's public klines endpoint at build time, and a tick stream is replayed forward from the present, so the chart and prices move like a live feed while you place and manage paper trades.
It's useful as:
- A standalone charting / paper-trading app you can host on any static host.
- A reference terminal UI you can point at your own market-data and trading backend — implement one adapter interface, no fork required (see Integrating OpenCharts).
- A learning sandbox for charting, technical drawing and order management.
The codebase was extracted from a closed-source prop-trading platform, so a few panels are gated off or stubbed rather than removed — those are called out explicitly below rather than advertised as features.
This is a community project. The goal is a genuinely open trading terminal interface — beautiful charts, a deep drawing toolkit, and enough customizability that you can bend it into your own terminal instead of fighting it. It is deliberately small, dependency-light and backend-agnostic so that anyone can clone it, understand it in an afternoon and extend it. Contributions of every size are welcome, from a new indicator or drawing tool to an exchange adapter, a theme, a bug report or a docs fix — see Building on OpenCharts and Contributing.
- Candlestick chart powered by
lightweight-chartsv4 with custom plugins. - Timeframes 1m → 1w (1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w), up to 1000 bars per timeframe (fewer where the exchange has less history, e.g. 461 weekly bars).
- Volume histogram, OHLC legend, live bid/ask price lines, crosshair and bar countdown.
- Toggleable chart plugins: crosshair highlight, session highlighting, session breaks, bands indicator, OHLCV tooltip and delta (multi-touch) tooltip.
- Chart settings dialog, right-click context menu, per-symbol preferences and
saveable chart templates — all persisted to
localStorage.
SMA, EMA, RSI, MACD, Bollinger Bands, ATR, Stochastic and VWAP, added from the toolbar and removable individually or in bulk from the chart context menu.
- Draggable, hideable tool rail grouped into Lines (trend line, ray, extended line, horizontal, vertical, parallel channel), Fibonacci (retracement, extension), Shapes (rectangle, ellipse, triangle, arrow), Trade (long position, short position, measure) and Text.
- Position tools show risk, R:R and position size against live account equity; trend lines show Δprice / Δ% / bar count while being drawn or selected.
- Per-object styling (color, width, line style, labels) with a TradingView-style
text editor, magnet mode (off / weak / strong snapping to OHLC), Shift for 45°
angle snapping and multi-select, and keyboard shortcuts (
Alt+H,Alt+T,Alt+F,Alt+R,Alt+M). - An object tree panel to select, toggle and delete drawings.
- Line-cross alerts: enable an alert on a horizontal line or trend line and the chart fires an in-session toast + sound when the mid price crosses it (in-session only — nothing is persisted or delivered offline).
- Drawings persist per symbol in
localStorageand survive reloads.
- Watchlist with live prices across all bundled instruments.
- Depth-of-market ladder around the live mid price.
- Order ticket: market / limit / stop tickets with volume presets, take-profit and stop-loss, one-click trade mode and an order confirmation dialog. See what's simulated for how limit/stop tickets currently behave.
- Positions / Orders / Trade History tabs with modify, close, partial close and close-all, plus Calendar (TradingView embed) and News tabs.
- Drag stop-loss / take-profit levels directly on the chart.
- Trade execution sound (mutable), connection indicator, market-closed banner and a dedicated mobile trading panel for narrow screens.
- Replay the charted symbol's 1-minute history with play / pause / stop, speeds from 0.5× to 10×, and a scrubber to seek anywhere in the session.
- Bars are revealed progressively as the cursor advances, and trades you closed during the replayed window appear as markers.
- Entirely client-driven — the clock runs in the browser off the bundled data.
- Log entries per trade with notes, tags, an emotion and a rating, filterable by
symbol and scoped to the account. Entries persist in
localStorage.
- $100,000 starting balance, 100× leverage, no commission or swap.
- Market, limit and stop orders with a real lifecycle: market fills at the
current price, limit and stop orders rest as
PENDINGand trigger when a tick crosses them (limits get price improvement, stops take the slippage). - Orders are validated before they reach the book — size, price, stop placement and free margin — and rejections explain themselves.
- Positions are marked to market on every tick with running P&L.
- Stop-loss / take-profit are evaluated automatically and close positions when hit.
- Account equity, balance, used and free margin update in real time.
- Bundled OHLC is real historical data fetched at build time — no random walks.
- A replay feed streams those closes forward from "now" so the terminal feels live.
- Everything runs client-side — deploy it as a static site.
Not on npm yet. The package is ready to publish but has not been released. Until it is, build the tarball from a clone and install that:
npm install && npm run build:lib && npm pack # then, in your app: npm install /path/to/OpenCharts/dylanpersonguy-opencharts-0.3.0.tgzOnce it is published, this becomes:
npm install @dylanpersonguy/openchartsimport { OpenChartsProvider, Terminal } from "@dylanpersonguy/opencharts";
import { createDemoAdapter } from "@dylanpersonguy/opencharts/demo";
import "@dylanpersonguy/opencharts/style.css";
export function TradingScreen() {
return (
<div style={{ height: 640 }}>
<OpenChartsProvider adapter={createDemoAdapter()}>
<Terminal />
</OpenChartsProvider>
</div>
);
}That runs the full terminal against the bundled demo data — nothing else to
configure. Swap createDemoAdapter() for your own adapter to point it at a real
backend (Integrating OpenCharts). React 18.2+ is the
only peer dependency; the stylesheet is precompiled, so your app does not need
Tailwind.
Requires Node 20+.
npm install
npm run devOpen the printed local URL (e.g. http://localhost:5173). The app boots straight
into a session with a funded paper account — pick a symbol from the watchlist,
set a size in the order ticket, and go long or short.
To build for production:
npm run build # outputs to dist/
npm run preview # serve the production build locallyOpenCharts is a paper terminal, and being precise about this matters more than marketing copy:
| Piece | Status |
|---|---|
| OHLC history (6 crypto pairs × 8 timeframes) | Real Binance klines, bundled as JSON |
| Tick feed | Real 1m closes, replayed every 600 ms starting at the latest close and reversing at each end of the series (no price jumps) |
| Bar timestamps | Shifted so the last real bar lands on the current period |
| Bid/ask spread | Derived — synthesized around the replayed close |
| DOM ladder sizes | Synthetic — derived deterministically from each price level, so a level's size is stable while price hovers |
| Order fills, positions, P&L, margin | Real arithmetic against the in-browser engine |
| Limit / stop orders | Rest as PENDING until a tick crosses their trigger, then fill — limits at their price or better, stops at the market that triggered them |
| Order modify / cancel | Real, and restricted to pending orders |
| Order validation | Real — quantity, price, side, stop placement, protective-level side and free margin are all checked, with the reason surfaced in a toast |
| News tab | Placeholder headlines, labelled "Sample data" in the UI and attributed to no real outlet |
| Economic calendar tab & technical-analysis gauge | TradingView embed widgets — real data, loaded from TradingView's CDN. The calendar has a toggle to a sample table, labelled as such |
| Trade journal | Working — entries persist to localStorage, scoped per account |
| Session replay | Working — plays the bundled 1m history for the charted symbol with play/pause/speed/seek |
| Trade calculator | Present in the code but never mounted |
| Account state (balance, positions, orders) | In-memory — resets on reload |
| Drawings, chart prefs, templates, sound mute | Persisted in localStorage |
The UI is backend-agnostic. It talks to an internal api / wsClient pair,
and those resolve against whichever OpenChartsAdapter is installed. The
bundled demo backend is simply the adapter that ships by default:
┌─────────────────────────────────────────────────────────┐
│ Terminal UI │
│ Chart · OrderTicket · DOM · Watchlist · … │
└─────────────┬─────────────────────────────┬─────────────┘
│ api.* │ wsClient.subscribe()
┌─────────────▼─────────────┐ ┌─────────────▼─────────────┐
│ services/api.ts │ │ services/ws.ts │
│ (internal facade) │ │ (adapter events → │
│ │ │ terminal channels) │
└─────────────┬─────────────┘ └─────────────┬─────────────┘
│ │
┌─────────────▼─────────────────────────────▼─────────────┐
│ OpenChartsAdapter (adapter/types.ts) │
│ marketData · trading? · storage? │
│ installed by <OpenChartsProvider adapter={…}> │
└─────────────┬───────────────────────────────────────────┘
│ default: createDemoAdapter()
┌─────────────▼───────────────────────────────────────────┐
│ adapter/demo.ts → services/demo/ │
│ engine.ts paper trading (positions, P&L, SL/TP) │
│ feed.ts replays real ticks as a live stream │
│ candles.ts serves bundled OHLC (shifted to now) │
│ bus.ts in-process pub/sub │
│ instruments.ts / data/ real OHLC + symbol specs │
└─────────────────────────────────────────────────────────┘
services/api.ts— the internal facade every component reads and writes through. Each method resolves against the installed adapter, mapping the small publicOpenChartsAdaptercontract onto the calls the UI makes.services/ws.ts— subscribes to the adapter's market-data and trading streams and republishes them on the four channels the terminal consumes (market-data,positions,orders,account). It also owns the connection lifecycle.adapter/demo.ts— the bundled backend expressed through the public contract, and the reference implementation for writing your own.services/demo/engine.ts— the paper-trading engine and single source of truth for the demo account, positions and orders. It marks positions to market on every tick.services/demo/feed.ts— replays the bundled 1-minute closes for every symbol as a forward-moving tick stream, every 600 ms, and drivesengine.mark().services/demo/candles.ts— serves the bundled history, time-shifted so the most recent bar aligns to "now" (OHLC values stay real; only the timeline is normalized).
Because the UI only ever sees api and wsClient, swapping the demo for a real
backend is a matter of installing a different adapter — no UI component
changes.
OpenCharts is built on top of TradingView's
Lightweight Charts™
(lightweight-charts ^4.2.0, resolved to 4.2.3 in the lockfile, which in turn
pulls in fancy-canvas, MIT). The chart canvas, series, panes, price scales,
crosshair and time axis are all
Lightweight Charts. OpenCharts is the terminal around it: the drawing engine
(src/lib/chart-plugins/drawing-tools/), the indicator layer, the series
primitives, and the panels, order flow and paper engine that surround the chart.
Several plugins under src/lib/chart-plugins/ (plugin-base.ts, tooltip/,
delta-tooltip/, session-highlighting/, session-breaks/,
bands-indicator/, highlight-bar-crosshair/, helpers/) are adapted from the
plugin-examples shipped in the Lightweight Charts repository, and carry that
project's license.
-
Lightweight Charts is open source under the Apache License 2.0 — free for commercial use, but Apache 2.0 comes with conditions.
-
Attribution is required. TradingView's terms state that you must add the attribution notice from their
NOTICEfile and a link to https://www.tradingview.com/ on the page of your site or app that users see. The notice reads:TradingView Lightweight Charts™ Copyright (с) 2025 TradingView, Inc. https://www.tradingview.com/ -
OpenCharts satisfies this via the chart's built-in attribution logo. The
attributionLogolayout option defaults totrueand this project never overrides it, so the TradingView link renders on the chart pane. Do not setattributionLogo: falseunless you place the notice and link somewhere else users can see. The option is set (or, here, left alone) where the chart is created insrc/pages/trading/ChartPanel.tsx. -
Keep the notice when you redistribute. Apache 2.0 requires that you pass along the license and notice with any copy or derivative — that includes the adapted plugin code in
src/lib/chart-plugins/. -
OpenCharts' own code is MIT (see LICENSE). MIT and Apache 2.0 are compatible, so a build that combines them is fine — you just have to honor Apache 2.0's attribution and notice terms for the chart portion.
None of this is legal advice; if you are shipping commercially, read the license yourself.
src/
├─ index.ts # the package's main entry (provider, terminal, panels, types)
├─ demo.ts # the "/demo" entry: createDemoAdapter()
├─ notifications.ts # onNotification(), the public notification stream
├─ main.tsx # hosted-demo entry: mounts Root, chunk-reload guard
├─ Root.tsx # hosted-demo root: installs an adapter, renders <Terminal />
├─ vendor/ # types inlined from the original platform's shared package
├─ pages/
│ ├─ TradingPage.tsx # the default layout, composed from the panels
│ └─ trading/ # chart, toolbars, order panel, DOM, watchlist,
│ # drawing rail, object tree, replay HUD/scrubber,
│ # news overlay, indicators, challenge levels
├─ lib/
│ ├─ chart-plugins/ # lightweight-charts plugins used by the chart
│ │ ├─ drawing-tools/ # manager, renderers, hit-testing, geometry, alerts
│ │ ├─ delta-tooltip/ tooltip/ highlight-bar-crosshair/
│ │ └─ session-breaks/ session-highlighting/ bands-indicator/
│ ├─ indicators.ts # SMA/EMA/RSI/MACD/BOLL/ATR/STOCH/VWAP
│ ├─ livePnl.ts # live P&L / price math shared by the tables
│ └─ utils.ts
├─ components/ # dialogs, mobile panel, TradingView embeds, ui primitives
├─ hooks/ # drawings, chart prefs, trader prefs, trade sound…
├─ terminal/ # composable terminal pieces
│ ├─ context.tsx # <TerminalProvider>: the state the panels share
│ ├─ panels.tsx # <Chart>, <OrderTicket>, <Positions>, …
│ ├─ use-terminal.ts # useTerminal(), for panels you write yourself
│ └─ Terminal.tsx # the full terminal in its default layout
├─ adapter/ # the public integration surface
│ ├─ types.ts # the OpenChartsAdapter contract
│ ├─ provider.tsx # <OpenChartsProvider adapter={...}>
│ ├─ registry.ts # the installed adapter, read by api / wsClient
│ ├─ demo.ts # the bundled backend, as a reference adapter
│ └─ conformance.ts # a test suite adapter authors can run
├─ services/
│ ├─ api.ts # internal facade, resolves against the adapter
│ ├─ ws.ts # adapter events → the terminal's channels
│ ├─ replay.ts # session-replay buffer and transport state
│ ├─ toast.ts # notification stream behind onNotification()
│ ├─ api/ # legacy HTTP clients from the original platform;
│ │ # chart-templates.ts is localStorage-backed and live
│ ├─ queries.ts # TanStack Query hooks
│ ├─ store.tsx # zustand stores (auth + trading state)
│ ├─ schemas.ts # zod schemas / shared types
│ └─ demo/ # engine, feed, candles, bus, instruments, bundled data
├─ __tests__/ # Vitest unit tests
└─ styles/
scripts/
└─ fetch-demo-data.mjs # refresh the bundled real OHLC
Everything below is a real extension point in the code, not a roadmap. npm run typecheck and npm run test are the two gates; TypeScript runs in strict mode
with noUncheckedIndexedAccess and noUnusedLocals, and @/ resolves to
src/.
- Write the pure calculation in
src/lib/indicators.ts(they take candles and return{ time, value }[]— copysmaorrsi). - Add the type to the
IndicatorTypeunion and an entry toINDICATOR_REGISTRY(label,pane: "overlay" | "below",defaultParams,color). The toolbar menu renders straight from the registry. - Render it in
src/pages/trading/useIndicators.ts, which creates and disposes the Lightweight Charts series per active indicator.
The drawing engine is custom and lives in
src/lib/chart-plugins/drawing-tools/.
A new tool touches five places:
| File | What you add |
|---|---|
pages/trading/constants.ts |
the tool name in the DrawingTool union and any new fields on DrawingLine |
drawing-tools/manager.ts |
a branch in buildNew() that turns anchor points into a DrawingLine, plus the single-click list in the pointer handler if it is a one-click tool |
drawing-tools/renderers.ts |
a case in the render switch that paints it |
drawing-tools/hit-test.ts |
a case so it can be selected and dragged |
pages/trading/DrawingToolRail.tsx |
the rail button, icon and group |
geometry.ts holds the shared math, types.ts the resolved (pixel-space)
shapes. Drawings are plain JSON, so anything you add persists automatically.
Extend PluginBase from
src/lib/chart-plugins/plugin-base.ts
(the Lightweight Charts ISeriesPrimitive contract: pane views + renderers),
attach it where the other primitives are attached in ChartPanel.tsx, then add
it to the plugin list in ChartToolbar.tsx so it gets a toggle. The existing
six plugins are the reference implementations.
lib/indicators.ts holds plain functions over candle arrays, with
49 tests pinning the maths (including the
degenerate cases: flat series, zero-range windows, zero-volume bars and invalid
periods). Add an indicator there and the guards apply automatically.
All seven are expected to pass before a PR, and all seven pass on main:
npm run typecheck # tsc --noEmit, strict, zero errors
npm run lint # ESLint 9 flat config, zero errors *and* zero warnings
npm run test # Vitest — 200 tests
npm run build # production bundle
npm run check:size # first-load payload budget
npm run build:lib # the npm package; fails if any CSS escapes the scope
npm run check:package # the built package matches its exportsGitHub Actions runs all seven on every pull request (.github/workflows/ci.yml),
with lint under --max-warnings=0. The size gate exists because the first-load
payload was 4.94 MB before the candle data was made lazy — see
scripts/check-bundle-size.mjs.
The paper engine is the part worth breaking carefully — its order lifecycle,
margin maths and rounding are covered in
src/__tests__/engine.test.ts.
- Zustand (
services/store.tsx) — auth and trading state (symbols, accounts, selected symbol, live ticks). - TanStack Query (
services/queries.ts) — everything fetched throughapi, keyed byqueryKeys. - The channel bus (
services/demo/bus.ts) —services/ws.tsrepublishes adapter events on themarket-data,positions,ordersandaccountchannels;components/MarketDataBridge.tsx, mounted by the provider, is the single place they land in React. localStorage— drawings (oc_drawings_<SYMBOL>), chart templates (oc_chart_templates), chart and trader preferences (trader_prefs, scoped per user id) and the sound mute flag (tradeSoundMuted). Chart preference changes broadcast achart-preferences-updatedwindow event rather than going through a store.
apiis typed exactly.services/api.tsis a plain object with real signatures, so calling a method it does not implement is a compile error. New backend capabilities belong in the adapter contract (adapter/types.ts) first, then a facade method that maps onto it.- Candle data loads lazily.
services/demo/candles.tsimports each<SYMBOL>_<tf>.jsonon demand, sogetHistoryis async and the feed primes the 1m series before it starts ticking. - Never touch
localStoragedirectly — uselib/safe-storage.ts. Raw access throws in private mode, when site data is blocked, and on quota exhaustion. REPLAY_ENABLEDinpages/trading/constants.tsstill gates the replay HUD and scrubber, should you want them off.- Dates are local, not UTC. Use
toLocalDateString()fromlib/utils.ts;toISOString().slice(0, 10)is a different day for most of the world and put the replay picker on a day with no data. - React runs in
StrictMode, so chart effects mount twice in development — every series, primitive and subscription needs a working cleanup path. - The demo engine is in-memory. Anything you want to survive a reload has to
be written to
localStorage(or a backend you wire up yourself).
The demo OHLC lives in src/services/demo/data/ as JSON, fetched from the public
Binance klines endpoint (no API key required):
node scripts/fetch-demo-data.mjsThis re-pulls up to 1000 bars per symbol per timeframe and rewrites the bundled files
plus data/manifest.json. The candles are genuine market history — OpenCharts
never ships synthetic OHLC.
You do not fork OpenCharts to use your own data. Implement one interface, hand it to the provider, and the whole terminal — chart, order ticket, positions, replay — runs against your backend.
import { OpenChartsProvider, Terminal, type OpenChartsAdapter } from "@dylanpersonguy/opencharts";
const adapter: OpenChartsAdapter = {
name: "my-broker",
marketData: {
getSymbols: () => myApi.instruments(),
getCandles: ({ symbol, timeframe, limit, fromMs, toMs }) =>
myApi.candles(symbol, timeframe, { limit, fromMs, toMs }),
subscribe: (handler) =>
myFeed.on("quote", (q) =>
handler({ type: "tick", symbol: q.symbol, bid: q.bid, ask: q.ask }),
),
},
};
<OpenChartsProvider adapter={adapter}>
<Terminal />
</OpenChartsProvider>;That is the minimum: marketData is the only required part. Add trading
to enable the order ticket and positions, and storage to keep a trader's
drawings, templates and journal with their account instead of their browser.
Defined in src/adapter/types.ts:
| Group | Required | What it covers |
|---|---|---|
marketData |
yes | getSymbols, getCandles, subscribe, and optionally getQuote, connect/disconnect, onConnectionChange, setSymbolInterest |
trading |
no | accounts, positions, orders, closed trades, place/modify/cancel, and a subscribe for fills and account updates |
storage |
no | a three-method key/value driver for drawings, chart templates and journal entries |
Without trading, the terminal runs as a read-only chart and trading calls
fail with a clear message rather than silently doing nothing.
- Candle times are seconds, not milliseconds, and bars come back oldest first.
toMsis exclusive. The chart pages backwards withfromMs/toMs; an adapter that ignores the window makes scroll-back loop, and one that treatstoMsas inclusive duplicates a bar per page.- Every method returns a promise and rejects on failure — never throw synchronously. Rejection messages are shown to the trader as-is, so write them for a human.
subscribereturns an unsubscribe function, and must stop delivering once it is called.
A conformance suite ships with the project and checks the things that fail quietly:
import { runAdapterConformance } from "@dylanpersonguy/opencharts/conformance";
runAdapterConformance("my-broker", () => createMyAdapter(), { symbol: "BTCUSD" });Trading checks submit and close orders, so they are opt-in
(allowTrading: true) — run them against a paper or sandbox gateway only.
<Terminal /> is the full terminal in its default arrangement. To put the chart
in your own dashboard, mount the panels individually instead — each reads what
it needs from <TerminalProvider>, so they stay in sync without prop drilling:
<OpenChartsProvider adapter={myAdapter}>
<TerminalProvider>
<main><Chart /></main>
<aside>
<Watchlist />
<OrderTicket />
</aside>
<footer><Positions /></footer>
</TerminalProvider>
</OpenChartsProvider>| Panel | What it is |
|---|---|
<Chart /> |
Candles, indicators, drawing tools, position overlays, replay |
<OrderTicket /> |
Market / limit / stop entry with take-profit and stop-loss |
<Positions /> |
Open positions with close, partial close and modify |
<Orders /> |
Working orders with cancel and modify |
<Watchlist /> |
Instruments with live prices |
<DepthLadder /> |
Depth ladder around the mid price |
TerminalProvider holds only shared state — the charted symbol, timeframe,
account, drawings, positions — plus the confirm and modify dialogs, so every
composition gets them. Layout is entirely yours. A full example is in
examples/embedded-chart/.
<OpenChartsProvider> is the whole runtime. It installs the adapter, opens the
market-data stream, loads instruments (and accounts, when the adapter trades),
and supplies everything the panels depend on — a query cache, tooltips,
notifications and an error boundary. If the adapter fails to load, it shows the
error with a retry button instead of a blank screen.
| Prop | Default | |
|---|---|---|
adapter |
— | Your OpenChartsAdapter (required) |
fallback |
"Loading OpenCharts…" | Shown while instruments and accounts load |
queryClient |
its own | Pass yours to share a TanStack Query cache; by default the terminal's stays separate from the host's |
showNotifications |
true |
Render the built-in notifications |
className / style |
— | Applied to the scoping element |
Every rule in style.css — Tailwind's reset, utilities and the theme variables —
is scoped under the .opencharts element the provider renders. Your page keeps
its own fonts, spacing, form controls, tables and scrolling, and a
shadcn/ui theme using the same variable names
(--background, --primary, …) is left alone. The build fails if any selector
escapes the scope.
The terminal fills its container, so give the parent a height. For the light
theme, add the light class to <html> or to the scoping element.
Order fills, rejections and their reasons, and validation warnings come through one stream:
import { onNotification } from "@dylanpersonguy/opencharts";
const stop = onNotification((n) => myToaster.show(n.title, n.message, n.type));Pair it with showNotifications={false} to replace the built-in toasts.
examples/binance-adapter/ is a complete live
adapter for Binance's public API in a single file: REST klines, book-ticker
websocket streams, reconnect with backoff, composed with the bundled paper
engine so you can trade live prices risk-free.
VITE_ADAPTER=binance npm run devIt is the same terminal in the screenshot above, with nothing patched — only a different adapter installed.
Demo instruments are defined in src/services/demo/instruments.ts (currently
BTCUSD, ETHUSD, SOLUSD, BNBUSD, XRPUSD and ADAUSD). To add one:
- Add a
Symbolentry (name, tick size, contract size, etc.). - Add its trading pair to the
SYMBOLSmap inscripts/fetch-demo-data.mjs. - Run
node scripts/fetch-demo-data.mjsto fetch and bundle its history.
Everything works with no configuration. Optional environment variables:
| Variable | Effect |
|---|---|
VITE_ADAPTER |
Set to binance to run the demo against live Binance prices |
VITE_API_URL |
Base URL used by the legacy HTTP client in src/services/api/ |
OpenCharts ships no analytics or telemetry. Nothing leaves the browser except the requests your adapter makes, and the TradingView embeds below.
Note that the Calendar tab and the technical-analysis gauge load TradingView's embed scripts from TradingView's CDN — they need no key, but they are third-party network requests. Drop those components if you want a strictly offline build.
| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server |
npm run dev:ci |
Dev server bound to 0.0.0.0:5173 |
npm run build |
Production build to dist/ |
npm run check:size |
Fail if the first-load payload exceeds its budget (run after build) |
npm run build:lib |
Build the npm package into dist-lib/ (fails if any CSS escapes the scope) |
npm run check:package |
Verify the package matches its exports (run after build:lib) |
npm run preview |
Preview the production build |
npm run typecheck |
Type-check the project with tsc |
npm run test |
Run the Vitest unit tests |
npm run test:watch |
Run tests in watch mode |
npm run lint |
ESLint 9 (flat config in eslint.config.js) |
node scripts/fetch-demo-data.mjs |
Refresh bundled real OHLC |
- React 19 + TypeScript 5.7 + Vite 6
- lightweight-charts 4 (+ custom plugins) for the chart
- Zustand for state, TanStack Query for caching
- Tailwind CSS 3 + Radix UI primitives + lucide-react icons
- Zod in
services/schemas.tsas the shared type source (types are inferred from the schemas; nothing is parsed at runtime) - Vitest + Testing Library for tests
The published package has one peer dependency (React 18.2+) and ships ESM, type declarations and a precompiled stylesheet. Lint, typecheck and tests run on ESLint 9 (flat config, including the React Compiler rule set), TypeScript 5.7 and Vitest.
- Account state is ephemeral — positions, orders and balance reset on reload.
Drawings, chart preferences and templates persist via
localStorage. - Timeline is normalized — bundled history is shifted so the latest bar is "now", and the tick replay loops when it reaches the end of the 1m series. OHLC values are real; the timestamps are remapped to feel live.
- The DOM ladder is synthetic — real mid price, simulated depth. There is no order book in the bundled data, so sizes are derived from the price level.
- One position per fill. The engine is hedging-style: a buy and a sell on the same symbol coexist rather than netting off.
- Crypto-only demo symbols out of the box (the data source is Binance). Wire your own adapter for FX, futures or equities.
- No news or sentiment integration. The News tab shows labelled sample items. Components that fabricated market analysis — a sentiment gauge and "STRONG BUY" trade signals derived from a hash of the symbol name — were deleted rather than left for someone to wire up.
- Replay covers roughly the last 16 hours. The session buffer is the bundled 1m history for the charted symbol, so picking an earlier date reports an empty session rather than inventing data.
- The trade calculator is still unmounted, and challenge/risk-rule price levels stay inert because the demo account reports no risk limits.
- Coverage is logic-first. The paper engine, indicators, replay, journal, storage, ids, live P&L and the event bus are covered, plus the order ticket and positions table at the component level. The chart itself is verified by driving the running app, not by unit tests. Playwright is a declared dev dependency but no end-to-end specs are checked in.
- The production
buildruns Vite only; runnpm run typecheckseparately for full type checking.
OpenCharts is a community project and it is open to anyone who wants to make it better — there is no core team you need permission from. Issues, pull requests, questions and design opinions are all welcome, and small first contributions are genuinely useful.
Good places to start:
- Indicators and drawing tools — the two most self-contained additions; see Building on OpenCharts.
- Persistence for the paper account, so a session survives a reload.
- Data adapters for other exchanges or brokers, written against the
OpenChartsAdaptercontract —examples/binance-adapter/is the template, and the conformance suite tells you when it is right. - Themes and layout customization — the chart palette lives in
pages/trading/constants.tsand the design tokens intailwind.config.js. - Finishing or removing the dormant panels listed under Known limitations.
- Tests, docs and bug reports — coverage is thin and every report helps.
CONTRIBUTING.md covers setup, the CI gates, the layout
of the codebase and what makes a reviewable PR. Please also read the
Code of Conduct, and report security issues privately via
SECURITY.md rather than in a public issue.
The one architectural rule: UI code talks to api and wsClient, never to an
adapter or the demo layer directly, and backends plug in only through the
OpenChartsAdapter contract. That is what keeps the terminal backend-agnostic.
OpenCharts stands on TradingView's open-source Lightweight Charts™, which renders every chart here, and on the plugin examples shipped in that repository. Thank you to TradingView for releasing it under a permissive license.
OpenCharts is released under the MIT License — see LICENSE.
It bundles TradingView's Lightweight Charts™, which is licensed under the Apache License 2.0 and requires that you keep its attribution notice and a link to https://www.tradingview.com/ visible to your users. That notice lives in NOTICE — keep it with any copy you redistribute. See Built on Lightweight Charts for what that means in practice before you deploy or redistribute a fork.
