Skip to main content

pragma_common/contract/
types.rs

1use std::fmt;
2
3use chrono::Datelike;
4
5use super::{FuturesContractBuilder, FuturesContractParseError};
6
7const MAX_MONTHS_IN_PAST: i32 = 0;
8const MAX_MONTHS_IN_FUTURE: i32 = 12;
9const MAX_FUTURES_ROOT_LEN: usize = 16;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[cfg_attr(
14    feature = "borsh",
15    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
16)]
17#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
18pub enum Contract {
19    Futures(FuturesContract),
20}
21
22impl Contract {
23    pub fn from_raw_symbol(symbol: &str) -> Result<Self, FuturesContractParseError> {
24        FuturesContractBuilder::raw(symbol)
25            .build()
26            .map(Contract::Futures)
27    }
28
29    pub fn from_cme_symbol(symbol: &str) -> Result<Self, FuturesContractParseError> {
30        Self::from_raw_symbol(symbol)
31    }
32
33    pub fn from_activ_symbol(symbol: &str) -> Result<Self, FuturesContractParseError> {
34        FuturesContractBuilder::activ(symbol).map(Contract::Futures)
35    }
36
37    pub fn raw_symbol(&self) -> String {
38        match self {
39            Self::Futures(contract) => contract.raw_symbol(),
40        }
41    }
42
43    pub fn contract_month_yyyymm(&self) -> String {
44        match self {
45            Self::Futures(contract) => contract.contract_month_yyyymm(),
46        }
47    }
48
49    pub fn validate_against_date(
50        &self,
51        date: chrono::NaiveDate,
52    ) -> Result<(), FuturesContractParseError> {
53        match self {
54            Self::Futures(contract) => contract.validate_against_date(date),
55        }
56    }
57}
58
59impl fmt::Display for Contract {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}", self.raw_symbol())
62    }
63}
64
65impl std::str::FromStr for Contract {
66    type Err = FuturesContractParseError;
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        Self::from_raw_symbol(s)
70    }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75#[cfg_attr(
76    feature = "borsh",
77    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
78)]
79#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
80pub struct FuturesContract {
81    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
82    pub root: FuturesRoot,
83    pub month: FuturesMonth,
84    pub year: u16,
85}
86
87impl FuturesContract {
88    pub fn raw_symbol(&self) -> String {
89        format!(
90            "{}{}{:02}",
91            self.root.cme_code(),
92            self.month.code(),
93            self.year % 100
94        )
95    }
96
97    pub fn contract_month_yyyymm(&self) -> String {
98        format!("{}{:02}", self.year, self.month.number())
99    }
100
101    pub fn validate_against_date(
102        &self,
103        date: chrono::NaiveDate,
104    ) -> Result<(), FuturesContractParseError> {
105        let contract_month_index = contract_month_index(self.year, self.month);
106        let reference_month_index = date_month_index(date);
107        let months_from_reference = contract_month_index - reference_month_index;
108
109        if months_from_reference < -MAX_MONTHS_IN_PAST {
110            return Err(FuturesContractParseError::TooFarInPast {
111                contract: self.raw_symbol(),
112                reference_date: date.to_string(),
113            });
114        }
115
116        if months_from_reference > MAX_MONTHS_IN_FUTURE {
117            return Err(FuturesContractParseError::TooFarInFuture {
118                contract: self.raw_symbol(),
119                reference_date: date.to_string(),
120            });
121        }
122
123        Ok(())
124    }
125}
126
127impl fmt::Display for FuturesContract {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "{}", self.raw_symbol())
130    }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
134#[cfg_attr(
135    feature = "borsh",
136    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
137)]
138#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
139pub struct FuturesRoot {
140    len: u8,
141    bytes: [u8; MAX_FUTURES_ROOT_LEN],
142}
143
144impl FuturesRoot {
145    pub fn new(root: &str) -> Result<Self, FuturesContractParseError> {
146        let normalized = root.trim();
147        if normalized.is_empty()
148            || normalized.len() > MAX_FUTURES_ROOT_LEN
149            || !normalized.bytes().all(|byte| byte.is_ascii_alphanumeric())
150        {
151            return Err(FuturesContractParseError::InvalidRoot(root.to_string()));
152        }
153
154        let mut bytes = [0; MAX_FUTURES_ROOT_LEN];
155        for (output, input) in bytes.iter_mut().zip(normalized.bytes()) {
156            *output = input.to_ascii_uppercase();
157        }
158
159        Ok(Self {
160            len: u8::try_from(normalized.len()).expect("MAX_FUTURES_ROOT_LEN must fit in u8"),
161            bytes,
162        })
163    }
164
165    pub fn cme_code(&self) -> &str {
166        std::str::from_utf8(&self.bytes[..usize::from(self.len)])
167            .expect("FuturesRoot is validated as ASCII")
168    }
169}
170
171impl fmt::Display for FuturesRoot {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(f, "{}", self.cme_code())
174    }
175}
176
177impl std::str::FromStr for FuturesRoot {
178    type Err = FuturesContractParseError;
179
180    fn from_str(s: &str) -> Result<Self, Self::Err> {
181        Self::new(s)
182    }
183}
184
185#[cfg(feature = "serde")]
186impl serde::Serialize for FuturesRoot {
187    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188    where
189        S: serde::Serializer,
190    {
191        serializer.serialize_str(self.cme_code())
192    }
193}
194
195#[cfg(feature = "serde")]
196impl<'de> serde::Deserialize<'de> for FuturesRoot {
197    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198    where
199        D: serde::Deserializer<'de>,
200    {
201        let root = <String as serde::Deserialize>::deserialize(deserializer)?;
202        Self::new(&root).map_err(serde::de::Error::custom)
203    }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
208#[cfg_attr(
209    feature = "borsh",
210    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
211)]
212#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
213pub enum FuturesMonth {
214    January,
215    February,
216    March,
217    April,
218    May,
219    June,
220    July,
221    August,
222    September,
223    October,
224    November,
225    December,
226}
227
228impl FuturesMonth {
229    pub fn code(self) -> char {
230        match self {
231            Self::January => 'F',
232            Self::February => 'G',
233            Self::March => 'H',
234            Self::April => 'J',
235            Self::May => 'K',
236            Self::June => 'M',
237            Self::July => 'N',
238            Self::August => 'Q',
239            Self::September => 'U',
240            Self::October => 'V',
241            Self::November => 'X',
242            Self::December => 'Z',
243        }
244    }
245
246    pub fn from_code(code: char) -> Option<Self> {
247        match code.to_ascii_uppercase() {
248            'F' => Some(Self::January),
249            'G' => Some(Self::February),
250            'H' => Some(Self::March),
251            'J' => Some(Self::April),
252            'K' => Some(Self::May),
253            'M' => Some(Self::June),
254            'N' => Some(Self::July),
255            'Q' => Some(Self::August),
256            'U' => Some(Self::September),
257            'V' => Some(Self::October),
258            'X' => Some(Self::November),
259            'Z' => Some(Self::December),
260            _ => None,
261        }
262    }
263
264    pub const fn number(self) -> u8 {
265        match self {
266            Self::January => 1,
267            Self::February => 2,
268            Self::March => 3,
269            Self::April => 4,
270            Self::May => 5,
271            Self::June => 6,
272            Self::July => 7,
273            Self::August => 8,
274            Self::September => 9,
275            Self::October => 10,
276            Self::November => 11,
277            Self::December => 12,
278        }
279    }
280}
281
282impl fmt::Display for FuturesMonth {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        write!(f, "{}", self.code())
285    }
286}
287
288fn contract_month_index(year: u16, month: FuturesMonth) -> i32 {
289    i32::from(year) * 12 + i32::from(month.number())
290}
291
292fn date_month_index(date: chrono::NaiveDate) -> i32 {
293    date.year() * 12 + i32::try_from(date.month()).expect("chrono month must fit in i32")
294}