Skip to main content

finance_query/adapters/fmp/
technical_indicators.rs

1//! Technical indicator endpoints (SMA, EMA, RSI, MACD, WMA, DEMA, TEMA, Williams, ADX).
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10// ============================================================================
11// Response types
12// ============================================================================
13
14/// A single technical indicator data point.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct TechnicalIndicatorValue {
18    /// Date or datetime string.
19    pub date: Option<String>,
20    /// Open price.
21    pub open: Option<f64>,
22    /// High price.
23    pub high: Option<f64>,
24    /// Low price.
25    pub low: Option<f64>,
26    /// Close price.
27    pub close: Option<f64>,
28    /// Trading volume.
29    pub volume: Option<f64>,
30    /// Simple moving average.
31    pub sma: Option<f64>,
32    /// Exponential moving average.
33    pub ema: Option<f64>,
34    /// Relative strength index.
35    pub rsi: Option<f64>,
36    /// MACD value.
37    pub macd: Option<f64>,
38    /// MACD signal line.
39    #[serde(rename = "macdSignal")]
40    pub macd_signal: Option<f64>,
41    /// MACD histogram.
42    #[serde(rename = "macdHist")]
43    pub macd_hist: Option<f64>,
44    /// Weighted moving average.
45    pub wma: Option<f64>,
46    /// Double exponential moving average.
47    pub dema: Option<f64>,
48    /// Triple exponential moving average.
49    pub tema: Option<f64>,
50    /// Williams %R.
51    pub williams: Option<f64>,
52    /// Average directional index.
53    pub adx: Option<f64>,
54}
55
56// ============================================================================
57// Helpers
58// ============================================================================
59
60/// Fetch a daily technical indicator.
61async fn fetch_daily_indicator(
62    symbol: &str,
63    indicator_name: &str,
64    period: u32,
65    type_: &str,
66) -> Result<Vec<TechnicalIndicatorValue>> {
67    let client = build_client()?;
68    let path = format!(
69        "/api/v3/technical_indicator/daily/{}",
70        encode_path_segment(symbol)
71    );
72    let period_str = period.to_string();
73    client
74        .get(
75            &path,
76            &[
77                ("period", &*period_str),
78                ("type", type_),
79                ("indicator", indicator_name),
80            ],
81        )
82        .await
83}
84
85/// Fetch an intraday technical indicator.
86async fn fetch_intraday_indicator(
87    symbol: &str,
88    interval: &str,
89    indicator_name: &str,
90    period: u32,
91    type_: &str,
92) -> Result<Vec<TechnicalIndicatorValue>> {
93    let client = build_client()?;
94    let path = format!(
95        "/api/v3/technical_indicator/{}/{}",
96        encode_path_segment(interval),
97        encode_path_segment(symbol)
98    );
99    let period_str = period.to_string();
100    client
101        .get(
102            &path,
103            &[
104                ("period", &*period_str),
105                ("type", type_),
106                ("indicator", indicator_name),
107            ],
108        )
109        .await
110}
111
112// ============================================================================
113// Daily indicators
114// ============================================================================
115
116/// Fetch daily Simple Moving Average (SMA).
117pub async fn daily_sma(
118    symbol: &str,
119    period: u32,
120    type_: &str,
121) -> Result<Vec<TechnicalIndicatorValue>> {
122    fetch_daily_indicator(symbol, "sma", period, type_).await
123}
124
125/// Fetch daily Exponential Moving Average (EMA).
126pub async fn daily_ema(
127    symbol: &str,
128    period: u32,
129    type_: &str,
130) -> Result<Vec<TechnicalIndicatorValue>> {
131    fetch_daily_indicator(symbol, "ema", period, type_).await
132}
133
134/// Fetch daily Relative Strength Index (RSI).
135pub async fn daily_rsi(
136    symbol: &str,
137    period: u32,
138    type_: &str,
139) -> Result<Vec<TechnicalIndicatorValue>> {
140    fetch_daily_indicator(symbol, "rsi", period, type_).await
141}
142
143/// Fetch daily MACD.
144pub async fn daily_macd(symbol: &str, type_: &str) -> Result<Vec<TechnicalIndicatorValue>> {
145    let client = build_client()?;
146    let path = format!(
147        "/api/v3/technical_indicator/daily/{}",
148        encode_path_segment(symbol)
149    );
150    client
151        .get(&path, &[("type", type_), ("indicator", "macd")])
152        .await
153}
154
155/// Fetch daily Weighted Moving Average (WMA).
156pub async fn daily_wma(
157    symbol: &str,
158    period: u32,
159    type_: &str,
160) -> Result<Vec<TechnicalIndicatorValue>> {
161    fetch_daily_indicator(symbol, "wma", period, type_).await
162}
163
164/// Fetch daily Double Exponential Moving Average (DEMA).
165pub async fn daily_dema(
166    symbol: &str,
167    period: u32,
168    type_: &str,
169) -> Result<Vec<TechnicalIndicatorValue>> {
170    fetch_daily_indicator(symbol, "dema", period, type_).await
171}
172
173/// Fetch daily Triple Exponential Moving Average (TEMA).
174pub async fn daily_tema(
175    symbol: &str,
176    period: u32,
177    type_: &str,
178) -> Result<Vec<TechnicalIndicatorValue>> {
179    fetch_daily_indicator(symbol, "tema", period, type_).await
180}
181
182/// Fetch daily Williams %R.
183pub async fn daily_williams(
184    symbol: &str,
185    period: u32,
186    type_: &str,
187) -> Result<Vec<TechnicalIndicatorValue>> {
188    fetch_daily_indicator(symbol, "williams", period, type_).await
189}
190
191/// Fetch daily Average Directional Index (ADX).
192pub async fn daily_adx(
193    symbol: &str,
194    period: u32,
195    type_: &str,
196) -> Result<Vec<TechnicalIndicatorValue>> {
197    fetch_daily_indicator(symbol, "adx", period, type_).await
198}
199
200// ============================================================================
201// Intraday indicators
202// ============================================================================
203
204/// Fetch intraday Simple Moving Average (SMA).
205///
206/// * `interval` - e.g., `"1min"`, `"5min"`, `"15min"`, `"30min"`, `"1hour"`, `"4hour"`
207pub async fn intraday_sma(
208    symbol: &str,
209    interval: &str,
210    period: u32,
211    type_: &str,
212) -> Result<Vec<TechnicalIndicatorValue>> {
213    fetch_intraday_indicator(symbol, interval, "sma", period, type_).await
214}
215
216/// Fetch intraday Exponential Moving Average (EMA).
217pub async fn intraday_ema(
218    symbol: &str,
219    interval: &str,
220    period: u32,
221    type_: &str,
222) -> Result<Vec<TechnicalIndicatorValue>> {
223    fetch_intraday_indicator(symbol, interval, "ema", period, type_).await
224}
225
226/// Fetch intraday Relative Strength Index (RSI).
227pub async fn intraday_rsi(
228    symbol: &str,
229    interval: &str,
230    period: u32,
231    type_: &str,
232) -> Result<Vec<TechnicalIndicatorValue>> {
233    fetch_intraday_indicator(symbol, interval, "rsi", period, type_).await
234}
235
236/// Fetch intraday MACD.
237pub async fn intraday_macd(
238    symbol: &str,
239    interval: &str,
240    type_: &str,
241) -> Result<Vec<TechnicalIndicatorValue>> {
242    let client = build_client()?;
243    let path = format!(
244        "/api/v3/technical_indicator/{}/{}",
245        encode_path_segment(interval),
246        encode_path_segment(symbol)
247    );
248    client
249        .get(&path, &[("type", type_), ("indicator", "macd")])
250        .await
251}
252
253/// Fetch intraday Weighted Moving Average (WMA).
254pub async fn intraday_wma(
255    symbol: &str,
256    interval: &str,
257    period: u32,
258    type_: &str,
259) -> Result<Vec<TechnicalIndicatorValue>> {
260    fetch_intraday_indicator(symbol, interval, "wma", period, type_).await
261}
262
263/// Fetch intraday Double Exponential Moving Average (DEMA).
264pub async fn intraday_dema(
265    symbol: &str,
266    interval: &str,
267    period: u32,
268    type_: &str,
269) -> Result<Vec<TechnicalIndicatorValue>> {
270    fetch_intraday_indicator(symbol, interval, "dema", period, type_).await
271}
272
273/// Fetch intraday Triple Exponential Moving Average (TEMA).
274pub async fn intraday_tema(
275    symbol: &str,
276    interval: &str,
277    period: u32,
278    type_: &str,
279) -> Result<Vec<TechnicalIndicatorValue>> {
280    fetch_intraday_indicator(symbol, interval, "tema", period, type_).await
281}
282
283/// Fetch intraday Williams %R.
284pub async fn intraday_williams(
285    symbol: &str,
286    interval: &str,
287    period: u32,
288    type_: &str,
289) -> Result<Vec<TechnicalIndicatorValue>> {
290    fetch_intraday_indicator(symbol, interval, "williams", period, type_).await
291}
292
293/// Fetch intraday Average Directional Index (ADX).
294pub async fn intraday_adx(
295    symbol: &str,
296    interval: &str,
297    period: u32,
298    type_: &str,
299) -> Result<Vec<TechnicalIndicatorValue>> {
300    fetch_intraday_indicator(symbol, interval, "adx", period, type_).await
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[tokio::test]
308    async fn test_daily_sma_mock() {
309        let mut server = mockito::Server::new_async().await;
310        let _mock = server
311            .mock("GET", "/api/v3/technical_indicator/daily/AAPL")
312            .match_query(mockito::Matcher::AllOf(vec![
313                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
314                mockito::Matcher::UrlEncoded("period".into(), "20".into()),
315                mockito::Matcher::UrlEncoded("type".into(), "close".into()),
316                mockito::Matcher::UrlEncoded("indicator".into(), "sma".into()),
317            ]))
318            .with_status(200)
319            .with_body(
320                serde_json::json!([
321                    {
322                        "date": "2024-01-15",
323                        "open": 182.0,
324                        "high": 185.0,
325                        "low": 181.0,
326                        "close": 184.0,
327                        "volume": 50000000.0,
328                        "sma": 183.5
329                    }
330                ])
331                .to_string(),
332            )
333            .create_async()
334            .await;
335
336        let client = super::super::build_test_client(&server.url()).unwrap();
337        let path = "/api/v3/technical_indicator/daily/AAPL";
338        let resp: Vec<TechnicalIndicatorValue> = client
339            .get(
340                path,
341                &[("period", "20"), ("type", "close"), ("indicator", "sma")],
342            )
343            .await
344            .unwrap();
345        assert_eq!(resp.len(), 1);
346        assert!((resp[0].sma.unwrap() - 183.5).abs() < 0.01);
347    }
348
349    #[tokio::test]
350    async fn test_intraday_ema_mock() {
351        let mut server = mockito::Server::new_async().await;
352        let _mock = server
353            .mock("GET", "/api/v3/technical_indicator/5min/AAPL")
354            .match_query(mockito::Matcher::AllOf(vec![
355                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
356                mockito::Matcher::UrlEncoded("period".into(), "10".into()),
357                mockito::Matcher::UrlEncoded("type".into(), "close".into()),
358                mockito::Matcher::UrlEncoded("indicator".into(), "ema".into()),
359            ]))
360            .with_status(200)
361            .with_body(
362                serde_json::json!([
363                    {
364                        "date": "2024-01-15 10:05:00",
365                        "open": 182.5,
366                        "high": 183.0,
367                        "low": 182.0,
368                        "close": 182.8,
369                        "volume": 1200000.0,
370                        "ema": 182.6
371                    }
372                ])
373                .to_string(),
374            )
375            .create_async()
376            .await;
377
378        let client = super::super::build_test_client(&server.url()).unwrap();
379        let path = "/api/v3/technical_indicator/5min/AAPL";
380        let resp: Vec<TechnicalIndicatorValue> = client
381            .get(
382                path,
383                &[("period", "10"), ("type", "close"), ("indicator", "ema")],
384            )
385            .await
386            .unwrap();
387        assert_eq!(resp.len(), 1);
388        assert!((resp[0].ema.unwrap() - 182.6).abs() < 0.01);
389    }
390}