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};
4use thiserror::Error;
5
6/// Reasons a [`ContractConfig`](super::ContractConfig) cannot be mapped to a
7/// valid [`Contract`].
8///
9/// Every variant represents an input that would otherwise force the builder to
10/// silently fabricate a *wrong* contract (e.g. defaulting a missing option
11/// `right` to Call, or an unknown `security_type` to a stock). Surfacing these
12/// at construction — rather than deferring to an opaque IBKR submission
13/// rejection — keeps failures observable, per the library's contract.
14///
15/// `#[non_exhaustive]`: new validation reasons may be added without a breaking
16/// change as the contract builders grow.
17#[derive(Debug, Clone, PartialEq, Eq, Error)]
18#[non_exhaustive]
19pub enum ContractConfigError {
20    /// An `OPT` contract was configured without a `right`.
21    #[error("OPT contract requires a `right` field (expected one of C/CALL/P/PUT)")]
22    MissingOptionRight,
23
24    /// A `right` was supplied but is not one of `C`/`CALL`/`P`/`PUT`.
25    #[error("unrecognized option right {right:?} (expected one of C/CALL/P/PUT)")]
26    UnrecognizedOptionRight { right: String },
27
28    /// An `OPT` contract was configured without a `strike`.
29    #[error("OPT contract requires a `strike` field")]
30    MissingStrike,
31
32    /// A `FUT` or `OPT` contract was configured without a `last_trade_date`.
33    #[error("FUT/OPT contract requires a `last_trade_date` field")]
34    MissingLastTradeDate,
35
36    /// The `security_type` is not one of the supported `STK`/`FUT`/`OPT`/`CASH`.
37    #[error("unrecognized security_type {security_type:?} (expected one of STK/FUT/OPT/CASH)")]
38    UnrecognizedSecurityType { security_type: String },
39}
40
41/// Map a human/wire option-right string to `OptionRight`.
42///
43/// Accepts IBKR wire values (`"C"`/`"P"`) and common long forms
44/// (`"CALL"`/`"PUT"`), case-insensitively. Returns `None` for anything else.
45fn parse_option_right(right: &str) -> Option<OptionRight> {
46    let right = right.trim();
47    if right.eq_ignore_ascii_case("C") || right.eq_ignore_ascii_case("CALL") {
48        Some(OptionRight::Call)
49    } else if right.eq_ignore_ascii_case("P") || right.eq_ignore_ascii_case("PUT") {
50        Some(OptionRight::Put)
51    } else {
52        None
53    }
54}
55
56/// Build a stock contract for the given symbol.
57pub fn stock_contract(symbol: &str, exchange: &str, currency: &str) -> Contract {
58    Contract::stock(symbol)
59        .on_exchange(exchange)
60        .in_currency(currency)
61        .build()
62}
63
64/// Build a futures contract.
65pub fn futures_contract(
66    symbol: &str,
67    last_trade_date: &str,
68    exchange: &str,
69    currency: &str,
70) -> Contract {
71    Contract {
72        symbol: Symbol::new(symbol),
73        security_type: SecurityType::Future,
74        last_trade_date_or_contract_month: last_trade_date.to_string(),
75        exchange: Exchange::new(exchange),
76        currency: Currency::new(currency),
77        ..Default::default()
78    }
79}
80
81/// Build an options contract.
82///
83/// # Errors
84///
85/// Returns [`ContractConfigError::UnrecognizedOptionRight`] if `right` is not
86/// one of `C`/`CALL`/`P`/`PUT` (case-insensitive; leading and trailing
87/// whitespace is ignored). An option contract is invalid without a valid right,
88/// so this is surfaced at construction rather than as a later IBKR submission
89/// rejection.
90///
91/// Only `right` is validated here. `last_trade_date` and `strike` are forwarded
92/// to the IBKR contract as-is; an empty date or a `0.0` strike will build a
93/// (quietly wrong) contract. Presence of those fields is checked one level up in
94/// [`ContractConfig::to_contract`](super::ContractConfig::to_contract), which is
95/// the intended entry point for config-driven construction.
96pub fn option_contract(
97    symbol: &str,
98    last_trade_date: &str,
99    strike: f64,
100    right: &str,
101    exchange: &str,
102    currency: &str,
103) -> Result<Contract, ContractConfigError> {
104    let right =
105        parse_option_right(right).ok_or_else(|| ContractConfigError::UnrecognizedOptionRight {
106            right: right.to_string(),
107        })?;
108    Ok(Contract {
109        symbol: Symbol::new(symbol),
110        security_type: SecurityType::Option,
111        last_trade_date_or_contract_month: last_trade_date.to_string(),
112        strike,
113        right: Some(right),
114        exchange: Exchange::new(exchange),
115        currency: Currency::new(currency),
116        ..Default::default()
117    })
118}
119
120/// Build a forex contract.
121pub fn forex_contract(symbol: &str, currency: &str) -> Contract {
122    Contract {
123        symbol: Symbol::new(symbol),
124        security_type: SecurityType::ForexPair,
125        exchange: Exchange::new("IDEALPRO"),
126        currency: Currency::new(currency),
127        ..Default::default()
128    }
129}