Skip to content
Trading Systems

Wiring TradingView Advanced Charts To Your Own Data: Custom Datafeeds, Real-Time Streaming And AI Signal Overlays That Do Not Lie About Time

Almost every trading platform eventually reaches the same decision: traders want the TradingView chart they already know, and you need it reading your prices, your instruments and your signals. The Datafeed API is how you do it, and the documentation covers the happy path well. What it does not cover is everything that bites in production - rebuilding resolutions the backend does not serve, the subscriber bookkeeping that leaks when a trader flips symbols forty times a minute, why bars arrive out of order at the session boundary, and the marks API detail that quietly turns an AI signal overlay into a look-ahead bias generator. This is the integration as we actually ship it, with code.

AlchmAI Engineering15 min read

3 APIs

TradingView exposes Charting Library, Datafeed and Broker REST - the Datafeed API is the one that decides whether your chart feels native

2 routes

UDF adapter over plain HTTP, or a hand-written Datafeed API implementation - the choice determines your streaming ceiling on day one

1 socket

A single shared WebSocket should serve every subscription on the page; one connection per subscriber is the most common production mistake

2 timestamps

Every AI signal needs both - when it refers to, and when it was generated. Conflating them is how backtests quietly become fiction

We have written fully custom GPU charting engines for clients who genuinely needed them, and we have integrated TradingView's Advanced Charts for many more who did not. The honest split is the one we gave in an earlier playbook: build custom when the value is in data volume or a domain view nobody else renders; integrate the standard library when the value is in the ecosystem traders already have in their fingers - the indicators, the drawing tools, the saved layouts, the muscle memory. In practice most platforms want the second, and the quality of that integration is decided almost entirely by one component: the datafeed.

TradingView documents two routes. The UDF adapter is a ready-made implementation that talks plain HTTP to a backend you write to a fixed response format - fast to stand up, and the right call for a read-only research view. The Datafeed API is a set of JavaScript methods you implement yourself, which gives you any transport you like, WebSockets included, and any custom logic you need. For anything with live prices, take the second route from the beginning. Teams that start on UDF because it is quicker almost always rewrite within a quarter, because streaming is not a feature you bolt onto a polling adapter.

Configuration: The Fields That Decide Your Pain

onReady looks trivial and sets constraints you live with for the rest of the project. Two fields matter more than the rest.

typescriptchart/datafeed/onReady.ts
const CONFIG: DatafeedConfiguration = {
  // Only advertise what your BACKEND serves natively. Anything listed here
  // that you actually rebuild client-side will be requested with a history
  // range sized for the advertised resolution, and you will under-fetch.
  supported_resolutions: ["1", "5", "15", "60", "240", "1D", "1W"],

  // Sessions are a calendar, not a guess. With US equities moving to a
  // 23-hour day, hardcoding "0930-1600" here ages badly - serve it per
  // instrument from resolveSymbol instead and keep this as a fallback.
  exchanges: [{ value: "LSE", name: "London Stock Exchange", desc: "LSE" }],
  symbols_types: [{ name: "Equity", value: "equity" }],

  supports_marks: true,          // AI signal overlays - see below
  supports_timescale_marks: true,
  supports_time: true,           // lets the library show server time, not local
};

export function onReady(callback: (c: DatafeedConfiguration) => void): void {
  // MUST be asynchronous. Calling back synchronously works in development
  // and produces an initialisation race in production builds.
  setTimeout(() => callback(CONFIG), 0);
}

getBars: History, Resolution Rebuilding And Ordering

getBars is where most of the real work sits. It receives a symbol, a resolution and a period range, and must return bars in ascending time order, with a noData flag when the range is genuinely empty rather than merely quiet.

typescriptchart/datafeed/getBars.ts
const NATIVE = new Set(["1", "5", "60", "1D"]);

export async function getBars(
  symbolInfo: LibrarySymbolInfo,
  resolution: ResolutionString,
  periodParams: PeriodParams,
  onResult: HistoryCallback,
  onError: (reason: string) => void,
): Promise<void> {
  const { from, to, countBack, firstDataRequest } = periodParams;

  try {
    let bars: Bar[];

    if (NATIVE.has(resolution)) {
      bars = await api.bars(symbolInfo.ticker!, resolution, from, to);
    } else {
      // Rebuild from the largest native resolution that divides this one, and
      // widen the fetch by the aggregation factor so countBack is satisfiable.
      const { base, factor } = baseFor(resolution);
      const widenedFrom = from - (to - from) * (factor - 1);
      const raw = await api.bars(symbolInfo.ticker!, base, widenedFrom, to);
      bars = aggregate(raw, factor, symbolInfo.session_holidays);
    }

    if (!bars.length) {
      // noData means "there is nothing further back", which stops the library
      // paginating forever. Do NOT return it for a quiet window inside the
      // available history, or the chart will refuse to scroll past it.
      return onResult([], { noData: firstDataRequest ? true : bars.length === 0 });
    }

    // The library requires strictly ascending time and tolerates nothing else.
    // Feeds that merge venues or replay corrections will hand you duplicates
    // and the occasional inversion; normalise here rather than debugging a
    // blank chart later.
    bars.sort((a, b) => a.time - b.time);
    bars = dedupeByTime(bars);

    onResult(bars, { noData: false });
  } catch (e) {
    onError(e instanceof Error ? e.message : "history_unavailable");
  }
}

