Migrate from v1 to v2

September 1, 2026 ยท View on GitHub

NautilusTrader v2 is the Rust core and PyO3 Python package under python/. It is the primary Python path on develop. Use this guide to migrate from the legacy v1 Cython package.

Legacy v1 lives on the develop_v1 branch, which accepts critical security backports for approximately three months after the v2 cutover. It does not receive new feature or parity work.

The v1 and v2 packages both install and import as nautilus_trader, so use a separate virtual environment for each and never install both into one.

Install v2

Install a release candidate from PyPI in a fresh environment:

uv venv --python 3.14
source .venv/bin/activate
uv pip install --pre nautilus_trader

Run this block outside a NautilusTrader source checkout. The repository's exclude-newer uv policy can filter out newly published release-candidate wheels.

To build from source, build the package from the repository root:

make build-debug
.venv/bin/python -c 'import nautilus_trader; print(nautilus_trader.__version__)'

The source build uses the root .venv and target/ directories. See Installation for platform support and package-index options.

Port Python code

Core strategy, data, order, risk, portfolio, backtest, and live workflows remain available. Update imports and configuration to the new module paths:

v1 pathv2 path
nautilus_trader.backtest.engine.BacktestEnginenautilus_trader.backtest.BacktestEngine
nautilus_trader.backtest.node.BacktestNodenautilus_trader.backtest.BacktestNode
nautilus_trader.live.node.TradingNodenautilus_trader.live.LiveNode
nautilus_trader.config.StrategyConfignautilus_trader.config.StrategyConfig
Adapter classes from nautilus_trader.adapters.<venue>.configRust/PyO3 classes from nautilus_trader.adapters.<venue>

Common API renames

V2 shortens common strategy and cache names. The QuoteTick, TradeTick, and register_indicator_for_*_ticks names do not change.

v1 namev2 name
on_quote_tickon_quote
on_trade_tickon_trade
on_order_bookon_book
on_order_book_deltason_book_deltas
on_order_book_depthon_book_depth
subscribe_quote_tickssubscribe_quotes
subscribe_trade_tickssubscribe_trades
unsubscribe_quote_ticksunsubscribe_quotes
unsubscribe_trade_ticksunsubscribe_trades
request_quote_ticksrequest_quotes
request_trade_ticksrequest_trades
subscribe_order_book_deltassubscribe_book_deltas
subscribe_order_book_depthsubscribe_book_depth10
subscribe_order_book_at_intervalsubscribe_book_at_interval
unsubscribe_order_book_deltasunsubscribe_book_deltas
unsubscribe_order_book_depthunsubscribe_book_depth10
unsubscribe_order_book_at_intervalunsubscribe_book_at_interval
request_order_book_snapshotrequest_book_snapshot
request_order_book_deltasrequest_book_deltas
request_order_book_depthrequest_book_depth
cache.quote_tickcache.quote
cache.trade_tickcache.trade
cache.quote_tickscache.quotes
cache.trade_tickscache.trades
cache.quote_tick_countcache.quote_count
cache.trade_tick_countcache.trade_count

API changes

V2 uses specific names for component and model identities:

v1 memberv2 member
Actor.idDataActor.actor_id
Strategy.idStrategy.strategy_id
ExecAlgorithm.idExecutionAlgorithm.exec_algorithm_id
Event idevent_id
Report idreport_id
Account typeaccount_type

Collection and lifecycle inspection also changes shape:

v1 memberv2 member
Order.eventsOrder.events()
Position.adjustmentsPosition.adjustments()
Position.client_order_idsPosition.client_order_ids()
Position.eventsPosition.events()
Position.trade_idsPosition.trade_ids()
Position.venue_order_idsPosition.venue_order_ids()
OrderList.ordersclient_order_ids(), then resolve each ID through the cache
OrderList.firstResolve first_client_order_id through the cache
Portfolio.initializedPortfolio.is_initialized()
Portfolio.analyzerstatistics(), snapshots(), and nautilus_trader.analysis
Actor.state/Strategy.stateDataActor.state()/Strategy.state()
ExecAlgorithm.stateExecutionAlgorithm.state remains a property
Component.is_runningis_running()
Component.is_stoppedis_stopped()
Component.is_disposedis_disposed()
Component.is_degradedis_degraded()
Component.is_faultedis_faulted()

Portfolio query names also change without compatibility aliases:

