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}