docs/tutorials/lighter_rwa_composite_mm.md
This tutorial runs the shipped CompositeMarketMaker strategy on Lighter's
NVDA-PERP.LIGHTER RWA market using Databento NVDA.EQUS quotes as an external
signal. The strategy quotes one post-only bid and one post-only ask around the
Lighter mid, then shifts both sides from a normalized Databento residual and the
current Lighter inventory.
The setup uses a Rust LiveNode, while the strategy itself runs as the native
Rust CompositeMarketMaker strategy.
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:
NVDA-PERP.LIGHTER.NVDA.EQUS.(databento_mid / baseline) - 1.0.signal_skew_factor * residual - inventory_skew_factor * net_position.With no configured baseline, the strategy captures the first observed 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. Set the
SIGNAL_BASELINE constant in the example source to pin the reference price for
deterministic runs.
In this setup, the Lighter BBO remains the spread anchor. Databento moves the quote center up or down through the normalized residual.
flowchart LR
subgraph Databento ["Databento data client"]
DQ["NVDA.EQUS QuoteTick
dataset = EQUS.MINI
schema = mbp-1"]
DS["signal_mid = (bid + ask) / 2"]
DR["residual = signal_mid / baseline - 1"]
end
subgraph Lighter ["Lighter data + execution clients"]
LQ["NVDA-PERP.LIGHTER QuoteTick"]
LM["anchor = (bid + ask) / 2"]
EX["Post-only limit orders"]
end
subgraph Strategy ["CompositeMarketMaker"]
TH{{"no target orders OR anchor/signal impact
>= requote_threshold_bps"}}
CA["cancel_all_orders()"]
SK["shift = signal_skew - inventory_skew"]
QU["bid = anchor - half_spread + shift
ask = anchor + half_spread + shift"]
PO["submit post-only bid/ask"]
end
DQ --> DS --> DR --> SK
LQ --> LM --> TH
TH -->|yes| CA --> SK --> QU --> PO --> EX
TH -->|no| LQ
The focus is the adapter wiring: one engine consumes a direct US equity feed and a crypto-native RWA venue, while order lifecycle, inventory, and quote state stay inside the same event-driven runtime.
EQUS.MINI), the default dataset for the bundled NVDA.EQUS route. Higher
tiers such as EQUS.PLUS need a separate Databento license; select one with
venue_dataset_map when your account is entitled.The example reads credentials from environment variables and keeps the strategy
parameters as editable Rust constants. It defaults to
LighterEnvironment::Testnet, so 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="0"
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.
The strategy, node, and adapters ship as crates, so you can depend on them from
your own Cargo project rather than working inside a NautilusTrader checkout. Add
the following to your Cargo.toml, pointing 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.
Download publishers.json from the Databento adapter
crate and point publishers_filepath at your local copy. The shipped example
resolves the same file relative to the checkout, so this step only applies to
your own project.
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. |
Subscribing to NVDA.EQUS requests top-of-book (mbp-1) quotes for NVDA from
Databento's EQUS.MINI dataset by default, delivered as a single QuoteTick
stream. EQUS.MINI is the lowest-cost consolidated US equities tier; richer
tiers such as EQUS.PLUS need a separate Databento license and can be selected
with the client's venue_dataset_map (for example {"EQUS": "EQUS.PLUS"}) once
your account is entitled. The adapter resolves the EQUS venue from a publishers
file: the example points DatabentoLiveClientConfig 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 aligns with the Lighter NVDA
minimum base amount observed during tutorial validation. Check the
market details endpoint before increasing size or changing instruments.
Lighter RWA markets trade continuously. NVDA.EQUS follows the US equity market
data session. The first live test should run during the regular cash session
(13:30-20:00 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 goes stale. The tutorial example keeps this explicit
instead of hiding it in a custom strategy.
There are two ways to run this: from a NautilusTrader checkout via the shipped
Lighter NVDA composite market maker example binary, or by
copying the node wiring below into a main in your own project that depends on
the crates from Project setup.
From a checkout, with the credential variables set, the shipped binary builds the node, registers all three clients, adds the built-in example strategy, and exits without connecting:
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 core of the setup is the three-client node plus CompositeMarketMaker:
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 databento_api_key = get_env_var("DATABENTO_API_KEY")?;
let databento_config =
DatabentoLiveClientConfig::new(databento_api_key, publishers_filepath, true, true);
let lighter_data_config = LighterDataClientConfig {
environment: lighter_environment,
..Default::default()
};
let lighter_exec_config = LighterExecClientConfig::builder()
.trader_id(trader_id)
.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_reconciliation(RUN_LIVE)
.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()?;
node.add_strategy(CompositeMarketMaker::new(strategy_config))?;
To connect and allow order submission, edit the constants near the top of the example source:
const RUN_LIVE: bool = true;
const ALLOW_LIVE_ORDERS: bool = true;
Then run the same command:
cargo run --bin lighter-nvda-composite-mm --package nautilus-tutorials --features examples
:::warning
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
setting RUN_LIVE and ALLOW_LIVE_ORDERS to true.
:::
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, Lighter data, and the order lifecycle. The
Databento residual remains zero until the first NVDA.EQUS quote arrives.
| Parameter | Value | Description |
|---|---|---|
instrument_id | NVDA-PERP.LIGHTER | Lighter RWA perpetual to quote. |
signal_instrument_id | NVDA.EQUS | Databento US Equities Plus signal feed. |
trade_size | 0.05 | Size per bid or ask. |
max_position | 0.20 | Hard cap on net Lighter 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 signal 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.
Signal ticks update internal state but do not submit orders by themselves. Until the first Databento quote arrives, the residual is zero. The next Lighter quote tick reads the latest signal residual and checks the quote state. A quote cycle occurs when:
requote_threshold_bps; orThe strategy then cancels open orders, reads current net position and pending
exposure from the cache, computes one bid and one ask, drops any side that
breaches max_position, and submits the remaining sides as post-only limits.
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.
uv sync --extra visualization
python3 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.
The next useful improvement is a signal-age gate. For example, cancel all
Lighter orders when the latest NVDA.EQUS quote is older than 30 seconds during
the cash session, or immediately after the cash session closes. That makes the
Databento signal an explicit operating dependency instead of an implicit one.
For a pure fair-value strategy, use this tutorial as the client wiring and write a small variant that anchors bid/ask directly on the Databento mid, then checks the Lighter BBO only for post-only and basis limits.