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