npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@watchgold/financial-charts

v0.1.0

Published

Financial charting primitives for React: futures contract-roll gap handling, moving averages, CME contract and session utilities, a watermark-ready chart frame for recharts, and a stale-data banner.

Readme

@watchgold/financial-charts

Financial charting primitives for React: futures contract-roll gap handling, moving averages, CME contract and session utilities, a watermark-ready chart frame for recharts, and a stale-data banner.

Why

A "continuous" futures price series is stitched from whichever contract is the front month at the time. Every time the front month rolls (e.g. GCQ6 → GCZ6), the stitched series splices the calendar spread between the two contracts into the line — in gold, a step on the order of $50 that is a contract change, not a market move. Drawn naively it becomes a fake cliff on the chart, and a naive last-minus-first change quotes the spread as if the market moved by it.

This package breaks the line at each roll and labels it instead:

  • insertRollBreaks inserts a synthetic gap point per roll (null the price → the line breaks with connectNulls={false}), optionally with a dashed "bridge" value so the eye still gets a continuous line while the solid stroke only draws real moves.
  • rollAdjustedChange reports the sum of intra-contract moves — exactly the movement the broken line draws — instead of quoting the spread.

Install

npm install @watchgold/financial-charts

The root entry (@watchgold/financial-charts) is pure TypeScript with zero dependencies. The ./react entry needs react and recharts, which are declared as optional peer dependencies — install them if you use the components:

npm install react recharts

Quick start: a roll-aware price line

import { insertRollBreaks, rollAdjustedChange } from '@watchgold/financial-charts';
import { ChartFrame, ChartWatermark } from '@watchgold/financial-charts/react';
import { Line, LineChart, ReferenceLine, Tooltip, XAxis, YAxis } from 'recharts';

type Bar = { time: string; price: number | null; contract: string | null; bridge?: number | null };

// bars: one point per candle, tagged with the contract it was built from.
const { displayData, rollBoundaries } = insertRollBreaks<Bar>(bars, {
  contractOf: (p) => p.contract,
  // Null the price so the line breaks at the roll (pair with connectNulls={false}).
  gapPoint: (prev, gapTime) => ({ time: gapTime, price: null, contract: prev.contract }),
  // Optional dashed connector across the gap.
  bridge: { priceOf: (p) => p.price, set: (p, bridge) => ({ ...p, bridge }) },
});

// Header change that matches the chart: spread excluded, only real moves.
const change = rollAdjustedChange(bars, (p) => p.price, (p) => p.contract);

export function PriceChart() {
  return (
    <ChartFrame height={320} watermark={<ChartWatermark src="/your-logo.svg" width={144} height={24} />}>
      <LineChart data={displayData}>
        <XAxis dataKey="time" />
        <YAxis domain={['auto', 'auto']} />
        <Tooltip />
        {rollBoundaries.map((b) => (
          <ReferenceLine key={b.time} x={b.time} strokeDasharray="4 4" label={`${b.from} → ${b.to}`} />
        ))}
        <Line dataKey="bridge" stroke="#d4a017" strokeDasharray="4 4" dot={false} isAnimationActive={false} />
        <Line dataKey="price" stroke="#d4a017" dot={false} connectNulls={false} />
      </LineChart>
    </ChartFrame>
  );
}

Utilities (root entry, zero dependencies)

Contract rolls — insertRollBreaks, rollAdjustedChange

See the quick start. insertRollBreaks can be applied more than once to the same series (once per contract-bearing line) as long as each pass's gap points carry the other passes' contract fields through. Unparseable timestamps still record the roll boundary; only the synthetic gap point is skipped.

Contracts — decodeCmeContractMonth, formatContractTicker, CME_MONTH_CODES

decodeCmeContractMonth('GCZ6', 'GC');  // 'Dec 2026' (current/future months only, else null)
decodeCmeContractMonth('SIU25', 'SI'); // 'Sep 2025'
formatContractTicker('GCM26');         // 'GCM6' (short ticker form)

