Claude Code Plugins

Community-maintained marketplace

Feedback

NautilusTrader algorithmic trading platform. Use for building trading strategies, backtesting, live trading, data handling, and quantitative finance applications.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name nautilus-trader
description NautilusTrader algorithmic trading platform. Use for building trading strategies, backtesting, live trading, data handling, and quantitative finance applications.

Nautilus-Trader Skill

Comprehensive assistance with nautilus-trader development, generated from official documentation.

When to Use This Skill

This skill should be triggered when:

  • Working with nautilus_trader
  • Asking about nautilus_trader features or APIs
  • Implementing nautilus_trader solutions
  • Debugging nautilus_trader code
  • Learning nautilus_trader best practices

Quick Reference

Common Patterns

Pattern 1: Strategies The heart of the NautilusTrader user experience is in writing and working with trading strategies. Defining a strategy involves inheriting the Strategy class and implementing the methods required by the strategy's logic. Key capabilities: All Actor capabilities. Order management. Relationship with actors: The Strategy class inherits from Actor, which means strategies have access to all actor functionality plus order management capabilities. tipWe recommend reviewing the Actors guide before diving into strategy development. Strategies can be added to Nautilus systems in any environment contexts and will start sending commands and receiving events based on their logic as soon as the system starts. Using the basic building blocks of data ingest, event handling, and order management (which we will discuss below), it's possible to implement any type of strategy including directional, momentum, re-balancing, pairs, market making etc. infoSee the Strategy API Reference for a complete description of all available methods. There are two main parts of a Nautilus trading strategy: The strategy implementation itself, defined by inheriting the Strategy class. The optional strategy configuration, defined by inheriting the StrategyConfig class. tipOnce a strategy is defined, the same source code can be used for backtesting and live trading. The main capabilities of a strategy include: Historical data requests. Live data feed subscriptions. Setting time alerts or timers. Cache access. Portfolio access. Creating and managing orders and positions. Strategy implementation​ Since a trading strategy is a class which inherits from Strategy, you must define a constructor where you can handle initialization. Minimally the base/super class needs to be initialized: from nautilus_trader.trading.strategy import Strategyclass MyStrategy(Strategy): def init(self) -> None: super().init() # <-- the superclass must be called to initialize the strategy From here, you can implement handlers as necessary to perform actions based on state transitions and events. warningDo not call components such as clock and logger in the __init__ constructor (which is prior to registration). This is because the systems clock and logging subsystem have not yet been initialized. Handlers​ Handlers are methods within the Strategy class which may perform actions based on different types of events or on state changes. These methods are named with the prefix on_*. You can choose to implement any or all of these handler methods depending on the specific goals and needs of your strategy. The purpose of having multiple handlers for similar types of events is to provide flexibility in handling granularity. This means that you can choose to respond to specific events with a dedicated handler, or use a more generic handler to react to a range of related events (using typical switch statement logic). The handlers are called in sequence from the most specific to the most general. Stateful actions​ These handlers are triggered by lifecycle state changes of the Strategy. It's recommended to: Use the on_start method to initialize your strategy (e.g., fetch instruments, subscribe to data). Use the on_stop method for cleanup tasks (e.g., cancel open orders, close open positions, unsubscribe from data). def on_start(self) -> None:def on_stop(self) -> None:def on_resume(self) -> None:def on_reset(self) -> None:def on_dispose(self) -> None:def on_degrade(self) -> None:def on_fault(self) -> None:def on_save(self) -> dict[str, bytes]: # Returns user-defined dictionary of state to be saveddef on_load(self, state: dict[str, bytes]) -> None: Data handling​ These handlers receive data updates, including built-in market data and custom user-defined data. You can use these handlers to define actions upon receiving data object instances. from nautilus_trader.core import Datafrom nautilus_trader.model import OrderBookfrom nautilus_trader.model import Barfrom nautilus_trader.model import QuoteTickfrom nautilus_trader.model import TradeTickfrom nautilus_trader.model import OrderBookDeltasfrom nautilus_trader.model import InstrumentClosefrom nautilus_trader.model import InstrumentStatusfrom nautilus_trader.model.instruments import Instrumentdef on_order_book_deltas(self, deltas: OrderBookDeltas) -> None:def on_order_book(self, order_book: OrderBook) -> None:def on_quote_tick(self, tick: QuoteTick) -> None:def on_trade_tick(self, tick: TradeTick) -> None:def on_bar(self, bar: Bar) -> None:def on_instrument(self, instrument: Instrument) -> None:def on_instrument_status(self, data: InstrumentStatus) -> None:def on_instrument_close(self, data: InstrumentClose) -> None:def on_historical_data(self, data: Data) -> None:def on_data(self, data: Data) -> None: # Custom data passed to this handlerdef on_signal(self, signal: Data) -> None: # Custom signals passed to this handler Order management​ These handlers receive events related to orders. OrderEvent type messages are passed to handlers in the following sequence: Specific handler (e.g., on_order_accepted, on_order_rejected, etc.) on_order_event(...) on_event(...) from nautilus_trader.model.events import OrderAcceptedfrom nautilus_trader.model.events import OrderCanceledfrom nautilus_trader.model.events import OrderCancelRejectedfrom nautilus_trader.model.events import OrderDeniedfrom nautilus_trader.model.events import OrderEmulatedfrom nautilus_trader.model.events import OrderEventfrom nautilus_trader.model.events import OrderExpiredfrom nautilus_trader.model.events import OrderFilledfrom nautilus_trader.model.events import OrderInitializedfrom nautilus_trader.model.events import OrderModifyRejectedfrom nautilus_trader.model.events import OrderPendingCancelfrom nautilus_trader.model.events import OrderPendingUpdatefrom nautilus_trader.model.events import OrderRejectedfrom nautilus_trader.model.events import OrderReleasedfrom nautilus_trader.model.events import OrderSubmittedfrom nautilus_trader.model.events import OrderTriggeredfrom nautilus_trader.model.events import OrderUpdateddef on_order_initialized(self, event: OrderInitialized) -> None:def on_order_denied(self, event: OrderDenied) -> None:def on_order_emulated(self, event: OrderEmulated) -> None:def on_order_released(self, event: OrderReleased) -> None:def on_order_submitted(self, event: OrderSubmitted) -> None:def on_order_rejected(self, event: OrderRejected) -> None:def on_order_accepted(self, event: OrderAccepted) -> None:def on_order_canceled(self, event: OrderCanceled) -> None:def on_order_expired(self, event: OrderExpired) -> None:def on_order_triggered(self, event: OrderTriggered) -> None:def on_order_pending_update(self, event: OrderPendingUpdate) -> None:def on_order_pending_cancel(self, event: OrderPendingCancel) -> None:def on_order_modify_rejected(self, event: OrderModifyRejected) -> None:def on_order_cancel_rejected(self, event: OrderCancelRejected) -> None:def on_order_updated(self, event: OrderUpdated) -> None:def on_order_filled(self, event: OrderFilled) -> None:def on_order_event(self, event: OrderEvent) -> None: # All order event messages are eventually passed to this handler Position management​ These handlers receive events related to positions. PositionEvent type messages are passed to handlers in the following sequence: Specific handler (e.g., on_position_opened, on_position_changed, etc.) on_position_event(...) on_event(...) from nautilus_trader.model.events import PositionChangedfrom nautilus_trader.model.events import PositionClosedfrom nautilus_trader.model.events import PositionEventfrom nautilus_trader.model.events import PositionOpeneddef on_position_opened(self, event: PositionOpened) -> None:def on_position_changed(self, event: PositionChanged) -> None:def on_position_closed(self, event: PositionClosed) -> None:def on_position_event(self, event: PositionEvent) -> None: # All position event messages are eventually passed to this handler Generic event handling​ This handler will eventually receive all event messages which arrive at the strategy, including those for which no other specific handler exists. from nautilus_trader.core.message import Eventdef on_event(self, event: Event) -> None: Handler example​ The following example shows a typical on_start handler method implementation (taken from the example EMA cross strategy). Here we can see the following: Indicators being registered to receive bar updates. Historical data being requested (to hydrate the indicators). Live data being subscribed to. def on_start(self) -> None: """ Actions to be performed on strategy start. """ self.instrument = self.cache.instrument(self.instrument_id) if self.instrument is None: self.log.error(f"Could not find instrument for {self.instrument_id}") self.stop() return # Register the indicators for updating self.register_indicator_for_bars(self.bar_type, self.fast_ema) self.register_indicator_for_bars(self.bar_type, self.slow_ema) # Get historical data self.request_bars(self.bar_type) # Subscribe to live data self.subscribe_bars(self.bar_type) self.subscribe_quote_ticks(self.instrument_id) Clock and timers​ Strategies have access to a Clock which provides a number of methods for creating different timestamps, as well as setting time alerts or timers to trigger TimeEvents. infoSee the Clock API reference for a complete list of available methods. Current timestamps​ While there are multiple ways to obtain current timestamps, here are two commonly used methods as examples: To get the current UTC timestamp as a tz-aware pd.Timestamp: import pandas as pdnow: pd.Timestamp = self.clock.utc_now() To get the current UTC timestamp as nanoseconds since the UNIX epoch: unix_nanos: int = self.clock.timestamp_ns() Time alerts​ Time alerts can be set which will result in a TimeEvent being dispatched to the on_event handler at the specified alert time. In a live context, this might be slightly delayed by a few microseconds. This example sets a time alert to trigger one minute from the current time: import pandas as pd# Fire a TimeEvent one minute from nowself.clock.set_time_alert( name="MyTimeAlert1", alert_time=self.clock.utc_now() + pd.Timedelta(minutes=1),) Timers​ Continuous timers can be set up which will generate a TimeEvent at regular intervals until the timer expires or is canceled. This example sets a timer to fire once per minute, starting immediately: import pandas as pd# Fire a TimeEvent every minuteself.clock.set_timer( name="MyTimer1", interval=pd.Timedelta(minutes=1),) Cache access​ The trader instances central Cache can be accessed to fetch data and execution objects (orders, positions etc). There are many methods available often with filtering functionality, here we go through some basic use cases. Fetching data​ The following example shows how data can be fetched from the cache (assuming some instrument ID attribute is assigned): last_quote = self.cache.quote_tick(self.instrument_id)last_trade = self.cache.trade_tick(self.instrument_id)last_bar = self.cache.bar(bar_type) Fetching execution objects​ The following example shows how individual order and position objects can be fetched from the cache: order = self.cache.order(client_order_id)position = self.cache.position(position_id) infoSee the Cache API Reference for a complete description of all available methods. Portfolio access​ The traders central Portfolio can be accessed to fetch account and positional information. The following shows a general outline of available methods. Account and positional information​ import decimalfrom nautilus_trader.accounting.accounts.base import Accountfrom nautilus_trader.model import Venuefrom nautilus_trader.model import Currencyfrom nautilus_trader.model import Moneyfrom nautilus_trader.model import InstrumentIddef account(self, venue: Venue) -> Accountdef balances_locked(self, venue: Venue) -> dict[Currency, Money]def margins_init(self, venue: Venue) -> dict[Currency, Money]def margins_maint(self, venue: Venue) -> dict[Currency, Money]def unrealized_pnls(self, venue: Venue) -> dict[Currency, Money]def realized_pnls(self, venue: Venue) -> dict[Currency, Money]def net_exposures(self, venue: Venue) -> dict[Currency, Money]def unrealized_pnl(self, instrument_id: InstrumentId) -> Moneydef realized_pnl(self, instrument_id: InstrumentId) -> Moneydef net_exposure(self, instrument_id: InstrumentId) -> Moneydef net_position(self, instrument_id: InstrumentId) -> decimal.Decimaldef is_net_long(self, instrument_id: InstrumentId) -> booldef is_net_short(self, instrument_id: InstrumentId) -> booldef is_flat(self, instrument_id: InstrumentId) -> booldef is_completely_flat(self) -> bool infoSee the Portfolio API Reference for a complete description of all available methods. Reports and analysis​ The Portfolio also makes a PortfolioAnalyzer available, which can be fed with a flexible amount of data (to accommodate different lookback windows). The analyzer can provide tracking for and generating of performance metrics and statistics. infoSee the PortfolioAnalyzer API Reference for a complete description of all available methods. infoSee the Portfolio statistics guide. Trading commands​ NautilusTrader offers a comprehensive suite of trading commands, enabling granular order management tailored for algorithmic trading. These commands are essential for executing strategies, managing risk, and ensuring seamless interaction with various trading venues. In the following sections, we will delve into the specifics of each command and its use cases. infoThe Execution guide explains the flow through the system, and can be helpful to read in conjunction with the below. Submitting orders​ An OrderFactory is provided on the base class for every Strategy as a convenience, reducing the amount of boilerplate required to create different Order objects (although these objects can still be initialized directly with the Order.init(...) constructor if the trader prefers). The component a SubmitOrder or SubmitOrderList command will flow to for execution depends on the following: If an emulation_trigger is specified, the command will firstly be sent to the OrderEmulator. If an exec_algorithm_id is specified (with no emulation_trigger), the command will firstly be sent to the relevant ExecAlgorithm. Otherwise, the command will firstly be sent to the RiskEngine. This example submits a LIMIT BUY order for emulation (see Emulated Orders): from nautilus_trader.model.enums import OrderSidefrom nautilus_trader.model.enums import TriggerTypefrom nautilus_trader.model.orders import LimitOrderdef buy(self) -> None: """ Users simple buy method (example). """ order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), price=self.instrument.make_price(5000.00), emulation_trigger=TriggerType.LAST_PRICE, ) self.submit_order(order) infoYou can specify both order emulation and an execution algorithm. This example submits a MARKET BUY order to a TWAP execution algorithm: from nautilus_trader.model.enums import OrderSidefrom nautilus_trader.model.enums import TimeInForcefrom nautilus_trader.model import ExecAlgorithmIddef buy(self) -> None: """ Users simple buy method (example). """ order: MarketOrder = self.order_factory.market( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), time_in_force=TimeInForce.FOK, exec_algorithm_id=ExecAlgorithmId("TWAP"), exec_algorithm_params={"horizon_secs": 20, "interval_secs": 2.5}, ) self.submit_order(order) Canceling orders​ Orders can be canceled individually, as a batch, or all orders for an instrument (with an optional side filter). If the order is already closed or already pending cancel, then a warning will be logged. If the order is currently open then the status will become PENDING_CANCEL. The component a CancelOrder, CancelAllOrders or BatchCancelOrders command will flow to for execution depends on the following: If the order is currently emulated, the command will firstly be sent to the OrderEmulator. If an exec_algorithm_id is specified (with no emulation_trigger), and the order is still active within the local system, the command will firstly be sent to the relevant ExecAlgorithm. Otherwise, the order will firstly be sent to the ExecutionEngine. infoAny managed GTD timer will also be canceled after the command has left the strategy. The following shows how to cancel an individual order: self.cancel_order(order) The following shows how to cancel a batch of orders: from nautilus_trader.model import Ordermy_order_list: list[Order] = [order1, order2, order3]self.cancel_orders(my_order_list) The following shows how to cancel all orders: self.cancel_all_orders() Modifying orders​ Orders can be modified individually when emulated, or open on a venue (if supported). If the order is already closed or already pending cancel, then a warning will be logged. If the order is currently open then the status will become PENDING_UPDATE. warningAt least one value must differ from the original order for the command to be valid. The component a ModifyOrder command will flow to for execution depends on the following: If the order is currently emulated, the command will firstly be sent to the OrderEmulator. Otherwise, the order will firstly be sent to the RiskEngine. infoOnce an order is under the control of an execution algorithm, it cannot be directly modified by a strategy (only canceled). The following shows how to modify the size of LIMIT BUY order currently open on a venue: from nautilus_trader.model import Quantitynew_quantity: Quantity = Quantity.from_int(5)self.modify_order(order, new_quantity) infoThe price and trigger price can also be modified (when emulated or supported by a venue). Strategy configuration​ The main purpose of a separate configuration class is to provide total flexibility over where and how a trading strategy can be instantiated. This includes being able to serialize strategies and their configurations over the wire, making distributed backtesting and firing up remote live trading possible. This configuration flexibility is actually opt-in, in that you can actually choose not to have any strategy configuration beyond the parameters you choose to pass into your strategies' constructor. If you would like to run distributed backtests or launch live trading servers remotely, then you will need to define a configuration. Here is an example configuration: from decimal import Decimalfrom nautilus_trader.config import StrategyConfigfrom nautilus_trader.model import Bar, BarTypefrom nautilus_trader.model import InstrumentIdfrom nautilus_trader.trading.strategy import Strategy# Configuration definitionclass MyStrategyConfig(StrategyConfig): instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE" bar_type: BarType # example value: "ETHUSDT-PERP.BINANCE-15-MINUTE[LAST]-EXTERNAL" fast_ema_period: int = 10 slow_ema_period: int = 20 trade_size: Decimal order_id_tag: str# Strategy definitionclass MyStrategy(Strategy): def init(self, config: MyStrategyConfig) -> None: # Always initialize the parent Strategy class # After this, configuration is stored and available via self.config super().init(config) # Custom state variables self.time_started = None self.count_of_processed_bars: int = 0 def on_start(self) -> None: self.time_started = self.clock.utc_now() # Remember time, when strategy started self.subscribe_bars(self.config.bar_type) # See how configuration data are exposed via self.config def on_bar(self, bar: Bar): self.count_of_processed_bars += 1 # Update count of processed bars# Instantiate configuration with specific values. By setting:# - InstrumentId - we parameterize the instrument the strategy will trade.# - BarType - we parameterize bar-data, that strategy will trade.config = MyStrategyConfig( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), bar_type=BarType.from_str("ETHUSDT-PERP.BINANCE-15-MINUTE[LAST]-EXTERNAL"), trade_size=Decimal(1), order_id_tag="001",)# Pass configuration to our trading strategy.strategy = MyStrategy(config=config) When implementing strategies, it's recommended to access configuration values directly through self.config. This provides clear separation between: Configuration data (accessed via self.config): Contains initial settings, that define how the strategy works. Example: self.config.trade_size, self.config.instrument_id Strategy state variables (as direct attributes): Track any custom state of the strategy. Example: self.time_started, self.count_of_processed_bars This separation makes code easier to understand and maintain. noteEven though it often makes sense to define a strategy which will trade a single instrument. The number of instruments a single strategy can work with is only limited by machine resources. Managed GTD expiry​ It's possible for the strategy to manage expiry for orders with a time in force of GTD (Good 'till Date). This may be desirable if the exchange/broker does not support this time in force option, or for any reason you prefer the strategy to manage this. To use this option, pass manage_gtd_expiry=True to your StrategyConfig. When an order is submitted with a time in force of GTD, the strategy will automatically start an internal time alert. Once the internal GTD time alert is reached, the order will be canceled (if not already closed). Some venues (such as Binance Futures) support the GTD time in force, so to avoid conflicts when using managed_gtd_expiry you should set use_gtd=False for your execution client config. Multiple strategies​ If you intend running multiple instances of the same strategy, with different configurations (such as trading different instruments), then you will need to define a unique order_id_tag for each of these strategies (as shown above). noteThe platform has built-in safety measures in the event that two strategies share a duplicated strategy ID, then an exception will be raised that the strategy ID has already been registered. The reason for this is that the system must be able to identify which strategy various commands and events belong to. A strategy ID is made up of the strategy class name, and the strategies order_id_tag separated by a hyphen. For example the above config would result in a strategy ID of MyStrategy-001. noteSee the StrategyId API Reference for further details.

