1use std::collections::BTreeSet;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 AssetId, DecimalGrid, EconomicsModelId, InstrumentAlias, InstrumentId, PositiveDecimal,
8 SpecRevision,
9};
10
11#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum AssetKind {
15 Fiat,
16 Stablecoin,
17 Crypto,
18 Commodity,
19 Equity,
20 Index,
21 Synthetic,
22 Other,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct AssetSpec {
29 pub asset: AssetId,
30 pub kind: AssetKind,
31 pub display_code: String,
32 pub storage_scale: Option<u8>,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct InstrumentAssets {
39 pub base: Option<AssetId>,
40 pub quote: Option<AssetId>,
41 pub settlement: AssetId,
42 #[serde(default)]
43 pub fee_assets: BTreeSet<AssetId>,
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum ListingStatus {
50 PreTrading,
51 Trading,
52 ReduceOnly,
53 Halted,
54 Delisted,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct PriceRules {
61 pub grid: DecimalGrid,
62 pub display_scale: u8,
63}
64
65#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct QuantityRules {
69 pub grid: DecimalGrid,
70 pub minimum: PositiveDecimal,
71 pub maximum: Option<PositiveDecimal>,
72 pub storage_scale: u8,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct NotionalRules {
79 pub asset: AssetId,
80 pub minimum: Option<PositiveDecimal>,
81 pub maximum: Option<PositiveDecimal>,
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum QuantityUnit {
88 StandardLot,
89 BaseAsset,
90 Contract,
91 QuoteAsset,
92}
93
94#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct InstrumentEconomics {
98 pub pnl_model: EconomicsModelId,
99 pub quantity_unit: QuantityUnit,
100 pub contract_multiplier: PositiveDecimal,
101 pub settlement_asset: AssetId,
102 pub fee_model: Option<EconomicsModelId>,
103 pub funding_model: Option<EconomicsModelId>,
104 pub margin_model: Option<EconomicsModelId>,
105}
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct EffectiveInterval {
111 pub valid_from: DateTime<Utc>,
112 pub valid_until: Option<DateTime<Utc>>,
113}
114
115impl EffectiveInterval {
116 pub fn new(
117 valid_from: DateTime<Utc>,
118 valid_until: Option<DateTime<Utc>>,
119 ) -> Result<Self, EffectiveIntervalError> {
120 if valid_until.is_some_and(|until| until <= valid_from) {
121 return Err(EffectiveIntervalError::InvalidBounds);
122 }
123 Ok(Self {
124 valid_from,
125 valid_until,
126 })
127 }
128
129 pub fn contains(&self, at: DateTime<Utc>) -> bool {
130 at >= self.valid_from && self.valid_until.is_none_or(|until| at < until)
131 }
132
133 pub fn overlaps(&self, other: &Self) -> bool {
134 self.valid_until
135 .is_none_or(|until| other.valid_from < until)
136 && other
137 .valid_until
138 .is_none_or(|until| self.valid_from < until)
139 }
140}
141
142#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct InstrumentSpec {
146 pub revision: SpecRevision,
147 pub instrument: InstrumentId,
148 pub effective: EffectiveInterval,
149 pub status: ListingStatus,
150 pub assets: InstrumentAssets,
151 pub price: PriceRules,
152 pub quantity: QuantityRules,
153 pub notional: Option<NotionalRules>,
154 pub economics: InstrumentEconomics,
155 #[serde(default)]
156 pub aliases: BTreeSet<InstrumentAlias>,
157}
158
159impl AssetSpec {
160 pub fn validate(&self) -> Result<(), SpecValidationError> {
161 if self.display_code.is_empty() || !self.display_code.is_ascii() {
162 return Err(SpecValidationError::InvalidAssetDisplayCode {
163 asset: self.asset.clone(),
164 });
165 }
166 if self
167 .storage_scale
168 .is_some_and(|scale| scale > crate::MAX_DECIMAL_SCALE)
169 {
170 return Err(SpecValidationError::ScaleTooLarge);
171 }
172 Ok(())
173 }
174}
175
176impl InstrumentSpec {
177 pub fn validate(&self) -> Result<(), SpecValidationError> {
178 if self
179 .effective
180 .valid_until
181 .is_some_and(|until| until <= self.effective.valid_from)
182 {
183 return Err(SpecValidationError::InvalidEffectiveInterval);
184 }
185 if self.price.display_scale > crate::MAX_DECIMAL_SCALE
186 || self.quantity.storage_scale > crate::MAX_DECIMAL_SCALE
187 {
188 return Err(SpecValidationError::ScaleTooLarge);
189 }
190 if !self.quantity.grid.contains(self.quantity.minimum.get())? {
191 return Err(SpecValidationError::QuantityMinimumOffGrid);
192 }
193 if let Some(maximum) = self.quantity.maximum {
194 if maximum < self.quantity.minimum {
195 return Err(SpecValidationError::InvalidQuantityBounds);
196 }
197 if !self.quantity.grid.contains(maximum.get())? {
198 return Err(SpecValidationError::QuantityMaximumOffGrid);
199 }
200 }
201 if let Some(notional) = &self.notional {
202 validate_optional_bounds(notional.minimum, notional.maximum)?;
203 }
204 if self.assets.settlement != self.economics.settlement_asset {
205 return Err(SpecValidationError::SettlementAssetMismatch);
206 }
207 Ok(())
208 }
209}
210
211fn validate_optional_bounds(
212 minimum: Option<PositiveDecimal>,
213 maximum: Option<PositiveDecimal>,
214) -> Result<(), SpecValidationError> {
215 if let (Some(minimum), Some(maximum)) = (minimum, maximum)
216 && maximum < minimum
217 {
218 return Err(SpecValidationError::InvalidNotionalBounds);
219 }
220 Ok(())
221}
222
223#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
225pub enum EffectiveIntervalError {
226 #[error("effective interval valid_until must be after valid_from")]
227 InvalidBounds,
228}
229
230#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
232pub enum SpecValidationError {
233 #[error("asset {asset} has an invalid display code")]
234 InvalidAssetDisplayCode { asset: AssetId },
235 #[error("effective interval valid_until must be after valid_from")]
236 InvalidEffectiveInterval,
237 #[error("declared scale exceeds the supported maximum")]
238 ScaleTooLarge,
239 #[error("quantity minimum is outside the declared grid")]
240 QuantityMinimumOffGrid,
241 #[error("quantity maximum is outside the declared grid")]
242 QuantityMaximumOffGrid,
243 #[error("quantity maximum is below quantity minimum")]
244 InvalidQuantityBounds,
245 #[error("notional maximum is below notional minimum")]
246 InvalidNotionalBounds,
247 #[error("instrument and economics settlement assets differ")]
248 SettlementAssetMismatch,
249 #[error(transparent)]
250 Grid(#[from] crate::GridError),
251 #[error(transparent)]
252 Decimal(#[from] crate::DecimalError),
253}