use std::collections::BTreeSet;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{
AssetId, DecimalGrid, EconomicsModelId, InstrumentAlias, InstrumentId, PositiveDecimal,
SpecRevision,
};
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssetKind {
Fiat,
Stablecoin,
Crypto,
Commodity,
Equity,
Index,
Synthetic,
Other,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AssetSpec {
pub asset: AssetId,
pub kind: AssetKind,
pub display_code: String,
pub storage_scale: Option<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstrumentAssets {
pub base: Option<AssetId>,
pub quote: Option<AssetId>,
pub settlement: AssetId,
#[serde(default)]
pub fee_assets: BTreeSet<AssetId>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ListingStatus {
PreTrading,
Trading,
ReduceOnly,
Halted,
Delisted,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PriceRules {
pub grid: DecimalGrid,
pub display_scale: u8,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct QuantityRules {
pub grid: DecimalGrid,
pub minimum: PositiveDecimal,
pub maximum: Option<PositiveDecimal>,
pub storage_scale: u8,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NotionalRules {
pub asset: AssetId,
pub minimum: Option<PositiveDecimal>,
pub maximum: Option<PositiveDecimal>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuantityUnit {
StandardLot,
BaseAsset,
Contract,
QuoteAsset,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstrumentEconomics {
pub pnl_model: EconomicsModelId,
pub quantity_unit: QuantityUnit,
pub contract_multiplier: PositiveDecimal,
pub settlement_asset: AssetId,
pub fee_model: Option<EconomicsModelId>,
pub funding_model: Option<EconomicsModelId>,
pub margin_model: Option<EconomicsModelId>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EffectiveInterval {
pub valid_from: DateTime<Utc>,
pub valid_until: Option<DateTime<Utc>>,
}
impl EffectiveInterval {
pub fn new(
valid_from: DateTime<Utc>,
valid_until: Option<DateTime<Utc>>,
) -> Result<Self, EffectiveIntervalError> {
if valid_until.is_some_and(|until| until <= valid_from) {
return Err(EffectiveIntervalError::InvalidBounds);
}
Ok(Self {
valid_from,
valid_until,
})
}
pub fn contains(&self, at: DateTime<Utc>) -> bool {
at >= self.valid_from && self.valid_until.is_none_or(|until| at < until)
}
pub fn overlaps(&self, other: &Self) -> bool {
self.valid_until
.is_none_or(|until| other.valid_from < until)
&& other
.valid_until
.is_none_or(|until| self.valid_from < until)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstrumentSpec {
pub revision: SpecRevision,
pub instrument: InstrumentId,
pub effective: EffectiveInterval,
pub status: ListingStatus,
pub assets: InstrumentAssets,
pub price: PriceRules,
pub quantity: QuantityRules,
pub notional: Option<NotionalRules>,
pub economics: InstrumentEconomics,
#[serde(default)]
pub aliases: BTreeSet<InstrumentAlias>,
}
impl AssetSpec {
pub fn validate(&self) -> Result<(), SpecValidationError> {
if self.display_code.is_empty() || !self.display_code.is_ascii() {
return Err(SpecValidationError::InvalidAssetDisplayCode {
asset: self.asset.clone(),
});
}
if self
.storage_scale
.is_some_and(|scale| scale > crate::MAX_DECIMAL_SCALE)
{
return Err(SpecValidationError::ScaleTooLarge);
}
Ok(())
}
}
impl InstrumentSpec {
pub fn validate(&self) -> Result<(), SpecValidationError> {
if self
.effective
.valid_until
.is_some_and(|until| until <= self.effective.valid_from)
{
return Err(SpecValidationError::InvalidEffectiveInterval);
}
if self.price.display_scale > crate::MAX_DECIMAL_SCALE
|| self.quantity.storage_scale > crate::MAX_DECIMAL_SCALE
{
return Err(SpecValidationError::ScaleTooLarge);
}
if !self.quantity.grid.contains(self.quantity.minimum.get())? {
return Err(SpecValidationError::QuantityMinimumOffGrid);
}
if let Some(maximum) = self.quantity.maximum {
if maximum < self.quantity.minimum {
return Err(SpecValidationError::InvalidQuantityBounds);
}
if !self.quantity.grid.contains(maximum.get())? {
return Err(SpecValidationError::QuantityMaximumOffGrid);
}
}
if let Some(notional) = &self.notional {
validate_optional_bounds(notional.minimum, notional.maximum)?;
}
if self.assets.settlement != self.economics.settlement_asset {
return Err(SpecValidationError::SettlementAssetMismatch);
}
Ok(())
}
}
fn validate_optional_bounds(
minimum: Option<PositiveDecimal>,
maximum: Option<PositiveDecimal>,
) -> Result<(), SpecValidationError> {
if let (Some(minimum), Some(maximum)) = (minimum, maximum)
&& maximum < minimum
{
return Err(SpecValidationError::InvalidNotionalBounds);
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum EffectiveIntervalError {
#[error("effective interval valid_until must be after valid_from")]
InvalidBounds,
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum SpecValidationError {
#[error("asset {asset} has an invalid display code")]
InvalidAssetDisplayCode { asset: AssetId },
#[error("effective interval valid_until must be after valid_from")]
InvalidEffectiveInterval,
#[error("declared scale exceeds the supported maximum")]
ScaleTooLarge,
#[error("quantity minimum is outside the declared grid")]
QuantityMinimumOffGrid,
#[error("quantity maximum is outside the declared grid")]
QuantityMaximumOffGrid,
#[error("quantity maximum is below quantity minimum")]
InvalidQuantityBounds,
#[error("notional maximum is below notional minimum")]
InvalidNotionalBounds,
#[error("instrument and economics settlement assets differ")]
SettlementAssetMismatch,
#[error(transparent)]
Grid(#[from] crate::GridError),
#[error(transparent)]
Decimal(#[from] crate::DecimalError),
}