Strategy

Pattern 2: Positions This guide explains how positions work in NautilusTrader, including their lifecycle, aggregation from order fills, profit and loss calculations, and the important concept of position snapshotting for netting OMS configurations. Overview​ A position represents an open exposure to a particular instrument in the market. Positions are fundamental to tracking trading performance and risk, as they aggregate all fills for a particular instrument and continuously calculate metrics like unrealized PnL, average entry price, and total exposure. The system automatically creates positions when orders fill and tracks them from open to close. The platform supports both netting and hedging position management styles through its OMS (Order Management System) configuration. Position lifecycle​ Creation​ The system opens a position on the first fill: NETTING OMS: Opens on first fill for an instrument (one position per instrument). HEDGING OMS: Opens on first fill for a new position_id (multiple positions per instrument). A position tracks: Opening order and fill details. Entry side (LONG or SHORT). Initial quantity and average price. Timestamps for initialization and opening. tipYou can access positions through the Cache using self.cache.position(position_id) or self.cache.positions(instrument_id=instrument_id) from within your actors/strategies. Updates​ As additional fills occur, the position: Aggregates quantities from buy and sell fills. Recalculates average entry and exit prices. Updates peak quantity (maximum exposure reached). Tracks all associated order IDs and trade IDs. Accumulates commissions by currency. Closure​ A position closes when the net quantity becomes zero (FLAT). At closure: The closing order ID is recorded. Duration is calculated from open to close. Final realized PnL is computed. In NETTING OMS, the engine preserves closed position state through snapshots to maintain historical PnL (see Position snapshotting). Order fill aggregation​ Positions aggregate order fills to maintain an accurate view of market exposure. The aggregation process handles both sides of trading activity: Buy fills​ When a BUY order fills: Increases long exposure or reduces short exposure. Updates average entry price for opening trades. Updates average exit price for closing trades. Calculates realized PnL for any closed portion. Sell fills​ When a SELL order fills: Increases short exposure or reduces long exposure. Updates average entry price for opening trades. Updates average exit price for closing trades. Calculates realized PnL for any closed portion. Net position calculation​ The position maintains a signed_qty field representing the net exposure: Positive values indicate LONG positions. Negative values indicate SHORT positions. Zero indicates a FLAT (closed) position. # Example: Position aggregation# Initial BUY 100 units at $50signed_qty = +100 # LONG position# Subsequent SELL 150 units at $55signed_qty = -50 # Now SHORT position# Final BUY 50 units at $52signed_qty = 0 # Position FLAT (closed) OMS types and position management​ NautilusTrader supports two primary OMS types that fundamentally affect how positions are tracked and managed. An OmsType.UNSPECIFIED option also exists, which defaults to the component's context. For comprehensive details, see the Execution guide. NETTING​ In NETTING mode, all fills for an instrument are aggregated into a single position: One position per instrument ID. All fills contribute to the same position. Position flips from LONG to SHORT (or vice versa) as net quantity changes. Historical snapshots preserve closed position states. HEDGING​ In HEDGING mode, multiple positions can exist for the same instrument: Multiple simultaneous LONG and SHORT positions. Each position has a unique position ID. Positions are tracked independently. No automatic netting across positions. warningWhen using HEDGING mode, be aware of increased margin requirements as each position consumes margin independently. Some venues may not support true hedging mode and will net positions automatically. Strategy vs venue OMS​ The platform allows different OMS configurations for strategies and venues: Strategy OMSVenue OMSBehaviorNETTINGNETTINGSingle position per instrument at both strategy and venue.HEDGINGHEDGINGMultiple positions supported at both levels.NETTINGHEDGINGVenue tracks multiple, Nautilus maintains single position.HEDGINGNETTINGVenue tracks single, Nautilus maintains virtual positions. tipFor most trading scenarios, keeping strategy and venue OMS types aligned simplifies position management. Override configurations are primarily useful for prop trading desks or when interfacing with legacy systems. See the Live guide for venue-specific OMS configuration. Position snapshotting​ Position snapshotting is an important feature for NETTING OMS configurations that preserves the state of closed positions for accurate PnL tracking and reporting. Why snapshotting matters​ In a NETTING system, when a position closes (becomes FLAT) and then reopens with a new trade, the position object is reset to track the new exposure. Without snapshotting, the historical realized PnL from the previous position cycle would be lost. How it works​ When a NETTING position closes and then receives a new fill for the same instrument, the execution engine snapshots the closed position state before resetting it, preserving: Final quantities and prices. Realized PnL. All fill events. Commission totals. This snapshot is stored in the cache indexed by position ID. The position then resets for the new cycle while previous snapshots remain accessible. The Portfolio aggregates PnL across all snapshots for accurate totals. noteThis historical snapshot mechanism differs from optional position state snapshots (snapshot_positions), which periodically record open-position state for telemetry. See the Live guide for snapshot_positions and snapshot_positions_interval_secs settings. Example scenario​ # NETTING OMS Example# Cycle 1: Open LONG positionBUY 100 units at $50 # Position opensSELL 100 units at $55 # Position closes, PnL = $500# Snapshot taken preserving $500 realized PnL# Cycle 2: Open SHORT positionSELL 50 units at $54 # Position reopens (SHORT)BUY 50 units at $52 # Position closes, PnL = $100# Snapshot taken preserving $100 realized PnL# Total realized PnL = $500 + $100 = $600 (from snapshots) Without snapshotting, only the most recent cycle's PnL would be available, leading to incorrect reporting and analysis. PnL calculations​ NautilusTrader provides comprehensive PnL calculations that account for instrument specifications and market conventions. Realized PnL​ Calculated when positions are partially or fully closed: # For standard instrumentsrealized_pnl = (exit_price - entry_price) * closed_quantity * multiplier# For inverse instruments (side-aware)# LONG: realized_pnl = closed_quantity * multiplier * (1/entry_price - 1/exit_price)# SHORT: realized_pnl = closed_quantity * multiplier * (1/exit_price - 1/entry_price) The engine automatically applies the correct formula based on position side. Unrealized PnL​ Calculated using current market prices for open positions. The price parameter accepts any reference price (bid, ask, mid, last, or mark): position.unrealized_pnl(last_price) # Using last traded priceposition.unrealized_pnl(bid_price) # Conservative for LONG positionsposition.unrealized_pnl(ask_price) # Conservative for SHORT positions Total PnL​ Combines realized and unrealized components: total_pnl = position.total_pnl(current_price)# Returns realized_pnl + unrealized_pnl Currency considerations​ PnL is calculated in the instrument's settlement currency. For Forex, this is typically the quote currency. For inverse contracts, PnL may be in the base currency. Portfolio aggregates realized PnL per instrument in settlement currency. Multi-currency totals require conversion outside the Position class. Commissions and costs​ Positions track all trading costs: Commissions are accumulated by currency. Each fill's commission is added to the running total. Multiple commission currencies are supported. Realized PnL includes commissions only when denominated in the settlement currency. Other commissions are tracked separately and may require conversion. commissions = position.commissions()# Returns list[Money] with aggregated commission totals per currencynotional = position.notional_value(current_price)# Returns Money in quote currency (standard) or base currency (inverse) Limitations: Panics if inverse instrument has no base_currency set. Does not handle quanto contracts (returns quote currency instead of settlement currency). For quanto instruments, use instrument.calculate_notional_value() instead. Position properties and state​ Identifiers​ id: Unique position identifier. instrument_id: The traded instrument. account_id: Account where position is held. trader_id: The trader who owns the position. strategy_id: The strategy managing the position. opening_order_id: Client order ID that opened the position. closing_order_id: Client order ID that closed the position. Position state​ side: Current position side (LONG, SHORT, or FLAT). entry: Direction of the currently open position (Buy for LONG, Sell for SHORT). Updates when position flips direction. quantity: Current absolute position size. signed_qty: Signed position size (positive for LONG, negative for SHORT). peak_qty: Maximum quantity reached during position lifetime. is_open: Whether position is currently open. is_closed: Whether position is closed (FLAT). is_long: Whether position side is LONG. is_short: Whether position side is SHORT. Pricing and valuation​ avg_px_open: Average entry price. avg_px_close: Average exit price when closing. realized_pnl: Realized profit/loss. realized_return: Realized return as decimal (e.g., 0.05 for 5%). quote_currency: Quote currency of the instrument. base_currency: Base currency if applicable. settlement_currency: Currency for PnL settlement. Instrument specifications​ multiplier: Contract multiplier. price_precision: Decimal precision for prices. size_precision: Decimal precision for quantities. is_inverse: Whether instrument is inverse. Timestamps​ ts_init: When position was initialized. ts_opened: When position was opened. ts_last: Last update timestamp. ts_closed: When position was closed. duration_ns: Duration from open to close in nanoseconds. Associated data​ symbol: The instrument's ticker symbol. venue: The trading venue. client_order_ids: All client order IDs associated with position. venue_order_ids: All venue order IDs associated with position. trade_ids: All trade/fill IDs from venue. events: All order fill events applied to position. event_count: Total number of fill events applied. last_event: Most recent fill event. last_trade_id: Most recent trade ID. infoFor complete type information and detailed property documentation, see the Position API Reference. Events and tracking​ Positions maintain a complete history of events: All order fill events are stored chronologically. Associated client order IDs are tracked. Trade IDs from the venue are preserved. Event count indicates total fills applied. This historical data enables: Detailed position analysis. Trade reconciliation. Performance attribution. Audit trails. tipUse position.events to access the full history of fills for reconciliation. The position.trade_ids property helps match against broker statements. See the Execution guide for reconciliation best practices. Numerical precision​ Position calculations use 64-bit floating-point (f64) arithmetic for PnL and average price computations. While fixed-point types (Price, Quantity, Money) preserve exact precision at configured decimal places, internal calculations convert to f64 for performance and overflow safety. Design rationale​ The platform uses f64 for position calculations to balance performance and accuracy: Floating-point operations are significantly faster than arbitrary-precision arithmetic. Raw integer multiplication can overflow even with 128-bit integers. Each calculation starts from precise fixed-point values, avoiding cumulative error. IEEE-754 double precision provides 15 decimal digits of accuracy. Validated precision characteristics​ Testing confirms f64 arithmetic maintains accuracy for typical trading scenarios: Standard amounts: No precision loss for amounts ≥ 0.01 in standard currencies. High-precision instruments: 9-decimal crypto prices preserved within 1e-6 tolerance. Sequential fills: 100 fills show no drift (commission accuracy to 1e-10). Extreme prices: Handles range from 0.00001 to 99,999.99999 without overflow. Round-trip trades: Opening and closing at same price produces exact PnL (commissions only). For implementation details, see test_position_pnl_precision_* tests in crates/model/src/position.rs. noteFor regulatory compliance or audit trails requiring exact decimal arithmetic, consider using Decimal types from external libraries. Very small amounts below f64 epsilon (1e-15) may round to zero, though this does not affect realistic trading scenarios with standard currency precisions. Integration with other components​ Positions interact with several key components: Portfolio: Aggregates positions across instruments and strategies. ExecutionEngine: Creates and updates positions from fills. Cache: Stores position state and snapshots. RiskEngine: Monitors position limits and exposure. notePositions are not created for spread instruments. While contingent orders can still trigger for spreads, they operate without position linkage. The engine handles spread instruments separately from regular positions. Summary​ Positions are central to tracking trading activity and performance in NautilusTrader. Understanding how positions aggregate fills, calculate PnL, and handle different OMS configurations is essential for building robust trading strategies. The position snapshotting mechanism ensures accurate historical tracking in NETTING mode, while the comprehensive event history supports detailed analysis and reconciliation.

