Skip to main content

nautilus_interactive_brokers/execution/
conditions.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Order conditions implementation for Interactive Brokers conditional orders.
17
18use std::str::FromStr;
19
20use anyhow::Context;
21use ibapi::orders::{
22    OrderCondition,
23    conditions::{
24        ExecutionCondition, MarginCondition, PercentChangeCondition, PriceCondition, TimeCondition,
25        VolumeCondition,
26    },
27};
28use serde_json::Value;
29
30use crate::common::enums::{
31    IbConditionConjunction, IbConditionKind, IbSecurityType, IbTriggerMethod,
32};
33
34/// Create IB order conditions from a list of condition dictionaries.
35///
36/// # Errors
37///
38/// Returns an error if conditions_data is not an array or if any condition is invalid.
39pub fn create_ib_conditions(conditions_data: &Value) -> anyhow::Result<Vec<OrderCondition>> {
40    let conditions_array = conditions_data
41        .as_array()
42        .context("Conditions must be an array")?;
43
44    let mut conditions = Vec::new();
45
46    for condition_dict in conditions_array {
47        let condition_type = condition_dict
48            .get("type")
49            .and_then(|v| v.as_str())
50            .context("Missing condition type")?;
51        let condition_kind = match IbConditionKind::from_str(condition_type) {
52            Ok(condition_kind) => condition_kind,
53            Err(_) => {
54                tracing::warn!("Unknown IB condition type: {condition_type}, skipping condition");
55                continue;
56            }
57        };
58
59        // Get conjunction (default to "and" = true)
60        let conjunction = condition_dict
61            .get("conjunction")
62            .and_then(|v| v.as_str())
63            .map(IbConditionConjunction::from_str)
64            .transpose()?
65            .unwrap_or(IbConditionConjunction::And);
66        let is_conjunction = conjunction.is_conjunction();
67
68        let condition = match condition_kind {
69            IbConditionKind::Price => {
70                let con_id = condition_dict
71                    .get("conId")
72                    .and_then(|v| v.as_i64())
73                    .unwrap_or(0) as i32;
74                let exchange = condition_dict
75                    .get("exchange")
76                    .and_then(|v| v.as_str())
77                    .unwrap_or("SMART");
78                let price = condition_dict
79                    .get("price")
80                    .and_then(|v| v.as_f64())
81                    .unwrap_or(0.0);
82                let is_more = condition_dict
83                    .get("isMore")
84                    .and_then(|v| v.as_bool())
85                    .unwrap_or(true);
86                let trigger_method = condition_dict
87                    .get("triggerMethod")
88                    .and_then(|v| v.as_i64())
89                    .unwrap_or(0) as i32;
90
91                let mut builder = PriceCondition::builder(con_id, exchange);
92
93                if !is_more {
94                    builder = builder.less_than(price);
95                } else {
96                    builder = builder.greater_than(price);
97                }
98                builder = builder
99                    .trigger_method(IbTriggerMethod::from(trigger_method).ibapi_trigger_method());
100                builder = builder.conjunction(is_conjunction);
101                OrderCondition::Price(builder.build())
102            }
103            IbConditionKind::Time => {
104                let time = condition_dict
105                    .get("time")
106                    .and_then(|v| v.as_str())
107                    .unwrap_or("");
108                let is_more = condition_dict
109                    .get("isMore")
110                    .and_then(|v| v.as_bool())
111                    .unwrap_or(true);
112
113                let mut builder = TimeCondition::builder();
114
115                if !is_more {
116                    builder = builder.less_than(time);
117                } else {
118                    builder = builder.greater_than(time);
119                }
120                builder = builder.conjunction(is_conjunction);
121                OrderCondition::Time(builder.build())
122            }
123            IbConditionKind::Margin => {
124                let percent = condition_dict
125                    .get("percent")
126                    .and_then(|v| v.as_i64())
127                    .unwrap_or(0) as i32;
128                let is_more = condition_dict
129                    .get("isMore")
130                    .and_then(|v| v.as_bool())
131                    .unwrap_or(true);
132
133                let mut builder = MarginCondition::builder();
134
135                if !is_more {
136                    builder = builder.less_than(percent);
137                } else {
138                    builder = builder.greater_than(percent);
139                }
140                builder = builder.conjunction(is_conjunction);
141                OrderCondition::Margin(builder.build())
142            }
143            IbConditionKind::Execution => {
144                let symbol = condition_dict
145                    .get("symbol")
146                    .and_then(|v| v.as_str())
147                    .context("Missing symbol for execution condition")?;
148                let sec_type = condition_dict
149                    .get("secType")
150                    .and_then(|v| v.as_str())
151                    .unwrap_or("STK");
152                let sec_type = IbSecurityType::from_str(sec_type)
153                    .map_or_else(|_| sec_type.to_string(), |sec_type| sec_type.to_string());
154                let exchange = condition_dict
155                    .get("exchange")
156                    .and_then(|v| v.as_str())
157                    .unwrap_or("SMART");
158
159                let mut builder = ExecutionCondition::builder(symbol, sec_type.as_str(), exchange);
160                builder = builder.conjunction(is_conjunction);
161                OrderCondition::Execution(builder.build())
162            }
163            IbConditionKind::Volume => {
164                let con_id = condition_dict
165                    .get("conId")
166                    .and_then(|v| v.as_i64())
167                    .unwrap_or(0) as i32;
168                let exchange = condition_dict
169                    .get("exchange")
170                    .and_then(|v| v.as_str())
171                    .unwrap_or("SMART");
172                let volume = condition_dict
173                    .get("volume")
174                    .and_then(|v| v.as_i64())
175                    .unwrap_or(0) as i32;
176                let is_more = condition_dict
177                    .get("isMore")
178                    .and_then(|v| v.as_bool())
179                    .unwrap_or(true);
180
181                let mut builder = VolumeCondition::builder(con_id, exchange);
182
183                if !is_more {
184                    builder = builder.less_than(volume);
185                } else {
186                    builder = builder.greater_than(volume);
187                }
188                builder = builder.conjunction(is_conjunction);
189                OrderCondition::Volume(builder.build())
190            }
191            IbConditionKind::PercentChange => {
192                let con_id = condition_dict
193                    .get("conId")
194                    .and_then(|v| v.as_i64())
195                    .unwrap_or(0) as i32;
196                let exchange = condition_dict
197                    .get("exchange")
198                    .and_then(|v| v.as_str())
199                    .unwrap_or("SMART");
200                let change_percent = condition_dict
201                    .get("changePercent")
202                    .and_then(|v| v.as_f64())
203                    .unwrap_or(0.0);
204                let is_more = condition_dict
205                    .get("isMore")
206                    .and_then(|v| v.as_bool())
207                    .unwrap_or(true);
208
209                let mut builder = PercentChangeCondition::builder(con_id, exchange);
210
211                if !is_more {
212                    builder = builder.less_than(change_percent);
213                } else {
214                    builder = builder.greater_than(change_percent);
215                }
216                builder = builder.conjunction(is_conjunction);
217                OrderCondition::PercentChange(builder.build())
218            }
219        };
220
221        conditions.push(condition);
222    }
223
224    Ok(conditions)
225}
226
227#[cfg(test)]
228mod tests {
229    use ibapi::orders::OrderCondition;
230    use rstest::rstest;
231
232    use super::*;
233
234    #[rstest]
235    fn test_create_conditions_from_json() {
236        let conditions_json = serde_json::json!([
237            {
238                "type": "price",
239                "conId": 265598,
240                "exchange": "SMART",
241                "isMore": true,
242                "price": 250.0,
243                "triggerMethod": 0,
244                "conjunction": "and",
245            },
246            {
247                "type": "time",
248                "time": "20250315-09:30:00",
249                "isMore": true,
250                "conjunction": "or",
251            },
252        ]);
253
254        let conditions = create_ib_conditions(&conditions_json).unwrap();
255        assert_eq!(conditions.len(), 2);
256        assert_eq!(conditions[0].condition_type(), 1); // Price condition type
257        assert_eq!(conditions[1].condition_type(), 3); // Time condition type
258        assert!(conditions[0].is_conjunction()); // "and"
259        assert!(!conditions[1].is_conjunction()); // "or"
260    }
261
262    #[rstest]
263    fn test_create_execution_condition_from_json() {
264        let conditions_json = serde_json::json!([
265            {
266                "type": "execution",
267                "symbol": "MSFT",
268                "secType": "STK",
269                "exchange": "SMART",
270                "conjunction": "or",
271            }
272        ]);
273
274        let conditions = create_ib_conditions(&conditions_json).unwrap();
275        assert_eq!(conditions.len(), 1);
276
277        match &conditions[0] {
278            OrderCondition::Execution(condition) => {
279                assert_eq!(condition.symbol, "MSFT");
280                assert_eq!(condition.security_type, "STK");
281                assert_eq!(condition.exchange, "SMART");
282                assert!(!condition.is_conjunction);
283            }
284            other => panic!("unexpected condition: {other:?}"),
285        }
286    }
287
288    #[rstest]
289    fn test_create_percent_change_condition_from_json() {
290        let conditions_json = serde_json::json!([
291            {
292                "type": "percent_change",
293                "conId": 123,
294                "exchange": "NASDAQ",
295                "changePercent": 2.5,
296                "isMore": false,
297                "conjunction": "and",
298            }
299        ]);
300
301        let conditions = create_ib_conditions(&conditions_json).unwrap();
302        assert_eq!(conditions.len(), 1);
303
304        match &conditions[0] {
305            OrderCondition::PercentChange(condition) => {
306                assert_eq!(condition.contract_id, 123);
307                assert_eq!(condition.exchange, "NASDAQ");
308                assert_eq!(condition.percent, 2.5);
309                assert!(!condition.is_more);
310                assert!(condition.is_conjunction);
311            }
312            other => panic!("unexpected condition: {other:?}"),
313        }
314    }
315
316    #[rstest]
317    fn test_create_conditions_skips_unknown_type() {
318        let conditions_json = serde_json::json!([
319            {
320                "type": "unknown",
321            },
322            {
323                "type": "margin",
324                "percent": 25,
325                "isMore": true,
326            }
327        ]);
328
329        let conditions = create_ib_conditions(&conditions_json).unwrap();
330        assert_eq!(conditions.len(), 1);
331        assert_eq!(conditions[0].condition_type(), 4);
332    }
333
334    #[rstest]
335    fn test_create_conditions_rejects_non_array_json() {
336        let conditions_json = serde_json::json!({
337            "type": "price",
338            "price": 123.0,
339        });
340
341        let result = create_ib_conditions(&conditions_json);
342        assert!(result.is_err());
343        assert_eq!(
344            result.unwrap_err().to_string(),
345            "Conditions must be an array"
346        );
347    }
348}