NautilusTrader
Tutorials

Nightly docs

These docs track unreleased changes and may change without notice. Their code examples can use APIs that the latest release lacks, so run them with a development wheel or switch to the latest release docs.

Composite Market Making on Lighter RWA with Databento US Equities NVDA

This tutorial runs the shipped CompositeMarketMaker strategy on Lighter's NVDA-PERP.LIGHTER RWA market with Databento NVDA.EQUS quotes as an external signal. The strategy quotes one post-only bid and one post-only ask around the Lighter mid. It shifts both sides based on a normalized Databento residual and the current Lighter inventory.

The setup uses a Rust LiveNode and the native Rust CompositeMarketMaker strategy. If you are new to the Lighter adapter, first complete Get started with Lighter. That guide covers the Rust and Python data-client paths separately. This tutorial then adds Databento signal data and live order flow.

Introduction

Lighter lists real-world asset (RWA) perpetuals that trade continuously, including single-name equity markets. See Lighter's RWA docs and market specifications for current venue details. Databento's US Equities datasets provide US equity top-of-book data for NVDA, with mbp-1 available through the Nautilus Databento adapter.

CompositeMarketMaker is a small two-input market maker:

  • The target instrument is the Lighter market to quote: NVDA-PERP.LIGHTER.
  • The signal instrument is the Databento reference feed: NVDA.EQUS.
  • The anchor is the Lighter mid.
  • The signal residual is (databento_mid / baseline) - 1.0.
  • The quote shift is signal_skew_factor * residual - inventory_skew_factor * net_position.

With no configured baseline, the strategy uses the first valid NVDA.EQUS mid as the reference price. The residual starts at zero and measures NVDA's move from that first signal mid, not the Lighter/Databento basis. For deterministic runs, set the SIGNAL_BASELINE constant in the example source to fix the reference price.

In this setup, the Lighter BBO remains the spread anchor. Databento moves the quote center up or down through the normalized residual.

The tutorial shows how one engine connects a direct US equity feed and a crypto-native RWA venue. Order lifecycle, inventory, and quote state remain inside the same event-driven runtime.

Prerequisites

  • A Rust toolchain (MSRV 1.99.1 or newer).
  • A Cargo project with the Nautilus, Lighter, and Databento crates as dependencies (see Project setup).
  • Python 3.13+ to regenerate the rendered panels.
  • A Databento API key with live access to Databento US Equities Mini (EQUS.MINI), the default dataset for the bundled NVDA.EQUS route. Higher tiers such as EQUS.PLUS need a separate Databento license. When your account has that entitlement, select a tier with venue_dataset_map.
  • Lighter API credentials (numeric account index, API key index, and API secret) for the configured environment (testnet by default), required only to connect and submit orders.
  • The Lighter integration guide: Lighter.
  • The Databento integration guide: Databento.

The example reads credentials from environment variables and keeps the strategy parameters as editable Rust constants. It defaults to LighterEnvironment::Testnet, so follow the account and API key setup and set the testnet Lighter credentials:

export DATABENTO_API_KEY="your-databento-api-key"
export LIGHTER_TESTNET_ACCOUNT_INDEX="123456"
export LIGHTER_TESTNET_API_KEY_INDEX="4"
export LIGHTER_TESTNET_API_SECRET="your-lighter-api-secret"

For mainnet, change LIGHTER_ENVIRONMENT in the source to LighterEnvironment::Mainnet and use the mainnet LIGHTER_* credential variables described in the integration guide. Set DATABENTO_API_KEY before running the example.

Project setup

The strategy, node, and adapters ship as crates that your own Cargo project can use. You do not need to work inside a NautilusTrader checkout. Add the following to your Cargo.toml. Point every Nautilus dependency at the same develop git source so the crates resolve to one consistent version:

[dependencies]
nautilus-common = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["live"] }
nautilus-core = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop" }
nautilus-databento = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["high-precision", "live"] }
nautilus-lighter = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["examples", "high-precision"] }
nautilus-live = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["node"] }
nautilus-model = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["high-precision"] }
nautilus-trading = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["examples", "high-precision"] }

tokio = { version = "1", features = ["full"] }

The examples feature on nautilus-trading exposes the CompositeMarketMaker strategy, and high-precision is required for Lighter's crypto-native pricing. For the general crate layout, feature flags, and the crates.io alternative to the git source, see the Rust project setup guide.

The Databento client also needs a publishers file that maps venues to datasets. For your own project, download publishers.json from the Databento adapter crate. Set publishers_filepath to your local copy. The shipped example resolves the same file relative to the checkout, so only your own project needs this step.

Why NVDA

NVDA is a liquid Nasdaq-listed single-name equity, and Lighter maps its RWA perpetual to NVDA-PERP.LIGHTER. This pairs a licensed Databento signal with a Lighter traded market:

RoleInstrument IDSourceNotes
Signal instrumentNVDA.EQUSDatabentoEQUS.MINI top-of-book quote updates.
Target instrumentNVDA-PERP.LIGHTERLighterRWA perpetual traded through Lighter.

