Skip to main content

ibapi/contracts/
mod.rs

1//! Contract definitions and related functionality for trading instruments.
2//!
3//! This module provides data structures for representing various financial instruments
4//! including stocks, options, futures, and complex securities. It includes contract
5//! creation helpers, validation, and conversion utilities.
6
7use std::convert::From;
8use std::fmt::Debug;
9use std::string::ToString;
10
11use log::warn;
12use serde::Deserialize;
13use serde::Serialize;
14use tick_types::TickType;
15
16use crate::encode_option_field;
17use crate::messages::RequestMessage;
18use crate::messages::ResponseMessage;
19use crate::{Error, ToField};
20
21// Re-export V2 API types
22pub use builders::*;
23pub use types::*;
24
25// Common implementation modules
26mod common;
27
28// V2 API modules
29pub mod builders;
30pub mod types;
31
32// Feature-specific implementations
33#[cfg(feature = "sync")]
34mod sync;
35
36#[cfg(feature = "async")]
37mod r#async;
38
39/// Tick type constants used in option computations and market data.
40pub mod tick_types;
41
42// Models
43
44#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
45#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
46/// SecurityType enumerates available security types
47pub enum SecurityType {
48    /// Stock (or ETF)
49    #[default]
50    Stock,
51    /// Option
52    Option,
53    /// Future
54    Future,
55    /// Continuous Future
56    ContinuousFuture,
57    /// Index
58    Index,
59    /// Futures option
60    FuturesOption,
61    /// Forex pair
62    ForexPair,
63    /// Combo
64    Spread,
65    ///  Warrant
66    Warrant,
67    /// Bond
68    Bond,
69    /// Commodity
70    Commodity,
71    /// News
72    News,
73    /// Mutual fund
74    MutualFund,
75    /// Crypto currency
76    Crypto,
77    /// Contract for difference
78    CFD,
79    /// Other
80    Other(String),
81}
82
83impl ToField for SecurityType {
84    fn to_field(&self) -> String {
85        self.to_string()
86    }
87}
88
89impl ToField for Option<SecurityType> {
90    fn to_field(&self) -> String {
91        encode_option_field(self)
92    }
93}
94
95impl std::fmt::Display for SecurityType {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            SecurityType::Stock => write!(f, "STK"),
99            SecurityType::Option => write!(f, "OPT"),
100            SecurityType::Future => write!(f, "FUT"),
101            SecurityType::ContinuousFuture => write!(f, "CONTFUT"),
102            SecurityType::Index => write!(f, "IND"),
103            SecurityType::FuturesOption => write!(f, "FOP"),
104            SecurityType::ForexPair => write!(f, "CASH"),
105            SecurityType::Spread => write!(f, "BAG"),
106            SecurityType::Warrant => write!(f, "WAR"),
107            SecurityType::Bond => write!(f, "BOND"),
108            SecurityType::Commodity => write!(f, "CMDTY"),
109            SecurityType::News => write!(f, "NEWS"),
110            SecurityType::MutualFund => write!(f, "FUND"),
111            SecurityType::Crypto => write!(f, "CRYPTO"),
112            SecurityType::CFD => write!(f, "CFD"),
113            SecurityType::Other(name) => write!(f, "{name}"),
114        }
115    }
116}
117
118impl SecurityType {
119    /// Create a [SecurityType] from an IB symbol code (e.g. `STK`, `OPT`).
120    pub fn from(name: &str) -> SecurityType {
121        match name {
122            "STK" => SecurityType::Stock,
123            "OPT" => SecurityType::Option,
124            "FUT" => SecurityType::Future,
125            "CONTFUT" => SecurityType::ContinuousFuture,
126            "IND" => SecurityType::Index,
127            "FOP" => SecurityType::FuturesOption,
128            "CASH" => SecurityType::ForexPair,
129            "BAG" => SecurityType::Spread,
130            "WAR" => SecurityType::Warrant,
131            "BOND" => SecurityType::Bond,
132            "CMDTY" => SecurityType::Commodity,
133            "NEWS" => SecurityType::News,
134            "FUND" => SecurityType::MutualFund,
135            "CRYPTO" => SecurityType::Crypto,
136            "CFD" => SecurityType::CFD,
137            other => {
138                warn!("Unknown security type: {other}. Defaulting to Other");
139                SecurityType::Other(other.to_string())
140            }
141        }
142    }
143}
144
145#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147/// Contract describes an instrument's definition
148pub struct Contract {
149    /// The unique IB contract identifier.
150    pub contract_id: i32,
151    /// The underlying's asset symbol.
152    pub symbol: Symbol,
153    /// Type of security (stock, option, future, etc.).
154    pub security_type: SecurityType,
155    /// The contract's last trading day or contract month (for Options and Futures).
156    /// Strings with format YYYYMM will be interpreted as the Contract Month whereas YYYYMMDD will be interpreted as Last Trading Day.
157    pub last_trade_date_or_contract_month: String,
158    /// The option's strike price.
159    pub strike: f64,
160    /// Either Put or Call (i.e. Options). Valid values are P, PUT, C, CALL.
161    pub right: String,
162    /// The instrument's multiplier (i.e. options, futures).
163    pub multiplier: String,
164    /// The destination exchange.
165    pub exchange: Exchange,
166    /// The underlying's currency.
167    pub currency: Currency,
168    /// The contract's symbol within its primary exchange For options, this will be the OCC symbol.
169    pub local_symbol: String,
170    /// The contract's primary exchange.
171    /// For smart routed contracts, used to define contract in case of ambiguity.
172    /// Should be defined as native exchange of contract, e.g. ISLAND for MSFT For exchanges which contain a period in name, will only be part of exchange name prior to period, i.e. ENEXT for ENEXT.BE.
173    pub primary_exchange: Exchange,
174    /// The trading class name for this contract. Available in TWS contract description window as well. For example, GBL Dec '13 future's trading class is "FGBL".
175    pub trading_class: String,
176    /// If set to true, contract details requests and historical data queries can be performed pertaining to expired futures contracts. Expired options or other instrument types are not available.
177    pub include_expired: bool,
178    /// Security's identifier when querying contract's details or placing orders ISIN - Example: Apple: US0378331005 CUSIP - Example: Apple: 037833100.
179    pub security_id_type: String,
180    /// Identifier of the security type.
181    pub security_id: String,
182    /// Description of the combo legs.
183    pub combo_legs_description: String,
184    /// Individual legs composing a combo contract.
185    pub combo_legs: Vec<ComboLeg>,
186    /// Delta and underlying price for Delta-Neutral combo orders. Underlying (STK or FUT), delta and underlying price goes into this attribute.
187    pub delta_neutral_contract: Option<DeltaNeutralContract>,
188
189    /// The last trade date of the contract, returned by the server for derivatives.
190    pub last_trade_date: Option<time::Date>,
191
192    /// Identifier of the issuer for bonds and structured products.
193    pub issuer_id: String,
194    /// Human-readable description provided by TWS.
195    pub description: String,
196}
197
198impl Default for Contract {
199    fn default() -> Self {
200        Self {
201            contract_id: 0,
202            symbol: Symbol::default(),
203            security_type: SecurityType::default(),
204            last_trade_date_or_contract_month: String::new(),
205            strike: 0.0,
206            right: String::new(),
207            multiplier: String::new(),
208            exchange: Exchange::default(), // "SMART"
209            currency: Currency::default(),
210            local_symbol: String::new(),
211            primary_exchange: Exchange::from(""), // Empty, not "SMART"
212            trading_class: String::new(),
213            include_expired: false,
214            security_id_type: String::new(),
215            security_id: String::new(),
216            combo_legs_description: String::new(),
217            combo_legs: Vec::new(),
218            delta_neutral_contract: None,
219            last_trade_date: None,
220            issuer_id: String::new(),
221            description: String::new(),
222        }
223    }
224}
225
226impl Contract {
227    /// Creates a stock contract builder.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use ibapi::contracts::{Contract, Exchange, Currency};
233    ///
234    /// // Simple stock
235    /// let aapl = Contract::stock("AAPL").build();
236    ///
237    /// // Stock with customization
238    /// let toyota = Contract::stock("7203")
239    ///     .on_exchange("TSEJ")
240    ///     .in_currency("JPY")
241    ///     .build();
242    /// ```
243    pub fn stock(symbol: impl Into<Symbol>) -> StockBuilder<Symbol> {
244        StockBuilder::new(symbol)
245    }
246
247    /// Creates a call option contract builder.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use ibapi::contracts::Contract;
253    ///
254    /// let call = Contract::call("AAPL")
255    ///     .strike(150.0)
256    ///     .expires_on(2024, 12, 20)
257    ///     .build();
258    /// ```
259    pub fn call(symbol: impl Into<Symbol>) -> OptionBuilder<Symbol, Missing, Missing> {
260        OptionBuilder::call(symbol)
261    }
262
263    /// Creates a put option contract builder.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use ibapi::contracts::Contract;
269    ///
270    /// let put = Contract::put("SPY")
271    ///     .strike(450.0)
272    ///     .expires_on(2024, 12, 20)
273    ///     .build();
274    /// ```
275    pub fn put(symbol: impl Into<Symbol>) -> OptionBuilder<Symbol, Missing, Missing> {
276        OptionBuilder::put(symbol)
277    }
278
279    /// Creates a futures contract builder.
280    ///
281    /// # Examples
282    ///
283    /// ```
284    /// use ibapi::contracts::{Contract, ContractMonth};
285    ///
286    /// let es = Contract::futures("ES")
287    ///     .expires_in(ContractMonth::new(2024, 3))
288    ///     .build();
289    /// ```
290    pub fn futures(symbol: impl Into<Symbol>) -> FuturesBuilder<Symbol, Missing> {
291        FuturesBuilder::new(symbol)
292    }
293
294    /// Creates a continuous futures contract builder.
295    ///
296    /// # Examples
297    ///
298    /// ```
299    /// use ibapi::contracts::{Contract, ContractMonth};
300    ///
301    /// let es = Contract::continuous_futures("ES")
302    ///     .on_exchange("CME")
303    ///     .build();
304    /// ```
305    pub fn continuous_futures(symbol: impl Into<Symbol>) -> ContinuousFuturesBuilder<Symbol> {
306        ContinuousFuturesBuilder::new(symbol)
307    }
308
309    /// Creates a forex contract builder.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use ibapi::contracts::{Contract, Currency};
315    ///
316    /// let eur_usd = Contract::forex("EUR", "USD").build();
317    /// ```
318    pub fn forex(base: impl Into<Currency>, quote: impl Into<Currency>) -> ForexBuilder {
319        ForexBuilder::new(base, quote)
320    }
321
322    /// Creates a crypto contract builder.
323    ///
324    /// # Examples
325    ///
326    /// ```
327    /// use ibapi::contracts::Contract;
328    ///
329    /// let btc = Contract::crypto("BTC").build();
330    /// ```
331    pub fn crypto(symbol: impl Into<Symbol>) -> CryptoBuilder {
332        CryptoBuilder::new(symbol)
333    }
334
335    /// Creates an index contract.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use ibapi::contracts::Contract;
341    ///
342    /// let spx = Contract::index("SPX");
343    /// ```
344    pub fn index(symbol: &str) -> Contract {
345        let (exchange, currency): (Exchange, Currency) = match symbol {
346            "SPX" | "NDX" | "DJI" | "RUT" => ("CBOE".into(), "USD".into()),
347            "DAX" => ("EUREX".into(), "EUR".into()),
348            "FTSE" => ("LSE".into(), "GBP".into()),
349            _ => ("SMART".into(), "USD".into()),
350        };
351
352        Contract {
353            symbol: Symbol::new(symbol),
354            security_type: SecurityType::Index,
355            exchange,
356            currency,
357            ..Default::default()
358        }
359    }
360
361    /// Create a bond contract with CUSIP identifier
362    ///
363    /// # Example
364    /// ```
365    /// use ibapi::contracts::Contract;
366    ///
367    /// // US Treasury bond by CUSIP
368    /// let bond = Contract::bond_cusip("912810RN0");
369    /// ```
370    pub fn bond_cusip(cusip: impl Into<String>) -> Contract {
371        let cusip_str = cusip.into();
372        Contract {
373            symbol: Symbol::new(cusip_str.clone()),
374            security_type: SecurityType::Bond,
375            security_id_type: "CUSIP".to_string(),
376            security_id: cusip_str,
377            exchange: "SMART".into(),
378            currency: "USD".into(),
379            ..Default::default()
380        }
381    }
382
383    /// Create a bond contract with ISIN identifier
384    ///
385    /// # Example
386    /// ```
387    /// use ibapi::contracts::Contract;
388    ///
389    /// // European bond by ISIN
390    /// let bond = Contract::bond_isin("DE0001102309");
391    /// ```
392    pub fn bond_isin(isin: impl Into<String>) -> Contract {
393        let isin_str = isin.into();
394        // Determine currency from ISIN country code (first 2 chars)
395        let currency = match isin_str.get(0..2) {
396            Some("US") | Some("CA") => "USD",
397            Some("GB") => "GBP",
398            Some("JP") => "JPY",
399            Some("CH") => "CHF",
400            Some("AU") => "AUD",
401            Some("DE") | Some("FR") | Some("IT") | Some("ES") | Some("NL") | Some("BE") => "EUR",
402            _ => "USD", // Default to USD
403        };
404
405        Contract {
406            symbol: Symbol::new(isin_str.clone()),
407            security_type: SecurityType::Bond,
408            security_id_type: "ISIN".to_string(),
409            security_id: isin_str,
410            exchange: "SMART".into(),
411            currency: currency.into(),
412            ..Default::default()
413        }
414    }
415
416    /// Create a bond contract with CUSIP or ISIN identifier
417    ///
418    /// # Example
419    /// ```
420    /// use ibapi::contracts::{Contract, BondIdentifier, Cusip, Isin};
421    ///
422    /// // US Treasury bond by CUSIP
423    /// let bond = Contract::bond(BondIdentifier::Cusip(Cusip::new("912810RN0")));
424    ///
425    /// // European bond by ISIN
426    /// let bond = Contract::bond(BondIdentifier::Isin(Isin::new("DE0001102309")));
427    /// ```
428    pub fn bond(identifier: BondIdentifier) -> Contract {
429        match identifier {
430            BondIdentifier::Cusip(cusip) => Contract {
431                symbol: Symbol::new(cusip.to_string()),
432                security_type: SecurityType::Bond,
433                security_id_type: "CUSIP".to_string(),
434                security_id: cusip.to_string(),
435                exchange: "SMART".into(),
436                currency: "USD".into(),
437                ..Default::default()
438            },
439            BondIdentifier::Isin(isin) => {
440                // Determine currency from ISIN country code (first 2 chars)
441                let currency = match isin.as_str().get(0..2) {
442                    Some("US") | Some("CA") => "USD",
443                    Some("GB") => "GBP",
444                    Some("JP") => "JPY",
445                    Some("CH") => "CHF",
446                    Some("AU") => "AUD",
447                    Some("DE") | Some("FR") | Some("IT") | Some("ES") | Some("NL") | Some("BE") => "EUR",
448                    _ => "USD", // Default to USD
449                };
450
451                Contract {
452                    symbol: Symbol::new(isin.to_string()),
453                    security_type: SecurityType::Bond,
454                    security_id_type: "ISIN".to_string(),
455                    security_id: isin.to_string(),
456                    exchange: "SMART".into(),
457                    currency: currency.into(),
458                    ..Default::default()
459                }
460            }
461        }
462    }
463
464    /// Creates a spread/combo contract builder.
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// use ibapi::contracts::{Contract, LegAction};
470    ///
471    /// let spread = Contract::spread()
472    ///     .calendar(12345, 67890)
473    ///     .build();
474    /// ```
475    pub fn spread() -> SpreadBuilder {
476        SpreadBuilder::new()
477    }
478
479    /// Creates a news contract from the specified provider code.
480    ///
481    /// # Examples
482    ///
483    /// ```
484    /// use ibapi::contracts::{Contract, Symbol, Exchange};
485    ///
486    /// let news = Contract::news("BRFG");
487    /// assert_eq!(news.symbol, Symbol::from("BRFG:BRFG_ALL"));
488    /// assert_eq!(news.exchange, Exchange::from("BRFG"));
489    /// ```
490    pub fn news(provider_code: &str) -> Contract {
491        Contract {
492            symbol: Symbol::new(format!("{provider_code}:{provider_code}_ALL")),
493            security_type: SecurityType::News,
494            exchange: Exchange::from(provider_code),
495            ..Default::default()
496        }
497    }
498
499    /// Creates an option contract from the specified parameters.
500    ///
501    /// Currency defaults to USD and exchange defaults to SMART.
502    ///
503    /// # Arguments
504    /// * `symbol` - Symbol of the underlying asset
505    /// * `expiration_date` - Expiration date of option contract (YYYYMMDD)
506    /// * `strike` - Strike price of the option contract
507    /// * `right` - Option type: "C" for Call, "P" for Put
508    ///
509    /// # Examples
510    ///
511    /// ```
512    /// use ibapi::contracts::{Contract, Symbol};
513    ///
514    /// let call = Contract::option("AAPL", "20240119", 150.0, "C");
515    /// assert_eq!(call.symbol, Symbol::from("AAPL"));
516    /// assert_eq!(call.strike, 150.0);
517    /// assert_eq!(call.right, "C");
518    /// ```
519    /// Creates a simple option contract (for backward compatibility).
520    /// For new code, use Contract::call() or Contract::put() builders instead.
521    pub fn option(symbol: &str, expiration_date: &str, strike: f64, right: &str) -> Contract {
522        Contract {
523            symbol: symbol.into(),
524            security_type: SecurityType::Option,
525            exchange: "SMART".into(),
526            currency: "USD".into(),
527            last_trade_date_or_contract_month: expiration_date.into(),
528            strike,
529            right: right.into(),
530            ..Default::default()
531        }
532    }
533
534    /// Returns true if this contract represents a bag/combo order.
535    pub fn is_bag(&self) -> bool {
536        self.security_type == SecurityType::Spread
537    }
538
539    pub(crate) fn push_fields(&self, message: &mut RequestMessage) {
540        message.push_field(&self.contract_id);
541        message.push_field(&self.symbol);
542        message.push_field(&self.security_type);
543        message.push_field(&self.last_trade_date_or_contract_month);
544        message.push_field(&self.strike);
545        message.push_field(&self.right);
546        message.push_field(&self.multiplier);
547        message.push_field(&self.exchange);
548        message.push_field(&self.primary_exchange);
549        message.push_field(&self.currency);
550        message.push_field(&self.local_symbol);
551        message.push_field(&self.trading_class);
552        message.push_field(&self.include_expired);
553    }
554}
555
556/// A single component within a combo contract.
557#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
558#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
559pub struct ComboLeg {
560    /// The Contract's IB's unique id.
561    pub contract_id: i32,
562    /// Select the relative number of contracts for the leg you are constructing. To help determine the ratio for a specific combination order, refer to the Interactive Analytics section of the User's Guide.
563    pub ratio: i32,
564    /// The side (buy or sell) of the leg:
565    pub action: String,
566    /// The destination exchange to which the order will be routed.
567    pub exchange: String,
568    /// Specifies whether an order is an open or closing order.
569    /// For institutional customers to determine if this order is to open or close a position.
570    pub open_close: ComboLegOpenClose,
571    /// For stock legs when doing short selling. Set to 1 = clearing broker, 2 = third party.
572    pub short_sale_slot: i32,
573    /// When ShortSaleSlot is 2, this field shall contain the designated location.
574    pub designated_location: String,
575    /// Regulation SHO code for the leg (0 = none).
576    pub exempt_code: i32,
577}
578
579#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
580#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
581/// OpenClose specifies whether an order is an open or closing order.
582pub enum ComboLegOpenClose {
583    /// 0 - Same as the parent security. This is the only option for retail customers.
584    #[default]
585    Same = 0,
586    /// 1 - Open. This value is only valid for institutional customers.
587    Open = 1,
588    /// 2 - Close. This value is only valid for institutional customers.
589    Close = 2,
590    /// 3 - Unknown.
591    Unknown = 3,
592}
593
594impl ToField for ComboLegOpenClose {
595    fn to_field(&self) -> String {
596        (*self as u8).to_string()
597    }
598}
599
600impl From<i32> for ComboLegOpenClose {
601    // TODO - verify these values
602    fn from(val: i32) -> Self {
603        match val {
604            0 => Self::Same,
605            1 => Self::Open,
606            2 => Self::Close,
607            3 => Self::Unknown,
608            _ => panic!("unsupported value: {val}"),
609        }
610    }
611}
612
613#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
614#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
615/// Delta and underlying price for Delta-Neutral combo orders.
616/// Underlying (STK or FUT), delta and underlying price goes into this attribute.
617pub struct DeltaNeutralContract {
618    /// The unique contract identifier specifying the security. Used for Delta-Neutral Combo contracts.
619    pub contract_id: i32,
620    /// The underlying stock or future delta. Used for Delta-Neutral Combo contracts.
621    pub delta: f64,
622    /// The price of the underlying. Used for Delta-Neutral Combo contracts.
623    pub price: f64,
624}
625
626/// ContractDetails provides extended contract details.
627#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
628#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
629pub struct ContractDetails {
630    /// A fully-defined Contract object.
631    pub contract: Contract,
632    /// The market name for this product.
633    pub market_name: String,
634    /// The minimum allowed price variation. Note that many securities vary their minimum tick size according to their price. This value will only show the smallest of the different minimum tick sizes regardless of the product's price. Full information about the minimum increment price structure can be obtained with the reqMarketRule function or the IB Contract and Security Search site.
635    pub min_tick: f64,
636    /// Allows execution and strike prices to be reported consistently with market data, historical data and the order price, i.e. Z on LIFFE is reported in Index points and not GBP. In TWS versions prior to 972, the price magnifier is used in defining future option strike prices (e.g. in the API the strike is specified in dollars, but in TWS it is specified in cents). In TWS versions 972 and higher, the price magnifier is not used in defining futures option strike prices so they are consistent in TWS and the API.
637    pub price_magnifier: i32,
638    /// Supported order types for this product.
639    pub order_types: Vec<String>,
640    /// Valid exchange fields when placing an order for this contract.
641    /// The list of exchanges will is provided in the same order as the corresponding MarketRuleIds list.
642    pub valid_exchanges: Vec<String>,
643    /// For derivatives, the contract ID (conID) of the underlying instrument.
644    pub under_contract_id: i32,
645    /// Descriptive name of the product.
646    pub long_name: String,
647    /// Typically the contract month of the underlying for a Future contract.
648    pub contract_month: String,
649    /// The industry classification of the underlying/product. For example, Financial.
650    pub industry: String,
651    /// The industry category of the underlying. For example, InvestmentSvc.
652    pub category: String,
653    /// The industry subcategory of the underlying. For example, Brokerage.
654    pub subcategory: String,
655    /// The time zone for the trading hours of the product. For example, EST.
656    pub time_zone_id: String,
657    /// The trading hours of the product. This value will contain the trading hours of the current day as well as the next's. For example, 20090507:0700-1830,1830-2330;20090508:CLOSED. In TWS versions 965+ there is an option in the Global Configuration API settings to return 1 month of trading hours. In TWS version 970+, the format includes the date of the closing time to clarify potential ambiguity, ex: 20180323:0400-20180323:2000;20180326:0400-20180326:2000 The trading hours will correspond to the hours for the product on the associated exchange. The same instrument can have different hours on different exchanges.
658    pub trading_hours: Vec<String>,
659    /// The liquid hours of the product. This value will contain the liquid hours (regular trading hours) of the contract on the specified exchange. Format for TWS versions until 969: 20090507:0700-1830,1830-2330;20090508:CLOSED. In TWS versions 965+ there is an option in the Global Configuration API settings to return 1 month of trading hours. In TWS v970 and above, the format includes the date of the closing time to clarify potential ambiguity, e.g. 20180323:0930-20180323:1600;20180326:0930-20180326:1600.
660    pub liquid_hours: Vec<String>,
661    /// Contains the Economic Value Rule name and the respective optional argument. The two values should be separated by a colon. For example, aussieBond:YearsToExpiration=3. When the optional argument is not present, the first value will be followed by a colon.
662    pub ev_rule: String,
663    /// Tells you approximately how much the market value of a contract would change if the price were to change by 1. It cannot be used to get market value by multiplying the price by the approximate multiplier.
664    pub ev_multiplier: f64,
665    /// Aggregated group Indicates the smart-routing group to which a contract belongs. contracts which cannot be smart-routed have aggGroup = -1.
666    pub agg_group: i32,
667    /// A list of contract identifiers that the customer is allowed to view. CUSIP/ISIN/etc. For US stocks, receiving the ISIN requires the CUSIP market data subscription. For Bonds, the CUSIP or ISIN is input directly into the symbol field of the Contract class.
668    pub sec_id_list: Vec<TagValue>,
669    /// For derivatives, the symbol of the underlying contract.
670    pub under_symbol: String,
671    /// For derivatives, returns the underlying security type.
672    pub under_security_type: String,
673    /// The list of market rule IDs separated by comma Market rule IDs can be used to determine the minimum price increment at a given price.
674    pub market_rule_ids: Vec<String>,
675    /// Real expiration date. Requires TWS 968+ and API v973.04+. Python API specifically requires API v973.06+.
676    pub real_expiration_date: String,
677    /// Last trade time.
678    pub last_trade_time: String,
679    /// Stock type.
680    pub stock_type: String,
681    /// The nine-character bond CUSIP. For Bonds only. Receiving CUSIPs requires a CUSIP market data subscription.
682    pub cusip: String,
683    /// Identifies the credit rating of the issuer. This field is not currently available from the TWS API. For Bonds only. A higher credit rating generally indicates a less risky investment. Bond ratings are from Moody's and S&P respectively. Not currently implemented due to bond market data restrictions.
684    pub ratings: String,
685    /// A description string containing further descriptive information about the bond. For Bonds only.
686    pub desc_append: String,
687    /// The type of bond, such as "CORP.".
688    pub bond_type: String,
689    /// The type of bond coupon. This field is currently not available from the TWS API. For Bonds only.
690    pub coupon_type: String,
691    /// If true, the bond can be called by the issuer under certain conditions. This field is currently not available from the TWS API. For Bonds only.
692    pub callable: bool,
693    /// Values are True or False. If true, the bond can be sold back to the issuer under certain conditions. This field is currently not available from the TWS API. For Bonds only.
694    pub putable: bool,
695    /// The interest rate used to calculate the amount you will receive in interest payments over the course of the year. This field is currently not available from the TWS API. For Bonds only.
696    pub coupon: f64,
697    /// Values are True or False. If true, the bond can be converted to stock under certain conditions. This field is currently not available from the TWS API. For Bonds only.
698    pub convertible: bool,
699    /// The date on which the issuer must repay the face value of the bond. This field is currently not available from the TWS API. For Bonds only. Not currently implemented due to bond market data restrictions.
700    pub maturity: String,
701    /// The date the bond was issued. This field is currently not available from the TWS API. For Bonds only. Not currently implemented due to bond market data restrictions.
702    pub issue_date: String,
703    /// Only if bond has embedded options. This field is currently not available from the TWS API. Refers to callable bonds and puttable bonds. Available in TWS description window for bonds.
704    pub next_option_date: String,
705    /// Type of embedded option. This field is currently not available from the TWS API. Only if bond has embedded options.
706    pub next_option_type: String,
707    /// Only if bond has embedded options. This field is currently not available from the TWS API. For Bonds only.
708    pub next_option_partial: bool,
709    /// If populated for the bond in IB's database. For Bonds only.
710    pub notes: String,
711    /// Order's minimal size.
712    pub min_size: f64,
713    /// Order's size increment.
714    pub size_increment: f64,
715    /// Order's suggested size increment.
716    pub suggested_size_increment: f64,
717
718    // Fund fields (populated only for FUND security type)
719    /// Fund name.
720    pub fund_name: String,
721    /// Fund family.
722    pub fund_family: String,
723    /// Fund type.
724    pub fund_type: String,
725    /// Fund front load.
726    pub fund_front_load: String,
727    /// Fund back load.
728    pub fund_back_load: String,
729    /// Fund back load time interval.
730    pub fund_back_load_time_interval: String,
731    /// Fund management fee.
732    pub fund_management_fee: String,
733    /// Whether the fund is closed.
734    pub fund_closed: bool,
735    /// Whether the fund is closed for new investors.
736    pub fund_closed_for_new_investors: bool,
737    /// Whether the fund is closed for new money.
738    pub fund_closed_for_new_money: bool,
739    /// Fund notify amount.
740    pub fund_notify_amount: String,
741    /// Fund minimum initial purchase.
742    pub fund_minimum_initial_purchase: String,
743    /// Fund subsequent minimum purchase.
744    pub fund_subsequent_minimum_purchase: String,
745    /// Fund blue sky states.
746    pub fund_blue_sky_states: String,
747    /// Fund blue sky territories.
748    pub fund_blue_sky_territories: String,
749    /// Fund distribution policy indicator.
750    pub fund_distribution_policy_indicator: FundDistributionPolicyIndicator,
751    /// Fund asset type.
752    pub fund_asset_type: FundAssetType,
753
754    /// Ineligibility reasons for the contract.
755    pub ineligibility_reasons: Vec<IneligibilityReason>,
756}
757
758/// Fund distribution policy indicator.
759#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
760#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
761pub enum FundDistributionPolicyIndicator {
762    /// No distribution policy specified.
763    #[default]
764    None,
765    /// Accumulation fund.
766    AccumulationFund,
767    /// Income fund.
768    IncomeFund,
769}
770
771impl From<&str> for FundDistributionPolicyIndicator {
772    fn from(s: &str) -> Self {
773        match s {
774            "N" => FundDistributionPolicyIndicator::AccumulationFund,
775            "Y" => FundDistributionPolicyIndicator::IncomeFund,
776            _ => FundDistributionPolicyIndicator::None,
777        }
778    }
779}
780
781/// Fund asset type.
782#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
783#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
784pub enum FundAssetType {
785    /// No asset type specified.
786    #[default]
787    None,
788    /// Other asset types.
789    Others,
790    /// Money market fund.
791    MoneyMarket,
792    /// Fixed income fund.
793    FixedIncome,
794    /// Multi-asset fund.
795    MultiAsset,
796    /// Equity fund.
797    Equity,
798    /// Sector fund.
799    Sector,
800    /// Guaranteed fund.
801    Guaranteed,
802    /// Alternative fund.
803    Alternative,
804}
805
806impl From<&str> for FundAssetType {
807    fn from(s: &str) -> Self {
808        match s {
809            "000" => FundAssetType::Others,
810            "001" => FundAssetType::MoneyMarket,
811            "002" => FundAssetType::FixedIncome,
812            "003" => FundAssetType::MultiAsset,
813            "004" => FundAssetType::Equity,
814            "005" => FundAssetType::Sector,
815            "006" => FundAssetType::Guaranteed,
816            "007" => FundAssetType::Alternative,
817            _ => FundAssetType::None,
818        }
819    }
820}
821
822/// Reason why a contract is ineligible for trading.
823#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
824#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
825pub struct IneligibilityReason {
826    /// Reason identifier.
827    pub id: String,
828    /// Human-readable description.
829    pub description: String,
830}
831
832/// TagValue is a convenience struct to define key-value pairs.
833#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
834#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
835pub struct TagValue {
836    /// Name of the tag.
837    pub tag: String,
838    /// String representation of the value.
839    pub value: String,
840}
841
842impl ToField for Vec<TagValue> {
843    fn to_field(&self) -> String {
844        let mut values = Vec::new();
845        for tag_value in self {
846            values.push(format!("{}={};", tag_value.tag, tag_value.value))
847        }
848        values.concat()
849    }
850}
851
852/// Receives option specific market data.
853/// TWS’s options model volatility, prices, and deltas, along with the present value of dividends expected on that options underlier.
854#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
855#[derive(Debug, Default)]
856pub struct OptionComputation {
857    /// Specifies the type of option computation.
858    pub field: TickType,
859    /// 0 – return based, 1- price based.
860    pub tick_attribute: Option<i32>,
861    /// The implied volatility calculated by the TWS option modeler, using the specified tick type value.
862    pub implied_volatility: Option<f64>,
863    /// The option delta value.
864    pub delta: Option<f64>,
865    /// The option price.
866    pub option_price: Option<f64>,
867    /// The present value of dividends expected on the option’s underlying.
868    pub present_value_dividend: Option<f64>,
869    /// The option gamma value.
870    pub gamma: Option<f64>,
871    /// The option vega value.
872    pub vega: Option<f64>,
873    /// The option theta value.
874    pub theta: Option<f64>,
875    /// The price of the underlying.
876    pub underlying_price: Option<f64>,
877}
878
879/// Option chain metadata for a specific underlying security.
880#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
881#[derive(Debug, Default)]
882pub struct OptionChain {
883    /// The contract ID of the underlying security.
884    pub underlying_contract_id: i32,
885    /// The option trading class.
886    pub trading_class: String,
887    /// The option multiplier.
888    pub multiplier: String,
889    /// Exchange for which the derivative is hosted.
890    pub exchange: String,
891    /// A list of the expiries for the options of this underlying on this exchange.
892    pub expirations: Vec<String>,
893    /// A list of the possible strikes for options of this underlying on this exchange.
894    pub strikes: Vec<f64>,
895}
896
897// === API ===
898
899/// Contract data and list of derivative security types
900#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
901#[derive(Debug)]
902pub struct ContractDescription {
903    /// Fully qualified contract metadata.
904    pub contract: Contract,
905    /// Derivative security types available for the contract.
906    pub derivative_security_types: Vec<String>,
907}
908
909#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
910#[derive(Debug, Default)]
911/// Minimum price increment structure for a particular market rule ID.
912pub struct MarketRule {
913    /// Market Rule ID requested.
914    pub market_rule_id: i32,
915    /// Returns the available price increments based on the market rule.
916    pub price_increments: Vec<PriceIncrement>,
917}
918
919/// Price ladder entry describing the minimum tick between price bands.
920#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
921#[derive(Debug, Default)]
922pub struct PriceIncrement {
923    /// Lower inclusive edge where the increment applies.
924    pub low_edge: f64,
925    /// Minimum tick size within this price band.
926    pub increment: f64,
927}
928
929// Re-export API functions based on active feature
930#[cfg(feature = "sync")]
931/// Blocking contract lookup helpers backed by the synchronous transport.
932pub mod blocking {
933    pub(crate) use super::sync::{
934        calculate_implied_volatility, calculate_option_price, cancel_contract_details, contract_details, market_rule, matching_symbols, option_chain,
935    };
936}
937
938#[cfg(all(feature = "sync", not(feature = "async")))]
939#[allow(unused_imports)]
940pub(crate) use sync::{
941    calculate_implied_volatility, calculate_option_price, cancel_contract_details, contract_details, market_rule, matching_symbols, option_chain,
942};
943
944#[cfg(feature = "async")]
945pub(crate) use r#async::{
946    calculate_implied_volatility, calculate_option_price, cancel_contract_details, contract_details, market_rule, matching_symbols, option_chain,
947};
948
949// Public function for decoding option computation (used by market_data module)
950pub(crate) fn decode_option_computation(server_version: i32, message: &mut ResponseMessage) -> Result<OptionComputation, Error> {
951    common::decoders::decode_option_computation(server_version, message)
952}
953
954// ContractBuilder is deprecated - use the new builder methods on Contract instead
955// e.g., Contract::stock(), Contract::call(), Contract::put(), etc.
956
957#[cfg(all(test, feature = "utoipa"))]
958mod utoipa_tests {
959    use super::*;
960    fn assert_schema<T: utoipa::ToSchema>() {}
961
962    #[test]
963    fn schema_derives_work() {
964        assert_schema::<Contract>();
965        assert_schema::<ContractDetails>();
966        assert_schema::<SecurityType>();
967        assert_schema::<TagValue>();
968    }
969}
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974
975    #[test]
976    fn test_v2_builders() {
977        // Test stock builder
978        let stock = Contract::stock("AAPL").build();
979        assert_eq!(stock.symbol, Symbol::from("AAPL"), "stock.symbol");
980        assert_eq!(stock.security_type, SecurityType::Stock, "stock.security_type");
981        assert_eq!(stock.currency, Currency::from("USD"), "stock.currency");
982        assert_eq!(stock.exchange, Exchange::from("SMART"), "stock.exchange");
983
984        // Test stock with customization
985        let toyota = Contract::stock("7203").on_exchange("TSEJ").in_currency("JPY").build();
986        assert_eq!(toyota.symbol, Symbol::from("7203"));
987        assert_eq!(toyota.exchange, Exchange::from("TSEJ"));
988        assert_eq!(toyota.currency, Currency::from("JPY"));
989
990        // Test call option builder
991        let call = Contract::call("AAPL").strike(150.0).expires_on(2023, 12, 15).build();
992        assert_eq!(call.symbol, Symbol::from("AAPL"));
993        assert_eq!(call.security_type, SecurityType::Option);
994        assert_eq!(call.strike, 150.0);
995        assert_eq!(call.right, "C");
996        assert_eq!(call.last_trade_date_or_contract_month, "20231215");
997
998        // Test put option builder
999        let put = Contract::put("SPY").strike(450.0).expires_on(2024, 1, 19).build();
1000        assert_eq!(put.symbol, Symbol::from("SPY"));
1001        assert_eq!(put.right, "P");
1002        assert_eq!(put.strike, 450.0);
1003
1004        // Test crypto builder
1005        let btc = Contract::crypto("BTC").build();
1006        assert_eq!(btc.symbol, Symbol::from("BTC"));
1007        assert_eq!(btc.security_type, SecurityType::Crypto);
1008        assert_eq!(btc.currency, Currency::from("USD"));
1009        assert_eq!(btc.exchange, Exchange::from("PAXOS"));
1010
1011        // Test index
1012        let spx = Contract::index("SPX");
1013        assert_eq!(spx.symbol, Symbol::from("SPX"));
1014        assert_eq!(spx.security_type, SecurityType::Index);
1015        assert_eq!(spx.exchange, Exchange::from("CBOE"));
1016        assert_eq!(spx.currency, Currency::from("USD"));
1017
1018        // Test news constructor (unchanged)
1019        let news = Contract::news("BZ");
1020        assert_eq!(news.symbol, Symbol::from("BZ:BZ_ALL"));
1021        assert_eq!(news.security_type, SecurityType::News);
1022        assert_eq!(news.exchange, Exchange::from("BZ"));
1023
1024        // Test backward compatibility with option constructor
1025        let option = Contract::option("AAPL", "20231215", 150.0, "C");
1026        assert_eq!(option.symbol, Symbol::from("AAPL"));
1027        assert_eq!(option.security_type, SecurityType::Option);
1028        assert_eq!(option.strike, 150.0);
1029        assert_eq!(option.right, "C");
1030    }
1031
1032    #[test]
1033    fn test_security_type_from() {
1034        // Test all known security types
1035        assert_eq!(SecurityType::from("STK"), SecurityType::Stock, "STK should be Stock");
1036        assert_eq!(SecurityType::from("OPT"), SecurityType::Option, "OPT should be Option");
1037        assert_eq!(SecurityType::from("FUT"), SecurityType::Future, "FUT should be Future");
1038        assert_eq!(
1039            SecurityType::from("CONTFUT"),
1040            SecurityType::ContinuousFuture,
1041            "CONTFUT should be ContinuousFuture"
1042        );
1043        assert_eq!(SecurityType::from("IND"), SecurityType::Index, "IND should be Index");
1044        assert_eq!(SecurityType::from("FOP"), SecurityType::FuturesOption, "FOP should be FuturesOption");
1045        assert_eq!(SecurityType::from("CASH"), SecurityType::ForexPair, "CASH should be ForexPair");
1046        assert_eq!(SecurityType::from("BAG"), SecurityType::Spread, "BAG should be Spread");
1047        assert_eq!(SecurityType::from("WAR"), SecurityType::Warrant, "WAR should be Warrant");
1048        assert_eq!(SecurityType::from("BOND"), SecurityType::Bond, "BOND should be Bond");
1049        assert_eq!(SecurityType::from("CMDTY"), SecurityType::Commodity, "CMDTY should be Commodity");
1050        assert_eq!(SecurityType::from("NEWS"), SecurityType::News, "NEWS should be News");
1051        assert_eq!(SecurityType::from("FUND"), SecurityType::MutualFund, "FUND should be MutualFund");
1052        assert_eq!(SecurityType::from("CRYPTO"), SecurityType::Crypto, "CRYPTO should be Crypto");
1053        assert_eq!(SecurityType::from("CFD"), SecurityType::CFD, "CFD should be CFD");
1054
1055        // Test unknown security type
1056        match SecurityType::from("UNKNOWN") {
1057            SecurityType::Other(name) => assert_eq!(name, "UNKNOWN", "Other should contain original string"),
1058            _ => panic!("Expected SecurityType::Other for unknown type"),
1059        }
1060    }
1061
1062    #[test]
1063    fn test_combo_leg_open_close() {
1064        // Test From<i32> implementation
1065        assert_eq!(ComboLegOpenClose::from(0), ComboLegOpenClose::Same, "0 should be Same");
1066        assert_eq!(ComboLegOpenClose::from(1), ComboLegOpenClose::Open, "1 should be Open");
1067        assert_eq!(ComboLegOpenClose::from(2), ComboLegOpenClose::Close, "2 should be Close");
1068        assert_eq!(ComboLegOpenClose::from(3), ComboLegOpenClose::Unknown, "3 should be Unknown");
1069
1070        // Test ToField implementation
1071        assert_eq!(ComboLegOpenClose::Same.to_field(), "0", "Same should be 0");
1072        assert_eq!(ComboLegOpenClose::Open.to_field(), "1", "Open should be 1");
1073        assert_eq!(ComboLegOpenClose::Close.to_field(), "2", "Close should be 2");
1074        assert_eq!(ComboLegOpenClose::Unknown.to_field(), "3", "Unknown should be 3");
1075
1076        // Test Default implementation
1077        assert_eq!(ComboLegOpenClose::default(), ComboLegOpenClose::Same, "Default should be Same");
1078    }
1079
1080    #[test]
1081    #[should_panic(expected = "unsupported value")]
1082    fn test_combo_leg_open_close_panic() {
1083        let _ = ComboLegOpenClose::from(4);
1084    }
1085
1086    #[test]
1087    fn test_tag_value_to_field() {
1088        // Test with multiple TagValue items
1089        let tag_values = vec![
1090            TagValue {
1091                tag: "TAG1".to_string(),
1092                value: "VALUE1".to_string(),
1093            },
1094            TagValue {
1095                tag: "TAG2".to_string(),
1096                value: "VALUE2".to_string(),
1097            },
1098            TagValue {
1099                tag: "TAG3".to_string(),
1100                value: "VALUE3".to_string(),
1101            },
1102        ];
1103
1104        assert_eq!(
1105            tag_values.to_field(),
1106            "TAG1=VALUE1;TAG2=VALUE2;TAG3=VALUE3;",
1107            "Tag values should be formatted as TAG=VALUE; pairs"
1108        );
1109
1110        // Test with a single TagValue
1111        let single_tag_value = vec![TagValue {
1112            tag: "SINGLE_TAG".to_string(),
1113            value: "SINGLE_VALUE".to_string(),
1114        }];
1115
1116        assert_eq!(
1117            single_tag_value.to_field(),
1118            "SINGLE_TAG=SINGLE_VALUE;",
1119            "Single tag value should be formatted as TAG=VALUE;"
1120        );
1121
1122        // Test with empty vec
1123        let empty: Vec<TagValue> = vec![];
1124        assert_eq!(empty.to_field(), "", "Empty vec should result in empty string");
1125
1126        // Test with empty tag/value
1127        let empty_fields = vec![TagValue {
1128            tag: "".to_string(),
1129            value: "".to_string(),
1130        }];
1131
1132        assert_eq!(empty_fields.to_field(), "=;", "Empty tag/value should be formatted as =;");
1133    }
1134
1135    #[test]
1136    fn test_is_bag() {
1137        // Test with a regular stock contract (not a bag/spread)
1138        let stock_contract = Contract::stock("AAPL").build();
1139        assert!(!stock_contract.is_bag(), "Stock contract should not be a bag");
1140
1141        // Test with a regular option contract (not a bag/spread)
1142        let option_contract = Contract::option("AAPL", "20231215", 150.0, "C");
1143        assert!(!option_contract.is_bag(), "Option contract should not be a bag");
1144
1145        // Test with a futures contract (not a bag/spread)
1146        // Using the simple factory method for futures that requires adding expiry
1147        let futures_contract = Contract {
1148            symbol: Symbol::from("ES"),
1149            security_type: SecurityType::Future,
1150            ..Default::default()
1151        };
1152        assert!(!futures_contract.is_bag(), "Futures contract should not be a bag");
1153
1154        // Test with a contract that is a bag/spread
1155        let spread_contract = Contract {
1156            security_type: SecurityType::Spread,
1157            ..Default::default()
1158        };
1159        assert!(spread_contract.is_bag(), "Spread contract should be a bag");
1160
1161        // Test with an explicitly set BAG security type
1162        let bag_contract = Contract {
1163            security_type: SecurityType::from("BAG"),
1164            ..Default::default()
1165        };
1166        assert!(bag_contract.is_bag(), "BAG contract should be a bag");
1167
1168        // Test with combo legs
1169        let combo_contract = Contract {
1170            security_type: SecurityType::Spread,
1171            combo_legs: vec![
1172                ComboLeg {
1173                    contract_id: 12345,
1174                    ratio: 1,
1175                    action: "BUY".to_string(),
1176                    exchange: "SMART".to_string(),
1177                    ..Default::default()
1178                },
1179                ComboLeg {
1180                    contract_id: 67890,
1181                    ratio: 1,
1182                    action: "SELL".to_string(),
1183                    exchange: "SMART".to_string(),
1184                    ..Default::default()
1185                },
1186            ],
1187            ..Default::default()
1188        };
1189        assert!(combo_contract.is_bag(), "Contract with combo legs should be a bag");
1190    }
1191}