v1 namev2 name
margins_init()instrument_initial_margins()
margins_maint()instrument_maintenance_margins()
is_flat()is_net_flat()
is_completely_flat()is_completely_net_flat()

unrealized_pnl(), total_pnl(), and net_exposure() implement the retained optional price parameter as a fresh calculation that does not replace cached values. realized_pnl(), realized_pnls(), unrealized_pnl(), unrealized_pnls(), total_pnl(), total_pnls(), net_exposure(), and net_exposures() accept target_currency and fail closed when a required price or conversion is unavailable or exact arithmetic overflows. Collection queries never return partial results. When a query accepts both venue and account_id, those scopes must identify the same account.

account() returns a detached account value. build_snapshot() returns an on-demand sample without recording it, and the Python Portfolio does not expose engine mutation commands or the internal recorded realized-PnL cache.

V1 is_initialized means that a component has advanced beyond PRE_INITIALIZED. V2 is_ready() means exactly READY, so it is not an equivalent replacement while a component is running, stopped, degraded, disposed, or faulted. Inspect state() on DataActor and Strategy, or the state property on ExecutionAlgorithm, and compare it with ComponentState.PRE_INITIALIZED.

Read the v1 Strategy runtime properties order_id_tag, oms_type, external_order_claims, manage_contingent_orders, manage_gtd_expiry, use_uuid_client_order_ids, and use_hyphens_in_client_order_ids through the same-name properties on Strategy.config. The two client-order-ID formatting options on a strategy-owned OrderFactory use the same config. A standalone factory has no equivalent flag readback.

Historical requests use type-specific batch callbacks in v2:

v1 data through on_historical_datav2 callbackv2 argument
Custom dataon_historical_dataOne CustomData or Sequence[CustomData]
Book snapshoton_bookOne OrderBook
Book deltason_historical_book_deltasSequence[OrderBookDelta]
Book depthon_historical_book_depthSequence[OrderBookDepth10]
Quote tickson_historical_quotesSequence[QuoteTick]
Trade tickson_historical_tradesSequence[TradeTick]
Funding rateson_historical_funding_ratesSequence[FundingRateUpdate]
Barson_historical_barsSequence[Bar]

Typed historical results no longer fall through to on_historical_data; that hook handles custom data. A response carrying one CustomData arrives as that object; a response carrying a batch arrives as one list, including an empty list. on_historical_mark_prices and on_historical_index_prices are available for native batch delivery, but the current public Python API does not initiate those requests.

The generic on_event hook is removed. Replace timer handling with on_time_event, aggregate order handling with on_order_event, and aggregate position handling with on_position_event. For custom messaging, use on_signal or a typed data subscription instead of overriding on_event.

Python v2 ExecutionAlgorithm does not inherit the full actor surface. Move market-data and historical callbacks to DataActor or Strategy. Its inherited v1 on_save and on_load hooks also have no v2 algorithm callback; retain that state in application configuration or move the stateful component to DataActor or Strategy. Change on_order_list(self, order_list) to on_order_list(self, order_list, orders).

V2 strategy order changes take client order IDs rather than order objects:

v1 methodv2 method
modify_order(order, ...)modify_order(order.client_order_id, ...)
cancel_order(order, ...)cancel_order(order.client_order_id, ...)
cancel_orders(orders, ...)cancel_orders(client_order_ids, ...)

Strategy.cancel_all_orders() now affects only orders associated with that strategy by default. Pass strategy_only=False to retain v1's broader instrument-and-side scope.

The public nautilus_trader.data.OptionChainManager is removed. Call subscribe_option_chain(...) from a DataActor or Strategy, then handle each aggregated OptionChainSlice in on_option_chain(slice). Call unsubscribe_option_chain(series_id) to stop the subscription.

Cache.actor_ids() is removed. Rust integrations can use Trader::actor_ids(); Python v2 does not expose a direct actor-ID collection.

Typed model objects

V2 removes the PyCapsule boundary between Python and Rust. Pass model objects directly and use normal Python type checks:

v1 APIv2 migration
nautilus_trader.core.is_pycapsule(value)isinstance(value, ExpectedModelType)
model.as_pycapsule()Pass the model object
OrderBookDeltas.from_pycapsule(capsule)Use the OrderBookDeltas object directly
Databento load_*_as_pycapsule(...)Call the corresponding load_*(...) method
Adapter callbacks receiving PyCapsuleHandle the typed model object
BacktestEngine.add_data with duck typingPass supported NautilusTrader model objects
Duck-typed portfolio-statistic position inputPass nautilus_trader.model.Position objects