By default, a subscription to NVDA.EQUS requests top-of-book (mbp-1) quotes for NVDA from Databento's EQUS.MINI dataset. The adapter delivers these quotes as a single QuoteTick stream. The EQUS.MINI dataset is the lowest-cost consolidated US equities tier. Richer tiers such as EQUS.PLUS need a separate Databento license. If your account has that entitlement, select the tier with the client's venue_dataset_map (for example {"EQUS": "EQUS.PLUS"}).

The adapter resolves the EQUS venue from a publishers file. The example points DatabentoDataClientConfig at the publishers.json bundled with the Databento adapter. See Instrument IDs and symbology for the mapping rules.

The older Databento Equities Basic (DBEQ.BASIC) dataset name appears in some grandfathered accounts and historical examples. New Databento subscriptions use the Databento US Equities product line, so this tutorial uses the consolidated EQUS venue. Treat the top-of-book feed as a licensed signal proxy for the tutorial wiring, not as a full depth Nasdaq TotalView book.

The example starts at trade_size=0.05, which matches the Lighter NVDA minimum base amount observed during tutorial validation. Before you increase size or change instruments, check the market details endpoint for the configured environment:

Session constraint

Lighter RWA markets trade continuously. NVDA.EQUS follows the US equity market data session. Run the first live test during the regular cash session ([13

, 20
] UTC, US daylight time), with special handling for holidays and half-days.

CompositeMarketMaker does not include a built-in session gate or signal-age guard. For production use, add an actor or strategy variant that cancels quotes when the Databento signal is stale. The tutorial uses the shipped strategy, so this operating constraint remains explicit.

Example node

You can run the example in two ways:

The Python example at examples/live/lighter/nvda_composite_mm.py uses the same Rust strategy through PyO3.

From a checkout, with the credential variables set, the shipped binary connects the data and execution clients. It defaults to DRY_RUN = false, which adds the order-submitting strategy.

This command can submit live orders. Start with the smallest accepted size on a funded test account or a mainnet account sized for loss. Confirm the active instrument ID, account ID, numeric account index, and Lighter credentials before running it.

cargo run --bin lighter-nvda-composite-mm --package nautilus-tutorials --features examples

Databento is a multi-venue data client without a fixed venue route, so the engine uses it as the default route for NVDA.EQUS. Lighter registers with the LIGHTER venue route and receives NVDA-PERP.LIGHTER subscriptions.

The setup combines a three-client node with CompositeMarketMaker. This excerpt is abbreviated. It omits these items, among other lines:

  • Source constants.
  • The publishers_filepath, max_position, and trade_size definitions.
  • The final node.run().await? call.

The example source has the complete main:

use nautilus_live::node::config::LiveExecutionEngineConfig;

let lighter_environment = LIGHTER_ENVIRONMENT;
let trader_id = TraderId::from(TRADER_ID);
let account_id = AccountId::from(ACCOUNT_ID);
let instrument_id = InstrumentId::from(INSTRUMENT_ID);
let signal_instrument_id = InstrumentId::from(SIGNAL_INSTRUMENT_ID);

let api_key = get_env_var("DATABENTO_API_KEY")?;
let databento_config =
    DatabentoDataClientConfig::new(api_key, publishers_filepath, true, true);
let lighter_data_config = LighterDataClientConfig::builder()
    .environment(lighter_environment)
    .build();
let lighter_exec_config = LighterExecutionClientConfig::builder()
    .account_id(account_id)
    .environment(lighter_environment)
    .build();

let mut strategy_config = CompositeMarketMakerConfig::builder()
    .instrument_id(instrument_id)
    .signal_instrument_id(signal_instrument_id)
    .max_position(max_position)
    .trade_size(trade_size)
    .half_spread_bps(HALF_SPREAD_BPS)
    .inventory_skew_factor(INVENTORY_SKEW_FACTOR)
    .signal_skew_factor(SIGNAL_SKEW_FACTOR)
    .requote_threshold_bps(REQUOTE_THRESHOLD_BPS)
    .on_cancel_resubmit(ON_CANCEL_RESUBMIT)
    .build();
strategy_config.base.strategy_id = Some(StrategyId::from("NVDA_COMPOSITE_MM-001"));
strategy_config.base.order_id_tag = Some("001".to_string());

let mut node = LiveNode::builder(trader_id, Environment::Live)?
    .with_name("LIGHTER-NVDA-COMPOSITE-MM-001".to_string())
    .with_exec_engine_config(LiveExecutionEngineConfig {
        reconciliation_lookback_mins: Some(60),
        reconciliation_instrument_ids: Some(vec![instrument_id.to_string()]),
        ..Default::default()
    })
    .with_reconciliation(!DRY_RUN)
    .with_delay_post_stop_secs(5)
    .add_data_client(
        None,
        Box::new(DatabentoDataClientFactory::new()),
        Box::new(databento_config),
    )?
    .add_data_client(
        None,
        Box::new(LighterDataClientFactory::new()),
        Box::new(lighter_data_config),
    )?
    .add_exec_client(
        None,
        Box::new(LighterExecutionClientFactory::new()),
        Box::new(lighter_exec_config),
    )?
    .build()?;

