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 anyhow::Context;
19use ibapi::orders::{
20    OrderCondition,
21    conditions::{
22        ExecutionCondition, MarginCondition, PercentChangeCondition, PriceCondition, TimeCondition,
23        TriggerMethod, VolumeCondition,
24    },
25};
26use serde_json::Value;
27
28/// Create IB order conditions from a list of condition dictionaries.
29///
30/// # Arguments
31///
32/// * `conditions_data` - A JSON array of condition dictionaries
33///
34/// # Returns
35///
36/// A vector of OrderCondition enum variants ready to be encoded into the order.
37///
38/// # Errors
39///
40/// Returns an error if conditions_data is not an array or if any condition is invalid.
41pub fn create_ib_conditions(conditions_data: &Value) -> anyhow::Result<Vec<OrderCondition>> {
42    let conditions_array = conditions_data
43        .as_array()
44        .context("Conditions must be an array")?;
45
46    let mut conditions = Vec::new();
47
48    for condition_dict in conditions_array {
49        let condition_type_str = condition_dict
50            .get("type")
51            .and_then(|v| v.as_str())
52            .context("Missing condition type")?;
53
54        // Get conjunction (default to "and" = true)
55        let conjunction_str = condition_dict
56            .get("conjunction")
57            .and_then(|v| v.as_str())
58            .unwrap_or("and");
59        let is_conjunction = conjunction_str.to_lowercase() == "and";
60
61        let condition = match condition_type_str {
62            "price" => {
63                let con_id = condition_dict
64                    .get("conId")
65                    .and_then(|v| v.as_i64())
66                    .unwrap_or(0) as i32;
67                let exchange = condition_dict
68                    .get("exchange")
69                    .and_then(|v| v.as_str())
70                    .unwrap_or("SMART");
71                let price = condition_dict
72                    .get("price")
73                    .and_then(|v| v.as_f64())
74                    .unwrap_or(0.0);
75                let is_more = condition_dict
76                    .get("isMore")
77                    .and_then(|v| v.as_bool())
78                    .unwrap_or(true);
79                let trigger_method = condition_dict
80                    .get("triggerMethod")
81                    .and_then(|v| v.as_i64())
82                    .unwrap_or(0) as i32;
83
84                let mut builder = PriceCondition::builder(con_id, exchange);
85
86                if !is_more {
87                    builder = builder.less_than(price);
88                } else {
89                    builder = builder.greater_than(price);
90                }
91                builder = builder.trigger_method(TriggerMethod::from(trigger_method));
92                builder = builder.conjunction(is_conjunction);
93                OrderCondition::Price(builder.build())
94            }
95            "time" => {
96                let time = condition_dict
97                    .get("time")
98                    .and_then(|v| v.as_str())
99                    .unwrap_or("");
100                let is_more = condition_dict
101                    .get("isMore")
102                    .and_then(|v| v.as_bool())
103                    .unwrap_or(true);
104
105                let mut builder = TimeCondition::builder();
106
107                if !is_more {
108                    builder = builder.less_than(time);
109                } else {
110                    builder = builder.greater_than(time);
111                }
112                builder = builder.conjunction(is_conjunction);
113                OrderCondition::Time(builder.build())
114            }
115            "margin" => {
116                let percent = condition_dict
117                    .get("percent")
118                    .and_then(|v| v.as_i64())
119                    .unwrap_or(0) as i32;
120                let is_more = condition_dict
121                    .get("isMore")
122                    .and_then(|v| v.as_bool())
123                    .unwrap_or(true);
124
125                let mut builder = MarginCondition::builder();
126
127                if !is_more {
128                    builder = builder.less_than(percent);
129                } else {
130                    builder = builder.greater_than(percent);
131                }
132                builder = builder.conjunction(is_conjunction);
133                OrderCondition::Margin(builder.build())
134            }
135            "execution" => {
136                let symbol = condition_dict
137                    .get("symbol")
138                    .and_then(|v| v.as_str())
139                    .context("Missing symbol for execution condition")?;
140                let sec_type = condition_dict
141                    .get("secType")
142                    .and_then(|v| v.as_str())
143                    .unwrap_or("STK");
144                let exchange = condition_dict
145                    .get("exchange")
146                    .and_then(|v| v.as_str())
147                    .unwrap_or("SMART");
148
149                let mut builder = ExecutionCondition::builder(symbol, sec_type, exchange);
150                builder = builder.conjunction(is_conjunction);
151                OrderCondition::Execution(builder.build())
152            }
153            "volume" => {
154                let con_id = condition_dict
155                    .get("conId")
156                    .and_then(|v| v.as_i64())
157                    .unwrap_or(0) as i32;
158                let exchange = condition_dict
159                    .get("exchange")
160                    .and_then(|v| v.as_str())
161                    .unwrap_or("SMART");
162                let volume = condition_dict
163                    .get("volume")
164                    .and_then(|v| v.as_i64())
165                    .unwrap_or(0) as i32;
166                let is_more = condition_dict
167                    .get("isMore")
168                    .and_then(|v| v.as_bool())
169                    .unwrap_or(true);
170
171                let mut builder = VolumeCondition::builder(con_id, exchange);
172
173                if !is_more {
174                    builder = builder.less_than(volume);
175                } else {
176                    builder = builder.greater_than(volume);
177                }
178                builder = builder.conjunction(is_conjunction);
179                OrderCondition::Volume(builder.build())
180            }
181            "percent_change" => {
182                let con_id = condition_dict
183                    .get("conId")
184                    .and_then(|v| v.as_i64())
185                    .unwrap_or(0) as i32;
186                let exchange = condition_dict
187                    .get("exchange")
188                    .and_then(|v| v.as_str())
189                    .unwrap_or("SMART");
190                let change_percent = condition_dict
191                    .get("changePercent")
192                    .and_then(|v| v.as_f64())
193                    .unwrap_or(0.0);
194                let is_more = condition_dict
195                    .get("isMore")
196                    .and_then(|v| v.as_bool())
197                    .unwrap_or(true);
198
199                let mut builder = PercentChangeCondition::builder(con_id, exchange);
200
201                if !is_more {
202                    builder = builder.less_than(change_percent);
203                } else {
204                    builder = builder.greater_than(change_percent);
205                }
206                builder = builder.conjunction(is_conjunction);
207                OrderCondition::PercentChange(builder.build())
208            }
209            _ => {
210                tracing::warn!("Unknown condition type: {}", condition_type_str);
211                continue;
212            }
213        };
214
215        conditions.push(condition);
216    }
217
218    Ok(conditions)
219}
220
221#[cfg(test)]
222mod tests {
223    use ibapi::orders::OrderCondition;
224    use rstest::rstest;
225
226    use super::*;
227
228    #[rstest]
229    fn test_create_conditions_from_json() {
230        let conditions_json = serde_json::json!([
231            {
232                "type": "price",
233                "conId": 265598,
234                "exchange": "SMART",
235                "isMore": true,
236                "price": 250.0,
237                "triggerMethod": 0,
238                "conjunction": "and",
239            },
240            {
241                "type": "time",
242                "time": "20250315-09:30:00",
243                "isMore": true,
244                "conjunction": "or",
245            },
246        ]);
247
248        let conditions = create_ib_conditions(&conditions_json).unwrap();
249        assert_eq!(conditions.len(), 2);
250        assert_eq!(conditions[0].condition_type(), 1); // Price condition type
251        assert_eq!(conditions[1].condition_type(), 3); // Time condition type
252        assert!(conditions[0].is_conjunction()); // "and"
253        assert!(!conditions[1].is_conjunction()); // "or"
254    }
255
256    #[rstest]
257    fn test_create_execution_condition_from_json() {
258        let conditions_json = serde_json::json!([
259            {
260                "type": "execution",
261                "symbol": "MSFT",
262                "secType": "STK",
263                "exchange": "SMART",
264                "conjunction": "or",
265            }
266        ]);
267
268        let conditions = create_ib_conditions(&conditions_json).unwrap();
269        assert_eq!(conditions.len(), 1);
270
271        match &conditions[0] {
272            OrderCondition::Execution(condition) => {
273                assert_eq!(condition.symbol, "MSFT");
274                assert_eq!(condition.security_type, "STK");
275                assert_eq!(condition.exchange, "SMART");
276                assert!(!condition.is_conjunction);
277            }
278            other => panic!("unexpected condition: {other:?}"),
279        }
280    }
281
282    #[rstest]
283    fn test_create_percent_change_condition_from_json() {
284        let conditions_json = serde_json::json!([
285            {
286                "type": "percent_change",
287                "conId": 123,
288                "exchange": "NASDAQ",
289                "changePercent": 2.5,
290                "isMore": false,
291                "conjunction": "and",
292            }
293        ]);
294
295        let conditions = create_ib_conditions(&conditions_json).unwrap();
296        assert_eq!(conditions.len(), 1);
297
298        match &conditions[0] {
299            OrderCondition::PercentChange(condition) => {
300                assert_eq!(condition.contract_id, 123);
301                assert_eq!(condition.exchange, "NASDAQ");
302                assert_eq!(condition.percent, 2.5);
303                assert!(!condition.is_more);
304                assert!(condition.is_conjunction);
305            }
306            other => panic!("unexpected condition: {other:?}"),
307        }
308    }
309
310    #[rstest]
311    fn test_create_conditions_skips_unknown_type() {
312        let conditions_json = serde_json::json!([
313            {
314                "type": "unknown",
315            },
316            {
317                "type": "margin",
318                "percent": 25,
319                "isMore": true,
320            }
321        ]);
322
323        let conditions = create_ib_conditions(&conditions_json).unwrap();
324        assert_eq!(conditions.len(), 1);
325        assert_eq!(conditions[0].condition_type(), 4);
326    }
327
328    #[rstest]
329    fn test_create_conditions_rejects_non_array_json() {
330        let conditions_json = serde_json::json!({
331            "type": "price",
332            "price": 123.0,
333        });
334
335        let result = create_ib_conditions(&conditions_json);
336        assert!(result.is_err());
337        assert_eq!(
338            result.unwrap_err().to_string(),
339            "Conditions must be an array"
340        );
341    }
342}