Skip to main content

ibapi/orders/
conditions.rs

1//! Builder types for order conditions.
2//!
3//! This module provides fluent builder APIs for constructing order conditions
4//! with type safety and validation.
5
6use serde::{Deserialize, Serialize};
7
8/// Price evaluation method for price conditions.
9///
10/// Determines which price feed to use when evaluating price conditions.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
12pub enum TriggerMethod {
13    /// Default method (last for most securities, double bid/ask for OTC and options)
14    #[default]
15    Default = 0,
16    /// Two consecutive bid or ask prices
17    DoubleBidAsk = 1,
18    /// Last traded price
19    Last = 2,
20    /// Two consecutive last prices
21    DoubleLast = 3,
22    /// Current bid or ask price
23    BidAsk = 4,
24    /// Last price or bid/ask if no last price available
25    LastOrBidAsk = 7,
26    /// Mid-point between bid and ask
27    Midpoint = 8,
28}
29
30impl From<TriggerMethod> for i32 {
31    fn from(method: TriggerMethod) -> i32 {
32        method as i32
33    }
34}
35
36impl From<i32> for TriggerMethod {
37    fn from(value: i32) -> Self {
38        match value {
39            0 => TriggerMethod::Default,
40            1 => TriggerMethod::DoubleBidAsk,
41            2 => TriggerMethod::Last,
42            3 => TriggerMethod::DoubleLast,
43            4 => TriggerMethod::BidAsk,
44            7 => TriggerMethod::LastOrBidAsk,
45            8 => TriggerMethod::Midpoint,
46            _ => TriggerMethod::Default,
47        }
48    }
49}
50
51impl crate::ToField for TriggerMethod {
52    fn to_field(&self) -> String {
53        i32::from(*self).to_string()
54    }
55}
56
57// ============================================================================
58// Condition Structs (to be created by Unit 1.1)
59// ============================================================================
60
61/// Price-based condition that activates an order when a contract reaches a specified price.
62///
63/// This condition monitors the price of a specific contract and triggers when the price
64/// crosses the specified threshold. The trigger method determines which price feed to use
65/// (last, bid/ask, mid-point, etc.).
66///
67/// # TWS Behavior
68///
69/// - The contract must be specified by its contract ID, which can be obtained via
70///   `contract_details()` API call
71/// - Different exchanges may have different price feeds available
72/// - The condition continuously monitors the price during market hours
73/// - When `conditions_ignore_rth` is true on the order, monitoring extends to
74///   after-hours trading
75///
76/// # Example
77///
78/// ```no_run
79/// use ibapi::orders::conditions::{PriceCondition, TriggerMethod};
80/// use ibapi::orders::OrderCondition;
81///
82/// // Trigger when AAPL (contract ID 265598) goes above $150 on SMART
83/// let condition = PriceCondition::builder(265598, "SMART")
84///     .greater_than(150.0)
85///     .trigger_method(TriggerMethod::Last)
86///     .build();
87///
88/// let order_condition = OrderCondition::Price(condition);
89/// ```
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91pub struct PriceCondition {
92    /// Contract identifier for the instrument to monitor.
93    /// Use contract_details() to obtain the contract_id for a symbol.
94    pub contract_id: i32,
95    /// Exchange where the price is monitored (e.g., "SMART", "NASDAQ", "NYSE").
96    pub exchange: String,
97    /// Trigger price threshold.
98    pub price: f64,
99    /// Method for price evaluation.
100    #[serde(serialize_with = "serialize_trigger_method", deserialize_with = "deserialize_trigger_method")]
101    pub trigger_method: TriggerMethod,
102    /// True to trigger when price goes above threshold, false for below.
103    pub is_more: bool,
104    /// True for AND condition (all conditions must be met), false for OR condition (any condition triggers).
105    pub is_conjunction: bool,
106}
107
108impl Default for PriceCondition {
109    fn default() -> Self {
110        Self {
111            contract_id: 0,
112            exchange: String::new(),
113            price: 0.0,
114            trigger_method: TriggerMethod::Default,
115            is_more: true,
116            is_conjunction: true,
117        }
118    }
119}
120
121// Serde helpers for TriggerMethod
122fn serialize_trigger_method<S>(method: &TriggerMethod, serializer: S) -> Result<S::Ok, S::Error>
123where
124    S: serde::Serializer,
125{
126    serializer.serialize_i32((*method).into())
127}
128
129fn deserialize_trigger_method<'de, D>(deserializer: D) -> Result<TriggerMethod, D::Error>
130where
131    D: serde::Deserializer<'de>,
132{
133    let value = i32::deserialize(deserializer)?;
134    Ok(value.into())
135}
136
137/// Time-based condition that activates an order at a specific date and time.
138///
139/// This condition triggers when the current time passes (or is before) the specified
140/// time threshold. Useful for scheduling orders to activate at specific times.
141///
142/// # TWS Behavior
143///
144/// - Time is evaluated based on the timezone specified in the time string
145/// - The condition checks continuously and triggers once the time threshold is crossed
146/// - Common use case: activate orders at market open, before close, or at specific times
147/// - Unlike `good_after_time`/`good_till_date` on the order itself, this can be combined
148///   with other conditions using AND/OR logic
149///
150/// # Time Format
151///
152/// Format: "YYYYMMDD HH:MM:SS TZ"
153/// - YYYYMMDD: Year, month, day (e.g., 20251230)
154/// - HH:MM:SS: Hour, minute, second in 24-hour format
155/// - TZ: Timezone (e.g., "UTC", "US/Eastern", "America/New_York")
156///
157/// # Example
158///
159/// ```no_run
160/// use ibapi::orders::conditions::TimeCondition;
161/// use ibapi::orders::OrderCondition;
162///
163/// // Trigger after 2:30 PM Eastern Time on December 30, 2025
164/// let condition = TimeCondition::builder()
165///     .greater_than("20251230 14:30:00 US/Eastern")
166///     .build();
167///
168/// let order_condition = OrderCondition::Time(condition);
169/// ```
170#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
171pub struct TimeCondition {
172    /// Time in format "YYYYMMDD HH:MM:SS TZ".
173    /// Example: "20251230 14:30:00 US/Eastern"
174    pub time: String,
175    /// True to trigger after the time, false for before.
176    pub is_more: bool,
177    /// True for AND condition (all conditions must be met), false for OR condition (any condition triggers).
178    pub is_conjunction: bool,
179}
180
181/// Margin cushion condition that activates an order based on account margin levels.
182///
183/// The margin cushion is a measure of account health, calculated as:
184/// (Equity with Loan Value - Maintenance Margin) / Net Liquidation Value
185///
186/// This condition monitors your account's margin cushion and triggers when it crosses
187/// the specified percentage threshold. Useful for risk management and protecting against
188/// margin calls.
189///
190/// # TWS Behavior
191///
192/// - Margin cushion is updated in real-time as positions and prices change
193/// - The percentage is specified as an integer (e.g., 30 for 30%)
194/// - Only applies to margin accounts; cash accounts will not trigger this condition
195/// - Common use: Submit protective orders when margin cushion falls below safe levels
196///
197/// # Example
198///
199/// ```no_run
200/// use ibapi::orders::conditions::MarginCondition;
201/// use ibapi::orders::OrderCondition;
202///
203/// // Trigger when margin cushion falls below 30%
204/// let condition = MarginCondition::builder()
205///     .less_than(30)
206///     .build();
207///
208/// let order_condition = OrderCondition::Margin(condition);
209/// ```
210#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
211pub struct MarginCondition {
212    /// Margin cushion percentage threshold (0-100).
213    /// Example: 30 represents 30% margin cushion.
214    pub percent: i32,
215    /// True to trigger when margin cushion goes above threshold, false for below.
216    pub is_more: bool,
217    /// True for AND condition (all conditions must be met), false for OR condition (any condition triggers).
218    pub is_conjunction: bool,
219}
220
221/// Execution-based condition that activates an order when a trade of a specific security executes.
222///
223/// This condition monitors executions in your account and triggers when any trade of the
224/// specified contract executes. The condition checks for executions matching the symbol,
225/// security type, and exchange.
226///
227/// # TWS Behavior
228///
229/// - The condition triggers on ANY execution of the specified contract, regardless of side or quantity
230/// - Only monitors executions in the current account
231/// - The execution can be from any order type (market, limit, stop, etc.)
232/// - Common use case: Place a hedge order immediately after an initial position is filled
233/// - The symbol must match exactly (case-sensitive in most cases)
234///
235/// # Example
236///
237/// ```no_run
238/// use ibapi::orders::conditions::ExecutionCondition;
239/// use ibapi::orders::OrderCondition;
240///
241/// // Trigger when MSFT stock executes on SMART exchange
242/// let condition = ExecutionCondition::builder("MSFT", "STK", "SMART")
243///     .build();
244///
245/// let order_condition = OrderCondition::Execution(condition);
246/// ```
247#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
248pub struct ExecutionCondition {
249    /// Symbol of the contract to monitor for executions.
250    pub symbol: String,
251    /// Security type: "STK" (stock), "OPT" (option), "FUT" (future), "FOP" (future option), etc.
252    pub security_type: String,
253    /// Exchange where execution is monitored (e.g., "SMART", "NASDAQ", "NYSE").
254    pub exchange: String,
255    /// True for AND condition (all conditions must be met), false for OR condition (any condition triggers).
256    pub is_conjunction: bool,
257}
258
259/// Volume-based condition that activates an order when cumulative volume reaches a threshold.
260///
261/// This condition monitors the cumulative trading volume for a specific contract throughout
262/// the trading day and triggers when the volume crosses the specified threshold.
263///
264/// # TWS Behavior
265///
266/// - Volume is cumulative from market open (resets daily)
267/// - The contract must be specified by its contract ID
268/// - Volume tracking is exchange-specific (different exchanges may show different volumes)
269/// - When `conditions_ignore_rth` is true on the order, includes after-hours volume
270/// - Common use case: Enter positions after sufficient liquidity is established
271///
272/// # Example
273///
274/// ```no_run
275/// use ibapi::orders::conditions::VolumeCondition;
276/// use ibapi::orders::OrderCondition;
277///
278/// // Trigger when TSLA volume exceeds 50 million shares
279/// let condition = VolumeCondition::builder(76792991, "SMART")
280///     .greater_than(50_000_000)
281///     .build();
282///
283/// let order_condition = OrderCondition::Volume(condition);
284/// ```
285#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
286pub struct VolumeCondition {
287    /// Contract identifier for the instrument to monitor.
288    /// Use contract_details() to obtain the contract_id for a symbol.
289    pub contract_id: i32,
290    /// Exchange where volume is monitored (e.g., "SMART", "NASDAQ", "NYSE").
291    pub exchange: String,
292    /// Volume threshold (number of shares/contracts traded).
293    pub volume: i32,
294    /// True to trigger when volume goes above threshold, false for below.
295    pub is_more: bool,
296    /// True for AND condition (all conditions must be met), false for OR condition (any condition triggers).
297    pub is_conjunction: bool,
298}
299
300/// Percent change condition that activates an order based on price movement percentage.
301///
302/// This condition monitors the percentage change in a contract's price from its value at
303/// the start of the trading day and triggers when the change crosses the specified threshold.
304/// The percentage can be positive (gain) or negative (loss).
305///
306/// # TWS Behavior
307///
308/// - Percent change is calculated from the session's opening price
309/// - The contract must be specified by its contract ID
310/// - The percentage is specified as a decimal (e.g., 2.0 for 2%, not 0.02)
311/// - When `is_more` is true, triggers on upward moves; when false, on downward moves
312/// - Resets at the start of each trading session
313/// - Common use case: Momentum trading or volatility-based order activation
314///
315/// # Example
316///
317/// ```no_run
318/// use ibapi::orders::conditions::PercentChangeCondition;
319/// use ibapi::orders::OrderCondition;
320///
321/// // Trigger when SPY moves more than 2% upward from open
322/// let condition = PercentChangeCondition::builder(756733, "SMART")
323///     .greater_than(2.0)
324///     .build();
325///
326/// let order_condition = OrderCondition::PercentChange(condition);
327/// ```
328#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
329pub struct PercentChangeCondition {
330    /// Contract identifier for the instrument to monitor.
331    /// Use contract_details() to obtain the contract_id for a symbol.
332    pub contract_id: i32,
333    /// Exchange where price change is monitored (e.g., "SMART", "NASDAQ", "NYSE").
334    pub exchange: String,
335    /// Percentage change threshold (e.g., 2.0 for 2%, 5.5 for 5.5%).
336    pub percent: f64,
337    /// True to trigger when percent change goes above threshold (gains), false for below (losses).
338    pub is_more: bool,
339    /// True for AND condition (all conditions must be met), false for OR condition (any condition triggers).
340    pub is_conjunction: bool,
341}
342
343// ============================================================================
344// Builder Types
345// ============================================================================
346
347/// Builder for [`PriceCondition`].
348///
349/// # Example
350///
351/// ```no_run
352/// use ibapi::orders::conditions::{PriceCondition, TriggerMethod};
353///
354/// let condition = PriceCondition::builder(12345, "NASDAQ")
355///     .greater_than(150.0)
356///     .trigger_method(TriggerMethod::Last)
357///     .conjunction(false)
358///     .build();
359/// ```
360#[derive(Debug, Clone)]
361pub struct PriceConditionBuilder {
362    contract_id: i32,
363    exchange: String,
364    price: Option<f64>,
365    trigger_method: TriggerMethod,
366    is_more: bool,
367    is_conjunction: bool,
368}
369
370impl PriceCondition {
371    /// Create a builder for a price condition.
372    ///
373    /// # Parameters
374    ///
375    /// - `contract_id`: Contract identifier for the instrument to monitor
376    /// - `exchange`: Exchange where the price is monitored
377    pub fn builder(contract_id: i32, exchange: impl Into<String>) -> PriceConditionBuilder {
378        PriceConditionBuilder::new(contract_id, exchange)
379    }
380}
381
382impl PriceConditionBuilder {
383    /// Create a new price condition builder.
384    ///
385    /// # Parameters
386    ///
387    /// - `contract_id`: Contract identifier for the instrument to monitor
388    /// - `exchange`: Exchange where the price is monitored
389    pub fn new(contract_id: i32, exchange: impl Into<String>) -> Self {
390        Self {
391            contract_id,
392            exchange: exchange.into(),
393            price: None,                            // Must be set by greater_than/less_than
394            trigger_method: TriggerMethod::Default, // Default trigger method
395            is_more: true,                          // Default: trigger when price goes above
396            is_conjunction: true,                   // Default: AND condition
397        }
398    }
399
400    /// Set trigger when price is greater than the specified value.
401    pub fn greater_than(mut self, price: f64) -> Self {
402        self.price = Some(price);
403        self.is_more = true;
404        self
405    }
406
407    /// Set trigger when price is less than the specified value.
408    pub fn less_than(mut self, price: f64) -> Self {
409        self.price = Some(price);
410        self.is_more = false;
411        self
412    }
413
414    /// Set the trigger method for price evaluation.
415    ///
416    /// # Example
417    ///
418    /// ```no_run
419    /// use ibapi::orders::conditions::{PriceCondition, TriggerMethod};
420    ///
421    /// let condition = PriceCondition::builder(12345, "NASDAQ")
422    ///     .greater_than(150.0)
423    ///     .trigger_method(TriggerMethod::Last)
424    ///     .build();
425    /// ```
426    pub fn trigger_method(mut self, method: TriggerMethod) -> Self {
427        self.trigger_method = method;
428        self
429    }
430
431    /// Set whether this is an AND (conjunction) or OR (disjunction) condition.
432    ///
433    /// Default is `true` (AND).
434    pub fn conjunction(mut self, is_conjunction: bool) -> Self {
435        self.is_conjunction = is_conjunction;
436        self
437    }
438
439    /// Build the price condition.
440    pub fn build(self) -> PriceCondition {
441        PriceCondition {
442            contract_id: self.contract_id,
443            exchange: self.exchange,
444            price: self
445                .price
446                .expect("PriceConditionBuilder requires a price threshold; call greater_than() or less_than() before build()"),
447            trigger_method: self.trigger_method,
448            is_more: self.is_more,
449            is_conjunction: self.is_conjunction,
450        }
451    }
452}
453
454/// Builder for [`TimeCondition`].
455///
456/// # Example
457///
458/// ```no_run
459/// use ibapi::orders::conditions::TimeCondition;
460///
461/// let condition = TimeCondition::builder()
462///     .greater_than("20251230 23:59:59 UTC")
463///     .build();
464/// ```
465#[derive(Debug, Clone)]
466pub struct TimeConditionBuilder {
467    time: Option<String>,
468    is_more: bool,
469    is_conjunction: bool,
470}
471
472impl TimeCondition {
473    /// Create a builder for a time condition.
474    pub fn builder() -> TimeConditionBuilder {
475        TimeConditionBuilder::new()
476    }
477}
478
479impl TimeConditionBuilder {
480    /// Create a new time condition builder.
481    pub fn new() -> Self {
482        Self {
483            time: None,           // Must be set by greater_than/less_than
484            is_more: true,        // Default: trigger after time
485            is_conjunction: true, // Default: AND condition
486        }
487    }
488
489    /// Set trigger when time is greater than (after) the specified time.
490    ///
491    /// # Parameters
492    ///
493    /// - `time`: Time in format "YYYYMMDD HH:MM:SS TZ"
494    pub fn greater_than(mut self, time: impl Into<String>) -> Self {
495        self.time = Some(time.into());
496        self.is_more = true;
497        self
498    }
499
500    /// Set trigger when time is less than (before) the specified time.
501    ///
502    /// # Parameters
503    ///
504    /// - `time`: Time in format "YYYYMMDD HH:MM:SS TZ"
505    pub fn less_than(mut self, time: impl Into<String>) -> Self {
506        self.time = Some(time.into());
507        self.is_more = false;
508        self
509    }
510
511    /// Set whether this is an AND (conjunction) or OR (disjunction) condition.
512    ///
513    /// Default is `true` (AND).
514    pub fn conjunction(mut self, is_conjunction: bool) -> Self {
515        self.is_conjunction = is_conjunction;
516        self
517    }
518
519    /// Build the time condition.
520    pub fn build(self) -> TimeCondition {
521        TimeCondition {
522            time: self
523                .time
524                .expect("TimeConditionBuilder requires a time value; call greater_than() or less_than() before build()"),
525            is_more: self.is_more,
526            is_conjunction: self.is_conjunction,
527        }
528    }
529}
530
531impl Default for TimeConditionBuilder {
532    fn default() -> Self {
533        Self::new()
534    }
535}
536
537/// Builder for [`MarginCondition`].
538///
539/// # Example
540///
541/// ```no_run
542/// use ibapi::orders::conditions::MarginCondition;
543///
544/// let condition = MarginCondition::builder()
545///     .less_than(30)
546///     .build();
547/// ```
548#[derive(Debug, Clone)]
549pub struct MarginConditionBuilder {
550    percent: Option<i32>,
551    is_more: bool,
552    is_conjunction: bool,
553}
554
555impl MarginCondition {
556    /// Create a builder for a margin cushion condition.
557    pub fn builder() -> MarginConditionBuilder {
558        MarginConditionBuilder::new()
559    }
560}
561
562impl MarginConditionBuilder {
563    /// Create a new margin condition builder.
564    pub fn new() -> Self {
565        Self {
566            percent: None,        // Must be set by greater_than/less_than
567            is_more: true,        // Default: trigger when above threshold
568            is_conjunction: true, // Default: AND condition
569        }
570    }
571
572    /// Set trigger when margin cushion is greater than the specified percentage.
573    pub fn greater_than(mut self, percent: i32) -> Self {
574        self.percent = Some(percent);
575        self.is_more = true;
576        self
577    }
578
579    /// Set trigger when margin cushion is less than the specified percentage.
580    pub fn less_than(mut self, percent: i32) -> Self {
581        self.percent = Some(percent);
582        self.is_more = false;
583        self
584    }
585
586    /// Set whether this is an AND (conjunction) or OR (disjunction) condition.
587    ///
588    /// Default is `true` (AND).
589    pub fn conjunction(mut self, is_conjunction: bool) -> Self {
590        self.is_conjunction = is_conjunction;
591        self
592    }
593
594    /// Build the margin condition.
595    pub fn build(self) -> MarginCondition {
596        MarginCondition {
597            percent: self
598                .percent
599                .expect("MarginConditionBuilder requires a percentage threshold; call greater_than() or less_than() before build()"),
600            is_more: self.is_more,
601            is_conjunction: self.is_conjunction,
602        }
603    }
604}
605
606impl Default for MarginConditionBuilder {
607    fn default() -> Self {
608        Self::new()
609    }
610}
611
612/// Builder for [`ExecutionCondition`].
613///
614/// # Example
615///
616/// ```no_run
617/// use ibapi::orders::conditions::ExecutionCondition;
618///
619/// let condition = ExecutionCondition::builder("AAPL", "STK", "SMART")
620///     .conjunction(false)
621///     .build();
622/// ```
623#[derive(Debug, Clone)]
624pub struct ExecutionConditionBuilder {
625    symbol: String,
626    security_type: String,
627    exchange: String,
628    is_conjunction: bool,
629}
630
631impl ExecutionCondition {
632    /// Create a builder for an execution condition.
633    ///
634    /// # Parameters
635    ///
636    /// - `symbol`: Symbol of the contract
637    /// - `security_type`: Security type (e.g., "STK", "OPT")
638    /// - `exchange`: Exchange where execution is monitored
639    pub fn builder(symbol: impl Into<String>, security_type: impl Into<String>, exchange: impl Into<String>) -> ExecutionConditionBuilder {
640        ExecutionConditionBuilder::new(symbol, security_type, exchange)
641    }
642}
643
644impl ExecutionConditionBuilder {
645    /// Create a new execution condition builder.
646    ///
647    /// # Parameters
648    ///
649    /// - `symbol`: Symbol of the contract
650    /// - `security_type`: Security type (e.g., "STK", "OPT")
651    /// - `exchange`: Exchange where execution is monitored
652    pub fn new(symbol: impl Into<String>, security_type: impl Into<String>, exchange: impl Into<String>) -> Self {
653        Self {
654            symbol: symbol.into(),
655            security_type: security_type.into(),
656            exchange: exchange.into(),
657            is_conjunction: true, // Default: AND condition
658        }
659    }
660
661    /// Set whether this is an AND (conjunction) or OR (disjunction) condition.
662    ///
663    /// Default is `true` (AND).
664    pub fn conjunction(mut self, is_conjunction: bool) -> Self {
665        self.is_conjunction = is_conjunction;
666        self
667    }
668
669    /// Build the execution condition.
670    pub fn build(self) -> ExecutionCondition {
671        ExecutionCondition {
672            symbol: self.symbol,
673            security_type: self.security_type,
674            exchange: self.exchange,
675            is_conjunction: self.is_conjunction,
676        }
677    }
678}
679
680/// Builder for [`VolumeCondition`].
681///
682/// # Example
683///
684/// ```no_run
685/// use ibapi::orders::conditions::VolumeCondition;
686///
687/// let condition = VolumeCondition::builder(12345, "NASDAQ")
688///     .greater_than(1000000)
689///     .build();
690/// ```
691#[derive(Debug, Clone)]
692pub struct VolumeConditionBuilder {
693    contract_id: i32,
694    exchange: String,
695    volume: Option<i32>,
696    is_more: bool,
697    is_conjunction: bool,
698}
699
700impl VolumeCondition {
701    /// Create a builder for a volume condition.
702    ///
703    /// # Parameters
704    ///
705    /// - `contract_id`: Contract identifier for the instrument to monitor
706    /// - `exchange`: Exchange where volume is monitored
707    pub fn builder(contract_id: i32, exchange: impl Into<String>) -> VolumeConditionBuilder {
708        VolumeConditionBuilder::new(contract_id, exchange)
709    }
710}
711
712impl VolumeConditionBuilder {
713    /// Create a new volume condition builder.
714    ///
715    /// # Parameters
716    ///
717    /// - `contract_id`: Contract identifier for the instrument to monitor
718    /// - `exchange`: Exchange where volume is monitored
719    pub fn new(contract_id: i32, exchange: impl Into<String>) -> Self {
720        Self {
721            contract_id,
722            exchange: exchange.into(),
723            volume: None,         // Must be set by greater_than/less_than
724            is_more: true,        // Default: trigger when above threshold
725            is_conjunction: true, // Default: AND condition
726        }
727    }
728
729    /// Set trigger when volume is greater than the specified value.
730    pub fn greater_than(mut self, volume: i32) -> Self {
731        self.volume = Some(volume);
732        self.is_more = true;
733        self
734    }
735
736    /// Set trigger when volume is less than the specified value.
737    pub fn less_than(mut self, volume: i32) -> Self {
738        self.volume = Some(volume);
739        self.is_more = false;
740        self
741    }
742
743    /// Set whether this is an AND (conjunction) or OR (disjunction) condition.
744    ///
745    /// Default is `true` (AND).
746    pub fn conjunction(mut self, is_conjunction: bool) -> Self {
747        self.is_conjunction = is_conjunction;
748        self
749    }
750
751    /// Build the volume condition.
752    pub fn build(self) -> VolumeCondition {
753        VolumeCondition {
754            contract_id: self.contract_id,
755            exchange: self.exchange,
756            volume: self
757                .volume
758                .expect("VolumeConditionBuilder requires a volume threshold; call greater_than() or less_than() before build()"),
759            is_more: self.is_more,
760            is_conjunction: self.is_conjunction,
761        }
762    }
763}
764
765/// Builder for [`PercentChangeCondition`].
766///
767/// # Example
768///
769/// ```no_run
770/// use ibapi::orders::conditions::PercentChangeCondition;
771///
772/// let condition = PercentChangeCondition::builder(12345, "NASDAQ")
773///     .greater_than(5.0)
774///     .build();
775/// ```
776#[derive(Debug, Clone)]
777pub struct PercentChangeConditionBuilder {
778    contract_id: i32,
779    exchange: String,
780    percent: Option<f64>,
781    is_more: bool,
782    is_conjunction: bool,
783}
784
785impl PercentChangeCondition {
786    /// Create a builder for a percent change condition.
787    ///
788    /// # Parameters
789    ///
790    /// - `contract_id`: Contract identifier for the instrument to monitor
791    /// - `exchange`: Exchange where price change is monitored
792    pub fn builder(contract_id: i32, exchange: impl Into<String>) -> PercentChangeConditionBuilder {
793        PercentChangeConditionBuilder::new(contract_id, exchange)
794    }
795}
796
797impl PercentChangeConditionBuilder {
798    /// Create a new percent change condition builder.
799    ///
800    /// # Parameters
801    ///
802    /// - `contract_id`: Contract identifier for the instrument to monitor
803    /// - `exchange`: Exchange where price change is monitored
804    pub fn new(contract_id: i32, exchange: impl Into<String>) -> Self {
805        Self {
806            contract_id,
807            exchange: exchange.into(),
808            percent: None,        // Must be set by greater_than/less_than
809            is_more: true,        // Default: trigger when above threshold
810            is_conjunction: true, // Default: AND condition
811        }
812    }
813
814    /// Set trigger when percent change is greater than the specified value.
815    pub fn greater_than(mut self, percent: f64) -> Self {
816        self.percent = Some(percent);
817        self.is_more = true;
818        self
819    }
820
821    /// Set trigger when percent change is less than the specified value.
822    pub fn less_than(mut self, percent: f64) -> Self {
823        self.percent = Some(percent);
824        self.is_more = false;
825        self
826    }
827
828    /// Set whether this is an AND (conjunction) or OR (disjunction) condition.
829    ///
830    /// Default is `true` (AND).
831    pub fn conjunction(mut self, is_conjunction: bool) -> Self {
832        self.is_conjunction = is_conjunction;
833        self
834    }
835
836    /// Build the percent change condition.
837    pub fn build(self) -> PercentChangeCondition {
838        PercentChangeCondition {
839            contract_id: self.contract_id,
840            exchange: self.exchange,
841            percent: self
842                .percent
843                .expect("PercentChangeConditionBuilder requires a threshold; call greater_than() or less_than() before build()"),
844            is_more: self.is_more,
845            is_conjunction: self.is_conjunction,
846        }
847    }
848}
849
850// From implementations to convert builders to OrderCondition
851impl From<PriceConditionBuilder> for crate::orders::OrderCondition {
852    fn from(builder: PriceConditionBuilder) -> Self {
853        crate::orders::OrderCondition::Price(builder.build())
854    }
855}
856
857impl From<TimeConditionBuilder> for crate::orders::OrderCondition {
858    fn from(builder: TimeConditionBuilder) -> Self {
859        crate::orders::OrderCondition::Time(builder.build())
860    }
861}
862
863impl From<MarginConditionBuilder> for crate::orders::OrderCondition {
864    fn from(builder: MarginConditionBuilder) -> Self {
865        crate::orders::OrderCondition::Margin(builder.build())
866    }
867}
868
869impl From<VolumeConditionBuilder> for crate::orders::OrderCondition {
870    fn from(builder: VolumeConditionBuilder) -> Self {
871        crate::orders::OrderCondition::Volume(builder.build())
872    }
873}
874
875impl From<PercentChangeConditionBuilder> for crate::orders::OrderCondition {
876    fn from(builder: PercentChangeConditionBuilder) -> Self {
877        crate::orders::OrderCondition::PercentChange(builder.build())
878    }
879}
880
881impl From<ExecutionConditionBuilder> for crate::orders::OrderCondition {
882    fn from(builder: ExecutionConditionBuilder) -> Self {
883        crate::orders::OrderCondition::Execution(builder.build())
884    }
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    #[test]
892    fn test_price_condition_builder() {
893        let condition = PriceCondition::builder(12345, "NASDAQ")
894            .greater_than(150.0)
895            .trigger_method(TriggerMethod::DoubleBidAsk)
896            .conjunction(false)
897            .build();
898
899        assert_eq!(condition.contract_id, 12345);
900        assert_eq!(condition.exchange, "NASDAQ");
901        assert_eq!(condition.price, 150.0);
902        assert_eq!(condition.trigger_method, TriggerMethod::DoubleBidAsk);
903        assert!(condition.is_more);
904        assert!(!condition.is_conjunction);
905    }
906
907    #[test]
908    fn test_time_condition_builder() {
909        let condition = TimeCondition::builder().less_than("20251230 23:59:59 UTC").build();
910
911        assert_eq!(condition.time, "20251230 23:59:59 UTC");
912        assert!(!condition.is_more);
913        assert!(condition.is_conjunction);
914    }
915
916    #[test]
917    fn test_margin_condition_builder() {
918        let condition = MarginCondition::builder().less_than(30).conjunction(false).build();
919
920        assert_eq!(condition.percent, 30);
921        assert!(!condition.is_more);
922        assert!(!condition.is_conjunction);
923    }
924
925    #[test]
926    fn test_execution_condition_builder() {
927        let condition = ExecutionCondition::builder("AAPL", "STK", "SMART").conjunction(false).build();
928
929        assert_eq!(condition.symbol, "AAPL");
930        assert_eq!(condition.security_type, "STK");
931        assert_eq!(condition.exchange, "SMART");
932        assert!(!condition.is_conjunction);
933    }
934
935    #[test]
936    fn test_volume_condition_builder() {
937        let condition = VolumeCondition::builder(12345, "NASDAQ").less_than(1000000).build();
938
939        assert_eq!(condition.contract_id, 12345);
940        assert_eq!(condition.exchange, "NASDAQ");
941        assert_eq!(condition.volume, 1000000);
942        assert!(!condition.is_more);
943        assert!(condition.is_conjunction);
944    }
945
946    #[test]
947    fn test_percent_change_condition_builder() {
948        let condition = PercentChangeCondition::builder(12345, "NASDAQ")
949            .greater_than(5.0)
950            .conjunction(false)
951            .build();
952
953        assert_eq!(condition.contract_id, 12345);
954        assert_eq!(condition.exchange, "NASDAQ");
955        assert_eq!(condition.percent, 5.0);
956        assert!(condition.is_more);
957        assert!(!condition.is_conjunction);
958    }
959
960    #[test]
961    fn test_default_values() {
962        let condition = PriceCondition::builder(12345, "NASDAQ").greater_than(150.0).build();
963
964        assert_eq!(condition.trigger_method, TriggerMethod::Default);
965        assert!(condition.is_more);
966        assert!(condition.is_conjunction);
967    }
968
969    #[test]
970    #[should_panic(expected = "PriceConditionBuilder requires a price threshold")]
971    fn test_price_condition_builder_missing_threshold_panics() {
972        let _ = PriceCondition::builder(12345, "NASDAQ").build();
973    }
974
975    #[test]
976    #[should_panic(expected = "TimeConditionBuilder requires a time value")]
977    fn test_time_condition_builder_missing_time_panics() {
978        let _ = TimeCondition::builder().build();
979    }
980
981    #[test]
982    #[should_panic(expected = "MarginConditionBuilder requires a percentage threshold")]
983    fn test_margin_condition_builder_missing_threshold_panics() {
984        let _ = MarginCondition::builder().build();
985    }
986
987    #[test]
988    #[should_panic(expected = "VolumeConditionBuilder requires a volume threshold")]
989    fn test_volume_condition_builder_missing_threshold_panics() {
990        let _ = VolumeCondition::builder(12345, "NASDAQ").build();
991    }
992
993    #[test]
994    #[should_panic(expected = "PercentChangeConditionBuilder requires a threshold")]
995    fn test_percent_change_condition_builder_missing_threshold_panics() {
996        let _ = PercentChangeCondition::builder(12345, "NASDAQ").build();
997    }
998}