Skip to main content

finance_query/adapters/fmp/
analysis.rs

1//! FMP financial analysis endpoints (ratios, metrics, DCF, ratings, growth).
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::models::Period;
9
10// ============================================================================
11// Response types
12// ============================================================================
13
14/// Financial ratios from FMP.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct FinancialRatios {
18    /// Ticker symbol.
19    pub symbol: Option<String>,
20    /// Date.
21    pub date: Option<String>,
22    /// Calendar year.
23    #[serde(rename = "calendarYear")]
24    pub calendar_year: Option<String>,
25    /// Fiscal period.
26    pub period: Option<String>,
27    /// Current ratio.
28    #[serde(rename = "currentRatio")]
29    pub current_ratio: Option<f64>,
30    /// Quick ratio.
31    #[serde(rename = "quickRatio")]
32    pub quick_ratio: Option<f64>,
33    /// Cash ratio.
34    #[serde(rename = "cashRatio")]
35    pub cash_ratio: Option<f64>,
36    /// Gross profit margin.
37    #[serde(rename = "grossProfitMargin")]
38    pub gross_profit_margin: Option<f64>,
39    /// Operating profit margin.
40    #[serde(rename = "operatingProfitMargin")]
41    pub operating_profit_margin: Option<f64>,
42    /// Net profit margin.
43    #[serde(rename = "netProfitMargin")]
44    pub net_profit_margin: Option<f64>,
45    /// Return on assets.
46    #[serde(rename = "returnOnAssets")]
47    pub return_on_assets: Option<f64>,
48    /// Return on equity.
49    #[serde(rename = "returnOnEquity")]
50    pub return_on_equity: Option<f64>,
51    /// Return on capital employed.
52    #[serde(rename = "returnOnCapitalEmployed")]
53    pub return_on_capital_employed: Option<f64>,
54    /// Debt-to-equity ratio.
55    #[serde(rename = "debtEquityRatio")]
56    pub debt_equity_ratio: Option<f64>,
57    /// Debt ratio.
58    #[serde(rename = "debtRatio")]
59    pub debt_ratio: Option<f64>,
60    /// Price-to-earnings ratio.
61    #[serde(rename = "priceEarningsRatio")]
62    pub price_earnings_ratio: Option<f64>,
63    /// Price-to-book ratio.
64    #[serde(rename = "priceToBookRatio")]
65    pub price_to_book_ratio: Option<f64>,
66    /// Price-to-sales ratio.
67    #[serde(rename = "priceToSalesRatio")]
68    pub price_to_sales_ratio: Option<f64>,
69    /// Price-to-free-cash-flow ratio.
70    #[serde(rename = "priceToFreeCashFlowsRatio")]
71    pub price_to_free_cash_flows_ratio: Option<f64>,
72    /// EV to EBITDA.
73    #[serde(rename = "enterpriseValueMultiple")]
74    pub enterprise_value_multiple: Option<f64>,
75    /// Dividend yield.
76    #[serde(rename = "dividendYield")]
77    pub dividend_yield: Option<f64>,
78    /// Payout ratio.
79    #[serde(rename = "payoutRatio")]
80    pub payout_ratio: Option<f64>,
81}
82
83/// Key metrics from FMP.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[non_exhaustive]
86pub struct KeyMetrics {
87    /// Ticker symbol.
88    pub symbol: Option<String>,
89    /// Date.
90    pub date: Option<String>,
91    /// Calendar year.
92    #[serde(rename = "calendarYear")]
93    pub calendar_year: Option<String>,
94    /// Fiscal period.
95    pub period: Option<String>,
96    /// Revenue per share.
97    #[serde(rename = "revenuePerShare")]
98    pub revenue_per_share: Option<f64>,
99    /// Net income per share.
100    #[serde(rename = "netIncomePerShare")]
101    pub net_income_per_share: Option<f64>,
102    /// Operating cash flow per share.
103    #[serde(rename = "operatingCashFlowPerShare")]
104    pub operating_cash_flow_per_share: Option<f64>,
105    /// Free cash flow per share.
106    #[serde(rename = "freeCashFlowPerShare")]
107    pub free_cash_flow_per_share: Option<f64>,
108    /// Cash per share.
109    #[serde(rename = "cashPerShare")]
110    pub cash_per_share: Option<f64>,
111    /// Book value per share.
112    #[serde(rename = "bookValuePerShare")]
113    pub book_value_per_share: Option<f64>,
114    /// Tangible book value per share.
115    #[serde(rename = "tangibleBookValuePerShare")]
116    pub tangible_book_value_per_share: Option<f64>,
117    /// Shareholders equity per share.
118    #[serde(rename = "shareholdersEquityPerShare")]
119    pub shareholders_equity_per_share: Option<f64>,
120    /// Interest debt per share.
121    #[serde(rename = "interestDebtPerShare")]
122    pub interest_debt_per_share: Option<f64>,
123    /// Market capitalization.
124    #[serde(rename = "marketCap")]
125    pub market_cap: Option<f64>,
126    /// Enterprise value.
127    #[serde(rename = "enterpriseValue")]
128    pub enterprise_value: Option<f64>,
129    /// PE ratio.
130    #[serde(rename = "peRatio")]
131    pub pe_ratio: Option<f64>,
132    /// PB ratio.
133    #[serde(rename = "pbRatio")]
134    pub pb_ratio: Option<f64>,
135    /// EV to sales.
136    #[serde(rename = "evToSales")]
137    pub ev_to_sales: Option<f64>,
138    /// EV to EBITDA (enterprise value multiple).
139    #[serde(rename = "enterpriseValueOverEBITDA")]
140    pub enterprise_value_over_ebitda: Option<f64>,
141    /// EV to operating cash flow.
142    #[serde(rename = "evToOperatingCashFlow")]
143    pub ev_to_operating_cash_flow: Option<f64>,
144    /// EV to free cash flow.
145    #[serde(rename = "evToFreeCashFlow")]
146    pub ev_to_free_cash_flow: Option<f64>,
147    /// Earnings yield.
148    #[serde(rename = "earningsYield")]
149    pub earnings_yield: Option<f64>,
150    /// Free cash flow yield.
151    #[serde(rename = "freeCashFlowYield")]
152    pub free_cash_flow_yield: Option<f64>,
153    /// Debt to equity.
154    #[serde(rename = "debtToEquity")]
155    pub debt_to_equity: Option<f64>,
156    /// Debt to assets.
157    #[serde(rename = "debtToAssets")]
158    pub debt_to_assets: Option<f64>,
159    /// Net debt to EBITDA.
160    #[serde(rename = "netDebtToEBITDA")]
161    pub net_debt_to_ebitda: Option<f64>,
162    /// Current ratio.
163    #[serde(rename = "currentRatio")]
164    pub current_ratio: Option<f64>,
165    /// Dividend yield.
166    #[serde(rename = "dividendYield")]
167    pub dividend_yield: Option<f64>,
168    /// Payout ratio.
169    #[serde(rename = "payoutRatio")]
170    pub payout_ratio: Option<f64>,
171}
172
173/// Enterprise value from FMP.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[non_exhaustive]
176pub struct EnterpriseValue {
177    /// Ticker symbol.
178    pub symbol: Option<String>,
179    /// Date.
180    pub date: Option<String>,
181    /// Stock price.
182    #[serde(rename = "stockPrice")]
183    pub stock_price: Option<f64>,
184    /// Number of shares.
185    #[serde(rename = "numberOfShares")]
186    pub number_of_shares: Option<f64>,
187    /// Market capitalization.
188    #[serde(rename = "marketCapitalization")]
189    pub market_capitalization: Option<f64>,
190    /// Minus cash and cash equivalents.
191    #[serde(rename = "minusCashAndCashEquivalents")]
192    pub minus_cash_and_cash_equivalents: Option<f64>,
193    /// Add total debt.
194    #[serde(rename = "addTotalDebt")]
195    pub add_total_debt: Option<f64>,
196    /// Enterprise value.
197    #[serde(rename = "enterpriseValue")]
198    pub enterprise_value: Option<f64>,
199}
200
201/// Discounted cash flow valuation from FMP.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[non_exhaustive]
204pub struct DiscountedCashFlow {
205    /// Ticker symbol.
206    pub symbol: Option<String>,
207    /// Date.
208    pub date: Option<String>,
209    /// DCF value.
210    pub dcf: Option<f64>,
211    /// Stock price.
212    #[serde(rename = "Stock Price")]
213    pub stock_price: Option<f64>,
214}
215
216/// Historical discounted cash flow from FMP.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[non_exhaustive]
219pub struct HistoricalDcf {
220    /// Ticker symbol.
221    pub symbol: Option<String>,
222    /// Date.
223    pub date: Option<String>,
224    /// DCF value.
225    pub dcf: Option<f64>,
226    /// Stock price.
227    pub price: Option<f64>,
228}
229
230/// Company rating from FMP.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232#[non_exhaustive]
233pub struct CompanyRating {
234    /// Ticker symbol.
235    pub symbol: Option<String>,
236    /// Date.
237    pub date: Option<String>,
238    /// Overall rating.
239    pub rating: Option<String>,
240    /// Rating score.
241    #[serde(rename = "ratingScore")]
242    pub rating_score: Option<i32>,
243    /// Rating recommendation (Buy, Sell, etc.).
244    #[serde(rename = "ratingRecommendation")]
245    pub rating_recommendation: Option<String>,
246    /// Rating DCF score.
247    #[serde(rename = "ratingDetailsDCFScore")]
248    pub rating_details_dcf_score: Option<i32>,
249    /// Rating DCF recommendation.
250    #[serde(rename = "ratingDetailsDCFRecommendation")]
251    pub rating_details_dcf_recommendation: Option<String>,
252    /// Rating ROE score.
253    #[serde(rename = "ratingDetailsROEScore")]
254    pub rating_details_roe_score: Option<i32>,
255    /// Rating ROE recommendation.
256    #[serde(rename = "ratingDetailsROERecommendation")]
257    pub rating_details_roe_recommendation: Option<String>,
258    /// Rating ROA score.
259    #[serde(rename = "ratingDetailsROAScore")]
260    pub rating_details_roa_score: Option<i32>,
261    /// Rating ROA recommendation.
262    #[serde(rename = "ratingDetailsROARecommendation")]
263    pub rating_details_roa_recommendation: Option<String>,
264    /// Rating DE score.
265    #[serde(rename = "ratingDetailsDEScore")]
266    pub rating_details_de_score: Option<i32>,
267    /// Rating DE recommendation.
268    #[serde(rename = "ratingDetailsDERecommendation")]
269    pub rating_details_de_recommendation: Option<String>,
270    /// Rating PE score.
271    #[serde(rename = "ratingDetailsPEScore")]
272    pub rating_details_pe_score: Option<i32>,
273    /// Rating PE recommendation.
274    #[serde(rename = "ratingDetailsPERecommendation")]
275    pub rating_details_pe_recommendation: Option<String>,
276    /// Rating PB score.
277    #[serde(rename = "ratingDetailsPBScore")]
278    pub rating_details_pb_score: Option<i32>,
279    /// Rating PB recommendation.
280    #[serde(rename = "ratingDetailsPBRecommendation")]
281    pub rating_details_pb_recommendation: Option<String>,
282}
283
284/// Financial growth metrics from FMP.
285#[derive(Debug, Clone, Serialize, Deserialize)]
286#[non_exhaustive]
287pub struct FinancialGrowth {
288    /// Ticker symbol.
289    pub symbol: Option<String>,
290    /// Date.
291    pub date: Option<String>,
292    /// Calendar year.
293    #[serde(rename = "calendarYear")]
294    pub calendar_year: Option<String>,
295    /// Fiscal period.
296    pub period: Option<String>,
297    /// Revenue growth.
298    #[serde(rename = "revenueGrowth")]
299    pub revenue_growth: Option<f64>,
300    /// Gross profit growth.
301    #[serde(rename = "grossProfitGrowth")]
302    pub gross_profit_growth: Option<f64>,
303    /// EBITDA growth.
304    #[serde(rename = "ebitgrowth")]
305    pub ebit_growth: Option<f64>,
306    /// Operating income growth.
307    #[serde(rename = "operatingIncomeGrowth")]
308    pub operating_income_growth: Option<f64>,
309    /// Net income growth.
310    #[serde(rename = "netIncomeGrowth")]
311    pub net_income_growth: Option<f64>,
312    /// EPS growth.
313    #[serde(rename = "epsgrowth")]
314    pub eps_growth: Option<f64>,
315    /// EPS diluted growth.
316    #[serde(rename = "epsdilutedGrowth")]
317    pub eps_diluted_growth: Option<f64>,
318    /// Weighted average shares growth.
319    #[serde(rename = "weightedAverageSharesGrowth")]
320    pub weighted_average_shares_growth: Option<f64>,
321    /// Weighted average shares diluted growth.
322    #[serde(rename = "weightedAverageSharesDilutedGrowth")]
323    pub weighted_average_shares_diluted_growth: Option<f64>,
324    /// Dividend per share growth.
325    #[serde(rename = "dividendsperShareGrowth")]
326    pub dividends_per_share_growth: Option<f64>,
327    /// Operating cash flow growth.
328    #[serde(rename = "operatingCashFlowGrowth")]
329    pub operating_cash_flow_growth: Option<f64>,
330    /// Free cash flow growth.
331    #[serde(rename = "freeCashFlowGrowth")]
332    pub free_cash_flow_growth: Option<f64>,
333    /// Receivables growth.
334    #[serde(rename = "receivablesGrowth")]
335    pub receivables_growth: Option<f64>,
336    /// Inventory growth.
337    #[serde(rename = "inventoryGrowth")]
338    pub inventory_growth: Option<f64>,
339    /// Asset growth.
340    #[serde(rename = "assetGrowth")]
341    pub asset_growth: Option<f64>,
342    /// Book value per share growth.
343    #[serde(rename = "bookValueperShareGrowth")]
344    pub book_value_per_share_growth: Option<f64>,
345    /// Debt growth.
346    #[serde(rename = "debtGrowth")]
347    pub debt_growth: Option<f64>,
348    /// R&D expense growth.
349    #[serde(rename = "rdexpenseGrowth")]
350    pub rd_expense_growth: Option<f64>,
351    /// SGA expenses growth.
352    #[serde(rename = "sgaexpensesGrowth")]
353    pub sga_expenses_growth: Option<f64>,
354}
355
356// ============================================================================
357// Query functions
358// ============================================================================
359
360/// Fetch financial ratios for a symbol.
361pub async fn financial_ratios(
362    symbol: &str,
363    period: Period,
364    limit: Option<u32>,
365) -> Result<Vec<FinancialRatios>> {
366    let client = super::build_client()?;
367    let limit_str = limit.unwrap_or(4).to_string();
368    client
369        .get(
370            &format!("/api/v3/ratios/{}", encode_path_segment(symbol)),
371            &[("period", period.as_str()), ("limit", &limit_str)],
372        )
373        .await
374}
375
376/// Fetch key metrics for a symbol.
377pub async fn key_metrics(
378    symbol: &str,
379    period: Period,
380    limit: Option<u32>,
381) -> Result<Vec<KeyMetrics>> {
382    let client = super::build_client()?;
383    let limit_str = limit.unwrap_or(4).to_string();
384    client
385        .get(
386            &format!("/api/v3/key-metrics/{}", encode_path_segment(symbol)),
387            &[("period", period.as_str()), ("limit", &limit_str)],
388        )
389        .await
390}
391
392/// Fetch enterprise values for a symbol.
393pub async fn enterprise_value(
394    symbol: &str,
395    period: Period,
396    limit: Option<u32>,
397) -> Result<Vec<EnterpriseValue>> {
398    let client = super::build_client()?;
399    let limit_str = limit.unwrap_or(4).to_string();
400    client
401        .get(
402            &format!("/api/v3/enterprise-values/{}", encode_path_segment(symbol)),
403            &[("period", period.as_str()), ("limit", &limit_str)],
404        )
405        .await
406}
407
408/// Fetch current discounted cash flow valuation for a symbol.
409pub async fn discounted_cash_flow(symbol: &str) -> Result<Vec<DiscountedCashFlow>> {
410    let client = super::build_client()?;
411    client
412        .get(
413            &format!(
414                "/api/v3/discounted-cash-flow/{}",
415                encode_path_segment(symbol)
416            ),
417            &[],
418        )
419        .await
420}
421
422/// Fetch historical discounted cash flow for a symbol.
423pub async fn historical_dcf(
424    symbol: &str,
425    period: Period,
426    limit: Option<u32>,
427) -> Result<Vec<HistoricalDcf>> {
428    let client = super::build_client()?;
429    let limit_str = limit.unwrap_or(10).to_string();
430    client
431        .get(
432            &format!(
433                "/api/v3/historical-discounted-cash-flow-statement/{}",
434                encode_path_segment(symbol)
435            ),
436            &[("period", period.as_str()), ("limit", &limit_str)],
437        )
438        .await
439}
440
441/// Fetch company rating for a symbol.
442pub async fn company_rating(symbol: &str) -> Result<Vec<CompanyRating>> {
443    let client = super::build_client()?;
444    client
445        .get(
446            &format!("/api/v3/rating/{}", encode_path_segment(symbol)),
447            &[],
448        )
449        .await
450}
451
452/// Fetch historical ratings for a symbol.
453pub async fn historical_rating(symbol: &str, limit: Option<u32>) -> Result<Vec<CompanyRating>> {
454    let client = super::build_client()?;
455    let limit_str = limit.unwrap_or(100).to_string();
456    client
457        .get(
458            &format!("/api/v3/historical-rating/{}", encode_path_segment(symbol)),
459            &[("limit", &limit_str)],
460        )
461        .await
462}
463
464/// Fetch financial growth metrics for a symbol.
465pub async fn financial_growth(
466    symbol: &str,
467    period: Period,
468    limit: Option<u32>,
469) -> Result<Vec<FinancialGrowth>> {
470    let client = super::build_client()?;
471    let limit_str = limit.unwrap_or(4).to_string();
472    client
473        .get(
474            &format!("/api/v3/financial-growth/{}", encode_path_segment(symbol)),
475            &[("period", period.as_str()), ("limit", &limit_str)],
476        )
477        .await
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[tokio::test]
485    async fn test_financial_ratios_mock() {
486        let mut server = mockito::Server::new_async().await;
487        let _mock = server
488            .mock("GET", "/api/v3/ratios/AAPL")
489            .match_query(mockito::Matcher::AllOf(vec![
490                mockito::Matcher::UrlEncoded("apikey".into(), "test-key".into()),
491                mockito::Matcher::UrlEncoded("period".into(), "annual".into()),
492                mockito::Matcher::UrlEncoded("limit".into(), "1".into()),
493            ]))
494            .with_status(200)
495            .with_body(
496                serde_json::json!([{
497                    "symbol": "AAPL",
498                    "date": "2024-09-28",
499                    "calendarYear": "2024",
500                    "period": "FY",
501                    "currentRatio": 0.8673,
502                    "quickRatio": 0.8268,
503                    "grossProfitMargin": 0.4623,
504                    "netProfitMargin": 0.2395,
505                    "returnOnEquity": 1.6067,
506                    "priceEarningsRatio": 34.12,
507                    "dividendYield": 0.0044
508                }])
509                .to_string(),
510            )
511            .create_async()
512            .await;
513
514        let client = super::super::build_test_client(&server.url()).unwrap();
515        let result: Vec<FinancialRatios> = client
516            .get(
517                "/api/v3/ratios/AAPL",
518                &[("period", "annual"), ("limit", "1")],
519            )
520            .await
521            .unwrap();
522
523        assert_eq!(result.len(), 1);
524        assert_eq!(result[0].symbol.as_deref(), Some("AAPL"));
525        assert_eq!(result[0].gross_profit_margin, Some(0.4623));
526        assert_eq!(result[0].price_earnings_ratio, Some(34.12));
527    }
528
529    #[tokio::test]
530    async fn test_company_rating_mock() {
531        let mut server = mockito::Server::new_async().await;
532        let _mock = server
533            .mock("GET", "/api/v3/rating/AAPL")
534            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
535                "apikey".into(),
536                "test-key".into(),
537            )]))
538            .with_status(200)
539            .with_body(
540                serde_json::json!([{
541                    "symbol": "AAPL",
542                    "date": "2024-12-01",
543                    "rating": "S",
544                    "ratingScore": 5,
545                    "ratingRecommendation": "Strong Buy",
546                    "ratingDetailsDCFScore": 5,
547                    "ratingDetailsDCFRecommendation": "Strong Buy",
548                    "ratingDetailsROEScore": 5,
549                    "ratingDetailsROERecommendation": "Strong Buy"
550                }])
551                .to_string(),
552            )
553            .create_async()
554            .await;
555
556        let client = super::super::build_test_client(&server.url()).unwrap();
557        let result: Vec<CompanyRating> = client.get("/api/v3/rating/AAPL", &[]).await.unwrap();
558
559        assert_eq!(result.len(), 1);
560        assert_eq!(result[0].rating.as_deref(), Some("S"));
561        assert_eq!(result[0].rating_score, Some(5));
562        assert_eq!(
563            result[0].rating_recommendation.as_deref(),
564            Some("Strong Buy")
565        );
566    }
567}