Nautilus Value Types
Nautilus represents prices, sizes, and money as immutable, precision-aware, fixed-point value types -
Price,Quantity,Money- backed internally by scaled integers, not floats. Each type carries a precision field that controls display but not identity. Same-type arithmetic preserves the type; mixing dimensions returnsDecimal.Quantityis unsigned.Moneyrequires matching currencies. Identifiers (InstrumentId,Symbol,Venue,TraderId,StrategyId,ClientOrderId, etc.) are likewise typed wrappers that validate at construction. The contract is that any numeric anomaly fails fast at the boundary instead of propagating into sizing, P&L, or order submission.
Core claim
The MK2 bug pattern of “float-vs-Decimal price comparison off-by-cents,
integer-division on a contract count, NaN mark-price into sizing” is closed
by construction in Nautilus: Price and Quantity are integer-scaled and
non-floating; cross-currency Money ops raise ValueError; Quantity
cannot go negative; conversions are explicit, not silent.
Reference: value types
Price
- Purpose. Market prices, quotes, price levels. Signed (can represent negative spreads, debits, etc. when used as a price-difference).
- Internal representation. Fixed-point integer scaled to a global precision (the framework uses 10^16 internally). Not a float.
- Precision field. Tracks decimal places for display and serialization.
“Precision controls display, not identity.” Two
Pricevalues with the same decimal magnitude but different precisions compare equal. - Construction.
Price(value, precision);Price.from_str(str)parses from strings;instrument.make_price(x)returns aPricethat respects the instrument’sprice_precisionandprice_increment(tick size). - Arithmetic (same-type).
Price + Price → Price,Price - Price → Price. Result uses the max precision of the operands (e.g.Price(100.5, p=1) + Price(0.125, p=3) → Price(100.625, p=3)). - Arithmetic (cross-dimensional).
Price * Price → Decimal,Price / Price → Decimal, floor-div and modulo also returnDecimal. Rationale from the docs: “Multiplying a price by a price produces ‘price squared’, not a price.” - Unary.
-Price → Price,+Price → Price,abs(Price) → Price. - Mixed scalar.
Price ± int → Decimal,Price ± float → float,Price ± Decimal → Decimal. Both directions (scalar op Price and Price op scalar) follow the same widening rule. - Conversions.
.as_decimal()(lossless),.as_double()(tofloat, use only at API boundaries),str(price)(formatted to precision). - Identity. Hashable; safe as dict keys.
Quantity
- Purpose. Trade sizes, order amounts, position quantities. Unsigned by contract.
- Hard constraint. “Attempting to create a negative quantity or subtract
a larger quantity from a smaller one raises an error.”
ValueErroris thrown at construction or at the failing operation. - Internal representation. Same fixed-point integer scaling as
Price. - Precision field. Mirrors
size_precisionon the instrument; lot-size rounding is the caller’s responsibility viainstrument.make_qty(x). - Arithmetic (same-type).
Quantity + Quantity → Quantity,Quantity - Quantity → Quantity(must not underflow zero).Quantity * Quantity / Quantity → Decimal. - Unary.
-Quantity → Decimal(becauseQuantityis unsigned and cannot itself represent a negative);+Quantity → Quantity;abs(Quantity) → Quantity. - Mixed scalar. Same widening as
Price: int →Decimal, float →float,Decimal→Decimal. - Conversions.
.as_decimal(),.as_double(),Quantity.from_str(s). - Construction discipline. Always go through
instrument.make_qty(...)in any path that submits orders - RiskEngine, the matching engine in backtest, and the venue all enforcesize_precision/size_increment.
Money
- Purpose. Monetary amounts, P&L, balances. Signed.
- Composition.
Money(amount, Currency)- currency is part of identity. - Hard constraint. Addition or subtraction across different currencies
“raises
ValueError- currency mismatch.” USD cannot silently coerce to EUR; conversion is explicit and goes through the Portfolio’s FX path. - Internal representation. Fixed-point integer scaled to the
Currency.precision(e.g., 2 for USD, 8 for BTC). - Arithmetic (same-currency, same-type).
Money + Money → Money,Money - Money → Money. Cross-dim ops returnDecimal. - Unary.
-Money → Money,+Money → Money,abs(Money) → Money. - Mixed scalar. Same widening rule as
PriceandQuantity. - Conversions.
.as_decimal(),.as_double(),Money.from_str(s). - Where it surfaces.
AccountBalancetriple, realized/unrealized PnL, commissions (accumulated by currency),total_margin_init/total_margin_mainttotals.
Currency
- Purpose. Identity for
Money; carriesprecision(decimal places),iso4217code (or zero for crypto), and a textual name. - Built-in catalog for fiat (USD, EUR, …) and major crypto (BTC, ETH, USDT, …); custom currencies registerable.
- Equality. By code; precision is a property of the registered
currency, so two
Money(1.00, USD)values with different precisions are a currency-precision contradiction - construct via the catalog, not ad-hoc.
AccountBalance
- Composition. Three
Moneyvalues in oneCurrencyplus invarianttotal == locked + free. Enforced at construction. - Behaviour. Cash accounts lock notional for pending orders;
reduce-only orders do not contribute to
balance_locked. Margin accounts reserve collateral viamargin_init/margin_maint. - Querying.
account.balance(currency),account.balances_total(),balances_free(),balances_locked(). Missing currency returnsNonefor point queries; totals always return aMoney(zero if empty) - robust to absent state.
MarginBalance
- Composition.
initial,maintenance, plus instrument scoping - per-instrument (isolated-margin venues) or account-wide (cross-margin). - Critical adapter contract.
MarginAccount.apply()replaces both stores from the incoming event. Partial snapshots that omit live margin entries cause those entries to disappear until the next full snapshot. Adapters MUST send full margin snapshots.
Position (value-type aspects)
signed_qtyis signed (long positive, short negative, flat zero) - this is the one signed quantity surface in the system, distinct fromQuantity(unsigned).- Average prices, realized/unrealized PnL, and commissions are typed
(
Price,Money) - never raw floats.
Reference: identifier types
All identifiers are typed string newtypes that validate at construction (non-empty, often charset-restricted) and are hashable for dict keys / cache lookups. The framework prevents the class of bug where two strings that look the same to Python compare unequal due to whitespace, case, or encoding drift.
Symbol- venue-native ticker (e.g.,"BTCUSDT","ESM5").Venue- venue identifier (e.g.,"BINANCE","IBKR").InstrumentId- composite{Symbol}.{Venue}. Unique per Nautilus system. Construct viaInstrumentId(Symbol("BTCUSDT"), Venue("BINANCE"))orInstrumentId.from_str("BTCUSDT.BINANCE").TraderId- operator-level identity, e.g.TraderId("TESTER-001"). Format<name>-<tag>; tag must be unique within the system.StrategyId- derived{ClassName}-{order_id_tag}; framework prevents duplicate registration.ClientOrderId- pre-venue order ID assigned by Nautilus at submission. Used for OMS routing and reconciliation joins.VenueOrderId- venue-assigned ID, populated on accept; used for modify/cancel after acceptance.PositionId- synthetic under NETTING (deterministic across restarts), venue-supplied under HEDGING.AccountId-{Venue}-{account_number}.TradeId- fill identity used for duplicate-fill detection alongside(order_side, last_px, last_qty).
Equality, hashing, ordering
- Equality is by value, not identity.
Price(100.0, p=2) == Price(100.000, p=4)isTrue. - Hashable. All value types and identifiers can be dict keys.
- Ordering.
<,>,<=,>=defined forPriceandQuantity; forMoney, ordering requires same-currency or raises.
Conversion rules
- To
Decimal..as_decimal()is the canonical exact conversion. Use anywhere precision matters (logging, display below the wire, ratios, statistics). - To
float..as_double()exists but is one-way and lossy. Reserved for ML feature pipelines and external APIs that demandfloat64. Never round-trip aPricethroughfloatand back. - From
str.Type.from_str(s)parses canonical decimal strings. Used for config files, replay logs, and tests. - Cross-type. No implicit
Price → MoneyorQuantity → Price. Conversions go through the instrument:instrument.notional_value(qty, price) → Money,instrument.calculate_pnl(...)etc.
Range validation and error conditions
- Negative
Quantity:ValueErrorat construction;Quantity - Quantityunderflow →ValueError. - Cross-currency
Moneyarithmetic:ValueError. - Precision overflow (a value that exceeds the global fixed-point capacity 10^16): construction raises rather than silently truncating.
- Construction with a precision that exceeds the type’s max precision raises.
- Identifier construction with empty/whitespace/invalid characters raises.
- Order submission with a
Pricewhose precision does not match the instrument’sprice_precisionis rejected by the RiskEngine pre-trade - “the framework does not round values automatically.”
NaN and infinity handling
Because Price, Quantity, and Money are integer-scaled fixed-point,
they cannot represent NaN or infinity. There is no bit pattern for
NaN in a Rust integer. Any NaN/inf entering at the boundary (a feed
parser, an ML feature, a divide-by-zero in upstream code) must be
caught and rejected before reaching Price.__init__ / Money.__init__,
which raise on non-finite input from float. This is the structural
fix for the MK2 mark-price-NaN-into-sizing bug class - there is no
representation in which NaN can propagate through scoring into an
order quantity.
Cortana MK3 implications
Each MK2 numeric bug class maps to a value-type rule that closes it.
-
MK2 bug: float-vs-Decimal price comparison off-by-cents.
Priceis integer-scaled with precision-aware equality (Price(100.0, p=2) == Price(100.000, p=4)). Cross-precision comparisons no longer drift;as_double()is one-way only. Cite: MK2 had a 1.0001 vs 1.00 comparison fail in the position-manager TP path. -
MK2 bug: integer division on a contract count.
Quantityis unsigned and divides toDecimal, not int (Quantity / Quantity → Decimal). Allocation math liketarget_notional / per_contract_notionalreturns aDecimalyou must explicitly convert viainstrument.make_qty(...)- andmake_qtyenforcessize_increment, so a 0.5-lot answer rounds explicitly per the lot-size rule, not silently viaint(). Cite: MK2 hadposition // 3truncate three contracts to one in the scaling logic. -
MK2 bug: NaN mark-price propagated into sizing.
PriceandMoneyconstructors raise on non-finitefloatinput; fixed-point integers have no NaN representation. A NaN at the feed is forced to fail at the boundary instead of crossing into the RiskEngine. Cite: MK2 had amark_price=nanfrom UW pass through into a position-size calculation that returnednancontracts. -
MK2 bug class: cross-currency P&L blending (latent).
Money + Moneyacross currencies raisesValueError. Commissions accumulate by currency. Portfolio FX conversion is the only path that changes a money’s currency. Cite: pre-emptive - MK2 is single-currency USD today, but multi-venue MK3 inherits the FX bug class for free. -
MK2 bug class: identifier typos and string drift.
InstrumentId,Symbol,Venue,ClientOrderIdare typed wrappers that validate at construction; topic strings on the bus are derived, not hand-typed. Cite: MK2 had at least one place where"SPY "(with trailing space) hashed to a different cache key than"SPY".
When it applies
Any MK3 component that touches a price, a size, an account balance,
P&L, or an identifier - which is to say all of execution, risk,
portfolio, scoring, position management, and persistence. The rule for
the MK3 codebase: floats only at adapter boundaries (in/out) and ML
feature pipelines. All trading-state math runs on Price / Quantity /
Money.
When it breaks
- ML feature pipelines. sklearn / PyTorch want
float64. Convert with.as_double()at the boundary; never feed a typed value into a numpy array shape-check that expectsfloat. - External logging / dashboards. JSON serialization will downgrade
to string or float; round-trip back through
from_strto restore identity. - Custom adapters. If you write a UW or IBKR adapter and forget
instrument.make_price()/make_qty(), the RiskEngine will reject your orders. That is the system working - not a bug to suppress with rounding.
See Also
- Nautilus Concepts - the value-types section is one of 16 architectural areas; this page expands it to a contract.
- Nautilus Developer Guide - the FFI memory contract and PyO3 patterns that implement these types.
- project_score_instability_april16
- earlier MK2 numeric-stability research.
Timeline
- 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep.