Back to Nautilus Trader

Migrate from v1 to v2

MIGRATION_V2.md

1.231.030.4 KB
Original Source

Migrate from v1 to v2

NautilusTrader v2 is the Rust core and PyO3 Python package under python/. It becomes the primary Python path when develop switches to that package. Until that cutover, the main distribution and general documentation still describe the legacy v1 Cython package. Use this guide to prepare a migration.

After cutover, v1 moves to the develop_v1 branch for approximately three months of critical security backports. 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:

bash
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 in its dedicated python/.venv:

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

The root .venv and root pyproject.toml belong to the legacy v1 build during the transition. 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.trading.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()

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
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. 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, ...)

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
CustomData.dataCustomData.value
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.

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 and consumed callbacks 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
SocketConfig.handlerhas_handler
WebSocketConfig.headersheader_names
WebSocketConfig.proxy_urlhas_proxy_url

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 and callbacks in application-owned state if they must be reused.

Betfair configuration moves and flattens in v2:

  • BetfairDataClientConfig becomes BetfairDataConfig, and BetfairExecClientConfig becomes BetfairExecConfig.
  • 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 BetfairDataConfig.
  • Execution reconciliation uses BetfairExecConfig.reconcile_market_ids directly. reconcile_market_ids_only still controls whether the filter applies.
  • 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 becomes DatabentoLiveClientConfig. 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 v2 live-node config. Reconnection remains an internal client concern; do not copy those v1 fields into v2 config construction.

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.

V1 types from nautilus_trader.config move beside their owning runtime. For example, BacktestRunConfig comes from nautilus_trader.backtest and PortfolioConfig from nautilus_trader.portfolio.

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

The Python v2 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:

python
from nautilus_trader.trading import Strategy
from nautilus_trader.trading import StrategyConfig


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.

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:

python
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

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
run()Owns the full lifecycle and blocks until shutdown.
start()Completes startup and returns, but does not service post-start channel traffic.
poll()Processes traffic queued at call entry, returns its count, and does not wait for more.
stop()Blocks through shutdown and services runner traffic during the residual-event grace period.

run() also owns maintenance, external message-bus ingress, signal handling, and automatic shutdown. A host that owns its loop must call start() once and schedule poll() repeatedly:

python
import asyncio


async def service_live_node(node):
    node.start()
    try:
        while application_running():
            node.poll()
            await asyncio.sleep(0.01)
    finally:
        node.stop()
        node.dispose()

poll() services time events, execution events, trading commands, data events, and data commands. Traffic arriving during a call remains queued for the next host cycle. The host decides when to stop a node in polling mode.

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.

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.

python
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__.

python
from nautilus_trader.model import ExecAlgorithmId
from nautilus_trader.trading import ExecutionAlgorithmConfig


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:

  • Custom data flows as native CustomData without the v1 wrapper semantics.
  • 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.
  • PortfolioConfig.use_mark_prices defaults to true; v1 defaulted to false. Set it to false to skip mark prices.
  • 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.
  • 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 now fails at connect time while the old column types remain.

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 cannot inject Redis cache databases or external message-bus backing factories into LiveNode; Rust builders still expose those backings.
  • SQL cache position and synthetic loads, actor and strategy state persistence, and heartbeat remain incomplete. Redis backing is available through Rust builders, but Python LiveNode configuration cannot select it.
  • External message-bus publishing of serialized order and position snapshots remains deferred.
  • V2 BacktestNode does not yet support the v1 StreamingConfig and DataCatalogConfig iterator workflow.
  • 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 published quickstart and backtesting tutorials still use v1 imports and configuration. Use the generated v2 stubs, the v2 backtest acceptance tests for backtesting, and Python v2 examples for live and adapter workflows while those tutorials are ported.

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