Skip to content

Commercial

Moving from TradingView

One real Charting Library widget ported end to end: the drop-in, what it ignores, and what onChartReady becomes.

Read this page as Markdown, or take the whole documentation in one file.

The Widget Every Terminal Has

This page ports one real TradingView Charting Library integration: a new TradingView.widget call with a theme, feature flags, overrides and a save/load service, a JS-API datafeed with callbacks and timescale marks, and an onChartReady block that adds two studies and a shape. The API mapping is the table to keep open beside it; this page is the port itself.

const widget = new TradingView.widget({
  container: "chart",
  library_path: "/charting_library/",
  symbol: "IBM",
  interval: "D",
  datafeed,                                   // onReady, searchSymbols, resolveSymbol, getBars, subscribeBars, getTimescaleMarks
  locale: "en",
  theme: "Dark",
  timezone: "America/New_York",
  autosize: true,
  disabled_features: ["header_symbol_search", "header_compare", "use_localstorage_for_settings", "header_saveload"],
  enabled_features: ["study_templates", "hide_left_toolbar_by_default"],
  overrides: { "mainSeriesProperties.candleStyle.upColor": "#26a69a", "mainSeriesProperties.candleStyle.downColor": "#ef5350", "paneProperties.background": "#131722" },
  studies_overrides: { "volume.volume.color.0": "#ef5350", "volume.volume.color.1": "#26a69a" },
  charts_storage_url: "https://saveload.example.com",
  charts_storage_api_version: "1.1",
  client_id: "terminal",
  user_id: userId,
});
widget.onChartReady(() => {
  const chart = widget.activeChart();
  chart.createStudy("Moving Average Exponential", false, false, [21]);
  chart.createStudy("Relative Strength Index", false, false, [14]);
  chart.createShape({ time: t, price: 250 }, { shape: "horizontal_line", text: "Target 250", lock: true });
});

Step One: The Drop-In

npm install @ortex-charts/migrate
import { tradingViewWidget } from "@ortex-charts/migrate";

const shell = tradingViewWidget(widgetOptions);
console.info("no counterpart:", shell.ignored);

widgetOptions is the typed ChartingLibraryWidgetOptions from above, and datafeed the typed IBasicDataFeed inside it, passed as they are; the translator's input types accept both without a cast. The datafeed runs unchanged: onReady is awaited once, resolveSymbol once per symbol, getBars is asked in seconds and answered in milliseconds as before, and subscribeBars streams into the chart. The chart is on screen with history, live bars, the dark theme, the New York time zone and no symbol box.

shell.ignored reads:

studies_overrides
charts_storage_url
charts_storage_api_version
client_id
user_id
disabled_features: use_localstorage_for_settings
disabled_features: header_saveload
enabled_features: study_templates

Everything not in that list was mapped: symbol and interval, theme, timezone, locale, header_symbol_search to toolbar.symbol: false, header_compare to compare: false, hide_left_toolbar_by_default to drawingRailCollapsed: true, and the three overrides to the up, down and background theme tokens. library_path is neither, because there is no bundle to load.

fromTVWidgetOptions(widgetOptions) returns the same shell options and the same ignored list without creating anything.

Step Two: What Is Left by Hand

Everything in ignored, plus what lived in onChartReady and in the datafeed's optional methods.

The onChartReady block. The shell is ready when tradingViewWidget returns, so the block becomes the lines after it. createStudy is shell.addIndicator; createShape is shell.drawings.add.

shell.addIndicator("ema", { length: 21 });
shell.addIndicator("rsi", { length: 14 });
shell.drawings?.add({ kind: "horizontalLine", points: [{ time: t, price: 250 }], style: { text: "Target 250" }, locked: true });

Study names become indicator ids: Moving Average Exponential is ema, Relative Strength Index is rsi, Bollinger Bands is bb, MACD is macd. The indicators page lists all 51, and listIndicators() is the same list at runtime.

Timescale marks. getTimescaleMarks has no wrapper; the earnings badges it drew are events with style: "lane":

shell.chart.setEvents(earnings.map((time) => ({ time, style: "lane", group: "Earnings", text: "E", tooltip: "Earnings" })));

