Skip to content

Guides

Migrate from TradingView

This article maps an existing TradingView Advanced Charts or Trading Platform integration onto a VerticalProp embed. Most of it ports directly: the datafeed contract, the method names and the resolution strings are deliberately the same. The differences are concentrated in two places, and both are consequences of the chart running in an iframe rather than in your page.

TradingView is a trademark of TradingView, Inc. This page exists to help you port an existing integration and makes no claim of affiliation or endorsement.

The two structural differences

Everything else on this page follows from these. Read them first.

What changes
The chart is in an iframeA charting library runs in your realm, so you hold real objects and call methods on them synchronously. Here the chart is a separate document on our origin, so every call crosses a boundary: **every method returns a promise**, and a chained setter becomes one object. None of our code runs on your page, which is the reason to do it this way; see the security model.
You bring the market dataWe distribute a charting engine rather than market data, so a chart published on your site supplies its own bars through data-datafeed. Your existing datafeed object is what goes there, largely unchanged. See Use your own data.

Widget options become URL parameters

The constructor options an Advanced Charts integration passes at construction are read from the embed URL instead, or set from the loader after mount. The full list is on URL parameters; these are the ones that carry over directly.

Widget optionEmbed equivalentNotes
symboldata-symbolAlso chart.setSymbol(s) after mount.
intervaldata-intervalSame resolution strings. Also chart.setInterval().
themedata-themelight or dark. Also chart.setTheme().
localedata-localeChart chrome language.
timezonedata-timezoneIANA names. A UTC offset is refused, since it cannot express daylight saving.
numeric_formattingdata-numberFormatA named preset (standard, european, plain, spaced) rather than two separator characters. The default is the reader's own locale.
header_widgetdata-toolbarOff by default here. A toolbar button requires it; see Control the chart.
overridesdata-colorsTen colour roles rather than individual engine tokens, since a role sets a family: up moves the rising candle's body, wick and border together. Also chart.setStyle(). See Styling.
studiesdata-indicatorsCommunity indicators. Built-in studies are added with chart.createStudy().
There is no disabled_features / enabled_features equivalent. The embed exposes the handful of switches that make sense on a published chart as named parameters, and data-toolbar, data-legend and the two view locks (setScrollEnabled, setZoomEnabled) cover what a featureset list was usually being used for.

The datafeed ports almost unchanged

The method names, their arguments and their callbacks are the same, so an existing datafeed object is the object you register. Two of the ten are required and the rest are optional.

MethodStatus
onReadyOptional
searchSymbolsOptional
resolveSymbolRequired
getBarsRequired
subscribeBarsNeeded for live updates
unsubscribeBarsNeeded for live updates
getMarksOptional
getServerTimeOptional
getTimescaleMarksOptional
searchSymbolsPaginatedOptional
getFootprintOptional
subscribeTradesOptional
unsubscribeTradesOptional

The one change is registration. Instead of passing the object to a constructor, you name it on the page and the loader wires it across the boundary for you.

Registration, before and after
// Advanced Charts
new TradingView.widget({ datafeed: myFeed, symbol: 'AAPL', interval: '60' });

// Embed: name the feed, and point the placeholder at it.
vpEmbed.datafeed('myFeed', myFeed);
// <div class="vp-chart" data-symbol="AAPL" data-interval="60"
//      data-datafeed="myFeed"></div>
time on a bar is in milliseconds while from and to in a getBars request are in seconds. That is inherited from the contract this mirrors, so a working feed already has it right. It is worth re-checking anyway, because it fails as a chart that draws nothing in the window it asked about, with no error.

Method names, and the two that differ

chart.createOrderLine(), createPositionLine(), createExecutionShape(), createShape(), createMultipointShape(), createStudy(), removeEntity(), removeAllShapes() and the getters keep their names. Two read methods were renamed here and both accept the original spelling as an alias, so code calling them runs unchanged.

You wroteAlso available asReturns
getAllShapes()getShapes(){ id, name } per drawing.
getAllStudies()getStudies(){ id, name } per indicator.

The change that does need editing is chaining. A line handle takes an object rather than a sequence of setters, because each setter would be its own message across the boundary and the frame between them would render the new price beside the old label.

Chained setters become one object
// Advanced Charts
line.setPrice(64200).setText('Buy limit').setLineColor('#f7931a');

// Embed: one message, one repaint.
await line.set({ price: 64200, text: 'Buy limit', lineColor: '#f7931a' });
An unrecognised property is refused by name rather than ignored, so a property that existed in your previous integration and does not exist here fails loudly on the first call instead of silently doing nothing. Every property and its domain is on Objects.

Events

widget.onChartReady(cb) becomes chart.on('dataLoaded', cb), and widget.subscribe(name, cb) becomes chart.on(name, cb). on returns an unsubscribe function; call it when your component unmounts. Commands issued before the chart exists are buffered and replayed in order, so you do not have to wait for readiness before calling a method.

Subscribing
const chart = vpEmbed.chart(document.querySelector('.vp-chart'));

const off = chart.on('symbolChanged', ({ symbol }) => console.log(symbol));
// later
off();

Port one screen and see it run

This is a complete integration: a host-supplied datafeed, an order line, a position with a bracket, and an event handler. It runs on a page that is not ours, which is the property the whole design exists to preserve.