Two details in that function are worth dwelling on, because both produce bugs that are hard to attribute. The first is the widened fetch: without it, rebuilt resolutions silently under-deliver history and traders report that the 4-hour chart has less data than the 1-hour chart. The second is the noData semantics. The flag means there is nothing further back in time, not that this particular window is empty. Return it for an illiquid instrument's quiet afternoon and the chart will refuse to scroll past that point forever.

Streaming: One Socket, A Subscriber Map, And A Bar Builder

Real-time updates come through subscribeBars and unsubscribeBars. The library hands you a subscriber UID and expects you to call back with an updated bar whenever one changes. Three rules keep this stable at desk scale.

typescriptchart/datafeed/streaming.ts
interface Subscriber {
  uid: string;
  resolution: ResolutionString;
  onTick: (bar: Bar) => void;
  lastBar: Bar;
}

// Keyed by ticker: many chart panes and many resolutions share one upstream
// subscription. One WebSocket for the page, one upstream subscription per
// instrument, many library subscribers per instrument.
const byTicker = new Map<string, Map<string, Subscriber>>();

export function subscribeBars(
  symbolInfo: LibrarySymbolInfo,
  resolution: ResolutionString,
  onTick: (bar: Bar) => void,
  uid: string,
  onReset: () => void,
  lastBar: Bar,
): void {
  const ticker = symbolInfo.ticker!;
  let subs = byTicker.get(ticker);
  if (!subs) {
    subs = new Map();
    byTicker.set(ticker, subs);
    socket.subscribe(ticker);          // first subscriber opens the upstream
  }
  // lastBar is supplied by the library and is the anchor for bar building.
  // Trusting your own cache here instead is how you get a duplicated bar
  // at the seam between history and live.
  subs.set(uid, { uid, resolution, onTick, lastBar });
}

export function unsubscribeBars(uid: string): void {
  for (const [ticker, subs] of byTicker) {
    if (!subs.delete(uid)) continue;
    if (subs.size === 0) {
      byTicker.delete(ticker);
      socket.unsubscribe(ticker);      // last subscriber closes the upstream
    }
    return;
  }
}

socket.onTrade((t: Trade) => {
  const subs = byTicker.get(t.ticker);
  if (!subs) return;

  for (const sub of subs.values()) {
    const bucket = bucketStart(t.timestamp, sub.resolution, t.sessionCalendar);

    if (bucket > sub.lastBar.time) {
      // New bar. Open at this trade, never at the previous close - carrying
      // the close forward hides overnight gaps, which for a 23-hour session
      // is exactly the information a trader is looking for.
      sub.lastBar = { time: bucket, open: t.price, high: t.price,
                      low: t.price, close: t.price, volume: t.size };
    } else if (bucket === sub.lastBar.time) {
      const b = sub.lastBar;
      // Fold the aggregate, do not overwrite. If ticks were coalesced
      // upstream, high and low must survive the ones you did not see.
      sub.lastBar = { ...b, high: Math.max(b.high, t.price),
                      low: Math.min(b.low, t.price),
                      close: t.price, volume: b.volume + t.size };
    } else {
      return;   // late tick for a closed bar - drop, or your history mutates
    }
    sub.onTick(sub.lastBar);
  }
});

The Marks API: AI Signals With Honest Provenance

This is where the integration meets the rest of our work. Setting supports_marks enables getMarks, which paints markers on bars - the natural home for an AI-generated buy/sell signal, a stock rating change or a sentiment shift. The mechanics are simple. The discipline is not, and getting it wrong creates a defect that is invisible in the interface and fatal in a backtest.