The save/load service. charts_storage_url, client_id and user_id pointed at TradingView's storage contract. There is none here, on purpose: shell.getLayout() is JSON and onLayoutChange fires when it changes, so the store is the one you already have. onLayoutChange is a creation option, so it goes in the second argument:

const shell = tradingViewWidget(widgetOptions, {
  layout: await api.loadChartLayout(userId),
  onLayoutChange: (layout) => void api.saveChartLayout(userId, layout),
  drawingsStore: { load: (s) => api.loadDrawings(userId, s), save: (s, d) => api.saveDrawings(userId, s, d) },
});

Drawings are stored per symbol rather than inside the layout; see the shell and layouts.

studies_overrides. Indicator colors are per call: shell.addIndicator("ema", { length: 21 }, { colors: { ema: "#4fc3f7" } }). The volume pane's colors are the volumeUp and volumeDown theme tokens.

Resolutions. The widget read supported_resolutions from the datafeed and offered only those. tradingViewWidget is synchronous, as the widget constructor was, and the feed's list is not, so it offers the shell's default resolutions: a daily-only feed would show minute buttons. tradingViewWidgetAsync waits for the list first:

const shell = await tradingViewWidgetAsync(widgetOptions);   // resolutions: what the symbol, else onReady, declared

Feature flags with no counterpart. use_localstorage_for_settings and header_saveload describe TradingView's own storage, which does not exist here. study_templates is a layout you store.

Step Three: The Finished Port

import { tradingViewWidgetAsync } from "@ortex-charts/migrate";

const shell = await tradingViewWidgetAsync(widgetOptions, {
  layout: await api.loadChartLayout(userId),
  onLayoutChange: (layout) => void api.saveChartLayout(userId, layout),
});

shell.addIndicator("ema", { length: 21 });
shell.addIndicator("rsi", { length: 14 });
shell.drawings?.add({ kind: "horizontalLine", points: [{ time: t, price: 250 }], style: { text: "Target 250" }, locked: true });
shell.chart.setEvents(earnings.map((time) => ({ time, style: "lane", group: "Earnings", text: "E" })));

The datafeed object is the one from the widget, untouched. Delete the library_path directory from public/: that was 6 MB.

Keeping Your Own Shell

A product that already has its own toolbar wants the datafeed and nothing else. tradingViewDatafeed wraps it, and supportedResolutions asks it what it serves:

import { tradingViewDatafeed } from "@ortex-charts/migrate";
import { createChartShell } from "@ortex-charts/ui";

const feed = tradingViewDatafeed(datafeed);
const shell = createChartShell(el, {
  datafeed: feed,
  symbol: "IBM",
  resolution: "1D",
  resolutions: await feed.supportedResolutions("IBM"),
  toolbar: { symbol: false, resolution: false },
});

From Lightweight Charts

There is no translator for Lightweight Charts and none is needed: the mental models are the same and the mapping table is exact. The one idiom to know is the overlay volume, a histogram on priceScaleId: "" with scaleMargins, which is createFinancialChart with volume: true; and times, which were seconds there and are milliseconds here.

const fc = createFinancialChart(el, { theme: "dark", timeZone: "America/New_York", resolution: "1D", data: bars, volume: true });
fc.addIndicator("sma", { length: 20 });                                       // was addSeries(LineSeries) + your own SMA
fc.chart.addPriceLine(fc.main.id, { price: 250, lineStyle: "dashed", title: "Target" }); // was series.createPriceLine
fc.setMarkers(markers);                                                       // was createSeriesMarkers
fc.chart.timeScale.fitContent();                                              // a property, not a method
fc.chart.subscribeCrosshairMove((e) => e.seriesValues.get(fc.main.id));      // was param.seriesData.get(series)

Before You Ship

  • Read shell.ignored once, then delete the log line.
  • Studies with no counterpart. Advanced Charts ships about 110 studies against 51 built in and 78 more in @ortex-charts/indicators-extra; the long tail is mostly variations. Budget for the ones you have to register.
  • Your CSS resets now apply. There is no iframe. A global canvas rule or * { box-sizing: content-box } will show.
  • Load an old layout, change something, save it, reload.
  • A symbol with a short history, so the indicators degrade rather than throw.
  • The licence key on the deployed origin, not just on localhost.