ig_client/application/services/
order_service.rs

1use crate::application::models::account::WorkingOrders;
2use crate::application::models::order::{
3    ClosePositionRequest, ClosePositionResponse, CreateOrderRequest, CreateOrderResponse,
4    OrderConfirmation, UpdatePositionRequest, UpdatePositionResponse,
5};
6use crate::application::models::working_order::{
7    CreateWorkingOrderRequest, CreateWorkingOrderResponse,
8};
9use crate::application::services::interfaces::order::OrderService;
10use crate::config::Config;
11use crate::error::AppError;
12use crate::session::interface::IgSession;
13use crate::transport::http_client::IgHttpClient;
14use async_trait::async_trait;
15use reqwest::Method;
16use std::sync::Arc;
17use tracing::{debug, info};
18
19/// Implementation of the order service
20pub struct OrderServiceImpl<T: IgHttpClient> {
21    config: Arc<Config>,
22    client: Arc<T>,
23}
24
25impl<T: IgHttpClient> OrderServiceImpl<T> {
26    /// Creates a new instance of the order service
27    pub fn new(config: Arc<Config>, client: Arc<T>) -> Self {
28        Self { config, client }
29    }
30
31    /// Gets the current configuration
32    ///
33    /// # Returns
34    /// * The current configuration as an `Arc<Config>`
35    pub fn get_config(&self) -> Arc<Config> {
36        self.config.clone()
37    }
38
39    /// Sets a new configuration
40    ///
41    /// # Arguments
42    /// * `config` - The new configuration to use
43    pub fn set_config(&mut self, config: Arc<Config>) {
44        self.config = config;
45    }
46}
47
48#[async_trait]
49impl<T: IgHttpClient + 'static> OrderService for OrderServiceImpl<T> {
50    async fn create_order(
51        &self,
52        session: &IgSession,
53        order: &CreateOrderRequest,
54    ) -> Result<CreateOrderResponse, AppError> {
55        info!("Creating order for: {}", order.epic);
56
57        let result = self
58            .client
59            .request::<CreateOrderRequest, CreateOrderResponse>(
60                Method::POST,
61                "positions/otc",
62                session,
63                Some(order),
64                "2",
65            )
66            .await?;
67
68        debug!("Order created with reference: {}", result.deal_reference);
69        Ok(result)
70    }
71
72    async fn get_order_confirmation(
73        &self,
74        session: &IgSession,
75        deal_reference: &str,
76    ) -> Result<OrderConfirmation, AppError> {
77        let path = format!("confirms/{deal_reference}");
78        info!("Getting confirmation for order: {}", deal_reference);
79
80        let result = self
81            .client
82            .request::<(), OrderConfirmation>(Method::GET, &path, session, None, "1")
83            .await?;
84
85        debug!("Confirmation obtained for order: {}", deal_reference);
86        Ok(result)
87    }
88
89    async fn update_position(
90        &self,
91        session: &IgSession,
92        deal_id: &str,
93        update: &UpdatePositionRequest,
94    ) -> Result<UpdatePositionResponse, AppError> {
95        let path = format!("positions/otc/{deal_id}");
96        info!("Updating position: {}", deal_id);
97
98        let result = self
99            .client
100            .request::<UpdatePositionRequest, UpdatePositionResponse>(
101                Method::PUT,
102                &path,
103                session,
104                Some(update),
105                "2",
106            )
107            .await?;
108
109        debug!(
110            "Position updated: {} with deal reference: {}",
111            deal_id, result.deal_reference
112        );
113        Ok(result)
114    }
115
116    async fn close_position(
117        &self,
118        session: &IgSession,
119        close_request: &ClosePositionRequest,
120    ) -> Result<ClosePositionResponse, AppError> {
121        use crate::constants::USER_AGENT;
122        use reqwest::Client;
123        use tracing::error;
124
125        info!("{}", serde_json::to_string(close_request)?);
126
127        // Create a direct POST request with _method: DELETE header
128        // This works around HTTP client limitations with DELETE + body
129        let url = format!("{}/positions/otc", self.config.rest_api.base_url);
130
131        // Respect rate limits before making the request
132        session.respect_rate_limit().await?;
133
134        let client = Client::new();
135        let response = client
136            .post(&url)
137            .header("Content-Type", "application/json; charset=utf-8")
138            .header("Accept", "application/json; charset=utf-8")
139            .header("User-Agent", USER_AGENT)
140            .header("Version", "1")
141            .header("X-IG-API-KEY", &self.config.credentials.api_key)
142            .header("CST", &session.cst)
143            .header("X-SECURITY-TOKEN", &session.token)
144            .header("_method", "DELETE") // This is the key header for IG API
145            .json(close_request)
146            .send()
147            .await?;
148
149        let status = response.status();
150        let response_text = response.text().await?;
151
152        if status.is_success() {
153            let close_response: ClosePositionResponse = serde_json::from_str(&response_text)?;
154            debug!(
155                "Position closed with reference: {}",
156                close_response.deal_reference
157            );
158            Ok(close_response)
159        } else {
160            error!(
161                "Unexpected status code {} for request to {}: {}",
162                status, url, response_text
163            );
164            Err(AppError::Unexpected(status))
165        }
166    }
167
168    async fn get_working_orders(&self, session: &IgSession) -> Result<WorkingOrders, AppError> {
169        info!("Getting all working orders");
170
171        let result = self
172            .client
173            .request::<(), WorkingOrders>(Method::GET, "workingorders", session, None, "2")
174            .await?;
175
176        debug!("Retrieved {} working orders", result.working_orders.len());
177        Ok(result)
178    }
179
180    async fn create_working_order(
181        &self,
182        session: &IgSession,
183        order: &CreateWorkingOrderRequest,
184    ) -> Result<CreateWorkingOrderResponse, AppError> {
185        info!("Creating working order for: {}", order.epic);
186
187        let result = self
188            .client
189            .request::<CreateWorkingOrderRequest, CreateWorkingOrderResponse>(
190                Method::POST,
191                "workingorders/otc",
192                session,
193                Some(order),
194                "2",
195            )
196            .await?;
197
198        debug!(
199            "Working order created with reference: {}",
200            result.deal_reference
201        );
202        Ok(result)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::config::Config;
210    use crate::transport::http_client::IgHttpClientImpl;
211    use crate::utils::rate_limiter::RateLimitType;
212    use std::sync::Arc;
213
214    #[test]
215    fn test_get_and_set_config() {
216        let config = Arc::new(Config::with_rate_limit_type(
217            RateLimitType::NonTradingAccount,
218            0.7,
219        ));
220        let client = Arc::new(IgHttpClientImpl::new(config.clone()));
221        let mut service = OrderServiceImpl::new(config.clone(), client.clone());
222
223        let cfg1 = service.get_config();
224        assert!(Arc::ptr_eq(&cfg1, &config));
225
226        let new_cfg = Arc::new(Config::default());
227        service.set_config(new_cfg.clone());
228        assert!(Arc::ptr_eq(&service.get_config(), &new_cfg));
229    }
230}