Module 04: Domain Model — The Language of TradingHow NautilusTrader translates every trading concept into precise, immutable code
The domain model is the bedrock of the entire system. It defines exactly what a “price”, “order”, or “position” is. Understanding the domain model means mastering the native “language” required to interface with the system.
1. Domain Model Overview
flowchart LR
subgraph layer_1["💎 Primitives & IDs"]
direction TB
values["Value Types (Price/Qty/Money)"]
identifiers["Identifiers (ID/Venue)"]
end
subgraph layer_2["🧩 Core Objects"]
direction TB
instruments["Instruments"]
orders["Orders"]
end
subgraph layer_3["📊 State & Capital"]
direction TB
events["Events (OrderFilled...)"]
positions["Positions"]
accounts["Accounts"]
events --> positions --> accounts
end
layer_1 ==> layer_2 ==> layer_3
2. Value Types — Why Standard Floating-Point Numbers Fail
In computer science, 0.1 + 0.2 = 0.30000000000000004, not exactly 0.3. If your trading system uses standard floating-point numbers to calculate prices, you might encounter scenarios where an order placed at $100.10 clears at $100.09999999. While the discrepancy seems microscopic, in high-frequency trading (HFT), compounding this error over millions of calculations can result in massive accounting discrepancies.
NautilusTrader solves this by employing fixed-point arithmetic (integers coupled with defined precision):
| Data Type | Internal Representation | Max Precision | Range | Application Target |
|---|---|---|---|---|
Price | i128 signed integer | 16 decimal places | ±17 Trillion | Quotes, execution prices |
Quantity | u128 unsigned integer | 16 decimal places | 0 ~ 34 Trillion | Order quantities, position sizing |
Money | i128 signed integer | 16 decimal places | ±17 Trillion | Account balances, PnL (Profit and Loss) |
Assume you need to represent 1.23450 (5 decimals of precision):
- The system stores it internally as the integer
123450with a precision of5 - For display purposes, it is divided by 105 =
1.23450 - Addition operations are strictly integer-based:
1.23450 + 2.10000→123450 + 210000 = 333450→3.33450
Why it matters: Integer-backed arithmetic avoids the floating-point drift that can break trading calculations.
# Python Code Example
from nautilus_trader.model.objects import Price, Quantity, Money
# Instantiate a Price object (5 decimal places of precision)
price = Price.from_str("1.23450")
print(price) # 1.23450
print(price.precision) # 5
print(price.raw) # 123450000000000 (Internal raw integer value)
# Exact calculation without floating-point drift
p1 = Price.from_str("0.10000")
p2 = Price.from_str("0.20000")
result = Price(p1.raw + p2.raw, precision=5)
print(result) # 0.30000 ✅ (Not 0.30000000000000004)3. Instruments — The Assets You Trade
Every tradable asset in NautilusTrader is represented by a specific type:
flowchart LR
subgraph base["🏁 Instrument Base Class"]
direction TB
In["+id, +symbol, +venue<br />+price_precision, +size_precision"]
end
subgraph subs["🎻 Subclass Implementations"]
direction LR
E["Equity<br />(Stocks)"]
F["Futures<br />(Derivatives)"]
O["Options<br />(Options)"]
C["Crypto<br />(Perpetuals)"]
FX["CurrencyPair<br />(Forex)"]
end
base ==> subs
style base fill:#0f1419,stroke:#4fc3f7,stroke-width:2px
style subs fill:#0f1419,stroke:#ab47bc,stroke-width:1px
| Instrument Type | Application | Intrinsic Attributes | Example |
|---|---|---|---|
CurrencyPair | Forex Trading | Base Currency, Quote Currency | EUR/USD |
Equity | Equity Trading | ISIN Code, Currency | AAPL |
FuturesContract | Futures Trading | Expiry Date, Contract Multiplier | ES 2024-03 |
OptionsContract | Options Trading | Strike Price, Call/Put | AAPL C 180 |
CryptoPerpetual | Crypto Perpetual Contracts | Inverse Contract Flag | BTC-USDT-PERP |
BettingInstrument | Sports Betting | Event Type, Market | Football Handicap |
Different instruments have fundamentally different settlement mechanisms. Equities settle in fiat currencies like USD; futures have specific contract multipliers and expiration dates; inverse crypto perpetuals settle in the base asset (e.g., BTC) rather than the quote asset (USDT). Categorizing them into distinct subclasses ensures that the idiosyncratic rules of each instrument are processed correctly.
4. Order Types — Your Execution Directives
NautilusTrader natively supports 11 distinct order types (9 foundational types + OrderList + simulated orders), comprehensively covering all execution scenarios:
❌ If only Market and Limit orders were available: You couldn’t easily implement logic like “automatically trigger a stop-loss when price breaches a support level” or “trail a stop-loss as profits accrue”. You would have to manually write timer loops to constantly poll market data—a suboptimal approach highly susceptible to latency and severe slippage.
✅ Utilizing a rich set of order types: Offloads stop-loss or trailing logic directly to the exchange’s matching engine (or NautilusTrader’s simulated exchange), ensuring significantly faster execution and deterministic reliability.
| Order Typology | Operational Context | Strategic Deployment |
|---|---|---|
MARKET | Market Order — Executes immediately at the best available price | When immediate market entry/exit is required |
LIMIT | Limit Order — Specifies a maximum buy price or minimum sell price | When seeking a more favorable entry price |
STOP_MARKET | Stop Market — Triggers a market order once a specified stop price is reached | Hard stop-loss to truncate drawdowns |
STOP_LIMIT | Stop Limit — Triggers a limit order once a specified stop price is reached | Precise control over the execution price of a stop |
MARKET_TO_LIMIT | Market-to-Limit — Executes at market first; any unfilled portion becomes a resting limit order | Desiring rapid execution without chasing excessive slippage |
MARKET_IF_TOUCHED | Market if Touched — Triggers a market order once a specified price is touched | Entering a position at a specific price inflection point |
LIMIT_IF_TOUCHED | Limit if Touched — Similar to above but executes as a limit order | Same as above, but with a strictly bounded execution price |
TRAILING_STOP_MARKET | Trailing Stop Market — Stop price dynamically trails the market price | Locking in unrealized PnL and letting profits run |
TRAILING_STOP_LIMIT | Trailing Stop Limit — Similar to above but executes as a limit order | Highly precise trailing stops with bounded slippage |
OrderList | Order Bracket — Binds multiple constituent orders into a unified logical group (e.g., Bracket Order) | Simultaneously placing an entry, take-profit, and stop-loss order |
Order Lifecycle
Every order transitions through a definitive sequence of states from inception to termination:
stateDiagram-v2
direction LR
[*] --> INITIALIZED
INITIALIZED --> SUBMITTED : Submitting
SUBMITTED --> ACCEPTED : Exchange Accepted
ACCEPTED --> FILLED : Order Filled
ACCEPTED --> CANCELED : Order Canceled
FILLED --> [*]
CANCELED --> [*]
state "Tertiary Transients" as other
SUBMITTED --> REJECTED : Exchange Rejected
ACCEPTED --> PARTIALLY_FILLED : Partial Fill Event
PARTIALLY_FILLED --> FILLED
REJECTED --> [*]
Time in Force (TiF)
| TiF Code | Full Nomenclature | Behavioral Semantic |
|---|---|---|
GTC | Good Till Cancel | Remains working until explicitly canceled by the client |
IOC | Immediate or Cancel | Executes immediately; any unexecuted portion is canceled (no resting liquidity) |
FOK | Fill or Kill | Must be executed in its entirety immediately, otherwise the entire order is canceled |
GTD | Good Till Date | Remains working until a specifically designated date/time |
DAY | Day Order | Valid for the current trading day; canceled at market close |
AT_THE_OPEN | At The Open (MOO/LOO) | Valid exclusively during the market open auction cross |
AT_THE_CLOSE | At The Close (MOC/LOC) | Valid exclusively during the market close auction cross |
5. Positions — The Event-Sourced Model
The Position construct features a profoundly elegant design. Rather than functioning as a static state record, a position is dynamically synthesized via event sourcing—an aggregation of sequential execution events:
sequenceDiagram
participant E as ⚡ Event Stream
participant P as 📊 Position Object
Note over P: Position does not exist
E->>P: OrderFilled(Buy 1.0 BTC @ 50,000)
Note over P: Open Position<br />Side=LONG, Quantity=1.0<br />Avg Entry Price=50,000
E->>P: OrderFilled(Buy 0.5 BTC @ 51,000)
Note over P: Scale In (Add to Position)<br />Side=LONG, Quantity=1.5<br />Avg Entry Price=50,333.33
E->>P: OrderFilled(Sell 1.0 BTC @ 55,000)
Note over P: Partial Close<br />Side=LONG, Quantity=0.5<br />Realized PnL=+4,666.67
E->>P: OrderFilled(Sell 0.5 BTC @ 54,000)
Note over P: Flat (Position Closed)<br />Side=FLAT, Quantity=0<br />Total Realized PnL=+6,500
❌ If only the final state is recorded: You know you currently “hold 0.5 BTC”, but you lose the execution lineage. Was it acquired in a single transaction, or did the account buy 1.0 BTC and later sell 0.5 BTC? What is the exact Volume-Weighted Average Price (VWAP) of the entry?
✅ Using Event Sourcing: Every state mutation is immutably logged. You can reconstruct the position’s lifecycle: when it was opened, scaled in, scaled out, and flattened, along with the precise price and quantity of every constituent fill. This is critical for quantitative trade analysis, backtesting fidelity, and institutional auditability.
Position Introspection (Code Syntax)
# Core Position attributes (accessible within a strategy)
position = self.cache.position(position_id)
# --- Identity & Topology ---
position.id # PositionId: P-001
position.instrument_id # InstrumentId: BTC-USDT-PERP.BINANCE
position.strategy_id # StrategyId: EMACross-001
position.side # PositionSide: LONG / SHORT / FLAT
# --- Volumetric Matrices ---
position.quantity # Quantity: 1.5 (Current open quantity)
position.peak_qty # Quantity: 2.0 (Maximum historical quantity held)
position.signed_qty # float: 1.5 (Positive for LONG, negative for SHORT)
# --- Pricing Metrics ---
position.avg_px_open # float: 50333.33 (Average entry price)
position.avg_px_close # float: 55000.0 (Average exit price, 0 if currently open)
position.last_px # Price: Last observed market price
# --- Profit & Loss (PnL) ---
position.realized_pnl # Money: Realized PnL (from closed portions)
position.unrealized_pnl(last_price) # Money: Unrealized PnL (Mark-to-Market)
# --- Temporal Horizons ---
position.ts_opened # uint64_t: Opening timestamp (in nanoseconds)
position.ts_closed # uint64_t: Closing timestamp (in nanoseconds, 0 if open)
position.duration_ns # uint64_t: Duration held (in nanoseconds)
# --- Event Lineage ---
position.events # list: List of all associated events
position.trade_ids # list: List of all constituent Trade IDs6. Account Types
CashAccount — Cash Account
Trading is strictly constrained by available settled capital. Purchasing equities debits the balance; selling credits it. No margin or leverage is permitted. Supports both single and multi-currency denominations.
MarginAccount — Margin Account
Facilitates leveraged trading by requiring only a fractional margin deposit to hold larger notional positions. The standard paradigm for futures and forex trading. Supports both single and multi-currency denominations.
BettingAccount — Betting Account
A specialized account structure engineered for betting exchanges (e.g., Betfair). Operates strictly on a single currency basis.
7. The Identifier System
In a distributed infrastructure interfacing with multiple exchanges and running concurrent strategies, deterministic identification of every entity is mission-critical:
| Semantic Identifier | Syntax Example | Operational Target |
|---|---|---|
InstrumentId | BTC-USDT-PERP.BINANCE | Unique identifier for a tradable asset (Symbol + Venue) |
Venue | BINANCE | The exchange or market data provider’s nomenclature |
ClientOrderId | O-20240105-001 | Client-generated unique nonce for an order |
VenueOrderId | 123456789 | Exchange-assigned order identifier |
TradeId | T-20240105-001 | Unique identifier for a specific execution (fill) |
StrategyId | EMACross-001 | Unique namespace for a specific strategy instance |
AccountId | BINANCE-main | Unique identifier for a specific financial account |
PositionId | P-001 | Unique identifier aggregating a specific position lifecycle |
InstrumentId explicitly incorporates the Venue designation. Therefore,
BTC-USDT.BINANCE and
BTC-USDT.OKX are treated as entirely distinct instruments, even if they reference the same underlying base pair. This guarantees cross-venue isolation and prevents routing collisions in multi-exchange topologies.
8. Time System — Nanosecond Precision
All timestamps in NautilusTrader use nanosecond-precision UTC time:
Syntax: 2024-01-05T15:30:45.123456789Z
2024-01-05— Gregorian DateT— Standard Time Delimiter15:30:45— UTC Chronology.123456789— 9-decimal fractional seconds (Nanoseconds)Z— Absolute UTC Meridian Indicator
In high-frequency trading (HFT) and market making, microsecond-level timing differences can change queue position, fill probability, and adverse-selection exposure. Nanosecond timestamps preserve event ordering at the precision needed for realistic replay and post-trade analysis.
9. Market Data Types
| Data Topology | Architectural Definition | Data Vector Footprint |
|---|---|---|
QuoteTick | Top-of-book snapshot (Best Bid/Ask Price & Size) | 4 scalar values |
TradeTick | Discrete execution fill (Price, Quantity, Aggressor Side) | 3 scalar values |
Bar | OHLCV Candlestick (Open, High, Low, Close, Volume) | 5 scalar values |
OrderBookDelta | Incremental L2 Order Book update | Single price-level mutation |
OrderBookDepth10 | L2 Order Book snapshot (Top 10 Bids/Asks) | 40 scalar values |
InstrumentStatus | Instrument trading state transition | Enum state flag |
10. Interactive Exercises
Suppose your strategy calculates a price utilizing: 0.1 + 0.2. What are the resulting evaluations in generic Python versus NautilusTrader?
Generic Python (float):
>>> 0.1 + 0.2
0.30000000000000004 ❌ Precision loss!NautilusTrader (Price):
>>> Price.from_str("0.1") + Price.from_str("0.2")Equivalates to the integer operation 1000... + 2000... = 3000...
Emitted Result: 0.3 ✅ Exact decimal result
Conclusion: Do not use generic float types to represent pricing structures in a production trading system.
Which order types should you deploy for the following execution scenarios?
- BTC is currently trading at 50,000. You require a hard stop-loss to flatten the position if the price breaches 45,000.
- You want to acquire BTC exposure, but strictly refuse to pay more than 49,500.
- BTC is trending upward. You want to lock in unrealized PnL by trailing your stop price behind the market peak.
- You require immediate BTC exposure, aggressively crossing the spread regardless of execution price.
STOP_MARKET(Stop Market)
Triggers when the price breaches 45,000, immediately routing a market order for liquidation.LIMIT(Limit Order)
Rests a passive bid in the limit order book, capping execution at a maximum of 49,500.TRAILING_STOP_MARKET(Trailing Stop Market)
The stop trigger dynamically ratchets upward with new market highs, systematically protecting accrued profit.MARKET(Market Order)
Instantly sweeps the order book at the best available ask, optimizing for minimal latency rather than price.
The following code block attempting to instantiate a LimitOrder contains 2 critical type errors:
from nautilus_trader.model.orders import LimitOrder
from nautilus_trader.model.enums import OrderSide, TimeInForce
order = LimitOrder(
instrument_id="BTC-USDT-PERP.BINANCE", # 🤔
order_side=OrderSide.BUY,
quantity=1.5, # 🤔
price=Price.from_str("50000.00"),
time_in_force=TimeInForce.GTC,
)- Error 1:
instrument_idstrictly requires anInstrumentIdobject, not a primitive string.
→ Correct:instrument_id=InstrumentId.from_str("BTC-USDT-PERP.BINANCE") - Error 2:
quantitydemands aQuantityobject, not a standard float.
→ Correct:quantity=Quantity.from_str("1.5")
💡 Takeaway: The Fail-Fast Philosophy of NautilusTrader
NautilusTrader leverages strong typing (Price, Quantity, InstrumentId) instead of generic primitives (float, str). This architectural choice guarantees that structural errors are caught decisively during compilation/initialization, entirely averting catastrophic failures during live production trading.
11. Module Summary
- Value Types:
Price,Quantity, andMoneyuse fixed-point arithmetic (internal integers + precision bounds) to avoid floating-point drift. - Instruments: Six distinct subclasses comprehensively cover all asset classes, each encapsulating its specific settlement and tick-size logic.
- 11 Order Types: Spanning from baseline Market orders to Trailing Stops and complex OrderLists (Bracket Orders), covering every conceivable execution requirement.
- Order State Machine: A rigorously defined lifecycle where every state transition correlates to a deterministic event.
- Positions as Event Aggregates: Positions are dynamically synthesized via sequential
OrderFilledevents, preserving a perfect, immutable historical audit trail. - Identifier System:
InstrumentIdinherently concatenates the specific venue, structurally preventing routing collisions in multi-exchange deployments. - Nanosecond Precision: Every timestamp within the engine natively resolves to a billionth of a second (UTC).