How to Extend Nautilus Trader with Custom Adapters: A Complete Implementation Guide
Extend Nautilus Trader by implementing a custom adapter that inherits from abstract base classes like LiveMarketDataClient or LiveExecutionClient, override the required async methods such as _connect and _subscribe, and register the class in your system configuration to wire it into the kernel.
Nautilus Trader is built around a plug-in architecture that separates core trading components from external integrations. To connect a new exchange, data source, or cache backend, you need to extend Nautilus Trader with custom adapters that implement well-defined abstract interfaces. This guide walks through the architecture, implementation steps, and concrete code examples based on the official template package.
Understanding the Adapter Architecture in Nautilus Trader
Core Abstract Base Classes
All custom adapters must inherit from specific abstract base classes defined in the nautilus_trader package:
- Instrument Provider:
nautilus_trader.common.providers.InstrumentProviderrequires implementingload_all_async,load_ids_async, andload_asyncto fetch instrument definitions. - Live Market Data Client:
nautilus_trader.live.data_client.LiveMarketDataClientrequires_connect,_disconnect,_subscribe,_unsubscribe, and_request_*methods to handle real-time data feeds. - Live Execution Client:
nautilus_trader.live.execution_client.LiveExecutionClientrequires_connect,_disconnect,_submit_order,_cancel_order, andgenerate_*_reportmethods for order management. - Cache Database:
nautilus_trader.cache.facade.CacheDatabaseFacaderequiresload,add, andupdate_*methods for custom persistence backends.
How the Kernel Wires Adapters
The kernel (nautilus_trader/system/kernel.py) instantiates adapters based on configuration entries. For example, when configuring a cache database, the kernel checks the configuration type and instantiates the appropriate adapter class:
if config.cache and config.cache.database.type == "redis":
cache_db = CacheDatabaseAdapter(
trader_id=self._trader_id,
instance_id=self._instance_id,
serializer=MsgSpecSerializer(...),
config=config.cache,
)
To use a custom cache adapter, you would replace CacheDatabaseAdapter with your own subclass of CacheDatabaseFacade and reference it in the configuration.
Step-by-Step Guide to Building a Custom Adapter
1. Copy the Template Package
Start by copying the official template package to create your adapter skeleton:
cp -r nautilus_trader/adapters/_template my_exchange
The template contains three key files:
providers.py– Instrument provider implementationdata.py– Live market data client implementationexecution.py– Live execution client implementation
2. Implement the Instrument Provider
In my_exchange/providers.py, inherit from InstrumentProvider and implement the three async loading methods:
from nautilus_trader.common.providers import InstrumentProvider
from nautilus_trader.model.instruments import Instrument
from nautilus_trader.model.identifiers import InstrumentId
class MyExchangeInstrumentProvider(InstrumentProvider):
async def load_all_async(self, filters: dict | None = None) -> list[Instrument]:
# Fetch all tradable instruments from your exchange API
pass
async def load_ids_async(
self,
instrument_ids: list[InstrumentId],
filters: dict | None = None,
) -> list[Instrument]:
# Fetch specific instruments by ID
pass
async def load_async(
self,
instrument_id: InstrumentId,
filters: dict | None = None,
) -> Instrument | None:
# Fetch a single instrument
pass
3. Build the Live Data Client
In my_exchange/data.py, create a class inheriting from LiveMarketDataClient and implement the connection and subscription handlers:
from nautilus_trader.live.data_client import LiveMarketDataClient
from nautilus_trader.data.messages import SubscribeBars, UnsubscribeBars
class MyExchangeLiveDataClient(LiveMarketDataClient):
async def _connect(self) -> None:
# Initialize WebSocket or HTTP connection
self._log.info("Connecting to MyExchange...")
async def _disconnect(self) -> None:
# Clean up connections
self._log.info("Disconnecting from MyExchange...")
async def _subscribe(self, command: SubscribeBars) -> None:
# Map Nautilus SubscribeBars to exchange subscription
instrument_id = command.data_type.metadata["instrument_id"]
# Subscribe via WebSocket...
async def _unsubscribe(self, command: UnsubscribeBars) -> None:
# Handle unsubscription
pass
4. Build the Live Execution Client
In my_exchange/execution.py, inherit from LiveExecutionClient to handle order management:
from nautilus_trader.live.execution_client import LiveExecutionClient
from nautilus_trader.execution.messages import SubmitOrder, CancelOrder
class MyExchangeLiveExecutionClient(LiveExecutionClient):
async def _connect(self) -> None:
# Connect to execution gateway
pass
async def _disconnect(self) -> None:
# Disconnect from gateway
pass
async def _submit_order(self, command: SubmitOrder) -> None:
# Convert Nautilus order to exchange format and submit
order = command.order
# API call to submit order...
async def _cancel_order(self, command: CancelOrder) -> None:
# Cancel order on exchange
pass
async def generate_order_status_report(self, order_id):
# Fetch order status from exchange
pass
async def generate_fill_report(self, order_id):
# Fetch fill/trade report
pass
5. Register Your Adapter in Configuration
Add your adapter to the system configuration so the kernel can instantiate it:
data:
client:
type: my_exchange
class: my_exchange.data.MyExchangeLiveDataClient
config:
venue: MYEX
api_key: ${MYEX_API_KEY}
api_secret: ${MYEX_API_SECRET}
exec:
client:
type: my_exchange
class: my_exchange.execution.MyExchangeLiveExecutionClient
config:
venue: MYEX
Complete Custom Adapter Example: Echo Data Client
Below is a minimal runnable example of a custom data adapter that echoes subscription requests back as synthetic data. This demonstrates the required method signatures without external dependencies.
# my_exchange/data.py
from nautilus_trader.live.data_client import LiveMarketDataClient
from nautilus_trader.common.providers import InstrumentProvider
from nautilus_trader.common.component import LiveClock, MessageBus
from nautilus_trader.cache.cache import Cache
from nautilus_trader.model.identifiers import Venue
from nautilus_trader.model.data import Bar, BarSpecification, BarType, BarAggregation, PriceType
from nautilus_trader.data.messages import SubscribeBars, UnsubscribeBars
from nautilus_trader.core.datetime import secs_to_nanos
class EchoLiveDataClient(LiveMarketDataClient):
"""
A trivial live data client that returns synthetic 1‑minute bars for any
subscribed instrument. Useful as a starting point for a custom adapter.
"""
async def _connect(self) -> None:
self._log.info("EchoLiveDataClient connected")
async def _disconnect(self) -> None:
self._log.info("EchoLiveDataClient disconnected")
async def _subscribe(self, command: SubscribeBars) -> None:
instrument_id = command.data_type.metadata["instrument_id"]
bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.LAST)
bar_type = BarType(instrument_id, bar_spec, aggregation_source="EXTERNAL")
synthetic_bar = Bar(
ts_event=secs_to_nanos(0),
ts_init=secs_to_nanos(0),
open=100,
high=101,
low=99,
close=100.5,
volume=10,
bar_type=bar_type,
)
self._handle_data(synthetic_bar)
async def _unsubscribe(self, command: UnsubscribeBars) -> None:
pass
async def _request_bars(self, request):
instrument_id = request.instrument_id
bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.LAST)
bar_type = BarType(instrument_id, bar_spec, aggregation_source="EXTERNAL")
bar = Bar(
ts_event=secs_to_nanos(0),
ts_init=secs_to_nanos(0),
open=100,
high=101,
low=99,
close=100.5,
volume=10,
bar_type=bar_type,
)
self._handle_data_response(
data_type=request.bar_type,
data=[bar],
correlation_id=request.id,
start=request.start,
end=request.end,
params=request.params,
)
Configuration snippet (config.yaml):
data:
client:
type: echo
class: my_exchange.data.EchoLiveDataClient
config:
venue: ECHO
When the kernel loads this configuration, the EchoLiveDataClient will be instantiated and the system will treat it like any other exchange data client.
Key Source Files for Custom Adapter Development
| Role | File | Description |
|---|---|---|
| Template – Provider | nautilus_trader/adapters/_template/providers.py |
Skeleton for instrument discovery |
| Template – Data Client | nautilus_trader/adapters/_template/data.py |
Skeleton for market data connections |
| Template – Execution Client | nautilus_trader/adapters/_template/execution.py |
Skeleton for order management |
| Cache Adapter Example | nautilus_trader/cache/adapter.py |
PostgreSQL cache backend implementation |
| Kernel Wiring | nautilus_trader/system/kernel.py |
Component instantiation and adapter lifecycle |
| Live Data Client Base | nautilus_trader/live/data_client.py |
Abstract base for data adapters |
| Live Execution Client Base | nautilus_trader/live/execution_client.py |
Abstract base for execution adapters |
| Instrument Provider Base | nautilus_trader/common/providers.py |
Abstract base for instrument providers |
| Cache Facade Base | nautilus_trader/cache/facade.py |
Abstract base for cache backends |
Summary
- Adapters are the primary extension mechanism in Nautilus Trader, enabling integration with any exchange, data vendor, or database backend.
- Four adapter types exist: Instrument Provider, Live Market Data Client, Live Execution Client, and Cache Database.
- Template package at
nautilus_trader/adapters/_template/provides a type-checked skeleton withpragma: no covermarkers for abstract methods. - Kernel instantiation automatically wires adapters based on configuration entries under
data.client,exec.client, orcache.database. - Implementation requires async methods for connection management, subscription handling, and protocol-specific translations between Nautilus message types and exchange APIs.
Frequently Asked Questions
What is the difference between a data client and an execution client in Nautilus Trader?
A data client (LiveMarketDataClient) handles market data subscriptions, historical data requests, and real-time feed connections via WebSocket or REST. An execution client (LiveExecutionClient) manages the order lifecycle, including submitting orders, canceling orders, and generating fill reports and account status. While both inherit from base client classes and implement _connect and _disconnect, they handle distinct message types: data clients process SubscribeBars and RequestQuoteTicks, while execution clients process SubmitOrder and CancelOrder commands.
Do I need to implement all abstract methods for a custom adapter?
Yes, you must implement all abstract methods defined in the base class you are extending. The template files in nautilus_trader/adapters/_template/ include these methods with # pragma: no cover comments to indicate they are intentionally unimplemented in the skeleton. For a minimal working adapter, you need at minimum: for data clients, implement _connect, _disconnect, _subscribe, and _unsubscribe; for execution clients, implement _connect, _disconnect, _submit_order, and _cancel_order. Additional methods like _request_bars or generate_order_status_report enable full functionality but can raise NotImplementedError initially if not required for your use case.
How do I test a custom adapter before connecting to a live exchange?
Use the template package structure to create a mock implementation that returns synthetic data, similar to the EchoLiveDataClient example provided above. You can implement _subscribe to generate synthetic Bar or QuoteTick objects without network calls, allowing you to test the integration with the Nautilus data engine and message bus. Additionally, the repository contains integration tests for built-in adapters (such as Binance) in the tests/ directory; you can create analogous tests using pytest and pytest-asyncio to verify your adapter's behavior against recorded API responses or mock servers before deploying to production.
Can I extend the cache backend with a custom adapter?
Yes, the cache system supports custom backends through the CacheDatabaseFacade abstract base class defined in nautilus_trader/cache/facade.py. You can implement a custom cache adapter by subclassing this facade and implementing methods such as load, add, update_order, and update_account. The kernel instantiates the cache adapter based on the cache.database configuration, similar to data and execution clients. The nautilus_trader/cache/adapter.py file provides a reference implementation using PostgreSQL, demonstrating how to serialize objects with MsgSpecSerializer and handle database transactions within the async facade methods.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →