diff --git a/Cargo.toml b/Cargo.toml index c1740b3..a263875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ futures-util = "0.3" # Serialization serde = { version = "1", features = ["derive"] } +simd-json = "0.13" serde_json = "1" # Decimal for precise financial calculations @@ -43,7 +44,11 @@ hex = "0.4" # Utilities chrono = { version = "0.4", features = ["serde"] } url = "2" -uuid = { version = "1", features = ["v4"] } +uuid = { version = "1", features = ["serde", "v4", "v7"] } +log = "0.4.29" +regex = "1.12.3" +parking_lot = "0.12" +disruptor = "4.3.0" [dev-dependencies] tokio-test = "0.4" diff --git a/src/client.rs b/src/client.rs index 989c2b6..a9c2372 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,8 @@ //! HTTP client for Bybit REST API. -use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; -use serde::de::DeserializeOwned; +use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; use serde::Serialize; +use serde::de::DeserializeOwned; use tracing::{debug, warn}; use crate::auth::{generate_signature, get_timestamp}; diff --git a/src/lib.rs b/src/lib.rs index b3a47bf..048dde1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ mod config; mod constants; mod error; mod models; +pub mod utils; // API modules pub mod api; diff --git a/src/models/trade.rs b/src/models/trade.rs index 174ea41..b468d15 100644 --- a/src/models/trade.rs +++ b/src/models/trade.rs @@ -149,7 +149,7 @@ impl PlaceOrderParams { None => { return Err(BybitError::InvalidParam( "price is required for limit orders".into(), - )) + )); } Some(p) => { let price: Decimal = p.parse().map_err(|_| { diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..3b0344e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,131 @@ +use chrono::{DateTime, NaiveDate, TimeDelta, Utc}; +use log::debug; +use std::hash::{Hash, Hasher}; + +// use regex::Captures; +use regex::Regex; +use std::str::FromStr; +use std::sync::OnceLock; + +use crate::websocket::fast_models::parse_float; + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum OptionType { + Put, + Call, +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub struct BybitInfo { + pub base: String, + pub expire: DateTime, + pub strike_price: String, + // pub mantissa: u8, + pub opt_type: OptionType, + pub quote: Option, +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub struct BybitPosition { + pub bybit_info: BybitInfo, + /// true -> long, false -> call + pub side: bool, +} + +pub fn parse_expiration_date(date: &str) -> DateTime { + let naive_date = NaiveDate::parse_from_str(date, "%d%b%y") + .expect("error parsing expire date from bybit symbol"); + + return naive_date + .and_hms_opt(8, 0, 0) + .expect("error creating utc datetime object from bybit symbol") + .and_utc(); +} + +pub fn calculate_years_to_maturity(expire: DateTime) -> f32 { + debug!("expire date time obj: {}", expire); + let time_to_expiration: TimeDelta = expire - Utc::now(); + debug!("time_to_expiration: {}", time_to_expiration); + let seconds_to_expiration = time_to_expiration.num_seconds(); + debug!("seconds_to_expiration: {}", seconds_to_expiration); + + let years_to_expiration = (seconds_to_expiration) as f32 / (60 * 60 * 24 * 365) as f32; + debug!("years_to_expiration: {}", years_to_expiration); + return years_to_expiration; +} + +pub fn extract_bybit_info(symbol: &str) -> Option { + static RE: OnceLock = OnceLock::new(); + + let re = RE.get_or_init(|| { + Regex::new(r"(?\w+)-(?\d+\w+\d+)-(?\d+\.?\d*)-(?C|P)(?:-(?USDT))?") + .expect("invalid regex extracting bybit infos from symbol!") + }); + + re.captures(symbol).map(|caps| { + let strike_price = &caps["strike_price"]; + // let strike_price_split: Vec<&str> = strike_price.split(".").collect(); + // let mantissa = strike_price_split[1].len(); + BybitInfo { + base: caps["base"].to_string(), + expire: parse_expiration_date(&caps["expire"]), + strike_price: strike_price.to_string(), + // mantissa: mantissa as u8, + opt_type: match &caps["side"] { + "C" => OptionType::Call, + "P" => OptionType::Put, + _ => unreachable!(), + }, + quote: caps.name("quote").map(|m| m.as_str().to_string()), + } + }) +} + +use reqwest::{Client, Error}; +use serde::{Deserialize, Serialize}; + +// Definiamo le strutture per mappare il JSON di Bybit +#[derive(Debug, Serialize, Deserialize)] +pub struct HttpBybitResponse { + #[serde(rename = "retCode")] + pub ret_code: i32, + #[serde(rename = "retMsg")] + pub ret_msg: String, + pub result: TickersResult, + pub time: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TickersResult { + pub category: String, + pub list: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OptionTicker { + pub symbol: String, + pub bid1_price: String, + pub ask1_price: String, + pub last_price: String, + pub mark_price: String, + pub open_interest: String, + #[serde(deserialize_with = "parse_float")] + pub delta: f32, + // Aggiungi altri campi se necessario +} + +/// Funzione di libreria che restituisce il JSON giĆ  parsato in una Struct +pub async fn get_option_tickers_json(base_coin: &str) -> Result { + let client = Client::new(); + let url = "https://api.bybit.com/v5/market/tickers"; + + let response = client + .get(url) + .query(&[("category", "option"), ("baseCoin", base_coin)]) + .send() + .await?; + + // Deserializza automaticamente il corpo della risposta nel tipo BybitResponse + response.json::().await +} diff --git a/src/websocket/client.rs b/src/websocket/client.rs index ef8e7bc..d2bd24e 100644 --- a/src/websocket/client.rs +++ b/src/websocket/client.rs @@ -1,22 +1,30 @@ //! WebSocket client implementation. +use disruptor::{MultiProducer, Producer, SingleConsumerBarrier}; use futures_util::{SinkExt, StreamExt}; +use parking_lot::Mutex; +// use tracing_subscriber::field::debug; +use log::{debug, error, info}; use std::collections::HashMap; use std::sync::Arc; use tokio::net::TcpStream; use tokio::sync::{mpsc, RwLock}; use tokio::time::{interval, Duration}; use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream}; -use tracing::{debug, error, info, warn}; use crate::auth::{generate_ws_signature, get_timestamp}; use crate::config::WsConfig; use crate::error::{BybitError, Result}; +// use crate::utils::BybitResponse; +use crate::websocket::fast_models::BybitResponse; use crate::websocket::models::*; +use crate::{MAINNET_WS_TRADE, TESTNET_WS_TRADE}; type WsStream = WebSocketStream>; type Callback = Arc; +// use simd_json::prelude::*; + /// WebSocket client for Bybit streaming API. pub struct BybitWebSocket { config: WsConfig, @@ -24,6 +32,8 @@ pub struct BybitWebSocket { callbacks: Arc>>, tx: Option>, is_connected: Arc>, + is_trade: bool, + producer: Option>, } impl BybitWebSocket { @@ -35,6 +45,8 @@ impl BybitWebSocket { callbacks: Arc::new(RwLock::new(HashMap::new())), tx: None, is_connected: Arc::new(RwLock::new(false)), + is_trade: false, + producer: None, } } @@ -46,13 +58,23 @@ impl BybitWebSocket { callbacks: Arc::new(RwLock::new(HashMap::new())), tx: None, is_connected: Arc::new(RwLock::new(false)), + is_trade: url == MAINNET_WS_TRADE || url == TESTNET_WS_TRADE, + producer: None, } } + pub fn set_disruptor_producer( + &mut self, + producer: MultiProducer, + ) { + self.producer = Some(producer); + debug!("producer set ",); + } + /// Connect to the WebSocket server. pub async fn connect(&mut self) -> Result<()> { let url = &self.config.url; - info!(url = %url, "Connecting to WebSocket"); + info!("Connecting to WebSocket: {}", url); let (ws_stream, _) = connect_async(url) .await @@ -101,126 +123,167 @@ impl BybitWebSocket { } }); + // let mut read_clone = read.clone(); // Spawn read task - let callbacks = self.callbacks.clone(); - let is_connected = self.is_connected.clone(); - let subscriptions = self.subscriptions.clone(); - let config = self.config.clone(); - let tx_reconnect = tx.clone(); + // let callbacks = self.callbacks.clone(); + // let is_connected = self.is_connected.clone(); + // let subscriptions = self.subscriptions.clone(); + // let config = self.config.clone(); + // let tx_reconnect = tx.clone(); + + // tokio::spawn(async move { + // Self::handle_messages( + // read, + // callbacks, + // is_connected, + // subscriptions, + // config, + // tx_reconnect, + // ) + // .await; + // }); + // let dis = disruptor::build_multi_producer(64, || BybitResponse::Empty(), BusySpinWithSpinLoopHint) + // .pin_at_core(1) + // .handle_events_with(|_,_,_| {}) + // .build(); + + let producer_clone = self.producer.clone().unwrap(); tokio::spawn(async move { - Self::handle_messages( - read, - callbacks, - is_connected, - subscriptions, - config, - tx_reconnect, - ) - .await; + Self::faster_handle(read, producer_clone).await; }); info!("WebSocket connected"); Ok(()) } - /// Handle incoming messages. - async fn handle_messages( + async fn faster_handle( mut read: futures_util::stream::SplitStream, - callbacks: Arc>>, - is_connected: Arc>, - _subscriptions: Arc>>, - _config: WsConfig, - _tx: mpsc::Sender, + mut producer: MultiProducer, ) { - while let Some(msg_result) = read.next().await { - match msg_result { - Ok(Message::Text(text)) => { - // Try to parse as JSON - let json: serde_json::Value = match serde_json::from_str(&text) { - Ok(v) => v, - Err(e) => { - warn!( - "Failed to parse message: {}, text: {}", - e, - &text[..text.len().min(200)] - ); - continue; // Don't panic, continue processing - } - }; - - // Handle different message types - if is_pong(&json) { - debug!("Pong received"); - continue; - } - - if is_auth_response(&json) { - if json - .get("success") - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - info!("Authentication successful"); - } else { - error!("Authentication failed: {:?}", json); - } - continue; - } - - if is_subscription_response(&json) { - if json - .get("success") - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - debug!("Subscription successful"); - } else { - warn!("Subscription failed: {:?}", json); - } - continue; - } - - // Handle data message - if is_data_message(&json) { - if let Ok(ws_msg) = serde_json::from_value::(json) { - let cbs = callbacks.read().await; - if let Some(callback) = cbs.get(&ws_msg.topic) { - callback(ws_msg.clone()); - } else { - // Try to find matching callback by prefix - for (topic, callback) in cbs.iter() { - if ws_msg - .topic - .starts_with(topic.split('.').next().unwrap_or("")) - { - callback(ws_msg.clone()); - break; - } - } - } - } - } - } - Ok(Message::Ping(_)) => { - debug!("Received ping frame"); - // Tungstenite handles pong automatically - } - Ok(Message::Close(_)) => { - info!("WebSocket closed"); - *is_connected.write().await = false; - break; + while let Some(msg) = read.next().await { + let Ok(Message::Text(text)) = msg else { + continue; + }; + + debug!("gate from socket: {}", &text); + + match serde_json::from_str::(&text) { + Ok(response) => { + // debug!("{}", text); + producer.publish(|e| { + *e = response; + }); } - Err(e) => { - error!("WebSocket error: {}", e); - *is_connected.write().await = false; - break; + Err(error) => { + error!("fuck! error parsing message from websocket {}", error) } - _ => {} } } } + /// Handle incoming messages. + // async fn handle_messages( + // mut read: futures_util::stream::SplitStream, + // callbacks: Arc>>, + // is_connected: Arc>, + // _subscriptions: Arc>>, + // _config: WsConfig, + // _tx: mpsc::Sender, + // ) { + // while let Some(msg_result) = read.next().await { + // match msg_result { + // Ok(Message::Text(text)) => { + // // Try to parse as JSON + // let json: serde_json::Value = match serde_json::from_str(&text) { + // Ok(v) => v, + // Err(e) => { + // warn!( + // "Failed to parse message: {}, text: {}", + // e, + // &text[..text.len().min(200)] + // ); + // continue; // Don't panic, continue processing + // } + // }; + + // // Handle different message types + // if is_pong(&json) { + // debug!("Pong received"); + // continue; + // } + + // if is_auth_response(&json) { + // if json + // .get("success") + // .and_then(|v| v.as_bool()) + // .unwrap_or(false) + // || json.get("retCode").and_then(|v| v.as_i64()) == Some(0) + // // ^^^ this is for *_WS_TRADE ^^^ + // // https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline#response-parameters + // { + // info!("Authentication successful"); + // } else { + // error!("Authentication failed: {:?}", json); + // } + // continue; + // } + + // if is_subscription_response(&json) { + // if json + // .get("success") + // .and_then(|v| v.as_bool()) + // .unwrap_or(false) + // { + // debug!("Subscription successful"); + // } else { + // warn!("Subscription failed: {:?}", json); + // } + // continue; + // } + + // // Handle data message + // if is_data_message(&json) { + // if let Ok(ws_msg) = serde_json::from_value::(json) { + // let cbs = callbacks.read().await; + // if let Some(callback) = cbs.get(&ws_msg.topic) { + // callback(ws_msg.clone()); + // } else { + // // Try to find matching callback by prefix + // for (topic, callback) in cbs.iter() { + // if ws_msg + // .topic + // .starts_with(topic.split('.').next().unwrap_or("")) + // { + // callback(ws_msg.clone()); + // break; + // } + // } + // } + // } + // } + + // debug!("{:#?}", text); + // } + // Ok(Message::Ping(_)) => { + // debug!("Received ping frame"); + // // Tungstenite handles pong automatically + // } + // Ok(Message::Close(_)) => { + // info!("WebSocket closed"); + // *is_connected.write().await = false; + // break; + // } + // Err(e) => { + // error!("WebSocket error: {}", e); + // *is_connected.write().await = false; + // break; + // } + // _ => {} + // } + // } + // } + /// Authenticate with the server (for private channels). async fn authenticate(&self) -> Result<()> { let api_key = self @@ -234,19 +297,31 @@ impl BybitWebSocket { .as_ref() .ok_or_else(|| BybitError::Auth("API secret not set".into()))?; + // 10_000 secs are three hours let expires = get_timestamp() + 10000; let signature = generate_ws_signature(api_secret, expires); - let auth_msg = WsAuthRequest { - req_id: uuid::Uuid::new_v4().to_string(), - op: "auth".to_string(), - args: vec![ - serde_json::Value::String(api_key.clone()), - serde_json::Value::Number(expires.into()), - serde_json::Value::String(signature), - ], + let auth_msg = if self.is_trade { + AuthRequest::Trade(WsTradeAuthRequest { + req_id: uuid::Uuid::new_v4().to_string(), + op: "auth".to_string(), + args: vec![ + serde_json::Value::String(api_key.clone()), + serde_json::Value::Number(expires.into()), + serde_json::Value::String(signature), + ], + }) + } else { + AuthRequest::Public(WsAuthRequest { + req_id: uuid::Uuid::new_v4().to_string(), + op: "auth".to_string(), + args: vec![ + serde_json::Value::String(api_key.clone()), + serde_json::Value::Number(expires.into()), + serde_json::Value::String(signature), + ], + }) }; - let msg = serde_json::to_string(&auth_msg).map_err(|e| BybitError::Parse(e.to_string()))?; self.send(msg).await?; @@ -291,6 +366,50 @@ impl BybitWebSocket { self.send(msg).await } + pub async fn subscribe_mut(&mut self, topics: Vec, callback: F) -> Result<()> + where + F: FnMut(WsMessage) + Send + Sync + 'static, + { + // 1. Wrap the FnMut in a Mutex to "convert" it to an Fn closure + let callback_mutable = Mutex::new(callback); + + // 2. Create an Fn closure that locks the mutex and calls the inner FnMut + let wrapped_callback = move |msg: WsMessage| { + let mut cb = callback_mutable.lock(); + (&mut *cb)(msg); + }; + + // 3. Wrap in Arc and cast to your existing Callback type + let callback_arc = Arc::new(wrapped_callback) as Callback; + + // --- The rest of the logic remains the same as your original function --- + + // Register callbacks + { + let mut cbs = self.callbacks.write().await; + for topic in &topics { + cbs.insert(topic.clone(), callback_arc.clone()); + } + } + + // Store subscriptions + { + let mut subs = self.subscriptions.write().await; + subs.extend(topics.clone()); + } + + // Send subscription request + let sub_msg = WsRequest { + req_id: uuid::Uuid::new_v4().to_string(), + op: "subscribe".to_string(), + args: topics, + }; + + let msg = serde_json::to_string(&sub_msg).map_err(|e| BybitError::Parse(e.to_string()))?; + + self.send(msg).await + } + /// Unsubscribe from topics. pub async fn unsubscribe(&mut self, topics: Vec) -> Result<()> { // Remove callbacks @@ -332,6 +451,30 @@ impl BybitWebSocket { Ok(()) } + pub fn send_sync(&self, msg: String) { + if let Some(tx) = &self.tx { + debug!("about to send message into socket: {}", &msg); + match tx.try_send(Message::Text(msg)) { + Ok(mex) => { + debug!("sent message into socket {}: {:#?}", self.config.url, &mex) + } + Err(err) => error!("{}", err), + } + } + } + + pub async fn send_order(&self, order: WsTradeOrder) -> Result<()> { + debug!("{:#?}", order); + if !self.is_trade { + error!("can t execute a trade on a non trade socket"); + return Err(BybitError::Parse( + "can t execute a trade on a non trade socket".to_string(), + )); + } + let msg = serde_json::to_string(&order).map_err(|e| BybitError::Parse(e.to_string()))?; + self.send(msg).await + } + /// Check if connected. pub async fn is_connected(&self) -> bool { *self.is_connected.read().await diff --git a/src/websocket/fast_models.rs b/src/websocket/fast_models.rs new file mode 100644 index 0000000..63b502e --- /dev/null +++ b/src/websocket/fast_models.rs @@ -0,0 +1,113 @@ +use serde::{Deserialize, Deserializer}; +use std::fmt; +// use serde_with + + + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +pub enum BybitResponse { + Ticker(TickerSnapshot), + Command(CommandResponse), + Execution(ExecutionResponse), + Any(String), + Empty(), +} + +#[derive(Deserialize, Debug, Clone)] +pub struct TickerSnapshot { + pub topic: String, + pub ts: u64, + #[serde(rename = "type")] + pub msg_type: String, + pub data: TickerData, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TickerData { + pub symbol: String, + #[serde(deserialize_with = "parse_float")] + pub bid_price: f32, + #[serde(deserialize_with = "parse_float")] + pub bid_size: f32, + #[serde(deserialize_with = "parse_float")] + pub bid_iv: f32, + #[serde(deserialize_with = "parse_float")] + pub ask_price: f32, + #[serde(deserialize_with = "parse_float")] + pub ask_size: f32, + #[serde(deserialize_with = "parse_float")] + pub ask_iv: f32, + #[serde(deserialize_with = "parse_float")] + pub last_price: f32, + pub high_price24h: String, + pub low_price24h: String, + #[serde(deserialize_with = "parse_float")] + pub mark_price: f32, + pub index_price: String, + #[serde(deserialize_with = "parse_float")] + pub mark_price_iv: f32, + pub underlying_price: String, + pub open_interest: String, + pub turnover24h: String, + pub volume24h: String, + pub total_volume: String, + pub total_turnover: String, + pub delta: String, + pub gamma: String, + pub vega: String, + pub theta: String, + pub predicted_delivery_price: String, + pub change24h: String, +} + +#[derive(Deserialize, Debug)] +pub struct CommandResponse { + pub op: String, + #[serde(alias = "retCode")] + pub ret_code: Option, + #[serde(alias = "retMsg")] + pub ret_msg: Option, + pub success: Option, + #[serde(alias = "connId", alias = "conn_id")] + pub conn_id: Option, +} + +#[derive(Deserialize, Debug)] +pub struct ExecutionResponse { + pub success: bool, + #[serde(rename = "type")] + pub msg_type: String, + #[serde(alias = "connId", alias = "conn_id")] + pub conn_id: Option, +} + +impl fmt::Display for TickerData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Scrivi qui come vuoi che appaia la stringa + write!( + f, + "Symbol: {} | Mark Price: {} | Delta: {}", + self.symbol, self.mark_price, self.delta + ) + } +} + +pub fn parse_float<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + // Inner enum to accept either a raw number or a string from the input JSON + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrFloat { + String(String), + Float(f32), + } + + match StringOrFloat::deserialize(deserializer)? { + StringOrFloat::String(s) => s.parse::().map_err(serde::de::Error::custom), + StringOrFloat::Float(f) => Ok(f), + } +} diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs index a6f53ce..b2ba487 100644 --- a/src/websocket/mod.rs +++ b/src/websocket/mod.rs @@ -2,6 +2,7 @@ mod client; mod models; +pub mod fast_models; pub use client::BybitWebSocket; pub use models::*; diff --git a/src/websocket/models.rs b/src/websocket/models.rs index 3140ffb..06ad322 100644 --- a/src/websocket/models.rs +++ b/src/websocket/models.rs @@ -24,6 +24,31 @@ pub struct WsAuthRequest { pub args: Vec, } +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WsTradeAuthRequest { + pub req_id: String, + pub op: String, + pub args: Vec, +} + +#[derive(Serialize)] +#[serde(untagged)] // Fondamentale per mantenere il formato JSON originale +pub enum AuthRequest { + Trade(WsTradeAuthRequest), + Public(WsAuthRequest), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WsTradeAuthResponse { + pub req_id: String, + pub ret_code: i32, + pub ret_msg: String, + pub op: String, + pub conn_id: String, +} + /// WebSocket response. #[derive(Debug, Clone, Deserialize)] pub struct WsResponse { @@ -103,6 +128,140 @@ pub struct WsPong { pub op: Option, } + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WsTradeOrderHeader { + #[serde(rename = "X-BAPI-TIMESTAMP")] + pub x_bapi_timestamp: String, + // #[serde(rename = "X-BAPI-RECV-WINDOW")] + // pub x_bapi_recv_window: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum WsTradeOrderCategory { + Spot, + Linear, + Inverse, + Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum WsTradeOrderOp { + Create, + Amend, + Delete +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WsTradeOrderArgs { + pub category: String, // linear, inverse, spot, option + pub symbol: String, + pub side: String, // Buy, Sell + pub order_type: String, // Market, Limit + pub qty: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub price: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub is_leverage: Option, // 0: false, 1: true + + #[serde(skip_serializing_if = "Option::is_none")] + pub market_unit: Option, // baseCoin, quoteCoin + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub time_in_force: Option, // GTC, IOC, FOK, PostOnly + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub order_link_id: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub take_profit: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub stop_loss: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub tp_trigger_by: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub sl_trigger_by: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub reduce_only: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub close_on_trigger: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub position_idx: Option, // 0, 1, 2 + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub trigger_price: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub trigger_by: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub tp_limit_price: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub sl_limit_price: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub tp_order_type: Option, + + // #[serde(skip_serializing_if = "Option::is_none")] + // pub sl_order_type: Option, + +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OptionTickerData { + pub symbol: String, + pub ask_iv: String, + pub ask_price: String, + pub ask_size: String, + pub bid_iv: String, + pub bid_price: String, + + pub bid_size: String, + + pub delta: String, + + pub gamma: String, + + pub theta: String, + + pub vega: String, + + pub mark_price: String, + + pub index_price: String, + + pub underlying_price: String, + + pub open_interest: String, + + pub volume24h: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WsTradeOrder { + pub req_id: uuid::Uuid, + pub header: WsTradeOrderHeader, + /// order.create order.amend order.cancel + pub op: String, + pub args: Vec + + +} + /// Check if message is a pong response. pub fn is_pong(msg: &serde_json::Value) -> bool { if let Some(op) = msg.get("op").and_then(|v| v.as_str()) {