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.
The two structural differences
Everything else on this page follows from these. Read them first.
| What changes | |
|---|---|
| The chart is in an iframe | A 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 data | We 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 option | Embed equivalent | Notes |
|---|---|---|
symbol | data-symbol | Also chart.setSymbol(s) after mount. |
interval | data-interval | Same resolution strings. Also chart.setInterval(). |
theme | data-theme | light or dark. Also chart.setTheme(). |
locale | data-locale | Chart chrome language. |
timezone | data-timezone | IANA names. A UTC offset is refused, since it cannot express daylight saving. |
numeric_formatting | data-numberFormat | A named preset (standard, european, plain, spaced) rather than two separator characters. The default is the reader's own locale. |
header_widget | data-toolbar | Off by default here. A toolbar button requires it; see Control the chart. |
overrides | data-colors | Ten 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. |
studies | data-indicators | Community indicators. Built-in studies are added with chart.createStudy(). |
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.
| Method | Status |
|---|---|
onReady | Optional |
searchSymbols | Optional |
resolveSymbol | Required |
getBars | Required |
subscribeBars | Needed for live updates |
unsubscribeBars | Needed for live updates |
getMarks | Optional |
getServerTime | Optional |
getTimescaleMarks | Optional |
searchSymbolsPaginated | Optional |
getFootprint | Optional |
subscribeTrades | Optional |
unsubscribeTrades | Optional |
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.
// 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 wrote | Also available as | Returns |
|---|---|---|
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.
// 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' });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.
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.
<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 available | Why, and what to do instead |
|---|---|
| Server-side chart storage | Deliberate. 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 events | Deliberate. 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 groups | Not built. Drawings are addressed individually by the id createShape returns. |
createAnchoredShape | Not built. Every drawing here is positioned in time and price rather than as a percentage of the viewport. |
ownerStudyId on a drawing | Not built. Drawings render on the price pane. |
takeScreenshot | Not 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 surfaces | Deliberate. 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.
| Check | How |
|---|---|
| Your datafeed answers from a third-party origin | The 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 frame | The 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.