use serde::{Deserialize, Deserializer};
use crate::client::http::{ApiError, Call, Transport, read};
use crate::markets::binary::Outcome;
use crate::markets::listed;
use crate::markets::stats::stat_value;
use crate::utils::dec::{Dec, Round};
use crate::utils::figure::Figure;
use crate::utils::ids::whole;
use crate::utils::{AccountIndex, AssetId, Grid, MarketId, MarketKind};
pub const MARGIN_FRACTION_ONE: u32 = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum MarketFilter {
#[default]
All,
Perps,
Spot,
}
impl MarketFilter {
#[must_use]
pub const fn wire(self) -> &'static str {
match self {
MarketFilter::All => "all",
MarketFilter::Perps => "perp",
MarketFilter::Spot => "spot",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MarketStatus {
Inactive,
Active,
WaitingForSettlement,
InSettlement,
Settled,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct MarketListing {
pub symbol: String,
pub market_id: MarketId,
#[serde(default, deserialize_with = "kind")]
pub market_type: Option<MarketKind>,
#[serde(default, deserialize_with = "asset")]
pub base_asset_id: Option<AssetId>,
#[serde(default, deserialize_with = "asset")]
pub quote_asset_id: Option<AssetId>,
pub status: MarketStatus,
pub taker_fee: Dec,
pub maker_fee: Dec,
pub liquidation_fee: Dec,
#[serde(default)]
pub is_taker_fee_enabled: bool,
#[serde(default)]
pub is_maker_fee_enabled: bool,
pub min_base_amount: Dec,
pub min_quote_amount: Dec,
pub order_quote_limit: Dec,
pub supported_size_decimals: u32,
pub supported_price_decimals: u32,
pub supported_quote_decimals: u32,
#[serde(deserialize_with = "whole")]
pub created_at: u64,
pub multiplier: Dec,
pub open_interest: Dec,
#[serde(default)]
pub open_interest_usd: Option<Dec>,
#[serde(default)]
pub open_interest_limit: Option<Dec>,
#[serde(default, deserialize_with = "whole")]
pub start_timestamp: u64,
#[serde(default, deserialize_with = "whole")]
pub end_timestamp: u64,
#[serde(default)]
pub outcome: Outcome,
#[serde(default, deserialize_with = "whole")]
pub operator_account_index: u64,
#[serde(default)]
pub settlement_cap: u32,
#[serde(default)]
pub settlement_type: u8,
#[serde(default)]
pub settlement_price: u32,
#[serde(default)]
pub default_price: u32,
#[serde(default)]
pub is_frozen: bool,
#[serde(default)]
pub market_deployer: Option<String>,
}
impl MarketListing {
#[must_use]
pub const fn grid(&self) -> Grid {
Grid::new(self.supported_size_decimals, self.supported_price_decimals)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct MarketConfig {
#[serde(default)]
pub market_margin_mode: u8,
#[serde(default, deserialize_with = "whole")]
pub insurance_fund_account_index: u64,
#[serde(default)]
pub liquidation_mode: u8,
#[serde(default)]
pub force_reduce_only: bool,
#[serde(default)]
pub funding_fee_discounts_enabled: bool,
#[serde(default)]
pub trading_hours: String,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub rfq_enabled: bool,
}
impl MarketConfig {
#[must_use]
pub fn insurance_fund(&self) -> Option<AccountIndex> {
AccountIndex::new(self.insurance_fund_account_index).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct MarketDetail {
pub symbol: String,
pub market_id: MarketId,
#[serde(default, deserialize_with = "kind")]
pub market_type: Option<MarketKind>,
#[serde(default, deserialize_with = "asset")]
pub base_asset_id: Option<AssetId>,
#[serde(default, deserialize_with = "asset")]
pub quote_asset_id: Option<AssetId>,
pub status: MarketStatus,
pub taker_fee: Dec,
pub maker_fee: Dec,
pub liquidation_fee: Dec,
#[serde(default)]
pub is_taker_fee_enabled: bool,
#[serde(default)]
pub is_maker_fee_enabled: bool,
pub min_base_amount: Dec,
pub min_quote_amount: Dec,
pub order_quote_limit: Dec,
pub supported_size_decimals: u32,
pub supported_price_decimals: u32,
pub supported_quote_decimals: u32,
pub size_decimals: u32,
pub price_decimals: u32,
#[serde(deserialize_with = "whole")]
pub created_at: u64,
pub multiplier: Dec,
#[serde(default)]
pub market_deployer: Option<String>,
pub last_trade_price: Figure,
#[serde(deserialize_with = "whole")]
pub daily_trades_count: u64,
pub daily_base_token_volume: Figure,
pub daily_quote_token_volume: Figure,
pub daily_price_low: Figure,
pub daily_price_high: Figure,
pub daily_price_change: Figure,
pub open_interest: Figure,
#[serde(default)]
pub mark_price: Option<Dec>,
#[serde(default)]
pub index_price: Option<Dec>,
#[serde(default)]
pub default_initial_margin_fraction: Option<u32>,
#[serde(default)]
pub min_initial_margin_fraction: Option<u32>,
#[serde(default)]
pub maintenance_margin_fraction: Option<u32>,
#[serde(default)]
pub closeout_margin_fraction: Option<u32>,
#[serde(default)]
pub quote_multiplier: Option<u64>,
#[serde(default)]
pub open_interest_usd: Option<Dec>,
#[serde(default)]
pub open_interest_limit: Option<Dec>,
#[serde(default)]
pub market_config: Option<MarketConfig>,
#[serde(default)]
pub strategy_index: Option<u32>,
#[serde(default)]
pub market_flags: Option<u64>,
#[serde(default)]
pub funding_premium_multiplier: Option<u32>,
#[serde(default)]
pub funding_clamp_small: Option<Dec>,
#[serde(default)]
pub funding_clamp_big: Option<Dec>,
#[serde(default)]
pub base_interest_rate: Option<Dec>,
}
impl MarketDetail {
#[must_use]
pub fn is_perp(&self) -> bool {
self.market_type == Some(MarketKind::Perps)
}
#[must_use]
pub fn is_spot(&self) -> bool {
self.market_type == Some(MarketKind::Spot)
}
#[must_use]
pub const fn grid(&self) -> Grid {
Grid::new(self.size_decimals, self.price_decimals)
}
#[must_use]
pub fn size_step(&self) -> Dec {
step(self.size_decimals.min(self.supported_size_decimals))
}
#[must_use]
pub fn tick(&self) -> Dec {
step(self.price_decimals.min(self.supported_price_decimals))
}
#[must_use]
pub fn is_active(&self) -> bool {
self.status == MarketStatus::Active
}
#[must_use]
pub fn is_hidden(&self) -> bool {
self.market_config.as_ref().is_some_and(|config| config.hidden)
}
#[must_use]
pub fn is_listed(&self) -> bool {
self.is_active() && !self.is_hidden()
}
#[must_use]
pub fn force_reduce_only(&self) -> bool {
self.market_config.as_ref().is_some_and(|config| config.force_reduce_only)
}
#[must_use]
pub fn is_close_only(&self) -> bool {
self.force_reduce_only() || !self.is_active()
}
#[must_use]
pub fn is_isolated_only(&self) -> bool {
self.market_config.as_ref().is_some_and(|config| config.market_margin_mode == 1)
}
#[must_use]
pub fn max_leverage(&self) -> Option<u32> {
let fraction = self.min_initial_margin_fraction.filter(|fraction| *fraction > 0)?;
Some((MARGIN_FRACTION_ONE / fraction).max(1))
}
#[must_use]
pub fn underlying(&self) -> (&str, Dec) {
underlying(&self.symbol)
}
#[must_use]
pub fn last_price(&self) -> Option<Dec> {
stat_value(&self.last_trade_price).filter(|price| price.is_positive())
}
#[must_use]
pub fn price(&self) -> Option<Dec> {
self.last_price().or(self.mark_price.filter(|mark| mark.is_positive()))
}
#[must_use]
pub fn daily_change(&self) -> Option<Dec> {
stat_value(&self.daily_price_change)?.checked_div(Dec::from_int(100), 18, Round::Nearest)
}
#[must_use]
pub fn daily_volume(&self) -> Option<Dec> {
stat_value(&self.daily_quote_token_volume)
}
#[must_use]
pub fn open_interest_base(&self) -> Option<Dec> {
stat_value(&self.open_interest)
}
}
fn step(decimals: u32) -> Dec {
Dec::new(1, decimals).unwrap_or(Dec::ONE)
}
#[must_use]
pub fn underlying(symbol: &str) -> (&str, Dec) {
match symbol.strip_prefix("1000") {
Some(rest) if !rest.is_empty() && rest.bytes().all(|byte| byte.is_ascii_alphabetic()) => {
(rest, Dec::from_int(1000))
}
_ => (symbol, Dec::ONE),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub struct Markets {
#[serde(rename = "order_book_details", default, deserialize_with = "listed")]
pub perps: Vec<MarketDetail>,
#[serde(rename = "spot_order_book_details", default, deserialize_with = "listed")]
pub spot: Vec<MarketDetail>,
}
impl Markets {
pub fn iter(&self) -> impl Iterator<Item = &MarketDetail> {
self.perps.iter().chain(&self.spot)
}
#[must_use]
pub fn len(&self) -> usize {
self.perps.len() + self.spot.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn get(&self, market: MarketId) -> Option<&MarketDetail> {
self.iter().find(|detail| detail.market_id == market)
}
#[must_use]
pub fn by_symbol(&self, symbol: &str) -> Option<&MarketDetail> {
self.iter()
.find(|detail| detail.symbol == symbol)
.or_else(|| self.iter().find(|detail| detail.symbol.eq_ignore_ascii_case(symbol)))
}
pub fn listed(&self) -> impl Iterator<Item = &MarketDetail> {
self.iter().filter(|detail| detail.is_listed())
}
pub fn extend(&mut self, other: Markets) {
for detail in other.perps.into_iter().chain(other.spot) {
self.perps.retain(|kept| kept.market_id != detail.market_id);
self.spot.retain(|kept| kept.market_id != detail.market_id);
match detail.is_spot() {
true => self.spot.push(detail),
false => self.perps.push(detail),
}
}
}
}
#[derive(Deserialize)]
struct Listings {
#[serde(default, deserialize_with = "listed")]
order_books: Vec<MarketListing>,
}
#[must_use]
pub fn order_books_call(filter: MarketFilter) -> Call {
Call::get("orderBooks").field("filter", filter.wire())
}
#[must_use]
pub fn listing_call(market: MarketId) -> Call {
Call::get("orderBooks").field("market_id", market)
}
pub async fn order_books(transport: &impl Transport, filter: MarketFilter) -> Result<Vec<MarketListing>, ApiError> {
Ok(read::<Listings>(transport, order_books_call(filter)).await?.order_books)
}
pub async fn listing(transport: &impl Transport, market: MarketId) -> Result<Option<MarketListing>, ApiError> {
let listings = read::<Listings>(transport, listing_call(market)).await?.order_books;
Ok(listings.into_iter().find(|listing| listing.market_id == market))
}
#[must_use]
pub fn order_book_details_call(filter: MarketFilter) -> Call {
Call::get("orderBookDetails").field("filter", filter.wire())
}
#[must_use]
pub fn market_detail_call(market: MarketId) -> Call {
Call::get("orderBookDetails").field("market_id", market)
}
pub async fn order_book_details(transport: &impl Transport, filter: MarketFilter) -> Result<Markets, ApiError> {
read(transport, order_book_details_call(filter)).await
}
pub async fn market_detail(transport: &impl Transport, market: MarketId) -> Result<Option<MarketDetail>, ApiError> {
let markets: Markets = read(transport, market_detail_call(market)).await?;
Ok(markets.get(market).cloned())
}
fn kind<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<MarketKind>, D::Error> {
crate::utils::order::known(deserializer)
}
fn asset<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<AssetId>, D::Error> {
match whole(deserializer)? {
0 => Ok(None),
id => u16::try_from(id)
.ok()
.and_then(|id| AssetId::new(id).ok())
.map(Some)
.ok_or_else(|| serde::de::Error::custom(format!("{id} is not an asset"))),
}
}
#[cfg(test)]
#[path = "tests/books.rs"]
mod tests;