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!(
132            "prices/{}?resolution={}&from={}&to={}",
133            epic, resolution, from, to
134        );
135        info!("Getting historical prices for: {}", epic);
136
137        let result = self
138            .client
139            .request::<(), HistoricalPricesResponse>(Method::GET, &path, session, None, "3")
140            .await?;
141
142        debug!("Historical prices obtained for: {}", epic);
143        Ok(result)
144    }
145
146    async fn get_market_navigation(
147        &self,
148        session: &IgSession,
149    ) -> Result<MarketNavigationResponse, AppError> {
150        let path = "marketnavigation";
151        info!("Getting top-level market navigation nodes");
152
153        let result = self
154            .client
155            .request::<(), MarketNavigationResponse>(Method::GET, path, session, None, "1")
156            .await?;
157
158        debug!("{} navigation nodes found", result.nodes.len());
159        debug!("{} markets found at root level", result.markets.len());
160        Ok(result)
161    }
162
163    async fn get_market_navigation_node(
164        &self,
165        session: &IgSession,
166        node_id: &str,
167    ) -> Result<MarketNavigationResponse, AppError> {
168        let path = format!("marketnavigation/{}", node_id);
169        info!("Getting market navigation node: {}", node_id);
170
171        let result = self
172            .client
173            .request::<(), MarketNavigationResponse>(Method::GET, &path, session, None, "1")
174            .await?;
175
176        debug!("{} child nodes found", result.nodes.len());
177        debug!("{} markets found in node {}", result.markets.len(), node_id);
178        Ok(result)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::config::Config;
186    use crate::transport::http_client::IgHttpClientImpl;
187    use crate::utils::rate_limiter::RateLimitType;
188    use std::sync::Arc;
189
190    #[test]
191    fn test_get_and_set_config() {
192        let config = Arc::new(Config::with_rate_limit_type(
193            RateLimitType::NonTradingAccount,
194            0.7,
195        ));
196        let client = Arc::new(IgHttpClientImpl::new(config.clone()));
197        let mut service = MarketServiceImpl::new(config.clone(), client.clone());
198        assert!(std::ptr::eq(service.get_config(), &*config));
199        let new_cfg = Arc::new(Config::default());
200        service.set_config(new_cfg.clone());
201        assert!(std::ptr::eq(service.get_config(), &*new_cfg));
202    }
203}