position_id

Pattern 3: # Example: Position aggregation# Initial BUY 100 units at $50signed_qty = +100 # LONG position# Subsequent SELL 150 units at $55signed_qty = -50 # Now SHORT position# Final BUY 50 units at $52signed_qty = 0 # Position FLAT (closed)

# Example: Position aggregation# Initial BUY 100 units at $50signed_qty = +100  # LONG position# Subsequent SELL 150 units at $55signed_qty = -50   # Now SHORT position# Final BUY 50 units at $52signed_qty = 0     # Position FLAT (closed)

Pattern 4: OKX Founded in 2017, OKX is a leading cryptocurrency exchange offering spot, perpetual swap, futures, and options trading. This integration supports live market data ingest and order execution on OKX. Overview​ This adapter is implemented in Rust, with optional Python bindings for ease of use in Python-based workflows. It does not require external OKX client libraries—the core components are compiled as a static library and linked automatically during the build. Examples​ You can find live example scripts here. Product support​ Product TypeData FeedTradingNotesSpot✓✓Use for index prices.Perpetual Swaps✓✓Linear and inverse contracts.Futures✓✓Specific expiration dates.Margin✓✓Spot trading with margin/leverage (spot margin).Options✓-Data feed supported, trading coming soon. noteOptions support: While you can subscribe to options market data and receive price updates, order execution for options is not yet implemented. You can use the symbology format shown above to subscribe to options data feeds. infoInstrument multipliers: For derivatives (SWAP, FUTURES, OPTIONS), instrument multipliers are calculated as the product of OKX's ctMult (contract multiplier) and ctVal (contract value) fields. This ensures position sizing accurately reflects both the contract size and value. The OKX adapter includes multiple components, which can be used separately or together depending on your use case. OKXHttpClient: Low-level HTTP API connectivity. OKXWebSocketClient: Low-level WebSocket API connectivity. OKXInstrumentProvider: Instrument parsing and loading functionality. OKXDataClient: Market data feed manager. OKXExecutionClient: Account management and trade execution gateway. OKXLiveDataClientFactory: Factory for OKX data clients (used by the trading node builder). OKXLiveExecClientFactory: Factory for OKX execution clients (used by the trading node builder). noteMost users will simply define a configuration for a live trading node (as shown below), and won’t need to work directly with these lower-level components. Symbology​ OKX uses specific symbol conventions for different instrument types. All instrument IDs should include the .OKX suffix when referencing them (e.g., BTC-USDT.OKX for spot Bitcoin). Symbol format by instrument type​ SPOT​ Format: {BaseCurrency}-{QuoteCurrency} Examples: BTC-USDT - Bitcoin against USDT (Tether) BTC-USDC - Bitcoin against USDC ETH-USDT - Ethereum against USDT SOL-USDT - Solana against USDT To subscribe to spot Bitcoin USD in your strategy: InstrumentId.from_str("BTC-USDT.OKX") # For USDT-quoted spotInstrumentId.from_str("BTC-USDC.OKX") # For USDC-quoted spot SWAP (Perpetual Futures)​ Format: {BaseCurrency}-{QuoteCurrency}-SWAP Examples: BTC-USDT-SWAP - Bitcoin perpetual swap (linear, USDT-margined) BTC-USD-SWAP - Bitcoin perpetual swap (inverse, coin-margined) ETH-USDT-SWAP - Ethereum perpetual swap (linear) ETH-USD-SWAP - Ethereum perpetual swap (inverse) Linear vs Inverse contracts: Linear (USDT-margined): Uses stablecoins like USDT as margin. Inverse (coin-margined): Uses the base cryptocurrency as margin. FUTURES (Dated Futures)​ Format: {BaseCurrency}-{QuoteCurrency}-{YYMMDD} Examples: BTC-USD-251226 - Bitcoin futures expiring December 26, 2025 ETH-USD-251226 - Ethereum futures expiring December 26, 2025 BTC-USD-250328 - Bitcoin futures expiring March 28, 2025 Note: Futures are typically inverse contracts (coin-margined). OPTIONS​ Format: {BaseCurrency}-{QuoteCurrency}-{YYMMDD}-{Strike}-{Type} Examples: BTC-USD-251226-100000-C - Bitcoin call option, $100,000 strike, expiring December 26, 2025 BTC-USD-251226-100000-P - Bitcoin put option, $100,000 strike, expiring December 26, 2025 ETH-USD-251226-4000-C - Ethereum call option, $4,000 strike, expiring December 26, 2025 Where: C = Call option P = Put option Common questions​ Q: How do I subscribe to spot Bitcoin USD? A: Use BTC-USDT.OKX for USDT-margined spot or BTC-USDC.OKX for USDC-margined spot. Q: What's the difference between BTC-USDT-SWAP and BTC-USD-SWAP? A: BTC-USDT-SWAP is a linear perpetual (USDT-margined), while BTC-USD-SWAP is an inverse perpetual (BTC-margined). Q: How do I know which contract type to use? A: Check the contract_types parameter in the configuration: For linear contracts: OKXContractType.LINEAR. For inverse contracts: OKXContractType.INVERSE. Orders capability​ Below are the order types, execution instructions, and time-in-force options supported for linear perpetual swap products on OKX. Client order ID requirements​ noteOKX has specific requirements for client order IDs: No hyphens allowed: OKX does not accept hyphens (-) in client order IDs. Maximum length: 32 characters. Allowed characters: alphanumeric characters and underscores only. When configuring your strategy, ensure you set:use_hyphens_in_client_order_ids=False Order types​ Order TypeLinear Perpetual SwapNotesMARKET✓Immediate execution at market price. Supports quote quantity.LIMIT✓Execution at specified price or better.STOP_MARKET✓Conditional market order (OKX algo order).STOP_LIMIT✓Conditional limit order (OKX algo order).MARKET_IF_TOUCHED✓Conditional market order (OKX algo order).LIMIT_IF_TOUCHED✓Conditional limit order (OKX algo order).TRAILING_STOP-Not yet supported. infoConditional orders: STOP_MARKET, STOP_LIMIT, MARKET_IF_TOUCHED, and LIMIT_IF_TOUCHED are implemented as OKX algo orders, providing advanced trigger capabilities with multiple price sources. Quantity semantics for spot margin trading​ When using spot margin trading (use_spot_margin=True), OKX interprets order quantities differently depending on the order side: Limit orders interpret quantity as the number of base currency units. Market SELL orders also use base-unit quantities. Market BUY orders interpret quantity as quote notional (e.g., USDT). warningWhen submitting spot margin market BUY orders, you must: Set quote_quantity=True on the order (or pre-compute the quote-denominated amount). Configure the execution engine with convert_quote_qty_to_base=False so the quote amount reaches the adapter unchanged. The OKX execution client will deny base-denominated market buy orders for spot margin to prevent unintended fills.On the first fill, the order quantity will be automatically updated from the quote quantity to the actual base quantity received, reflecting the executed trade. from nautilus_trader.execution.config import ExecEngineConfigfrom nautilus_trader.execution.engine import ExecutionEngine# Disable automatic conversion for quote quantitiesconfig = ExecEngineConfig(convert_quote_qty_to_base=False)engine = ExecutionEngine(msgbus=msgbus, cache=cache, clock=clock, config=config)# Correct: Spot margin market BUY with quote quantity (spend $100 USDT)order = strategy.order_factory.market( instrument_id=instrument_id, order_side=OrderSide.BUY, quantity=instrument.make_qty(100.0), quote_quantity=True, # Interpret as USDT notional)strategy.submit_order(order) Execution instructions​ InstructionLinear Perpetual SwapNotespost_only✓Only for LIMIT orders.reduce_only✓Only for derivatives. Time in force​ Time in forceLinear Perpetual SwapNotesGTC✓Good Till Canceled.FOK✓Fill or Kill.IOC✓Immediate or Cancel.GTD✗Not supported by OKX API. noteGTD (Good Till Date) time in force: OKX does not support native GTD functionality through their API.If you need GTD functionality, you must use Nautilus's strategy-managed GTD feature, which will handle the order expiration by canceling the order at the specified expiry time. Batch operations​ OperationLinear Perpetual SwapNotesBatch Submit✓Submit multiple orders in single request.Batch Modify✓Modify multiple orders in single request.Batch Cancel✓Cancel multiple orders in single request. Position management​ FeatureLinear Perpetual SwapNotesQuery positions✓Real-time position updates.Position mode✓Net vs Long/Short mode (see below).Leverage control✓Dynamic leverage adjustment per instrument.Margin mode✓Supports cash, isolated, cross, spot_isolated modes. Position modes​ OKX supports two position modes for derivatives trading: Net mode (Netting): Single position per instrument that can be positive (LONG) or negative (SHORT). Buy and sell orders net against each other. This is the default and recommended for most traders. Long/Short mode (Hedging): Separate long and short positions for the same instrument. Allows simultaneous long and short positions, useful for hedging strategies. notePosition mode must be configured via the OKX Web/App interface and applies account-wide. The adapter automatically detects the current position mode and handles position reporting accordingly. Trade modes and margin configuration​ OKX's unified account system supports different trade modes for spot and derivatives trading. The adapter automatically determines the correct trade mode based on your configuration and instrument type. noteImportant: Account modes must be initially configured via the OKX Web/App interface. The API cannot set the account mode for the first time. For more details on OKX's account modes and margin system, see the OKX Account Mode documentation. Trade modes overview​ OKX supports four trade modes, which the adapter selects automatically based on your configuration: ModeUsed ForLeverageBorrowingConfigurationcashSimple spot trading--use_spot_margin=False (default for SPOT)spot_isolatedSpot trading with margin/leverage✓✓use_spot_margin=TrueisolatedDerivatives trading (SWAP/FUTURES/OPTIONS)✓✓margin_mode=ISOLATED or unset (default for derivatives)crossDerivatives with shared margin pool✓✓margin_mode=CROSS Configuration-based trade mode selection​ The adapter automatically selects the correct trade mode based on: Instrument type (SPOT vs derivatives) Configuration settings (use_spot_margin for SPOT, margin_mode for derivatives) For SPOT trading​ # Simple SPOT trading without leverage (uses 'cash' mode)exec_clients={ OKX: OKXExecClientConfig( instrument_types=(OKXInstrumentType.SPOT,), use_spot_margin=False, # Default - simple SPOT # ... other config ),}# SPOT trading WITH margin/leverage (uses 'spot_isolated' mode)exec_clients={ OKX: OKXExecClientConfig( instrument_types=(OKXInstrumentType.SPOT,), use_spot_margin=True, # Enable margin trading for SPOT # ... other config ),} For derivatives trading (SWAP/FUTURES/OPTIONS)​ # Derivatives with isolated margin (default - uses 'isolated' mode)exec_clients={ OKX: OKXExecClientConfig( instrument_types=(OKXInstrumentType.SWAP,), margin_mode=OKXMarginMode.ISOLATED, # Or omit - ISOLATED is default # ... other config ),}# Derivatives with cross margin (uses 'cross' mode)exec_clients={ OKX: OKXExecClientConfig( instrument_types=(OKXInstrumentType.SWAP,), margin_mode=OKXMarginMode.CROSS, # Share margin across all positions # ... other config ),} For mixed SPOT and derivatives trading​ When trading both SPOT and derivatives instruments simultaneously, the adapter automatically determines the correct trade mode per-order based on the instrument being traded: # Mixed SPOT + SWAP configurationexec_clients={ OKX: OKXExecClientConfig( instrument_types=(OKXInstrumentType.SPOT, OKXInstrumentType.SWAP), use_spot_margin=True, # Applies to SPOT orders only margin_mode=OKXMarginMode.CROSS, # Applies to SWAP orders only # ... other config ),} How it works: SPOT orders → Uses spot_isolated mode (because use_spot_margin=True) SWAP orders → Uses cross mode (because margin_mode=CROSS) Each order automatically gets the correct tdMode based on its instrument type No manual intervention required This enables strategies that trade across multiple instrument types with different margin configurations, such as: Spot-futures arbitrage strategies Delta-neutral strategies combining spot and perpetual swaps Market making across spot and derivatives markets warningManual trade mode override: While you can still manually override the trade mode per order using params={"td_mode": "..."}, this is not recommended as it bypasses automatic mode selection and can lead to order rejection if the wrong mode is specified for the instrument type (e.g., using isolated for SPOT instruments).Only use manual override if you have specific requirements that cannot be met through configuration. Benefits of configuration-based approach​ Type-safe: Configuration is validated at startup before placing any orders. Automatic: System chooses correct mode based on instrument type and intent. Clear: Field names explain purpose (use_spot_margin vs obscure td_mode parameter). Safe: Impossible to use incompatible combinations (e.g., isolated mode for SPOT). Backwards compatible: Default values maintain existing behavior. Order querying​ FeatureLinear Perpetual SwapNotesQuery open orders✓List all active orders.Query order history✓Historical order data.Order status updates✓Real-time order state changes.Trade history✓Execution and fill reports. Contingent orders​ FeatureLinear Perpetual SwapNotesOrder lists-Not supported.OCO orders✓One-Cancels-Other orders.Bracket orders✓Stop loss + take profit combinations.Conditional orders✓Stop and limit-if-touched orders. Conditional order architecture​ Conditional orders (OKX algo orders) use a hybrid architecture for optimal performance and reliability: Submission: Via HTTP REST API (/api/v5/trade/order-algo) Status updates: Via WebSocket business endpoint (/ws/v5/business) on the orders-algo channel Cancellation: Via HTTP REST API using algo order ID tracking This design ensures: Immediate submission acknowledgment through HTTP. Real-time status updates through WebSocket. Proper order lifecycle management with algo order ID mapping. Supported conditional order types​ Order TypeTrigger TypesNotesSTOP_MARKETLast, Mark, IndexMarket execution when triggered.STOP_LIMITLast, Mark, IndexLimit order placement when triggered.MARKET_IF_TOUCHEDLast, Mark, IndexMarket execution when price touched.LIMIT_IF_TOUCHEDLast, Mark, IndexLimit order placement when price touched. Trigger price types​ Conditional orders support different trigger price sources: Last Price (TriggerType.LAST_PRICE): Uses the last traded price (default). Mark Price (TriggerType.MARK_PRICE): Uses the mark price (recommended for derivatives). Index Price (TriggerType.INDEX_PRICE): Uses the underlying index price. # Example: Stop loss using mark price triggerstop_order = order_factory.stop_market( instrument_id=instrument_id, order_side=OrderSide.SELL, quantity=Quantity.from_str("0.1"), trigger_price=Price.from_str("45000.0"), trigger_type=TriggerType.MARK_PRICE, # Use mark price for trigger)strategy.submit_order(stop_order) Risk management​ Liquidation and ADL event handling​ The OKX adapter automatically detects and handles exchange-initiated risk management events: Liquidation orders: When a position is liquidated by the exchange (full or partial), the adapter detects the liquidation category and logs warnings with order details. These orders are processed normally through the order and fill pipeline. Auto-Deleveraging (ADL): When your position is closed by the exchange to offset a counterparty's liquidation, the adapter detects and logs the ADL event with position details. infoLiquidation and ADL events are logged at WARNING level with details including order ID, instrument, and state. Monitor your logs for these events as part of your risk management process.The adapter handles these exchange-generated orders seamlessly, generating appropriate OrderFilled events and updating positions accordingly. No special handling is required in your strategy code. Authentication​ To use the OKX adapter, you'll need to create API credentials in your OKX account: Log into your OKX account and navigate to the API management page. Create a new API key with the required permissions for trading and data access. Note down your API key, secret key, and passphrase. You can provide these credentials through environment variables: export OKX_API_KEY="your_api_key"export OKX_API_SECRET="your_api_secret"export OKX_API_PASSPHRASE="your_passphrase" Or pass them directly in the configuration (not recommended for production). Demo trading​ OKX provides a demo trading environment for testing strategies without real funds. To use demo mode, set is_demo=True in your client configuration: config = TradingNodeConfig( data_clients={ OKX: OKXDataClientConfig( is_demo=True, # Enable demo mode # ... other config ), }, exec_clients={ OKX: OKXExecClientConfig( is_demo=True, # Enable demo mode # ... other config ), },) When demo mode is enabled: REST API requests include the x-simulated-trading: 1 header. WebSocket connections use demo endpoints (wspap.okx.com). The same API credentials are used as production. noteYou must use API keys created specifically for demo trading. Production API keys will not work in demo mode. Rate limiting​ The adapter enforces OKX’s per-endpoint quotas while keeping sensible defaults for both REST and WebSocket calls. REST limits​ Global cap: 250 requests per second (matches 500 requests / 2 seconds IP allowance). Endpoint-specific quotas appear in the table below and mirror OKX’s published limits where available. WebSocket limits​ Connection establishment: 3 requests per second (per IP). Subscription operations (subscribe/unsubscribe/login): 480 requests per hour per connection. Order actions (place/cancel/amend): 250 requests per second. warningOKX enforces per-endpoint and per-account quotas; exceeding them leads to HTTP 429 responses and temporary throttling on that key. Key / EndpointLimit (req/sec)Notesokx:global250Matches 500 req / 2 s IP allowance./api/v5/public/instruments10Matches OKX 20 req / 2 s docs./api/v5/market/candles50Higher allowance for streaming candles./api/v5/market/history-candles20Conservative quota for large historical pulls./api/v5/market/history-trades30Trade history pulls./api/v5/account/balance5OKX guidance: 10 req / 2 s./api/v5/trade/order3060 requests / 2 seconds per-instrument limit./api/v5/trade/orders-pending20Open order fetch./api/v5/trade/orders-history20Historical orders./api/v5/trade/fills30Execution reports./api/v5/trade/order-algo10Algo placements (conditional orders)./api/v5/trade/cancel-algos10Algo cancellation. All keys automatically include the okx:global bucket. URLs are normalised (query strings removed) before rate limiting, so requests with different filters share the same quota. infoFor more details on rate limiting, see the official documentation: https://www.okx.com/docs-v5/en/#rest-api-rate-limit. Configuration​ Configuration options​ The OKX data client provides the following configuration options: Data client​ OptionDefaultDescriptioninstrument_types(OKXInstrumentType.SPOT,)Controls which OKX instrument families are loaded (spot, swap, futures, options).contract_typesNoneRestricts loading to specific contract styles when combined with instrument_types.instrument_familiesNoneInstrument families to load (e.g., "BTC-USD", "ETH-USD"). Required for OPTIONS. Optional for FUTURES/SWAP. Not applicable for SPOT/MARGIN.base_url_httpNoneOverride for the OKX REST endpoint; defaults to the production URL resolved at runtime.base_url_wsNoneOverride for the market data WebSocket endpoint.api_keyNoneFalls back to OKX_API_KEY environment variable when unset.api_secretNoneFalls back to OKX_API_SECRET environment variable when unset.api_passphraseNoneFalls back to OKX_PASSPHRASE environment variable when unset.is_demoFalseConnects to the OKX demo environment when True.http_timeout_secs60Request timeout (seconds) for REST market data calls.max_retries3Maximum retry attempts for recoverable REST errors.retry_delay_initial_ms1,000Initial delay (milliseconds) before retrying a failed request.retry_delay_max_ms10,000Upper bound for exponential backoff delay between retries.update_instruments_interval_mins60Interval, in minutes, between background instrument refreshes.vip_levelNoneEnables higher-depth order book channels when set to the matching OKX VIP tier. The OKX execution client provides the following configuration options: Execution client​ OptionDefaultDescriptioninstrument_types(OKXInstrumentType.SPOT,)Instrument families that should be tradable for this client.contract_typesNoneRestricts tradable contracts (linear, inverse, options) when paired with instrument_types.instrument_familiesNoneInstrument families to load (e.g., "BTC-USD", "ETH-USD"). Required for OPTIONS. Optional for FUTURES/SWAP. Not applicable for SPOT/MARGIN.base_url_httpNoneOverride for the OKX trading REST endpoint.base_url_wsNoneOverride for the private WebSocket endpoint.api_keyNoneFalls back to OKX_API_KEY environment variable when unset.api_secretNoneFalls back to OKX_API_SECRET environment variable when unset.api_passphraseNoneFalls back to OKX_PASSPHRASE environment variable when unset.margin_modeNoneMargin mode for derivatives trading (ISOLATED or CROSS). Only applies to SWAP/FUTURES/OPTIONS. Defaults to ISOLATED if not specified.use_spot_marginFalseEnables margin/leverage for SPOT trading. When True, uses spot_isolated trade mode. When False, uses cash trade mode (no leverage). Only applies to SPOT instruments.is_demoFalseConnects to the OKX demo trading environment.http_timeout_secs60Request timeout (seconds) for REST trading calls.use_fills_channelFalseSubscribes to the dedicated fills channel (VIP5+ required) for lower-latency fill reports.use_mm_mass_cancelFalseUses the market-maker bulk cancel endpoint when available; otherwise falls back to per-order cancels.max_retries3Maximum retry attempts for recoverable REST errors.retry_delay_initial_ms1,000Initial delay (milliseconds) applied before retrying a failed request.retry_delay_max_ms10,000Upper bound for the exponential backoff delay between retries. Below is an example configuration for a live trading node using OKX data and execution clients: from nautilus_trader.adapters.okx import OKXfrom nautilus_trader.adapters.okx import OKXDataClientConfig, OKXExecClientConfigfrom nautilus_trader.adapters.okx.factories import OKXLiveDataClientFactory, OKXLiveExecClientFactoryfrom nautilus_trader.config import InstrumentProviderConfig, LiveExecEngineConfig, LoggingConfig, TradingNodeConfigfrom nautilus_trader.core.nautilus_pyo3 import OKXContractTypefrom nautilus_trader.core.nautilus_pyo3 import OKXInstrumentTypefrom nautilus_trader.core.nautilus_pyo3 import OKXMarginModefrom nautilus_trader.live.node import TradingNodeconfig = TradingNodeConfig( ..., data_clients={ OKX: OKXDataClientConfig( api_key=None, # Will use OKX_API_KEY env var api_secret=None, # Will use OKX_API_SECRET env var api_passphrase=None, # Will use OKX_API_PASSPHRASE env var base_url_http=None, instrument_provider=InstrumentProviderConfig(load_all=True), instrument_types=(OKXInstrumentType.SWAP,), contract_types=(OKXContractType.LINEAR,), is_demo=False, ), }, exec_clients={ OKX: OKXExecClientConfig( api_key=None, api_secret=None, api_passphrase=None, base_url_http=None, base_url_ws=None, instrument_provider=InstrumentProviderConfig(load_all=True), instrument_types=(OKXInstrumentType.SWAP,), contract_types=(OKXContractType.LINEAR,), is_demo=False, ), },)node = TradingNode(config=config)node.add_data_client_factory(OKX, OKXLiveDataClientFactory)node.add_exec_client_factory(OKX, OKXLiveExecClientFactory)node.build() infoFor additional features or to contribute to the OKX adapter, please see our contributing guide.

