//! Order request builders for the cTrader Open API. //! //! Provides helper functions to construct `ProtoMessage` envelopes //! for new order, cancel, amend, and close position requests. use prost::Message; use crate::proto::{self, ProtoMessage, ProtoOaTradeSide}; /// Build a new order request envelope. #[allow(clippy::too_many_arguments)] pub fn new_order( account_id: i64, symbol_id: i64, side: ProtoOaTradeSide, volume: i64, order_type: proto::ProtoOaOrderType, limit_price: Option, stop_price: Option, stop_loss: Option, take_profit: Option, comment: Option, ) -> ProtoMessage { let req = proto::ProtoOaNewOrderReq { payload_type: Some(proto::PT_NEW_ORDER_REQ as i32), ctid_trader_account_id: account_id, symbol_id, order_type: order_type as i32, trade_side: side as i32, volume, limit_price, stop_price, time_in_force: None, expiration_timestamp: None, stop_loss, take_profit, comment, base_slippage_price: None, slippage_in_points: None, label: None, position_id: None, client_order_id: None, relative_stop_loss: None, relative_take_profit: None, guaranteed_stop_loss: None, trailing_stop_loss: None, stop_trigger_method: None, }; ProtoMessage { payload_type: proto::PT_NEW_ORDER_REQ, payload: Some(req.encode_to_vec()), client_msg_id: None, } } /// Build a cancel order request envelope. pub fn cancel_order(account_id: i64, order_id: i64) -> ProtoMessage { let req = proto::ProtoOaCancelOrderReq { payload_type: Some(proto::PT_CANCEL_ORDER_REQ as i32), ctid_trader_account_id: account_id, order_id, }; ProtoMessage { payload_type: proto::PT_CANCEL_ORDER_REQ, payload: Some(req.encode_to_vec()), client_msg_id: None, } } /// Build an amend order request envelope. #[allow(clippy::too_many_arguments)] pub fn amend_order( account_id: i64, order_id: i64, volume: Option, limit_price: Option, stop_price: Option, stop_loss: Option, take_profit: Option, ) -> ProtoMessage { let req = proto::ProtoOaAmendOrderReq { payload_type: Some(proto::PT_AMEND_ORDER_REQ as i32), ctid_trader_account_id: account_id, order_id, volume, limit_price, stop_price, expiration_timestamp: None, stop_loss, take_profit, slippage_in_points: None, relative_stop_loss: None, relative_take_profit: None, guaranteed_stop_loss: None, trailing_stop_loss: None, stop_trigger_method: None, }; ProtoMessage { payload_type: proto::PT_AMEND_ORDER_REQ, payload: Some(req.encode_to_vec()), client_msg_id: None, } } /// Build a close position request envelope. pub fn close_position(account_id: i64, position_id: i64, volume: i64) -> ProtoMessage { let req = proto::ProtoOaClosePositionReq { payload_type: Some(proto::PT_CLOSE_POSITION_REQ as i32), ctid_trader_account_id: account_id, position_id, volume, }; ProtoMessage { payload_type: proto::PT_CLOSE_POSITION_REQ, payload: Some(req.encode_to_vec()), client_msg_id: None, } } // ── Execution event parsing ────────────────────────────────────── /// Parsed execution event info (avoids exposing raw proto types to consumers). #[derive(Debug, Clone)] pub struct ExecutionEventInfo { /// cTrader order ID. pub order_id: i64, /// Symbol ID. pub symbol_id: i64, /// Trade side as proto enum value (1=Buy, 2=Sell). pub trade_side: i32, /// Volume in cTrader units (cents of lot). pub volume: i64, /// Execution price (for filled orders). pub execution_price: Option, /// Executed volume in cents. pub executed_volume: Option, /// Linked position ID (if any). pub position_id: Option, /// Execution type as proto enum value. pub execution_type: i32, /// Open timestamp (Unix ms). pub timestamp: Option, } /// Try to parse an execution event from a raw `ProtoMessage`. /// /// Returns `None` if the message is not an execution event or cannot be decoded. pub fn parse_execution_event(msg: &ProtoMessage) -> Option { if msg.payload_type != proto::PT_EXECUTION_EVENT { return None; } let payload = msg.payload.as_deref()?; let event = proto::ProtoOaExecutionEvent::decode(payload).ok()?; let order = event.order?; Some(ExecutionEventInfo { order_id: order.order_id, symbol_id: order.trade_data.symbol_id, trade_side: order.trade_data.trade_side, volume: order.trade_data.volume, execution_price: order.execution_price, executed_volume: order.executed_volume, position_id: event.position.map(|p| p.position_id), execution_type: event.execution_type, timestamp: order.trade_data.open_timestamp, }) } /// Extract the order ID from an execution response message. pub fn extract_order_id(msg: &ProtoMessage) -> Option { parse_execution_event(msg).map(|e| e.order_id) } #[cfg(test)] mod tests { use super::*; #[test] fn new_order_builds_valid_envelope() { let msg = new_order( 12345, 1, ProtoOaTradeSide::Buy, 100_000, proto::ProtoOaOrderType::Market, None, None, None, None, Some("test order".into()), ); assert_eq!(msg.payload_type, proto::PT_NEW_ORDER_REQ); assert!(msg.payload.is_some()); let decoded = proto::ProtoOaNewOrderReq::decode( msg.payload.as_deref().expect("payload"), ) .expect("decode"); assert_eq!(decoded.ctid_trader_account_id, 12345); assert_eq!(decoded.symbol_id, 1); assert_eq!(decoded.trade_side, ProtoOaTradeSide::Buy as i32); assert_eq!(decoded.volume, 100_000); assert_eq!( decoded.order_type, proto::ProtoOaOrderType::Market as i32 ); assert_eq!(decoded.comment.as_deref(), Some("test order")); } #[test] fn cancel_order_envelope() { let msg = cancel_order(12345, 999); assert_eq!(msg.payload_type, proto::PT_CANCEL_ORDER_REQ); let decoded = proto::ProtoOaCancelOrderReq::decode( msg.payload.as_deref().expect("payload"), ) .expect("decode"); assert_eq!(decoded.ctid_trader_account_id, 12345); assert_eq!(decoded.order_id, 999); } #[test] fn close_position_envelope() { let msg = close_position(12345, 777, 50_000); assert_eq!(msg.payload_type, proto::PT_CLOSE_POSITION_REQ); let decoded = proto::ProtoOaClosePositionReq::decode( msg.payload.as_deref().expect("payload"), ) .expect("decode"); assert_eq!(decoded.ctid_trader_account_id, 12345); assert_eq!(decoded.position_id, 777); assert_eq!(decoded.volume, 50_000); } }