tradingview/client/
misc.rs

1use std::sync::Arc;
2
3use crate::{
4    models::{
5        pine_indicator::{ self, BuiltinIndicators, PineInfo, PineMetadata, PineSearchResult },
6        Symbol,
7        SymbolSearchResponse,
8        SymbolMarketType,
9        UserCookies,
10        ChartDrawing,
11    },
12    utils::build_request,
13    error::Error,
14    Result,
15};
16use reqwest::Response;
17use tokio::{ sync::Semaphore, task::JoinHandle };
18use tracing::debug;
19use serde_json::Value;
20
21/// Sends an HTTP GET request to the specified URL using the provided client and returns the response.
22///
23/// # Arguments
24///
25/// * `client` - An optional reference to a `UserCookies` struct representing the client to use for the request.
26/// * `url` - A string slice representing the URL to send the request to.
27///
28/// # Returns
29///
30/// A `Result` containing a `Response` struct representing the response from the server, or an error if the request failed.
31async fn get(client: Option<&UserCookies>, url: &str) -> Result<Response> {
32    if let Some(client) = client {
33        let cookie = format!(
34            "sessionid={}; sessionid_sign={}; device_t={};",
35            client.session,
36            client.session_signature,
37            client.device_token
38        );
39        let client = build_request(Some(&cookie))?;
40        let response = client.get(url).send().await?;
41        return Ok(response);
42    }
43    Ok(build_request(None)?.get(url).send().await?)
44}
45
46/// Searches for a symbol using the specified search parameters.
47///
48/// # Arguments
49///
50/// * `search` - A string slice representing the search query.
51/// * `exchange` - A string slice representing the exchange to search in.
52/// * `market_type` - A `SymbolMarketType` enum representing the type of market to search in.
53/// * `start` - An unsigned 64-bit integer representing the starting index of the search results.
54/// * `country` - A string slice representing the country to search in.
55/// * `domain` - A string slice representing the domain to search in. Defaults to "production" if empty.
56///
57/// # Returns
58///
59/// A `Result` containing a `SymbolSearchResponse` struct representing the search results, or an error if the search failed.
60#[tracing::instrument]
61pub async fn search_symbol(
62    search: &str,
63    exchange: &str,
64    market_type: &SymbolMarketType,
65    start: u64,
66    country: &str,
67    domain: &str
68) -> Result<SymbolSearchResponse> {
69    let search_data: SymbolSearchResponse = get(
70        None,
71        &format!(
72            "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}",
73            search = search,
74            exchange = exchange,
75            search_type = market_type,
76            start = start,
77            country = country,
78            domain = if domain.is_empty() { "production" } else { domain }
79        )
80    ).await?.json().await?;
81    Ok(search_data)
82}
83
84/// Lists symbols based on the specified search parameters.
85///
86/// # Arguments
87///
88/// * `exchange` - An optional string representing the exchange to search in.
89/// * `market_type` - An optional `SymbolMarketType` enum representing the type of market to search in.
90/// * `country` - An optional string representing the country to search in.
91/// * `domain` - An optional string representing the domain to search in.
92///
93/// # Returns
94///
95/// A `Result` containing a vector of `Symbol` structs representing the search results, or an error if the search failed.
96#[tracing::instrument]
97pub async fn list_symbols(
98    exchange: Option<String>,
99    market_type: Option<SymbolMarketType>,
100    country: Option<String>,
101    domain: Option<String>
102) -> Result<Vec<Symbol>> {
103    let market_type: Arc<SymbolMarketType> = Arc::new(market_type.unwrap_or_default());
104    let exchange: Arc<String> = Arc::new(exchange.unwrap_or("".to_string()));
105    let country = Arc::new(country.unwrap_or("".to_string()));
106    let domain = Arc::new(domain.unwrap_or("".to_string()));
107
108    let search_symbol_reps = search_symbol(
109        "",
110        &exchange,
111        &market_type,
112        0,
113        &country,
114        &domain
115    ).await?;
116    let remaining = search_symbol_reps.remaining;
117    let mut symbols = search_symbol_reps.symbols;
118
119    let max_concurrent_tasks = 50;
120    let semaphore = Arc::new(Semaphore::new(max_concurrent_tasks));
121
122    let mut tasks = Vec::new();
123
124    for i in (50..remaining).step_by(50) {
125        let market_type = Arc::clone(&market_type);
126        let exchange = Arc::clone(&exchange);
127        let country = Arc::clone(&country);
128        let domain = Arc::clone(&domain);
129        let semaphore = Arc::clone(&semaphore);
130
131        let task = tokio::spawn(async move {
132            let _permit = semaphore.acquire().await.unwrap();
133            search_symbol("", &exchange, &market_type, i, &country, &domain).await.map(
134                |resp| resp.symbols
135            )
136        });
137
138        tasks.push(task);
139    }
140
141    for handler in tasks {
142        symbols.extend(handler.await??);
143    }
144
145    Ok(symbols)
146}
147
148/// Retrieves a chart token for the specified layout ID using the provided client.
149///
150/// # Arguments
151///
152/// * `client` - A reference to a `UserCookies` struct representing the client to use for the request.
153/// * `layout_id` - A string slice representing the layout ID to retrieve the chart token for.
154///
155/// # Returns
156///
157/// A `Result` containing a string representing the chart token, or an error if the token could not be retrieved.
158#[tracing::instrument(skip(client))]
159pub async fn get_chart_token(client: &UserCookies, layout_id: &str) -> Result<String> {
160    let data: Value = get(
161        Some(client),
162        &format!(
163            "https://www.tradingview.com/chart-token/?image_url={}&user_id={}",
164            layout_id,
165            client.id
166        )
167    ).await?.json().await?;
168
169    match data.get("token") {
170        Some(token) => {
171            return Ok(match token.as_str() {
172                Some(token) => token.to_string(),
173                None => {
174                    return Err(Error::NoChartTokenFound);
175                }
176            });
177        }
178        None => { Err(Error::NoChartTokenFound) }
179    }
180}
181
182/// Retrieves the quote token from TradingView.
183///
184/// # Arguments
185///
186/// * `client` - A reference to a `UserCookies` struct containing the user's cookies.
187///
188/// # Returns
189///
190/// A `Result` containing a `String` with the quote token if successful, or an error if the request fails.
191#[tracing::instrument(skip(client))]
192pub async fn get_quote_token(client: &UserCookies) -> Result<String> {
193    let data: String = get(
194        Some(client),
195        "https://www.tradingview.com/quote_token"
196    ).await?.json().await?;
197    Ok(data)
198}
199
200/// Retrieves a chart drawing from TradingView's charts-storage API.
201///
202/// # Arguments
203///
204/// * `client` - A reference to a `UserCookies` instance.
205/// * `layout_id` - The ID of the chart layout.
206/// * `symbol` - The symbol of the financial instrument to retrieve the chart drawing for.
207/// * `chart_id` - (Optional) The ID of the chart to retrieve. If not provided, the shared chart will be retrieved.
208///
209/// # Returns
210///
211/// A `Result` containing a `ChartDrawing` instance if successful, or an error if the request fails.
212#[tracing::instrument(skip(client))]
213pub async fn get_drawing(
214    client: &UserCookies,
215    layout_id: &str,
216    symbol: &str,
217    chart_id: Option<&str>
218) -> Result<ChartDrawing> {
219    let token = get_chart_token(client, layout_id).await?;
220
221    debug!("Chart token: {}", token);
222    let url = format!(
223        "https://charts-storage.tradingview.com/charts-storage/get/layout/{layout_id}/sources?chart_id={chart_id}&jwt={token}&symbol={symbol}",
224        layout_id = layout_id,
225        chart_id = chart_id.unwrap_or("_shared"),
226        token = token,
227        symbol = symbol
228    );
229
230    let response_data: ChartDrawing = get(Some(client), &url).await?.json().await?;
231
232    Ok(response_data)
233}
234
235#[tracing::instrument(skip(client))]
236pub async fn get_private_indicators(client: &UserCookies) -> Result<Vec<PineInfo>> {
237    let indicators = get(
238        Some(client),
239        "https://pine-facade.tradingview.com/pine-facade/list?filter=saved"
240    ).await?.json::<Vec<PineInfo>>().await?;
241    Ok(indicators)
242}
243
244// Retrieves a list of built-in indicators of the specified type.
245///
246/// # Arguments
247///
248/// * `indicator_type` - A `BuiltinIndicators` enum representing the type of built-in indicators to retrieve.
249///
250/// # Returns
251///
252/// A `Result` containing a vector of `PineInfo` structs representing the built-in indicators, or an error if the indicators could not be retrieved.
253#[tracing::instrument]
254pub async fn get_builtin_indicators(indicator_type: BuiltinIndicators) -> Result<Vec<PineInfo>> {
255    let indicator_types = match indicator_type {
256        BuiltinIndicators::All => vec!["fundamental", "standard", "candlestick"],
257        BuiltinIndicators::Fundamental => vec!["fundamental"],
258        BuiltinIndicators::Standard => vec!["standard"],
259        BuiltinIndicators::Candlestick => vec!["candlestick"],
260    };
261    let mut indicators: Vec<PineInfo> = vec![];
262
263    let mut tasks: Vec<JoinHandle<Result<Vec<PineInfo>>>> = Vec::new();
264
265    for indicator_type in indicator_types {
266        let url =
267            format!("https://pine-facade.tradingview.com/pine-facade/list/?filter={}", indicator_type);
268        let task = tokio::spawn(async move {
269            let data = get(None, &url).await?.json::<Vec<PineInfo>>().await?;
270            Ok(data)
271        });
272
273        tasks.push(task);
274    }
275
276    for handler in tasks {
277        indicators.extend(handler.await??);
278    }
279
280    Ok(indicators)
281}
282
283/// Searches for indicators on TradingView.
284///
285/// # Arguments
286///
287/// * `client` - An optional reference to a `UserCookies` object.
288/// * `search` - A string slice containing the search query.
289/// * `offset` - An integer representing the offset of the search results.
290///
291/// # Returns
292///
293/// A `Result` containing a vector of `PineSearchResult` objects if successful, or an `Error` if unsuccessful.
294///
295/// # Example
296///
297/// ```rust
298/// use tradingview::api::search_indicator;
299///
300/// #[tokio::main]
301/// async fn main() {
302///     let results = search_indicator(None, "rsi", 0).await.unwrap();
303///     println!("{:?}", results);
304/// }
305/// ```
306#[tracing::instrument(skip(client))]
307pub async fn search_indicator(
308    client: Option<&UserCookies>,
309    search: &str,
310    offset: i32
311) -> Result<Vec<PineSearchResult>> {
312    let url = format!(
313        "https://www.tradingview.com/pubscripts-suggest-json/?search={}&offset={}",
314        search,
315        offset
316    );
317    let resp: pine_indicator::SearchResponse = get(client, &url).await?.json().await?;
318    debug!("Response: {:?}", resp);
319
320    if resp.results.is_empty() {
321        return Err(Error::Generic("No results found".to_string()));
322    }
323
324    Ok(resp.results)
325}
326
327/// Retrieves metadata for a TradingView Pine indicator.
328///
329/// # Arguments
330///
331/// * `client` - An optional reference to a `UserCookies` struct.
332/// * `pinescript_id` - The ID of the Pine script.
333/// * `pinescript_version` - The version of the Pine script.
334///
335/// # Returns
336///
337/// Returns a `Result` containing a `PineMetadata` struct if successful, or an `Error` if unsuccessful.
338///
339/// # Examples
340///
341/// ```rust
342/// use tradingview::api::get_indicator_metadata;
343///
344/// async fn run() -> Result<(), Box<dyn std::error::Error>> {
345///     let client = None;
346///     let pinescript_id = "PUB;2187";
347///     let pinescript_version = "-1";
348///
349///     let metadata = get_indicator_metadata(client.as_ref(), pinescript_id, pinescript_version).await?;
350///     println!("{:?}", metadata);
351///     Ok(())
352/// }
353/// ```
354#[tracing::instrument(skip(client))]
355pub async fn get_indicator_metadata(
356    client: Option<&UserCookies>,
357    pinescript_id: &str,
358    pinescript_version: &str
359) -> Result<PineMetadata> {
360    use urlencoding::encode;
361    let url = format!(
362        "https://pine-facade.tradingview.com/pine-facade/translate/{}/{}",
363        encode(pinescript_id),
364        encode(pinescript_version)
365    );
366    debug!("URL: {}", url);
367    let resp: pine_indicator::TranslateResponse = get(client, &url).await?.json().await?;
368
369    if resp.success {
370        return Ok(resp.result);
371    }
372
373    Err(Error::Generic("Failed to get indicator metadata".to_string()))
374}