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.