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}