DataQueryResult iteration returns list chunks containing typed Python objects. Use to_list() to flatten all remaining chunks. Query and decode failures raise RuntimeError instead of appearing as an exhausted iterator.

Enum absence and side names

AggressorSide.BUYER and AggressorSide.SELLER become AggressorSide.BUY and AggressorSide.SELL. String, serde, and PostgreSQL output also use BUY and SELL; legacy BUYER and SELLER input remains deprecated compatibility input.

These Python compatibility attributes now evaluate to None, not enum members:

  • OrderSide.NO_ORDER_SIDE
  • PositionSide.NO_POSITION_SIDE
  • ContingencyType.NO_CONTINGENCY
  • TrailingOffsetType.NO_TRAILING_OFFSET
  • TriggerType.NO_TRIGGER

Use is None for absence checks. The legacy NO_* string tokens still parse to None, but the attributes no longer have enum properties such as .name or .value and do not appear in variants().

This absence also reaches affected return values. BookOrder.side, DatabentoImbalance.side, OrderStatusReport.order_side, OrderStatusReport.contingency_type, and OrderStatusReport.trailing_offset_type can return None. The concrete order classes also return None from closing_side(PositionSide.FLAT). Handle the optional value before accessing enum properties, and use OrderBookDelta.clear(...) when constructing a clear delta.

Inspection and state renames

V2 exposes consistent read-only inspection across economic instrument types. Properties include asset_class, instrument_class, currencies, fees, margins, quantity and price limits, multiplier, and tick_scheme; values may be None or a documented default. SyntheticInstrument is formula-derived, so inspect its id, components, formula, price precision and increment, and timestamps instead.

Several v1 inspection names have direct v2 replacements:

v1 namev2 name
instrument.symbolinstrument.id.symbol
instrument.venueinstrument.id.venue
instrument.activation_utcinstrument.activation_ns
instrument.expiration_utcinstrument.expiration_ns
instrument.tick_scheme_nameinstrument.tick_scheme
AdaptiveMovingAverage.period or .period_er.period_efficiency_ratio
AdaptiveMovingAverage.period_alpha_fast.period_fast
AdaptiveMovingAverage.period_alpha_slow.period_slow
LinearRegression.R2LinearRegression.r2
DirectionalMovement.value.pos and .neg
DataType.typeDataType.type_name
OrderBookDelta.is_add/is_clear/is_delete/is_updateinspect OrderBookDelta.action
OrderBookDeltas.is_snapshotinspect OrderBookDeltas.flags
BookLevel.sideuse the containing bid or ask context
Bar.is_revisionremoved

activation_ns and expiration_ns contain UNIX nanoseconds; convert them to the datetime type used by the application when calendar-time inspection is needed. V1 DirectionalMovement.value never changed from zero, so v2 exposes the meaningful positive and negative outputs instead.

MarginAccount.margin(), MarginAccount.margins(), and MarginAccount.account_margins() keep their v1 names. Other read-only margin queries move the measure or scope qualifier to the front in v2: for example, margin_init() becomes initial_margin(), and margin_init_for_currency() becomes account_initial_margin().

v1 namev2 name
margin_init()initial_margin()
margins_init()initial_margins()
margin_maint()maintenance_margin()
margins_maint()maintenance_margins()
margin_for_currency()account_margin()
margin_init_for_currency()account_initial_margin()
margin_maint_for_currency()account_maintenance_margin()
account_margins_init()account_initial_margins()
account_margins_maint()account_maintenance_margins()
total_margin_init()total_initial_margin()
total_margin_maint()total_maintenance_margin()

The v2 Python query surface exposes only the read-only methods above. It does not bind the Rust engine mutation methods update_margin, clear_margin, clear_account_margin, clear_initial_margin, clear_maintenance_margin, or set_margin_model. This v2 boundary does not describe which command APIs were available in v1. Existing v2 Python methods, including update_initial_margin, update_maintenance_margin, set_default_leverage, and set_leverage, remain unchanged.

The payload on nautilus_trader.model.CustomData remains available through .data. The separate nautilus_trader.common.CustomData byte container exposes .value; it is not the type accepted by DataActor.publish_data().

Config readback and sensitive values

V2 immutable configs expose non-secret constructor values as read-only properties. This includes engine, backtest venue and run, live reconciliation, and data/execution tester settings. LiveRiskEngineConfig.max_notional_per_order returns v2's validated strings even when constructed from Python integers or decimal values.

