format

Format trading values and parse or step quotes in each instrument's convention.

import { createInstrumentFormatter, formatBps, formatCoupon, formatDv01, formatMaturity, formatNotional, formatPercent, formatPrice, formatQuantity, formatSigned, formatTicks, formatYield, ticksBetween } from "@/lib/format"

const ust = createInstrumentFormatter({ price: { kind: "fraction", denominator: 32, half: "+" }, tick: 1 / 64 })
const bund = createInstrumentFormatter({ price: { kind: "tick", tick: 0.005 }, tick: 0.005 })
const bill = createInstrumentFormatter({ price: { kind: "decimal", decimals: 3 }, tick: 0.0005, quoteBasis: "discount" })

const groups: { title: string; rows: [string, string][] }[] = [
  {
    title: "Quotes",
    rows: [
      ["UST price 99.515625", ust.price(99.515625)],
      ["One tick above 99-16", ust.price(ust.step(99.5, 1))],
      ['Parse "99-17"', String(ust.parsePrice("99-17"))],
      ["Bund price 130.0049", bund.price(130.0049)],
      ["Bill discount 4.2531", bill.quote(4.2531)],
      ["Missing price", formatPrice(null, { kind: "decimal", decimals: 2 })],
    ],
  },
  {
    title: "Instrument details",
    rows: [
      ["Coupon 4.125", formatCoupon(4.125)],
      ["Maturity 2034-05-15", formatMaturity("2034-05-15")],
      ["99-17 vs 99-16+", formatTicks(ticksBetween(99.53125, 99.515625, 1 / 64), { unit: "tick" })],
      ["5,000,000 in millions", formatNotional(5e6, { unit: "mm" })],
    ],
  },
  {
    title: "Scalar values",
    rows: [
      ["Yield 4.2531", formatYield(4.2531)],
      ["−12.5 bps, signed", formatBps(-12.5, { signed: true })],
      ["DV01 1234.4", formatDv01(1234.4)],
      ["1,250,000, compact", formatNotional(1.25e6, { compact: true })],
      ["1.234%, signed", formatPercent(1.234, { signed: true })],
      ["Quantity 5,000,000", formatQuantity(5e6)],
      ["Signed zero", formatSigned(0)],
    ],
  },
]

export default function FormatDemo() {
  return (
    <div className="w-fit max-w-full space-y-4 text-xs lining-nums tabular-nums">
      {groups.map(({ title, rows }) => (
        <table key={title} className="w-full">
          <caption className="pb-1 text-left font-medium">{title}</caption>
          <tbody>
            {rows.map(([label, value]) => (
              <tr key={label} className="border-b border-border">
                <th scope="row" className="py-1 pr-4 text-left font-normal text-muted-foreground">{label}</th>
                <td className="py-1 text-right whitespace-nowrap font-(family-name:--tradecn-font-mono)">{value}</td>
              </tr>
            ))}
          </tbody>
        </table>
      ))}
    </div>
  )
}

Installation

pnpm dlx shadcn@latest add tradecn/ui/format#v2.0.0
npx shadcn@latest add tradecn/ui/format#v2.0.0
yarn dlx shadcn@latest add tradecn/ui/format#v2.0.0
bunx --bun shadcn@latest add tradecn/ui/format#v2.0.0

Usage

Bind the instrument's convention once, then format, parse, and step prices through the same formatter.

import { createInstrumentFormatter } from "@/lib/format"

const ust = createInstrumentFormatter({
  price: { kind: "fraction", denominator: 32, half: "+" },
  tick: 1 / 64,
})

ust.price(99.515625) // "99-16+"
ust.parsePrice("99-17") // 99.53125
ust.price(ust.step(99.5, 1)) // "99-16+"

The preview groups quote conventions, instrument details, and scalar values. It uses the default en-US locale; yield, discount, and percent inputs are percentage points, and mm means millions.

API Reference

Pure functions with no React dependency. Numeric formatters accept Nullable (number | null | undefined) and return NULL_TOKEN (–, an en dash) for null, undefined, NaN, and infinities. Negative numeric output uses the typographic minus (−, U+2212).

Locale is { locale?: string }, with "en-US" as the default. Decimal and date output can use another locale, such as { locale: "de-DE" }; fraction notation stays fixed. Parsing is not locale-aware, a bound locale included: parsePrice and parseQuote read a decimal point, trim whitespace, accept either minus sign, and read commas only as thousands separators in the whole part (1,234.5). A comma anywhere else, as in 99,5 typed on a decimal-comma keyboard, 1,0,0, or among the 32nds, reads as no price rather than a bigger one. Where the convention prints decimals, a number with one comma and nothing after its whole part reads either way, 4,253 as 4253 or as 4.253 with a decimal comma, so it reads as no price too; write 4,253.000 or 4253 for the thousands. A convention that prints no decimals reads 5,012 as its own text prints it. stripGrouping applies the same rule to other numeric text.

Prices

