gurufocus_api/
lib.rs

1//! # GuruFocus API
2//!
3//! This project provides a set of functions to receive data from the
4//! the guru focus website via the [GuruFocus API](https://www.gurufocus.com/api.php).
5//!
6//! # Usage
7//! Please note that you need at least a premium account to use this API. There a couple of
8//! examples demonstrating how to use the API in your own rust projects. To run this example,
9//! you first need to define an environment variable holding the user Token you got from
10//! GuruFocus:
11//! ```bash
12//! export GURUFOCUS_TOKEN='<your user token>'
13//! ```
14//!
15//! The examples can be executed via the command
16//! ```dummy
17//! cargo test --example <name of example>
18//! ```
19//! Here, `<name of example>` could be the name of any of the files in the examples folder
20//! without the `.rs` extension
21//! Please note that running any of the examples increases your API access counter by at least 1.
22//!
23//! The GuruFocus API provides all data in JSON format, and the basic API functions currently
24//! will just return these JSON structures as `serde_json::Value` types without any further
25//! processing. The `serde_json::Value` types can be deserialized
26//! into more meaningful data structures, as is demonstrated in the `gurulist` example.
27//!
28//! The GuruFocus API returns numbers sometimes as numbers, sometimes as strings. This is dealt
29//! with by introducing a new struct `FloatOrString` containing a float value, but which can
30//! be read from either a string or float automatically. The drawback is that `.0` as to be
31//! added to the variable name of a specific data structure. I.e., to access the quoted price
32//! in a variable of type Quote, i.e. `q: Quote`, the price can be accessed via `q.price.0` instead
33//! of `q.price`. In a few cases, the string contains not a number, but an error message, like
34//! "Negative Tangible Equity". In such cases, if the string can not be parsed to a number, the
35//! value is set to `NAN`.
36//!
37//! Since version 0.4, all requests using the ```async``` attribute, returning a Future instead of
38//! waiting for the response and returning the result. To get the actual results, ```.await``` or ```block_on```
39//! or something similar needs to be used. The examples demonstrate how the library could be used.
40//!
41//! Please note that the library is not yet stable and that the user interface is still subject to change.
42//! However, feedback regarding the usability and suggestions for improving the interface are welcome.
43
44use serde_json::{self, Value};
45use thiserror::Error;
46
47/// Special types for dealing with Gurus.
48pub mod gurus;
49
50/// Special types for dealing with stocks.
51pub mod stock;
52pub use stock::*;
53
54/// Special types for dealing with financial data.
55pub mod financials;
56pub use financials::*;
57
58/// Special types for key ratios.
59pub mod keyratios;
60pub use keyratios::*;
61
62/// Special types for insider tradingey ratios.
63pub mod insiders;
64pub use insiders::*;
65
66/// Special types for user portfolio.
67pub mod portfolio;
68pub use portfolio::*;
69
70/// Module for special string / number derserializer
71pub mod strnum;
72
73/// Module for special hex num derserializer
74pub mod hexnum;
75
76#[derive(Error, Debug)]
77pub enum GuruFocusError {
78    #[error("Request failure")]
79    RequestFailure(#[from] reqwest::Error),
80}
81
82/// Container for connection parameters to gurufocus server.
83pub struct GuruFocusConnector {
84    url: &'static str,
85    user_token: String,
86}
87
88impl GuruFocusConnector {
89    /// Constructor for a new instance of GuruFocusConnector.
90    /// token is the user token you get from gurufocus if you subscribe for
91    /// a premium or premium plus account.
92    pub fn new(token: String) -> GuruFocusConnector {
93        GuruFocusConnector {
94            url: "https://api.gurufocus.com/public/user/",
95            user_token: token,
96        }
97    }
98
99    /// Returns the full history of financial data for stock symbol given as argument
100    pub async fn get_financials(&self, stock: &str) -> Result<Value, GuruFocusError> {
101        let args = format!("stock/{}/financials", stock);
102        self.send_request(args.as_str()).await
103    }
104
105    /// Returns the current key statistic figures for stock symbol given as argument
106    pub async fn get_key_ratios(&self, stock: &str) -> Result<Value, GuruFocusError> {
107        let args = format!("stock/{}/keyratios", stock);
108        self.send_request(args.as_str()).await
109    }
110
111    /// Returns the current quote data of a comma separated list of symbols given as argument
112    pub async fn get_quotes(&self, stocks: &[&str]) -> Result<Value, GuruFocusError> {
113        let args = format!("stock/{}/quote", compact_list(stocks));
114        self.send_request(args.as_str()).await
115    }
116
117    /// Returns the history of (adjusted) quoted prices for symbol given as argument
118    pub async fn get_price_hist(&self, stock: &str) -> Result<Value, GuruFocusError> {
119        let args = format!("stock/{}/price", stock);
120        self.send_request(args.as_str()).await
121    }
122
123    /// Returns the history of (unadjusted) quoted prices for symbol given as argument
124    pub async fn get_unadj_price_hist(&self, stock: &str) -> Result<Value, GuruFocusError> {
125        let args = format!("stock/{}/unadjusted_price", stock);
126        self.send_request(args.as_str()).await
127    }
128
129    /// Returns companies current price, valuation rations and ranks for symbol given as argument
130    pub async fn get_stock_summary(&self, stock: &str) -> Result<Value, GuruFocusError> {
131        let args = format!("stock/{}/summary", stock);
132        self.send_request(args.as_str()).await
133    }
134
135    /// Returns real-time guru trades and holding data for symbol given as argument
136    pub async fn get_guru_trades(&self, stock: &str) -> Result<Value, GuruFocusError> {
137        let args = format!("stock/{}/gurus", stock);
138        self.send_request(args.as_str()).await
139    }
140
141    /// Returns real-time insider trades for symbol given as argument
142    pub async fn get_insider_trades(&self, stock: &str) -> Result<Value, GuruFocusError> {
143        let args = format!("stock/{}/insider", stock);
144        self.send_request(args.as_str()).await
145    }
146
147    /// Returns lists of all and personalized gurus
148    pub async fn get_gurus(&self) -> Result<Value, GuruFocusError> {
149        self.send_request("gurulist").await
150    }
151
152    /// Returns list of gurus stock picks using list of guru ids since a given start date.
153    pub async fn get_guru_picks(
154        &self,
155        gurus: &[&str],
156        start_date: time::Date,
157        page: i32,
158    ) -> Result<Value, GuruFocusError> {
159        let args = format!("guru/{}/picks/{}/{}", compact_list(gurus), start_date, page);
160        self.send_request(args.as_str()).await
161    }
162
163    /// Returns list of aggregated guru portfolios given a slice of guru ids
164    pub async fn get_guru_portfolios(&self, gurus: &[&str]) -> Result<Value, GuruFocusError> {
165        let args = format!("guru/{}/aggregated", compact_list(gurus));
166        self.send_request(args.as_str()).await
167    }
168
169    /// Returns list of supported exchanges
170    pub async fn get_exchanges(&self) -> Result<Value, GuruFocusError> {
171        self.send_request("exchange_list").await
172    }
173
174    /// Returns list of all stocks of a particular exchange
175    pub async fn get_listed_stocks(&self, exchange: &str) -> Result<Value, GuruFocusError> {
176        let args = format!("exchange_stocks/{}", exchange);
177        self.send_request(args.as_str()).await
178    }
179
180    /// Returns list of latest insider trades ordered by insider transctions time
181    pub async fn get_insider_updates(&self) -> Result<Value, GuruFocusError> {
182        self.send_request("insider_updates").await
183    }
184
185    /// Returns 30 years dividend history data of a stock
186    pub async fn get_dividend_history(&self, stock: &str) -> Result<Value, GuruFocusError> {
187        let args = format!("stock/{}/dividend", stock);
188        self.send_request(args.as_str()).await
189    }
190
191    /// Returns analyst estimate data of a stock
192    pub async fn get_analyst_estimate(&self, stock: &str) -> Result<Value, GuruFocusError> {
193        let args = format!("stock/{}/analyst_estimate ", stock);
194        self.send_request(args.as_str()).await
195    }
196
197    /// Returns list of personal portfolios
198    pub async fn get_personal_portfolio(&self) -> Result<Value, GuruFocusError> {
199        self.send_request("portfolio/my_portfolios").await
200    }
201
202    /// Returns list of all stocks with updated fundamental data within a week of the given date
203    pub async fn get_updated_stocks(&self, date: time::Date) -> Result<Value, GuruFocusError> {
204        let args = format!("funda_updated/{}", date);
205        self.send_request(args.as_str()).await
206    }
207
208    /// Returns lists of politicians
209    pub async fn get_politicians(&self) -> Result<Value, GuruFocusError> {
210        self.send_request("politicians").await
211    }
212
213    // Returns list of latest politician transactions
214    pub async fn get_politician_transactions(
215        &self,
216        page: u32,
217        asset_type: Option<crate::gurus::AssetType>,
218    ) -> Result<Value, GuruFocusError> {
219        let args = format!(
220            "politicians/transactions?page={page}&asset_type={}",
221            match asset_type {
222                None => "All".to_string(),
223                Some(at) => format!("{at:?}"),
224            }
225        );
226        self.send_request(args.as_str()).await
227    }
228
229    /// Send request to gurufocus server and transform response to JSON value
230    async fn send_request(&self, args: &str) -> Result<Value, GuruFocusError> {
231        let url: String = format!("{}{}/{}", self.url, self.user_token, args);
232        println!("curl -o data.json {url}");
233        let resp = reqwest::get(url.as_str()).await?;
234        Ok(resp.json().await?)
235    }
236}
237
238/// Compact list as input to url
239fn compact_list(a: &[&str]) -> String {
240    if a.is_empty() {
241        return String::new();
242    }
243    let mut it = a.iter();
244    let mut res = it.next().unwrap().to_string();
245    for n in it {
246        res.push_str(&format!(",{}", n));
247    }
248    res
249}
250
251#[cfg(test)]
252mod tests {
253    use std::collections::HashMap;
254    use std::env;
255
256    use super::*;
257    use serde::Deserialize;
258
259    #[test]
260    fn test_compact_list() {
261        assert_eq!(compact_list(&["1", "2", "3"]), "1,2,3");
262        assert_eq!(compact_list(&[]), "");
263        assert_eq!(compact_list(&["3"]), "3");
264    }
265
266    #[derive(Deserialize, Debug)]
267    #[serde(deny_unknown_fields)]
268    struct SimpleStruct {
269        pub value: i32,
270    }
271
272    #[test]
273    fn deserialize_extra_fields() {
274        // correct data
275        let data_correct = r#"
276        {
277            "value": 42
278        }"#;
279
280        let s: SimpleStruct = serde_json::from_str(data_correct).unwrap();
281        assert_eq!(s.value, 42);
282
283        let data_wrong = r#"
284        {
285            "value": 42,
286            "text": "bla"
287        }"#;
288
289        let s: serde_json::Result<SimpleStruct> = serde_json::from_str(data_wrong);
290        // Fails if json has extra fields
291        assert!(s.is_err());
292    }
293
294    #[tokio::test]
295    async fn test_exchanges() {
296        if let Ok(token) = env::var("GURUFOCUS_TOKEN") {
297            if !token.is_empty() {
298                let gf_connect = GuruFocusConnector::new(token);
299                let exchanges = gf_connect.get_exchanges().await;
300                assert!(exchanges.is_ok());
301                let exchange_map =
302                    serde_json::from_value::<HashMap<String, Vec<String>>>(exchanges.unwrap());
303                assert!(exchange_map.is_ok());
304            }
305        }
306    }
307}