Skip to content
Trading Systems

Lightweight Charts Finally Has Official Plugins: Building A Probability Series For Binary Contracts, And Conflating Ten Years Of Ticks Without Dropping The Wick

On 16 September TradingView shipped the first official Lightweight Charts plugin packages - ten plugins graduated from the examples collection, a shared @tradingview/lwc-toolkit, and an npm create lwc-plugin scaffold - on top of v5.1's data conflation and the CustomConflationReducer interface that lets a custom series decide how its own points merge when a user zooms out. Two problems those solve land directly on trading platforms this quarter: Cboe Predicts and the prediction-market boom mean traders now hold step-payoff contracts that no candlestick can honestly render, and extended sessions mean charts that hold years of bars must conflate without turning a wick into an average. This is both, as plugins, with code.

AlchmAI Engineering15 min read

16 Sept

First official plugin packages: ten plugins, the shared @tradingview/lwc-toolkit, and create-lwc-plugin 0.2.0

0.5px

Below this on-screen spacing, v5.1 conflation merges points - and CustomConflationReducer decides how for a custom series

v5.2.1

Current core release: hoveredSeriesOnTop, per-item hit testing, tickMarkDensity, and the crosshair no longer clones the dataset per move

1 step

A binary contract's payoff. Rendering it as a candle is technically correct and practically misleading

Lightweight Charts has been the default choice for anyone who wants a TradingView-quality chart without the Advanced Charts licence, and its main weakness was always the same: the moment you needed a series type it did not ship, you were in the plugin-examples repository copying code with no versioning and no upgrade path. That changed on 16 September. TradingView released the first official plugin packages - ten plugins graduated from the examples collection, installable with npm install @tradingview/lwc-plugin-<name>, a shared @tradingview/lwc-toolkit that provides the common utilities, and a create-lwc-plugin scaffold at version 0.2.0 invoked with npm create lwc-plugin@latest. Every package takes lightweight-charts ^5.0.0 as a peer dependency; the core library itself stays at v5.2.1.

The ten are a sensible starting set - accessibility, brushable area, dual-range histogram, HLC area, image watermark, pretty histogram, rounded candles, stacked area, stacked bars and vertical line - but the interesting part for a trading platform is not the plugins. It is that the toolkit and scaffold make a custom series a first-class, versioned artefact, and that v5.1's conflation machinery - enableConflation, conflationThresholdFactor, precomputeConflationOnInit, precomputeConflationPriority - is exposed to custom series through the CustomConflationReducer interface. Together those solve the two charting problems we are being asked about most this quarter.

Scaffold The Plugin, Then Throw Away The Example

bashscaffold.sh
# Official scaffold (create-lwc-plugin 0.2.0). Choose "custom series" when asked.
npm create lwc-plugin@latest probability-series
cd probability-series
npm install lightweight-charts@^5.2.1 @tradingview/lwc-toolkit

# You get: src/<name>.ts (the ICustomSeriesPaneView), a renderer, a
# data-item type, a vite dev harness, and a package.json wired to publish
# as @yourscope/lwc-plugin-probability-series with the correct peer dep.

A Probability Series For Binary Contracts

The custom-series contract is: a data item type, a pane view that turns items into renderable geometry, and a renderer that draws with the canvas the library hands you. The design goal for a binary is to draw the price as a probability band with an explicit resolution marker, not a candle. Each point carries the mid-probability, a bid-ask band, and the volume of contracts, and the series draws a filled band, a mid line, and shades the region above 50% differently so a glance tells you which side the market favours.

typescriptsrc/probability-series.ts
import type {
  CustomData, CustomSeriesOptions, ICustomSeriesPaneRenderer, ICustomSeriesPaneView,
  PaneRendererCustomData, Time, WhitespaceData, PriceToCoordinateConverter,
} from "lightweight-charts";
import { positionsBox, positionsLine } from "@tradingview/lwc-toolkit";

export interface ProbabilityData extends CustomData<Time> {
  prob: number;        // mid, 0..1 - the "price" of YES
  bid: number;
  ask: number;
  contracts: number;   // volume, drives band opacity
  resolvesAt: number;  // unix seconds; used by the legend, not the renderer
}

export interface ProbabilityOptions extends CustomSeriesOptions {
  yesColor: string; noColor: string; bandOpacity: number; midWidth: number;
}
export const defaultOptions: ProbabilityOptions = {
  yesColor: "#12c48b", noColor: "#e84d5c", bandOpacity: 0.28, midWidth: 2,
} as ProbabilityOptions;

