use std::time::Duration;
use nautilus_bybit::{
common::enums::{BybitPositionIdx, BybitProductType},
http::client::BybitHttpClient,
};
use nautilus_core::UUID4;
use nautilus_model::{
enums::{OrderSide, OrderType, PositionSide, TimeInForce},
identifiers::{AccountId, ClientOrderId},
reports::position::PositionStatusReport,
};
const VENUE_SUFFIX: &str = "BYBIT";
const FLATTEN_PRODUCT_TYPES: &[BybitProductType] =
&[BybitProductType::Linear, BybitProductType::Inverse];
fn env_flag(name: &str) -> bool {
std::env::var(name)
.ok()
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(false)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
nautilus_common::logging::ensure_logging_initialized();
let demo = env_flag("BYBIT_DEMO");
let testnet = env_flag("BYBIT_TESTNET");
if demo && testnet {
anyhow::bail!("BYBIT_DEMO and BYBIT_TESTNET cannot both be set");
}
let env_label = if demo {
"DEMO"
} else if testnet {
"TESTNET"
} else {
"MAINNET"
};
log::warn!("Bybit flatten starting against {env_label}");
let client = BybitHttpClient::new_with_env(
None, None, None, demo, testnet, 30, 3, 1_000, 10_000, 5_000, None,
)?;
let account_id = AccountId::new(format!("{VENUE_SUFFIX}-UNIFIED"));
for product_type in FLATTEN_PRODUCT_TYPES {
let instruments = client
.request_instruments(*product_type, None, None)
.await?;
log::info!(
"Bootstrapped {} {product_type:?} instruments",
instruments.len()
);
}
let mut closed_any = false;
for product_type in FLATTEN_PRODUCT_TYPES {
let positions = client
.request_position_status_reports(account_id, *product_type, None)
.await?;
let open: Vec<&PositionStatusReport> = positions
.iter()
.filter(|p| p.position_side != PositionSide::Flat && !p.quantity.is_zero())
.collect();
if open.is_empty() {
log::info!("{product_type:?}: no open positions");
continue;
}
log::info!("{product_type:?}: closing {} position(s)", open.len());
for position in &open {
close_position(&client, account_id, *product_type, position).await?;
closed_any = true;
}
}
if !closed_any {
log::info!("Flatten complete: nothing to close");
return Ok(());
}
tokio::time::sleep(Duration::from_secs(2)).await;
verify_flat(&client, account_id).await
}
async fn close_position(
client: &BybitHttpClient,
account_id: AccountId,
product_type: BybitProductType,
position: &PositionStatusReport,
) -> anyhow::Result<()> {
let instrument_id = position.instrument_id;
match client
.cancel_all_orders(account_id, product_type, instrument_id)
.await
{
Ok(cancelled) => {
if !cancelled.is_empty() {
log::info!(
"{instrument_id}: cancelled {} open order(s)",
cancelled.len(),
);
}
}
Err(e) => log::warn!("{instrument_id}: cancel_all_orders failed: {e}"),
}
let close_side = match position.position_side {
PositionSide::Long => OrderSide::Sell,
PositionSide::Short => OrderSide::Buy,
PositionSide::Flat => return Ok(()),
};
let position_idx = position
.venue_position_id
.as_ref()
.and_then(|pid| infer_position_idx(pid.as_str(), position.position_side));
let cid = ClientOrderId::new(format!("flatten-{}", UUID4::new()));
log::info!(
"{instrument_id}: closing {} {} via reduce-only Market",
position.quantity,
close_side,
);
let report = client
.submit_order(
account_id,
product_type,
instrument_id,
cid,
close_side,
OrderType::Market,
position.quantity,
Some(TimeInForce::Ioc),
None,
None,
Some(false),
true,
false,
false,
position_idx,
None,
None,
None,
None,
)
.await?;
log::info!(
"{instrument_id}: submitted close venue_order_id={}",
report.venue_order_id,
);
Ok(())
}
fn infer_position_idx(venue_pid: &str, side: PositionSide) -> Option<BybitPositionIdx> {
if venue_pid.ends_with("-LONG") {
Some(BybitPositionIdx::BuyHedge)
} else if venue_pid.ends_with("-SHORT") {
Some(BybitPositionIdx::SellHedge)
} else {
match side {
PositionSide::Long => Some(BybitPositionIdx::OneWay),
PositionSide::Short => Some(BybitPositionIdx::OneWay),
PositionSide::Flat => None,
}
}
}
async fn verify_flat(client: &BybitHttpClient, account_id: AccountId) -> anyhow::Result<()> {
let mut residual = Vec::new();
for product_type in FLATTEN_PRODUCT_TYPES {
let positions = client
.request_position_status_reports(account_id, *product_type, None)
.await?;
for p in positions {
if p.position_side != PositionSide::Flat && !p.quantity.is_zero() {
residual.push((product_type, p));
}
}
}
if residual.is_empty() {
log::info!("Flatten complete: all derivatives positions closed");
return Ok(());
}
for (product_type, p) in &residual {
log::error!(
"Residual {product_type:?} position: {} side={:?} qty={}",
p.instrument_id,
p.position_side,
p.quantity,
);
}
anyhow::bail!("{} residual position(s) after flatten", residual.len())
}