Potential credentials use bounded inspection properties instead of raw readback:

Constructor fieldInspection property
BacktestDataConfig.catalog_fs_storage_optionscatalog_fs_storage_option_keys
BacktestDataConfig.catalog_fs_rust_storage_optionscatalog_fs_rust_storage_option_keys

These raw fields and adapter credentials remain private. Some configs provide has_* checks for credential-bearing proxy, database, or gateway settings without returning their values. Keep secrets in application-owned state if they must be reused.

Betfair configuration moves and flattens in v2:

  • BetfairDataClientConfig remains the data factory input, while BetfairExecClientConfig becomes BetfairExecutionClientConfig.
  • BetfairInstrumentProviderConfig no longer exists as a separate config. Its account_currency, default_min_notional, event_type_ids, event_type_names, event_ids, market_ids, country_codes, market_types, min_market_start_time, and max_market_start_time fields move directly onto BetfairDataClientConfig.
  • Execution reconciliation uses BetfairExecutionClientConfig.reconcile_market_ids directly. reconcile_market_ids_only still controls whether the filter applies.
  • Rename stream_heartbeat_ms to stream_heartbeat_secs and stream_idle_timeout_ms to stream_heartbeat_timeout_secs, then convert configured values from milliseconds to seconds.
  • certs_dir is removed because v2 uses interactive login. The HTTP keepalive interval is fixed internally at 36,000 seconds rather than exposed as keep_alive_secs.

Databento configuration also changes shape:

  • DatabentoDataClientConfig remains the factory input. It keeps use_exchange_as_venue, bars_timestamp_on_close, and venue_dataset_map, adds the required publishers_filepath, and accepts api_key as a private constructor value.
  • The v1 startup preload fields instrument_ids and parent_symbols are removed. V2 handles live subscriptions and historical instrument requests directly instead of configuring an instrument provider preload.
  • http_gateway, live_gateway, timeout_initial_load, mbo_subscriptions_delay, and reconnect_timeout_mins are not accepted by the Python DatabentoDataClientConfig constructor. Reconnection remains an internal client concern; do not copy those v1 fields into current config construction.

Bybit's bybit_bar_spec_to_interval now takes a BarAggregation and step instead of the aggregation's integer value and step.

Interactive Brokers legacy mutation fields have constructor or builder replacements:

V1 field or aliasV2 replacement
legacy_market_data_typePass market_data_type to InteractiveBrokersDataClientConfig.
legacy_load_idsPass load_ids to InteractiveBrokersInstrumentProviderConfig.
legacy_load_contractsPass load_contracts to the instrument provider config.
legacy_symbology_methodPass symbology_method to the instrument provider config.
pickle_pathPass or set cache_path on the instrument provider config.
routingPass RoutingConfig to LiveNodeBuilder.add_data_client or add_exec_client.
dockerized_gatewayStart the gateway outside v2, then pass its host and port.

V2 retains writable instrument_provider fields on the data and execution client configs and cache_path on the provider config. A non-None dockerized_gateway is rejected because Python v2 does not own the container lifecycle.

Core config types remain grouped under nautilus_trader.config. Adapter configs move to the adapter's public module, such as nautilus_trader.adapters.databento.

The core config names change as follows:

v1 configv2 config
ActorConfigDataActorConfig
ExecAlgorithmConfigExecutionAlgorithmConfig
ExecEngineConfigExecutionEngineConfig
LoggingConfigLoggerConfig
TradingNodeConfigLiveNodeConfig

Import the current names from nautilus_trader.config. ControllerConfig has no direct replacement: define controller fields on a DataActorConfig subclass, then refer to that class through ImportableControllerConfig.

V2 uses Execution in project-owned type names and omits Live from ordinary client names. This changes these public names:

v1 or earlier v2 namev2 name
<Venue>ExecClientConfig<Venue>ExecutionClientConfig
BetfairDataConfigBetfairDataClientConfig
BetfairExecConfigBetfairExecutionClientConfig
DatabentoLiveClientConfigDatabentoDataClientConfig
LiveDataClientConfigDataClientConfig
LiveExecClientConfigExecutionClientConfig
LiveExecEngineConfigLiveExecutionEngineConfig
ImportableExecAlgorithmConfigImportableExecutionAlgorithmConfig
ExecFactoryExtractorExecutionFactoryExtractor
SimExecFactoryExtractorSimulatedExecutionFactoryExtractor

