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