Skip to main content

rustrade_execution/client/ibkr/
contract.rs

1//! Contract builders for IB integration.
2
3use ibapi::contracts::{Contract, Currency, Exchange, OptionRight, SecurityType, Symbol};
4
5/// Error returned by [`option_contract`] when the `right` string cannot be
6/// mapped to an [`OptionRight`].
7///
8/// Carries the offending input so callers can surface it (via [`Display`] or
9/// [`InvalidOptionRight::right`]). Building an option contract without a valid
10/// right is rejected at construction rather than deferred to an opaque IBKR
11/// submission failure.
12///
13/// [`Display`]: std::fmt::Display
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct InvalidOptionRight(String);
16
17impl InvalidOptionRight {
18    /// The unrecognized `right` input that triggered this error.
19    #[must_use]
20    pub fn right(&self) -> &str {
21        &self.0
22    }
23}
24
25impl std::fmt::Display for InvalidOptionRight {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(
28            f,
29            "unrecognized option right {:?} (expected one of C/CALL/P/PUT)",
30            self.0
31        )
32    }
33}
34
35impl std::error::Error for InvalidOptionRight {}
36
37/// Map a human/wire option-right string to `OptionRight`.
38///
39/// Accepts IBKR wire values (`"C"`/`"P"`) and common long forms
40/// (`"CALL"`/`"PUT"`), case-insensitively. Returns `None` for anything else.
41fn parse_option_right(right: &str) -> Option<OptionRight> {
42    let right = right.trim();
43    if right.eq_ignore_ascii_case("C") || right.eq_ignore_ascii_case("CALL") {
44        Some(OptionRight::Call)
45    } else if right.eq_ignore_ascii_case("P") || right.eq_ignore_ascii_case("PUT") {
46        Some(OptionRight::Put)
47    } else {
48        None
49    }
50}
51
52/// Build a stock contract for the given symbol.
53pub fn stock_contract(symbol: &str, exchange: &str, currency: &str) -> Contract {
54    Contract::stock(symbol)
55        .on_exchange(exchange)
56        .in_currency(currency)
57        .build()
58}
59
60/// Build a futures contract.
61pub fn futures_contract(
62    symbol: &str,
63    last_trade_date: &str,
64    exchange: &str,
65    currency: &str,
66) -> Contract {
67    Contract {
68        symbol: Symbol::new(symbol),
69        security_type: SecurityType::Future,
70        last_trade_date_or_contract_month: last_trade_date.to_string(),
71        exchange: Exchange::new(exchange),
72        currency: Currency::new(currency),
73        ..Default::default()
74    }
75}
76
77/// Build an options contract.
78///
79/// # Errors
80///
81/// Returns [`InvalidOptionRight`] if `right` is not one of `C`/`CALL`/`P`/`PUT`
82/// (case-insensitive; leading and trailing whitespace is ignored). An option
83/// contract is invalid without a right, so this is surfaced at construction
84/// rather than as a later IBKR submission rejection.
85pub fn option_contract(
86    symbol: &str,
87    last_trade_date: &str,
88    strike: f64,
89    right: &str,
90    exchange: &str,
91    currency: &str,
92) -> Result<Contract, InvalidOptionRight> {
93    let right = parse_option_right(right).ok_or_else(|| InvalidOptionRight(right.to_string()))?;
94    Ok(Contract {
95        symbol: Symbol::new(symbol),
96        security_type: SecurityType::Option,
97        last_trade_date_or_contract_month: last_trade_date.to_string(),
98        strike,
99        right: Some(right),
100        exchange: Exchange::new(exchange),
101        currency: Currency::new(currency),
102        ..Default::default()
103    })
104}
105
106/// Build a forex contract.
107pub fn forex_contract(symbol: &str, currency: &str) -> Contract {
108    Contract {
109        symbol: Symbol::new(symbol),
110        security_type: SecurityType::ForexPair,
111        exchange: Exchange::new("IDEALPRO"),
112        currency: Currency::new(currency),
113        ..Default::default()
114    }
115}