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!("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}