tradingview/client/
misc.rs1use 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
21async 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#[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#[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#[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#[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#[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#[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#[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#[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}