Skip to main content

finance_query/adapters/polygon/crypto/
trades.rs

1//! Crypto trade endpoints: last trade, historical trades.
2
3use 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/// Last trade data for a crypto pair.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct CryptoLastTrade {
15    /// Price of the last trade.
16    pub price: Option<f64>,
17    /// Size of the last trade.
18    pub size: Option<f64>,
19    /// Exchange where the trade occurred.
20    pub exchange: Option<i32>,
21    /// Trade conditions.
22    pub conditions: Option<Vec<i32>>,
23    /// Timestamp of the trade.
24    pub timestamp: Option<i64>,
25}
26
27/// Response wrapper for crypto last trade.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[non_exhaustive]
30pub struct CryptoLastTradeResponse {
31    /// The last trade data.
32    pub last: Option<CryptoLastTrade>,
33    /// Request identifier.
34    pub request_id: Option<String>,
35    /// Response status.
36    pub status: Option<String>,
37    /// Symbol of the crypto pair.
38    pub symbol: Option<String>,
39}
40
41/// Fetch the most recent trade for a crypto pair.
42///
43/// The ticker should be in the format `"X:BTCUSD"`. The `from` and `to` components
44/// are extracted automatically (e.g., `BTC` and `USD`).
45///
46/// * `from` - The base currency (e.g., `"BTC"`)
47/// * `to` - The quote currency (e.g., `"USD"`)
48pub async fn crypto_last_trade(from: &str, to: &str) -> Result<CryptoLastTradeResponse> {
49    let client = build_client()?;
50    let path = format!(
51        "/v1/last/crypto/{}/{}",
52        encode_path_segment(from),
53        encode_path_segment(to)
54    );
55    let json = client.get_raw(&path, &[]).await?;
56    serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
57        field: "crypto_last_trade".to_string(),
58        context: format!("Failed to parse crypto last trade response: {e}"),
59    })
60}
61
62/// Fetch historical trades for a crypto ticker.
63///
64/// * `ticker` - Crypto ticker symbol with `X:` prefix (e.g., `"X:BTCUSD"`)
65/// * `params` - Optional query params: `timestamp`, `order`, `limit`, `sort`
66pub async fn crypto_trades(
67    ticker: &str,
68    params: &[(&str, &str)],
69) -> Result<PaginatedResponse<Trade>> {
70    let client = build_client()?;
71    let path = format!("/v3/trades/{}", encode_path_segment(ticker));
72    client.get(&path, params).await
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[tokio::test]
80    async fn test_crypto_last_trade_mock() {
81        let mut server = mockito::Server::new_async().await;
82        let _mock = server
83            .mock("GET", "/v1/last/crypto/BTC/USD")
84            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
85                "apiKey".into(),
86                "test-key".into(),
87            )]))
88            .with_status(200)
89            .with_header("content-type", "application/json")
90            .with_body(
91                serde_json::json!({
92                    "request_id": "abc123",
93                    "status": "OK",
94                    "symbol": "X:BTCUSD",
95                    "last": {
96                        "price": 43100.50,
97                        "size": 0.5,
98                        "exchange": 2,
99                        "conditions": [1],
100                        "timestamp": 1705363200000_i64
101                    }
102                })
103                .to_string(),
104            )
105            .create_async()
106            .await;
107
108        let client = super::super::super::build_test_client(&server.url()).unwrap();
109        let json = client
110            .get_raw("/v1/last/crypto/BTC/USD", &[])
111            .await
112            .unwrap();
113
114        let resp: CryptoLastTradeResponse = serde_json::from_value(json).unwrap();
115        assert_eq!(resp.status.as_deref(), Some("OK"));
116        assert_eq!(resp.symbol.as_deref(), Some("X:BTCUSD"));
117        let last = resp.last.unwrap();
118        assert!((last.price.unwrap() - 43100.50).abs() < 0.01);
119        assert!((last.size.unwrap() - 0.5).abs() < 0.01);
120    }
121}