PriceConvention selects the notation and precision. Fields below are required unless marked optional.

kind Fields Formatting
"decimal" decimals: number Fixed decimal places; 1234.5 with decimals: 2 is 1,234.50
"tick" tick: number Snap to the tick grid and derive decimal places from the tick; 100.0049 with tick: 0.005 is 100.005
"fraction" denominator: 32 | 64, half: "+" | "5", optional eighths: boolean (default false) Round to halves of a 32nd or 64th, or eighths when enabled

In 32nds, 99.515625 prints as 99-16+, or 99-165 with half: "5". With eighths: true, the trailing digit counts eighths: 99.5078125 is 99-162 and 99.515625 is 99-164. A denominator of 64 uses the same notation in 64ths. From 1e21, where a whole part would print in exponent form, fraction notation prints NULL_TOKEN, and parsePrice returns null for a value in that range.

Function Result
formatPrice(value, convention, locale?) Format a Nullable price; the third argument is a Locale object
formatFraction(value, convention) Format a Nullable price with the fraction variant of PriceConvention
parsePrice(text, convention) Read a string in the convention's notation or as a plain decimal; return a number, or null for unreadable text or a value too large to print back in the convention's notation
stripGrouping(text, { decimals }) Trim numeric text, read either minus sign as -, and remove its thousands separators; return null when a comma isn't one, and with decimals also for one comma with nothing after the whole part, such as 4,253; exponent text right after its last group, such as 1,000e3, is refused too; the rest of the text is left to the caller's own pattern
decimalsFromTick(tick, max = 8) Decimal places needed for a numeric tick, capped at max; 0.005 gives 3, 1 / 32 gives 5; nonpositive or nonfinite ticks give 0
roundToTick(value, tick) Snap a number to the nearest tick and clean float noise; return value unchanged if it or the tick is nonfinite, or the tick is nonpositive
stepByTick(value, tick, steps) Snap to the grid, move by steps ticks, then snap again; all inputs are numbers and negative steps are allowed

parsePrice rounds decimal input to decimals for a decimal convention and snaps it to tick for a tick convention. For a fraction convention, plain decimal input snaps to the printable grid — half or eighth ticks — so the parsed value is the price its formatted text shows, with a negative tie snapping away from zero as it prints. With the default locale, formatted prices read back on the convention's representable grid.

Instrument formatter

createInstrumentFormatter(convention, locale?) binds an InstrumentConvention and optional Locale once. A grid column can then call formatters[row.instrumentId].price(value); a new instrument convention is another data object.

InstrumentConvention field Type Default Purpose
price PriceConvention Required Price notation
tick number Required Price increment for stepping
yieldDecimals number 3 Decimal places for the bound yield formatter
quantityUnit "notional" | "contracts" Unset Metadata; the bound quantity formatter always prints whole quantities
quoteBasis QuoteBasis "price" Basis used by quote methods
quoteStep number Basis default below Step for yield, discount, or spread quotes; ignored for price quotes
quoteDecimals number Basis default below Decimal places for yield, discount, or spread quotes; ignored for price quotes
quoteInverted boolean Yield and discount invert; price and spread do not Whether a higher quote means a lower price, so a normal market quotes the bid above the offer. A CDS spread quotes bid below offer; cash credit quotes the other way, so declare it where the default reads wrong. quoteInvertedOf(convention) reads the effective direction
Bound member Equivalent
tick, basis, quoteStep convention.tick, quoteBasisOf(convention), quoteStepOf(convention)
price(value), parsePrice(text) formatPrice and parsePrice with convention.price; the bound locale applies to output only
yield(value) formatYield with yieldDecimals
quantity(value) formatQuantity
step(value, steps) stepByTick with convention.tick
quote(value), parseQuote(text), stepQuote(value, steps) Quote functions below with the bound convention; the bound locale applies to output only: quote in de-DE prints text parseQuote reads as no quote, such as 99,50, or as another one, such as an integer tick's 5.012, which reads as 5.012 and snaps to 5

The quote basis

QuoteBasis says what the trader types and reads. Bills quote on discount, some bonds on yield, and credit often on spread. ticket's quote field uses these quote functions.

Basis Default step Default decimals QUOTE_BASIS_LABELS
"price" convention.tick From convention.price "Price"
"yield" 0.001 3 "Yield"
"discount" 0.001 3 "Discount"
"spread" 0.1 1 "Spread"
Function Result
quoteBasisOf(convention) Effective basis, defaulting to "price"
quoteStepOf(convention) Effective step from the table or an applicable quoteStep override
formatQuote(value, convention, locale?) Format a Nullable quote; optional third argument is Locale
parseQuote(text, convention) Parse a string as a price or a decimal in the selected basis; return a number, or null for unreadable text or a value too large to print back in its basis
stepQuote(value, convention, steps) Move a numeric quote by steps from the nearest grid value; negative steps are allowed