A ported integration
<script src="https://view.verticalprop.com/embed.js"></script>
<div class="vp-chart"
     data-symbol="DEMO"
     data-interval="60"
     data-toolbar="1"
     data-theme="dark"
     data-datafeed="myFeed"
     style="height: 420px"></div>
<pre id="log"></pre>

<style>
body { margin: 0; background: #0b0f19; color: #e6e8eb;
       font: 13px/1.5 ui-monospace, monospace; }
#log { padding: 8px 12px; margin: 0; white-space: pre-wrap; }
</style>

<script>
// A deterministic series, so the pen never depends on a venue and the
// same code always draws the same chart.
function bars(from, to, stepSec) {
  const out = [];
  let price = 100;
  for (let t = Math.ceil(from / stepSec) * stepSec; t <= to; t += stepSec) {
    price += Math.sin(t / 5000) * 0.6;
    const open = price;
    const close = price + Math.cos(t / 3000) * 0.8;
    out.push({
      time: t * 1000,                       // a bar's time is MILLISECONDS
      open, close,
      high: Math.max(open, close) + 0.5,
      low: Math.min(open, close) - 0.5,
      volume: 1000 + (t % 500),
    });
  }
  return out;
}

vpEmbed.datafeed('myFeed', {
  onReady: () => ({ supported_resolutions: ['60'] }),
  resolveSymbol: (name) => ({
    name, ticker: name, pricescale: 100, session: '24x7', timezone: 'Etc/UTC',
  }),
  // from and to are SECONDS; a bar's own time is MILLISECONDS.
  getBars: (symbol, resolution, from, to) => bars(from, to, 3600),
  subscribeBars: () => {},
  unsubscribeBars: () => {},
});

const log = (m) => { document.getElementById('log').textContent += m + '\n'; };
const chart = vpEmbed.chart(document.querySelector('.vp-chart'));

chart.on('dataLoaded', async () => {
  // Declare that this page can carry an order, or the price-scale menu
  // offers alerts and drawings only.
  await chart.setTrading({ quantity: 1 });

  const entry = await chart.createOrderLine({
    price: 101, text: 'Buy limit', quantity: '1', lineColor: '#26a69a',
  });

  await chart.createPositionLine({
    price: 99.5, text: 'Long', quantity: '2',
    takeProfit: 104, stopLoss: 97,
  });

  await chart.createExecutionShape({
    // SECONDS here, and it must be a bar's own opening time.
    time: Math.floor(Date.now() / 3600000) * 3600 - 3600 * 6,
    price: 99.5, direction: 'buy', text: 'Filled 2 @ 99.50',
    tooltip: 'Entry\n2 @ 99.50',
  });

  // One message, one repaint. There is no chaining.
  await entry.set({ price: 100.8, text: 'Buy limit (moved)' });
  log('objects placed. Drag the blue line.');
});

chart.on('orderMove', ({ handle, price }) => log('moved ' + handle + ' to ' + price));
chart.on('orderCancel', ({ handle }) => log('cancelled ' + handle));
chart.on('orderSubmit', (o) => log('submit ' + JSON.stringify(o)));
chart.on('positionClose', ({ handle }) => log('close ' + handle));
</script>

The bars are generated in the pen rather than fetched, so it draws on the first run with no key and no venue to be unavailable.

What is not here

Stated plainly, because the expensive way to find out is three days into a port. Some of these are deliberate and some are simply not built yet, and the difference is named in each row.

Not availableWhy, and what to do instead
Server-side chart storageDeliberate. We keep nothing about your reader, so there is no account to save a layout against. saveState and loadState hand the whole chart to you as plain data and you store it. See Remember the reader's chart.
crossHairMoved and pointer-level eventsDeliberate. Cursor position is something a reader is doing rather than something they made, and no host feature needs it. visibleRangeChanged and dataLoaded cover the cases that motivated it.
Drawing events (drawing, drawing_event)Not built. Nothing reports the moment a reader finishes a trend line, so save on an event your page already handles rather than polling. Read the current set with getShapes or getDrawings.
Drawing groupsNot built. Drawings are addressed individually by the id createShape returns.
createAnchoredShapeNot built. Every drawing here is positioned in time and price rather than as a percentage of the viewport.
ownerStudyId on a drawingNot built. Drawings render on the price pane.
takeScreenshotNot built, and it needs a decision rather than a method: the engine's capture ends in a download or an upload, so there are no bytes to send back across the boundary.
Strategy or backtest surfacesDeliberate. The embed draws; it does not run a strategy engine.

Before you commit to the port

Two things are worth confirming against a real page early, because both are cheap to check now and expensive to discover late.

CheckHow
Your datafeed answers from a third-party originThe chart calls it from an iframe on your page, so anything your feed fetches has to be reachable from the browser with the right CORS headers. A feed that worked server-side in a proxy may not.
Your content security policy allows the frameThe embed needs frame-src for our origin. Nothing of ours executes in your page, so no script-src entry is required, which is usually the point.

If your organisation will not load a third-party script at all, the loader is optional: the message protocol is documented and a bridge is about thirty lines you write and own. See Without our script.

A first-party embed, drawn from our own data. On your site the same chart carries your feed.