finance_query/adapters/polygon/crypto/
aggregates.rs1use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::{FinanceError, Result};
7
8use super::super::build_client;
9use super::super::models::*;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct CryptoDailyOpenClose {
15 pub symbol: Option<String>,
17 #[serde(rename = "isUTC")]
19 pub is_utc: Option<bool>,
20 pub day: Option<String>,
22 pub open: Option<f64>,
24 pub close: Option<f64>,
26 #[serde(rename = "openTrades")]
28 pub open_trades: Option<Vec<CryptoOpenCloseTrade>>,
29 #[serde(rename = "closingTrades")]
31 pub closing_trades: Option<Vec<CryptoOpenCloseTrade>>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36#[non_exhaustive]
37pub struct CryptoOpenCloseTrade {
38 #[serde(rename = "p")]
40 pub price: Option<f64>,
41 #[serde(rename = "s")]
43 pub size: Option<f64>,
44 #[serde(rename = "x")]
46 pub exchange: Option<i32>,
47 pub conditions: Option<Vec<i32>>,
49 #[serde(rename = "t")]
51 pub timestamp: Option<i64>,
52}
53
54pub async fn crypto_aggregates(
65 ticker: &str,
66 multiplier: u32,
67 timespan: Timespan,
68 from: &str,
69 to: &str,
70 params: Option<AggregateParams>,
71) -> Result<AggregateResponse> {
72 let client = build_client()?;
73 let path = format!(
74 "/v2/aggs/ticker/{}/range/{}/{}/{}/{}",
75 ticker,
76 multiplier,
77 timespan.as_str(),
78 from,
79 to
80 );
81
82 let mut query_params: Vec<(&str, String)> = Vec::new();
83 if let Some(ref p) = params {
84 if let Some(adjusted) = p.adjusted {
85 query_params.push(("adjusted", adjusted.to_string()));
86 }
87 if let Some(sort) = p.sort {
88 query_params.push(("sort", sort.as_str().to_string()));
89 }
90 if let Some(limit) = p.limit {
91 query_params.push(("limit", limit.to_string()));
92 }
93 }
94
95 let query_refs: Vec<(&str, &str)> =
96 query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
97
98 let json = client.get_raw(&path, &query_refs).await?;
99 serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
100 field: "crypto_aggregates".to_string(),
101 context: format!("Failed to parse crypto aggregate response: {e}"),
102 })
103}
104
105pub async fn crypto_previous_close(
110 ticker: &str,
111 adjusted: Option<bool>,
112) -> Result<AggregateResponse> {
113 let client = build_client()?;
114 let path = format!("/v2/aggs/ticker/{}/prev", encode_path_segment(ticker));
115
116 let adj_str = adjusted.unwrap_or(true).to_string();
117 let params = [("adjusted", adj_str.as_str())];
118
119 let json = client.get_raw(&path, ¶ms).await?;
120 serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
121 field: "crypto_previous_close".to_string(),
122 context: format!("Failed to parse crypto previous close response: {e}"),
123 })
124}
125
126pub async fn crypto_grouped_daily(date: &str, adjusted: Option<bool>) -> Result<AggregateResponse> {
131 let client = build_client()?;
132 let path = format!(
133 "/v2/aggs/grouped/locale/global/market/crypto/{}",
134 encode_path_segment(date)
135 );
136
137 let adj_str = adjusted.unwrap_or(true).to_string();
138 let params = [("adjusted", adj_str.as_str())];
139
140 let json = client.get_raw(&path, ¶ms).await?;
141 serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
142 field: "crypto_grouped_daily".to_string(),
143 context: format!("Failed to parse crypto grouped daily response: {e}"),
144 })
145}
146
147pub async fn crypto_daily_open_close(
153 from: &str,
154 to: &str,
155 date: &str,
156) -> Result<CryptoDailyOpenClose> {
157 let client = build_client()?;
158 let path = format!(
159 "/v1/open-close/crypto/{}/{}/{}",
160 encode_path_segment(from),
161 encode_path_segment(to),
162 encode_path_segment(date)
163 );
164
165 let json = client.get_raw(&path, &[]).await?;
166 serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
167 field: "crypto_daily_open_close".to_string(),
168 context: format!("Failed to parse crypto daily open/close response: {e}"),
169 })
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[tokio::test]
177 async fn test_crypto_aggregates_mock() {
178 let mut server = mockito::Server::new_async().await;
179 let _mock = server
180 .mock(
181 "GET",
182 "/v2/aggs/ticker/X:BTCUSD/range/1/day/2024-01-01/2024-01-31",
183 )
184 .match_query(mockito::Matcher::AllOf(vec![
185 mockito::Matcher::UrlEncoded("apiKey".into(), "test-key".into()),
186 ]))
187 .with_status(200)
188 .with_header("content-type", "application/json")
189 .with_body(
190 serde_json::json!({
191 "ticker": "X:BTCUSD",
192 "status": "OK",
193 "adjusted": true,
194 "queryCount": 1,
195 "resultsCount": 2,
196 "request_id": "abc123",
197 "results": [
198 { "o": 42000.0, "h": 43500.0, "l": 41800.0, "c": 43100.0, "v": 12345.67, "vw": 42750.0, "t": 1704067200000_i64, "n": 150000 },
199 { "o": 43100.0, "h": 44000.0, "l": 42900.0, "c": 43800.0, "v": 11000.50, "vw": 43400.0, "t": 1704153600000_i64, "n": 140000 }
200 ]
201 })
202 .to_string(),
203 )
204 .create_async()
205 .await;
206
207 let client = super::super::super::build_test_client(&server.url()).unwrap();
208 let json = client
209 .get_raw(
210 "/v2/aggs/ticker/X:BTCUSD/range/1/day/2024-01-01/2024-01-31",
211 &[],
212 )
213 .await
214 .unwrap();
215
216 let resp: AggregateResponse = serde_json::from_value(json).unwrap();
217 assert_eq!(resp.ticker.as_deref(), Some("X:BTCUSD"));
218 let results = resp.results.unwrap();
219 assert_eq!(results.len(), 2);
220 assert!((results[0].open - 42000.0).abs() < 0.01);
221 assert!((results[0].close - 43100.0).abs() < 0.01);
222 assert_eq!(results[0].timestamp, 1704067200000);
223 }
224}