1use super::*;
4use anyhow::{anyhow, bail, Result};
5use chrono::{DateTime, Datelike, NaiveDate, Utc};
6use derive_more::{AsRef, Deref, Display, From};
7use rust_decimal::Decimal;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use serde_with::{DeserializeFromStr, SerializeDisplay};
11use std::str::FromStr;
12use strum_macros::{EnumString, IntoStaticStr};
13
14#[derive(
15 Debug,
16 Display,
17 From,
18 AsRef,
19 Deref,
20 Clone,
21 PartialEq,
22 Eq,
23 PartialOrd,
24 Ord,
25 Hash,
26 Deserialize,
27 Serialize,
28 JsonSchema,
29)]
30#[as_ref(forward)]
31#[serde(transparent)]
32#[cfg_attr(feature = "graphql", derive(juniper::GraphQLScalar))]
33#[cfg_attr(feature = "graphql", graphql(transparent))]
34#[cfg_attr(feature = "postgres", derive(postgres_types::ToSql))]
35#[cfg_attr(feature = "postgres", postgres(transparent))]
36pub struct Product(pub(crate) String);
37
38impl Product {
39 fn new(
40 symbol: &str,
41 venue_discriminant: Option<&str>,
42 product_kind: &str,
43 ) -> Result<Self> {
44 if symbol.contains('/')
45 || venue_discriminant.is_some_and(|v| v.contains('/'))
46 || product_kind.contains('/')
47 {
48 bail!("product symbol cannot contain the forward slash character '/'");
49 }
50 let inner = match venue_discriminant {
51 Some(venue_discriminant) => {
52 if venue_discriminant.is_empty() {
53 bail!("venue discriminant cannot be empty if provided");
54 }
55 format!(
56 "{} {} {}",
57 symbol,
58 venue_discriminant.to_uppercase(),
59 product_kind
60 )
61 }
62 None => format!("{} {}", symbol, product_kind),
63 };
64 Ok(Self(inner))
65 }
66
67 pub fn fiat(symbol: &str) -> Result<Self> {
68 if symbol.contains(char::is_whitespace) {
69 bail!("fiat product symbol cannot contain whitespace");
70 }
71 if symbol.contains('/') {
72 bail!("fiat product symbol cannot contain the forward slash character '/'");
73 }
74 Ok(Self(symbol.to_string()))
75 }
76
77 pub fn commodity(symbol: &str) -> Result<Self> {
78 Self::new(symbol, None, "Commodity")
79 }
80
81 pub fn crypto(symbol: &str) -> Result<Self> {
82 Self::new(symbol, None, "Crypto")
83 }
84
85 pub fn index(symbol: &str, venue_discriminant: Option<&str>) -> Result<Self> {
86 Self::new(symbol, venue_discriminant, "Index")
87 }
88
89 pub fn equity(symbol: &str, country: &str) -> Result<Self> {
90 let symbol = format!("{symbol} {country}");
91 Self::new(&symbol, None, "Equity")
92 }
93
94 pub fn future(
95 symbol: &str,
96 expiration: NaiveDate,
97 venue_discriminant: Option<&str>,
98 ) -> Result<Self> {
99 let symbol = format!("{symbol} {}", expiration.format("%Y%m%d"));
100 Self::new(&symbol, venue_discriminant, "Future")
101 }
102
103 pub fn futures_spread<'a>(
104 legs: impl IntoIterator<Item = &'a str>,
105 ratios: Option<impl IntoIterator<Item = Decimal>>,
106 expiration: NaiveDate,
107 venue_discriminant: Option<&str>,
108 ) -> Result<Self> {
109 let legs_str = legs.into_iter().collect::<Vec<_>>().join("-");
110 let expiration_str = expiration.format("%Y%m%d");
111 let symbol = if let Some(ratios) = ratios {
112 format!(
113 "{legs_str} {} {}",
114 ratios.into_iter().map(|k| k.to_string()).collect::<Vec<_>>().join(":"),
115 expiration_str
116 )
117 } else {
118 format!("{legs_str} {expiration_str}")
119 };
120 Self::new(&symbol, venue_discriminant, "Futures Spread")
121 }
122
123 pub fn perpetual(symbol: &str, venue_discriminant: Option<&str>) -> Result<Self> {
124 Self::new(symbol, venue_discriminant, "Perpetual")
125 }
126
127 pub fn option(
129 stem: &str,
130 expiration: NaiveDate,
131 strike: Decimal,
132 put_or_call: PutOrCall,
133 venue_discriminant: Option<&str>,
134 ) -> Result<Self> {
135 let strike_str = strike.to_string();
136 let (dollar_part, decimal_part) =
137 strike_str.split_once('.').unwrap_or((&strike_str, "000"));
138 let put_or_call_char = match put_or_call {
139 PutOrCall::Put => "P",
140 PutOrCall::Call => "C",
141 };
142 let osi_symbol = format!(
143 "{:<6}{:02}{:02}{:02}{}{:0>5}{:0<3}",
144 stem,
145 expiration.year() % 100,
146 expiration.month(),
147 expiration.day(),
148 put_or_call_char,
149 dollar_part,
150 &decimal_part[..decimal_part.len().min(3)]
151 );
152 Self::new(&osi_symbol, venue_discriminant, "Option")
153 }
154
155 pub fn nominative_expiration(&self) -> Option<NaiveDate> {
163 if self.ends_with(" Future") {
164 let date_part = self.split(' ').nth(1)?;
165 NaiveDate::parse_from_str(date_part, "%Y%m%d").ok()
166 } else {
167 None
168 }
169 }
170}
171
172impl std::borrow::Borrow<str> for Product {
173 fn borrow(&self) -> &str {
174 &self.0
175 }
176}
177
178impl FromStr for Product {
179 type Err = anyhow::Error;
180
181 fn from_str(s: &str) -> Result<Self> {
182 if s.contains('/') {
183 bail!("product symbol cannot contain the forward slash character '/'");
184 }
185 Ok(Self(s.to_string()))
186 }
187}
188
189#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
190pub struct ProductInfo {
191 pub product_type: ProductType,
192 pub primary_venue: Option<String>,
193 pub price_display_format: Option<PriceDisplayFormat>,
194}
195
196impl ProductInfo {
197 pub fn series(&self) -> Option<&str> {
198 match &self.product_type {
199 ProductType::Future { series, .. } => series.as_deref(),
200 _ => None,
201 }
202 }
203
204 pub fn multiplier(&self) -> Option<Decimal> {
205 match &self.product_type {
206 ProductType::Crypto
207 | ProductType::Fiat
208 | ProductType::Equity { .. }
209 | ProductType::Index
210 | ProductType::Commodity
211 | ProductType::Unknown
212 | ProductType::Option { .. }
213 | ProductType::EventContract { .. }
214 | ProductType::FutureSpread { .. } => None,
215 ProductType::Perpetual { multiplier, .. }
216 | ProductType::Future { multiplier, .. } => Some(*multiplier),
217 }
218 }
219
220 pub fn underlying(&self) -> Option<&Product> {
221 match &self.product_type {
222 ProductType::Crypto
223 | ProductType::Fiat
224 | ProductType::Equity { .. }
225 | ProductType::Index
226 | ProductType::Commodity
227 | ProductType::Unknown
228 | ProductType::Option { .. }
229 | ProductType::EventContract { .. }
230 | ProductType::FutureSpread { .. } => None,
231 ProductType::Perpetual { underlying, .. }
232 | ProductType::Future { underlying, .. } => underlying.as_ref(),
233 }
234 }
235
236 pub fn expiration(&self) -> Option<DateTime<Utc>> {
237 match &self.product_type {
238 ProductType::Crypto
239 | ProductType::Fiat
240 | ProductType::Equity { .. }
241 | ProductType::Index
242 | ProductType::Commodity
243 | ProductType::Unknown
244 | ProductType::Perpetual { .. }
245 | ProductType::FutureSpread { .. } => None,
246 ProductType::Option {
247 instance: OptionsSeriesInstance { expiration, .. },
248 ..
249 } => Some(*expiration),
250 ProductType::EventContract { instance, .. } => instance.expiration(),
251 ProductType::Future { expiration, .. } => Some(*expiration),
252 }
253 }
254
255 pub fn is_expired(&self, cutoff: DateTime<Utc>) -> bool {
256 if let Some(expiration) = self.expiration() {
257 expiration <= cutoff
258 } else {
259 false
260 }
261 }
262
263 pub fn derivative_kind(&self) -> Option<DerivativeKind> {
264 match &self.product_type {
265 ProductType::Future { derivative_kind, .. }
266 | ProductType::Perpetual { derivative_kind, .. } => Some(*derivative_kind),
267 _ => None,
268 }
269 }
270
271 pub fn first_notice_date(&self) -> Option<NaiveDate> {
272 match &self.product_type {
273 ProductType::Future { first_notice_date, .. } => *first_notice_date,
274 _ => None,
275 }
276 }
277
278 pub fn easy_to_borrow(&self) -> Option<bool> {
279 match &self.product_type {
280 ProductType::Equity { easy_to_borrow, .. } => *easy_to_borrow,
281 _ => None,
282 }
283 }
284}
285
286#[derive(Debug, Clone, IntoStaticStr, Deserialize, Serialize, JsonSchema)]
287#[serde(tag = "product_type")]
288pub enum ProductType {
289 #[schemars(title = "Fiat")]
290 Fiat,
291 #[schemars(title = "Commodity")]
292 Commodity,
293 #[schemars(title = "Crypto")]
294 Crypto,
295 #[schemars(title = "Equity")]
296 Equity { easy_to_borrow: Option<bool> },
297 #[schemars(title = "Index")]
298 Index,
299 #[schemars(title = "Future")]
300 Future {
301 series: Option<String>,
302 underlying: Option<Product>,
303 multiplier: Decimal,
304 expiration: DateTime<Utc>,
305 derivative_kind: DerivativeKind,
306 #[serde(default)]
307 first_notice_date: Option<NaiveDate>,
308 },
309 #[schemars(title = "FutureSpread")]
310 FutureSpread { legs: Vec<SpreadLeg> },
311 #[schemars(title = "Perpetual")]
312 Perpetual {
313 underlying: Option<Product>,
314 multiplier: Decimal,
315 derivative_kind: DerivativeKind,
316 },
317 #[schemars(title = "Option")]
318 Option { series: OptionsSeries, instance: OptionsSeriesInstance },
319 #[schemars(title = "EventContract")]
320 EventContract { series: EventContractSeries, instance: EventContractSeriesInstance },
321 #[serde(other)]
322 #[schemars(title = "Unknown")]
323 Unknown,
324}
325
326#[derive(
327 Debug, Copy, Clone, EnumString, IntoStaticStr, Serialize, Deserialize, JsonSchema,
328)]
329#[strum(ascii_case_insensitive)]
330pub enum DerivativeKind {
331 Linear,
333 Inverse,
335 Quanto,
337}
338
339#[cfg(feature = "postgres")]
340crate::to_sql_str!(DerivativeKind);
341
342#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
343#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))]
344pub struct SpreadLeg {
345 pub product: Product,
346 pub quantity: Decimal,
349}
350
351impl std::fmt::Display for SpreadLeg {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 if self.quantity > Decimal::ZERO {
354 write!(f, "+{} {}", self.quantity, self.product)
355 } else {
356 write!(f, "{} {}", self.quantity, self.product)
357 }
358 }
359}
360
361impl std::str::FromStr for SpreadLeg {
362 type Err = anyhow::Error;
363
364 fn from_str(s: &str) -> Result<Self> {
365 let (quantity, product) =
366 s.split_once(' ').ok_or_else(|| anyhow!("invalid leg format"))?;
367 Ok(Self { product: product.parse()?, quantity: quantity.parse()? })
368 }
369}
370
371#[derive(
372 Debug,
373 Clone,
374 Copy,
375 PartialEq,
376 Eq,
377 PartialOrd,
378 Ord,
379 Hash,
380 EnumString,
381 IntoStaticStr,
382 Deserialize,
383 Serialize,
384 JsonSchema,
385)]
386#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
387#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
388pub enum AliasKind {
389 CmeGlobex,
390 Cfe,
391}
392
393#[cfg(feature = "postgres")]
394crate::to_sql_str!(AliasKind);
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
397pub enum PriceDisplayFormat {
398 CmeFractional {
399 main_fraction: i32,
400 sub_fraction: i32, tick_decimals: i32, },
403}
404
405impl PriceDisplayFormat {
406 pub fn main_fraction(&self) -> Option<i32> {
407 match self {
408 Self::CmeFractional { main_fraction, .. } => Some(*main_fraction),
409 }
410 }
411
412 pub fn tick_decimals(&self) -> Option<i32> {
413 match self {
414 Self::CmeFractional { tick_decimals, .. } => Some(*tick_decimals),
415 }
416 }
417}
418
419crate::json_schema_is_string!(PriceDisplayFormat);
420
421impl std::fmt::Display for PriceDisplayFormat {
422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423 match self {
424 Self::CmeFractional { main_fraction, sub_fraction, tick_decimals } => {
425 write!(
426 f,
427 "CME_FRACTIONAL({main_fraction},{sub_fraction},{tick_decimals})"
428 )
429 }
430 }
431 }
432}
433
434impl std::str::FromStr for PriceDisplayFormat {
435 type Err = anyhow::Error;
436
437 fn from_str(s: &str) -> Result<Self> {
438 if s.starts_with("CME_FRACTIONAL(") {
439 let s = s.trim_end_matches(')');
440 let parts: Vec<&str> = s["CME_FRACTIONAL(".len()..].split(',').collect();
441 if parts.len() != 3 {
442 return Err(anyhow!("invalid CME_FRACTIONAL format"));
443 }
444 let main_fraction = parts[0].parse()?;
445 let sub_fraction = parts[1].parse()?;
446 let tick_decimals = parts[2].parse()?;
447 Ok(Self::CmeFractional { main_fraction, sub_fraction, tick_decimals })
448 } else {
449 Err(anyhow!("invalid price display format"))
450 }
451 }
452}
453
454#[cfg(feature = "postgres")]
455crate::to_sql_display!(PriceDisplayFormat);