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#v1.4.13
npx shadcn@latest add tradecn/ui/format#v1.4.13
yarn dlx shadcn@latest add tradecn/ui/format#v1.4.13
bunx --bun shadcn@latest add tradecn/ui/format#v1.4.13
-
Copy the files into your project:
lib/format.ts // Number formatting for trading screens. Pure functions, no React, no dependencies. // Every formatter returns NULL_TOKEN for null, undefined, NaN, and infinities, so a grid // never prints "NaN" or "undefined" while a feed is warming up. export const NULL_TOKEN = "–" const MINUS = "−" export type Nullable = number | null | undefined export interface Locale { /** BCP 47 tag; defaults to en-US. */ locale?: string } export type PriceConvention = /** Fixed decimals, e.g. equities at 2. */ | { kind: "decimal"; decimals: number } /** Decimals derived from the instrument's tick size, e.g. 0.001 gives 3. */ | { kind: "tick"; tick: number } /** * Fractional quoting. `denominator` 32 or 64; `half` renders a half tick as "+" (99-16+) or "5" (99-165); * `eighths` renders eighths of a tick as a trailing digit (99-162 is 99 and 16 2/8 thirty-seconds). */ | { kind: "fraction"; denominator: 32 | 64; half: "+" | "5"; eighths?: boolean } /** What a quote is typed and read in. Bills quote on discount, most bonds on price, some on yield, credit often on a spread in basis points. */ export type QuoteBasis = "price" | "yield" | "discount" | "spread" export interface InstrumentConvention { price: PriceConvention /** Smallest price increment, used for stepping and rounding. */ tick: number yieldDecimals?: number quantityUnit?: "notional" | "contracts" /** Default price. */ quoteBasis?: QuoteBasis /** What a quote steps by in its basis. Default: the tick for price, 0.001 for yield and discount, 0.1 for spread. */ quoteStep?: number /** Decimals for a yield, discount, or spread quote. Default 3, 3, and 1. Price quotes follow their convention. */ quoteDecimals?: number } function isNil(v: Nullable): v is null | undefined { return v === null || v === undefined || !Number.isFinite(v) } // One Intl.NumberFormat per locale and option set; construction is the expensive part. const formatters = new Map<string, Intl.NumberFormat>() export function numberFormat(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat { const key = `${locale ?? "en-US"}|${JSON.stringify(options)}` let nf = formatters.get(key) if (!nf) { nf = new Intl.NumberFormat(locale ?? "en-US", options) formatters.set(key, nf) } return nf } function fixed(v: number, decimals: number, locale?: string, extra: Intl.NumberFormatOptions = {}): string { return numberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, ...extra }).format(v) } function typographicMinus(s: string): string { return s.replace(/-/g, MINUS) } /** Decimal places needed to show a price on this tick grid. 0.001 gives 3, 1/32 gives 5, 1 gives 0. Capped at `max`. */ export function decimalsFromTick(tick: number, max = 8): number { if (!Number.isFinite(tick) || tick <= 0) return 0 for (let d = 0; d <= max; d++) { const scaled = tick * 10 ** d if (Math.abs(scaled - Math.round(scaled)) < 1e-9 * Math.max(1, scaled)) return d } return max } /** Nearest price on the tick grid, cleaned of float noise. */ export function roundToTick(v: number, tick: number): number { if (!Number.isFinite(v) || !Number.isFinite(tick) || tick <= 0) return v return Number((Math.round(v / tick) * tick).toFixed(decimalsFromTick(tick))) } /** Move `steps` ticks (negative allowed) from the nearest grid price. */ export function stepByTick(v: number, tick: number, steps: number): number { return roundToTick(roundToTick(v, tick) + steps * tick, tick) } /** Whole-number and sub-tick decomposition on an integer grid so 32nds never drift. */ function decomposeFraction(v: number, c: Extract<PriceConvention, { kind: "fraction" }>) { const sub = c.eighths ? 8 : 2 const unitsPerWhole = c.denominator * sub const total = Math.round(Math.abs(v) * unitsPerWhole) const whole = Math.floor(total / unitsPerWhole) const rem = total - whole * unitsPerWhole const ticks = Math.floor(rem / sub) const subticks = rem - ticks * sub return { negative: v < 0 && total > 0, whole, ticks, subticks, sub } } /** 99.515625 as "99-16+" (32nds, half "+"), "99-165" (half "5"), or "99-164" with eighths. */ export function formatFraction(v: Nullable, c: Extract<PriceConvention, { kind: "fraction" }>): string { if (isNil(v)) return NULL_TOKEN const { negative, whole, ticks, subticks, sub } = decomposeFraction(v, c) let tail = "" if (subticks !== 0) { if (c.eighths) tail = String(subticks) else tail = c.half } void sub return `${negative ? MINUS : ""}${whole}-${String(ticks).padStart(2, "0")}${tail}` } export function formatPrice(v: Nullable, c: PriceConvention, l?: Locale): string { if (isNil(v)) return NULL_TOKEN switch (c.kind) { case "decimal": return typographicMinus(fixed(v, c.decimals, l?.locale)) case "tick": return typographicMinus(fixed(roundToTick(v, c.tick), decimalsFromTick(c.tick), l?.locale)) case "fraction": return formatFraction(v, c) } } /** * Inverse of formatPrice for ticket entry. Accepts the convention's own notation and a plain decimal. * Returns null when the text is not a price. Fraction input: "99-16+", "99-165", "99-162", "99-16", "99". */ export function parsePrice(s: string, c: PriceConvention): number | null { const text = s.trim().replace(/−/g, "-").replace(/,/g, "") if (!text) return null if (c.kind === "fraction") { const m = /^([+-]?)(\d+)(?:-(\d{1,2})([+0-9])?)?$/.exec(text) if (m) { const [, sign, wholeText, ticksText, tail] = m const ticks = ticksText ? Number(ticksText) : 0 if (ticks >= c.denominator) return null let subFraction = 0 if (tail === "+") subFraction = 0.5 else if (tail !== undefined) { const digit = Number(tail) if (c.eighths) { if (digit > 7) return null subFraction = digit / 8 } else if (c.half === "5" && digit === 5) subFraction = 0.5 else if (digit === 0) subFraction = 0 else return null } const value = Number(wholeText) + (ticks + subFraction) / c.denominator return sign === "-" ? -value : value } } if (!/^[+-]?(\d+\.?\d*|\.\d+)$/.test(text)) return null const n = Number(text) if (!Number.isFinite(n)) return null if (c.kind === "tick") return roundToTick(n, c.tick) if (c.kind === "decimal") return Number(n.toFixed(c.decimals)) return n } /** 4.2531 as "4.253%". */ export function formatYield(v: Nullable, o: { decimals?: number; suffix?: "%" | "" } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN return typographicMinus(fixed(v, o.decimals ?? 3, o.locale)) + (o.suffix ?? "%") } /** 12.5 as "12.5 bp"; signed gives "+12.5 bp". */ export function formatBps(v: Nullable, o: { decimals?: number; signed?: boolean; unit?: "bp" | "bps" | "" } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN const unit = o.unit ?? "bp" const body = o.signed ? formatSigned(v, { decimals: o.decimals ?? 1, locale: o.locale }) : typographicMinus(fixed(v, o.decimals ?? 1, o.locale)) return unit ? `${body} ${unit}` : body } /** Dollar value of a basis point as whole currency; compact gives "$1.23K". */ export function formatDv01(v: Nullable, o: { currency?: string; compact?: boolean } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN const currency = o.currency ?? "USD" if (o.compact) { const symbol = numberFormat(o.locale, { style: "currency", currency, currencyDisplay: "narrowSymbol", maximumFractionDigits: 0 }) .formatToParts(0) .find((p) => p.type === "currency")?.value ?? "" return `${v < 0 ? MINUS : ""}${symbol}${formatNotional(Math.abs(v), { compact: true, locale: o.locale })}` } return typographicMinus(numberFormat(o.locale, { style: "currency", currency, currencyDisplay: "narrowSymbol", maximumFractionDigits: 0 }).format(v)) } /** 1250000 as "1.25M" (compact), "1.25mm" (unit "mm", the desk's word for millions, never scaled further), or "1,250,000.00". Compact suffixes K, M, B, T. */ export function formatNotional(v: Nullable, o: { compact?: boolean; decimals?: number; unit?: "mm" } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN const decimals = o.decimals ?? 2 if (o.unit === "mm") return typographicMinus(numberFormat(o.locale, { minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(v / 1e6)) + "mm" if (!o.compact) return typographicMinus(fixed(v, decimals, o.locale)) const abs = Math.abs(v) const [divisor, suffix] = abs >= 1e12 ? [1e12, "T"] : abs >= 1e9 ? [1e9, "B"] : abs >= 1e6 ? [1e6, "M"] : abs >= 1e3 ? [1e3, "K"] : [1, ""] return typographicMinus(fixed(v / divisor, suffix ? decimals : 0, o.locale)) + suffix } /** "+0.12", "−0.12" (U+2212), and "0.00" for zero: flat carries no sign. */ export function formatSigned(v: Nullable, o: { decimals?: number } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN return typographicMinus(fixed(v, o.decimals ?? 2, o.locale, { signDisplay: "exceptZero" })) } /** 1.234 as "1.23%"; signed gives "+1.23%". */ export function formatPercent(v: Nullable, o: { decimals?: number; signed?: boolean } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN const decimals = o.decimals ?? 2 return (o.signed ? formatSigned(v, { decimals, locale: o.locale }) : typographicMinus(fixed(v, decimals, o.locale))) + "%" } /** Whole quantities with grouping: 1000000 as "1,000,000". */ export function formatQuantity(v: Nullable, l?: Locale): string { if (isNil(v)) return NULL_TOKEN return typographicMinus(fixed(Math.round(v), 0, l?.locale)) } /** * A coupon the way a run prints it: 4.125 as "4 1/8", 4.5 as "4 1/2", 4 as "4", 0 as "0" (coupons step in eighths). * One off the eighths grid prints as a plain decimal, "4.1". `style: "decimal"` prints "4.125%". */ export function formatCoupon(v: Nullable, o: { style?: "fraction" | "decimal"; decimals?: number } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN if (o.style === "decimal") return typographicMinus(fixed(v, o.decimals ?? 3, o.locale)) + "%" const eighths = Math.round(Math.abs(v) * 8) if (Math.abs(eighths / 8 - Math.abs(v)) > 1e-9) return typographicMinus(numberFormat(o.locale, { minimumFractionDigits: 0, maximumFractionDigits: o.decimals ?? 3 }).format(v)) const whole = Math.floor(eighths / 8) const rem = eighths % 8 const fraction = rem === 0 ? "" : rem % 4 === 0 ? "1/2" : rem % 2 === 0 ? `${rem / 2}/4` : `${rem}/8` const body = fraction ? (whole ? `${whole} ${fraction}` : fraction) : String(whole) return `${v < 0 && eighths > 0 ? MINUS : ""}${body}` } /** A date as an instant, a millisecond count, or a string `Date` can read. A date-only string ("2034-05-15") is that day in UTC. */ export type DateLike = Date | number | string function toDate(d: DateLike | null | undefined): Date | null { if (d === null || d === undefined) return null const date = d instanceof Date ? d : new Date(d) return Number.isNaN(date.getTime()) ? null : date } const dateFormatters = new Map<string, Intl.DateTimeFormat>() /** * A maturity as a run prints it: "05/15/34", or "05/15/2034" with `year: "numeric"`. Read in UTC, so a * date-only string prints as that day wherever the screen is. */ export function formatMaturity(d: DateLike | null | undefined, o: { year?: "2-digit" | "numeric" } & Locale = {}): string { const date = toDate(d) if (!date) return NULL_TOKEN const key = `${o.locale ?? "en-US"}|${o.year ?? "2-digit"}` let f = dateFormatters.get(key) if (!f) { f = new Intl.DateTimeFormat(o.locale ?? "en-US", { month: "2-digit", day: "2-digit", year: o.year ?? "2-digit", timeZone: "UTC" }) dateFormatters.set(key, f) } return f.format(date) } /** Whole UTC days from `now` (the moment, by default) to a maturity. Negative once it has passed; null when the date does not read. */ export function daysToMaturity(d: DateLike | null | undefined, now: DateLike = Date.now()): number | null { const maturity = toDate(d) const from = toDate(now) if (!maturity || !from) return null const day = (x: Date) => Date.UTC(x.getUTCFullYear(), x.getUTCMonth(), x.getUTCDate()) return Math.round((day(maturity) - day(from)) / 86_400_000) } /** How many ticks `a` is from `b`, signed, to the nearest eighth of a tick: 99-17 against 99-16+ on a 1/64 tick is 1. NaN when a side is not a number or the tick is not positive. */ export function ticksBetween(a: number, b: number, tick: number): number { if (!Number.isFinite(a) || !Number.isFinite(b) || !(tick > 0)) return NaN return Math.round(((a - b) / tick) * 8) / 8 } /** A count of ticks: "+1", "−0.5", "0", up to three decimals and no trailing zeros. Signed unless told otherwise; `unit` adds a word. */ export function formatTicks(v: Nullable, o: { signed?: boolean; unit?: string } & Locale = {}): string { if (isNil(v)) return NULL_TOKEN const body = typographicMinus(numberFormat(o.locale, { minimumFractionDigits: 0, maximumFractionDigits: 3, signDisplay: o.signed === false ? "auto" : "exceptZero" }).format(v)) return o.unit ? `${body} ${o.unit}` : body } /** * The class a number wears (contract rule 14): lining, tabular figures whatever the font, in the numeric family. * `--tradecn-font-numeric` is the sans by default, so letters stay proportional and only the digits take one * width, which is what a column of prices wants. docs/typography.md has the reasoning and the tokens. */ export const NUMERIC_CLASS = "font-(family-name:--tradecn-font-numeric) lining-nums tabular-nums" /** The same figures in the mono stack, `--tradecn-font-mono`: for a fraction quote, whose dash, ticks, and tail must line up down a column. */ export const MONO_NUMERIC_CLASS = "font-(family-name:--tradecn-font-mono) lining-nums tabular-nums" /** * The class for a number printed under a convention: a fraction price (99-16+) sets in the mono stack, so * 99-16+ over 99-17 keeps its dash and its tail in the same place; every other price, and every quote in * another basis, keeps the numeric family. */ export function numericFontClass(c?: PriceConvention | InstrumentConvention | null): string { const price = c && "price" in c ? (quoteBasisOf(c) === "price" ? c.price : null) : c return price?.kind === "fraction" ? MONO_NUMERIC_CLASS : NUMERIC_CLASS } /** The word for a quote field's label, per basis. */ export const QUOTE_BASIS_LABELS: Record<QuoteBasis, string> = { price: "Price", yield: "Yield", discount: "Discount", spread: "Spread" } const QUOTE_DEFAULTS: Record<Exclude<QuoteBasis, "price">, { step: number; decimals: number }> = { yield: { step: 0.001, decimals: 3 }, discount: { step: 0.001, decimals: 3 }, spread: { step: 0.1, decimals: 1 }, } export function quoteBasisOf(c: InstrumentConvention): QuoteBasis { return c.quoteBasis ?? "price" } /** What a quote steps by in its basis: the tick for a price, else `quoteStep` or the basis default. */ export function quoteStepOf(c: InstrumentConvention): number { const basis = quoteBasisOf(c) return basis === "price" ? c.tick : (c.quoteStep ?? QUOTE_DEFAULTS[basis].step) } function quoteDecimalsOf(c: InstrumentConvention): number { const basis = quoteBasisOf(c) return basis === "price" ? 0 : (c.quoteDecimals ?? QUOTE_DEFAULTS[basis].decimals) } /** A quote in the instrument's basis: a price by its convention, else a fixed decimal with the basis's decimals and no unit, since the field's label says the basis. */ export function formatQuote(v: Nullable, c: InstrumentConvention, l?: Locale): string { if (isNil(v)) return NULL_TOKEN if (quoteBasisOf(c) === "price") return formatPrice(v, c.price, l) return typographicMinus(fixed(roundToTick(v, quoteStepOf(c)), quoteDecimalsOf(c), l?.locale)) } /** Inverse of formatQuote for a quote field. A price takes its notation or a decimal; the others take a decimal and snap to the quote step. Null when the text is not a quote. */ export function parseQuote(s: string, c: InstrumentConvention): number | null { if (quoteBasisOf(c) === "price") return parsePrice(s, c.price) const text = s.trim().replace(/−/g, "-").replace(/,/g, "") if (!/^[+-]?(\d+\.?\d*|\.\d+)$/.test(text)) return null const n = Number(text) return Number.isFinite(n) ? roundToTick(n, quoteStepOf(c)) : null } /** Move a quote by `steps` of its step, from the nearest grid value. */ export function stepQuote(v: number, c: InstrumentConvention, steps: number): number { return stepByTick(v, quoteStepOf(c), steps) } /** Bind a convention once per instrument; a grid column calls `formatters[row.instrumentId].price(v)`, a quote field its `quote`, `parseQuote`, and `stepQuote`. */ export function createInstrumentFormatter(c: InstrumentConvention, l?: Locale) { return { tick: c.tick, basis: quoteBasisOf(c), quoteStep: quoteStepOf(c), price: (v: Nullable) => formatPrice(v, c.price, l), yield: (v: Nullable) => formatYield(v, { decimals: c.yieldDecimals ?? 3, locale: l?.locale }), step: (v: number, steps: number) => stepByTick(v, c.tick, steps), parsePrice: (s: string) => parsePrice(s, c.price), quantity: (v: Nullable) => formatQuantity(v, l), quote: (v: Nullable) => formatQuote(v, c, l), parseQuote: (s: string) => parseQuote(s, c), stepQuote: (v: number, steps: number) => stepQuote(v, c, steps), } } -
Add the tokens to your stylesheet:
:root { --tradecn-font-accessible: 'Atkinson Hyperlegible Next', 'Atkinson Hyperlegible', ui-sans-serif, system-ui, sans-serif; --tradecn-font-accessible-mono: 'Atkinson Hyperlegible Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --tradecn-font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --tradecn-font-numeric: var(--tradecn-font-sans); --tradecn-font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; } .dark { --tradecn-font-accessible: 'Atkinson Hyperlegible Next', 'Atkinson Hyperlegible', ui-sans-serif, system-ui, sans-serif; --tradecn-font-accessible-mono: 'Atkinson Hyperlegible Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --tradecn-font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --tradecn-font-numeric: var(--tradecn-font-sans); --tradecn-font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; } -
Append this to your stylesheet:
:root[data-accessibility="hyperlegible"], [data-accessibility="hyperlegible"] { --tradecn-font-sans: var(--tradecn-font-accessible); --tradecn-font-mono: var(--tradecn-font-accessible-mono); }
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: parsePrice and parseQuote accept a decimal point, remove commas, trim whitespace, and accept either minus sign.
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.
| 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 |
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 is accepted without snapping. 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 |
| Bound member | Equivalent |
|---|---|
tick, basis, quoteStep |
convention.tick, quoteBasisOf(convention), quoteStepOf(convention) |
price(value), parsePrice(text) |
formatPrice and parsePrice with convention.price |
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 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 |
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 |
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), 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 so dashes and tails align too. 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 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. 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 v1.4.13.