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 = 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 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); assert_eq!(conditions[1].condition_type(), 3); assert!(conditions[0].is_conjunction()); assert!(!conditions[1].is_conjunction()); }
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}