Skip to main content

Financial charts

Financial charting in pond is two things that meet on the plot: a library of studies (technical indicators, built on pond as plain series transforms) and the chart assembly that draws them — candles, a volume pane, a session-aware axis, an OHLC readout. This page leads with the studies, then assembles the chart around them.

src/examples/charts-financial-studies.tsx
import {
BandChart,
Candlestick,
ChartContainer,
ChartRow,
Layers,
LineChart,
YAxis,
} from '@pond-ts/charts';
import '@pond-ts/financial/fluent';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { marketBars, sessionWindow } from './lib/financial-fixtures';

/** Studies **append** columns to a bar `TimeSeries` and return the widened
* series — so they chain, and you draw their output as ordinary chart
* layers. Importing `@pond-ts/financial/fluent` mounts them as methods:
* `.bollinger()` adds `bbUpper`/`bbMiddle`/`bbLower`, `.ema()` adds `ema`,
* and the band and line just read those columns over the same candles.
*
* Prices are **modelled**, not measured — see `lib/financial-fixtures.ts`.
* The window is the last 120 sessions of that year, cropped *before* the
* studies run so the warm-up rows are the window's own. */
export default function ChartsFinancialStudies({ width }: { width: number }) {
const theme = useSiteChartTheme();
const set = marketBars();
const { range, bars } = sessionWindow(set, 120);
const study = bars.bollinger({ period: 20 }).ema({ period: 10 });

return (
<ChartContainer
range={range}
width={width}
theme={theme}
calendar={set.calendar}
cursor="crosshair"
>
<ChartRow height={240}>
<YAxis id="price" side="right" format={set.priceFormat} width={62} />
<Layers>
<BandChart
series={study}
lower="bbLower"
upper="bbUpper"
axis="price"
as="inner"
/>
<LineChart series={study} column="ema" axis="price" as="secondary" />
<Candlestick series={bars} as={set.symbol} showOHLC gap={1} />
</Layers>
</ChartRow>
</ChartContainer>
);
}

Above: daily candles with a Bollinger band and an EMA overlaid — each one a study that appended its columns to the bar series, drawn as an ordinary BandChart / LineChart layer.

The prices are modelled, not measured — every chart on this page draws the same 251-session year as the Gallery's finance cards, generated by the process model in the fixture header (regimes, volatility clustering, opening gaps, a real NYSE session calendar). Market data is not redistributable, so no real feed is used; the calendar the axis collapses is the real one.

The studies library

A study is a pure function (series, options) => series that appends one or more columns to a bar TimeSeries and returns the widened series. They're thin assemblies over a small rolling kernel — they add vocabulary, not new math — and each is checked against an independent reference (a pandas/TA-Lib "oracle") before it ships.

Three conventions make the corpus uniform and composable:

  • column — which field to read (default 'close'). No study hard-codes close, so a study can run over any numeric column — including another study's output (ChartIQ's "Field" idea).
  • output / prefix — what to name the result column(s), so overlays don't collide.
  • Bar-count periods, length-preserving warm-upperiod is a number of bars; the warm-up rows are undefined (the chart renders that as a clean gap, so a moving average simply starts once it has enough history). For a rolling study that is the first period − 1 rows; the difference-built ones cost one more — see the warm-up note under the tables.
import '@pond-ts/financial/fluent';

// each study appends its columns and returns the widened series, so they chain
const bars = candles.bollinger({ period: 20 }).ema({ period: 10 });
// bars now has bbUpper / bbMiddle / bbLower (from bollinger) + ema (from ema)

That is the fluent form — one import '@pond-ts/financial/fluent' mounts every study as a method on TimeSeries, and it is what the examples on this page use. Each study is also a plain function, (series, options) => series, for code that would rather not augment the prototype: ema(bollinger(candles, { period: 20 }), { period: 10 }) is the same series.

What's here today

Twenty studies, every one verified against a pandas oracle before it shipped — and the eight named indicators (rsi, macd, atr, stochastic, williamsR, momentum, percentChange, obv) bar-for-bar against TA-Lib too. Each is a batch transform (TimeSeries → TimeSeries + columns). Grouped by where they are drawn:

Overlays — drawn on the price row

StudyAppendsComputes
smasmaSimple moving average (mean of the last period bars).
emaemaExponential moving average (α = 2/(period+1)).
bollingerbbMiddle / bbUpper / bbLowerSMA ± stdDev×population σ (default 2).
envelopeenvMiddle / envUpper / envLowerMoving-average envelope, middle × (1 ± percent/100).
donchiandcUpper / dcLower / dcMiddleHighest high / lowest low over period bars, and the midpoint.
vwapvwapRolling volume-weighted typical price, Σ tp·v / Σ v; period is required.

Oscillators — drawn on their own row (the example below)