if !DRY_RUN {
    node.add_strategy(CompositeMarketMaker::new(strategy_config))?;
}

To connect without adding the order-submitting strategy, edit the constant near the top of the example source:

const DRY_RUN: bool = true;

Then run the same command:

cargo run --bin lighter-nvda-composite-mm --package nautilus-tutorials --features examples

For a testnet smoke run, keep LIGHTER_ENVIRONMENT as LighterEnvironment::Testnet and use the LIGHTER_TESTNET_* credential variables. If the run is outside the Databento US Equities cash session, it can still validate node startup, routing, and Lighter data. The strategy waits for a valid NVDA.EQUS quote before submitting its first orders, so order lifecycle validation requires the Databento signal.

With reconciliation enabled, both examples reconcile only the configured Lighter target instrument, with a 60-minute lookback. They skip reports for other Lighter markets on the account. On stop, the strategy cancels its own target orders. It submits reduce-only market orders to close its own target positions.

Start with a flat target position on the account. Lighter nets all exposure in a market, while the strategy caps and closes only the position it owns. After shutdown, confirm that no orders or positions from the run remain. Fills can arrive while cancellation is pending.

Strategy parameters

ParameterValueDescription
instrument_idNVDA-PERP.LIGHTERLighter RWA perpetual to quote.
signal_instrument_idNVDA.EQUSDatabento US Equities Mini signal feed.
trade_size0.05Size per bid or ask.
max_position0.20Hard cap on the strategy's target exposure.
half_spread_bps25Half-spread around the Lighter anchor.
inventory_skew_factor2.0Price units per unit of net position.
signal_skew_factor55.0Price units per unit of normalized Databento residual.
signal_baselineFirst valid midOptional reference price for the Databento residual.
requote_threshold_bps5Anchor or signal-impact move that triggers cancel and requote.

With a Lighter mid of 207.00 and half_spread_bps=25, the unskewed half spread is 0.5175 USD. If Databento is 30 bps above its baseline, a signal_skew_factor of 55.0 shifts both sides up by 0.165 USD before inventory skew. A long position of 0.05 with inventory_skew_factor=2.0 shifts both sides down by 0.10 USD.

Requote behavior

Signal ticks update internal state but do not submit orders by themselves. With nonzero signal_skew_factor, target ticks wait for both of these conditions:

  • A signal quote with positive, uncrossed bid and ask prices.
  • A finite, positive baseline.

The first valid signal mid supplies the baseline when signal_baseline is unset. An explicit baseline remains unchanged and still requires a valid signal quote. An invalid baseline prevents quoting. With signal_skew_factor=0, the strategy can quote without a signal or baseline.

After readiness, the next Lighter quote tick reads the latest signal residual and checks the quote state. A quote cycle occurs when any of these conditions holds:

  • No target orders are open or in flight.
  • The Lighter anchor moves by at least requote_threshold_bps.
  • The price impact of the signal residual change reaches the same threshold.

The strategy then performs these actions:

  1. Cancels open orders.
  2. Reads current net position and pending exposure from the cache.
  3. Calculates one bid and one ask.
  4. Removes any side that exceeds max_position.
  5. Submits the remaining sides as post-only limits.

Panels

The panels below use deterministic replay data. They show the quoting mechanics and the cash-session constraint. They are not a captured live Lighter fill trace.

NVDA composite quote center against Databento and Lighter mids

Figure 1. Databento NVDA.EQUS mid, Lighter NVDA-PERP.LIGHTER mid, composite bid, composite ask, and quote center.

Databento residual, Lighter basis, and quote-center shift

Figure 2. Databento residual, Lighter basis, and quote-center shift in bps.

Inventory skew terms for the composite market maker

Figure 3. Net position, signal shift, inventory adjustment, and total shift for a 0.05 NVDA trade size and 0.20 NVDA position cap.

Lighter continuous trading and Databento session clock

Figure 4. Lighter's continuous RWA market clock against the Databento US Equities cash-session signal, with signal age after the regular session.

Regenerate the panels

After building NautilusTrader from source, run these commands from the repository root:

make sync
uv run --project python --no-sync \
    python docs/tutorials/assets/lighter_rwa_composite_mm/render_panels.py

The renderer writes four PNGs into docs/tutorials/assets/lighter_rwa_composite_mm/. It uses the nautilus_dark Plotly theme and deterministic replay data so docs builds do not depend on vendor data licenses or live exchange access.

Extensions

A signal-age gate can make the Databento signal an explicit operating dependency. For example, cancel all Lighter orders if the latest NVDA.EQUS quote is older than 30 seconds during the cash session. Also cancel them immediately after the cash session closes.

For a pure fair-value strategy, use this tutorial's client setup. Write a small variant that anchors bid/ask directly on the Databento mid. Then check the Lighter BBO only for post-only and basis limits.

On this page