Skip to main content

ibapi/contracts/
types.rs

1//! Strong types for contract building with validation.
2
3use crate::ToField;
4use std::fmt;
5use time::{Date, Duration, Month, OffsetDateTime, Weekday};
6
7#[cfg(test)]
8#[path = "types_tests.rs"]
9mod tests;
10
11/// Strong type for trading symbols
12#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
13#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14pub struct Symbol(pub String);
15
16impl Symbol {
17    /// Create a symbol from any string-like input.
18    pub fn new(s: impl Into<String>) -> Self {
19        Symbol(s.into())
20    }
21
22    /// Return the raw symbol text.
23    pub fn as_str(&self) -> &str {
24        &self.0
25    }
26}
27
28impl From<&str> for Symbol {
29    fn from(s: &str) -> Self {
30        Symbol(s.to_string())
31    }
32}
33
34impl From<String> for Symbol {
35    fn from(s: String) -> Self {
36        Symbol(s)
37    }
38}
39
40impl From<&String> for Symbol {
41    fn from(s: &String) -> Self {
42        Symbol(s.clone())
43    }
44}
45
46impl fmt::Display for Symbol {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "{}", self.0)
49    }
50}
51
52impl ToField for Symbol {
53    fn to_field(&self) -> String {
54        self.0.clone()
55    }
56}
57
58impl_str_partial_eq!(Symbol);
59
60/// Exchange identifier
61///
62/// IBKR supports 160+ exchanges worldwide. This type provides a lightweight wrapper
63/// around exchange codes.
64#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
65#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
66pub struct Exchange(pub String);
67
68impl Exchange {
69    /// Create a new exchange
70    pub fn new(s: impl Into<String>) -> Self {
71        Exchange(s.into())
72    }
73
74    /// Get the exchange code as a string slice
75    pub fn as_str(&self) -> &str {
76        &self.0
77    }
78
79    /// Check if the exchange string is empty
80    pub fn is_empty(&self) -> bool {
81        self.0.is_empty()
82    }
83}
84
85impl Default for Exchange {
86    fn default() -> Self {
87        Exchange("SMART".to_string())
88    }
89}
90
91impl From<&str> for Exchange {
92    fn from(s: &str) -> Self {
93        Exchange(s.to_string())
94    }
95}
96
97impl From<String> for Exchange {
98    fn from(s: String) -> Self {
99        Exchange(s)
100    }
101}
102
103impl From<&String> for Exchange {
104    fn from(s: &String) -> Self {
105        Exchange(s.clone())
106    }
107}
108
109impl fmt::Display for Exchange {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.0)
112    }
113}
114
115impl ToField for Exchange {
116    fn to_field(&self) -> String {
117        self.0.clone()
118    }
119}
120
121impl_str_partial_eq!(Exchange);
122
123/// Currency identifier
124///
125/// IBKR supports trading in many currencies worldwide. This type provides a lightweight
126/// wrapper around currency codes.
127#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
128#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
129pub struct Currency(pub String);
130
131impl Currency {
132    /// Create a new currency
133    pub fn new(s: impl Into<String>) -> Self {
134        Currency(s.into())
135    }
136
137    /// Get the currency code as a string slice
138    pub fn as_str(&self) -> &str {
139        &self.0
140    }
141}
142
143impl Default for Currency {
144    fn default() -> Self {
145        Currency("USD".to_string())
146    }
147}
148
149impl From<&str> for Currency {
150    fn from(s: &str) -> Self {
151        Currency(s.to_string())
152    }
153}
154
155impl From<String> for Currency {
156    fn from(s: String) -> Self {
157        Currency(s)
158    }
159}
160
161impl From<&String> for Currency {
162    fn from(s: &String) -> Self {
163        Currency(s.clone())
164    }
165}
166
167impl fmt::Display for Currency {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "{}", self.0)
170    }
171}
172
173impl ToField for Currency {
174    fn to_field(&self) -> String {
175        self.0.clone()
176    }
177}
178
179impl_str_partial_eq!(Currency);
180
181/// Option right (Call or Put). Matches IBKR's wire vocabulary `"C"` / `"P"`.
182///
183/// No `Default` — `Contract.right: Option<OptionRight>` carries the no-right
184/// state via `None`.
185#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
187#[non_exhaustive]
188pub enum OptionRight {
189    /// Call option right.
190    Call,
191    /// Put option right.
192    Put,
193}
194
195impl OptionRight {
196    /// Return the canonical single-character wire string (`"C"` or `"P"`).
197    pub fn as_str(&self) -> &'static str {
198        match self {
199            OptionRight::Call => "C",
200            OptionRight::Put => "P",
201        }
202    }
203
204    fn from_wire(s: &str) -> Option<Self> {
205        match s {
206            "C" => Some(Self::Call),
207            "P" => Some(Self::Put),
208            _ => None,
209        }
210    }
211}
212
213impl_wire_enum!(OptionRight);
214
215/// Security identifier scheme. Matches IBKR's `secIdType` wire vocabulary.
216///
217/// No `Default` — `Contract.security_id_type: Option<SecurityIdType>` carries
218/// the no-identifier state via `None`. `#[non_exhaustive]` because IBKR's
219/// catalogue grows over time.
220#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
222#[non_exhaustive]
223pub enum SecurityIdType {
224    /// CUSIP — North American security identifier.
225    Cusip,
226    /// ISIN — international security identifier.
227    Isin,
228    /// SEDOL — UK/Irish security identifier.
229    Sedol,
230    /// RIC — Reuters Instrument Code.
231    Ric,
232    /// FIGI — Bloomberg Financial Instrument Global Identifier.
233    Figi,
234}
235
236impl SecurityIdType {
237    /// Return the canonical IBKR wire string.
238    pub fn as_str(&self) -> &'static str {
239        match self {
240            SecurityIdType::Cusip => "CUSIP",
241            SecurityIdType::Isin => "ISIN",
242            SecurityIdType::Sedol => "SEDOL",
243            SecurityIdType::Ric => "RIC",
244            SecurityIdType::Figi => "FIGI",
245        }
246    }
247
248    fn from_wire(s: &str) -> Option<Self> {
249        match s {
250            "CUSIP" => Some(Self::Cusip),
251            "ISIN" => Some(Self::Isin),
252            "SEDOL" => Some(Self::Sedol),
253            "RIC" => Some(Self::Ric),
254            "FIGI" => Some(Self::Figi),
255            _ => None,
256        }
257    }
258}
259
260impl_wire_enum!(SecurityIdType);
261
262/// Validated strike price (must be positive)
263#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
264#[derive(Debug, Clone, Copy, PartialEq)]
265pub struct Strike(f64);
266
267impl Strike {
268    /// Construct a validated strike price ensuring it is positive.
269    pub fn new(price: f64) -> Result<Self, String> {
270        if price <= 0.0 {
271            Err("Strike price must be positive".to_string())
272        } else {
273            Ok(Strike(price))
274        }
275    }
276
277    /// Create a strike price, panicking if invalid (for internal use in builders)
278    pub(crate) fn new_unchecked(price: f64) -> Self {
279        Strike::new(price).expect("Strike price must be positive")
280    }
281
282    /// Access the numeric strike value.
283    pub fn value(&self) -> f64 {
284        self.0
285    }
286}
287
288/// Date for option expiration
289#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct ExpirationDate {
292    year: u16,
293    month: u8,
294    day: u8,
295}
296
297impl ExpirationDate {
298    /// Create an option expiration date from year/month/day components.
299    pub fn new(year: u16, month: u8, day: u8) -> Self {
300        ExpirationDate { year, month, day }
301    }
302
303    /// Helper to calculate days until next Friday from a given weekday
304    fn days_until_friday(from_weekday: Weekday) -> i64 {
305        match from_weekday {
306            Weekday::Saturday => 6,
307            Weekday::Sunday => 5,
308            Weekday::Monday => 4,
309            Weekday::Tuesday => 3,
310            Weekday::Wednesday => 2,
311            Weekday::Thursday => 1,
312            Weekday::Friday => 0,
313        }
314    }
315
316    /// Get the next Friday from today
317    pub fn next_friday() -> Self {
318        Self::next_friday_from(OffsetDateTime::now_utc().date())
319    }
320
321    fn next_friday_from(today: Date) -> Self {
322        let days_to_add = match today.weekday() {
323            Weekday::Friday => 7,
324            other => Self::days_until_friday(other),
325        };
326        let next_friday = today + Duration::days(days_to_add);
327
328        ExpirationDate {
329            year: next_friday.year() as u16,
330            month: next_friday.month() as u8,
331            day: next_friday.day(),
332        }
333    }
334
335    /// Get the third Friday of the current month (standard monthly options expiration)
336    pub fn third_friday_of_month() -> Self {
337        Self::third_friday_from(OffsetDateTime::now_utc().date())
338    }
339
340    fn third_friday_from(today: Date) -> Self {
341        let year = today.year();
342        let month = today.month();
343
344        // Find the first day of the month
345        let first_of_month = Date::from_calendar_date(year, month, 1).expect("Valid date");
346
347        // Find the first Friday, then add 14 days to get third Friday
348        let days_to_first_friday = Self::days_until_friday(first_of_month.weekday());
349        let third_friday = first_of_month + Duration::days(days_to_first_friday + 14);
350
351        // If we've passed this month's third Friday, get next month's
352        if today > third_friday {
353            let next_month = if month == Month::December {
354                Date::from_calendar_date(year + 1, Month::January, 1)
355            } else {
356                Date::from_calendar_date(year, month.next(), 1)
357            }
358            .expect("Valid date");
359
360            let days_to_first_friday_next = Self::days_until_friday(next_month.weekday());
361            let third_friday_next = next_month + Duration::days(days_to_first_friday_next + 14);
362
363            ExpirationDate {
364                year: third_friday_next.year() as u16,
365                month: third_friday_next.month() as u8,
366                day: third_friday_next.day(),
367            }
368        } else {
369            ExpirationDate {
370                year: third_friday.year() as u16,
371                month: third_friday.month() as u8,
372                day: third_friday.day(),
373            }
374        }
375    }
376}
377
378impl fmt::Display for ExpirationDate {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        write!(f, "{:04}{:02}{:02}", self.year, self.month, self.day)
381    }
382}
383
384/// Contract month for futures
385#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct ContractMonth {
388    year: u16,
389    month: u8,
390}
391
392impl ContractMonth {
393    /// Construct a futures contract month given year and month.
394    pub fn new(year: u16, month: u8) -> Self {
395        ContractMonth { year, month }
396    }
397
398    /// Get the front month contract (next expiring)
399    pub fn front() -> Self {
400        let now = OffsetDateTime::now_utc();
401        Self::front_from(now.year() as u16, now.month() as u8, now.day())
402    }
403
404    fn front_from(current_year: u16, current_month: u8, current_day: u8) -> Self {
405        // Futures typically expire around the third Friday of the month
406        // If we're past the 15th, assume current month has expired
407        if current_day > 15 {
408            if current_month == 12 {
409                ContractMonth::new(current_year + 1, 1)
410            } else {
411                ContractMonth::new(current_year, current_month + 1)
412            }
413        } else {
414            ContractMonth::new(current_year, current_month)
415        }
416    }
417
418    /// Get the next quarterly contract month (Mar, Jun, Sep, Dec)
419    pub fn next_quarter() -> Self {
420        let now = OffsetDateTime::now_utc();
421        Self::next_quarter_from(now.year() as u16, now.month() as u8, now.day())
422    }
423
424    fn next_quarter_from(current_year: u16, current_month: u8, current_day: u8) -> Self {
425        // Find next quarterly month
426        let next_quarter_month = match current_month {
427            1 | 2 => 3,
428            3 if current_day > 15 => 6,
429            3 => 3,
430            4 | 5 => 6,
431            6 if current_day > 15 => 9,
432            6 => 6,
433            7 | 8 => 9,
434            9 if current_day > 15 => 12,
435            9 => 9,
436            10 | 11 => 12,
437            12 if current_day > 15 => 3,
438            12 => 12,
439            _ => 3,
440        };
441
442        // Adjust year if we wrapped around
443        let year = if current_month == 12 && next_quarter_month == 3 {
444            current_year + 1
445        } else {
446            current_year
447        };
448
449        ContractMonth::new(year, next_quarter_month)
450    }
451}
452
453impl fmt::Display for ContractMonth {
454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455        write!(f, "{:04}{:02}", self.year, self.month)
456    }
457}
458
459/// CUSIP identifier
460#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
461#[derive(Debug, Clone, PartialEq, Eq)]
462pub struct Cusip(pub String);
463
464impl Cusip {
465    /// Create a CUSIP identifier from any string-like value.
466    pub fn new(s: impl Into<String>) -> Self {
467        Cusip(s.into())
468    }
469
470    /// Return the underlying CUSIP text.
471    pub fn as_str(&self) -> &str {
472        &self.0
473    }
474}
475
476impl From<&str> for Cusip {
477    fn from(s: &str) -> Self {
478        Cusip(s.to_string())
479    }
480}
481
482impl From<String> for Cusip {
483    fn from(s: String) -> Self {
484        Cusip(s)
485    }
486}
487
488impl From<&String> for Cusip {
489    fn from(s: &String) -> Self {
490        Cusip(s.clone())
491    }
492}
493
494impl fmt::Display for Cusip {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        write!(f, "{}", self.0)
497    }
498}
499
500/// ISIN identifier
501#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
502#[derive(Debug, Clone, PartialEq, Eq)]
503pub struct Isin(pub String);
504
505impl Isin {
506    /// Create an ISIN identifier from any string-like value.
507    pub fn new(s: impl Into<String>) -> Self {
508        Isin(s.into())
509    }
510
511    /// Return the underlying ISIN text.
512    pub fn as_str(&self) -> &str {
513        &self.0
514    }
515}
516
517impl From<&str> for Isin {
518    fn from(s: &str) -> Self {
519        Isin(s.to_string())
520    }
521}
522
523impl From<String> for Isin {
524    fn from(s: String) -> Self {
525        Isin(s)
526    }
527}
528
529impl From<&String> for Isin {
530    fn from(s: &String) -> Self {
531        Isin(s.clone())
532    }
533}
534
535impl fmt::Display for Isin {
536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        write!(f, "{}", self.0)
538    }
539}
540
541/// Bond identifier type
542#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
543#[derive(Debug, Clone, PartialEq, Eq)]
544pub enum BondIdentifier {
545    /// A bond identified by a CUSIP code.
546    Cusip(Cusip),
547    /// A bond identified by an ISIN code.
548    Isin(Isin),
549}
550
551/// Trading action for spread/combo legs. Mirrors the IBKR wire vocabulary
552/// `BUY` / `SELL` / `SSHORT`. `SLONG` is not accepted on combo legs — only the
553/// SSHORT short-sale form is gated (`SSHORT_COMBO_LEGS = 35`, well below our
554/// floor of 210), so all three variants are unconditionally valid.
555#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
556#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
557#[non_exhaustive]
558pub enum LegAction {
559    /// Buy the leg.
560    #[default]
561    Buy,
562    /// Sell the leg.
563    Sell,
564    /// Short-sell the leg.
565    SellShort,
566}
567
568impl LegAction {
569    /// Return the canonical IB wire string (`"BUY"` / `"SELL"` / `"SSHORT"`).
570    pub fn as_str(&self) -> &'static str {
571        match self {
572            LegAction::Buy => "BUY",
573            LegAction::Sell => "SELL",
574            LegAction::SellShort => "SSHORT",
575        }
576    }
577
578    fn from_wire(s: &str) -> Option<Self> {
579        match s {
580            "BUY" => Some(Self::Buy),
581            "SELL" => Some(Self::Sell),
582            "SSHORT" => Some(Self::SellShort),
583            _ => None,
584        }
585    }
586}
587
588impl_wire_enum!(LegAction);
589
590/// Marker type for missing required fields in type-state builders
591#[derive(Debug, Clone, Copy)]
592pub struct Missing;
593
594/// Marker type for a futures builder with an unspecified contract month (open query).
595///
596/// Reached via [`FuturesBuilder::any_month`](crate::contracts::FuturesBuilder::any_month) to build
597/// a month-less futures contract for an enumerate-all `contract_details` lookup.
598#[derive(Debug, Clone, Copy)]
599pub struct AnyMonth;