nautilus_interactive_brokers/execution/
conditions.rs1use 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
28pub 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 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); assert_eq!(conditions[1].condition_type(), 3); assert!(conditions[0].is_conjunction()); assert!(!conditions[1].is_conjunction()); }
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}