class Renderer implements ICustomSeriesPaneRenderer {
  private data: PaneRendererCustomData<Time, ProbabilityData> | null = null;
  private opts = defaultOptions;
  update(d: PaneRendererCustomData<Time, ProbabilityData>, o: ProbabilityOptions) { this.data = d; this.opts = o; }

  draw(target: any, toY: PriceToCoordinateConverter) {
    if (!this.data?.visibleRange) return;
    target.useBitmapCoordinateSpace(({ context: ctx, horizontalPixelRatio: hr, verticalPixelRatio: vr }: any) => {
      const { from, to } = this.data!.visibleRange!;
      const bars = this.data!.bars;
      const half = this.data!.barSpacing / 2;

      // 1. Bid-ask band, one rect per bar, opacity scaled by contract volume so
      //    an illiquid quote visibly reads as an illiquid quote.
      for (let i = from; i < to; i++) {
        const b = bars[i]; const d = b.originalData;
        if (!d || d.prob == null) continue;
        const x = positionsBox(b.x - half, b.x + half, hr);
        const yTop = toY(d.ask)!, yBot = toY(d.bid)!;
        const y = positionsBox(Math.min(yTop, yBot), Math.max(yTop, yBot), vr);
        const alpha = Math.min(1, 0.15 + Math.log10(1 + d.contracts) / 4) * this.opts.bandOpacity;
        ctx.fillStyle = withAlpha(d.prob >= 0.5 ? this.opts.yesColor : this.opts.noColor, alpha);
        ctx.fillRect(x.position, y.position, x.length, Math.max(1, y.length));
      }

      // 2. Mid line, coloured by which side of 50% it sits on.
      ctx.lineWidth = this.opts.midWidth * vr;
      let prev: { x: number; y: number; yes: boolean } | null = null;
      for (let i = from; i < to; i++) {
        const b = bars[i]; const d = b.originalData;
        if (!d || d.prob == null) { prev = null; continue; }
        const pt = { x: b.x * hr, y: toY(d.prob)! * vr, yes: d.prob >= 0.5 };
        if (prev) {
          ctx.strokeStyle = pt.yes ? this.opts.yesColor : this.opts.noColor;
          ctx.beginPath(); ctx.moveTo(prev.x, prev.y); ctx.lineTo(pt.x, pt.y); ctx.stroke();
        }
        prev = pt;
      }

      // 3. The 50% line. Binaries are read relative to it; draw it once, thin.
      const y50 = positionsLine(toY(0.5)!, vr, 1);
      ctx.fillStyle = "rgba(255,255,255,0.18)";
      ctx.fillRect(0, y50.position, ctx.canvas.width, y50.length);
    });
  }
}

export class ProbabilitySeries implements ICustomSeriesPaneView<Time, ProbabilityData, ProbabilityOptions> {
  private r = new Renderer();
  priceValueBuilder(p: ProbabilityData) { return [p.ask, p.bid, p.prob]; }  // autoscale sees the band
  isWhitespace(d: ProbabilityData | WhitespaceData<Time>): d is WhitespaceData<Time> { return (d as ProbabilityData).prob === undefined; }
  renderer() { return this.r; }
  update(d: PaneRendererCustomData<Time, ProbabilityData>, o: ProbabilityOptions) { this.r.update(d, o); }
  defaultOptions() { return defaultOptions; }
}

A Conflation Reducer That Keeps The Extremes

Now the second problem. With enableConflation on, the library merges data points that would render in under half a pixel, with conflationThresholdFactor adjusting how aggressively and precomputeConflationOnInit pre-computing levels at startup under the Prioritized Task Scheduling API. For a built-in line series the reducer averages. A custom series supplies its own through CustomConflationReducer, and for anything with a range - a candle, a bid-ask band, a probability that swung - the reducer must keep the extremes and carry the volume, not average them away.

typescriptsrc/probability-reducer.ts
import type { CustomConflationReducer } from "lightweight-charts";
import type { ProbabilityData } from "./probability-series";

/**
 * Merge N items into one bucket without lying:
 *  - bid = min bid, ask = max ask   (the band shows the full range traded)
 *  - prob = volume-weighted mid     (a big print moves the line; a tiny one does not)
 *  - contracts = sum                (opacity stays honest at every zoom level)
 *  - time = first item's time       (bucket anchors left, like a bar)
 * Averaging prob and dropping the band is what the default reducer would do,
 * and it turns a 90 -> 10 collapse into a placid 50.
 */
