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 = json_str(condition_dict, "type").context("Missing condition type")?;
48
49        let condition_kind = match IbConditionKind::from_str(condition_type) {
50            Ok(condition_kind) => condition_kind,
51            Err(_) => {
52                tracing::warn!("Unknown IB condition type: {condition_type}, skipping condition");
53                continue;
54            }
55        };
56
57        // Get conjunction (default to "and" = true)
58        let conjunction = json_str(condition_dict, "conjunction")
59            .map(IbConditionConjunction::from_str)
60            .transpose()?
61            .unwrap_or(IbConditionConjunction::And);
62        let is_conjunction = conjunction.is_conjunction();
63
64        let condition = match condition_kind {
65            IbConditionKind::Price => {
66                let con_id = json_i32(condition_dict, "conId").unwrap_or(0);
67                let exchange = json_str(condition_dict, "exchange").unwrap_or("SMART");
68                let price = json_f64(condition_dict, "price").unwrap_or(0.0);
69                let is_more = json_bool(condition_dict, "isMore").unwrap_or(true);
70                let trigger_method = json_i32(condition_dict, "triggerMethod").unwrap_or(0);
71
72                let mut builder = PriceCondition::builder(con_id, exchange);
73                if is_more {
74                    builder = builder.greater_than(price);
75                } else {
76                    builder = builder.less_than(price);
77                }
78
79                builder = builder.trigger_method(
80                    IbTriggerMethod::try_from(trigger_method)?.ibapi_trigger_method(),
81                );
82                builder = builder.conjunction(is_conjunction);
83                OrderCondition::Price(builder.build())
84            }
85            IbConditionKind::Time => {
86                let time = json_str(condition_dict, "time").unwrap_or("");
87                let is_more = json_bool(condition_dict, "isMore").unwrap_or(true);
88
89                let mut builder = TimeCondition::builder();
90                if is_more {
91                    builder = builder.greater_than(time);
92                } else {
93                    builder = builder.less_than(time);
94                }
95
96                builder = builder.conjunction(is_conjunction);
97                OrderCondition::Time(builder.build())
98            }
99            IbConditionKind::Margin => {
100                let percent = json_i32(condition_dict, "percent").unwrap_or(0);
101                let is_more = json_bool(condition_dict, "isMore").unwrap_or(true);
102
103                let mut builder = MarginCondition::builder();
104                if is_more {
105                    builder = builder.greater_than(percent);
106                } else {
107                    builder = builder.less_than(percent);
108                }
109
110                builder = builder.conjunction(is_conjunction);
111                OrderCondition::Margin(builder.build())
112            }
113            IbConditionKind::Execution => {
114                let symbol = json_str(condition_dict, "symbol")
115                    .context("Missing symbol for execution condition")?;
116                let sec_type = json_str(condition_dict, "secType").unwrap_or("STK");
117                let sec_type = IbSecurityType::from_str(sec_type)
118                    .map_or_else(|_| sec_type.to_string(), |sec_type| sec_type.to_string());
119                let exchange = json_str(condition_dict, "exchange").unwrap_or("SMART");
120
121                let mut builder = ExecutionCondition::builder(symbol, sec_type.as_str(), exchange);
122                builder = builder.conjunction(is_conjunction);
123                OrderCondition::Execution(builder.build())
124            }
125            IbConditionKind::Volume => {
126                let con_id = json_i32(condition_dict, "conId").unwrap_or(0);
127                let exchange = json_str(condition_dict, "exchange").unwrap_or("SMART");
128                let volume = json_i32(condition_dict, "volume").unwrap_or(0);
129                let is_more = json_bool(condition_dict, "isMore").unwrap_or(true);
130
131                let mut builder = VolumeCondition::builder(con_id, exchange);
132                if is_more {
133                    builder = builder.greater_than(volume);
134                } else {
135                    builder = builder.less_than(volume);
136                }
137
138                builder = builder.conjunction(is_conjunction);
139                OrderCondition::Volume(builder.build())
140            }
141            IbConditionKind::PercentChange => {
142                let con_id = json_i32(condition_dict, "conId").unwrap_or(0);
143                let exchange = json_str(condition_dict, "exchange").unwrap_or("SMART");
144                let change_percent = json_f64(condition_dict, "changePercent").unwrap_or(0.0);
145                let is_more = json_bool(condition_dict, "isMore").unwrap_or(true);
146
147                let mut builder = PercentChangeCondition::builder(con_id, exchange);
148                if is_more {
149                    builder = builder.greater_than(change_percent);
150                } else {
151                    builder = builder.less_than(change_percent);
152                }
153
154                builder = builder.conjunction(is_conjunction);
155                OrderCondition::PercentChange(builder.build())
156            }
157        };
158
159        conditions.push(condition);
160    }
161
162    Ok(conditions)
163}
164
165fn json_str<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
166    value.get(key).and_then(Value::as_str)
167}
168
169fn json_i32(value: &Value, key: &str) -> Option<i32> {
170    value
171        .get(key)
172        .and_then(Value::as_i64)
173        .map(|value| value as i32)
174}
175
176fn json_f64(value: &Value, key: &str) -> Option<f64> {
177    value.get(key).and_then(Value::as_f64)
178}
179
180fn json_bool(value: &Value, key: &str) -> Option<bool> {
181    value.get(key).and_then(Value::as_bool)
182}
183
184#[cfg(test)]
185mod tests {
186    use ibapi::orders::OrderCondition;
187    use rstest::rstest;
188
189    use super::*;
190
191    #[rstest]
192    fn test_create_conditions_from_json() {
193        let conditions_json = serde_json::json!([
194            {
195                "type": "price",
196                "conId": 265598,
197                "exchange": "SMART",
198                "isMore": true,
199                "price": 250.0,
200                "triggerMethod": 0,
201                "conjunction": "and",
202            },
203            {
204                "type": "time",
205                "time": "20250315-09:30:00",
206                "isMore": true,
207                "conjunction": "or",
208            },
209        ]);
210
211        let conditions = create_ib_conditions(&conditions_json).unwrap();
212        assert_eq!(conditions.len(), 2);
213        assert_eq!(conditions[0].condition_type(), 1); // Price condition type
214        assert_eq!(conditions[1].condition_type(), 3); // Time condition type
215        assert!(conditions[0].is_conjunction()); // "and"
216        assert!(!conditions[1].is_conjunction()); // "or"
217    }
218
219    #[rstest]
220    fn test_create_execution_condition_from_json() {
221        let conditions_json = serde_json::json!([
222            {
223                "type": "execution",
224                "symbol": "MSFT",
225                "secType": "STK",
226                "exchange": "SMART",
227                "conjunction": "or",
228            }
229        ]);
230
231        let conditions = create_ib_conditions(&conditions_json).unwrap();
232        assert_eq!(conditions.len(), 1);
233
234        match &conditions[0] {
235            OrderCondition::Execution(condition) => {
236                assert_eq!(condition.symbol, "MSFT");
237                assert_eq!(condition.security_type, "STK");
238                assert_eq!(condition.exchange, "SMART");
239                assert!(!condition.is_conjunction);
240            }
241            other => panic!("unexpected condition: {other:?}"),
242        }
243    }
244
245    #[rstest]
246    fn test_create_percent_change_condition_from_json() {
247        let conditions_json = serde_json::json!([
248            {
249                "type": "percent_change",
250                "conId": 123,
251                "exchange": "NASDAQ",
252                "changePercent": 2.5,
253                "isMore": false,
254                "conjunction": "and",
255            }
256        ]);
257
258        let conditions = create_ib_conditions(&conditions_json).unwrap();
259        assert_eq!(conditions.len(), 1);
260
261        match &conditions[0] {
262            OrderCondition::PercentChange(condition) => {
263                assert_eq!(condition.contract_id, 123);
264                assert_eq!(condition.exchange, "NASDAQ");
265                assert_eq!(condition.percent, 2.5);
266                assert!(!condition.is_more);
267                assert!(condition.is_conjunction);
268            }
269            other => panic!("unexpected condition: {other:?}"),
270        }
271    }
272
273    #[rstest]
274    fn test_create_conditions_skips_unknown_type() {
275        let conditions_json = serde_json::json!([
276            {
277                "type": "unknown",
278            },
279            {
280                "type": "margin",
281                "percent": 25,
282                "isMore": true,
283            }
284        ]);
285
286        let conditions = create_ib_conditions(&conditions_json).unwrap();
287        assert_eq!(conditions.len(), 1);
288        assert_eq!(conditions[0].condition_type(), 4);
289    }
290
291    #[rstest]
292    fn test_create_conditions_rejects_non_array_json() {
293        let conditions_json = serde_json::json!({
294            "type": "price",
295            "price": 123.0,
296        });
297
298        let result = create_ib_conditions(&conditions_json);
299        assert!(result.is_err());
300        assert_eq!(
301            result.unwrap_err().to_string(),
302            "Conditions must be an array"
303        );
304    }
305}