Skip to content

DecimalAmount

DecimalAmount is a struct representing an arbitrary-precision decimal number. It's a (long Whole, int Fraction, byte N) triple where N is the precision (number of fractional digits). Unlike System.Decimal which has a fixed 28-digit scale, DecimalAmount carries its precision as part of the value.

Why not decimal?

System.Decimal is the obvious choice for financial math, but it has limitations:

Concern decimal DecimalAmount
Precision fixed 28 digits arbitrary (N field)
Cross-language .NET only serializes via FlatBuffers (C# / Python / TypeScript)
Equality at different precisions 1.0m != 1.00m (false) 1.0 != 1.00 (true) — precision is part of the value
Sub-millisecond conversions implicit scale-of-10 math explicit N carries the scale
Memory 16 bytes 16 bytes (3 fields, but smaller N is a byte)

DecimalAmount is the type the trading platform uses for prices, quantities, fees, P&L — anywhere precision matters. The decimal type is only used in legacy API surfaces.

The struct

public readonly struct DecimalAmount
{
    public long Whole { get; }      // integer part
    public int Fraction { get; }    // fractional part (raw, scale = N)
    public byte N { get; }          // precision: number of fractional digits

    public DecimalAmount(long whole, int fraction, byte n);
}

The actual numeric value is Whole + Fraction / 10^N. The Fraction is stored as a raw integer, not a normalized fraction.

var price1 = new DecimalAmount(100, 50, 2);      // 100.50  (precision 2)
var price2 = new DecimalAmount(100, 5000, 4);    // 100.5000 (precision 4)
Console.WriteLine(price1 == price2);              // False — different precisions

Arithmetic

All arithmetic uses checked semantics and throws OverflowException on overflow. The result's precision is the max of the operands' precisions.

var a = new DecimalAmount(10, 50, 2);     // 10.50
var b = new DecimalAmount(3, 25, 2);      // 3.25

var sum = a + b;        // 13.75  (precision 2)
var diff = a - b;       // 7.25   (precision 2)
var prod = a * b;       // 34.1250 (precision 4 — max of 2+2)
var quot = a / b;       // 3.230769... (precision = some default, max(2,2))

// Overflow
var huge = new DecimalAmount(long.MaxValue, 0, 0);
var overflow = huge + new DecimalAmount(1, 0, 0);  // throws OverflowException

Subtraction may yield a negative Whole — that's not an error. The caller decides whether negative makes sense (it does for cash flows, doesn't for inventory).

Cash: currency-aware wrapper

Cash wraps a DecimalAmount plus a Currency. Operations between Cash values of different currencies throw:

var usd = new Cash(new DecimalAmount(100, 0, 2), Currency.USD);
var eur = new Cash(new DecimalAmount(100, 0, 2), Currency.EUR);

var sum = usd + eur;  // throws CurrencyMismatchException

This is by design — the platform never silently does FX conversion. If you need to combine currencies, do the FX step explicitly (typically via a strategy that subscribes to FX feeds and emits cross-currency events).

Implicit conversions

DecimalAmount has implicit conversions from byte, short, int, long, and decimal (the .NET type). Conversions to double and float are explicit (lossy).

DecimalAmount a = 42;            // implicit from int
DecimalAmount b = 100L;          // implicit from long
DecimalAmount c = 3.14m;         // implicit from decimal (preserves 28-digit precision)

double d = (double)a;            // explicit, lossy

Equality

Two DecimalAmount values are equal if and only if all three fields match: Whole, Fraction, AND N. This is intentional — 1.0 (precision 1) and 1.00 (precision 2) are different values. The platform distinguishes them because precision carries semantic information: "I quoted you 1.0" is different from "I quoted you 1.00".

var one1 = new DecimalAmount(1, 0, 1);
var one2 = new DecimalAmount(1, 0, 2);
Console.WriteLine(one1 == one2);  // False

// For comparison ignoring precision, normalize first
public static bool ValueEquals(DecimalAmount a, DecimalAmount b) =>
    a.N == b.N && a.Whole == b.Whole && a.Fraction == b.Fraction;
// (use with care — usually you want to track the precision)

Quantity traits

DecimalAmount is the value type behind several IQuantity traits in Core:

Trait DecimalAmount impl Use
IQuantity DecimalAmountQuantity Read-only quantities (price, notional)
ISignedQuantity SignedDecimalAmountQuantity Quantities that can be negative (cash flow, P&L)
IQuantityFactory<V, I> DecimalAmountQuantityFactory Construct from decimal / long for a given identifier

This lets functions be polymorphic over quantity type: IExecutor<TState, TAction, TEvent> can be parameterized by any quantity implementation.

Performance

DecimalAmount is a readonly struct of 16 bytes (8 + 4 + 1 + 3 padding). It can be stack-allocated and passed by value without allocation. Equality and arithmetic are O(1). The N field is a byte (0-255) which limits precision to 255 fractional digits — more than enough for any financial use case.

For very high-frequency paths (market data ingestion, order book updates), DecimalAmount is the recommended type. System.Decimal is fine for occasional calculations.