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}