ExecAlgorithmId, its associated exec_* fields, and ExecTester retain their established names. Venue protocol terms such as ExecType also remain unchanged. The extractor aliases are Rust extension APIs under nautilus_system::python::registry.

V2 removes component and client collections from node configuration. Register them explicitly:

v1 config fieldv2 migration
NautilusKernelConfig.message_busPass msgbus to BacktestEngineConfig or LiveNodeConfig.
NautilusKernelConfig.actorsCall add_actor or add_actor_from_config on the node.
NautilusKernelConfig.strategiesCall add_strategy or add_strategy_from_config on the node.
NautilusKernelConfig.exec_algorithmsCall add_exec_algorithm or add_exec_algorithm_from_config.
TradingNodeConfig.data_clientsCall LiveNodeBuilder.add_data_client.
TradingNodeConfig.exec_clientsCall LiveNodeBuilder.add_exec_client or add_simulated_exec_client.

On LiveNodeConfig, timeout names now state their unit and the post-stop wait is a delay:

v1 fieldv2 LiveNodeConfig field
timeout_connectiontimeout_connection_secs
timeout_reconciliationtimeout_reconciliation_secs
timeout_portfoliotimeout_portfolio_secs
timeout_disconnectiontimeout_disconnection_secs
timeout_post_stopdelay_post_stop_secs
timeout_shutdowntimeout_shutdown_secs

BacktestEngineConfig keeps the v1 timeout names without the _secs suffix, except that timeout_post_stop becomes delay_post_stop.

Execution factories now consume the corresponding execution client config directly. Remove BitmexExecFactoryConfig, DeriveExecFactoryConfig, and HyperliquidExecFactoryConfig wrappers, and pass BitmexExecutionClientConfig, DeriveExecutionClientConfig, or HyperliquidExecutionClientConfig to add_exec_client.

The live node owns the trader identity. Remove trader_id from adapter execution client config construction; LiveNodeConfig or LiveNode.builder(...) supplies it to every execution factory. Keep the venue-specific account_id on the execution client config. The Bybit, Coinbase, and Interactive Brokers execution factories now use no-argument constructors. Custom Rust execution factories must accept TraderId in their ExecutionClientFactory::create or SimulatedExecutionClientFactory::create implementation.

The v1 fill, fee, latency, and simulation-module config and factory wrappers are also removed. Construct the current model or module directly, such as ProbabilisticFillModel, FixedFeeModel, StaticLatencyModel, or FXRolloverInterestModule, and pass it to the backtest venue. SimulationModuleConfig is therefore no longer a separate Python type.

DataCatalogConfig and StreamingConfig have v2-native Python equivalents for BacktestNode. Configure existing built-in-data catalog queries and Feather output through BacktestEngineConfig. These configs do not restore the v1 factory, download, custom-data, or generic serialization workflows. DatabaseConfig has no public v2 Python equivalent. For live trading, configure Redis or Postgres cache backing through LiveNodeBuilder; this does not restore the generic v1 DatabaseConfig workflow. See cache database configuration.

Custom Rust cache database adapters used with live orders must implement the batch index_order_clients operation. The default trait implementation rejects non-empty claims.

The generic Python APIs under nautilus_trader.network have no v2 public Python equivalent: HttpClient, HttpMethod, HttpResponse, SocketClient, WebSocketClient, SocketConfig, WebSocketConfig, Quota, network exceptions, and the http_* functions. Adapter-specific low-level Python WebSocket clients and their request, error, and channel-control types are also removed. Use LiveNode data and execution clients for streaming venue workflows, retained adapter HTTP clients for supported direct requests, or the Rust nautilus-network crate for custom networking. TransportBackend remains available from nautilus_trader.network only for adapter config transport selection.

The v1 NautilusConfig, NautilusKernelConfig, ImportableConfig, config factory classes, and encoding and path utilities are not application config objects in the current package. Use the concrete configs and registration methods instead.

Use the generated type stubs in python/nautilus_trader/ as the supported Python contract. Some adapter wire DTOs expose extra runtime attributes that are not part of that contract. The following methods are callable at runtime but absent from the stubs, so static type checkers cannot resolve them:

  • KrakenFuturesHttpClient.edit_orders_batch
  • KrakenFuturesHttpClient.submit_orders_batch
  • KrakenSpotHttpClient.submit_orders_batch

See the Python concept guide for the runtime ownership model and public API boundaries.

The Rust-native Python examples show current live-node builders, adapter factories, strategies, actors, and data/execution testers.