decodeCmeContractMonth accepts { now } to anchor the single-digit-year decade heuristic and the past-month rejection (inject it in tests).

Sessions — getEasternSessionOpenMs, getEasternMidnightMs, cmeHourlySnapshotCaptureMs, wallClockMs, timeZoneOffsetMs

  • getEasternSessionOpenMs(referenceMs?) — the most recent CME Globex session open (18:00 America/New_York); sessions run 18:00 ET → 17:00 ET the next day, so this is "today's trading" for futures.
  • getEasternMidnightMs(referenceMs?) — 00:00 ET of the Eastern calendar date, for calendar-day intraday views.
  • cmeHourlySnapshotCaptureMs(nowMs?) — CME snapshot-license timing: a redistributed price may only be a single snapshot captured at :05 each hour and published no earlier than :15; this returns the capture instant of the most recent snapshot already publishable.
  • wallClockMs(referenceMs, hour, timeZone) / timeZoneOffsetMs(utcMs, timeZone) — DST-correct wall-clock math via the Intl API, no time-zone database dependency.

Delayed change — computeDelayedWindowChange

For delayed-redistribution feeds (e.g. CME Smart Stream): drops candles newer than delayMs (default 10 minutes), then computes last-minus-first close across windowMs (default 24 hours, since a futures session spans two calendar dates). Returns { price, change, pct }, with change/pct null when fewer than two candles are usable. Compute every ticker and card from this one function so the number shown next to a chart always matches the chart.

Candles — candleEndTime, DEFAULT_INTRADAY_INTERVAL_MS

Intraday candles are conventionally labeled with their bucket-OPEN time, but a price line renders the candle's close — the value at the END of the bucket. candleEndTime(timestamp, interval) shifts the label to the bucket close (a 1:55–2:00 bar's close shows as ~2:00, not "1:55"). Daily/unknown intervals pass through unchanged; supply your own interval map as the third argument if needed.

Indicators — simpleMovingAverage, buildDailyMaSeries, maConfigsForPeriod, defaultMaConfigs, defaultMaWindowsByPeriod

simpleMovingAverage(closes, 20); // null before the window fills, then trailing averages

// MAs computed over the FULL history, then sliced to the visible window,
// so the average is already correct at the first visible point.
const series = buildDailyMaSeries(fullDailyCandles, '1M');
// → [{ time, price, open, high, low, ma5, ma20 }, …]

maConfigsForPeriod(period) resolves which overlays a chart period gets (1M → MA5+MA20, 1Y → MA50+MA200, …); pass custom configs / windowsByPeriod / displayDaysByPeriod to re-map any of it.

Components (./react entry)

import { ChartFrame, ChartWatermark, StaleDataBanner } from '@watchgold/financial-charts/react';

ChartFrame + ChartWatermark

ChartFrame is a drop-in replacement for recharts' ResponsiveContainer with a watermark slot. The watermark is any React node — typically <ChartWatermark src="/your-logo.svg" />, which renders an absolutely-positioned, pointer-events: none, aria-hidden image (defaults: upper right, opacity 0.3) so recharts tooltips and hover keep working underneath. Pass children to ChartWatermark to render arbitrary content (a text credit, a custom SVG) instead of an image.

StaleDataBanner

A translucent amber banner flagging that on-screen data may be out of date, with an optional refresh button (disabled with a spinning icon while isRefreshing). Strings come in as props (message, refreshLabel) so any i18n layer can supply them; a compact variant fits card headers.

<StaleDataBanner
  message={`Data may be stale — last updated ${lastUpdated}`}
  onRefresh={refetch}
  isRefreshing={isFetching}
/>

Both components are styled with inline styles only — no Tailwind, no CSS import, no styling framework required — and the icons are small hand-authored inline SVGs, so the ./react entry brings no dependencies beyond react and recharts.


Extracted from the production codebase of WatchGold, a precious-metals market-data platform.

MIT