Skip to main content

ctrader_rs/
order.rs

1///
2///
3///
4///
5///
6///
7///
8///
9///
10///
11///
12///
13///
14///
15use prost::Message;
16use tokio::sync::oneshot;
17
18use crate::payload;
19use crate::proto::common::*;
20use crate::{client::Client, error::Error};
21
22// ── Internal helper ───────────────────────────────────────────────────────────
23
24impl Client {
25    /// Send a `ProtoOANewOrderReq` without a `clientMsgId` and await the
26    ///
27    ///
28    ///
29    ///
30    ///
31    ///
32    ///
33    ///
34    /// resulting `ProtoOAExecutionEvent` (or `ProtoOAOrderErrorEvent`) via the
35    ///
36    ///
37    ///
38    /// pending-orders queue.
39    async fn send_order(&self, req: ProtoOaNewOrderReq) -> Result<ProtoOaExecutionEvent, Error> {
40        // Register waiter BEFORE sending to avoid a race
41        let (_tx, rx) = oneshot::channel::<Result<ProtoOaExecutionEvent, crate::error::Error>>();
42        // self.pending_orders.lock().await.push(tx);
43
44        // Encode inner message
45        let mut payload_bytes = Vec::new();
46        req.encode(&mut payload_bytes)?;
47
48        // Envelope — intentionally NO clientMsgId so the server sends an
49        // unsolicited ExecutionEvent back
50        let envelope = ProtoMessage {
51            payload_type: payload::OA_NEW_ORDER_REQ as u32,
52            payload: Some(payload_bytes),
53            client_msg_id: None,
54        };
55
56        let mut frame = Vec::new();
57        envelope.encode(&mut frame)?;
58        self.transport.send(&frame).await?;
59
60        tokio::time::timeout(self.config.deadline, rx)
61            .await
62            .map_err(|_| Error::Timeout)?
63            .map_err(|_| Error::Disconnected)?
64    }
65
66    // ── Market orders ─────────────────────────────────────────────────────────
67
68    /// Place a market order (immediate fill at best available price).
69    ///
70    ///
71    ///
72    ///
73    ///
74    ///
75    ///
76    ///
77    ///
78    ///
79    ///
80    ///
81    /// `volume` is in units of 0.01 lots: 100 = 0.01 lot, 100_000 = 1 lot.
82    pub async fn new_market_order(
83        &self,
84        ctid_trader_account_id: i64,
85        symbol_id: i64,
86        trade_side: ProtoOaTradeSide,
87        volume: i64,
88    ) -> Result<ProtoOaExecutionEvent, Error> {
89        self.send_order(ProtoOaNewOrderReq {
90            ctid_trader_account_id,
91            symbol_id,
92            trade_side: trade_side as i32,
93            volume,
94            order_type: ProtoOaOrderType::Market as i32,
95            payload_type: Some(payload::OA_NEW_ORDER_REQ as i32),
96            ..Default::default()
97        })
98        .await
99    }
100
101    /// Place a market order with relative SL/TP (distance from fill price).
102    ///
103    ///
104    ///
105    ///
106    /// `relative_stop_loss` and `relative_take_profit` are in 1/100000 of a
107    /// price unit.  1 pip on a 5-decimal pair = 10 points = 1000 in protocol.
108    ///
109    /// For a BUY:   SL = fillPrice − relSL,  TP = fillPrice + relTP
110    /// For a SELL:  SL = fillPrice + relSL,  TP = fillPrice − relTP
111    ///
112    ///
113    ///
114    ///
115    /// Helper: `pips_to_points(n)` converts pip count → protocol value.
116    pub async fn new_market_order_with_sltp(
117        &self,
118        ctid_trader_account_id: i64,
119        symbol_id: i64,
120        trade_side: ProtoOaTradeSide,
121        volume: i64,
122        relative_stop_loss: i64,
123        relative_take_profit: i64,
124    ) -> Result<ProtoOaExecutionEvent, Error> {
125        self.send_order(ProtoOaNewOrderReq {
126            ctid_trader_account_id,
127            symbol_id,
128            trade_side: trade_side as i32,
129            volume,
130            order_type: ProtoOaOrderType::Market as i32,
131            relative_stop_loss: Some(relative_stop_loss),
132            relative_take_profit: Some(relative_take_profit),
133            payload_type: Some(payload::OA_NEW_ORDER_REQ as i32),
134            ..Default::default()
135        })
136        .await
137    }
138
139    /// Market order with trailing stop loss (relative distance).
140    ///
141    ///
142    ///
143    ///
144    ///
145    ///
146    ///
147    ///
148    ///
149    ///
150    ///
151    pub async fn new_market_order_with_trailing_sl(
152        &self,
153        ctid_trader_account_id: i64,
154        symbol_id: i64,
155        trade_side: ProtoOaTradeSide,
156        volume: i64,
157        relative_stop_loss: i64,
158    ) -> Result<ProtoOaExecutionEvent, Error> {
159        self.send_order(ProtoOaNewOrderReq {
160            ctid_trader_account_id,
161            symbol_id,
162            trade_side: trade_side as i32,
163            volume,
164            order_type: ProtoOaOrderType::Market as i32,
165            relative_stop_loss: Some(relative_stop_loss),
166            trailing_stop_loss: Some(true),
167            payload_type: Some(payload::OA_NEW_ORDER_REQ as i32),
168            ..Default::default()
169        })
170        .await
171    }
172
173    /// Market range order — fills within a slippage band around `base_price`.
174    ///
175    ///
176    ///
177    ///
178    ///
179    ///
180    ///
181    ///
182    ///
183    ///
184    /// `slippage_in_points` is the maximum allowed deviation in points.
185    pub async fn new_market_range_order(
186        &self,
187        ctid_trader_account_id: i64,
188        symbol_id: i64,
189        trade_side: ProtoOaTradeSide,
190        volume: i64,
191        base_slippage_price: f64,
192        slippage_in_points: i32,
193    ) -> Result<ProtoOaExecutionEvent, Error> {
194        self.send_order(ProtoOaNewOrderReq {
195            volume,
196            symbol_id,
197            ctid_trader_account_id,
198            trade_side: trade_side as i32,
199            slippage_in_points: Some(slippage_in_points),
200            base_slippage_price: Some(base_slippage_price),
201            order_type: ProtoOaOrderType::MarketRange as i32,
202            payload_type: Some(payload::OA_NEW_ORDER_REQ as i32),
203            ..Default::default()
204        })
205        .await
206    }
207
208    // ── Pending orders ────────────────────────────────────────────────────────
209
210    /// Place a limit order (fill when price reaches `limit_price` or better).
211    ///
212    ///
213    ///
214    ///
215    ///
216    ///
217    ///
218    ///
219    ///
220    /// `stop_loss` and `take_profit` are absolute price levels.
221    /// Leave as `None` to omit them.
222    pub async fn new_limit_order(
223        &self,
224        ctid_trader_account_id: i64,
225        symbol_id: i64,
226        trade_side: ProtoOaTradeSide,
227        volume: i64,
228        limit_price: f64,
229        stop_loss: Option<f64>,
230        take_profit: Option<f64>,
231    ) -> Result<ProtoOaExecutionEvent, Error> {
232        self.send_order(ProtoOaNewOrderReq {
233            volume,
234            symbol_id,
235            stop_loss,
236            take_profit,
237            ctid_trader_account_id,
238            limit_price: Some(limit_price),
239            trade_side: trade_side as i32,
240            order_type: ProtoOaOrderType::Limit as i32,
241            payload_type: Some(payload::OA_NEW_ORDER_REQ as i32),
242            ..Default::default()
243        })
244        .await
245    }
246
247    /// Place a stop order (fill when price breaks through `stop_price`).
248    ///
249    ///
250    ///
251    ///
252    ///
253    ///
254    ///
255    ///
256    ///
257    ///
258    ///
259    ///
260    /// `stop_loss` and `take_profit` are absolute price levels.
261    pub async fn new_stop_order(
262        &self,
263        ctid_trader_account_id: i64,
264        symbol_id: i64,
265        trade_side: ProtoOaTradeSide,
266        volume: i64,
267        stop_price: f64,
268        stop_loss: Option<f64>,
269        take_profit: Option<f64>,
270    ) -> Result<ProtoOaExecutionEvent, Error> {
271        self.send_order(ProtoOaNewOrderReq {
272            volume,
273            symbol_id,
274            stop_loss,
275            take_profit,
276            ctid_trader_account_id,
277            stop_price: Some(stop_price),
278            trade_side: trade_side as i32,
279            order_type: ProtoOaOrderType::Stop as i32,
280            payload_type: Some(payload::OA_NEW_ORDER_REQ as i32),
281            ..Default::default()
282        })
283        .await
284    }
285
286    /// Place a stop-limit order (pending stop that becomes a limit on trigger).
287    ///
288    ///
289    ///
290    ///
291    ///
292    ///
293    ///
294    ///
295    ///
296    ///
297    /// `stop_price`       — price level that triggers the order.
298    /// `slippage_in_points` — max deviation from stop price for the limit fill.
299    pub async fn new_stop_limit_order(
300        &self,
301        ctid_trader_account_id: i64,
302        symbol_id: i64,
303        trade_side: ProtoOaTradeSide,
304        volume: i64,
305        stop_price: f64,
306        slippage_in_points: i32,
307        stop_loss: Option<f64>,
308        take_profit: Option<f64>,
309    ) -> Result<ProtoOaExecutionEvent, Error> {
310        self.send_order(ProtoOaNewOrderReq {
311            volume,
312            symbol_id,
313            stop_loss,
314            take_profit,
315            ctid_trader_account_id,
316            stop_price: Some(stop_price),
317            trade_side: trade_side as i32,
318            slippage_in_points: Some(slippage_in_points),
319            order_type: ProtoOaOrderType::StopLimit as i32,
320            payload_type: Some(payload::OA_NEW_ORDER_REQ as i32),
321            ..Default::default()
322        })
323        .await
324    }
325
326    // ── Order management ──────────────────────────────────────────────────────
327
328    /// Cancel an existing pending order.
329    ///
330    ///
331    ///
332    ///
333    ///
334    ///
335    ///
336    ///
337    ///
338    ///
339    ///
340    ///
341    ///
342    ///
343    pub async fn cancel_order(
344        &self,
345        ctid_trader_account_id: i64,
346        order_id: i64,
347    ) -> Result<ProtoOaExecutionEvent, Error> {
348        let req = ProtoOaCancelOrderReq {
349            order_id,
350            ctid_trader_account_id,
351            payload_type: Some(payload::OA_CANCEL_ORDER_REQ as i32),
352        };
353
354        self.command(
355            payload::OA_CANCEL_ORDER_REQ,
356            req,
357            payload::OA_EXECUTION_EVENT,
358        )
359        .await
360    }
361
362    /// Amend an existing pending order.
363    ///
364    ///
365    ///
366    ///
367    ///
368    ///
369    ///
370    ///
371    ///
372    ///
373    ///
374    ///
375    ///
376    ///
377    ///
378    /// Only supply the fields you want to change — all are optional except
379    /// `ctid_trader_account_id` and `order_id`.
380    pub async fn amend_order(
381        &self,
382        ctid_trader_account_id: i64,
383        order_id: i64,
384        volume: Option<i64>,
385        limit_price: Option<f64>,
386        stop_price: Option<f64>,
387        stop_loss: Option<f64>,
388        take_profit: Option<f64>,
389        trailing_stop_loss: Option<bool>,
390        expiration_timestamp: Option<i64>,
391    ) -> Result<ProtoOaExecutionEvent, Error> {
392        let req = ProtoOaAmendOrderReq {
393            order_id,
394            volume,
395            limit_price,
396            stop_price,
397            stop_loss,
398            take_profit,
399            trailing_stop_loss,
400            expiration_timestamp,
401            ctid_trader_account_id,
402            payload_type: Some(payload::OA_AMEND_ORDER_REQ as i32),
403            ..Default::default()
404        };
405        self.command(
406            payload::OA_AMEND_ORDER_REQ,
407            req,
408            payload::OA_EXECUTION_EVENT,
409        )
410        .await
411    }
412
413    /// Get open positions and pending orders (current state snapshot).
414    ///
415    ///
416    ///
417    ///
418    ///
419    ///
420    ///
421    ///
422    ///
423    ///
424    ///
425    ///
426    /// Set `return_protection_orders = true` to receive SL/TP as separate
427    /// orders; otherwise they appear as fields on the position.
428    pub async fn reconcile(
429        &self,
430        ctid_trader_account_id: i64,
431        return_protection_orders: bool,
432    ) -> Result<ProtoOaReconcileRes, Error> {
433        let req = ProtoOaReconcileReq {
434            ctid_trader_account_id,
435            payload_type: Some(payload::OA_RECONCILE_REQ as i32),
436            return_protection_orders: Some(return_protection_orders),
437        };
438        self.command(payload::OA_RECONCILE_REQ, req, payload::OA_RECONCILE_RES)
439            .await
440    }
441
442    /// List orders filtered by time range.
443    ///
444    ///
445    ///
446    ///
447    ///
448    ///
449    ///
450    ///
451    ///
452    ///
453    ///
454    ///
455    ///
456    pub async fn order_list(
457        &self,
458        ctid_trader_account_id: i64,
459        from_timestamp: Option<i64>,
460        to_timestamp: Option<i64>,
461    ) -> Result<ProtoOaOrderListRes, Error> {
462        let req = ProtoOaOrderListReq {
463            to_timestamp,
464            from_timestamp,
465            ctid_trader_account_id,
466            payload_type: Some(payload::OA_ORDER_LIST_REQ as i32),
467        };
468        self.command(payload::OA_ORDER_LIST_REQ, req, payload::OA_ORDER_LIST_RES)
469            .await
470    }
471
472    /// Get full details for a single order by ID.
473    ///
474    ///
475    ///
476    ///
477    ///
478    ///
479    ///
480    ///
481    ///
482    ///
483    ///
484    ///
485    ///
486    ///
487    pub async fn order_details(
488        &self,
489        ctid_trader_account_id: i64,
490        order_id: i64,
491    ) -> Result<ProtoOaOrderDetailsRes, Error> {
492        let req = ProtoOaOrderDetailsReq {
493            order_id,
494            ctid_trader_account_id,
495            payload_type: Some(payload::OA_ORDER_DETAILS_REQ as i32),
496        };
497        self.command(
498            payload::OA_ORDER_DETAILS_REQ,
499            req,
500            payload::OA_ORDER_DETAILS_RES,
501        )
502        .await
503    }
504
505    /// List orders associated with a specific position.
506    ///
507    ///
508    ///
509    ///
510    ///
511    ///
512    ///
513    ///
514    ///
515    ///
516    ///
517    ///
518    ///
519    ///
520    ///
521    pub async fn get_order_list_by_position(
522        &self,
523        ctid_trader_account_id: i64,
524        position_id: i64,
525        from_timestamp: Option<i64>,
526        to_timestamp: Option<i64>,
527    ) -> Result<ProtoOaOrderListByPositionIdRes, Error> {
528        let req = ProtoOaOrderListByPositionIdReq {
529            position_id,
530            to_timestamp,
531            from_timestamp,
532            ctid_trader_account_id,
533            payload_type: Some(payload::OA_ORDER_LIST_BY_POSITION_ID_REQ as i32),
534        };
535        self.command(
536            payload::OA_ORDER_LIST_BY_POSITION_ID_REQ,
537            req,
538            payload::OA_ORDER_LIST_BY_POSITION_ID_RES,
539        )
540        .await
541    }
542}
543
544#[cfg(test)]
545mod tests {
546
547    #[tokio::test]
548    async fn test() {}
549}