export const probabilityReducer: CustomConflationReducer<ProbabilityData> = (items) => {
  let bid = Infinity, ask = -Infinity, vol = 0, weighted = 0;
  for (const it of items) {
    if (it.prob === undefined) continue;
    bid = Math.min(bid, it.bid); ask = Math.max(ask, it.ask);
    vol += it.contracts; weighted += it.prob * it.contracts;
  }
  const first = items[0];
  return {
    time: first.time,
    prob: vol > 0 ? weighted / vol : first.prob,
    bid, ask, contracts: vol,
    resolvesAt: first.resolvesAt,
  };
};

// Wire it up once:
const series = chart.addCustomSeries(new ProbabilitySeries(), {
  ...defaultOptions,
  conflationReducer: probabilityReducer,
});
chart.applyOptions({
  enableConflation: true,
  conflationThresholdFactor: 2.0,        // smoother at deep zoom-out; tune on real data
  precomputeConflationOnInit: true,      // pre-build levels so the first zoom is instant
  precomputeConflationPriority: "background",
});

The same principle gives you a candlestick reducer that behaves - open from the first, close from the last, high as max, low as min, volume summed - and it is the reducer to reach for before the 23-hour equity day arrives in December, because a chart holding years of extended-session bars will conflate constantly, and a wick that vanishes at zoom-out is a trader's complaint you will hear on day one.


Hit Testing And The Legend: What v5.2 Gives You

  • v5.2's mouse events carry hoveredItem and hoveredTarget, so the legend can show the specific contract's bid, ask, contracts and resolution time under the cursor without the plugin re-implementing hit testing.
  • hoveredSeriesOnTop (default true) lifts the hovered series above others - useful when a probability series is overlaid on the underlying index, which is the natural layout for a Cboe Predicts contract against XSP.
  • v5.2.1 stops cloning the dataset on every crosshair move. On a ten-year probability history that was the difference between a smooth crosshair and a sticky one.
  • The accessibility plugin now ships in the package. For a retail-facing binary-contract view that will face suitability scrutiny, that is not optional.
  • attributionLogo is on by default in LayoutOptions and satisfies the licence's attribution requirement. Leave it on.

Signals On A Probability Chart: The Provenance Rule Still Applies

The pull for AI on a prediction-market chart is obvious - a model's own probability estimate overlaid on the market's - and the rule from our datafeed playbook applies unchanged. A model estimate has a time it refers to and a time it was generated, and it must be rendered by the first and filtered by the second, with model version and generation time in the hover state. A probability series makes the failure mode worse, not better: an overlay of a model's 'true' probability that was actually generated after resolution is the cleanest possible way to make any model look prescient. The reducer above carries resolvesAt for exactly this reason; the legend should refuse to show a model estimate generated after it.

Publish It

  1. 01Keep the plugin in its own package with lightweight-charts as a peer dependency, exactly as the official ten do. Bundling the library is how two chart versions end up on one page.
  2. 02Version the reducer with the series. A reducer change alters what historical charts look like; that is a reviewed change on a trading platform, not a patch release.
  3. 03Test conflation against a real long tape at three zoom levels and assert the extremes survive - the max ask and min bid of any bucket must equal the max and min of its members.
  4. 04Ship the accessibility plugin alongside any retail-facing series and test the announced values are probabilities, not raw floats.
  5. 05Document the 0-1 scale and the 50% reference in the plugin's README; the next developer will otherwise autoscale it and undo the point.

The Bottom Line

The 16 September plugin release makes a custom Lightweight Charts series a versioned, installable, scaffolded artefact rather than a copy from an examples folder, and v5.1's CustomConflationReducer gives that series control over the one thing that matters when a chart holds years of data: what survives when points merge. Those two capabilities are precisely what this quarter's market structure demands. Binary contracts are a probability and a step, and they need a series that draws a band against a fixed 0-1 scale with a 50% reference and volume-honest opacity - not a candle. Long extended-session histories need a reducer that keeps the high, the low and the volume rather than averaging them into a comfortable lie. Build both as plugins, version the reducer with the series, keep signal provenance honest, and the chart will still be telling the truth at every zoom level in December. That is the trading-chart engineering we do in London, and it just got a proper toolchain.

References & Further Reading

trading charts AILightweight Charts pluginsAI Automation Trading codetrading automationcustom series conflationTrading AI architectureAI Agency Developer London
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