Skip to main content

ibapi/contracts/
builders.rs

1//! Type-safe builders for different contract types.
2
3use super::types::*;
4use super::{ComboLeg, Contract, SecurityType};
5use crate::Error;
6
7/// Stock contract builder with type-safe API
8#[derive(Debug, Clone)]
9#[must_use = "StockBuilder does nothing until you call .build()"]
10pub struct StockBuilder<S = Missing> {
11    symbol: S,
12    exchange: Exchange,
13    currency: Currency,
14    primary_exchange: Option<Exchange>,
15    trading_class: Option<String>,
16}
17
18impl StockBuilder<Missing> {
19    /// Start building a stock contract for the provided symbol.
20    pub fn new(symbol: impl Into<Symbol>) -> StockBuilder<Symbol> {
21        StockBuilder {
22            symbol: symbol.into(),
23            exchange: "SMART".into(),
24            currency: "USD".into(),
25            primary_exchange: None,
26            trading_class: None,
27        }
28    }
29}
30
31impl StockBuilder<Symbol> {
32    /// Route the order to the specified exchange instead of the default.
33    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
34        self.exchange = exchange.into();
35        self
36    }
37
38    /// Quote the contract in a different currency.
39    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
40        self.currency = currency.into();
41        self
42    }
43
44    /// Prefer a specific primary exchange when resolving the contract.
45    pub fn primary(mut self, exchange: impl Into<Exchange>) -> Self {
46        self.primary_exchange = Some(exchange.into());
47        self
48    }
49
50    /// Hint the trading class for venues that require it.
51    pub fn trading_class(mut self, class: impl Into<String>) -> Self {
52        self.trading_class = Some(class.into());
53        self
54    }
55
56    /// Build the contract - cannot fail for stocks
57    pub fn build(self) -> Contract {
58        Contract {
59            symbol: self.symbol,
60            security_type: SecurityType::Stock,
61            exchange: self.exchange,
62            currency: self.currency,
63            primary_exchange: self.primary_exchange.unwrap_or_else(|| Exchange::from("")),
64            trading_class: self.trading_class.unwrap_or_default(),
65            ..Default::default()
66        }
67    }
68}
69
70/// Option contract builder with type states for required fields
71#[derive(Debug, Clone)]
72#[must_use = "OptionBuilder does nothing until you call .build()"]
73pub struct OptionBuilder<Symbol = Missing, Strike = Missing, Expiry = Missing> {
74    symbol: Symbol,
75    right: OptionRight,
76    strike: Strike,
77    expiry: Expiry,
78    exchange: Exchange,
79    currency: Currency,
80    multiplier: u32,
81    primary_exchange: Option<Exchange>,
82    trading_class: Option<String>,
83}
84
85impl OptionBuilder<Missing, Missing, Missing> {
86    /// Begin constructing a call option contract for the provided symbol.
87    pub fn call(symbol: impl Into<Symbol>) -> OptionBuilder<Symbol, Missing, Missing> {
88        OptionBuilder {
89            symbol: symbol.into(),
90            right: OptionRight::Call,
91            strike: Missing,
92            expiry: Missing,
93            exchange: "SMART".into(),
94            currency: "USD".into(),
95            multiplier: 100,
96            primary_exchange: None,
97            trading_class: None,
98        }
99    }
100
101    /// Begin constructing a put option contract for the provided symbol.
102    pub fn put(symbol: impl Into<Symbol>) -> OptionBuilder<Symbol, Missing, Missing> {
103        OptionBuilder {
104            symbol: symbol.into(),
105            right: OptionRight::Put,
106            strike: Missing,
107            expiry: Missing,
108            exchange: "SMART".into(),
109            currency: "USD".into(),
110            multiplier: 100,
111            primary_exchange: None,
112            trading_class: None,
113        }
114    }
115}
116
117// Can only set strike when symbol is present
118impl<E> OptionBuilder<Symbol, Missing, E> {
119    /// Specify the option strike price.
120    pub fn strike(self, price: f64) -> OptionBuilder<Symbol, Strike, E> {
121        OptionBuilder {
122            symbol: self.symbol,
123            right: self.right,
124            strike: Strike::new_unchecked(price),
125            expiry: self.expiry,
126            exchange: self.exchange,
127            currency: self.currency,
128            multiplier: self.multiplier,
129            primary_exchange: self.primary_exchange,
130            trading_class: self.trading_class,
131        }
132    }
133}
134
135// Can only set expiry when symbol is present
136impl<S> OptionBuilder<Symbol, S, Missing> {
137    /// Provide an explicit expiration date.
138    pub fn expires(self, date: ExpirationDate) -> OptionBuilder<Symbol, S, ExpirationDate> {
139        OptionBuilder {
140            symbol: self.symbol,
141            right: self.right,
142            strike: self.strike,
143            expiry: date,
144            exchange: self.exchange,
145            currency: self.currency,
146            multiplier: self.multiplier,
147            primary_exchange: self.primary_exchange,
148            trading_class: self.trading_class,
149        }
150    }
151
152    /// Convenience helper to set a specific calendar date.
153    pub fn expires_on(self, year: u16, month: u8, day: u8) -> OptionBuilder<Symbol, S, ExpirationDate> {
154        self.expires(ExpirationDate::new(year, month, day))
155    }
156
157    /// Set the expiry to the next Friday weekly contract.
158    pub fn expires_weekly(self) -> OptionBuilder<Symbol, S, ExpirationDate> {
159        self.expires(ExpirationDate::next_friday())
160    }
161
162    /// Set the expiry to the standard monthly contract.
163    pub fn expires_monthly(self) -> OptionBuilder<Symbol, S, ExpirationDate> {
164        self.expires(ExpirationDate::third_friday_of_month())
165    }
166}
167
168// Optional setters available at any stage when symbol is present
169impl<S, E> OptionBuilder<Symbol, S, E> {
170    /// Route the option to a specific exchange.
171    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
172        self.exchange = exchange.into();
173        self
174    }
175
176    /// Quote the option in a different currency.
177    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
178        self.currency = currency.into();
179        self
180    }
181
182    /// Override the contract multiplier (defaults to 100).
183    pub fn multiplier(mut self, multiplier: u32) -> Self {
184        self.multiplier = multiplier;
185        self
186    }
187
188    /// Prefer a specific primary exchange when resolving the option.
189    pub fn primary(mut self, exchange: impl Into<Exchange>) -> Self {
190        self.primary_exchange = Some(exchange.into());
191        self
192    }
193
194    /// Hint the trading class used by this contract.
195    pub fn trading_class(mut self, class: impl Into<String>) -> Self {
196        self.trading_class = Some(class.into());
197        self
198    }
199}
200
201// Build only available when all required fields are set
202impl OptionBuilder<Symbol, Strike, ExpirationDate> {
203    /// Finalize the option contract once symbol, strike, and expiry are set.
204    pub fn build(self) -> Contract {
205        Contract {
206            symbol: self.symbol,
207            security_type: SecurityType::Option,
208            strike: self.strike.value(),
209            right: Some(self.right),
210            last_trade_date_or_contract_month: self.expiry.to_string(),
211            exchange: self.exchange,
212            currency: self.currency,
213            multiplier: self.multiplier.to_string(),
214            primary_exchange: self.primary_exchange.unwrap_or_else(|| Exchange::from("")),
215            trading_class: self.trading_class.unwrap_or_default(),
216            ..Default::default()
217        }
218    }
219}
220
221/// Futures contract builder with type states
222#[derive(Debug, Clone)]
223#[must_use = "FuturesBuilder does nothing until you call .build()"]
224pub struct FuturesBuilder<Symbol = Missing, Month = Missing> {
225    symbol: Symbol,
226    contract_month: Month,
227    exchange: Exchange,
228    currency: Currency,
229    multiplier: Option<u32>,
230}
231
232impl FuturesBuilder<Missing, Missing> {
233    /// Start building a futures contract for the given symbol.
234    pub fn new(symbol: impl Into<Symbol>) -> FuturesBuilder<Symbol, Missing> {
235        FuturesBuilder {
236            symbol: symbol.into(),
237            contract_month: Missing,
238            exchange: "CME".into(),
239            currency: "USD".into(),
240            multiplier: None,
241        }
242    }
243}
244
245impl FuturesBuilder<Symbol, Missing> {
246    /// Specify the contract month to target for the future.
247    pub fn expires_in(self, month: ContractMonth) -> FuturesBuilder<Symbol, ContractMonth> {
248        FuturesBuilder {
249            symbol: self.symbol,
250            contract_month: month,
251            exchange: self.exchange,
252            currency: self.currency,
253            multiplier: self.multiplier,
254        }
255    }
256
257    /// Shortcut for selecting the current front-month contract.
258    pub fn front_month(self) -> FuturesBuilder<Symbol, ContractMonth> {
259        self.expires_in(ContractMonth::front())
260    }
261
262    /// Shortcut for selecting the next quarterly contract.
263    pub fn next_quarter(self) -> FuturesBuilder<Symbol, ContractMonth> {
264        self.expires_in(ContractMonth::next_quarter())
265    }
266
267    /// Leave the contract month unspecified for an open `contract_details` query.
268    ///
269    /// Use this when the month is what you are discovering: pass the resulting contract to
270    /// `contract_details(..)` to enumerate every listing, then pick or roll the front month
271    /// yourself from each `real_expiration_date`.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use ibapi::contracts::Contract;
277    ///
278    /// let query = Contract::futures("ES")
279    ///     .on_exchange("CME")
280    ///     .in_currency("USD")
281    ///     .any_month()
282    ///     .build();
283    /// ```
284    pub fn any_month(self) -> FuturesBuilder<Symbol, AnyMonth> {
285        FuturesBuilder {
286            symbol: self.symbol,
287            contract_month: AnyMonth,
288            exchange: self.exchange,
289            currency: self.currency,
290            multiplier: self.multiplier,
291        }
292    }
293}
294
295impl<M> FuturesBuilder<Symbol, M> {
296    /// Route the futures contract to a specific exchange.
297    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
298        self.exchange = exchange.into();
299        self
300    }
301
302    /// Quote the future in a different currency.
303    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
304        self.currency = currency.into();
305        self
306    }
307
308    /// Set a custom multiplier value for the contract.
309    pub fn multiplier(mut self, value: u32) -> Self {
310        self.multiplier = Some(value);
311        self
312    }
313}
314
315impl FuturesBuilder<Symbol, ContractMonth> {
316    /// Finalize the futures contract once the contract month is chosen.
317    pub fn build(self) -> Contract {
318        Contract {
319            symbol: self.symbol,
320            security_type: SecurityType::Future,
321            last_trade_date_or_contract_month: self.contract_month.to_string(),
322            exchange: self.exchange,
323            currency: self.currency,
324            multiplier: self.multiplier.map(|m| m.to_string()).unwrap_or_default(),
325            ..Default::default()
326        }
327    }
328}
329
330impl FuturesBuilder<Symbol, AnyMonth> {
331    /// Finalize a month-less futures contract for an open `contract_details` query.
332    ///
333    /// The contract month is left empty so the contract enumerates every listing rather than
334    /// targeting one expiration.
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// use ibapi::contracts::Contract;
340    ///
341    /// let query = Contract::futures("ES").in_currency("USD").any_month().build();
342    /// ```
343    pub fn build(self) -> Contract {
344        Contract {
345            symbol: self.symbol,
346            security_type: SecurityType::Future,
347            // last_trade_date_or_contract_month left empty (open query)
348            exchange: self.exchange,
349            currency: self.currency,
350            multiplier: self.multiplier.map(|m| m.to_string()).unwrap_or_default(),
351            ..Default::default()
352        }
353    }
354}
355
356/// Continuous futures contract builder with type states
357#[derive(Debug, Clone)]
358#[must_use = "ContinuousFuturesBuilder does nothing until you call .build()"]
359pub struct ContinuousFuturesBuilder<Symbol = Missing> {
360    symbol: Symbol,
361    exchange: Exchange,
362    currency: Currency,
363    multiplier: Option<u32>,
364}
365
366impl ContinuousFuturesBuilder<Missing> {
367    /// Create a continuous future contract for the given symbol.
368    pub fn new(symbol: impl Into<Symbol>) -> ContinuousFuturesBuilder<Symbol> {
369        ContinuousFuturesBuilder {
370            symbol: symbol.into(),
371            exchange: "CME".into(),
372            currency: "USD".into(),
373            multiplier: None,
374        }
375    }
376}
377
378impl ContinuousFuturesBuilder<Symbol> {
379    /// Route the continuous future to a specific exchange.
380    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
381        self.exchange = exchange.into();
382        self
383    }
384
385    /// Quote the continuous future in a different currency.
386    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
387        self.currency = currency.into();
388        self
389    }
390
391    /// Set a custom multiplier value for the continuous future.
392    pub fn multiplier(mut self, value: u32) -> Self {
393        self.multiplier = Some(value);
394        self
395    }
396
397    /// Finalize the continuous future contract definition.
398    pub fn build(self) -> Contract {
399        Contract {
400            symbol: self.symbol,
401            security_type: SecurityType::ContinuousFuture,
402            exchange: self.exchange,
403            currency: self.currency,
404            multiplier: self.multiplier.map(|m| m.to_string()).unwrap_or_default(),
405            ..Default::default()
406        }
407    }
408}
409
410/// Forex pair builder
411#[derive(Debug, Clone)]
412#[must_use = "ForexBuilder does nothing until you call .build()"]
413pub struct ForexBuilder {
414    base: Currency,
415    quote: Currency,
416    exchange: Exchange,
417}
418
419impl ForexBuilder {
420    /// Create a forex contract using the given base and quote currencies.
421    pub fn new(base: impl Into<Currency>, quote: impl Into<Currency>) -> Self {
422        ForexBuilder {
423            base: base.into(),
424            quote: quote.into(),
425            exchange: "IDEALPRO".into(),
426        }
427    }
428
429    /// Route the trade to a different forex venue.
430    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
431        self.exchange = exchange.into();
432        self
433    }
434
435    /// Complete the forex contract definition.
436    pub fn build(self) -> Contract {
437        Contract {
438            symbol: Symbol::new(self.base.0),
439            security_type: SecurityType::ForexPair,
440            exchange: self.exchange,
441            currency: self.quote,
442            ..Default::default()
443        }
444    }
445}
446
447/// Crypto currency builder
448#[derive(Debug, Clone)]
449#[must_use = "CryptoBuilder does nothing until you call .build()"]
450pub struct CryptoBuilder {
451    symbol: Symbol,
452    exchange: Exchange,
453    currency: Currency,
454}
455
456impl CryptoBuilder {
457    /// Create a crypto contract for the specified symbol (e.g. `BTC`).
458    pub fn new(symbol: impl Into<Symbol>) -> Self {
459        CryptoBuilder {
460            symbol: symbol.into(),
461            exchange: "PAXOS".into(),
462            currency: "USD".into(),
463        }
464    }
465
466    /// Route the trade to a specific crypto venue.
467    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
468        self.exchange = exchange.into();
469        self
470    }
471
472    /// Quote the pair in an alternate fiat or stablecoin.
473    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
474        self.currency = currency.into();
475        self
476    }
477
478    /// Finish building the crypto contract.
479    pub fn build(self) -> Contract {
480        Contract {
481            symbol: self.symbol,
482            security_type: SecurityType::Crypto,
483            exchange: self.exchange,
484            currency: self.currency,
485            ..Default::default()
486        }
487    }
488}
489
490/// Spread/Combo builder
491#[derive(Debug, Clone)]
492#[must_use = "SpreadBuilder does nothing until you call .build()"]
493pub struct SpreadBuilder {
494    legs: Vec<Leg>,
495    currency: Currency,
496    exchange: Exchange,
497}
498
499/// Internal representation of a spread leg used by [SpreadBuilder].
500#[derive(Debug, Clone)]
501pub struct Leg {
502    contract_id: i32,
503    action: LegAction,
504    ratio: i32,
505    exchange: Option<Exchange>,
506}
507
508impl SpreadBuilder {
509    /// Create an empty spread builder ready to accept legs.
510    pub fn new() -> Self {
511        SpreadBuilder {
512            legs: Vec::new(),
513            currency: "USD".into(),
514            exchange: "SMART".into(),
515        }
516    }
517}
518
519impl Default for SpreadBuilder {
520    fn default() -> Self {
521        Self::new()
522    }
523}
524
525impl SpreadBuilder {
526    /// Begin configuring a new leg for the spread.
527    pub fn add_leg(self, contract_id: i32, action: LegAction) -> LegBuilder {
528        LegBuilder {
529            parent: self,
530            leg: Leg {
531                contract_id,
532                action,
533                ratio: 1,
534                exchange: None,
535            },
536        }
537    }
538
539    /// Calendar spread convenience method
540    pub fn calendar(self, near_id: i32, far_id: i32) -> Self {
541        self.add_leg(near_id, LegAction::Buy).done().add_leg(far_id, LegAction::Sell).done()
542    }
543
544    /// Vertical spread convenience method
545    pub fn vertical(self, long_id: i32, short_id: i32) -> Self {
546        self.add_leg(long_id, LegAction::Buy).done().add_leg(short_id, LegAction::Sell).done()
547    }
548
549    /// Iron condor spread convenience method
550    pub fn iron_condor(self, long_put_id: i32, short_put_id: i32, short_call_id: i32, long_call_id: i32) -> Self {
551        self.add_leg(long_put_id, LegAction::Buy)
552            .done()
553            .add_leg(short_put_id, LegAction::Sell)
554            .done()
555            .add_leg(short_call_id, LegAction::Sell)
556            .done()
557            .add_leg(long_call_id, LegAction::Buy)
558            .done()
559    }
560
561    /// Override the spread currency, useful for non-USD underlyings.
562    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
563        self.currency = currency.into();
564        self
565    }
566
567    /// Route the spread order to a specific exchange.
568    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
569        self.exchange = exchange.into();
570        self
571    }
572
573    /// Finalize the spread contract, returning an error if no legs were added.
574    pub fn build(self) -> Result<Contract, Error> {
575        if self.legs.is_empty() {
576            return Err(Error::InvalidArgument("Spread must have at least one leg".into()));
577        }
578
579        let combo_legs: Vec<ComboLeg> = self
580            .legs
581            .into_iter()
582            .map(|leg| ComboLeg {
583                contract_id: leg.contract_id,
584                ratio: leg.ratio,
585                action: leg.action,
586                exchange: leg.exchange.map(|e| e.to_string()).unwrap_or_default(),
587                ..Default::default()
588            })
589            .collect();
590
591        Ok(Contract {
592            security_type: SecurityType::Spread,
593            currency: self.currency,
594            exchange: self.exchange,
595            combo_legs,
596            ..Default::default()
597        })
598    }
599}
600
601/// Builder for individual spread legs
602#[must_use = "LegBuilder must be terminated with .done() to add the leg to the SpreadBuilder"]
603pub struct LegBuilder {
604    parent: SpreadBuilder,
605    leg: Leg,
606}
607
608impl LegBuilder {
609    /// Set the contract ratio for the current leg.
610    pub fn ratio(mut self, ratio: i32) -> Self {
611        self.leg.ratio = ratio;
612        self
613    }
614
615    /// Target a specific exchange for the leg.
616    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
617        self.leg.exchange = Some(exchange.into());
618        self
619    }
620
621    /// Finish the leg and return control to the parent spread builder.
622    pub fn done(mut self) -> SpreadBuilder {
623        self.parent.legs.push(self.leg);
624        self.parent
625    }
626}
627
628#[cfg(test)]
629mod tests;