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/// Forex pair builder
301#[derive(Debug, Clone)]
302pub struct ForexBuilder {
303    base: Currency,
304    quote: Currency,
305    exchange: Exchange,
306}
307
308impl ForexBuilder {
309    /// Create a forex contract using the given base and quote currencies.
310    pub fn new(base: impl Into<Currency>, quote: impl Into<Currency>) -> Self {
311        ForexBuilder {
312            base: base.into(),
313            quote: quote.into(),
314            exchange: "IDEALPRO".into(),
315        }
316    }
317
318    /// Route the trade to a different forex venue.
319    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
320        self.exchange = exchange.into();
321        self
322    }
323
324    /// Complete the forex contract definition.
325    pub fn build(self) -> Contract {
326        Contract {
327            symbol: Symbol::new(self.base.0),
328            security_type: SecurityType::ForexPair,
329            exchange: self.exchange,
330            currency: self.quote,
331            ..Default::default()
332        }
333    }
334}
335
336/// Crypto currency builder
337#[derive(Debug, Clone)]
338pub struct CryptoBuilder {
339    symbol: Symbol,
340    exchange: Exchange,
341    currency: Currency,
342}
343
344impl CryptoBuilder {
345    /// Create a crypto contract for the specified symbol (e.g. `BTC`).
346    pub fn new(symbol: impl Into<Symbol>) -> Self {
347        CryptoBuilder {
348            symbol: symbol.into(),
349            exchange: "PAXOS".into(),
350            currency: "USD".into(),
351        }
352    }
353
354    /// Route the trade to a specific crypto venue.
355    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
356        self.exchange = exchange.into();
357        self
358    }
359
360    /// Quote the pair in an alternate fiat or stablecoin.
361    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
362        self.currency = currency.into();
363        self
364    }
365
366    /// Finish building the crypto contract.
367    pub fn build(self) -> Contract {
368        Contract {
369            symbol: self.symbol,
370            security_type: SecurityType::Crypto,
371            exchange: self.exchange,
372            currency: self.currency,
373            ..Default::default()
374        }
375    }
376}
377
378/// Spread/Combo builder
379#[derive(Debug, Clone)]
380pub struct SpreadBuilder {
381    legs: Vec<Leg>,
382    currency: Currency,
383    exchange: Exchange,
384}
385
386/// Internal representation of a spread leg used by [SpreadBuilder].
387#[derive(Debug, Clone)]
388pub struct Leg {
389    contract_id: i32,
390    action: LegAction,
391    ratio: i32,
392    exchange: Option<Exchange>,
393}
394
395impl SpreadBuilder {
396    /// Create an empty spread builder ready to accept legs.
397    pub fn new() -> Self {
398        SpreadBuilder {
399            legs: Vec::new(),
400            currency: "USD".into(),
401            exchange: "SMART".into(),
402        }
403    }
404}
405
406impl Default for SpreadBuilder {
407    fn default() -> Self {
408        Self::new()
409    }
410}
411
412impl SpreadBuilder {
413    /// Begin configuring a new leg for the spread.
414    pub fn add_leg(self, contract_id: i32, action: LegAction) -> LegBuilder {
415        LegBuilder {
416            parent: self,
417            leg: Leg {
418                contract_id,
419                action,
420                ratio: 1,
421                exchange: None,
422            },
423        }
424    }
425
426    /// Calendar spread convenience method
427    pub fn calendar(self, near_id: i32, far_id: i32) -> Self {
428        self.add_leg(near_id, LegAction::Buy).done().add_leg(far_id, LegAction::Sell).done()
429    }
430
431    /// Vertical spread convenience method
432    pub fn vertical(self, long_id: i32, short_id: i32) -> Self {
433        self.add_leg(long_id, LegAction::Buy).done().add_leg(short_id, LegAction::Sell).done()
434    }
435
436    /// Iron condor spread convenience method
437    pub fn iron_condor(self, long_put_id: i32, short_put_id: i32, short_call_id: i32, long_call_id: i32) -> Self {
438        self.add_leg(long_put_id, LegAction::Buy)
439            .done()
440            .add_leg(short_put_id, LegAction::Sell)
441            .done()
442            .add_leg(short_call_id, LegAction::Sell)
443            .done()
444            .add_leg(long_call_id, LegAction::Buy)
445            .done()
446    }
447
448    /// Override the spread currency, useful for non-USD underlyings.
449    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
450        self.currency = currency.into();
451        self
452    }
453
454    /// Route the spread order to a specific exchange.
455    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
456        self.exchange = exchange.into();
457        self
458    }
459
460    /// Finalize the spread contract, returning an error if no legs were added.
461    pub fn build(self) -> Result<Contract, Error> {
462        if self.legs.is_empty() {
463            return Err(Error::Simple("Spread must have at least one leg".into()));
464        }
465
466        let combo_legs: Vec<ComboLeg> = self
467            .legs
468            .into_iter()
469            .map(|leg| ComboLeg {
470                contract_id: leg.contract_id,
471                ratio: leg.ratio,
472                action: leg.action.to_string(),
473                exchange: leg.exchange.map(|e| e.to_string()).unwrap_or_default(),
474                ..Default::default()
475            })
476            .collect();
477
478        Ok(Contract {
479            security_type: SecurityType::Spread,
480            currency: self.currency,
481            exchange: self.exchange,
482            combo_legs,
483            ..Default::default()
484        })
485    }
486}
487
488/// Builder for individual spread legs
489pub struct LegBuilder {
490    parent: SpreadBuilder,
491    leg: Leg,
492}
493
494impl LegBuilder {
495    /// Set the contract ratio for the current leg.
496    pub fn ratio(mut self, ratio: i32) -> Self {
497        self.leg.ratio = ratio;
498        self
499    }
500
501    /// Target a specific exchange for the leg.
502    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
503        self.leg.exchange = Some(exchange.into());
504        self
505    }
506
507    /// Finish the leg and return control to the parent spread builder.
508    pub fn done(mut self) -> SpreadBuilder {
509        self.parent.legs.push(self.leg);
510        self.parent
511    }
512}
513
514#[cfg(test)]
515mod tests;