Python v2 strategies subclass Strategy and override lifecycle or data callbacks:

from nautilus_trader.config import StrategyConfig
from nautilus_trader.trading import Strategy


class MyStrategyConfig(StrategyConfig):
    pass


class MyStrategy(Strategy):
    def on_start(self) -> None:
        pass

Annotated custom fields on a v1 StrategyConfig subclass do not carry over. In v2, remove custom keyword arguments in __new__ before the PyO3 base validates them, then assign the fields in __init__. See the v2 strategy config example.

Register actors, strategies, and algorithms

V1 node configs contain importable actors, strategies, and execution algorithms. V2 registers each component on the node instead:

  • For BacktestNode, call node.build() first. Then pass the run config ID and a constructed component to add_actor, add_strategy, or add_exec_algorithm, or use the corresponding _from_config method. Call node.run() after registration.
  • For LiveNode, register constructed components or importable configs with the same method pairs before calling run() or run_async().

LiveNode.add_actor accepts constructed Python actor instances. Registration applies the actor's config, derives its ID, and rejects duplicate IDs or registration after the node leaves its idle state.

Backtest node post-run inspection

V2 keeps BacktestNode engines internal; the v1 get_engine and get_engines calls are unavailable. For post-run inspection, set BacktestRunConfig.dispose_on_completion=False; the True default drops engine state. Then pass the run config ID to the node inspection methods:

config = BacktestRunConfig(..., dispose_on_completion=False)
node = BacktestNode([config])
results = node.run()

cache = node.get_engine_cache(config.id)
portfolio = node.get_engine_portfolio(config.id)
statistics = portfolio.statistics()
fills = node.generate_fills_report(config.id)

These additional reports also take the run config ID first:

  • generate_orders_report
  • generate_order_fills_report
  • generate_positions_report
  • generate_account_report

The getting-started backtest guides show the current high-level BacktestNode and low-level BacktestEngine APIs.

Live node inspection and host-loop integration

V2 exposes the Rust-owned cache and portfolio through node.cache and node.portfolio. These shared wrappers provide normal inspection without exposing runtime internals.

Choose the lifecycle method based on who owns the loop:

MethodContract
LiveNode.run()Runs on the calling thread, owns signal handling, and blocks until shutdown.
LiveNode.run_async()Runs on the caller's asyncio loop and resolves once the node has stopped.
LiveNodeHandle.stop()Requests graceful shutdown and returns immediately; the active run completes afterward.

Earlier v2 LiveNode.start() and LiveNode.poll() no longer exist. Replace an owned-loop start/poll sequence with run(), or await run_async() when the application owns the event loop.

Both entry points run the same lifecycle, so a hosted node performs the same startup ordering, maintenance, external message-bus ingress, reconciliation, and shutdown as an owned one. A host that owns its loop awaits run_async() and stops the node through its handle. It must wait for the handle to report Running, supervise the run task for unexpected completion, await graceful shutdown, and then dispose the node.

Capture cache, portfolio, and handle() before starting, because run_async() lends the node to the returned coroutine for the run's duration. Use LiveNodeHandle.stop() as the external stop path while either execution mode owns the node. See the live trading concept guide for the canonical recipe and full contract.

Order factory configuration readback

OrderFactory.trader_id and strategy_id remain available. For the v1 use_uuid_client_order_ids and use_hyphens_in_client_order_ids flags, read Strategy.config in a v2 strategy. Standalone factories provide no equivalent flag readback; retain those values in application configuration if needed.

Execution algorithms

Python v2 ExecutionAlgorithm remains a routed-order component rather than inheriting the full Actor authoring surface. Supported override points include:

  • on_order
  • on_order_list
  • Order and position callbacks
  • Lifecycle callbacks
  • on_signal

The runtime owns command routing and calls execute; do not call or override execute as the algorithm entrypoint.

An execution algorithm cannot submit a spawned order with a live emulation trigger. Use emulation_trigger=None; Python raises ValueError if submit_order receives a triggered child. After a failed spawn with reduce_primary=True, discard or refresh the caller-held primary order: quantity restoration updates the cached primary order.

V2 OrderList stores client order IDs instead of order objects. The runtime resolves those IDs through the cache and calls on_order_list(order_list, orders), where orders follows the client order ID order. If the subclass overrides on_order_list, it receives one list callback and the runtime does not also call on_order. Without an override, the default implementation calls on_order once for each resolved order. Change v1 one-argument overrides to accept orders; the v1 default did not fan out order lists.

