Skip to main content

finance_query/adapters/fmp/
crypto.rs

1//! Cryptocurrency endpoints for Financial Modeling Prep.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9use super::models::{FmpQuote, HistoricalPriceResponse, IntradayPrice};
10
11/// An available cryptocurrency or forex/commodity symbol.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct AvailableSymbol {
15    /// Ticker symbol (e.g., `"BTCUSD"`).
16    pub symbol: Option<String>,
17    /// Display name.
18    pub name: Option<String>,
19    /// Currency.
20    pub currency: Option<String>,
21    /// Exchange name.
22    #[serde(rename = "stockExchange")]
23    pub stock_exchange: Option<String>,
24    /// Short exchange name.
25    #[serde(rename = "exchangeShortName")]
26    pub exchange_short_name: Option<String>,
27}
28
29/// Fetch a real-time crypto quote.
30///
31/// * `symbol` - e.g., `"BTCUSD"`
32pub async fn crypto_quote(symbol: &str) -> Result<Vec<FmpQuote>> {
33    let client = build_client()?;
34    let path = format!("/api/v3/quote/{}", encode_path_segment(symbol));
35    client.get(&path, &[]).await
36}
37
38/// List all available cryptocurrency pairs.
39pub async fn crypto_available() -> Result<Vec<AvailableSymbol>> {
40    let client = build_client()?;
41    client
42        .get("/api/v3/symbol/available-cryptocurrencies", &[])
43        .await
44}
45
46/// Fetch daily historical prices for a cryptocurrency.
47///
48/// * `symbol` - e.g., `"BTCUSD"`
49/// * `params` - Optional query params such as `from`, `to`
50pub async fn crypto_historical(
51    symbol: &str,
52    params: &[(&str, &str)],
53) -> Result<HistoricalPriceResponse> {
54    let client = build_client()?;
55    let path = format!(
56        "/api/v3/historical-price-full/{}",
57        encode_path_segment(symbol)
58    );
59    client.get(&path, params).await
60}
61
62/// Fetch intraday prices for a cryptocurrency.
63///
64/// * `symbol` - e.g., `"BTCUSD"`
65/// * `interval` - e.g., `"1min"`, `"5min"`, `"15min"`, `"30min"`, `"1hour"`, `"4hour"`
66/// * `params` - Optional query params such as `from`, `to`
67pub async fn crypto_intraday(
68    symbol: &str,
69    interval: &str,
70    params: &[(&str, &str)],
71) -> Result<Vec<IntradayPrice>> {
72    let client = build_client()?;
73    let path = format!(
74        "/api/v3/historical-chart/{}/{}",
75        encode_path_segment(interval),
76        encode_path_segment(symbol)
77    );
78    client.get(&path, params).await
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[tokio::test]
86    async fn test_crypto_available_mock() {
87        let mut server = mockito::Server::new_async().await;
88        let _mock = server
89            .mock("GET", "/api/v3/symbol/available-cryptocurrencies")
90            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
91                "apikey".into(),
92                "test-key".into(),
93            )]))
94            .with_status(200)
95            .with_body(
96                serde_json::json!([
97                    {
98                        "symbol": "BTCUSD",
99                        "name": "Bitcoin USD",
100                        "currency": "USD",
101                        "stockExchange": "CCC",
102                        "exchangeShortName": "CRYPTO"
103                    }
104                ])
105                .to_string(),
106            )
107            .create_async()
108            .await;
109
110        let client = super::super::build_test_client(&server.url()).unwrap();
111        let result: Vec<AvailableSymbol> = client
112            .get("/api/v3/symbol/available-cryptocurrencies", &[])
113            .await
114            .unwrap();
115        assert_eq!(result.len(), 1);
116        assert_eq!(result[0].symbol.as_deref(), Some("BTCUSD"));
117    }
118}