ig_client/application/services/
market_service.rs

1use crate::application::services::MarketService;
2use crate::{
3    application::models::market::{
4        HistoricalPricesResponse, MarketDetails, MarketNavigationResponse, MarketSearchResult,
5    },
6    config::Config,
7    error::AppError,
8    session::interface::IgSession,
9    transport::http_client::IgHttpClient,
10};
11use async_trait::async_trait;
12use reqwest::Method;
13use std::sync::Arc;
14use tracing::{debug, info};
15
16/// Implementation of the market service
17pub struct MarketServiceImpl<T: IgHttpClient> {
18    config: Arc<Config>,
19    client: Arc<T>,
20}
21
22impl<T: IgHttpClient> MarketServiceImpl<T> {
23    /// Creates a new instance of the market service
24    pub fn new(config: Arc<Config>, client: Arc<T>) -> Self {
25        Self { config, client }
26    }
27
28    /// Gets the current configuration
29    ///
30    /// # Returns
31    /// * Reference to the current configuration
32    pub fn get_config(&self) -> &Config {
33        &self.config
34    }
35
36    /// Sets a new configuration
37    ///
38    /// # Arguments
39    /// * `config` - The new configuration to use
40    pub fn set_config(&mut self, config: Arc<Config>) {
41        self.config = config;
42    }
43}
44
45#[async_trait]
46impl<T: IgHttpClient + 'static> MarketService for MarketServiceImpl<T> {
47    async fn search_markets(
48        &self,
49        session: &IgSession,
50        search_term: &str,
51    ) -> Result<MarketSearchResult, AppError> {
52        let path = format!("markets?searchTerm={search_term}");
53        info!("Searching markets with term: {}", search_term);
54
55        let result = self
56            .client
57            .request::<(), MarketSearchResult>(Method::GET, &path, session, None, "1")
58            .await?;
59
60        debug!("{} markets found", result.markets.len());
61        Ok(result)
62    }
63
64    async fn get_market_details(
65        &self,
66        session: &IgSession,
67        epic: &str,
68    ) -> Result<MarketDetails, AppError> {
69        let path = format!("markets/{epic}");
70        info!("Getting market details: {}", epic);
71
72        let result = self
73            .client
74            .request::<(), MarketDetails>(Method::GET, &path, session, None, "3")
75            .await?;
76
77        debug!("Market details obtained for: {}", epic);
78        Ok(result)
79    }
80
81    async fn get_multiple_market_details(
82        &self,
83        session: &IgSession,
84        epics: &[String],
85    ) -> Result<Vec<MarketDetails>, AppError> {
86        if epics.is_empty() {
87            return Ok(Vec::new());
88        } else if epics.len() > 50 {
89            return Err(AppError::InvalidInput(
90                "The maximum number of EPICs is 50".to_string(),
91            ));
92        }
93
94        // Join the EPICs with commas to create a single request
95        let epics_str = epics.join(",");
96        let path = format!("markets?epics={epics_str}");
97
98        debug!(
99            "Getting market details for {} EPICs in a batch: {}",
100            epics.len(),
101            epics_str
102        );
103
104        // The API returns an object with un array de MarketDetails en la propiedad marketDetails
105        #[derive(serde::Deserialize)]
106        struct MarketDetailsResponse {
107            #[serde(rename = "marketDetails")]
108            market_details: Vec<MarketDetails>,
109        }
110
111        let response = self
112            .client
113            .request::<(), MarketDetailsResponse>(Method::GET, &path, session, None, "2")
114            .await?;
115
116        debug!(
117            "Market details obtained for {} EPICs",
118            response.market_details.len()
119        );
120        Ok(response.market_details)
121    }
122
123    async fn get_historical_prices(
124        &self,
125        session: &IgSession,
126        epic: &str,
127        resolution: &str,
128        from: &str,
129        to: &str,
130    ) -> Result<HistoricalPricesResponse, AppError> {
131        let path = format!("prices/{epic}?resolution={resolution}&from={from}&to={to}");
132        info!("Getting historical prices for: {}", epic);
133
134        let result = self
135            .client
136            .request::<(), HistoricalPricesResponse>(Method::GET, &path, session, None, "3")
137            .await?;
138
139        debug!("Historical prices obtained for: {}", epic);
140        Ok(result)
141    }
142
143    async fn get_market_navigation(
144        &self,
145        session: &IgSession,
146    ) -> Result<MarketNavigationResponse, AppError> {
147        let path = "marketnavigation";
148        info!("Getting top-level market navigation nodes");
149
150        let result = self
151            .client
152            .request::<(), MarketNavigationResponse>(Method::GET, path, session, None, "1")
153            .await?;
154
155        debug!("{} navigation nodes found", result.nodes.len());
156        debug!("{} markets found at root level", result.markets.len());
157        Ok(result)
158    }
159
160    async fn get_market_navigation_node(
161        &self,
162        session: &IgSession,
163        node_id: &str,
164    ) -> Result<MarketNavigationResponse, AppError> {
165        let path = format!("marketnavigation/{node_id}");
166        info!("Getting market navigation node: {}", node_id);
167
168        let result = self
169            .client
170            .request::<(), MarketNavigationResponse>(Method::GET, &path, session, None, "1")
171            .await?;
172
173        debug!("{} child nodes found", result.nodes.len());
174        debug!("{} markets found in node {}", result.markets.len(), node_id);
175        Ok(result)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::config::Config;
183    use crate::transport::http_client::IgHttpClientImpl;
184    use crate::utils::rate_limiter::RateLimitType;
185    use std::sync::Arc;
186
187    #[test]
188    fn test_get_and_set_config() {
189        let config = Arc::new(Config::with_rate_limit_type(
190            RateLimitType::NonTradingAccount,
191            0.7,
192        ));
193        let client = Arc::new(IgHttpClientImpl::new(config.clone()));
194        let mut service = MarketServiceImpl::new(config.clone(), client.clone());
195        assert!(std::ptr::eq(service.get_config(), &*config));
196        let new_cfg = Arc::new(Config::default());
197        service.set_config(new_cfg.clone());
198        assert!(std::ptr::eq(service.get_config(), &*new_cfg));
199    }
200}