use std::{
collections::{BTreeMap, VecDeque},
str::FromStr,
};
use ahash::AHashMap;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use serde_with::{DisplayFromStr, serde_as};
use v_exchanges_adapters::bybit::BybitOption;
use v_utils::{
trades::{Kline, Ohlc, Pair},
utils::filter_nulls,
};
use super::{BybitInterval, BybitIntervalTime};
use crate::{
ExchangeName, ExchangeResult, Instrument, Symbol,
core::{ExchangeInfo, Klines, OpenInterest, PairInfo, RequestRange},
};
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KlineResponse {
pub result: ResponseResult,
pub ret_code: i32,
pub ret_ext_info: AHashMap<String, serde_json::Value>,
pub ret_msg: String,
pub time: i64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseResult {
pub category: String,
pub list: Vec<KlineData>,
pub symbol: String,
}
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
pub struct KlineData(
#[serde_as(as = "DisplayFromStr")] pub i64,
#[serde_as(as = "DisplayFromStr")] pub f64,
#[serde_as(as = "DisplayFromStr")] pub f64,
#[serde_as(as = "DisplayFromStr")] pub f64,
#[serde_as(as = "DisplayFromStr")] pub f64,
#[serde_as(as = "DisplayFromStr")] pub f64,
#[serde_as(as = "DisplayFromStr")] pub f64,
);
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketTickerResponse {
pub ret_code: i32,
pub ret_msg: String,
pub result: MarketTickerResult,
pub ret_ext_info: AHashMap<String, Value>,
pub time: i64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketTickerResult {
pub category: String,
pub list: Vec<MarketTickerData>,
}
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketTickerData {
pub symbol: String,
#[serde_as(as = "DisplayFromStr")]
pub last_price: f64,
#[serde_as(as = "DisplayFromStr")]
pub index_price: f64,
#[serde_as(as = "DisplayFromStr")]
pub mark_price: f64,
#[serde_as(as = "DisplayFromStr")]
pub prev_price24h: f64,
#[serde_as(as = "DisplayFromStr")]
pub price24h_pcnt: f64,
#[serde_as(as = "DisplayFromStr")]
pub high_price24h: f64,
#[serde_as(as = "DisplayFromStr")]
pub low_price24h: f64,
#[serde_as(as = "DisplayFromStr")]
pub prev_price1h: f64,
#[serde_as(as = "DisplayFromStr")]
pub open_interest: f64,
#[serde_as(as = "DisplayFromStr")]
pub open_interest_value: f64,
#[serde_as(as = "DisplayFromStr")]
pub turnover24h: f64,
#[serde_as(as = "DisplayFromStr")]
pub volume24h: f64,
#[serde_as(as = "DisplayFromStr")]
pub funding_rate: f64,
pub next_funding_time: String,
#[serde_as(as = "DisplayFromStr")]
pub bid1_price: f64,
#[serde_as(as = "DisplayFromStr")]
pub bid1_size: f64,
#[serde_as(as = "DisplayFromStr")]
pub ask1_price: f64,
#[serde_as(as = "DisplayFromStr")]
pub ask1_size: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenInterestResponse {
pub ret_code: i32,
pub ret_msg: String,
pub result: OpenInterestResult,
pub ret_ext_info: AHashMap<String, Value>,
pub time: i64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenInterestResult {
pub symbol: String,
pub category: String,
pub list: Vec<OpenInterestData>,
pub next_page_cursor: String,
}
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenInterestData {
#[serde_as(as = "DisplayFromStr")]
pub open_interest: f64,
#[serde_as(as = "DisplayFromStr")]
pub timestamp: i64,
}
pub(super) async fn klines(client: &v_exchanges_adapters::Client, symbol: Symbol, tf: BybitInterval, range: RequestRange) -> ExchangeResult<Klines> {
range.ensure_allowed(1..=1000, &tf)?;
let range_json = range.serialize(ExchangeName::Bybit);
let base_params = filter_nulls(json!({
"category": "linear", "symbol": symbol.pair.fmt_bybit(),
"interval": tf.to_string(),
}));
let mut base_map = base_params.as_object().unwrap().clone();
let range_map = range_json.as_object().unwrap();
base_map.extend(range_map.clone());
let params = filter_nulls(serde_json::Value::Object(base_map));
let options = vec![BybitOption::None];
let kline_response: KlineResponse = client.get("/v5/market/kline", ¶ms, options).await?;
let mut klines = VecDeque::with_capacity(kline_response.result.list.len());
for k in kline_response.result.list {
if kline_response.time > k.0 + tf.duration().as_millis() as i64
{
klines.push_back(Kline {
open_time: Timestamp::from_millisecond(k.0).unwrap(),
ohlc: Ohlc {
open: k.1,
close: k.2,
high: k.3,
low: k.4,
},
volume_quote: k.5,
trades: None,
taker_buy_volume_quote: None,
});
}
}
Ok(Klines::new(klines, *tf))
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TickerPriceEntry {
symbol: String,
#[serde_as(as = "Option<DisplayFromStr>")]
last_price: Option<f64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TickerPriceResult {
list: Vec<TickerPriceEntry>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TickerPriceResponse {
result: TickerPriceResult,
}
pub(super) async fn prices(client: &v_exchanges_adapters::Client, pairs: Option<Vec<Pair>>, instrument: Instrument) -> ExchangeResult<BTreeMap<Pair, f64>> {
let category = match instrument {
Instrument::Perp => "linear",
_ => unimplemented!(),
};
let params = filter_nulls(json!({ "category": category }));
let options = vec![BybitOption::None];
let response: TickerPriceResponse = client.get("/v5/market/tickers", ¶ms, options).await?;
let mut price_map = BTreeMap::default();
for entry in response.result.list {
let Some(price) = entry.last_price else { continue };
let Ok(pair) = Pair::from_str(&entry.symbol) else { continue };
if let Some(ref requested) = pairs
&& !requested.contains(&pair)
{
continue;
}
price_map.insert(pair, price);
}
Ok(price_map)
}
pub(super) async fn open_interest(client: &v_exchanges_adapters::Client, symbol: Symbol, tf: BybitIntervalTime, range: RequestRange) -> ExchangeResult<Vec<OpenInterest>> {
range.ensure_allowed(1..=200, &tf)?;
let range_json = range.serialize(ExchangeName::Bybit);
let base_params = filter_nulls(json!({
"category": "linear",
"symbol": symbol.pair.fmt_bybit(),
"intervalTime": tf.to_string(),
}));
let mut base_map = base_params.as_object().unwrap().clone();
let range_map = range_json.as_object().unwrap();
base_map.extend(range_map.clone());
let params = filter_nulls(serde_json::Value::Object(base_map));
let options = vec![BybitOption::None];
let response: OpenInterestResponse = client.get("/v5/market/open-interest", ¶ms, options).await?;
if response.result.list.is_empty() {
return Err(crate::ExchangeError::Other(eyre::eyre!("No open interest data returned")));
}
let price = if symbol.instrument == Instrument::PerpInverse {
let params = filter_nulls(json!({
"category": "linear",
"symbol": symbol.pair.fmt_bybit(),
}));
let options = vec![BybitOption::None];
let ticker_response: MarketTickerResponse = client.get("/v5/market/tickers", ¶ms, options).await?;
Some(ticker_response.result.list[0].last_price)
} else {
None
};
let mut result = Vec::with_capacity(response.result.list.len());
for data in response.result.list {
let (val_asset, val_quote) = match symbol.instrument {
Instrument::PerpInverse => {
let val_quote = data.open_interest;
let price = price.expect("price should be set for PerpInverse");
let val_asset = val_quote / price;
(val_asset, Some(val_quote))
}
Instrument::Perp => (data.open_interest, None),
_ => unreachable!(),
};
result.push(OpenInterest {
val_asset,
val_quote,
timestamp: Timestamp::from_millisecond(data.timestamp).unwrap(),
..Default::default()
});
}
Ok(result)
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct InstrumentsInfoResponse {
result: InstrumentsInfoResult,
time: i64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct InstrumentsInfoResult {
list: Vec<InstrumentInfo>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct LotSizeFilter {
qty_step: String,
}
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct InstrumentInfo {
symbol: String,
price_scale: String,
#[serde_as(as = "DisplayFromStr")]
delivery_time: i64,
lot_size_filter: LotSizeFilter,
}
pub(super) async fn exchange_info(client: &v_exchanges_adapters::Client, instrument: Instrument) -> ExchangeResult<ExchangeInfo> {
let category = match instrument {
Instrument::Perp => "linear",
Instrument::PerpInverse => "inverse",
_ => unimplemented!(),
};
let response: InstrumentsInfoResponse = client
.get("/v5/market/instruments-info", &[("category", category), ("limit", "1000")], vec![BybitOption::None])
.await?;
let server_time = Timestamp::from_millisecond(response.time).expect("Bybit time is valid ms");
let pairs = response
.result
.list
.into_iter()
.filter_map(|i| {
let pair: Pair = i.symbol.try_into().ok()?;
let price_precision = i.price_scale.parse::<u8>().unwrap_or(0);
let qty_precision = i
.lot_size_filter
.qty_step
.split_once('.')
.map(|(_, decimals)| decimals.trim_end_matches('0').len() as u8)
.unwrap_or(0);
let delivery_date = match i.delivery_time {
0 => None,
ms => Some(Timestamp::from_millisecond(ms).expect("Bybit deliveryTime is valid ms")),
};
Some((
pair,
PairInfo {
price_precision,
qty_precision,
delivery_date,
},
))
})
.collect();
Ok(ExchangeInfo { server_time, pairs })
}