Skip to content
All library documents

Cross-Venue NVDA Perpetual Market Making with an Equity Signal

Code NautilusTrader

Summary

This example configures a live market-making strategy for an NVDA perpetual contract on Lighter, using NVDA equity quotes from Databento as an external signal. The strategy combines a configured half-spread with inventory and signal skew, limits position size, and sets a threshold for requoting. It also connects data and execution clients, configures order reconciliation, and offers a dry-run switch that starts clients without launching the order-submitting strategy.

The document provides implementation settings rather than performance evidence: it gives example position and trade sizes, spread and skew parameters, and a testnet environment setting. These values illustrate configuration choices but do not establish profitability or suitability. The source includes a warning that disabling dry run can submit orders and may use real funds. It does not explain how the composite strategy calculates its signal or inventory adjustments, nor does it provide backtest results, risk analysis, or guidance for calibrating parameters.

Key ideas

  • The example uses NVDA equity quotes as a signal for market making in an NVDA perpetual contract.
  • Quote placement incorporates spread, inventory skew, and signal skew parameters.
  • The configuration limits maximum position and trade size and sets a requote threshold.
  • A dry-run setting controls whether the order-submitting strategy starts.
  • The example supplies configuration details but no evidence of trading performance.

Tags

Full text
# lighter_nvda_composite_mm.rs


```rs
// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may not use this file except in compliance with the License.
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

//! Example demonstrating Lighter NVDA RWA market making with a Databento signal.
//!
//! This builds a live node with:
//! - Databento `NVDA.EQUS` quotes as the signal instrument.
//! - Lighter `NVDA-PERP.LIGHTER` data and execution as the target instrument.
//! - The native Rust `CompositeMarketMaker` strategy.
//!
//! Run with:
//! `cargo run --bin lighter-nvda-composite-mm --package nautilus-tutorials --features examples`
//!
//! Required credential environment variables:
//! - `DATABENTO_API_KEY`
//! - `LIGHTER_TESTNET_ACCOUNT_INDEX`, `LIGHTER_TESTNET_API_KEY_INDEX`, and
//!   `LIGHTER_TESTNET_API_SECRET` when the `LIGHTER_ENVIRONMENT` source constant
//!   is `LighterEnvironment::Testnet`
//! - `LIGHTER_ACCOUNT_INDEX`, `LIGHTER_API_KEY_INDEX`, and `LIGHTER_API_SECRET`
//!   when the `LIGHTER_ENVIRONMENT` source constant is
//!   `LighterEnvironment::Mainnet`

use std::{error::Error, io, path::PathBuf, str::FromStr};

use nautilus_common::enums::Environment;
use nautilus_core::env::get_env_var;
use nautilus_databento::{data::DatabentoDataClientConfig, factories::DatabentoDataClientFactory};
use nautilus_lighter::{
    common::enums::LighterEnvironment,
    config::{LighterDataClientConfig, LighterExecutionClientConfig},
    factories::{LighterDataClientFactory, LighterExecutionClientFactory},
};
use nautilus_live::node::{LiveNode, config::LiveExecutionEngineConfig};
use nautilus_model::{
    identifiers::{AccountId, InstrumentId, StrategyId, TraderId},
    types::Quantity,
};
use nautilus_trading::examples::strategies::composite_market_maker::{
    CompositeMarketMaker, CompositeMarketMakerConfig,
};

// WARNING: With `DRY_RUN = false`, this strategy submits orders to the configured
// environment and may use real funds. Set `DRY_RUN = true` to connect without
// starting the order-submitting strategy.
const DRY_RUN: bool = false;
const LIGHTER_ENVIRONMENT: LighterEnvironment = LighterEnvironment::Testnet;

const TRADER_ID: &str = "TESTER-001";
const ACCOUNT_ID: &str = "LIGHTER-001";
const INSTRUMENT_ID: &str = "NVDA-PERP.LIGHTER";
const SIGNAL_INSTRUMENT_ID: &str = "NVDA.EQUS";

const MAX_POSITION: &str = "0.20";
const TRADE_SIZE: &str = "0.05";
const HALF_SPREAD_BPS: u32 = 25;
const INVENTORY_SKEW_FACTOR: f64 = 2.0;
const SIGNAL_SKEW_FACTOR: f64 = 55.0;
const REQUOTE_THRESHOLD_BPS: u32 = 5;
const ON_CANCEL_RESUBMIT: bool = false;
const SIGNAL_BASELINE: Option<f64> = None;
const EXPIRE_TIME_SECS: Option<u64> = None;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    dotenvy::dotenv().ok();

    let environment = Environment::Live;
    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")?;
    if api_key.trim().is_empty() {
        return Err(invalid_input_error(
            "DATABENTO_API_KEY must not be empty".to_string(),
        ));
    }

    let publishers_filepath = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../crates/adapters/databento/publishers.json");
    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 max_position = parse_quantity("MAX_POSITION", MAX_POSITION)?;
    let trade_size = parse_quantity("TRADE_SIZE", TRADE_SIZE)?;

    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)
        .maybe_signal_baseline(SIGNAL_BASELINE)
        .maybe_expire_time_secs(EXPIRE_TIME_SECS)
        .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)?
        .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 {
        println!("DRY_RUN is true; starting clients without the CompositeMarketMaker strategy.");
    } else {
        node.add_strategy(CompositeMarketMaker::new(strategy_config))?;
    }

    node.run().await?;

    Ok(())
}

fn parse_quantity(name: &str, value: &str) -> Result<Quantity, Box<dyn Error>> {
    Quantity::from_str(value).map_err(|e| {
        invalid_input_error(format!("{name} must be a quantity; received {value}: {e}"))
    })
}

fn invalid_input_error(message: String) -> Box<dyn Error> {
    io::Error::new(io::ErrorKind::InvalidInput, message).into()
}

```

Shown in full with attribution under the source's licence. Licence: LGPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.