StudyAppendsComputes
rsirsiWilder's Relative Strength Index, 0–100 (default 14).
macdmacdLine / macdSignal / macdHistEMA(fast) − EMA(slow), its EMA signal, and the difference (12/26/9).
stochasticstochK / stochD%K = where the close sits in the kPeriod range, smoothed by slowing; %D its SMA. slowing: 1 is the fast stochastic.
williamsRwilliamsRWilliams %R, −100..0 — stochastic({ slowing: 1 }).K − 100.
momentummomentumvalue − value[period bars ago] (default 10).
percentChangepctChangeRate of change vs periods bars ago, in percent (TA-Lib's ROC).
zScorezscore(value − SMA) / σ, standardized deviation.
obvobvOn-Balance Volume — the running total of volume signed by the close change. No period.

Volatility and range statistics

StudyAppendsComputes
atratrWilder's Average True Range, in price units (default 14).
historicalVolatilityhvAnnualised σ of log returns (annualize bars per year, default 252).
rollingStdevstdevRolling population standard deviation.
rollingMinminRolling minimum (one Donchian edge).
rollingMaxmaxRolling maximum (the other).
rollingPercentilep{q}Rolling q-th percentile (linear interpolation).

Multi-input studies (atr, stochastic, williamsR, donchian, obv, vwap) name each bar column they read — high, low, close, volume — with the conventional defaults, so they run over any column layout the same way column lets a single-input study run over any field.

Two conventions worth knowing before you draw them: warm-up length is the first period − 1 rows for a rolling study, but one more (period) for the difference-built ones (rsi, atr, momentum, historicalVolatility, and percentChange over its periods), kPeriod + slowing − 2 for %K, and each macd column from its own first valid bar; obv has no warm-up at all. And a flat window is undefined, not 0 — rsi, stochastic and williamsR all report no position when there is no range to have a position in (TA-Lib returns 0 there; the package's docstrings record every such delta).

Oscillators in their own rows

An oscillator is just another column on the same series, drawn on its own <ChartRow> with its own <YAxis>. RSI gets a fixed min={0} max={100} axis and two <Baseline>s at 30 and 70; MACD gets its line and signal on an auto-scaled axis around a zero baseline. The window (160 sessions, October to June) spans the year's −23.5% correction, which is where an oscillator earns its row: RSI drops below 30 in two clusters during the sell-off (late December, late March) and above 70 on the run-up either side, and the MACD line crosses its signal four times:

The crosshair reads all three rows at once — the OHLC readout on the candles, and each oscillator's value on its own axis.

What's coming

The corpus assessment maps ~124 ChartIQ studies onto ~11 rolling kernels, so the library extends cheaply, and Phase 1 breadth (the oscillators and volume studies above) has landed. Still open in that phase: ATR bands (Keltner-style close ± k·ATR) and the session-anchored VWAP — the rolling vwap above is a window, not the intraday desk's running line, which needs a session reset. A later phase covers the stateful ones (PSAR, SuperTrend, ATR trailing stop, ZigZag) and the other session-anchored studies (pivot points). Adding one is a short, oracle-verified assembly — see the studies/README.md pattern in the package.

Assembling the chart

The chart around the studies is the standard @pond-ts/charts composition — there's no separate "financial mode", just the pieces:

src/examples/gallery-financial.tsx
import {
Candlestick,
ChartContainer,
ChartRow,
Layers,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { marketBars, sessionWindow } from './lib/financial-fixtures';

/** Financial terminal: daily OHLC candles on a session calendar, with the
* crosshair cursor and the axis-pill OHLC readout — first-class support, not
* a bar-chart hack. Prices are **modelled**, not measured — see
* `lib/financial-fixtures.ts`. */
export default function GalleryFinancial({ width }: { width: number }) {
const theme = useSiteChartTheme();
const set = marketBars();
const { range, bars } = sessionWindow(set, 60);

return (
<ChartContainer
range={range}
width={width}
theme={theme}
calendar={set.calendar}
cursor="crosshair"
>
<ChartRow height={220}>
<YAxis id="price" side="right" format={set.priceFormat} width={62} />
<Layers>
<Candlestick series={bars} as={set.symbol} showOHLC gap={1} />
</Layers>
</ChartRow>
</ChartContainer>
);
}
  • Candles<Candlestick> reads the four OHLC columns; showOHLC fans the full open/high/low/close to the readout.

  • Crosshaircursor="crosshair" on <ChartContainer> gives the trading-terminal reticle with on-axis pills.

  • A volume pane — a second <ChartRow> with a <BarChart> on the volume column and its own axis:

    <ChartRow height={110}>
    <YAxis id="vol" side="right" format=",.0s" />
    <Layers>
    <BarChart series={bars} column="volume" axis="vol" />
    </Layers>
    </ChartRow>
  • A session-aware axis — hand <ChartContainer> a calendar and closed-market gaps collapse; see the Trading-time axis.

  • A live price pill — a YAxisIndicator driven by createLiveValue pins the last price to the axis edge without re-rendering the chart; see Axis indicators & live values.

Coming from TradingView

If you're arriving from TradingView / ChartIQ, the vocabulary maps like this:

TradingView / ChartIQpond-ts
Indicator / Studythe studies library (sma, bollinger, …)
Field (indicator input)the column option (or the named high / low / close / volume inputs)
Candles / OHLC bars<Candlestick>
Bar / Candle / Hollow style<Candlestick variant>
Volume sub-panea second <ChartRow> with <BarChart column="volume">
Pane / sub-charta <ChartRow> (Layout)
Crosshaircursor="crosshair"
OHLC legend / data window<Candlestick showOHLC>
Last-price line / labelYAxisIndicator + createLiveValue
Session separatorsessionDividers on <ChartContainer>
Regular trading hours (no gaps)the calendar prop + a TradingCalendar

See also