use nautilus_common::enums::Environment;
use nautilus_derive::{
common::{
consts::DERIVE_CLIENT_ID,
enums::{DeriveEnvironment, DeriveInstrumentType},
},
config::DeriveDataClientConfig,
factories::DeriveDataClientFactory,
};
use nautilus_live::node::LiveNode;
use nautilus_model::{
data::BarType,
identifiers::{InstrumentId, TraderId},
stubs::TestDefault,
};
use nautilus_testkit::testers::{DataTester, DataTesterConfig};
const TOKEN: &str = "ETH";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let environment = Environment::Live;
let derive_environment = derive_environment_from_env();
let trader_id = TraderId::test_default();
let node_name = "DERIVE-DATA-TESTER-001".to_string();
let setup = InstrumentSetup::from_env(TOKEN)?;
let instrument_id = InstrumentId::from(format!("{}.DERIVE", setup.symbol).as_str());
let bar_type =
BarType::from(format!("{}.DERIVE-15-MINUTE-LAST-EXTERNAL", setup.symbol).as_str());
let derive_config = DeriveDataClientConfig {
environment: derive_environment,
currencies: vec![TOKEN.to_string()],
..Default::default()
};
let client_factory = DeriveDataClientFactory::new();
let client_id = *DERIVE_CLIENT_ID;
let mut node = LiveNode::builder(trader_id, environment)?
.with_name(node_name)
.with_delay_post_stop_secs(2)
.add_data_client(None, Box::new(client_factory), Box::new(derive_config))?
.build()?;
let tester_config = DataTesterConfig::builder()
.client_id(client_id)
.instrument_ids(vec![instrument_id])
.bar_types(vec![bar_type])
.subscribe_quotes(true)
.subscribe_trades(true)
.subscribe_mark_prices(true)
.subscribe_index_prices(true)
.subscribe_funding_rates(setup.has_funding)
.subscribe_option_greeks(setup.has_greeks)
.request_instruments(true)
.request_trades(true)
.request_bars(true)
.request_funding_rates(setup.has_funding)
.manage_book(true)
.build();
let tester = DataTester::new(tester_config);
node.add_actor(tester)?;
node.run().await?;
Ok(())
}
struct InstrumentSetup {
symbol: String,
has_funding: bool,
has_greeks: bool,
}
impl InstrumentSetup {
fn from_env(token: &str) -> Result<Self, Box<dyn std::error::Error>> {
let kind = instrument_type_from_env()?;
let mut setup = Self::resolve(kind, token);
if let Ok(symbol) = std::env::var("DERIVE_DATA_TESTER_SYMBOL")
&& !symbol.trim().is_empty()
{
setup.symbol = symbol.trim().to_string();
}
Ok(setup)
}
fn resolve(kind: DeriveInstrumentType, token: &str) -> Self {
match kind {
DeriveInstrumentType::Perp => Self {
symbol: format!("{token}-PERP"),
has_funding: true,
has_greeks: false,
},
DeriveInstrumentType::Option => Self {
symbol: format!("{token}-20260627-3500-C"),
has_funding: false,
has_greeks: true,
},
DeriveInstrumentType::Erc20 => Self {
symbol: format!("{token}-USDC"),
has_funding: false,
has_greeks: false,
},
}
}
}
fn instrument_type_from_env() -> Result<DeriveInstrumentType, Box<dyn std::error::Error>> {
match std::env::var("DERIVE_DATA_TESTER_KIND") {
Ok(value) if value.eq_ignore_ascii_case("spot") => Ok(DeriveInstrumentType::Erc20),
Ok(value) if value.eq_ignore_ascii_case("erc20") => Ok(DeriveInstrumentType::Erc20),
Ok(value) if value.eq_ignore_ascii_case("option") => Ok(DeriveInstrumentType::Option),
Ok(value) if value.eq_ignore_ascii_case("perp") => Ok(DeriveInstrumentType::Perp),
Ok(value) if value.eq_ignore_ascii_case("perpetual") => Ok(DeriveInstrumentType::Perp),
Ok(value) => Err(format!("unsupported DERIVE_DATA_TESTER_KIND: {value}").into()),
Err(_) => Ok(DeriveInstrumentType::Perp),
}
}
fn derive_environment_from_env() -> DeriveEnvironment {
match std::env::var("DERIVE_ENVIRONMENT") {
Ok(value)
if value.eq_ignore_ascii_case("mainnet") || value.eq_ignore_ascii_case("live") =>
{
DeriveEnvironment::Mainnet
}
_ => DeriveEnvironment::Testnet,
}
}