diff --git a/Cargo.toml b/Cargo.toml index 7cff2b8..7492ec3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,11 @@ path = "src/lib.rs" default = ["user"] user = ["dep:google-authenticator"] protobuf = ["dep:prost"] +sqlx = [] [dependencies] async-trait = "0.1" # Remove this when Rust 1.75 async trait is stable -tokio = { version = "1", default-features = false } +tokio = { version = "1", default-features = false, features = ["rt", "macros"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } reqwest = { version = "0.12", features = ["rustls-tls", "json", "cookies"] } diff --git a/README.md b/README.md index ad14c6b..9aa126a 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,28 @@ This is a data source library for algorithmic trading written in Rust inspired by [TradingView-API](https://github.com/Mathieu2301/TradingView-API). It is currently in **alpha** stage and not ready for production use. ## Features - +- [x] Async support - [x] Multi-Threading for working with large amounts of data -- [ ] Shared session between threads (Not overwhelming TradingView servers) -- [ ] TradingView Premium features -- [ ] Realtime data +- [x] Shared session between threads (Not overwhelming TradingView servers) +- [x] TradingView Premium features +- [x] Realtime data - [ ] Fundamental data - [ ] Technical data -- [ ] Get drawings you made on your chart +- [x] Get drawings you made on your chart - [ ] Works with invite-only indicators -- [ ] Unlimited simultaneous indicators +- [x] Unlimited simultaneous indicators - [ ] Get TradingView's technical analysis -- [ ] Replay mode +- [x] Replay mode - [ ] Get values from a specific date range - [ ] Interact with public chats - [ ] Get Screener top values - [ ] Get Calendar +- [ ] Get News +- [ ] Convert to Vectorized Data ## Use cases -- [Fenrir Data](https://github.com/bitbytelabio/fenrir-data) - A data engine that applies an event-driven architecture with RedPanda (Kafka), SurrealDB, DragonflyDB (Redis). +- [Fenrir Data](https://github.com/bitbytelabio/fenrir-data) - A data engine that applies an event-driven architecture with RedPanda (Kafka), PostgreSQL with TimeScaleDB, PGVector, DragonflyDB (Redis). ## Getting Started diff --git a/examples/all.rs b/examples/all.rs index 0040580..d482149 100644 --- a/examples/all.rs +++ b/examples/all.rs @@ -2,26 +2,35 @@ use dotenv::dotenv; use std::env; use tradingview::{ - chart, + callback::Callbacks, chart::ChartOptions, - data_loader::DataLoader, - models::{pine_indicator::ScriptType, Interval}, - quote, - socket::{DataServer, SocketSession}, + pine_indicator::ScriptType, + socket::DataServer, + websocket::{WebSocket, WebSocketClient}, + Interval, QuoteValue, }; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv().ok(); tracing_subscriber::fmt::init(); - let auth_token = env::var("TV_AUTH_TOKEN").unwrap(); + let auth_token = env::var("TV_AUTH_TOKEN").expect("TV_AUTH_TOKEN is not set"); - let socket = SocketSession::new(DataServer::ProData, auth_token).await?; + let quote_callback = |data: QuoteValue| async move { + println!("{:#?}", data); + }; - let publisher: DataLoader = DataLoader::default(); + let callbacks = Callbacks::default().on_quote_data(quote_callback); - let mut chart = chart::session::WebSocket::new(publisher.clone(), socket.clone()); - let mut quote = quote::session::WebSocket::new(publisher.clone(), socket.clone()); + let client = WebSocketClient::default().set_callbacks(callbacks); + + let mut websocket = WebSocket::new() + .server(DataServer::ProData) + .auth_token(&auth_token) + .client(client) + .build() + .await + .unwrap(); let opts = ChartOptions::new("BINANCE:BTCUSDT", Interval::OneMinute).bar_count(100); let opts2 = ChartOptions::new("BINANCE:BTCUSDT", Interval::Daily) @@ -35,7 +44,7 @@ async fn main() -> anyhow::Result<()> { .replay_mode(true) .replay_from(1698624060); - chart + websocket .set_market(opts) .await? .set_market(opts2) @@ -43,8 +52,8 @@ async fn main() -> anyhow::Result<()> { .set_market(opts3) .await?; - quote - .create_session() + websocket + .create_quote_session() .await? .set_fields() .await? @@ -57,8 +66,7 @@ async fn main() -> anyhow::Result<()> { ]) .await?; - tokio::spawn(async move { chart.clone().subscribe().await }); - tokio::spawn(async move { quote.clone().subscribe().await }); + tokio::spawn(async move { websocket.subscribe().await }); loop {} } diff --git a/examples/misc.rs b/examples/misc.rs index a55cc0f..be8b7e5 100644 --- a/examples/misc.rs +++ b/examples/misc.rs @@ -1,5 +1,5 @@ use tracing::info; -use tradingview::models::pine_indicator::*; +use tradingview::pine_indicator::*; #[tokio::main] async fn main() { @@ -13,7 +13,8 @@ async fn main() { // info!("{:#?}", info); let pine = PineIndicator::build() - .fetch("STD;Fund_total_revenue_fq", "62.0", ScriptType::Script).await + .fetch("STD;Fund_total_revenue_fq", "62.0", ScriptType::Script) + .await .unwrap(); let test = pine.to_study_inputs().unwrap(); diff --git a/examples/user.rs b/examples/user.rs index bebe905..7a0fbb4 100644 --- a/examples/user.rs +++ b/examples/user.rs @@ -1,5 +1,5 @@ #![cfg(feature = "user")] -use tradingview::user::UserCookies; +use tradingview::UserCookies; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/src/callback.rs b/src/callback.rs index 0b37523..64e2ee4 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -52,9 +52,9 @@ impl Default for Callbacks<'_> { impl<'a> Callbacks<'a> { pub fn on_chart_data( - &mut self, + mut self, f: impl Fn((ChartOptions, Vec)) -> Fut + Send + Sync + 'a, - ) -> &mut Self + ) -> Self where Fut: Future + Send + 'a, { @@ -62,10 +62,7 @@ impl<'a> Callbacks<'a> { self } - pub fn on_quote_data( - &mut self, - f: impl Fn(QuoteValue) -> Fut + Send + Sync + 'a, - ) -> &mut Self + pub fn on_quote_data(mut self, f: impl Fn(QuoteValue) -> Fut + Send + Sync + 'a) -> Self where Fut: Future + Send + 'a, { @@ -74,9 +71,9 @@ impl<'a> Callbacks<'a> { } pub fn on_study_data( - &mut self, + mut self, f: impl Fn((StudyOptions, StudyResponseData)) -> Fut + Send + Sync + 'a, - ) -> &mut Self + ) -> Self where Fut: Future + Send + 'a, { @@ -84,7 +81,7 @@ impl<'a> Callbacks<'a> { self } - pub fn on_error(&mut self, f: impl Fn(Error) -> Fut + Send + Sync + 'a) -> &mut Self + pub fn on_error(mut self, f: impl Fn(Error) -> Fut + Send + Sync + 'a) -> Self where Fut: Future + Send + 'a, { @@ -92,10 +89,7 @@ impl<'a> Callbacks<'a> { self } - pub fn on_symbol_info( - &mut self, - f: impl Fn(SymbolInfo) -> Fut + Send + Sync + 'a, - ) -> &mut Self + pub fn on_symbol_info(mut self, f: impl Fn(SymbolInfo) -> Fut + Send + Sync + 'a) -> Self where Fut: Future + Send + 'a, { @@ -104,9 +98,9 @@ impl<'a> Callbacks<'a> { } pub fn on_other_event( - &mut self, + mut self, f: impl Fn((TradingViewDataEvent, Vec)) -> Fut + Send + Sync + 'a, - ) -> &mut Self + ) -> Self where Fut: Future + Send + 'a, { diff --git a/src/chart/mod.rs b/src/chart/mod.rs index 9597a32..94e2286 100644 --- a/src/chart/mod.rs +++ b/src/chart/mod.rs @@ -4,7 +4,6 @@ use crate::models::{pine_indicator::ScriptType, Interval, MarketAdjustment, Sess pub mod models; pub(crate) mod options; -pub mod session; pub mod study; pub(crate) mod utils; @@ -13,17 +12,17 @@ pub struct ChartOptions { // Required pub symbol: String, pub interval: Interval, - bar_count: u64, + pub(crate) bar_count: u64, - range: Option, - from: Option, - to: Option, - replay_mode: bool, - replay_from: i64, - replay_session: Option, - adjustment: Option, - currency: Option, - session_type: Option, + pub(crate) range: Option, + pub(crate) from: Option, + pub(crate) to: Option, + pub(crate) replay_mode: bool, + pub(crate) replay_from: i64, + pub(crate) replay_session: Option, + pub(crate) adjustment: Option, + pub(crate) currency: Option, + pub(crate) session_type: Option, pub study_config: Option, } diff --git a/src/chart/models.rs b/src/chart/models.rs index 299660e..f15e37f 100644 --- a/src/chart/models.rs +++ b/src/chart/models.rs @@ -51,7 +51,7 @@ pub struct GraphicDataResponse { #[cfg_attr(not(feature = "protobuf"), derive(Debug, Default))] #[cfg_attr(feature = "protobuf", derive(prost::Message))] -#[derive(Clone, Deserialize)] +#[derive(Clone, Deserialize, Serialize, PartialEq)] pub struct DataPoint { #[cfg_attr(feature = "protobuf", prost(int64, tag = "1"))] #[serde(rename(deserialize = "i"))] diff --git a/src/chart/utils.rs b/src/chart/utils.rs index 8b13789..d37a1cc 100644 --- a/src/chart/utils.rs +++ b/src/chart/utils.rs @@ -1 +1,4 @@ - +// TODO: Implement this module +pub fn _graphics_parser() { + unimplemented!() +} diff --git a/src/client/fin_calendar.rs b/src/client/fin_calendar.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/client/fin_calendar.rs @@ -0,0 +1 @@ + diff --git a/src/client/misc.rs b/src/client/misc.rs index 744ab3e..0b7d1ec 100644 --- a/src/client/misc.rs +++ b/src/client/misc.rs @@ -1,22 +1,18 @@ use std::sync::Arc; use crate::{ - models::{ - pine_indicator::{ self, BuiltinIndicators, PineInfo, PineMetadata, PineSearchResult }, - Symbol, - SymbolSearchResponse, - SymbolMarketType, - UserCookies, - ChartDrawing, - }, - utils::build_request, error::Error, - Result, + pine_indicator::{self, BuiltinIndicators, PineInfo, PineMetadata, PineSearchResult}, + utils::build_request, + ChartDrawing, CryptoCentralization, EconomicCategory, EconomicSource, FuturesProductType, + MarketType, Result, StockSector, Symbol, SymbolSearchResponse, UserCookies, }; use reqwest::Response; -use tokio::{ sync::Semaphore, task::JoinHandle }; -use tracing::debug; use serde_json::Value; +use tokio::{sync::Semaphore, task::JoinHandle}; +use tracing::debug; + +static SEARCH_BASE_URL: &str = "https://symbol-search.tradingview.com/symbol_search/v3/"; /// Sends an HTTP GET request to the specified URL using the provided client and returns the response. /// @@ -32,9 +28,7 @@ async fn get(client: Option<&UserCookies>, url: &str) -> Result { if let Some(client) = client { let cookie = format!( "sessionid={}; sessionid_sign={}; device_t={};", - client.session, - client.session_signature, - client.device_token + client.session, client.session_signature, client.device_token ); let client = build_request(Some(&cookie))?; let response = client.get(url).send().await?; @@ -43,6 +37,48 @@ async fn get(client: Option<&UserCookies>, url: &str) -> Result { Ok(build_request(None)?.get(url).send().await?) } +pub async fn search_one_symbol(search: &str, exchange: &str) -> Result { + let search_data = advanced_search_symbol( + search, + exchange, + &MarketType::All, + 0, + None, + None, + None, + None, + None, + None, + None, + ) + .await?; + let symbol = match search_data.symbols.first() { + Some(symbol) => symbol, + None => { + return Err(Error::Generic("No symbol found".to_string())); + } + }; + Ok(symbol.to_owned()) +} + +pub async fn search_symbols(search: &str, exchange: &str) -> Result> { + let search_data = advanced_search_symbol( + search, + exchange, + &MarketType::All, + 0, + None, + None, + None, + None, + None, + None, + None, + ) + .await?; + Ok(search_data.symbols) +} + /// Searches for a symbol using the specified search parameters. /// /// # Arguments @@ -57,27 +93,76 @@ async fn get(client: Option<&UserCookies>, url: &str) -> Result { /// # Returns /// /// A `Result` containing a `SymbolSearchResponse` struct representing the search results, or an error if the search failed. -#[tracing::instrument] -pub async fn search_symbol( +#[allow(clippy::too_many_arguments)] +pub async fn advanced_search_symbol( search: &str, exchange: &str, - market_type: &SymbolMarketType, + market_type: &MarketType, start: u64, - country: &str, - domain: &str + country: Option<&str>, + domain: Option<&str>, + futures_type: Option<&FuturesProductType>, // For Futures Only + stock_sector: Option<&StockSector>, // For Stock Only + crypto_centralization: Option<&CryptoCentralization>, // For Crypto Only + economic_source: Option<&EconomicSource>, // For Economy Only + economic_category: Option<&EconomicCategory>, // For Economy Only ) -> Result { + let mut params: Vec<(String, String)> = Vec::new(); + let domain = domain.unwrap_or("production"); + params.push(("text".to_string(), search.to_string())); + params.push(("exchange".to_string(), exchange.to_string())); + params.push(("search_type".to_string(), market_type.to_string())); + params.push(("domain".to_string(), domain.to_string())); + if let Some(country) = country { + params.push(("country".to_string(), country.to_string())); + params.push(("sort_by_country".to_string(), country.to_string())); + } + match market_type { + MarketType::Futures => { + if let Some(futures_type) = futures_type { + params.push(("product".to_string(), futures_type.to_string())); + } + } + MarketType::Stocks(_) => { + if let Some(stock_sector) = stock_sector { + params.push(("sector".to_string(), stock_sector.to_string())); + } + } + MarketType::Crypto(_) => { + if let Some(crypto_centralization) = crypto_centralization { + params.push(( + "centralization".to_string(), + crypto_centralization.to_string(), + )); + } + } + MarketType::Economy => { + if let Some(economic_source) = economic_source { + params.push(("source_id".to_string(), economic_source.to_string())); + } + if let Some(economic_category) = economic_category { + params.push(( + "economic_category".to_string(), + economic_category.to_string(), + )); + } + } + _ => {} + }; + + let params_str = params + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>() + .join("&"); + let search_data: SymbolSearchResponse = get( None, - &format!( - "https://symbol-search.tradingview.com/symbol_search/v3/?text={search}&country={country}&hl=0&exchange={exchange}&lang=en&search_type={search_type}&start={start}&domain={domain}&sort_by_country={country}", - search = search, - exchange = exchange, - search_type = market_type, - start = start, - country = country, - domain = if domain.is_empty() { "production" } else { domain } - ) - ).await?.json().await?; + &format!("{SEARCH_BASE_URL}?{params_str}&hl=0&lang=en&start={start}"), + ) + .await? + .json() + .await?; Ok(search_data) } @@ -96,27 +181,33 @@ pub async fn search_symbol( #[tracing::instrument] pub async fn list_symbols( exchange: Option, - market_type: Option, + market_type: Option, country: Option, - domain: Option + domain: Option, ) -> Result> { - let market_type: Arc = Arc::new(market_type.unwrap_or_default()); + let market_type: Arc = Arc::new(market_type.unwrap_or_default()); let exchange: Arc = Arc::new(exchange.unwrap_or("".to_string())); let country = Arc::new(country.unwrap_or("".to_string())); - let domain = Arc::new(domain.unwrap_or("".to_string())); + let domain = Arc::new(domain.unwrap_or("production".to_string())); - let search_symbol_reps = search_symbol( + let search_symbol_reps = advanced_search_symbol( "", &exchange, &market_type, 0, - &country, - &domain - ).await?; + Some(&country), + Some(&domain), + None, + None, + None, + None, + None, + ) + .await?; let remaining = search_symbol_reps.remaining; let mut symbols = search_symbol_reps.symbols; - let max_concurrent_tasks = 50; + let max_concurrent_tasks = 30; let semaphore = Arc::new(Semaphore::new(max_concurrent_tasks)); let mut tasks = Vec::new(); @@ -130,9 +221,21 @@ pub async fn list_symbols( let task = tokio::spawn(async move { let _permit = semaphore.acquire().await.unwrap(); - search_symbol("", &exchange, &market_type, i, &country, &domain).await.map( - |resp| resp.symbols + advanced_search_symbol( + "", + &exchange, + &market_type, + i, + Some(&country), + Some(&domain), + None, + None, + None, + None, + None, ) + .await + .map(|resp| resp.symbols) }); tasks.push(task); @@ -161,10 +264,12 @@ pub async fn get_chart_token(client: &UserCookies, layout_id: &str) -> Result { @@ -175,7 +280,7 @@ pub async fn get_chart_token(client: &UserCookies, layout_id: &str) -> Result { Err(Error::NoChartTokenFound) } + None => Err(Error::NoChartTokenFound), } } @@ -190,10 +295,10 @@ pub async fn get_chart_token(client: &UserCookies, layout_id: &str) -> Result Result { - let data: String = get( - Some(client), - "https://www.tradingview.com/quote_token" - ).await?.json().await?; + let data: String = get(Some(client), "https://www.tradingview.com/quote_token") + .await? + .json() + .await?; Ok(data) } @@ -214,17 +319,14 @@ pub async fn get_drawing( client: &UserCookies, layout_id: &str, symbol: &str, - chart_id: Option<&str> + chart_id: Option<&str>, ) -> Result { let token = get_chart_token(client, layout_id).await?; debug!("Chart token: {}", token); let url = format!( "https://charts-storage.tradingview.com/charts-storage/get/layout/{layout_id}/sources?chart_id={chart_id}&jwt={token}&symbol={symbol}", - layout_id = layout_id, chart_id = chart_id.unwrap_or("_shared"), - token = token, - symbol = symbol ); let response_data: ChartDrawing = get(Some(client), &url).await?.json().await?; @@ -236,8 +338,11 @@ pub async fn get_drawing( pub async fn get_private_indicators(client: &UserCookies) -> Result> { let indicators = get( Some(client), - "https://pine-facade.tradingview.com/pine-facade/list?filter=saved" - ).await?.json::>().await?; + "https://pine-facade.tradingview.com/pine-facade/list?filter=saved", + ) + .await? + .json::>() + .await?; Ok(indicators) } @@ -263,8 +368,9 @@ pub async fn get_builtin_indicators(indicator_type: BuiltinIndicators) -> Result let mut tasks: Vec>>> = Vec::new(); for indicator_type in indicator_types { - let url = - format!("https://pine-facade.tradingview.com/pine-facade/list/?filter={}", indicator_type); + let url = format!( + "https://pine-facade.tradingview.com/pine-facade/list/?filter={indicator_type}" + ); let task = tokio::spawn(async move { let data = get(None, &url).await?.json::>().await?; Ok(data) @@ -295,7 +401,7 @@ pub async fn get_builtin_indicators(indicator_type: BuiltinIndicators) -> Result /// # Example /// /// ```rust -/// use tradingview::api::search_indicator; +/// use tradingview::search_indicator; /// /// #[tokio::main] /// async fn main() { @@ -307,12 +413,10 @@ pub async fn get_builtin_indicators(indicator_type: BuiltinIndicators) -> Result pub async fn search_indicator( client: Option<&UserCookies>, search: &str, - offset: i32 + offset: i32, ) -> Result> { let url = format!( - "https://www.tradingview.com/pubscripts-suggest-json/?search={}&offset={}", - search, - offset + "https://www.tradingview.com/pubscripts-suggest-json/?search={search}&offset={offset}", ); let resp: pine_indicator::SearchResponse = get(client, &url).await?.json().await?; debug!("Response: {:?}", resp); @@ -339,7 +443,7 @@ pub async fn search_indicator( /// # Examples /// /// ```rust -/// use tradingview::api::get_indicator_metadata; +/// use tradingview::get_indicator_metadata; /// /// async fn run() -> Result<(), Box> { /// let client = None; @@ -355,7 +459,7 @@ pub async fn search_indicator( pub async fn get_indicator_metadata( client: Option<&UserCookies>, pinescript_id: &str, - pinescript_version: &str + pinescript_version: &str, ) -> Result { use urlencoding::encode; let url = format!( @@ -370,5 +474,7 @@ pub async fn get_indicator_metadata( return Ok(resp.result); } - Err(Error::Generic("Failed to get indicator metadata".to_string())) + Err(Error::Generic( + "Failed to get indicator metadata".to_string(), + )) } diff --git a/src/client/mod.rs b/src/client/mod.rs index 91f82b7..83cf078 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1 +1,4 @@ +pub mod fin_calendar; pub mod misc; +pub mod news; +pub mod websocket; diff --git a/src/client/news.rs b/src/client/news.rs new file mode 100644 index 0000000..2ef3a27 --- /dev/null +++ b/src/client/news.rs @@ -0,0 +1,166 @@ +use crate::{ + utils::get, MarketType, News, NewsArea, NewsContent, NewsHeadlines, NewsSection, Result, + UserCookies, +}; + +static BASE_NEWS_URL: &str = "https://news-headlines.tradingview.com/v2"; + +fn get_news_category(market_type: &MarketType) -> &str { + match market_type { + MarketType::All => "base", + MarketType::Stocks(_) => "stock", + MarketType::Funds(_) => "etf", + MarketType::Futures => "futures", + MarketType::Forex => "forex", + MarketType::Crypto(_) => "crypto", + MarketType::Indices => "index", + MarketType::Bonds => "bond", + MarketType::Economy => "economic", + } +} + +fn get_news_area(area: &NewsArea) -> &str { + match area { + NewsArea::World => "WLD", + NewsArea::Americas => "AME", + NewsArea::Europe => "EUR", + NewsArea::Asia => "ASI", + NewsArea::Oceania => "OCN", + NewsArea::Africa => "AFR", + } +} + +fn get_news_section(section: &NewsSection) -> &str { + match section { + NewsSection::PressRelease => "press_release", + NewsSection::FinancialStatement => "financial_statement", + NewsSection::InsiderTrading => "insider_trading", + NewsSection::ESG => "esg", + NewsSection::CorpActivitiesAll => "corp_activity", + NewsSection::AnalysisAll => "analysis", + NewsSection::AnalysisRecommendations => "recommendation", + NewsSection::EstimatesAndForecasts => "prediction", + NewsSection::MarketToday => "markets_today", + NewsSection::Surveys => "survey", + } +} + +pub async fn list_news( + client: Option<&UserCookies>, + category: &MarketType, + area: Option<&NewsArea>, + section: Option<&NewsSection>, +) -> Result { + let category = get_news_category(category); + let mut queries = vec![ + ("category", category), + ("client", "web"), + ("lang", "en"), + ("streaming", "false"), + ]; + if let Some(area) = area { + queries.push(("area", get_news_area(area))); + } + if let Some(section) = section { + queries.push(("section", get_news_section(section))); + } + let res = get(client, &format!("{BASE_NEWS_URL}/headlines"), &queries) + .await? + .json::() + .await?; + + Ok(res) +} + +async fn fetch_news(id: &str) -> Result { + let res = get( + None, + &format!("{BASE_NEWS_URL}/story"), + &[("id", id), ("lang", "en")], + ) + .await? + .json::() + .await?; + + Ok(res) +} + +impl News { + pub fn get_url(&self) -> String { + format!("https://www.tradingview.com{}", self.story_path) + } + + pub fn get_source_url(&self) -> String { + if let Some(url) = &self.link { + return url.to_string(); + } + self.get_url() + } + + pub fn get_related_symbols(&self) -> Vec { + self.related_symbols + .iter() + .map(|s| s.symbol.to_owned()) + .collect() + } + + pub async fn get_content(&self) -> Result { + fetch_news(&self.id).await + } + + pub async fn get_source_html(&self) -> Result { + let res = get(None, &self.get_source_url(), &[]).await?.text().await?; + Ok(res) + } +} + +#[tokio::test] +async fn test_list_news() -> Result<()> { + let res = list_news( + None, + &MarketType::All, + None, + Some(&NewsSection::AnalysisAll), + ) + .await?; + println!("{:#?}", res); + Ok(()) +} + +#[tokio::test] +async fn test_fetch_news() -> Result<()> { + let _ = fetch_news("tag:reuters.com,2024:newsml_L4N3E9476:0").await?; + + let res = list_news( + None, + &MarketType::All, + None, + Some(&NewsSection::AnalysisAll), + ) + .await?; + + for item in res.items[0..2].iter() { + let content = item.get_content().await.unwrap(); + println!("{:#?}", content); + } + + Ok(()) +} + +#[tokio::test] +async fn test_get_source_html() -> Result<()> { + let res = list_news( + None, + &MarketType::All, + None, + Some(&NewsSection::AnalysisAll), + ) + .await?; + + for item in res.items[0..1].iter() { + let html = item.get_source_html().await.unwrap(); + println!("{:#?}", html); + } + + Ok(()) +} diff --git a/src/chart/session.rs b/src/client/websocket.rs similarity index 56% rename from src/chart/session.rs rename to src/client/websocket.rs index d8b46fc..d8ecf76 100644 --- a/src/chart/session.rs +++ b/src/client/websocket.rs @@ -1,40 +1,159 @@ use crate::{ - chart::{ChartOptions, StudyOptions}, - data_loader::DataLoader, - models::{pine_indicator::PineIndicator, Interval, Timezone}, + callback::Callbacks, + chart::{ + models::{ChartResponseData, StudyResponseData, SymbolInfo}, + ChartOptions, StudyOptions, + }, + error::TradingViewError, payload, - socket::{Socket, SocketMessageDe, SocketSession, TradingViewDataEvent}, + pine_indicator::PineIndicator, + quote::{ + models::{QuoteData, QuoteValue}, + utils::merge_quotes, + ALL_QUOTE_FIELDS, + }, + socket::{DataServer, Socket, SocketMessageDe, SocketSession, TradingViewDataEvent}, utils::{gen_id, gen_session_id, symbol_init}, - Error, Result, + Error, Interval, Result, Timezone, }; +use serde::Deserialize; use serde_json::Value; -use std::fmt::Debug; +use std::collections::HashMap; +use tracing::{debug, error, trace}; + +#[derive(Clone, Default)] +pub struct WebSocketClient<'a> { + metadata: Metadata, + callbacks: Callbacks<'a>, +} + +#[derive(Default, Clone)] +struct Metadata { + series_count: u16, + series: HashMap, + studies_count: u16, + studies: HashMap, + quotes: HashMap, + quote_session: String, +} #[derive(Clone)] pub struct WebSocket<'a> { - data_loader: DataLoader<'a>, + client: WebSocketClient<'a>, socket: SocketSession, } +#[derive(Default)] +pub struct WebSocketBuilder<'a> { + client: Option>, + auth_token: Option, + server: Option, +} + #[derive(Debug, Clone, Default)] pub struct SeriesInfo { pub chart_session: String, pub options: ChartOptions, } +impl<'a> WebSocketBuilder<'a> { + pub fn client(mut self, client: WebSocketClient<'a>) -> Self { + self.client = Some(client); + self + } + + pub fn auth_token(mut self, auth_token: &str) -> Self { + self.auth_token = Some(auth_token.to_string()); + self + } + + pub fn server(mut self, server: DataServer) -> Self { + self.server = Some(server); + self + } + + pub async fn build(self) -> Result> { + let auth_token = self + .auth_token + .unwrap_or("unauthorized_user_token".to_string()); + let server = self.server.unwrap_or_default(); + + let socket = SocketSession::new(server, auth_token).await?; + let client = self.client.unwrap_or_default(); + + Ok(WebSocket::new_with_session(client, socket)) + } +} + impl<'a> WebSocket<'a> { - pub fn new(data_loader: DataLoader<'a>, socket: SocketSession) -> Self { - Self { - data_loader, - socket, - } + #[allow(clippy::new_ret_no_self)] + pub fn new() -> WebSocketBuilder<'a> { + WebSocketBuilder::default() } - // Begin TradingView WebSocket methods + pub fn new_with_session(client: WebSocketClient<'a>, socket: SocketSession) -> Self { + Self { client, socket } + } - pub async fn set_locale(&mut self) -> Result<&mut Self> { + // Begin TradingView WebSocket Quote methods + pub async fn create_quote_session(&mut self) -> Result<&mut Self> { + let quote_session = gen_session_id("qs"); + self.client.metadata.quote_session = quote_session.clone(); self.socket - .send("set_locale", &payload!("en", "US")) + .send("quote_create_session", &payload!(quote_session)) + .await?; + Ok(self) + } + + pub async fn delete_quote_session(&mut self) -> Result<&mut Self> { + self.socket + .send( + "quote_delete_session", + &payload!(self.client.metadata.quote_session.clone()), + ) + .await?; + Ok(self) + } + + pub async fn set_fields(&mut self) -> Result<&mut Self> { + let mut quote_fields = payload![self.client.metadata.quote_session.clone().to_string()]; + quote_fields.extend(ALL_QUOTE_FIELDS.clone().into_iter().map(Value::from)); + self.socket.send("quote_set_fields", "e_fields).await?; + Ok(self) + } + + pub async fn add_symbols(&mut self, symbols: Vec<&str>) -> Result<&mut Self> { + let mut payloads = payload![self.client.metadata.quote_session.clone()]; + payloads.extend(symbols.into_iter().map(Value::from)); + self.socket.send("quote_add_symbols", &payloads).await?; + Ok(self) + } + + pub async fn update_auth_token(&mut self, auth_token: &str) -> Result<&mut Self> { + self.socket.update_auth_token(auth_token).await?; + Ok(self) + } + + pub async fn fast_symbols(&mut self, symbols: Vec<&str>) -> Result<&mut Self> { + let mut payloads = payload![self.client.metadata.quote_session.clone()]; + payloads.extend(symbols.into_iter().map(Value::from)); + self.socket.send("quote_fast_symbols", &payloads).await?; + Ok(self) + } + + pub async fn remove_symbols(&mut self, symbols: Vec<&str>) -> Result<&mut Self> { + let mut payloads = payload![self.client.metadata.quote_session.clone()]; + payloads.extend(symbols.into_iter().map(Value::from)); + self.socket.send("quote_remove_symbols", &payloads).await?; + Ok(self) + } + // End TradingView WebSocket Quote methods + + // Begin TradingView WebSocket Chart methods + /// Example: local = ("en", "US") + pub async fn set_locale(&mut self, local: (&str, &str)) -> Result<&mut Self> { + self.socket + .send("set_locale", &payload!(local.0, local.1)) .await?; Ok(self) } @@ -55,11 +174,6 @@ impl<'a> WebSocket<'a> { Ok(self) } - pub async fn update_auth_token(&mut self, auth_token: &str) -> Result<&mut Self> { - self.socket.update_auth_token(auth_token).await?; - Ok(self) - } - pub async fn create_chart_session(&mut self, session: &str) -> Result<&mut Self> { self.socket .send("chart_create_session", &payload!(session)) @@ -327,7 +441,7 @@ impl<'a> WebSocket<'a> { } pub async fn delete(&mut self) -> Result<&mut Self> { - for (_, s) in self.data_loader.metadata.series.clone() { + for (_, s) in self.client.metadata.series.clone() { self.delete_chart_session_id(&s.chart_session).await?; } self.socket.close().await?; @@ -369,8 +483,8 @@ impl<'a> WebSocket<'a> { chart_session: &str, series_id: &str, ) -> Result<&mut Self> { - self.data_loader.metadata.studies_count += 1; - let study_count = self.data_loader.metadata.studies_count; + self.client.metadata.studies_count += 1; + let study_count = self.client.metadata.studies_count; let study_id = format!("st{}", study_count); let indicator = PineIndicator::build() @@ -381,7 +495,7 @@ impl<'a> WebSocket<'a> { ) .await?; - self.data_loader + self.client .metadata .studies .insert(indicator.metadata.data.id.clone(), study_id.clone()); @@ -392,8 +506,8 @@ impl<'a> WebSocket<'a> { } pub async fn set_market(&mut self, options: ChartOptions) -> Result<&mut Self> { - self.data_loader.metadata.series_count += 1; - let series_count = self.data_loader.metadata.series_count; + self.client.metadata.series_count += 1; + let series_count = self.client.metadata.series_count; let symbol_series_id = format!("sds_sym_{}", series_count); let series_id = format!("sds_{}", series_count); let series_version = format!("s{}", series_count); @@ -433,28 +547,137 @@ impl<'a> WebSocket<'a> { options, }; - self.data_loader - .metadata - .series - .insert(series_id, series_info); + self.client.metadata.series.insert(series_id, series_info); Ok(self) } pub async fn subscribe(&mut self) { - self.event_loop(&mut self.socket.clone()).await; + self.event_loop(&mut self.socket.to_owned()).await; } } #[async_trait::async_trait] impl<'a> Socket for WebSocket<'a> { async fn handle_message_data(&mut self, message: SocketMessageDe) -> Result<()> { - let event = TradingViewDataEvent::from(message.m.clone()); - self.data_loader.handle_events(event, &message.p).await; + let event = TradingViewDataEvent::from(message.m.to_owned()); + self.client.handle_events(event, &message.p).await; Ok(()) } async fn handle_error(&self, error: Error) { - (self.data_loader.callbacks.on_error)(error).await; + (self.client.callbacks.on_error)(error).await; + } +} + +impl<'a> WebSocketClient<'a> { + pub(crate) async fn handle_events( + &mut self, + event: TradingViewDataEvent, + message: &Vec, + ) { + match event { + TradingViewDataEvent::OnChartData | TradingViewDataEvent::OnChartDataUpdate => { + trace!("received raw chart data: {:?}", message); + match self + .handle_chart_data(&self.metadata.series, &self.metadata.studies, message) + .await + { + Ok(_) => (), + Err(e) => { + error!("chart data parsing error: {:?}", e); + (self.callbacks.on_error)(e).await; + } + }; + } + TradingViewDataEvent::OnQuoteData => self.handle_quote_data(message).await, + TradingViewDataEvent::OnSymbolResolved => { + match SymbolInfo::deserialize(&message[2]) { + Ok(s) => { + debug!("receive symbol info: {:?}", s); + (self.callbacks.on_symbol_info)(s).await; + } + Err(e) => { + error!("symbol resolved parsing error: {:?}", e); + (self.callbacks.on_error)(Error::JsonParseError(e)).await; + } + }; + } + _ => { + debug!("event: {:?}, message: {:?}", event, message); + (self.callbacks.on_other_event)((event, message.to_owned())).await; + } + } + } + + async fn handle_chart_data( + &self, + series: &HashMap, + studies: &HashMap, + message: &[Value], + ) -> Result<()> { + for (id, s) in series.iter() { + debug!("received raw message - v: {:?}, m: {:?}", s, message); + match message[1].get(id.as_str()) { + Some(resp_data) => { + let data = ChartResponseData::deserialize(resp_data)?.series; + debug!("series data extracted: {:?}", data); + (self.callbacks.on_chart_data)((s.options.clone(), data)).await; + } + None => { + debug!("receive empty data on series: {:?}", s); + } + } + + if let Some(study_options) = &s.options.study_config { + self.handle_study_data(study_options, studies, message) + .await?; + } + } + Ok(()) + } + + async fn handle_study_data( + &self, + options: &StudyOptions, + studies: &HashMap, + message: &[Value], + ) -> Result<()> { + for (k, v) in studies.iter() { + if let Some(resp_data) = message[1].get(v.as_str()) { + debug!("study data received: {} - {:?}", k, resp_data); + let data = StudyResponseData::deserialize(resp_data)?; + (self.callbacks.on_study_data)((options.clone(), data)).await; + } + } + Ok(()) + } + + async fn handle_quote_data(&mut self, message: &[Value]) { + debug!("received raw quote data: {:?}", message); + let qsd = QuoteData::deserialize(&message[1]).unwrap_or_default(); + if qsd.status == "ok" { + if let Some(prev_quote) = self.metadata.quotes.get_mut(&qsd.name) { + *prev_quote = merge_quotes(prev_quote, &qsd.value); + } else { + self.metadata.quotes.insert(qsd.name, qsd.value); + } + + for q in self.metadata.quotes.values() { + debug!("quote data: {:?}", q); + (self.callbacks.on_quote_data)(q.to_owned()).await; + } + } else { + error!("quote data status error: {:?}", qsd); + (self.callbacks.on_error)(Error::TradingViewError( + TradingViewError::QuoteDataStatusError, + )) + .await; + } + } + + pub fn set_callbacks(mut self, callbacks: Callbacks<'a>) -> Self { + self.callbacks = callbacks; + self } } diff --git a/src/data_loader.rs b/src/data_loader.rs deleted file mode 100644 index 95bafd7..0000000 --- a/src/data_loader.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::{ - callback::Callbacks, - chart::{ - models::{ChartResponseData, StudyResponseData, SymbolInfo}, - session::SeriesInfo, - StudyOptions, - }, - error::TradingViewError, - quote::{ - models::{QuoteData, QuoteValue}, - utils::merge_quotes, - }, - socket::TradingViewDataEvent, - Error, Result, -}; -use serde::Deserialize; -use serde_json::Value; -use std::collections::HashMap; -use tracing::{debug, error, trace}; - -#[derive(Clone, Default)] -pub struct DataLoader<'a> { - pub(crate) metadata: Metadata, - pub(crate) callbacks: Callbacks<'a>, -} - -#[derive(Default, Debug, Clone)] -pub struct DataLoaderBuilder {} - -#[derive(Default, Clone)] -pub struct Metadata { - pub series_count: u16, - pub series: HashMap, - pub studies_count: u16, - pub studies: HashMap, - pub quotes: HashMap, - pub quote_session: String, -} - -impl<'a> DataLoader<'a> { - pub(crate) async fn handle_events( - &mut self, - event: TradingViewDataEvent, - message: &Vec, - ) { - match event { - TradingViewDataEvent::OnChartData | TradingViewDataEvent::OnChartDataUpdate => { - trace!("received chart data: {:?}", message); - match self - .handle_chart_data(&self.metadata.series, &self.metadata.studies, message) - .await - { - Ok(_) => (), - Err(e) => { - error!("chart data parsing error: {:?}", e); - (self.callbacks.on_error)(e).await; - } - }; - } - TradingViewDataEvent::OnQuoteData => self.handle_quote_data(message).await, - TradingViewDataEvent::OnSymbolResolved => { - match SymbolInfo::deserialize(&message[2]) { - Ok(s) => { - debug!("receive symbol info: {:?}", s); - (self.callbacks.on_symbol_info)(s).await; - } - Err(e) => { - error!("symbol resolved parsing error: {:?}", e); - (self.callbacks.on_error)(Error::JsonParseError(e)).await; - } - }; - } - _ => { - debug!("event: {:?}, message: {:?}", event, message); - (self.callbacks.on_other_event)((event, message.to_owned())).await; - } - } - } - - async fn handle_chart_data( - &self, - series: &HashMap, - studies: &HashMap, - message: &[Value], - ) -> Result<()> { - for (id, s) in series.iter() { - trace!("received v: {:?}, m: {:?}", s, message); - match message[1].get(id.as_str()) { - Some(resp_data) => { - let data = ChartResponseData::deserialize(resp_data)?.series; - debug!("series data extracted: {:?}", data); - (self.callbacks.on_chart_data)((s.options.clone(), data)).await; - } - None => { - debug!("receive empty data on series: {:?}", s); - } - } - - if let Some(study_options) = &s.options.study_config { - self.handle_study_data(study_options, studies, message) - .await?; - } - } - Ok(()) - } - - async fn handle_study_data( - &self, - options: &StudyOptions, - studies: &HashMap, - message: &[Value], - ) -> Result<()> { - for (k, v) in studies.iter() { - if let Some(resp_data) = message[1].get(v.as_str()) { - debug!("study data received: {} - {:?}", k, resp_data); - let data = StudyResponseData::deserialize(resp_data)?; - (self.callbacks.on_study_data)((options.clone(), data)).await; - } - } - Ok(()) - } - - async fn handle_quote_data(&mut self, message: &[Value]) { - let qsd = QuoteData::deserialize(&message[1]).unwrap(); - if qsd.status == "ok" { - if let Some(prev_quote) = self.metadata.quotes.get_mut(&qsd.name) { - *prev_quote = merge_quotes(prev_quote, &qsd.value); - } else { - self.metadata.quotes.insert(qsd.name, qsd.value); - } - - for q in self.metadata.quotes.values() { - debug!("quote data: {:?}", q); - (self.callbacks.on_quote_data)(q.to_owned()).await; - } - } else { - error!("quote data status error: {:?}", qsd); - (self.callbacks.on_error)(Error::TradingViewError( - TradingViewError::QuoteDataStatusError, - )) - .await; - } - } -} diff --git a/src/lib.rs b/src/lib.rs index c78c18f..329fedb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ pub mod callback; pub mod chart; pub mod client; -pub mod data_loader; pub mod error; pub mod models; pub mod quote; @@ -14,13 +13,18 @@ mod utils; static UA: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"; -pub mod api { - pub use crate::client::misc::{ - get_builtin_indicators, get_chart_token, get_drawing, get_indicator_metadata, - get_private_indicators, get_quote_token, list_symbols, search_indicator, search_symbol, - }; +pub use crate::client::misc::{ + advanced_search_symbol, get_builtin_indicators, get_chart_token, get_drawing, + get_indicator_metadata, get_private_indicators, get_quote_token, list_symbols, + search_indicator, +}; + +pub mod websocket { + pub use crate::client::websocket::*; } -pub type Result = std::result::Result; +pub use crate::models::*; + +pub type Result = std::result::Result; pub use error::Error; diff --git a/src/models/mod.rs b/src/models/mod.rs index 8ba4068..63b5bcd 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,12 @@ -use std::collections::HashMap; +pub use self::news::*; +pub use self::MarketType::*; +pub use crate::chart::models::*; +pub use crate::quote::models::*; + +use std::{collections::HashMap, fmt::Display}; use serde::{Deserialize, Deserializer, Serialize}; +pub mod news; pub mod pine_indicator; #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -69,30 +75,32 @@ pub struct SymbolSearchResponse { pub symbols: Vec, } -#[cfg_attr(not(feature = "protobuf"), derive(Debug, Default))] -#[cfg_attr(feature = "protobuf", derive(prost::Message))] -#[derive(Clone, Deserialize)] +#[derive(Clone, PartialEq, Deserialize, Serialize, Debug, Default)] pub struct Symbol { - #[cfg_attr(feature = "protobuf", prost(string, tag = "1"))] pub symbol: String, - #[cfg_attr(feature = "protobuf", prost(string, tag = "2"))] #[serde(default)] pub description: String, - #[cfg_attr(feature = "protobuf", prost(string, tag = "3"))] #[serde(default, rename(deserialize = "type"))] pub market_type: String, #[serde(default)] - #[cfg_attr(feature = "protobuf", prost(string, tag = "4"))] pub exchange: String, - #[cfg_attr(feature = "protobuf", prost(string, tag = "5"))] #[serde(default)] pub currency_code: String, - #[cfg_attr(feature = "protobuf", prost(string, tag = "6"))] #[serde(default, rename(deserialize = "provider_id"))] pub data_provider: String, - #[cfg_attr(feature = "protobuf", prost(string, tag = "7"))] #[serde(default, rename(deserialize = "country"))] pub country_code: String, + #[serde(default, rename(deserialize = "typespecs"))] + pub type_specs: Vec, + #[serde(default, rename(deserialize = "source2"))] + pub exchange_source: ExchangeSource, +} + +#[derive(Clone, PartialEq, Deserialize, Serialize, Debug, Default)] +pub struct ExchangeSource { + pub id: String, + pub name: String, + pub description: String, } #[derive(Debug, Default, Clone, Serialize)] @@ -104,7 +112,7 @@ pub enum SessionType { PostMarket, } -impl std::fmt::Display for SessionType { +impl Display for SessionType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SessionType::Regular => write!(f, "regular"), @@ -122,7 +130,7 @@ pub enum MarketAdjustment { Dividends, } -impl std::fmt::Display for MarketAdjustment { +impl Display for MarketAdjustment { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MarketAdjustment::Splits => write!(f, "splits"), @@ -140,7 +148,7 @@ pub enum MarketStatus { Pre, } -impl std::fmt::Display for MarketStatus { +impl Display for MarketStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MarketStatus::Holiday => write!(f, "holiday"), @@ -246,7 +254,7 @@ pub enum Timezone { EtcUTC, } -impl std::fmt::Display for Timezone { +impl Display for Timezone { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Timezone::AfricaCairo => write!(f, "Africa/Cairo"), @@ -367,7 +375,7 @@ pub enum Interval { Yearly = 19, } -impl std::fmt::Display for Interval { +impl Display for Interval { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let time_interval = match self { Interval::OneSecond => "1S", @@ -429,7 +437,7 @@ pub enum LanguageCode { TraditionalChinese, } -impl std::fmt::Display for LanguageCode { +impl Display for LanguageCode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { LanguageCode::Arabic => write!(f, "ar"), @@ -493,7 +501,7 @@ impl<'de> Deserialize<'de> for FinancialPeriod { } } -impl std::fmt::Display for FinancialPeriod { +impl Display for FinancialPeriod { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { FinancialPeriod::FiscalYear => write!(f, "FY"), @@ -504,7 +512,6 @@ impl std::fmt::Display for FinancialPeriod { } } } - pub enum SymbolType { Stock, Index, @@ -529,7 +536,7 @@ pub enum SymbolType { Spot, } -impl std::fmt::Display for SymbolType { +impl Display for SymbolType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { SymbolType::Stock => write!(f, "stock"), @@ -557,32 +564,261 @@ impl std::fmt::Display for SymbolType { } } -#[derive(Default, Debug, Clone, Copy, Serialize)] -pub enum SymbolMarketType { +#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq)] +pub enum MarketType { #[default] All, - Stocks, - Funds, + Stocks(StocksType), + Funds(FundsType), Futures, Forex, - Crypto, + Crypto(CryptoType), Indices, Bonds, Economy, } -impl std::fmt::Display for SymbolMarketType { +#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq)] +pub enum StocksType { + #[default] + All, + Common, + Preferred, + DepositoryReceipt, + Warrant, +} + +#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq)] +pub enum CryptoType { + #[default] + All, + Spot, + Futures, + Swap, + Index, + Fundamental, +} + +#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq)] +pub enum FundsType { + #[default] + All, + ETF, + MutualFund, + Trust, + REIT, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq)] +pub enum CryptoCentralization { + CEX, + DEX, +} + +impl Display for MarketType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { - SymbolMarketType::All => write!(f, "undefined"), - SymbolMarketType::Stocks => write!(f, "stocks"), - SymbolMarketType::Funds => write!(f, "funds"), - SymbolMarketType::Futures => write!(f, "futures"), - SymbolMarketType::Forex => write!(f, "forex"), - SymbolMarketType::Crypto => write!(f, "crypto"), - SymbolMarketType::Indices => write!(f, "index"), - SymbolMarketType::Bonds => write!(f, "bond"), - SymbolMarketType::Economy => write!(f, "economic"), + MarketType::All => write!(f, "undefined"), + MarketType::Stocks(t) => match t { + StocksType::All => write!(f, "stocks"), + StocksType::Common => write!(f, "common_stock"), + StocksType::Preferred => write!(f, "preferred_stock"), + StocksType::DepositoryReceipt => write!(f, "depository_receipt"), + StocksType::Warrant => write!(f, "warrant"), + }, + MarketType::Funds(t) => match t { + FundsType::All => write!(f, "funds"), + FundsType::ETF => write!(f, "etf"), + FundsType::MutualFund => write!(f, "mutual_fund"), + FundsType::Trust => write!(f, "trust_fund"), + FundsType::REIT => write!(f, "reit"), + }, + MarketType::Futures => write!(f, "futures"), + MarketType::Forex => write!(f, "forex"), + MarketType::Crypto(t) => match t { + CryptoType::All => write!(f, "crypto"), + CryptoType::Spot => write!(f, "crypto_spot"), + CryptoType::Futures => write!(f, "crypto_futures"), + CryptoType::Swap => write!(f, "crypto_swap"), + CryptoType::Index => write!(f, "crypto_index"), + CryptoType::Fundamental => write!(f, "crypto_fundamental"), + }, + MarketType::Indices => write!(f, "index"), + MarketType::Bonds => write!(f, "bond"), + MarketType::Economy => write!(f, "economic"), + } + } +} + +impl Display for CryptoCentralization { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + CryptoCentralization::CEX => write!(f, "cex"), + CryptoCentralization::DEX => write!(f, "dex"), + } + } +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum FuturesProductType { + SingleStock, + WorldIndices, + Currencies, + InterestRates, + Energy, + Agriculture, + Metals, + Weather, +} + +impl Display for FuturesProductType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + FuturesProductType::SingleStock => write!(f, "Financial%2FEquity"), + FuturesProductType::WorldIndices => write!(f, "Financial%2FIndex"), + FuturesProductType::Currencies => write!(f, "Financial%2FCurrency"), + FuturesProductType::InterestRates => write!(f, "=Financial%2FInterestRate"), + FuturesProductType::Energy => write!(f, "Financial%2FEnergy"), + FuturesProductType::Agriculture => write!(f, "Financial%2FAgriculture"), + FuturesProductType::Metals => write!(f, "Financial%2FMetals"), + FuturesProductType::Weather => write!(f, "Financial%2FWeather"), + } + } +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum StockSector { + CommercialServices, + Communications, + ConsumerDurables, + ConsumerNonDurables, + ConsumerServices, + DistributionServices, + ElectronicTechnology, + EnergyMinerals, + Finance, + Government, + HealthServices, + HealthTechnology, + IndustrialServices, + Miscellaneous, + NonEnergyMinerals, + ProcessIndustries, + ProducerManufacturing, + RetailTrade, + TechnologyServices, + Transportation, + Utilities, +} + +impl Display for StockSector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + StockSector::CommercialServices => write!(f, "Commercial+Services"), + StockSector::Communications => write!(f, "Communications"), + StockSector::ConsumerDurables => write!(f, "Consumer+Durables"), + StockSector::ConsumerNonDurables => write!(f, "Consumer+Non-Durables"), + StockSector::ConsumerServices => write!(f, "Consumer+Services"), + StockSector::DistributionServices => write!(f, "Distribution+Services"), + StockSector::ElectronicTechnology => write!(f, "Electronic+Technology"), + StockSector::EnergyMinerals => write!(f, "Energy+Minerals"), + StockSector::Finance => write!(f, "Finance"), + StockSector::Government => write!(f, "Government"), + StockSector::HealthServices => write!(f, "Health+Services"), + StockSector::HealthTechnology => write!(f, "Health+Technology"), + StockSector::IndustrialServices => write!(f, "Industrial+Services"), + StockSector::Miscellaneous => write!(f, "Miscellaneous"), + StockSector::NonEnergyMinerals => write!(f, "Non-Energy+Minerals"), + StockSector::ProcessIndustries => write!(f, "Process+Industries"), + StockSector::ProducerManufacturing => write!(f, "Producer+Manufacturing"), + StockSector::RetailTrade => write!(f, "Retail+Trade"), + StockSector::TechnologyServices => write!(f, "Technology+Services"), + StockSector::Transportation => write!(f, "Transportation"), + StockSector::Utilities => write!(f, "Utilities"), + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq)] +pub enum EconomicSource { + WorldBank, + EUROSTAT, + AKAMAI, + TransparencyInternational, + OrganizationForEconomicCooperationAndDevelopment, + WorldEconomicForum, + WageIndicatorFoundation, + BureauOfLaborStatistics, + FederalReserve, + StockholmInternationalPeaceResearchInstitute, + InstituteForEconomicsAndPeace, + BureauOfEconomicAnalysis, + WorldGoldCouncil, + CensusBureau, + CentralBankOfWestAfricanStates, + InternationalMonetaryFund, + USEnergyInformationAdministration, + StatisticCanada, + OfficeForNationalStatistics, + StatisticsNorway, +} + +impl Display for EconomicSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + EconomicSource::WorldBank => write!(f, "__WB"), + EconomicSource::EUROSTAT => write!(f, "__EUROSTAT"), + EconomicSource::AKAMAI => write!(f, "__AKAMAI"), + EconomicSource::TransparencyInternational => write!(f, "__TI"), + EconomicSource::OrganizationForEconomicCooperationAndDevelopment => write!(f, "__OECD"), + EconomicSource::WorldEconomicForum => write!(f, "__WEF"), + EconomicSource::WageIndicatorFoundation => write!(f, "__WIF"), + EconomicSource::BureauOfLaborStatistics => write!(f, "USBLS"), + EconomicSource::FederalReserve => write!(f, "USFR"), + EconomicSource::StockholmInternationalPeaceResearchInstitute => write!(f, "__SIPRI"), + EconomicSource::InstituteForEconomicsAndPeace => write!(f, "__IEP"), + EconomicSource::BureauOfEconomicAnalysis => write!(f, "USBEA"), + EconomicSource::WorldGoldCouncil => write!(f, "__WGC"), + EconomicSource::CensusBureau => write!(f, "USCB"), + EconomicSource::CentralBankOfWestAfricanStates => write!(f, "__BCEAO"), + EconomicSource::InternationalMonetaryFund => write!(f, "__IMF"), + EconomicSource::USEnergyInformationAdministration => write!(f, "__UEIA"), + EconomicSource::StatisticCanada => write!(f, "CASC"), + EconomicSource::OfficeForNationalStatistics => write!(f, "GBONS"), + EconomicSource::StatisticsNorway => write!(f, "NOSN"), + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq)] +pub enum EconomicCategory { + GDP, + Labor, + Prices, + Health, + Money, + Trade, + Government, + Business, + Consumer, + Housing, + Taxes, +} + +impl Display for EconomicCategory { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + EconomicCategory::GDP => write!(f, "gdp"), + EconomicCategory::Labor => write!(f, "lbr"), + EconomicCategory::Prices => write!(f, "prce"), + EconomicCategory::Health => write!(f, "hlth"), + EconomicCategory::Money => write!(f, "mny"), + EconomicCategory::Trade => write!(f, "trd"), + EconomicCategory::Government => write!(f, "gov"), + EconomicCategory::Business => write!(f, "bsnss"), + EconomicCategory::Consumer => write!(f, "cnsm"), + EconomicCategory::Housing => write!(f, "hse"), + EconomicCategory::Taxes => write!(f, "txs"), } } } diff --git a/src/models/news.rs b/src/models/news.rs new file mode 100644 index 0000000..813c614 --- /dev/null +++ b/src/models/news.rs @@ -0,0 +1,102 @@ +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; + +pub enum NewsArea { + World, + Americas, + Europe, + Asia, + Oceania, + Africa, +} + +pub enum NewsSection { + AnalysisAll, + AnalysisRecommendations, + EstimatesAndForecasts, + MarketToday, + Surveys, + PressRelease, + FinancialStatement, + InsiderTrading, + ESG, + CorpActivitiesAll, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NewsHeadlines { + #[serde(rename = "items")] + pub items: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct News { + pub id: String, + pub title: String, + pub provider: String, + pub published: i64, + pub source: String, + pub urgency: i64, + pub permission: Option, + #[serde(default)] + pub related_symbols: Vec, + pub story_path: String, + pub link: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RelatedSymbol { + pub symbol: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NewsContent { + pub short_description: String, + pub ast_description: AstDescription, + pub language: String, + pub tags: Vec, + #[serde(default)] + pub robots: Vec, + pub copyright: Option, + pub id: String, + pub title: String, + pub provider: String, + pub published: i64, + pub source: String, + pub urgency: i64, + pub permission: Option, + pub story_path: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AstDescription { + #[serde(rename = "type")] + pub type_field: String, + pub children: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Children { + #[serde(rename = "type")] + pub type_field: String, + pub children: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Tag { + pub title: String, + pub args: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Arg { + pub id: String, + pub value: String, +} diff --git a/src/models/pine_indicator.rs b/src/models/pine_indicator.rs index 0b44e45..5fdd9a3 100644 --- a/src/models/pine_indicator.rs +++ b/src/models/pine_indicator.rs @@ -29,26 +29,17 @@ impl std::fmt::Display for BuiltinIndicators { } } -#[derive(Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "protobuf"), derive(Debug))] -// #[cfg_attr(feature = "protobuf", derive(prost::Message))] pub struct PineInfo { - // #[cfg_attr(feature = "protobuf", prost(int64, optional, tag = "1"))] pub user_id: i64, - // #[cfg_attr(feature = "protobuf", prost(string, optional, tag = "2"))] pub script_name: String, - // #[cfg_attr(feature = "protobuf", prost(string, optional, tag = "3"))] pub script_source: String, - // #[cfg_attr(feature = "protobuf", prost(string, optional, tag = "4"))] #[serde(rename(deserialize = "scriptIdPart"))] pub script_id: String, - // #[cfg_attr(feature = "protobuf", prost(string, optional, tag = "5"))] pub script_access: String, - // #[cfg_attr(feature = "protobuf", prost(string, optional, tag = "6"))] #[serde(rename(deserialize = "version"))] pub script_version: String, - // #[cfg_attr(feature = "protobuf", prost(message, optional, tag = "7"))] pub extra: PineInfoExtra, } @@ -214,6 +205,12 @@ impl std::fmt::Display for ScriptType { } } +impl From for ScriptType { + fn from(_value: String) -> Self { + todo!() + } +} + pub struct PineIndicator { pub script_id: String, pub script_version: String, diff --git a/src/quote/mod.rs b/src/quote/mod.rs index 60ebc19..e003ec0 100644 --- a/src/quote/mod.rs +++ b/src/quote/mod.rs @@ -1,4 +1,3 @@ -pub mod session; pub mod models; pub(crate) mod utils; diff --git a/src/quote/models.rs b/src/quote/models.rs index db85fd8..dd7f5e4 100644 --- a/src/quote/models.rs +++ b/src/quote/models.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] pub struct QuoteData { #[serde(rename(deserialize = "n"))] pub name: String, @@ -12,7 +12,7 @@ pub struct QuoteData { #[derive(Clone, PartialEq, Deserialize, Serialize)] #[cfg_attr(feature = "protobuf", derive(prost::Message))] -#[cfg_attr(not(feature = "protobuf"), derive(Debug))] +#[cfg_attr(not(feature = "protobuf"), derive(Debug, Default))] pub struct QuoteValue { #[cfg_attr(feature = "protobuf", prost(double, optional, tag = "1"))] #[serde(default)] diff --git a/src/quote/session.rs b/src/quote/session.rs deleted file mode 100644 index 6685d03..0000000 --- a/src/quote/session.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::{ - data_loader::DataLoader, - payload, - quote::ALL_QUOTE_FIELDS, - socket::{Socket, SocketMessageDe, SocketSession, TradingViewDataEvent}, - utils::gen_session_id, - Error, Result, -}; -use serde_json::Value; - -#[derive(Clone)] -pub struct WebSocket<'a> { - pub(crate) data_loader: DataLoader<'a>, - socket: SocketSession, -} - -impl<'a> WebSocket<'a> { - pub fn new(data_loader: DataLoader<'a>, socket: SocketSession) -> Self { - Self { - data_loader, - socket, - } - } - - pub async fn create_session(&mut self) -> Result<&mut Self> { - let quote_session = gen_session_id("qs"); - self.data_loader.metadata.quote_session = quote_session.clone(); - self.socket - .send("quote_create_session", &payload!(quote_session)) - .await?; - Ok(self) - } - - pub async fn delete_session(&mut self) -> Result<&mut Self> { - self.socket - .send( - "quote_delete_session", - &payload!(self.data_loader.metadata.quote_session.clone()), - ) - .await?; - Ok(self) - } - - pub async fn set_fields(&mut self) -> Result<&mut Self> { - let mut quote_fields = - payload![self.data_loader.metadata.quote_session.clone().to_string()]; - quote_fields.extend(ALL_QUOTE_FIELDS.clone().into_iter().map(Value::from)); - self.socket.send("quote_set_fields", "e_fields).await?; - Ok(self) - } - - pub async fn add_symbols(&mut self, symbols: Vec<&str>) -> Result<&mut Self> { - let mut payloads = payload![self.data_loader.metadata.quote_session.clone()]; - payloads.extend(symbols.into_iter().map(Value::from)); - self.socket.send("quote_add_symbols", &payloads).await?; - Ok(self) - } - - pub async fn update_auth_token(&mut self, auth_token: &str) -> Result<&mut Self> { - self.socket.update_auth_token(auth_token).await?; - Ok(self) - } - - pub async fn fast_symbols(&mut self, symbols: Vec<&str>) -> Result<&mut Self> { - let mut payloads = payload![self.data_loader.metadata.quote_session.clone()]; - payloads.extend(symbols.into_iter().map(Value::from)); - self.socket.send("quote_fast_symbols", &payloads).await?; - Ok(self) - } - - pub async fn remove_symbols(&mut self, symbols: Vec<&str>) -> Result<&mut Self> { - let mut payloads = payload![self.data_loader.metadata.quote_session.clone()]; - payloads.extend(symbols.into_iter().map(Value::from)); - self.socket.send("quote_remove_symbols", &payloads).await?; - Ok(self) - } - - pub async fn subscribe(&mut self) { - self.event_loop(&mut self.socket.clone()).await; - } -} - -#[async_trait::async_trait] -impl<'a> Socket for WebSocket<'a> { - async fn handle_message_data(&mut self, message: SocketMessageDe) -> Result<()> { - let event = TradingViewDataEvent::from(message.m.clone()); - self.data_loader.handle_events(event, &message.p).await; - Ok(()) - } - - async fn handle_error(&self, error: Error) { - (self.data_loader.callbacks.on_error)(error).await; - } -} diff --git a/src/quote/utils.rs b/src/quote/utils.rs index 7c232ed..88fb2a4 100644 --- a/src/quote/utils.rs +++ b/src/quote/utils.rs @@ -15,10 +15,12 @@ pub fn merge_quotes(quote_old: &QuoteValue, quote_new: &QuoteValue) -> QuoteValu price: quote_new.price.or(quote_old.price), timestamp: quote_new.timestamp.or(quote_old.timestamp), volume: quote_new.volume.or(quote_old.volume), - currency: quote_new.currency.clone().or(quote_old.currency.clone()), symbol: quote_new.symbol.clone().or(quote_old.symbol.clone()), exchange: quote_new.exchange.clone().or(quote_old.exchange.clone()), - market_type: quote_new.market_type.clone().or(quote_old.market_type.clone()), + market_type: quote_new + .market_type + .clone() + .or(quote_old.market_type.clone()), } } diff --git a/src/utils.rs b/src/utils.rs index a546525..07f4ef0 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,13 +1,16 @@ use crate::{ models::{MarketAdjustment, SessionType}, socket::{SocketMessage, SocketMessageDe}, - Result, + Result, UserCookies, }; use base64::engine::{general_purpose::STANDARD as BASE64, Engine as _}; use iso_currency::Currency; use rand::Rng; use regex::Regex; -use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, COOKIE, ORIGIN, REFERER}; +use reqwest::{ + header::{HeaderMap, HeaderValue, ACCEPT, COOKIE, ORIGIN, REFERER}, + Response, +}; use serde::Serialize; use serde_json::Value; use std::{ @@ -138,6 +141,26 @@ pub fn _parse_compressed(data: &str) -> Result { Ok(parsed_data) } +pub async fn get( + client: Option<&UserCookies>, + url: &str, + queries: &[(&str, &str)], +) -> Result { + let c = match client { + Some(client) => { + let cookie = format!( + "sessionid={}; sessionid_sign={}; device_t={};", + client.session, client.session_signature, client.device_token + ); + build_request(Some(&cookie))? + } + None => build_request(None)?, + }; + + let response = c.get(url).query(queries).send().await?; + Ok(response) +} + #[cfg(test)] mod tests { use serde_json::json; diff --git a/tests/client_test.rs b/tests/client_test.rs deleted file mode 100644 index 7db110b..0000000 --- a/tests/client_test.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(test)] -mod tests { - use tradingview::client::misc::*; - use tradingview::models::*; - - #[tokio::test] - async fn test_get_builtin_indicators() { - let indicators = get_builtin_indicators(pine_indicator::BuiltinIndicators::All).await; - assert!(indicators.is_ok()); - assert!(!indicators.unwrap().is_empty()); - } -} diff --git a/tests/misc_test.rs b/tests/misc_test.rs new file mode 100644 index 0000000..1195c5b --- /dev/null +++ b/tests/misc_test.rs @@ -0,0 +1,59 @@ +#[cfg(test)] +mod tests { + use tradingview::*; + + #[tokio::test] + async fn test_search_symbol() { + let res = advanced_search_symbol( + "", + "", + &MarketType::Crypto(CryptoType::All), + 0, + None, + None, + None, + None, + None, + None, + None, + ) + .await + .unwrap(); + + println!("{:#?}", res); + assert!(!res.symbols.is_empty()); + } + + #[tokio::test] + async fn test_list_symbol() { + let res = list_symbols(None, None, None, None).await.unwrap(); + + println!("{:#?}", res.len()); + assert!(!res.is_empty()); + } + + #[tokio::test] + async fn test_get_builtin_indicators() { + let indicators = get_builtin_indicators(pine_indicator::BuiltinIndicators::All) + .await + .unwrap(); + println!("{:#?}", indicators); + assert!(!indicators.is_empty()); + } + + #[tokio::test] + async fn test_get_indicator_metadata() { + let metadata = get_indicator_metadata( + None, + "STD;Candlestick%1Pattern%1Bullish%1Upside%1Tasuki%1Gap", + "19.0", + ) + .await + .unwrap(); + println!("{:#?}", metadata); + assert_eq!( + metadata.data.id, + "Script$STD;Candlestick%1Pattern%1Bullish%1Upside%1Tasuki%1Gap@tv-scripting-101" + ); + } +}