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::accounts::types::ContractId;
6use crate::Error;
7
8/// Stock contract builder with type-safe API
9#[derive(Debug, Clone)]
10#[must_use = "StockBuilder does nothing until you call .build()"]
11pub struct StockBuilder<S = Missing> {
12    symbol: S,
13    exchange: Exchange,
14    currency: Currency,
15    primary_exchange: Option<Exchange>,
16    trading_class: Option<String>,
17}
18
19impl StockBuilder<Missing> {
20    /// Start building a stock contract for the provided symbol.
21    pub fn new(symbol: impl Into<Symbol>) -> StockBuilder<Symbol> {
22        StockBuilder {
23            symbol: symbol.into(),
24            exchange: "SMART".into(),
25            currency: "USD".into(),
26            primary_exchange: None,
27            trading_class: None,
28        }
29    }
30}
31
32impl StockBuilder<Symbol> {
33    /// Route the order to the specified exchange instead of the default.
34    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
35        self.exchange = exchange.into();
36        self
37    }
38
39    /// Quote the contract in a different currency.
40    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
41        self.currency = currency.into();
42        self
43    }
44
45    /// Prefer a specific primary exchange when resolving the contract.
46    pub fn primary(mut self, exchange: impl Into<Exchange>) -> Self {
47        self.primary_exchange = Some(exchange.into());
48        self
49    }
50
51    /// Hint the trading class for venues that require it.
52    pub fn trading_class(mut self, class: impl Into<String>) -> Self {
53        self.trading_class = Some(class.into());
54        self
55    }
56
57    /// Build the contract - cannot fail for stocks
58    pub fn build(self) -> Contract {
59        Contract {
60            symbol: self.symbol,
61            security_type: SecurityType::Stock,
62            exchange: self.exchange,
63            currency: self.currency,
64            primary_exchange: self.primary_exchange.unwrap_or_else(|| Exchange::from("")),
65            trading_class: self.trading_class.unwrap_or_default(),
66            ..Default::default()
67        }
68    }
69}
70
71/// Option contract builder with type states for required fields
72#[derive(Debug, Clone)]
73#[must_use = "OptionBuilder does nothing until you call .build()"]
74pub struct OptionBuilder<Symbol = Missing, Strike = Missing, Expiry = Missing> {
75    symbol: Symbol,
76    right: OptionRight,
77    strike: Strike,
78    expiry: Expiry,
79    exchange: Exchange,
80    currency: Currency,
81    multiplier: u32,
82    primary_exchange: Option<Exchange>,
83    trading_class: Option<String>,
84}
85
86impl OptionBuilder<Missing, Missing, Missing> {
87    /// Begin constructing a call option contract for the provided symbol.
88    pub fn call(symbol: impl Into<Symbol>) -> OptionBuilder<Symbol, Missing, Missing> {
89        OptionBuilder {
90            symbol: symbol.into(),
91            right: OptionRight::Call,
92            strike: Missing,
93            expiry: Missing,
94            exchange: "SMART".into(),
95            currency: "USD".into(),
96            multiplier: 100,
97            primary_exchange: None,
98            trading_class: None,
99        }
100    }
101
102    /// Begin constructing a put option contract for the provided symbol.
103    pub fn put(symbol: impl Into<Symbol>) -> OptionBuilder<Symbol, Missing, Missing> {
104        OptionBuilder {
105            symbol: symbol.into(),
106            right: OptionRight::Put,
107            strike: Missing,
108            expiry: Missing,
109            exchange: "SMART".into(),
110            currency: "USD".into(),
111            multiplier: 100,
112            primary_exchange: None,
113            trading_class: None,
114        }
115    }
116}
117
118// Can only set strike when symbol is present
119impl<E> OptionBuilder<Symbol, Missing, E> {
120    /// Specify the option strike price.
121    ///
122    /// # Panics
123    ///
124    /// If `price` is not positive and finite (zero, negative, NaN or infinite).
125    pub fn strike(self, price: f64) -> OptionBuilder<Symbol, Strike, E> {
126        OptionBuilder {
127            symbol: self.symbol,
128            right: self.right,
129            strike: Strike::new_unchecked(price),
130            expiry: self.expiry,
131            exchange: self.exchange,
132            currency: self.currency,
133            multiplier: self.multiplier,
134            primary_exchange: self.primary_exchange,
135            trading_class: self.trading_class,
136        }
137    }
138}
139
140// Can only set expiry when symbol is present
141impl<S> OptionBuilder<Symbol, S, Missing> {
142    /// Provide an explicit expiration date.
143    pub fn expires(self, date: ExpirationDate) -> OptionBuilder<Symbol, S, ExpirationDate> {
144        OptionBuilder {
145            symbol: self.symbol,
146            right: self.right,
147            strike: self.strike,
148            expiry: date,
149            exchange: self.exchange,
150            currency: self.currency,
151            multiplier: self.multiplier,
152            primary_exchange: self.primary_exchange,
153            trading_class: self.trading_class,
154        }
155    }
156
157    /// Convenience helper to set a specific calendar date.
158    pub fn expires_on(self, year: u16, month: u8, day: u8) -> OptionBuilder<Symbol, S, ExpirationDate> {
159        self.expires(ExpirationDate::new(year, month, day))
160    }
161
162    /// Set the expiry to the next Friday weekly contract.
163    pub fn expires_weekly(self) -> OptionBuilder<Symbol, S, ExpirationDate> {
164        self.expires(ExpirationDate::next_friday())
165    }
166
167    /// Set the expiry to the standard monthly contract.
168    pub fn expires_monthly(self) -> OptionBuilder<Symbol, S, ExpirationDate> {
169        self.expires(ExpirationDate::third_friday_of_month())
170    }
171}
172
173// Optional setters available at any stage when symbol is present
174impl<S, E> OptionBuilder<Symbol, S, E> {
175    /// Route the option to a specific exchange.
176    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
177        self.exchange = exchange.into();
178        self
179    }
180
181    /// Quote the option in a different currency.
182    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
183        self.currency = currency.into();
184        self
185    }
186
187    /// Override the contract multiplier (defaults to 100).
188    pub fn multiplier(mut self, multiplier: u32) -> Self {
189        self.multiplier = multiplier;
190        self
191    }
192
193    /// Prefer a specific primary exchange when resolving the option.
194    pub fn primary(mut self, exchange: impl Into<Exchange>) -> Self {
195        self.primary_exchange = Some(exchange.into());
196        self
197    }
198
199    /// Hint the trading class used by this contract.
200    pub fn trading_class(mut self, class: impl Into<String>) -> Self {
201        self.trading_class = Some(class.into());
202        self
203    }
204}
205
206// Build only available when all required fields are set
207impl OptionBuilder<Symbol, Strike, ExpirationDate> {
208    /// Finalize the option contract once symbol, strike, and expiry are set.
209    pub fn build(self) -> Contract {
210        Contract {
211            symbol: self.symbol,
212            security_type: SecurityType::Option,
213            strike: self.strike.value(),
214            right: Some(self.right),
215            last_trade_date_or_contract_month: self.expiry.to_string(),
216            exchange: self.exchange,
217            currency: self.currency,
218            multiplier: self.multiplier.to_string(),
219            primary_exchange: self.primary_exchange.unwrap_or_else(|| Exchange::from("")),
220            trading_class: self.trading_class.unwrap_or_default(),
221            ..Default::default()
222        }
223    }
224}
225
226/// Futures contract builder with type states
227#[derive(Debug, Clone)]
228#[must_use = "FuturesBuilder does nothing until you call .build()"]
229pub struct FuturesBuilder<Symbol = Missing, Month = Missing> {
230    symbol: Symbol,
231    contract_month: Month,
232    exchange: Exchange,
233    currency: Currency,
234    multiplier: Option<u32>,
235}
236
237impl FuturesBuilder<Missing, Missing> {
238    /// Start building a futures contract for the given symbol.
239    pub fn new(symbol: impl Into<Symbol>) -> FuturesBuilder<Symbol, Missing> {
240        FuturesBuilder {
241            symbol: symbol.into(),
242            contract_month: Missing,
243            exchange: "CME".into(),
244            currency: "USD".into(),
245            multiplier: None,
246        }
247    }
248}
249
250impl FuturesBuilder<Symbol, Missing> {
251    /// Specify the contract month to target for the future.
252    pub fn expires_in(self, month: ContractMonth) -> FuturesBuilder<Symbol, ContractMonth> {
253        FuturesBuilder {
254            symbol: self.symbol,
255            contract_month: month,
256            exchange: self.exchange,
257            currency: self.currency,
258            multiplier: self.multiplier,
259        }
260    }
261
262    /// Shortcut for selecting the current front-month contract.
263    pub fn front_month(self) -> FuturesBuilder<Symbol, ContractMonth> {
264        self.expires_in(ContractMonth::front())
265    }
266
267    /// Shortcut for selecting the next quarterly contract.
268    pub fn next_quarter(self) -> FuturesBuilder<Symbol, ContractMonth> {
269        self.expires_in(ContractMonth::next_quarter())
270    }
271
272    /// Leave the contract month unspecified for an open `contract_details` query.
273    ///
274    /// Use this when the month is what you are discovering: pass the resulting contract to
275    /// `contract_details(..)` to enumerate every listing, then pick or roll the front month
276    /// yourself from each `real_expiration_date`.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// use ibapi::contracts::Contract;
282    ///
283    /// let query = Contract::futures("ES")
284    ///     .on_exchange("CME")
285    ///     .in_currency("USD")
286    ///     .any_month()
287    ///     .build();
288    /// ```
289    pub fn any_month(self) -> FuturesBuilder<Symbol, AnyMonth> {
290        FuturesBuilder {
291            symbol: self.symbol,
292            contract_month: AnyMonth,
293            exchange: self.exchange,
294            currency: self.currency,
295            multiplier: self.multiplier,
296        }
297    }
298}
299
300impl<M> FuturesBuilder<Symbol, M> {
301    /// Route the futures contract to a specific exchange.
302    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
303        self.exchange = exchange.into();
304        self
305    }
306
307    /// Quote the future in a different currency.
308    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
309        self.currency = currency.into();
310        self
311    }
312
313    /// Set a custom multiplier value for the contract.
314    pub fn multiplier(mut self, value: u32) -> Self {
315        self.multiplier = Some(value);
316        self
317    }
318}
319
320impl FuturesBuilder<Symbol, ContractMonth> {
321    /// Finalize the futures contract once the contract month is chosen.
322    pub fn build(self) -> Contract {
323        Contract {
324            symbol: self.symbol,
325            security_type: SecurityType::Future,
326            last_trade_date_or_contract_month: self.contract_month.to_string(),
327            exchange: self.exchange,
328            currency: self.currency,
329            multiplier: self.multiplier.map(|m| m.to_string()).unwrap_or_default(),
330            ..Default::default()
331        }
332    }
333}
334
335impl FuturesBuilder<Symbol, AnyMonth> {
336    /// Finalize a month-less futures contract for an open `contract_details` query.
337    ///
338    /// The contract month is left empty so the contract enumerates every listing rather than
339    /// targeting one expiration.
340    ///
341    /// # Examples
342    ///
343    /// ```
344    /// use ibapi::contracts::Contract;
345    ///
346    /// let query = Contract::futures("ES").in_currency("USD").any_month().build();
347    /// ```
348    pub fn build(self) -> Contract {
349        Contract {
350            symbol: self.symbol,
351            security_type: SecurityType::Future,
352            // last_trade_date_or_contract_month left empty (open query)
353            exchange: self.exchange,
354            currency: self.currency,
355            multiplier: self.multiplier.map(|m| m.to_string()).unwrap_or_default(),
356            ..Default::default()
357        }
358    }
359}
360
361/// Continuous futures contract builder with type states
362#[derive(Debug, Clone)]
363#[must_use = "ContinuousFuturesBuilder does nothing until you call .build()"]
364pub struct ContinuousFuturesBuilder<Symbol = Missing> {
365    symbol: Symbol,
366    exchange: Exchange,
367    currency: Currency,
368    multiplier: Option<u32>,
369}
370
371impl ContinuousFuturesBuilder<Missing> {
372    /// Create a continuous future contract for the given symbol.
373    pub fn new(symbol: impl Into<Symbol>) -> ContinuousFuturesBuilder<Symbol> {
374        ContinuousFuturesBuilder {
375            symbol: symbol.into(),
376            exchange: "CME".into(),
377            currency: "USD".into(),
378            multiplier: None,
379        }
380    }
381}
382
383impl ContinuousFuturesBuilder<Symbol> {
384    /// Route the continuous future to a specific exchange.
385    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
386        self.exchange = exchange.into();
387        self
388    }
389
390    /// Quote the continuous future in a different currency.
391    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
392        self.currency = currency.into();
393        self
394    }
395
396    /// Set a custom multiplier value for the continuous future.
397    pub fn multiplier(mut self, value: u32) -> Self {
398        self.multiplier = Some(value);
399        self
400    }
401
402    /// Finalize the continuous future contract definition.
403    pub fn build(self) -> Contract {
404        Contract {
405            symbol: self.symbol,
406            security_type: SecurityType::ContinuousFuture,
407            exchange: self.exchange,
408            currency: self.currency,
409            multiplier: self.multiplier.map(|m| m.to_string()).unwrap_or_default(),
410            ..Default::default()
411        }
412    }
413}
414
415/// Forex pair builder
416#[derive(Debug, Clone)]
417#[must_use = "ForexBuilder does nothing until you call .build()"]
418pub struct ForexBuilder {
419    base: Currency,
420    quote: Currency,
421    exchange: Exchange,
422}
423
424impl ForexBuilder {
425    /// Create a forex contract using the given base and quote currencies.
426    pub fn new(base: impl Into<Currency>, quote: impl Into<Currency>) -> Self {
427        ForexBuilder {
428            base: base.into(),
429            quote: quote.into(),
430            exchange: "IDEALPRO".into(),
431        }
432    }
433
434    /// Route the trade to a different forex venue.
435    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
436        self.exchange = exchange.into();
437        self
438    }
439
440    /// Complete the forex contract definition.
441    pub fn build(self) -> Contract {
442        Contract {
443            symbol: Symbol::new(self.base.0),
444            security_type: SecurityType::ForexPair,
445            exchange: self.exchange,
446            currency: self.quote,
447            ..Default::default()
448        }
449    }
450}
451
452/// Crypto currency builder
453#[derive(Debug, Clone)]
454#[must_use = "CryptoBuilder does nothing until you call .build()"]
455pub struct CryptoBuilder {
456    symbol: Symbol,
457    exchange: Exchange,
458    currency: Currency,
459}
460
461impl CryptoBuilder {
462    /// Create a crypto contract for the specified symbol (e.g. `BTC`).
463    pub fn new(symbol: impl Into<Symbol>) -> Self {
464        CryptoBuilder {
465            symbol: symbol.into(),
466            exchange: "PAXOS".into(),
467            currency: "USD".into(),
468        }
469    }
470
471    /// Route the trade to a specific crypto venue.
472    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
473        self.exchange = exchange.into();
474        self
475    }
476
477    /// Quote the pair in an alternate fiat or stablecoin.
478    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
479        self.currency = currency.into();
480        self
481    }
482
483    /// Finish building the crypto contract.
484    pub fn build(self) -> Contract {
485        Contract {
486            symbol: self.symbol,
487            security_type: SecurityType::Crypto,
488            exchange: self.exchange,
489            currency: self.currency,
490            ..Default::default()
491        }
492    }
493}
494
495/// Spread/Combo builder
496#[derive(Debug, Clone)]
497#[must_use = "SpreadBuilder does nothing until you call .build()"]
498pub struct SpreadBuilder {
499    legs: Vec<Leg>,
500    symbol: Symbol,
501    currency: Currency,
502    exchange: Exchange,
503}
504
505/// Internal representation of a spread leg used by [SpreadBuilder].
506#[derive(Debug, Clone)]
507pub struct Leg {
508    contract_id: i32,
509    action: LegAction,
510    ratio: i32,
511    exchange: Option<Exchange>,
512}
513
514impl SpreadBuilder {
515    /// Create an empty spread builder ready to accept legs.
516    pub fn new() -> Self {
517        SpreadBuilder {
518            legs: Vec::new(),
519            symbol: Symbol::default(),
520            currency: "USD".into(),
521            exchange: "SMART".into(),
522        }
523    }
524}
525
526impl Default for SpreadBuilder {
527    fn default() -> Self {
528        Self::new()
529    }
530}
531
532impl SpreadBuilder {
533    /// Begin configuring a new leg for the spread.
534    pub fn add_leg(self, contract_id: impl Into<ContractId>, action: LegAction) -> LegBuilder {
535        LegBuilder {
536            parent: self,
537            leg: Leg {
538                contract_id: contract_id.into().value(),
539                action,
540                ratio: 1,
541                exchange: None,
542            },
543        }
544    }
545
546    /// Long calendar spread: sell the near-dated leg, buy the far-dated leg.
547    ///
548    /// Buy the combo to open. Both legs normally share strike and right.
549    pub fn long_calendar(self, near_id: impl Into<ContractId>, far_id: impl Into<ContractId>) -> Self {
550        self.add_leg(near_id, LegAction::Sell).done().add_leg(far_id, LegAction::Buy).done()
551    }
552
553    /// Short calendar spread: buy the near-dated leg, sell the far-dated leg.
554    ///
555    /// Buy the combo to open. Both legs normally share strike and right.
556    pub fn short_calendar(self, near_id: impl Into<ContractId>, far_id: impl Into<ContractId>) -> Self {
557        self.add_leg(near_id, LegAction::Buy).done().add_leg(far_id, LegAction::Sell).done()
558    }
559
560    /// Vertical spread: buy the long leg, sell the short leg.
561    ///
562    /// For an iron condor, use [`iron_condor`](Self::iron_condor).
563    ///
564    /// ```
565    /// use ibapi::contracts::Contract;
566    ///
567    /// let (long_call, short_call) = (100, 105);
568    /// let bull_call = Contract::spread().symbol("AAPL").vertical(long_call, short_call).build()?;
569    /// # Ok::<(), ibapi::Error>(())
570    /// ```
571    pub fn vertical(self, long_id: impl Into<ContractId>, short_id: impl Into<ContractId>) -> Self {
572        self.add_leg(long_id, LegAction::Buy).done().add_leg(short_id, LegAction::Sell).done()
573    }
574
575    /// Long straddle: buy a call and a put with the same strike and expiry.
576    ///
577    /// Buy the combo to open.
578    ///
579    /// ```
580    /// use ibapi::contracts::Contract;
581    ///
582    /// let (call, put) = (100, 101);
583    /// let straddle = Contract::spread().symbol("AAPL").straddle(call, put).build()?;
584    /// # Ok::<(), ibapi::Error>(())
585    /// ```
586    pub fn straddle(self, call_id: impl Into<ContractId>, put_id: impl Into<ContractId>) -> Self {
587        self.add_leg(call_id, LegAction::Buy).done().add_leg(put_id, LegAction::Buy).done()
588    }
589
590    /// Long strangle: buy an out-of-the-money call and put with the same expiry and different
591    /// strikes.
592    ///
593    /// Buy the combo to open. The legs are those of [`straddle`](Self::straddle); only the
594    /// strikes differ.
595    ///
596    /// ```
597    /// use ibapi::contracts::Contract;
598    ///
599    /// let (call, put) = (110, 90);
600    /// let strangle = Contract::spread().symbol("AAPL").strangle(call, put).build()?;
601    /// # Ok::<(), ibapi::Error>(())
602    /// ```
603    pub fn strangle(self, call_id: impl Into<ContractId>, put_id: impl Into<ContractId>) -> Self {
604        self.straddle(call_id, put_id)
605    }
606
607    /// Long butterfly: buy 1 lower, sell 2 middle, buy 1 upper.
608    ///
609    /// All legs share right and expiry. Buy the combo to open. The legs are named fields, so a
610    /// wing cannot be swapped with the body by argument order.
611    ///
612    /// ```
613    /// use ibapi::contracts::{ButterflyLegs, Contract};
614    ///
615    /// let butterfly = Contract::spread()
616    ///     .symbol("AAPL")
617    ///     .butterfly(ButterflyLegs {
618    ///         lower: 100.into(),
619    ///         middle: 105.into(),
620    ///         upper: 110.into(),
621    ///     })
622    ///     .build()?;
623    /// # Ok::<(), ibapi::Error>(())
624    /// ```
625    pub fn butterfly(self, legs: ButterflyLegs) -> Self {
626        self.add_leg(legs.lower, LegAction::Buy)
627            .done()
628            .add_leg(legs.middle, LegAction::Sell)
629            .ratio(2)
630            .done()
631            .add_leg(legs.upper, LegAction::Buy)
632            .done()
633    }
634
635    /// Iron condor: a put vertical plus a call vertical, each long the wing and short the body.
636    ///
637    /// Legs are added as buy long put, sell short put, buy long call, sell short call. Buy the
638    /// combo to open: it opens for a credit, so the limit price is negative. TWS rejects a
639    /// positive price with 201 ("Guaranteed-to-Lose"). The legs are named fields because four
640    /// ids in a fixed order compile when swapped and build a different strategy.
641    ///
642    /// ```
643    /// use ibapi::contracts::{Contract, IronCondorLegs};
644    ///
645    /// let iron_condor = Contract::spread()
646    ///     .symbol("AAPL")
647    ///     .iron_condor(IronCondorLegs {
648    ///         long_put: 100.into(),
649    ///         short_put: 105.into(),
650    ///         short_call: 110.into(),
651    ///         long_call: 115.into(),
652    ///     })
653    ///     .build()?;
654    /// # Ok::<(), ibapi::Error>(())
655    /// ```
656    pub fn iron_condor(self, legs: IronCondorLegs) -> Self {
657        self.vertical(legs.long_put, legs.short_put).vertical(legs.long_call, legs.short_call)
658    }
659
660    /// Collar: buy the stock, buy a put below it as a floor, sell a call above it as a cap.
661    ///
662    /// Legs are added as buy stock (ratio `shares_per_contract`), buy put, sell call. Buy the
663    /// combo to open. The limit price is per combo, a debit of about
664    /// `shares_per_contract × spot + put − call`.
665    ///
666    /// # Examples
667    ///
668    /// ```
669    /// use ibapi::contracts::{CollarLegs, Contract};
670    ///
671    /// let collar = Contract::spread()
672    ///     .symbol("AAPL")
673    ///     .collar(CollarLegs {
674    ///         stock: 265598.into(),
675    ///         put: 100.into(),
676    ///         call: 101.into(),
677    ///         shares_per_contract: 100,
678    ///     })
679    ///     .build()?;
680    /// # Ok::<(), ibapi::Error>(())
681    /// ```
682    pub fn collar(self, legs: CollarLegs) -> Self {
683        self.add_leg(legs.stock, LegAction::Buy)
684            .ratio(legs.shares_per_contract)
685            .done()
686            .add_leg(legs.put, LegAction::Buy)
687            .done()
688            .add_leg(legs.call, LegAction::Sell)
689            .done()
690    }
691
692    /// Set the combo's symbol. TWS requires it to place the combo and rejects one without it
693    /// with 321.
694    ///
695    /// Usually the underlying. For a combo across underlyings, TWS's samples join them with a
696    /// comma (`"IBKR,MCD"`).
697    ///
698    /// # Examples
699    ///
700    /// ```
701    /// use ibapi::contracts::Contract;
702    ///
703    /// let (long_call, short_call) = (100, 105);
704    /// let bull_call = Contract::spread().symbol("AAPL").vertical(long_call, short_call).build()?;
705    /// assert_eq!(bull_call.symbol.to_string(), "AAPL");
706    /// # Ok::<(), ibapi::Error>(())
707    /// ```
708    pub fn symbol(mut self, symbol: impl Into<Symbol>) -> Self {
709        self.symbol = symbol.into();
710        self
711    }
712
713    /// Override the spread currency, useful for non-USD underlyings.
714    pub fn in_currency(mut self, currency: impl Into<Currency>) -> Self {
715        self.currency = currency.into();
716        self
717    }
718
719    /// Route the spread order to a specific exchange.
720    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
721        self.exchange = exchange.into();
722        self
723    }
724
725    /// Finalize the spread contract, returning an error if no legs were added.
726    ///
727    /// A combo built without [`symbol`](Self::symbol) builds, but TWS rejects it with 321.
728    pub fn build(self) -> Result<Contract, Error> {
729        if self.legs.is_empty() {
730            return Err(Error::InvalidArgument("Spread must have at least one leg".into()));
731        }
732
733        let combo_legs: Vec<ComboLeg> = self
734            .legs
735            .into_iter()
736            .map(|leg| ComboLeg {
737                contract_id: leg.contract_id,
738                ratio: leg.ratio,
739                action: leg.action,
740                exchange: leg.exchange.map(|e| e.to_string()).unwrap_or_default(),
741                ..Default::default()
742            })
743            .collect();
744
745        Ok(Contract {
746            symbol: self.symbol,
747            security_type: SecurityType::Spread,
748            currency: self.currency,
749            exchange: self.exchange,
750            combo_legs,
751            ..Default::default()
752        })
753    }
754}
755
756/// Legs of a [`SpreadBuilder::butterfly`], by option contract id.
757#[derive(Debug, Clone, Copy, PartialEq, Eq)]
758pub struct ButterflyLegs {
759    /// Lower-strike wing, bought.
760    pub lower: ContractId,
761    /// Middle strike, sold twice.
762    pub middle: ContractId,
763    /// Upper-strike wing, bought.
764    pub upper: ContractId,
765}
766
767/// Legs of a [`SpreadBuilder::iron_condor`], by option contract id.
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub struct IronCondorLegs {
770    /// Lowest-strike put, bought.
771    pub long_put: ContractId,
772    /// Higher-strike put, sold.
773    pub short_put: ContractId,
774    /// Lower-strike call, sold.
775    pub short_call: ContractId,
776    /// Highest-strike call, bought.
777    pub long_call: ContractId,
778}
779
780/// Legs of a [`SpreadBuilder::collar`], by contract id.
781#[derive(Debug, Clone, Copy, PartialEq, Eq)]
782pub struct CollarLegs {
783    /// The underlying stock, bought.
784    pub stock: ContractId,
785    /// Put below the stock price, bought.
786    pub put: ContractId,
787    /// Call above the stock price, sold.
788    pub call: ContractId,
789    /// Shares one option contract delivers: the stock leg's ratio. Usually 100, and for
790    /// split-adjusted options usually the option's `multiplier` (see
791    /// [`ContractDetails`](crate::contracts::ContractDetails)). After a merger or spin-off the
792    /// deliverable can differ from both, so check it before collaring an adjusted option. A
793    /// wrong value still places, as a mis-hedged position rather than a collar.
794    pub shares_per_contract: i32,
795}
796
797/// Builder for individual spread legs
798#[must_use = "LegBuilder must be terminated with .done() to add the leg to the SpreadBuilder"]
799pub struct LegBuilder {
800    parent: SpreadBuilder,
801    leg: Leg,
802}
803
804impl LegBuilder {
805    /// Set the contract ratio for the current leg.
806    pub fn ratio(mut self, ratio: i32) -> Self {
807        self.leg.ratio = ratio;
808        self
809    }
810
811    /// Target a specific exchange for the leg.
812    pub fn on_exchange(mut self, exchange: impl Into<Exchange>) -> Self {
813        self.leg.exchange = Some(exchange.into());
814        self
815    }
816
817    /// Finish the leg and return control to the parent spread builder.
818    pub fn done(mut self) -> SpreadBuilder {
819        self.parent.legs.push(self.leg);
820        self.parent
821    }
822}
823
824#[cfg(test)]
825#[path = "builders_tests.rs"]
826mod tests;