Nightly 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 bundledNVDA.EQUSroute. Higher tiers such asEQUS.PLUSneed a separate Databento license. When your account has that entitlement, select a tier withvenue_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:
| Role | Instrument ID | Source | Notes |
|---|---|---|---|
| Signal instrument | NVDA.EQUS | Databento | EQUS.MINI top-of-book quote updates. |
| Target instrument | NVDA-PERP.LIGHTER | Lighter | RWA 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
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:
- From a NautilusTrader checkout, use the shipped Lighter NVDA composite market maker binary.
- In your own project, copy the node setup below into
mainwith the crates from Project setup.
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 examplesDatabento 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, andtrade_sizedefinitions. - 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 examplesFor 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
| Parameter | Value | Description |
|---|---|---|
instrument_id | NVDA-PERP.LIGHTER | Lighter RWA perpetual to quote. |
signal_instrument_id | NVDA.EQUS | Databento US Equities Mini signal feed. |
trade_size | 0.05 | Size per bid or ask. |
max_position | 0.20 | Hard cap on the strategy's target exposure. |
half_spread_bps | 25 | Half-spread around the Lighter anchor. |
inventory_skew_factor | 2.0 | Price units per unit of net position. |
signal_skew_factor | 55.0 | Price units per unit of normalized Databento residual. |
signal_baseline | First valid mid | Optional reference price for the Databento residual. |
requote_threshold_bps | 5 | Anchor 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:
- Cancels open orders.
- Reads current net position and pending exposure from the cache.
- Calculates one bid and one ask.
- Removes any side that exceeds
max_position. - 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.

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

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

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

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.pyThe 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.
Book Imbalance Backtest (Betfair)
This tutorial backtests a BookImbalanceActor on a Betfair MATCH_ODDS market. It loads a raw historical streaming .gz file, feeds it through the Rust...
Hurst/VPIN Directional Strategy (Kraken Futures)
This tutorial backtests a directional strategy on PF_XBTUSD, the USD-margined Bitcoin perpetual on Kraken Futures. The strategy combines a Hurst-exponent...