ctMult

Pattern 5: Tardis Tardis provides granular data for cryptocurrency markets including tick-by-tick order book snapshots & updates, trades, open interest, funding rates, options chains and liquidations data for leading crypto exchanges. NautilusTrader provides an integration with the Tardis API and data formats, enabling seamless access. The capabilities of this adapter include: TardisCSVDataLoader: Reads Tardis-format CSV files and converts them into Nautilus data, with support for both bulk loading and memory-efficient streaming. TardisMachineClient: Supports live streaming and historical replay of data from the Tardis Machine WebSocket server - converting messages into Nautilus data. TardisHttpClient: Requests instrument definition metadata from the Tardis HTTP API, parsing it into Nautilus instrument definitions. TardisDataClient: Provides a live data client for subscribing to data streams from a Tardis Machine WebSocket server. TardisInstrumentProvider: Provides instrument definitions from Tardis through the HTTP instrument metadata API. Data pipeline functions: Enables replay of historical data from Tardis Machine and writes it to the Nautilus Parquet format, including direct catalog integration for streamlined data management (see below). infoA Tardis API key is required for the adapter to operate correctly. See also environment variables. Overview​ This adapter is implemented in Rust, with optional Python bindings for ease of use in Python-based workflows. It does not require any external Tardis client library dependencies. infoThere is no need for additional installation steps for tardis. The core components of the adapter are compiled as static libraries and automatically linked during the build process. Tardis documentation​ Tardis provides extensive user documentation. We recommend also referring to the Tardis documentation in conjunction with this NautilusTrader integration guide. Supported formats​ Tardis provides normalized market data—a unified format consistent across all supported exchanges. This normalization is highly valuable because it allows a single parser to handle data from any Tardis-supported exchange, reducing development time and complexity. As a result, NautilusTrader will not support exchange-native market data formats, as it would be inefficient to implement separate parsers for each exchange at this stage. The following normalized Tardis formats are supported by NautilusTrader: Tardis formatNautilus data typebook_changeOrderBookDeltabook_snapshot_*OrderBookDepth10quoteQuoteTickquote_10sQuoteTicktradeTradetrade_bar_BarinstrumentCurrencyPair, CryptoFuture, CryptoPerpetual, OptionContractderivative_tickerNot yet supporteddisconnectNot applicable Notes: quote is an alias for book_snapshot_1_0ms. quote_10s is an alias for book_snapshot_1_10s. Both quote, quote_10s, and one-level snapshots are parsed as QuoteTick. infoSee also the Tardis normalized market data APIs. Bars​ The adapter will automatically convert Tardis trade bar interval and suffix to Nautilus BarTypes. This includes the following: Tardis suffixNautilus bar aggregationms - millisecondsMILLISECONDs - secondsSECONDm - minutesMINUTEticks - number of ticksTICKvol - volume sizeVOLUME Symbology and normalization​ The Tardis integration ensures seamless compatibility with NautilusTrader’s crypto exchange adapters by consistently normalizing symbols. Typically, NautilusTrader uses the native exchange naming conventions provided by Tardis. However, for certain exchanges, raw symbols are adjusted to adhere to the Nautilus symbology normalization, as outlined below: Common rules​ All symbols are converted to uppercase. Market type suffixes are appended with a hyphen for some exchanges (see exchange-specific normalizations). Original exchange symbols are preserved in the Nautilus instrument definitions raw_symbol field. Exchange-specific normalizations​ Binance: Nautilus appends the suffix -PERP to all perpetual symbols. Bybit: Nautilus uses specific product category suffixes, including -SPOT, -LINEAR, -INVERSE, -OPTION. dYdX: Nautilus appends the suffix -PERP to all perpetual symbols. Gate.io: Nautilus appends the suffix -PERP to all perpetual symbols. For detailed symbology documentation per exchange: Binance symbology Bybit symbology dYdX symbology Venues​ Some exchanges on Tardis are partitioned into multiple venues. The table below outlines the mappings between Nautilus venues and corresponding Tardis exchanges, as well as the exchanges that Tardis supports: Nautilus venueTardis exchange(s)ASCENDEXascendexBINANCEbinance, binance-dex, binance-european-options, binance-futures, binance-jersey, binance-optionsBINANCE_DELIVERYbinance-delivery (COIN-margined contracts)BINANCE_USbinance-usBITFINEXbitfinex, bitfinex-derivativesBITFLYERbitflyerBITGETbitget, bitget-futuresBITMEXbitmexBITNOMIALbitnomialBITSTAMPbitstampBLOCKCHAIN_COMblockchain-comBYBITbybit, bybit-options, bybit-spotCOINBASEcoinbaseCOINBASE_INTXcoinbase-internationalCOINFLEXcoinflex (for historical research)CRYPTO_COMcrypto-com, crypto-com-derivativesCRYPTOFACILITIEScryptofacilitiesDELTAdeltaDERIBITderibitDYDXdydxDYDX_V4dydx-v4FTXftx, ftx-us (historical research)GATE_IOgate-io, gate-io-futuresGEMINIgeminiHITBTChitbtcHUOBIhuobi, huobi-dm, huobi-dm-linear-swap, huobi-dm-optionsHUOBI_DELIVERYhuobi-dm-swapHYPERLIQUIDhyperliquidKRAKENkrakenKUCOINkucoin, kucoin-futuresMANGOmangoOKCOINokcoinOKEXokex, okex-futures, okex-options, okex-spreads, okex-swapPHEMEXphemexPOLONIEXpoloniexSERUMserum (historical research)STAR_ATLASstar-atlasUPBITupbitWOO_Xwoo-x Environment variables​ The following environment variables are used by Tardis and NautilusTrader. TM_API_KEY: API key for the Tardis Machine. TARDIS_API_KEY: API key for NautilusTrader Tardis clients. TARDIS_MACHINE_WS_URL (optional): WebSocket URL for the TardisMachineClient in NautilusTrader. TARDIS_BASE_URL (optional): Base URL for the TardisHttpClient in NautilusTrader. NAUTILUS_PATH (optional): Parent directory containing the catalog/ subdirectory for writing replay data in the Nautilus catalog format. Running Tardis Machine historical replays​ The Tardis Machine Server is a locally runnable server with built-in data caching, providing both tick-level historical and consolidated real-time cryptocurrency market data through HTTP and WebSocket APIs. You can perform complete Tardis Machine WebSocket replays of historical data and output the results in Nautilus Parquet format, using either Python or Rust. Since the function is implemented in Rust, performance is consistent whether run from Python or Rust, letting you choose based on your preferred workflow. The end-to-end run_tardis_machine_replay data pipeline function utilizes a specified configuration to execute the following steps: Connect to the Tardis Machine server. Request and parse all necessary instrument definitions from the Tardis instruments metadata HTTP API. Stream all requested instruments and data types for the specified time ranges from the Tardis Machine server. For each instrument, data type and date (UTC), generate a .parquet file in the catalog-compatible format. Disconnect from the Tardis Machine server, and terminate the program. File Naming Convention Files are written one per day, per instrument, using ISO 8601 timestamp ranges that clearly indicate the exact time span of data: Format: {start_timestamp}{end_timestamp}.parquet Example: 2023-10-01T00-00-00-000000000Z_2023-10-01T23-59-59-999999999Z.parquet Structure: data/{data_type}/{instrument_id}/{filename} This format is fully compatible with the Nautilus data catalog, enabling seamless querying, consolidation, and data management operations. noteYou can request data for the first day of each month without an API key. For all other dates, a Tardis Machine API key is required. This process is optimized for direct output to a Nautilus Parquet data catalog. Ensure that the NAUTILUS_PATH environment variable is set to the parent directory containing the catalog/ subdirectory. Parquet files will then be organized under /catalog/data/ in the expected subdirectories corresponding to data type and instrument. If no output_path is specified in the configuration file and the NAUTILUS_PATH environment variable is unset, the system will default to the current working directory. Procedure​ First, ensure the tardis-machine docker container is running. Use the following command: docker run -p 8000:8000 -p 8001:8001 -e "TM_API_KEY=YOUR_API_KEY" -d tardisdev/tardis-machine This command starts the tardis-machine server without a persistent local cache, which may affect performance. For improved performance, consider running the server with a persistent volume. Refer to the Tardis Docker documentation for details. Configuration​ Next, ensure you have a configuration JSON file available. Configuration JSON format FieldTypeDescriptionDefaulttardis_ws_urlstring (optional)The Tardis Machine WebSocket URL.If null then will use the TARDIS_MACHINE_WS_URL env var.normalize_symbolsbool (optional)If Nautilus symbol normalization should be applied.If null then will default to true.output_pathstring (optional)The output directory path to write Nautilus Parquet data to.If null then will use the NAUTILUS_PATH env var, otherwise the current working directory.optionsJSON[]An array of ReplayNormalizedRequestOptions objects. An example configuration file, example_config.json, is available here: { "tardis_ws_url": "ws://localhost:8001", "output_path": null, "options": [ { "exchange": "bitmex", "symbols": [ "xbtusd", "ethusd" ], "data_types": [ "trade" ], "from": "2019-10-01", "to": "2019-10-02" } ]} Python Replays​ To run a replay in Python, create a script similar to the following: import asynciofrom nautilus_trader.core import nautilus_pyo3async def run(): config_filepath = Path("YOUR_CONFIG_FILEPATH") await nautilus_pyo3.run_tardis_machine_replay(str(config_filepath.resolve()))if name == "main": asyncio.run(run()) Rust Replays​ To run a replay in Rust, create a binary similar to the following: use std::{env, path::PathBuf};use nautilus_adapters::tardis::replay::run_tardis_machine_replay_from_config;#[tokio::main]async fn main() { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init(); let config_filepath = PathBuf::from("YOUR_CONFIG_FILEPATH"); run_tardis_machine_replay_from_config(&config_filepath).await;} Make sure to enable Rust logging by exporting the following environment variable: export RUST_LOG=debug A working example binary can be found here. This can also be run using cargo: cargo run --bin tardis-replay Loading Tardis CSV data​ Tardis-format CSV data can be loaded using either Python or Rust. The loader reads the CSV text data from disk and parses it into Nautilus data. Since the loader is implemented in Rust, performance remains consistent regardless of whether you run it from Python or Rust, allowing you to choose based on your preferred workflow. You can also optionally specify a limit parameter for the load functions/methods to control the maximum number of rows loaded. noteLoading mixed-instrument CSV files is challenging due to precision requirements and is not recommended. Use single-instrument CSV files instead (see below). Loading CSV Data in Python​ You can load Tardis-format CSV data in Python using the TardisCSVDataLoader. When loading data, you can optionally specify the instrument ID but must specify both the price precision, and size precision. Providing the instrument ID improves loading performance, while specifying the precisions is required, as they cannot be inferred from the text data alone. To load the data, create a script similar to the following: from nautilus_trader.adapters.tardis import TardisCSVDataLoaderfrom nautilus_trader.model import InstrumentIdinstrument_id = InstrumentId.from_str("BTC-PERPETUAL.DERIBIT")loader = TardisCSVDataLoader( price_precision=1, size_precision=0, instrument_id=instrument_id,)filepath = Path("YOUR_CSV_DATA_PATH")limit = Nonedeltas = loader.load_deltas(filepath, limit) Loading CSV Data in Rust​ You can load Tardis-format CSV data in Rust using the loading functions found here. When loading data, you can optionally specify the instrument ID but must specify both the price precision and size precision. Providing the instrument ID improves loading performance, while specifying the precisions is required, as they cannot be inferred from the text data alone. For a complete example, see the example binary here. To load the data, you can use code similar to the following: use std::path::Path;use nautilus_adapters::tardis;use nautilus_model::identifiers::InstrumentId;#[tokio::main]async fn main() { // You must specify precisions and the CSV filepath let price_precision = 1; let size_precision = 0; let filepath = Path::new("YOUR_CSV_DATA_PATH"); // Optionally specify an instrument ID and/or limit let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT"); let limit = None; // Consider propagating any parsing error depending on your workflow let _deltas = tardis::csv::load_deltas( filepath, price_precision, size_precision, Some(instrument_id), limit, ) .unwrap();} Streaming Tardis CSV Data​ For memory-efficient processing of large CSV files, the Tardis integration provides streaming capabilities that load and process data in configurable chunks rather than loading entire files into memory at once. This is particularly useful for processing multi-gigabyte CSV files without exhausting system memory. The streaming functionality is available for all supported Tardis data types: Order book deltas (stream_deltas). Quote ticks (stream_quotes). Trade ticks (stream_trades). Order book depth snapshots (stream_depth10). Streaming CSV Data in Python​ The TardisCSVDataLoader provides streaming methods that yield chunks of data as iterators. Each method accepts a chunk_size parameter that controls how many records are read from the CSV file per chunk: from nautilus_trader.adapters.tardis import TardisCSVDataLoaderfrom nautilus_trader.model import InstrumentIdinstrument_id = InstrumentId.from_str("BTC-PERPETUAL.DERIBIT")loader = TardisCSVDataLoader( price_precision=1, size_precision=0, instrument_id=instrument_id,)filepath = Path("large_trades_file.csv")chunk_size = 100_000 # Process 100,000 records per chunk (default)# Stream trade ticks in chunksfor chunk in loader.stream_trades(filepath, chunk_size): print(f"Processing chunk with {len(chunk)} trades") # Process each chunk - only this chunk is in memory for trade in chunk: # Your processing logic here pass Streaming Order Book Data​ For order book data, streaming is available for both deltas and depth snapshots: # Stream order book deltasfor chunk in loader.stream_deltas(filepath): print(f"Processing {len(chunk)} deltas") # Process delta chunk# Stream depth10 snapshots (specify levels: 5 or 25)for chunk in loader.stream_depth10(filepath, levels=5): print(f"Processing {len(chunk)} depth snapshots") # Process depth chunk Streaming Quote Data​ Quote data can be streamed similarly: # Stream quote ticksfor chunk in loader.stream_quotes(filepath): print(f"Processing {len(chunk)} quotes") # Process quote chunk Memory Efficiency Benefits​ The streaming approach provides significant memory efficiency advantages: Controlled Memory Usage: Only one chunk is loaded in memory at a time. Scalable Processing: Can process files larger than available RAM. Configurable Chunk Sizes: Tune chunk_size based on your system's memory and performance requirements (default 100,000). warningWhen using streaming with precision inference (not providing explicit precisions), the inferred precision may differ from bulk loading the entire file. This is because precision inference works within chunk boundaries, and different chunks may contain values with different precision requirements. For deterministic precision behavior, provide explicit price_precision and size_precision parameters when calling streaming methods. Streaming CSV Data in Rust​ The underlying streaming functionality is implemented in Rust and can be used directly: use std::path::Path;use nautilus_adapters::tardis::csv::{stream_trades, stream_deltas};use nautilus_model::identifiers::InstrumentId;#[tokio::main]async fn main() { let filepath = Path::new("large_trades_file.csv"); let chunk_size = 100_000; let price_precision = Some(1); let size_precision = Some(0); let instrument_id = Some(InstrumentId::from("BTC-PERPETUAL.DERIBIT")); // Stream trades in chunks let stream = stream_trades( filepath, chunk_size, price_precision, size_precision, instrument_id, ).unwrap(); for chunk_result in stream { match chunk_result { Ok(chunk) => { println!("Processing chunk with {} trades", chunk.len()); // Process chunk } Err(e) => { eprintln!("Error processing chunk: {}", e); break; } } }} Requesting instrument definitions​ You can request instrument definitions in both Python and Rust using the TardisHttpClient. This client interacts with the Tardis instruments metadata API to request and parse instrument metadata into Nautilus instruments. The TardisHttpClient constructor accepts optional parameters for api_key, base_url, and timeout_secs (default is 60 seconds). The client provides methods to retrieve either a specific instrument, or all instruments available on a particular exchange. Ensure that you use Tardis’s lower-kebab casing convention when referring to a Tardis-supported exchange. noteA Tardis API key is required to access the instruments metadata API. Requesting Instruments in Python​ To request instrument definitions in Python, create a script similar to the following: import asynciofrom nautilus_trader.core import nautilus_pyo3async def run(): http_client = nautilus_pyo3.TardisHttpClient() instrument = await http_client.instrument("bitmex", "xbtusd") print(f"Received: {instrument}") instruments = await http_client.instruments("bitmex") print(f"Received: {len(instruments)} instruments")if name == "main": asyncio.run(run()) Requesting Instruments in Rust​ To request instrument definitions in Rust, use code similar to the following. For a complete example, see the example binary here. use nautilus_adapters::tardis::{enums::Exchange, http::client::TardisHttpClient};#[tokio::main]async fn main() { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init(); let client = TardisHttpClient::new(None, None, None).unwrap(); // Nautilus instrument definitions let resp = client.instruments(Exchange::Bitmex).await; println!("Received: {resp:?}"); let resp = client.instrument(Exchange::Bitmex, "ETHUSDT").await; println!("Received: {resp:?}");} Instrument provider​ The TardisInstrumentProvider requests and parses instrument definitions from Tardis through the HTTP instrument metadata API. Since there are multiple Tardis-supported exchanges, when loading all instruments, you must filter for the desired venues using an InstrumentProviderConfig: from nautilus_trader.config import InstrumentProviderConfig# See supported venues https://nautilustrader.io/docs/nightly/integrations/tardis#venuesvenues = {"BINANCE", "BYBIT"}filters = {"venues": frozenset(venues)}instrument_provider_config = InstrumentProviderConfig(load_all=True, filters=filters) You can also load specific instrument definitions in the usual way: from nautilus_trader.config import InstrumentProviderConfiginstrument_ids = [ InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), # Will use the 'binance-futures' exchange InstrumentId.from_str("BTCUSDT.BINANCE"), # Will use the 'binance' exchange]instrument_provider_config = InstrumentProviderConfig(load_ids=instrument_ids) Option exchange filtering​ The instrument provider automatically filters out option-specific exchanges (such as binance-options, binance-european-options, bybit-options, okex-options, and huobi-dm-options) when the instrument_type filter is not provided or does not include "option". To explicitly load option instruments, include "option" in the instrument_type filter: from nautilus_trader.config import InstrumentProviderConfigvenues = {"BINANCE", "BYBIT"}filters = { "venues": frozenset(venues), "instrument_type": {"option"}, # Explicitly request options}instrument_provider_config = InstrumentProviderConfig(load_all=True, filters=filters) This filtering mechanism prevents unnecessary API calls to option exchanges when they are not needed, improving performance and reducing API usage. noteInstruments must be available in the cache for all subscriptions. For simplicity, it's recommended to load all instruments for the venues you intend to subscribe to. Live data client​ The TardisDataClient enables integration of a Tardis Machine with a running NautilusTrader system. It supports subscriptions to the following data types: OrderBookDelta (L2 granularity from Tardis, includes all changes or full-depth snapshots) OrderBookDepth10 (L2 granularity from Tardis, provides snapshots up to 10 levels) QuoteTick TradeTick Bar (trade bars with Tardis-supported bar aggregations) Data WebSockets​ The main TardisMachineClient data WebSocket manages all stream subscriptions received during the initial connection phase, up to the duration specified by ws_connection_delay_secs. For any additional subscriptions made after this period, a new TardisMachineClient is created. This approach optimizes performance by allowing the main WebSocket to handle potentially hundreds of subscriptions in a single stream if they are provided at startup. When an initial subscription delay is set with ws_connection_delay_secs, unsubscribing from any of these streams will not actually remove the subscription from the Tardis Machine stream, as selective unsubscription is not supported by Tardis. However, the component will still unsubscribe from message bus publishing as expected. All subscriptions made after any initial delay will behave normally, fully unsubscribing from the Tardis Machine stream when requested. tipIf you anticipate frequent subscription and unsubscription of data, it is recommended to set ws_connection_delay_secs to zero. This will create a new client for each initial subscription, allowing them to be later closed individually upon unsubscription. Limitations and considerations​ The following limitations and considerations are currently known: Historical data requests are not supported, as each would require a minimum one-day replay from the Tardis Machine, potentially with a filter. This approach is neither practical nor efficient. infoFor additional features or to contribute to the Tardis adapter, please see our contributing guide.