Price quotes format and parse through convention.price. Other bases snap to quoteStep and print a fixed decimal with no unit; the field's label supplies the basis. Yield and discount values use percentage points; spread values use basis points. Set quoteDecimals high enough to display the chosen step without losing precision.

Coupons, maturities, and millions

Function Inputs and options Example output
formatCoupon(value, options?) Nullable; style: "fraction" | "decimal" (default "fraction"), decimals: number (default 3), locale 4.125 → 4 1/8; decimal style → 4.125%
formatMaturity(date, options?) DateLike | null | undefined; year: "2-digit" | "numeric" (default "2-digit"), locale "2034-05-15" → 05/15/34; numeric year → 05/15/2034
isInstant(value) Any value; true for a time a Date can hold, a finite number of milliseconds within 8.64e15 of the epoch either way. Guard a server's timestamp with it before formatting Date.now() → true; NaN, Infinity, "1700000000000" → false
daysToMaturity(date, now?) DateLike | null | undefined; now: DateLike defaults to the current time Whole UTC calendar days to maturity; 0 on the same day, negative on later days, null if either date is invalid
formatNotional(value, { unit: "mm" }) Nullable; optional decimals: number (default 2) and locale 5e6 → 5mm; 1.25e9 → 1,250mm

Fraction coupons reduce eighths: 4.5 is 4 1/2 and 4 is 4. Values off the eighths grid use up to decimals places, so 4.1 stays 4.1; decimal style always uses fixed places and a % suffix. The mm notional unit means millions, trims trailing zeros, and takes precedence over compact without scaling to billions.

DateLike is Date | number | string: a date, milliseconds since the Unix epoch, or a string Date can read. Maturities format in UTC, so a date-only string stays on the same day in every timezone. formatMaturity returns NULL_TOKEN for missing or invalid dates.

Ticks between two prices

Function Inputs and options Result
ticksBetween(a, b, tick) Three numbers (a - b) / tick, rounded to the nearest eighth; NaN if either price is nonfinite or tick > 0 is false
formatTicks(value, options?) Nullable; signed: boolean (default true), unit: string (default none), grouping: boolean (default true; false prints no thousands separators, for an editor's text), locale Up to three decimals, no trailing zeros: +1, −0.5, 0; a unit adds a space and suffix, such as +2 ticks

A quote of 99-17 against a composite of 99-16+ on a 1 / 64 tick is one tick: ticksBetween(99.53125, 99.515625, 1 / 64) returns 1. Use formatBps for spread differences in basis points.

Numeric classes

Formatters add no alignment spaces. tradecn components set lining, tabular figures on numeric nodes; use these classes on your own nodes to meet rule 14 of the contract.

Export Value or behavior
NUMERIC_CLASS font-(family-name:--tradecn-font-numeric) lining-nums tabular-nums
MONO_NUMERIC_CLASS font-(family-name:--tradecn-font-mono) lining-nums tabular-nums
numericFontClass(convention?) Accepts PriceConvention | InstrumentConvention | null; selects mono for a fraction price and the numeric family otherwise, including non-price quote bases and no convention

The numeric family defaults to sans: letters stay proportional and digits have equal widths. Fraction prices use mono for equal digit and dash widths; quotes with and without a tail still differ in length, so a right-aligned column lines tails up only against padding you add. quote-field, ticket, and rfq-ticket use numericFontClass; a data-grid column uses numeric: true and font: "mono" for the same effect. typography.md explains the tokens and choices.

The install adds the --tradecn-font-sans, --tradecn-font-mono, and --tradecn-font-numeric tokens, their accessible pair, and the hyperlegible remap alongside these helpers.

The rest

These functions take a Nullable value and an optional options object. Every options object accepts locale; examples use "en-US".

Function Options and defaults Example output
formatYield decimals: number = 3, suffix: "%" | "" = "%" 4.2531 → 4.253%
formatBps decimals: number = 1, signed: boolean = false, unit: "bp" | "bps" | "" = "bp" 12.5 → 12.5 bp
formatDv01 currency: string = "USD", compact: boolean = false 1234 → $1,234; compact → $1.23K
formatNotional decimals: number = 2, compact: boolean = false, optional unit: "mm" 1250000 → 1,250,000.00; compact → 1.25M
formatSigned decimals: number = 2 0.12 → +0.12; 0 → 0.00
formatPercent decimals: number = 2, signed: boolean = false 1.234 → 1.23%; signed → +1.23%
formatQuantity No options beyond locale 1000000.6 → 1,000,001

Yield and percent inputs are already percentage points: 1 means 1%. DV01 rounds to whole currency units in its full form; the compact form keeps the compact decimals, as in $1.23K. Compact notional output uses K, M, B, or T; values below 1,000 round to a whole number. Signed formatters omit the sign when the displayed value rounds to zero.

numberFormat(locale, options) returns a cached Intl.NumberFormat for a locale string (or undefined for "en-US") and Intl.NumberFormatOptions. Numeric formatters share this cache by locale and option set.

This page is docs/format.md at v2.0.0.