ig_client/application/services/
market_service.rs1use 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
16pub struct MarketServiceImpl<T: IgHttpClient> {
18 config: Arc<Config>,
19 client: Arc<T>,
20}
21
22impl<T: IgHttpClient> MarketServiceImpl<T> {
23 pub fn new(config: Arc<Config>, client: Arc<T>) -> Self {
25 Self { config, client }
26 }
27
28 pub fn get_config(&self) -> &Config {
33 &self.config
34 }
35
36 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 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 #[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}