TardisCSVDataLoader

Pattern 6: Rust Style Guide The Rust programming language is an ideal fit for implementing the mission-critical core of the platform and systems. Its strong type system, ownership model, and compile-time checks eliminate memory errors and data races by construction, while zero-cost abstractions and the absence of a garbage collector deliver C-like performance—critical for high-frequency trading workloads. Cargo manifest conventions​ In [dependencies], list internal crates (nautilus-) first in alphabetical order, insert a blank line, then external required dependencies alphabetically, followed by another blank line and the optional dependencies (those with optional = true) in alphabetical order. Preserve inline comments with their dependency. Add "python" to every extension-module feature list that builds a Python artefact, keeping it adjacent to "pyo3/extension-module" so the full Python stack is obvious. When a manifest groups adapters separately (for example crates/pyo3), keep the # Adapters block immediately below the internal crate list so downstream consumers can scan adapter coverage quickly. Always include a blank line before [dev-dependencies] and [build-dependencies] sections. Apply the same layout across related manifests when the feature or dependency sets change to avoid drift between crates. Use snake_case filenames for bin/ sources (for example bin/ws_data.rs) and reflect those paths in each [[bin]] section. Keep [[bin]] name entries in kebab-case (for example name = "hyperliquid-ws-data") so the compiled binaries retain their intended CLI names. Versioning guidance​ Use workspace inheritance for shared dependencies (for example serde = { workspace = true }). Only pin versions directly for crate-specific dependencies that are not part of the workspace. Group workspace-provided dependencies before crate-only dependencies so the inheritance is easy to audit. Feature flag conventions​ Prefer additive feature flags—enabling a feature must not break existing functionality. Use descriptive flag names that explain what capability is enabled. Document every feature in the crate-level documentation so consumers know what they toggle. Common patterns: high-precision: switches the value-type backing (64-bit or 128-bit integers) to support domains that require extra precision. default = []: keep defaults minimal. python: enables Python bindings. extension-module: builds a Python extension module (always include python). ffi: enables C FFI bindings. stubs: exposes testing stubs. Module organization​ Keep modules focused on a single responsibility. Use mod.rs as the module root when defining submodules. Prefer relatively flat hierarchies over deep nesting to keep paths manageable. Re-export commonly used items from the crate root for convenience. Code style and conventions​ File header requirements​ All Rust files must include the standardized copyright header: // -------------------------------------------------------------------------------------------------// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.// https://nautechsystems.io//// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");// You may not use this file except in compliance with the License.// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// ------------------------------------------------------------------------------------------------- Code formatting​ Import formatting is automatically handled by rustfmt when running make format. The tool organizes imports into groups (standard library, external crates, local imports) and sorts them alphabetically within each group. Within this section, follow these spacing rules: Leave one blank line between functions (including tests) – this improves readability and mirrors the default behavior of rustfmt. Leave one blank line above every doc comment (/// or //!) so that the comment is clearly detached from the previous code block. String formatting​ Prefer inline format strings over positional arguments: // Preferred - inline format with variable namesanyhow::bail!("Failed to subtract {n} months from {datetime}");// Instead of - positional argumentsanyhow::bail!("Failed to subtract {} months from {}", n, datetime); This makes messages more readable and self-documenting, especially when there are multiple variables. Type qualification​ Follow these conventions for qualifying types in code: anyhow: Always fully qualify anyhow macros (anyhow::bail!, anyhow::anyhow!) and the Result type (anyhow::Result). Nautilus domain types: Do not fully qualify Nautilus domain types. Use them directly after importing (e.g., Symbol, InstrumentId, Price). tokio: Generally fully qualify tokio types as they can have equivalents in std library and other crates (e.g., tokio::spawn, tokio::time::timeout). use nautilus_model::identifiers::Symbol;pub fn process_symbol(symbol: Symbol) -> anyhow::Result<()> { if !symbol.is_valid() { anyhow::bail!("Invalid symbol: {symbol}"); } tokio::spawn(async move { // Process symbol asynchronously }); Ok(())} Logging​ Fully qualify logging macros so the backend is explicit: Use log::… (log::info!, log::warn!, etc.) inside synchronous core crates. Use tracing::… (tracing::debug!, tracing::info!, etc.) for async runtimes, adapters, and peripheral components. Start messages with a capitalised word, prefer complete sentences, and omit terminal periods (e.g. "Processing batch", not "Processing batch."). Error handling​ Use structured error handling patterns consistently: Primary Pattern: Use anyhow::Result for fallible functions: pub fn calculate_balance(&mut self) -> anyhow::Result { // Implementation} Custom Error Types: Use thiserror for domain-specific errors: #[derive(Error, Debug)]pub enum NetworkError { #[error("Connection failed: {0}")] ConnectionFailed(String), #[error("Timeout occurred")] Timeout,} Error Propagation: Use the ? operator for clean error propagation. Error Creation: Prefer anyhow::bail! for early returns with errors: // Preferred - using bail! for early returnspub fn process_value(value: i32) -> anyhow::Result { if value < 0 { anyhow::bail!("Value cannot be negative: {value}"); } Ok(value * 2)}// Instead of - verbose return statementif value < 0 { return Err(anyhow::anyhow!("Value cannot be negative: {value}"));} Note: Use anyhow::bail! for early returns, but anyhow::anyhow! in closure contexts like ok_or_else() where early returns aren't possible. Async patterns​ Use consistent async/await patterns: Async function naming: No special suffix is required; prefer natural names. Tokio usage: Use tokio::spawn for fire-and-forget work, and document when that background task is expected to finish. Error handling: Return anyhow::Result from async functions to match the synchronous conventions. Cancellation safety: Call out whether the function is cancellation-safe and what invariants still hold when it is cancelled. Stream handling: Use tokio_stream (or futures::Stream) for async iterators to make back-pressure explicit. Timeout patterns: Wrap network or long-running awaits with timeouts (tokio::time::timeout) and propagate or handle the timeout error. Attribute patterns​ Consistent attribute usage and ordering: #[repr(C)]#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]#[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.model"))]pub struct Symbol(Ustr); For enums with extensive derive attributes: #[repr(C)]#[derive( Copy, Clone, Debug, Display, Hash, PartialEq, Eq, PartialOrd, Ord, AsRefStr, FromRepr, EnumIter, EnumString,)]#[strum(ascii_case_insensitive)]#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]#[cfg_attr( feature = "python", pyo3::pyclass(eq, eq_int, module = "nautilus_trader.model"))]pub enum AccountType { /// An account with unleveraged cash assets only. Cash = 1, /// An account which facilitates trading on margin, using account assets as collateral. Margin = 2,} Constructor patterns​ Use the new() vs new_checked() convention consistently: /// Creates a new [Symbol] instance with correctness checking.////// # Errors////// Returns an error if value is not a valid string.////// # Notes////// PyO3 requires a Result type for proper error handling and stacktrace printing in Python.pub fn new_checked<T: AsRef>(value: T) -> anyhow::Result { // Implementation}/// Creates a new [Symbol] instance.////// # Panics////// Panics if value is not a valid string.pub fn new<T: AsRef>(value: T) -> Self { Self::new_checked(value).expect(FAILED)} Always use the FAILED constant for .expect() messages related to correctness checks: use nautilus_core::correctness::FAILED; Constants and naming conventions​ Use SCREAMING_SNAKE_CASE for constants with descriptive names: /// Number of nanoseconds in one second.pub const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;/// Bar specification for 1-minute last price bars.pub const BAR_SPEC_1_MINUTE_LAST: BarSpecification = BarSpecification { step: NonZero::new(1).unwrap(), aggregation: BarAggregation::Minute, price_type: PriceType::Last,}; Hash collections​ Prefer AHashMap and AHashSet from the ahash crate over the standard library's HashMap and HashSet: use ahash::{AHashMap, AHashSet};// Preferred - using AHashMap/AHashSetlet mut symbols: AHashSet = AHashSet::new();let mut prices: AHashMap<InstrumentId, Price> = AHashMap::new();// Instead of - standard library HashMap/HashSetuse std::collections::{HashMap, HashSet};let mut symbols: HashSet = HashSet::new();let mut prices: HashMap<InstrumentId, Price> = HashMap::new(); Why use ahash? Superior performance: AHash uses AES-NI hardware instructions when available, providing 2-3x faster hashing compared to the default SipHash. Low collision rates: Despite being non-cryptographic, AHash provides excellent distribution and low collision rates for typical data. Drop-in replacement: Fully compatible API with standard library collections. When to use standard HashMap/HashSet: Cryptographic security required: Use standard HashMap when hash flooding attacks are a concern (e.g., handling untrusted user input in network protocols). Network clients: Currently prefer standard HashMap for network-facing components where security considerations outweigh performance benefits. Re-export patterns​ Organize re-exports alphabetically and place at the end of lib.rs files: // Re-exportspub use crate::{ nanos::UnixNanos, time::AtomicTime, uuid::UUID4,};// Module-level re-exportspub use crate::identifiers::{ account_id::AccountId, actor_id::ActorId, client_id::ClientId,}; Documentation standards​ Module-Level documentation​ All modules must have module-level documentation starting with a brief description: //! Functions for correctness checks similar to the design by contract philosophy.//!//! This module provides validation checking of function or method conditions.//!//! A condition is a predicate which must be true just prior to the execution of//! some section of code - for correct behavior as per the design specification. For modules with feature flags, document them clearly: //! # Feature flags//!//! This crate provides feature flags to control source code inclusion during compilation,//! depending on the intended use case://!//! - ffi: Enables the C foreign function interface (FFI) from cbindgen.//! - python: Enables Python bindings from PyO3.//! - extension-module: Builds as a Python extension module (used with python).//! - stubs: Enables type stubs for use in testing scenarios. Field documentation​ All struct and enum fields must have documentation with terminating periods: pub struct Currency { /// The currency code as an alpha-3 string (e.g., "USD", "EUR"). pub code: Ustr, /// The currency decimal precision. pub precision: u8, /// The ISO 4217 currency code. pub iso4217: u16, /// The full name of the currency. pub name: Ustr, /// The currency type, indicating its category (e.g. Fiat, Crypto). pub currency_type: CurrencyType,} Function documentation​ Document all public functions with: Purpose and behavior Explanation of input argument usage Error conditions (if applicable) Panic conditions (if applicable) /// Returns a reference to the AccountBalance for the specified currency, or None if absent.////// # Panics////// Panics if currency is None and self.base_currency is None.pub fn base_balance(&self, currency: Option) -> Option<&AccountBalance> { // Implementation} Errors and panics documentation format​ For single line errors and panics documentation, use sentence case with the following convention: /// Returns a reference to the AccountBalance for the specified currency, or None if absent.////// # Errors////// Returns an error if the currency conversion fails.////// # Panics////// Panics if currency is None and self.base_currency is None.pub fn base_balance(&self, currency: Option) -> anyhow::Result<Option<&AccountBalance>> { // Implementation} For multi-line errors and panics documentation, use sentence case with bullets and terminating periods: /// Calculates the unrealized profit and loss for the position.////// # Errors////// Returns an error if:/// - The market price for the instrument cannot be found./// - The conversion rate calculation fails./// - Invalid position state is encountered.////// # Panics////// This function panics if:/// - The instrument ID is invalid or uninitialized./// - Required market data is missing from the cache./// - Internal state consistency checks fail.pub fn calculate_unrealized_pnl(&self, market_price: Price) -> anyhow::Result { // Implementation} Safety documentation format​ For Safety documentation, use the SAFETY: prefix followed by a short description explaining why the unsafe operation is valid: /// Creates a new instance from raw components without validation.////// # Safety////// The caller must ensure that all input parameters are valid and properly initialized.pub unsafe fn from_raw_parts(ptr: const u8, len: usize) -> Self { // SAFETY: Caller guarantees ptr is valid and len is correct Self { data: std::slice::from_raw_parts(ptr, len), }} For inline unsafe blocks, use the SAFETY: comment directly above the unsafe code: impl Send for MessageBus { fn send(&self) { // SAFETY: Message bus is not meant to be passed between threads unsafe { // unsafe operation here } }} Python bindings​ Python bindings are provided via Cython and PyO3, allowing users to import NautilusTrader crates directly in Python without a Rust toolchain. PyO3 naming conventions​ When exposing Rust functions to Python via PyO3: The Rust symbol must be prefixed with py_ to make its purpose explicit inside the Rust codebase. Use the #[pyo3(name = "…")] attribute to publish the Python name without the py_ prefix so the Python API remains clean. #[pyo3(name = "do_something")]pub fn py_do_something() -> PyResult<()> { // …} Testing conventions​ Use mod tests as the standard test module name unless you need to specifically compartmentalize. Use #[rstest] attributes consistently, this standardization reduces cognitive overhead. Do not use Arrange, Act, Assert separator comments in Rust tests. Test organization​ Use consistent test module structure with section separators: ////////////////////////////////////////////////////////////////////////////////// Tests////////////////////////////////////////////////////////////////////////////////#[cfg(test)]mod tests { use rstest::rstest; use super::; use crate::identifiers::{Symbol, stubs::}; #[rstest] fn test_string_reprs(symbol_eth_perp: Symbol) { assert_eq!(symbol_eth_perp.as_str(), "ETH-PERP"); assert_eq!(format!("{symbol_eth_perp}"), "ETH-PERP"); }} Parameterized testing​ Use the rstest attribute consistently, and for parameterized tests: #[rstest]#[case("AUDUSD", false)]#[case("AUD/USD", false)]#[case("CL.FUT", true)]fn test_symbol_is_composite(#[case] input: &str, #[case] expected: bool) { let symbol = Symbol::new(input); assert_eq!(symbol.is_composite(), expected);} Test naming​ Use descriptive test names that explain the scenario: fn test_sma_with_no_inputs()fn test_sma_with_single_input()fn test_symbol_is_composite() Rust-Python memory management​ When working with PyO3 bindings, it's critical to understand and avoid reference cycles between Rust's Arc reference counting and Python's garbage collector. This section documents best practices for handling Python objects in Rust callback-holding structures. The reference cycle problem​ Problem: Using Arc in callback-holding structs creates circular references: Rust Arc holds Python objects → increases Python reference count. Python objects might reference Rust objects → creates cycles. Neither side can be garbage collected → memory leak. Example of problematic pattern: // AVOID: This creates reference cyclesstruct CallbackHolder { handler: Option<Arc>, // ❌ Arc wrapper causes cycles} The solution: GIL-based cloning​ Solution: Use plain PyObject with proper GIL-based cloning via clone_py_object(): use nautilus_core::python::clone_py_object;// CORRECT: Use plain PyObject without Arc wrapperstruct CallbackHolder { handler: Option, // ✅ No Arc wrapper}// Manual Clone implementation using clone_py_objectimpl Clone for CallbackHolder { fn clone(&self) -> Self { Self { handler: self.handler.as_ref().map(clone_py_object), } }} Best practices​ 1. Use clone_py_object() for Python object cloning​ // When cloning Python callbackslet cloned_callback = clone_py_object(&original_callback);// In manual Clone implementationsself.py_handler.as_ref().map(clone_py_object) 2. Remove #[derive(Clone)] from callback-holding structs​ // BEFORE: Automatic derive causes issues with PyObject#[derive(Clone)] // ❌ Remove thisstruct Config { handler: Option,}// AFTER: Manual implementation with proper cloningstruct Config { handler: Option,}impl Clone for Config { fn clone(&self) -> Self { Self { // Clone regular fields normally url: self.url.clone(), // Use clone_py_object for Python objects handler: self.handler.as_ref().map(clone_py_object), } }} 3. Update function signatures to accept PyObject​ // BEFORE: Arc wrapper in function signaturesfn spawn_task(handler: Arc) { ... } // ❌// AFTER: Plain PyObjectfn spawn_task(handler: PyObject) { ... } // ✅ 4. Avoid Arc::new() when creating Python callbacks​ // BEFORE: Wrapping in Arclet callback = Arc::new(py_function); // ❌// AFTER: Use directlylet callback = py_function; // ✅ Why this works​ The clone_py_object() function: Acquires the Python GIL before performing clone operations. Uses Python's native reference counting via clone_ref(). Avoids Rust Arc wrappers that interfere with Python GC. Maintains thread safety through proper GIL management. This approach allows both Rust and Python garbage collectors to work correctly, eliminating memory leaks from reference cycles. Common anti-patterns​ Avoid .clone() in hot paths – favour borrowing or shared ownership via Arc. Avoid .unwrap() in production code – generally propagate errors with ? or map them into domain errors, but unwrapping lock poisoning is acceptable because it signals a severe program state that should abort fast. Avoid String when &str suffices – minimise allocations on tight loops. Avoid exposing interior mutability – hide mutexes/RefCell behind safe APIs. Avoid large structs in Result<T, E> – box large error payloads (Box<dyn Error + Send + Sync>). Unsafe Rust​ It will be necessary to write unsafe Rust code to be able to achieve the value of interoperating between Cython and Rust. The ability to step outside the boundaries of safe Rust is what makes it possible to implement many of the most fundamental features of the Rust language itself, just as C and C++ are used to implement their own standard libraries. Great care will be taken with the use of Rusts unsafe facility - which just enables a small set of additional language features, thereby changing the contract between the interface and caller, shifting some responsibility for guaranteeing correctness from the Rust compiler, and onto us. The goal is to realize the advantages of the unsafe facility, whilst avoiding any undefined behavior. The definition for what the Rust language designers consider undefined behavior can be found in the language reference. Safety policy​ To maintain correctness, any use of unsafe Rust must follow our policy: If a function is unsafe to call, there must be a Safety section in the documentation explaining why the function is unsafe. and covering the invariants which the function expects the callers to uphold, and how to meet their obligations in that contract. Document why each function is unsafe in its doc comment's Safety section, and cover all unsafe blocks with unit tests. Always include a SAFETY: comment explaining why the unsafe operation is valid: // SAFETY: Message bus is not meant to be passed between threads#[allow(unsafe_code)]unsafe impl Send for MessageBus {} Crate-level lint – every crate that exposes FFI symbols enables #![deny(unsafe_op_in_unsafe_fn)]. Even inside an unsafe fn, each pointer dereference or other dangerous operation must be wrapped in its own unsafe { … } block. CVec contract – for raw vectors that cross the FFI boundary read the FFI Memory Contract. Foreign code becomes the owner of the allocation and must call the matching vec_drop_ function exactly once. Tooling configuration​ The project uses several tools for code quality: rustfmt: Automatic code formatting (see rustfmt.toml). clippy: Linting and best practices (see clippy.toml). cbindgen: C header generation for FFI. Rust version management​ The project pins to a specific Rust version via rust-toolchain.toml. Keep your toolchain synchronized with CI: rustup update # Update to latest stable Rustrustup show # Verify correct toolchain is active If pre-commit passes locally but fails in CI, clear the pre-commit cache and re-run: pre-commit clean # Clear cached environmentsmake pre-commit # Re-run all checks This ensures you're using the same Rust and clippy versions as CI. Resources​ The Rustonomicon – The Dark Arts of Unsafe Rust. The Rust Reference – Unsafety. Safe Bindings in Rust – Russell Johnston. Google – Rust and C interoperability.