The supported authoring surface has these v1 dispositions:

V1 ExecAlgorithm / Actor capabilityPython v2 contract
cacheAvailable as a read-only property after node or engine registration.
portfolioAvailable as a read-only property after node or engine registration.
greeksConstruct GreeksCalculator(self.cache, self.clock) after registration.
msgbusNot exposed; use signals for supported custom messaging.
Registered indicatorsUse DataActor or Strategy for indicator-driven workflows.
Market-data subscriptions and callbacksUse DataActor or Strategy; algorithms inspect cache and routed events.
Lifecycle state and controlUse is_*() and lifecycle methods; the Rust component remains authoritative.
Direct register(...)Use BacktestEngine.add_exec_algorithm or LiveNode.add_exec_algorithm.

Signals replace direct message-bus access on Python v2 DataActor, Strategy, and ExecutionAlgorithm:

  • Call subscribe_signal(name) during on_start.
  • Handle on_signal(signal).
  • Call publish_signal(name, value).

Signal values use their string representation. Raw message-bus endpoints and handlers remain runtime internals.

from nautilus_trader.common import GreeksCalculator
from nautilus_trader.trading import ExecutionAlgorithm


class RoutedAlgorithm(ExecutionAlgorithm):
    def on_start(self) -> None:
        self._greeks = GreeksCalculator(self.cache, self.clock)
        self.subscribe_signal("execution-control")

    def on_signal(self, signal) -> None:
        self.log.info(f"Received {signal.value}")

    def on_order(self, order) -> None:
        instrument = self.cache.instrument(order.instrument_id)
        portfolio_ready = self.portfolio.is_initialized()
        self.log.info(f"Routing {instrument.id}; portfolio ready={portfolio_ready}")

Order exec_algorithm_params keys and values remain string-only across the v2 model and Python bindings. Encode each value as a string when constructing the order, then parse it in the algorithm. For example, pass exec_algorithm_params={"horizon_secs": "300", "interval_secs": "10"}, not numeric values. This keeps Python authoring aligned with the Rust IndexMap<Ustr, Ustr> contract.

ExecutionAlgorithmConfig supports Python subclasses with custom fields. The inherited __new__ applies the base fields before the Python __init__ runs, so the subclass initializes only its custom attributes. Keep **_kwargs so the subclass accepts the base keywords. The base constructor ignores other unmatched keywords, so validate optional custom inputs in __init__.

from nautilus_trader.config import ExecutionAlgorithmConfig
from nautilus_trader.model import ExecAlgorithmId


class RoutedAlgorithmConfig(ExecutionAlgorithmConfig):
    def __init__(
        self,
        horizon_secs: str,
        interval_secs: str,
        **_kwargs,
    ) -> None:
        self.horizon_secs = horizon_secs
        self.interval_secs = interval_secs


config = RoutedAlgorithmConfig(
    exec_algorithm_id=ExecAlgorithmId("ROUTED"),
    horizon_secs="300",
    interval_secs="10",
    log_events=False,
)
algorithm = RoutedAlgorithm(config)

If an algorithm subclass defines __init__, call super().__init__(config) to retain its Python instance and config for export.

Define the algorithm and config classes at module scope so the exported import paths resolve.

Constructed instances and importable configs work in backtest and live workflows:

  • Register v2 ExecutionAlgorithm instances with BacktestEngine.add_exec_algorithm or LiveNode.add_exec_algorithm.
  • Call algorithm.to_importable_config() to export the algorithm path, config path, and config values.
  • Register the result with BacktestEngine.add_exec_algorithm_from_config or LiveNode.add_exec_algorithm_from_config.
  • Register DataActor-based compatibility algorithms with add_exec_algorithm_from_config.

Nodes normally drive lifecycle transitions. Direct lifecycle methods remain available for control-plane integrations and dispatch the same Python callbacks.

Port one workflow at a time and verify the generated stub before replacing a v1 convenience method. Do not assume that a v1 adapter config field also exists on its v2 Rust config.

Behavior changes

