finance_query/adapters/polygon/forex/
quotes.rs1use crate::adapters::common::encode_path_segment;
4use crate::error::{FinanceError, Result};
5use serde::{Deserialize, Serialize};
6
7use super::super::build_client;
8use super::super::models::*;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct ForexLastQuote {
18 pub bid: Option<f64>,
20 pub ask: Option<f64>,
22 pub exchange: Option<i32>,
24 pub timestamp: Option<i64>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[non_exhaustive]
31pub struct ForexQuoteResponse {
32 pub status: Option<String>,
34 pub request_id: Option<String>,
36 pub last: Option<ForexLastQuote>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42#[non_exhaustive]
43pub struct ConversionLast {
44 pub bid: Option<f64>,
46 pub ask: Option<f64>,
48 pub exchange: Option<i32>,
50 pub timestamp: Option<i64>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[non_exhaustive]
57pub struct CurrencyConversion {
58 pub status: Option<String>,
60 pub converted: Option<f64>,
62 pub from: Option<String>,
64 pub to: Option<String>,
66 #[serde(rename = "initialAmount")]
68 pub initial_amount: Option<f64>,
69 pub last: Option<ConversionLast>,
71}
72
73pub async fn forex_last_quote(from: &str, to: &str) -> Result<ForexQuoteResponse> {
84 let client = build_client()?;
85 let path = format!(
86 "/v1/last_quote/currencies/{}/{}",
87 encode_path_segment(from),
88 encode_path_segment(to)
89 );
90 let json = client.get_raw(&path, &[]).await?;
91 serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
92 field: "forex_last_quote".to_string(),
93 context: format!("Failed to parse forex last quote response: {e}"),
94 })
95}
96
97pub async fn forex_quotes(
104 ticker: &str,
105 params: &[(&str, &str)],
106) -> Result<PaginatedResponse<Quote>> {
107 let client = build_client()?;
108 let path = format!("/v3/quotes/{}", encode_path_segment(ticker));
109 client.get(&path, params).await
110}
111
112pub async fn currency_conversion(from: &str, to: &str, amount: f64) -> Result<CurrencyConversion> {
120 let client = build_client()?;
121 let path = format!(
122 "/v1/conversion/{}/{}",
123 encode_path_segment(from),
124 encode_path_segment(to)
125 );
126 let amount_str = amount.to_string();
127 let params = [("amount", amount_str.as_str())];
128 let json = client.get_raw(&path, ¶ms).await?;
129 serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
130 field: "currency_conversion".to_string(),
131 context: format!("Failed to parse currency conversion response: {e}"),
132 })
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[tokio::test]
140 async fn test_forex_last_quote_mock() {
141 let mut server = mockito::Server::new_async().await;
142 let _mock = server
143 .mock("GET", "/v1/last_quote/currencies/EUR/USD")
144 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
145 "apiKey".into(),
146 "test-key".into(),
147 )]))
148 .with_status(200)
149 .with_header("content-type", "application/json")
150 .with_body(
151 serde_json::json!({
152 "status": "OK",
153 "request_id": "abc123",
154 "last": {
155 "bid": 1.1050,
156 "ask": 1.1052,
157 "exchange": 48,
158 "timestamp": 1705363200000_i64
159 }
160 })
161 .to_string(),
162 )
163 .create_async()
164 .await;
165
166 let client = super::super::super::build_test_client(&server.url()).unwrap();
167 let json = client
168 .get_raw("/v1/last_quote/currencies/EUR/USD", &[])
169 .await
170 .unwrap();
171
172 let resp: ForexQuoteResponse = serde_json::from_value(json).unwrap();
173 assert_eq!(resp.status.as_deref(), Some("OK"));
174 let last = resp.last.unwrap();
175 assert!((last.bid.unwrap() - 1.1050).abs() < 0.0001);
176 assert!((last.ask.unwrap() - 1.1052).abs() < 0.0001);
177 assert_eq!(last.exchange.unwrap(), 48);
178 }
179
180 #[tokio::test]
181 async fn test_forex_quotes_mock() {
182 let mut server = mockito::Server::new_async().await;
183 let _mock = server
184 .mock("GET", "/v3/quotes/C:EURUSD")
185 .match_query(mockito::Matcher::AllOf(vec![
186 mockito::Matcher::UrlEncoded("apiKey".into(), "test-key".into()),
187 ]))
188 .with_status(200)
189 .with_header("content-type", "application/json")
190 .with_body(
191 serde_json::json!({
192 "request_id": "abc123",
193 "status": "OK",
194 "results": [
195 { "ask_price": 1.1052, "bid_price": 1.1050, "ask_size": 1000.0, "bid_size": 1500.0, "sip_timestamp": 1705363200000000000_i64 },
196 { "ask_price": 1.1053, "bid_price": 1.1051, "ask_size": 800.0, "bid_size": 1200.0, "sip_timestamp": 1705363200100000000_i64 }
197 ]
198 })
199 .to_string(),
200 )
201 .create_async()
202 .await;
203
204 let client = super::super::super::build_test_client(&server.url()).unwrap();
205 let resp: PaginatedResponse<Quote> = client.get("/v3/quotes/C:EURUSD", &[]).await.unwrap();
206 let quotes = resp.results.unwrap();
207 assert_eq!(quotes.len(), 2);
208 assert!((quotes[0].ask_price.unwrap() - 1.1052).abs() < 0.0001);
209 assert!((quotes[0].bid_price.unwrap() - 1.1050).abs() < 0.0001);
210 }
211
212 #[tokio::test]
213 async fn test_currency_conversion_mock() {
214 let mut server = mockito::Server::new_async().await;
215 let _mock = server
216 .mock("GET", "/v1/conversion/EUR/USD")
217 .match_query(mockito::Matcher::AllOf(vec![
218 mockito::Matcher::UrlEncoded("apiKey".into(), "test-key".into()),
219 mockito::Matcher::UrlEncoded("amount".into(), "100".into()),
220 ]))
221 .with_status(200)
222 .with_header("content-type", "application/json")
223 .with_body(
224 serde_json::json!({
225 "status": "OK",
226 "converted": 110.50,
227 "from": "EUR",
228 "to": "USD",
229 "initialAmount": 100.0,
230 "last": {
231 "bid": 1.1050,
232 "ask": 1.1052,
233 "exchange": 48,
234 "timestamp": 1705363200000_i64
235 }
236 })
237 .to_string(),
238 )
239 .create_async()
240 .await;
241
242 let client = super::super::super::build_test_client(&server.url()).unwrap();
243 let json = client
244 .get_raw("/v1/conversion/EUR/USD", &[("amount", "100")])
245 .await
246 .unwrap();
247
248 let resp: CurrencyConversion = serde_json::from_value(json).unwrap();
249 assert_eq!(resp.status.as_deref(), Some("OK"));
250 assert!((resp.converted.unwrap() - 110.50).abs() < 0.01);
251 assert_eq!(resp.from.as_deref(), Some("EUR"));
252 assert_eq!(resp.to.as_deref(), Some("USD"));
253 assert!((resp.initial_amount.unwrap() - 100.0).abs() < 0.01);
254 let last = resp.last.unwrap();
255 assert!((last.bid.unwrap() - 1.1050).abs() < 0.0001);
256 }
257}