[dependencies]

Pattern 7: Primary Pattern: Use anyhow::Result for fallible functions:

anyhow::Result<T>

Pattern 8: Example of problematic pattern:

// AVOID: This creates reference cyclesstruct CallbackHolder {    handler: Option<Arc<PyObject>>,  // ❌ Arc wrapper causes cycles}

Example Code Patterns

Example 1 (bash):

pip install -U nautilus_trader

Example 2 (bash):

pip install -U "nautilus_trader[docker,ib]"

Example 3 (python):

InstrumentId.from_str("BTC-USDT.OKX")  # For USDT-quoted spotInstrumentId.from_str("BTC-USDC.OKX")  # For USDC-quoted spot

Example 4 (python):

use_hyphens_in_client_order_ids=False

Example 5 (bash):

docker run -p 8000:8000 -p 8001:8001 -e "TM_API_KEY=YOUR_API_KEY" -d tardisdev/tardis-machine

Reference Files

This skill includes comprehensive documentation in references/:

  • api.md - Api documentation
  • backtesting.md - Backtesting documentation
  • concepts.md - Concepts documentation
  • data.md - Data documentation
  • getting_started.md - Getting Started documentation
  • other.md - Other documentation
  • strategies.md - Strategies documentation

Use view to read specific reference files when detailed information is needed.

Working with This Skill

For Beginners

Start with the getting_started or tutorials reference files for foundational concepts.

For Specific Features

Use the appropriate category reference file (api, guides, etc.) for detailed information.

For Code Examples

The quick reference section above contains common patterns extracted from the official docs.

Resources

references/

Organized documentation extracted from official sources. These files contain:

  • Detailed explanations
  • Code examples with language annotations
  • Links to original documentation
  • Table of contents for quick navigation

scripts/

Add helper scripts here for common automation tasks.

assets/

Add templates, boilerplate, or example projects here.

Notes

  • This skill was automatically generated from official documentation
  • Reference files preserve the structure and examples from source docs
  • Code examples include language detection for better syntax highlighting
  • Quick reference patterns are extracted from common usage examples in the docs

Updating

To refresh this skill with updated documentation:

  1. Re-run the scraper with the same configuration
  2. The skill will be rebuilt with the latest information