spread_tracker/
lib.rs

1//! # Xylex spread tracker
2//!
3//! `spread_tracker` is a library for tracking the spread of various symbols in the forex market.
4//!
5//! Every forex broker has a different spread for each symbol. This library is used to track the spread of various symbols in the forex market.
6//! Retrieve & scrape the spread of various symbols from different brokers.
7//!
8//!
9//!
10//! ### Features
11//! - Track the spread of various symbols in the forex market.
12//! - Retrieve & scrape the spread of various symbols from different brokers.
13//! - Store the spread data in a database.
14//! - Cache the spread data to minimize the number of requests to the broker.
15//!
16//! ### Granularity
17//! - Query the spread of all symbols from all listed brokers.
18//! - Query the spread of a specific symbol from all listed brokers.
19//! - Query the spread of all symbols from a specific broker.
20//! - Query the spread of a specific symbol from a specific broker.
21//!
22//!
23//! ### Usage
24//! Add this to your `Cargo.toml`:
25//! ```toml
26//! [dependencies]
27//! spread_tracker = "0.1.0"
28//! ```
29//!
30//! ### How to assign the correct Broker type
31//! ```rust
32//! use spread_tracker::config::{
33//!     SpreadBrokerUrl,
34//!     Brokers
35//! };
36//! ```
37//!
38//! ### How to get their spreads
39//!
40//!
41//! <div class="warning">
42//! This is currently strictly available for asynchronous context!
43//! </div>
44//!
45//!
46//! ```rust
47//! use spread_tracker::config::SpreadBrokerUrl;
48//! use spread_tracker::model::{
49//!     Symbol,
50//!     SymbolSpread
51//! };
52//!
53//! let config: SpreadBrokerUrl = SpreadBrokerUrl::new();
54//!
55//! // In this example, we are tracking the spread of the symbols from the Vantage and EightCap brokers.
56//! let brokers: Vec<Brokers> = vec![
57//!     Brokers::Vantage,
58//!     Brokers::EightCap,
59//! ];
60//!
61//! // Get the spread of the symbols from the brokers
62//! let spread: Value = SpreadTracker::get_spread(
63//!     config,
64//!     brokers
65//! ).await.unwrap();
66//!
67//! println!("Spread: {:#?}", spread);
68//!
69//! Result:
70//!
71//! {
72//!  "spread": {
73//!    "eightcap": [
74//!      {
75//!        "ask": 2197.92,
76//!        "bid": 2209.92,
77//!        "spread": 12.0,
78//!        "symbol": "XAUUSD"
79//!      },
80//!      {
81//!        "ask": 2031.57,
82//!        "bid": 2060.8973,
83//!        "spread": 29.3273,
84//!        "symbol": "XAUEUR"
85//!      },
86//!      {
87//! ---------------- cut for brevity ----------------
88//! ```
89//! ### Return type
90//! The return type is a `serde_json::Value` object, which is a JSON object.
91//!
92//! #### Structure
93//! Object[spread]Vector[broker] -> Object[symbol, ask, bid, spread]
94//!
95//! * `spread` is the key for the spread data.
96//! * `broker` is the key for the broker name, which will differ based on the broker.
97//!
98//! - Notes:
99//! A Vector of objects is sometimes referred to as a list of objects. It is a collection of objects that are stored in no particular order.
100//!
101//! ### Configuration
102//! The `config.yaml` is used to store the broker URLs for the spread tracking, and the `model.rs` is used to store the data model for the spread tracking. The `config.yaml` file should be in the following format:
103//! ```yaml
104//! BrokerSpreadUrls:
105//!     Vantage: "https://www.vantagefx.com/trading-info/forex-market-hours/"
106//!     MyFxBook: "https://www.myfxbook.com/forex-broker-quotes/vantage/6052"
107//! ```
108//!
109//! Where `Vantage` and `MyFxBook` are the broker names and the URLs are the URLs of the brokers for spread tracking, derived from the MyFxBook website.
110//!
111//!
112//!
113//! ### Caching
114//! wip
115//! You can use the `db` module to store the spread data in a database.
116//! Currently, the library relies on `supabase_rs` to store the data in a Supabase database.
117//!
118//! If you want to use a different database, you can implement the `Db` trait for your database.
119//! Or simply call the `store_spread` method in the `model.rs` file to store the spread data in your database via a manual file like `.json`
120//!
121//!
122//! ### Asynchronous vs Synchronous API
123//! The main library is already asynchronous, so you can use it in an asynchronous context.
124//! Sync API is **not** due to more testing being needed
125//!
126//!
127//!
128//! ### Database
129//! wip
130//! You can use the `db` module to store the spread data in a database.
131//! Currently, the library relies on `supabase_rs` to store the data in a Supabase database.
132//!
133//! If you want to use a different database, you can implement the `Db` trait for your database.
134//! Or simply call the `store_spread` method in the `model.rs` file to store the spread data in your database via a manual file like `.json`
135//!
136//! ### Errors
137//!
138//! <div class="warning">
139//! Custom type errors are not yet implemented!
140//! </div>
141//!
142//! * `url_invalid` will be returned if the URL is invalid.
143//! * `url_not_reachable` will be returned if the URL is not reachable.
144//! * `url_not_found` will be returned if the URL is not found.
145//! * `invalid_body` will be returned if the body is invalid.
146//! * `internal_error` will be returned if there is an internal error.
147//! * `empty_body` will be returned if the body is empty.
148//! * `could_not_extract_broker_name` will be returned if the broker name could not be extracted from the URL.
149//! * `failed_to_open_config` will be returned if the config.yaml file is not found.
150//! * `failed_to_read_yaml` will be returned if the file is not in the correct format.
151//! * `failed_to_parse_symbol_spread` will be returned if the Vec<String> could not be parsed into a Vec<SymbolSpread>.
152//! * `failed_to_parse_symbol` will be returned if the Symbol could not be parsed from the string.
153//! * `failed_to_parse_ask_price` will be returned if the ask price could not be parsed from the string.
154//! * `failed_to_parse_bid_price` will be returned if the bid price could not be parsed from the string.
155//! * `failed_to_parse_spread` will be returned if the spread could not be parsed from the string.
156//! * `could_not_retrieve_spread_data` will be returned if the spread data could not be retrieved from the broker URL.
157//! * `failed_to_bind_json_object_to_key` will be returned if the JSON object could not be bound to the key.
158//!
159//!
160//!
161//! ### Modules
162//! - `db`: This module is used to store the spread data in a database.
163//! - `caching`: This module is used to cache the spread data to minimize the number of requests to the broker.
164//! - `utils`: This module is used to save the spread data to a `.json` file.
165//! - `config`: This module is used to load the configuration from the `config.yaml` file.
166//! - `model`: This module is used to store the data model for the spread tracking.
167//! - `errors`: This module is used to handle the errors in the library.
168//!
169#![doc(
170    html_logo_url = "https://storage.googleapis.com/xylex_images/Svg/svgviewer-output%20(1).svg",
171    html_favicon_url = "https://storage.googleapis.com/xylex_images/Svg/svgviewer-output%20(1).svg"
172)]
173
174
175#![allow(unused_imports)]
176#![allow(dead_code)]
177#![allow(rustdoc::invalid_rust_codeblocks)]
178#![allow(rustdoc::invalid_html_tags)]
179
180// import the necessary modules into the hierarchy
181pub mod caching;
182pub mod db;
183pub mod utils;
184pub mod config;
185pub mod model;
186pub mod errors;
187
188
189
190
191// import the necessary external crates into the hierarchy
192use std::fs::File;
193use std::io::Write;
194use std::error::Error as StdError;
195use reqwest::get;
196use serde_json::{
197    Value,
198    Map
199};
200
201
202// import the necessary modules into the hierarchy
203use crate::utils::regex_finder::find_symbol_spread;
204use crate::model::{
205    Symbol,
206    SymbolSpread
207};
208use crate::config::{
209    SpreadBrokerUrl,
210    Brokers
211};
212use crate::utils::format::{
213    wrap_json_under_key,
214    vec_to_json,
215    extract_broker_name
216};
217
218
219
220
221/// ### The `SpreadTracker` struct is used to track the spread of various symbols in the forex market.
222///
223/// This struct is used to track the spread of various symbols in the forex market.
224///
225pub struct SpreadTracker {
226    spread_broker_url: SpreadBrokerUrl,
227    spread: SymbolSpread
228}
229
230
231impl SpreadTracker {
232    /// The `get_spreads` function is used to get the spread of various symbols from the broker URL.
233    ///
234    /// This function is used to get the spread of various symbols from the broker URL.
235    ///
236    /// ### Example
237    ///
238    /// ```
239    /// use spread_tracker::SpreadTracker;
240    /// use spread_tracker::config::SpreadBrokerUrl;
241    ///
242    /// let result = SpreadTracker::get_spreads(SpreadBrokerUrl::new());
243    ///
244    /// println!("Result: {:#?}", result);
245    /// Output:
246    /// {
247    ///    "ask": Number(0.90451),
248    ///    "bid": Number(1.37551),
249    ///    "spread": Number(0.471),
250    ///    "symbol": String("USDCHF"),
251    /// });
252    /// ```
253    ///
254    /// ### Errors
255    /// `url_not_reachable` will be returned if the URL is not reachable.
256    /// `url_not_found` will be returned if the URL is not found.
257    /// `invalid_url` will be returned if the URL is invalid.
258    /// `invalid_body` will be returned if the body is invalid.
259    /// `internal_error` will be returned if there is an internal error.
260    ///
261    pub async fn get_spread(
262        config: SpreadBrokerUrl,
263        brokers: Vec<Brokers>
264    ) -> Result<Value, Box<dyn StdError + Send + Sync + 'static>> {
265
266        let mut all_broker_spreads: Map<String, Value> = serde_json::Map::new();
267
268        for broker in brokers {
269            let url: String = config.get_url(broker.clone());
270            let url: &str = url.as_str();
271
272            let name: String = extract_broker_name(url).unwrap_or_else(|_| broker.to_string());
273
274            let spread_tracker: Result<String, Box<dyn StdError + Send + Sync>> = SpreadTracker::download_html_body(url).await;
275
276
277            if let Ok(body) = spread_tracker {
278                if let Ok(results) = SpreadTracker::regex_find_symbol_spread(&body).await {
279                    if let Ok(json_output) = vec_to_json(results) {
280
281                        // Wrap the JSON output under the broker's name
282                        all_broker_spreads.insert(name, json_output);
283                    }
284                }
285            }
286        }
287
288        // Wrap all broker spreads under the key "spread"
289        let wrapped_all_broker_spreads: Value = serde_json::json!({ "spread": all_broker_spreads });
290
291        Ok(wrapped_all_broker_spreads)
292    }
293
294    /// The `download_html_body` function is used to download the HTML body from the URL.
295    /// This function is used to download the HTML body from the URL.
296    ///
297    /// ### Example
298    ///
299    /// ```
300    /// use spread_tracker::SpreadTracker;
301    ///
302    /// let url = "https://www.vantagefx.com/trading-info/forex-market-hours/";
303    ///
304    /// let body = SpreadTracker::download_html_body(url);
305    ///
306    /// assert_eq!(body, "EURUSD 0.0001 1.1234 1.1233");
307    /// ```
308    pub async fn download_html_body(
309        url: &str
310    ) -> Result<String, Box<dyn StdError + Send + Sync + 'static>> {
311
312        // Download the HTML body
313        let response = get(url).await;
314        if response.is_err() {
315            return Err(Box::new(response.err().unwrap()));
316        }
317        let text = response.unwrap().text().await;
318        if text.is_err() {
319            return Err(Box::new(text.err().unwrap()));
320        }
321        let body: String = text.unwrap();
322
323        let file_result = File::create("body.txt");
324        if file_result.is_err() {
325            return Err(Box::new(file_result.err().unwrap()));
326        }
327        let mut file: File = file_result.unwrap();
328        let write_result = write!(file, "{}", body);
329        if write_result.is_err() {
330            return Err(Box::new(write_result.err().unwrap()));
331        }
332
333        Ok(body)
334    }
335
336    /// The `regex_find_symbol_spread` function is used to find the symbol spread in the HTML body.
337    /// This function is used to find the symbol spread in the HTML body.
338    ///
339    /// ### Example
340    ///
341    /// ```
342    /// use spread_tracker::SpreadTracker;
343    ///
344    /// let body = "<span></span> 1.1234 1.1233 0.0001Spread";
345    ///
346    /// let spread = SpreadTracker::regex_find_symbol_spread(& body);
347    ///
348    /// assert_eq!(spread, vec!["1.1234 1.1233 0.0001Spread"]);
349    ///
350    /// ```
351    ///
352    /// ### Errors
353    /// `empty_body` will be returned if the body is empty.
354    ///
355    pub async fn regex_find_symbol_spread(
356        body: &str
357    ) -> Result<Vec<String>, Box<dyn StdError + Send + Sync + 'static>>{
358        let results: Vec<String> = find_symbol_spread(body);
359
360
361        Ok(results)
362    }
363
364
365}