Account for these differences from v1:

  • v2 caches OptionGreeks for option fee calculation; this extends v1.
  • Bar.is_revision is not exposed on the v2 Python surface. Do not depend on it during migration.
  • A direct Position.apply fill that crosses zero resets the open entry price to the flipping fill. v1 retains the old side's entry price; the v2 behavior is the go-forward contract.
  • Position.apply validates the fill's instrument ID, position ID, and ordinary trade-ID uniqueness before mutation. Invalid fills leave the position unchanged and raise ValueError; v1 raises KeyError for a duplicate trade ID and does not validate both identities on every apply.
  • PortfolioConfig.use_mark_prices defaults to true; v1 defaulted to false. Set it to false to skip mark prices.
  • Backtest venues no longer accept settlement_prices. Add InstrumentClose data with close_type=InstrumentCloseType.CONTRACT_EXPIRED; its exact close_price settles futures, binary contracts, and option close legs at expiry.
  • An omitted backtest default_leverage now selects 10x for margin accounts and 1x for cash accounts. Set default_leverage=Decimal(1) to retain v1's unleveraged behavior.
  • v2 OrderList stores client order IDs instead of order objects:
    • Use the resolved orders argument in ExecutionAlgorithm.on_order_list(order_list, orders).
    • Elsewhere, replace order_list.orders with order_list.client_order_ids(), then resolve each ID through cache.order(client_order_id).
    • Replace order_list.first with cache.order(order_list.first_client_order_id) after checking the ID is not None.
  • Catalog order-event data written before activation_price and OrderFilled.info were added cannot be read by the new schema. Regenerate or migrate that data before upgrading a catalog in place.
  • Order.avg_px and Order.slippage are decimal.Decimal, where v1 exposes float. v2 no longer converts the weighted average through f64, so comparisons against float literals can fail on a fractional value: Decimal("0.70000") == 0.7 is False. Compare against Decimal("0.7"), or wrap the operand with Decimal(str(value)).
  • Order.to_dict() returns avg_px and slippage as strings, matching how the other decimal fields already serialize. Wrap the value in Decimal(...) before doing arithmetic on it.

PostgreSQL schema changes

Postgres-backed deployments must run nautilus database init before starting a v2 node. The order.avg_px and order.slippage columns move from double precision to NUMERIC, and the node fails at connect time while the old column types remain.

Existing databases whose AGGRESSOR_SIDE enum still contains BUYER and SELLER also need this one-time migration before ingesting v2 data:

ALTER TYPE AGGRESSOR_SIDE RENAME VALUE 'BUYER' TO 'BUY';
ALTER TYPE AGGRESSOR_SIDE RENAME VALUE 'SELLER' TO 'SELL';

Do not run those statements if the enum already contains BUY and SELL.

Known limitations

These gaps can affect migration but do not block supported cutover workflows:

  • Python request callbacks do not provide v1 joined-response, pending-request cleanup, or late and duplicate delivery convenience behavior.
  • Python v2 accepts built-in backing configs such as RedisMessageBusConfig, but arbitrary Python factory classes remain unsupported. V1 MessageBusConfig(database=DatabaseConfig(...), external_streams=[...]) maps to the builder calls LiveNodeBuilder.with_msgbus_config(...) and LiveNodeBuilder.with_external_msgbus_factory(RedisMessageBusConfig(...)). See live message-bus configuration. The existing RedisMessageBusFactory(RedisMessageBusConfig(...)) wrapper remains supported.
  • Official in-tree live adapters remain configurable from Python through LiveNodeBuilder.add_data_client and add_exec_client. Sandbox simulated execution uses add_simulated_exec_client. A working sandbox client does not mean a custom live factory will register: neither path currently accepts an out-of-tree Python factory, and v1 LiveDataClient and LiveExecutionClient subclassing has no v2 equivalent yet. An out-of-tree Python adapter surface is planned. See Python support boundaries and issue 4694.
  • The PostgreSQL cache loads positions but not synthetics. It also does not persist actor or strategy state or write cache heartbeats. Redis backing supports these operations.
  • External message-bus publishing of serialized order and position snapshots remains deferred.
  • Python LiveNodeConfig does not accept the v1 kernel-level streaming or emulator fields, and loop_debug=True is rejected by the Rust live runtime. Order emulation itself remains available through order emulation triggers.
  • V2 BacktestNode catalog configuration does not support v1 data-client factories, a download engine, on-the-fly downloads, custom data, or data frames.
  • Instrument-provider filter dictionaries are not a common v2 adapter contract. Hyperliquid v2 loads its configured instrument universe and does not accept the v1 instrument_provider field. Check each adapter's Rust/PyO3 config rather than copying v1 provider examples.

The v2 roadmap tracks the wider post-cutover surface. Release-specific breaking changes remain in RELEASES.md.