Replace FIX-protocol-based integration tests with tests using real cTrader types (ICMarketsConfig, TradingOrder, BrokerInterface). All 21 tests pass: broker_failover (5), icmarkets_validation (10), order_lifecycle (6). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
241 lines
7.2 KiB
Rust
241 lines
7.2 KiB
Rust
//! 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<f64>,
|
|
stop_price: Option<f64>,
|
|
stop_loss: Option<f64>,
|
|
take_profit: Option<f64>,
|
|
comment: Option<String>,
|
|
) -> 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<i64>,
|
|
limit_price: Option<f64>,
|
|
stop_price: Option<f64>,
|
|
stop_loss: Option<f64>,
|
|
take_profit: Option<f64>,
|
|
) -> 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<f64>,
|
|
/// Executed volume in cents.
|
|
pub executed_volume: Option<i64>,
|
|
/// Linked position ID (if any).
|
|
pub position_id: Option<i64>,
|
|
/// Execution type as proto enum value.
|
|
pub execution_type: i32,
|
|
/// Open timestamp (Unix ms).
|
|
pub timestamp: Option<i64>,
|
|
}
|
|
|
|
/// 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<ExecutionEventInfo> {
|
|
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<i64> {
|
|
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);
|
|
}
|
|
}
|