Blog

Adding a chart() Method to Tala

  • typescript
  • opensource
  • showdev
  • tradingview

Tala logo

I added a chart() method to tala, my TypeScript library for technical analysis, because its results were not that satisfying to look at as raw objects. Technical analysis means running formulas called indicators, like moving averages, over past prices. You give tala an asset’s price history, chain the indicators you want, and get the data back with those indicators added.

const result = tala()
  .sma(14)
  .rsi(14)
  .macd()
  .run(history)

After using it for a while, staring at rsi14, macdHistogram or sma20 in a JSON result didn’t give me the full picture. A moving average is useful because of where it sits next to the candles. RSI matters because of how it moves over time, and MACD is easier to follow when you can see its lines cross. That’s why tala needed a way to turn its indicators into a chart:

.chart()

Tala chart output with candlesticks, indicator overlays, oscillator panes and sidebar controls.

Where chart() lives

My first thought was a separate package:

tala
tala-viz

As two packages, the API felt awkward. If someone already has this:

tala()
  .sma(14)
  .rsi(14)
  .macd()

then making them move to a second package just to see the result is clumsy. Ending the chain with the chart was simpler:

await tala()
  .sma(14)
  .rsi(14)
  .macd()
  .chart(history)

It reads as “calculate these indicators, then show them to me”. I dropped the separate package and made chart() part of the main tala API.

For the drawing I picked TradingView Lightweight Charts. It’s made for financial charts and it’s open source. It also let me show candles, overlays and oscillators on one page without building a dashboard first.

Panes and layout

Overlay indicators like SMA, EMA, Bollinger Bands and VWAP use the same units as the price, so they sit on the candlestick chart. Oscillators swing within their own range of values instead. RSI, MACD, Stochastic, CCI, Williams %R and ADX live in different ranges, and forced into one pane they are technically complete but unreadable. The design went from one pane to two, and ended with three oscillator panes grouped by range:

0–100:
RSI, ADX, Stochastic

Around zero:
MACD, Williams %R, Fisher Transform

Wide range:
CCI

The interface changed a lot too. The sidebar started as a slide-out panel. Then it became always visible, moved to the left, and finally worked more like a hover overlay so the charts stayed usable. The toggles became checkboxes and then went back to toggles. A controls button existed for a while and then disappeared.

A table also lets you edit the OHLCV data, which is the open, high, low, close and volume of each candle. My first attempt put it inline with the chart, and it took too much space and made the page feel cramped. I moved it into a modal, so you open it only when you need it.

The bugs

Tooltips were slightly off at first. I positioned them with the chart point that Lightweight Charts returns, but that point is relative to the chart’s drawing area, not the page. Using the mouse event’s viewport coordinates fixed it.

Keeping the panes in step was similar. I first synced them with logical ranges, which count bars, and the panes never lined up. Syncing on visible time ranges made it better.

The most painful part was the generated HTML. The chart output is a self-contained HTML page, so TypeScript generates the HTML, and the HTML contains JavaScript. At one point I was writing TypeScript template literals that produced JavaScript strings inside an HTML document. Escaping was tricky, and one missing brace could leave the whole page blank. I ended up writing small scripts just to check that every brace in the generated output had its pair.

Server mode

The first version wrote an HTML file. Then the question came up of what happens when someone edits the data. A static page can redraw the candles, but it can’t re-run the indicators, because tala runs in Node.js. I added server mode to fix that:

await tala()
  .sma(14)
  .rsi(14)
  .chart(history, {
    format: 'server',
    port: 3000,
  })

It starts a local HTTP server with a recalculation endpoint. You edit the OHLCV data in the browser, click Recalculate, and the server re-runs the same indicator chain.

The browser wants chart data oldest first, and tala expects history newest first:

history[0] === most recent candle

To handle that, recalculation reverses the data before sending it to the server, then reverses the response again for display. Without that, everything still draws, but tala calculates the indicators in the wrong order.

What shipped

The feature shipped in @jimzandueta/[email protected], with both static HTML output and local server mode:

await tala()
  .sma(14)
  .ema(26)
  .bb(20, 2)
  .macd()
  .rsi(14)
  .chart(history, { format: 'html' })
const chart = await tala()
  .sma(14)
  .rsi(14)
  .chart(history, {
    format: 'server',
    port: 3000,
  })

chart.close()

The release ended with these test numbers:

243 passing tests
37 test suites
29 new visualization tests

It also kept the parts of tala I did not want to give up:

Zero runtime dependencies for normal usage
TypeScript strict mode
Dual CJS/ESM output
Tree-shakeable build

Tree-shakeable means a bundler can drop the parts you don’t use. The charting code only loads when you call chart(), so people who only want the calculations still get the light library they had before. You still call it the same way, at the end of the chain:

.chart()

Try it

You can install tala from npm:

npm install @jimzandueta/tala

Then generate a chart:

import { tala } from '@jimzandueta/tala'

await tala()
  .sma(14)
  .ema(26)
  .bb(20, 2)
  .macd()
  .rsi(14)
  .chart(history, { format: 'html' })

Links:

If you try chart(), I’d like to hear which indicators or chart interactions you want next.

Also on dev.to

Loading the terminal…