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