typescriptchart/datafeed/getMarks.ts
export async function getMarks(
  symbolInfo: LibrarySymbolInfo,
  from: number,
  to: number,
  onResult: (marks: Mark[]) => void,
  resolution: ResolutionString,
): Promise<void> {
  // asOf is MANDATORY. Rendering today's signal set against a historical
  // window is how a chart silently acquires look-ahead bias: the reviewer
  // sees a marker on the 14:30 bar and assumes it existed at 14:30.
  const signals = await api.signals({
    ticker: symbolInfo.ticker!,
    from,
    to,
    asOf: chartState.asOf ?? "live",
  });

  onResult(
    signals
      // A signal GENERATED after the window still belongs to a bar inside it,
      // but it must not be shown as if it were available then.
      .filter((s) => s.generatedAt <= (chartState.asOf ?? Date.now() / 1000))
      .map((s) => ({
        id: s.id,
        time: bucketStart(s.refersToTimestamp, resolution, symbolInfo.session_holidays),
        color: s.direction === "buy" ? "#12c48b" : "#e84d5c",
        label: s.direction === "buy" ? "B" : "S",
        labelFontColor: "#ffffff",
        minSize: 16,
        // Provenance in the tooltip. If a trader cannot see which model
        // version produced a marker and when, the marker is an unsourced
        // assertion sitting on top of a price series.
        text: [
          `${s.direction.toUpperCase()} · confidence ${s.confidence.toFixed(2)}`,
          `model ${s.modelVersion} · fingerprint ${s.decisionFingerprint}`,
          `generated ${new Date(s.generatedAt * 1000).toISOString()}`,
          `refers to bar ${new Date(s.refersToTimestamp * 1000).toISOString()}`,
        ],
      })),
  );
}

The two timestamps are the entire point. A signal has a bar it refers to and a moment it was generated, and in any real pipeline the second is later than the first - sometimes by seconds, sometimes by a full bar while the model waits for the close. Render by the first and filter by the second, and your chart tells the truth about what was knowable when. Render by the first alone and you have built a machine that makes every strategy look prescient.


Things That Bite, In Rough Order Of Frequency

  1. 01Subscriber leaks. A trader flipping symbols rapidly generates subscribe/unsubscribe churn. If unsubscribeBars does not reliably tear down the upstream, you accumulate socket subscriptions until the feed throttles you. Assert an empty subscriber map in a test that simulates fifty symbol changes.
  2. 02Timezone handling. Bar times are UTC seconds; the library renders in the symbol's exchange timezone from resolveSymbol. Doing the conversion yourself as well shifts every bar by the offset, and it looks plausible enough to ship.
  3. 03Session breaks inferred from the data. If you detect gaps by absence of trades, a thin overnight tape reads as a break. Session structure must come from a calendar served per instrument.
  4. 04resolveSymbol being slow. It is called on every symbol change and blocks the chart. Cache instrument metadata aggressively; it changes on corporate actions, not on page loads.
  5. 05Corporate actions applied without a reset. Adjusting history for a split while a subscriber is live leaves a chart mixing adjusted history with unadjusted live bars. Use the onReset callback the library hands to subscribeBars.
  6. 06has_intraday and minmov left at defaults. Instruments priced in fractions or with unusual tick sizes render with wrong precision, and traders lose confidence in the whole platform over a rounding artefact.

When To Stop Integrating And Start Building

A fair question to close on, since we do both. Stay with Advanced Charts while your requirements are a trader's familiar workspace over your own instruments and signals - which covers the large majority of platforms, and rebuilding that ecosystem is years of work for no differentiation. Reach for a custom engine when the chart itself is the product: full-depth order book replay, nanosecond microstructure, a volatility surface, execution-quality visualisation, or point counts where the question is whether the thing renders at all. In practice the systems we ship most often run both against one data layer - the standard library for the workspace, a custom surface for the two or three views that are genuinely yours.

The Bottom Line

A TradingView integration is judged entirely on the datafeed, and the datafeed is judged on four things the tutorials skim: advertise only the resolutions you actually serve and widen the fetch for the ones you rebuild; return strictly ascending, deduplicated bars and reserve noData for the true end of history; run one shared socket with a subscriber map that tears down cleanly, folding high and low across coalesced ticks and opening new bars at the first trade rather than the previous close; and render AI signals with both timestamps so the chart never implies a marker was knowable before it existed. Get those right and traders stop noticing the chart, which is the entire objective. That is the work we do as a trading platform and charting developer in London - and the difference between a chart that feels native and one that feels borrowed is almost always in the four hundred lines nobody wanted to write.

References & Further Reading

trading charts AITradingView datafeedAI Automation Trading codetrading automationTrading Workflow architectureAI Agency Developer Londonreal-time streaming
Share Email
AI

AlchmAI Engineering

Engineering, London

Written by the AlchmAI engineering team in Mayfair, London. We build trading platforms, real-time charts, market data pipelines and AI features for brokers, prop firms and fintech teams. The Playbook is where we explain how we approach these systems, with code you can run and sources you can check.

Code in this guide is illustrative and supplied without warranty. Review and test it before production use. Nothing here is investment advice. Important information