tradingview_screener.query
1from __future__ import annotations 2 3__all__ = ['And', 'Or', 'Query'] 4 5import copy 6import pprint 7from typing import TYPE_CHECKING 8 9import requests 10 11from tradingview_screener.column import Column 12 13if TYPE_CHECKING: 14 import pandas as pd 15 from typing import Literal, Any 16 from typing_extensions import Self 17 from tradingview_screener.models import ( 18 QueryDict, 19 SortByDict, 20 ScreenerDict, 21 FilterOperationDict, 22 OperationDict, 23 ScreenerDictV2, 24 ) 25 26 27DEFAULT_RANGE = [0, 50] 28URL = 'https://scanner.tradingview.com/{market}/scan' 29HEADERS = { 30 'authority': 'scanner.tradingview.com', 31 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"', 32 'accept': 'text/plain, */*; q=0.01', 33 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 34 'sec-ch-ua-mobile': '?0', 35 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)' 36 'Chrome/98.0.4758.102 Safari/537.36', 37 'sec-ch-ua-platform': '"Windows"', 38 'origin': 'https://www.tradingview.com', 39 'sec-fetch-site': 'same-site', 40 'sec-fetch-mode': 'cors', 41 'sec-fetch-dest': 'empty', 42 'referer': 'https://www.tradingview.com/', 43 'accept-language': 'en-US,en;q=0.9,it;q=0.8', 44} 45STOCKS_QUERY = { 46 'markets': [], 47 'symbols': {}, 48 'options': {'lang': 'en'}, 49 'columns': [ 50 'name', 51 'close', 52 'type', 53 'typespecs', 54 'pricescale', 55 'minmov', 56 'fractional', 57 'minmove2', 58 'currency', 59 'change', 60 'volume', 61 'relative_volume_10d_calc', 62 'market_cap_basic', 63 'fundamental_currency_code', 64 'price_earnings_ttm', 65 'earnings_per_share_diluted_ttm', 66 'earnings_per_share_diluted_yoy_growth_ttm', 67 'dividends_yield_current', 68 'sector.tr', 69 'market', 70 'sector', 71 'AnalystRating', 72 'AnalystRating.tr', 73 ], 74 'filter': [{'left': 'is_primary', 'operation': 'equal', 'right': True}], 75 'filter2': { 76 'operator': 'and', 77 'operands': [ 78 { 79 'operation': { 80 'operator': 'or', 81 'operands': [ 82 { 83 'operation': { 84 'operator': 'and', 85 'operands': [ 86 { 87 'expression': { 88 'left': 'type', 89 'operation': 'equal', 90 'right': 'stock', 91 } 92 }, 93 { 94 'expression': { 95 'left': 'typespecs', 96 'operation': 'has', 97 'right': ['common'], 98 } 99 }, 100 ], 101 } 102 }, 103 { 104 'operation': { 105 'operator': 'and', 106 'operands': [ 107 { 108 'expression': { 109 'left': 'type', 110 'operation': 'equal', 111 'right': 'stock', 112 } 113 }, 114 { 115 'expression': { 116 'left': 'typespecs', 117 'operation': 'has', 118 'right': ['preferred'], 119 } 120 }, 121 ], 122 } 123 }, 124 { 125 'operation': { 126 'operator': 'and', 127 'operands': [ 128 { 129 'expression': { 130 'left': 'type', 131 'operation': 'equal', 132 'right': 'dr', 133 } 134 }, 135 ], 136 } 137 }, 138 { 139 'operation': { 140 'operator': 'and', 141 'operands': [ 142 { 143 'expression': { 144 'left': 'type', 145 'operation': 'equal', 146 'right': 'fund', 147 } 148 }, 149 { 150 'expression': { 151 'left': 'typespecs', 152 'operation': 'has_none_of', 153 'right': ['etf', 'mutual', 'closedend'], 154 } 155 }, 156 ], 157 } 158 }, 159 ], 160 } 161 }, 162 {'expression': {'left': 'typespecs', 'operation': 'has_none_of', 'right': ['pre-ipo']}}, 163 ], 164 }, 165 'sort': {'sortBy': 'market_cap_basic', 'sortOrder': 'desc'}, 166 'range': DEFAULT_RANGE.copy(), 167 'ignore_unknown_fields': False, 168} 169 170 171def _impl_and_or_chaining( 172 expressions: tuple[FilterOperationDict | OperationDict, ...], operator: Literal['and', 'or'] 173) -> OperationDict: 174 # we want to wrap all the `FilterOperationDict` expressions with `{'expression': expr}`, 175 # to know if it's an instance of `FilterOperationDict` we simply check if it has the `left` key, 176 # which no other TypedDict has. 177 lst = [] 178 for expr in expressions: 179 if 'left' in expr: # if isinstance(expr, FilterOperationDict): ... 180 lst.append({'expression': expr}) 181 else: 182 lst.append(expr) 183 return {'operation': {'operator': operator, 'operands': lst}} 184 185 186def And(*expressions: FilterOperationDict | OperationDict) -> OperationDict: 187 return _impl_and_or_chaining(expressions, operator='and') 188 189 190def Or(*expressions: FilterOperationDict | OperationDict) -> OperationDict: 191 return _impl_and_or_chaining(expressions, operator='or') 192 193 194class Query: 195 """ 196 This class allows you to perform SQL-like queries on the tradingview stock-screener. 197 198 The `Query` object reppresents a query that can be made to the official tradingview API, and it 199 stores all the data as JSON internally. 200 201 Examples: 202 203 To perform a simple query all you have to do is: 204 >>> from tradingview_screener import Query 205 >>> Query().get_scanner_data() 206 (18060, 207 ticker name close volume market_cap_basic 208 0 AMEX:SPY SPY 410.68 107367671 NaN 209 1 NASDAQ:QQQ QQQ 345.31 63475390 NaN 210 2 NASDAQ:TSLA TSLA 207.30 94879471 6.589904e+11 211 3 NASDAQ:NVDA NVDA 405.00 41677185 1.000350e+12 212 4 NASDAQ:AMZN AMZN 127.74 125309313 1.310658e+12 213 .. ... ... ... ... ... 214 45 NYSE:UNH UNH 524.66 2585616 4.859952e+11 215 46 NASDAQ:DXCM DXCM 89.29 14954605 3.449933e+10 216 47 NYSE:MA MA 364.08 3624883 3.429080e+11 217 48 NYSE:ABBV ABBV 138.93 9427212 2.452179e+11 218 49 AMEX:XLK XLK 161.12 8115780 NaN 219 [50 rows x 5 columns]) 220 221 The `get_scanner_data()` method will return a tuple with the first element being the number of 222 records that were found (like a `COUNT(*)`), and the second element contains the data that was 223 found as a DataFrame. 224 225 --- 226 227 By default, the `Query` will select the columns: `name`, `close`, `volume`, `market_cap_basic`, 228 but you override that 229 >>> (Query() 230 ... .select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI', 'Price to Earnings Ratio (TTM)') 231 ... .get_scanner_data()) 232 (18060, 233 ticker open high ... MACD.macd RSI price_earnings_ttm 234 0 AMEX:SPY 414.19 414.600 ... -5.397135 29.113396 NaN 235 1 NASDAQ:QQQ 346.43 348.840 ... -4.321482 34.335449 NaN 236 2 NASDAQ:TSLA 210.60 212.410 ... -12.224250 28.777229 66.752536 237 3 NASDAQ:NVDA 411.30 412.060 ... -8.738986 37.845668 97.835540 238 4 NASDAQ:AMZN 126.20 130.020 ... -2.025378 48.665666 66.697995 239 .. ... ... ... ... ... ... ... 240 45 NYSE:UNH 525.99 527.740 ... 6.448129 54.614775 22.770713 241 46 NASDAQ:DXCM 92.73 92.988 ... -2.376942 52.908093 98.914368 242 47 NYSE:MA 366.49 368.285 ... -7.496065 22.614078 31.711800 243 48 NYSE:ABBV 138.77 143.000 ... -1.708497 27.117232 37.960054 244 49 AMEX:XLK 161.17 162.750 ... -1.520828 36.868658 NaN 245 [50 rows x 8 columns]) 246 247 You can find the 250+ columns available in `tradingview_screener.constants.COLUMNS`. 248 249 Now let's do some queries using the `WHERE` statement, select all the stocks that the `close` is 250 bigger or equal than 350 251 >>> (Query() 252 ... .select('close', 'volume', '52 Week High') 253 ... .where(Column('close') >= 350) 254 ... .get_scanner_data()) 255 (159, 256 ticker close volume price_52_week_high 257 0 AMEX:SPY 410.68 107367671 459.44 258 1 NASDAQ:NVDA 405.00 41677185 502.66 259 2 NYSE:BRK.A 503375.05 7910 566569.97 260 3 AMEX:IVV 412.55 5604525 461.88 261 4 AMEX:VOO 377.32 5638752 422.15 262 .. ... ... ... ... 263 45 NASDAQ:EQIX 710.39 338549 821.63 264 46 NYSE:MCK 448.03 527406 465.90 265 47 NYSE:MTD 976.25 241733 1615.97 266 48 NASDAQ:CTAS 496.41 464631 525.37 267 49 NASDAQ:ROP 475.57 450141 508.90 268 [50 rows x 4 columns]) 269 270 You can even use other columns in these kind of operations 271 >>> (Query() 272 ... .select('close', 'VWAP') 273 ... .where(Column('close') >= Column('VWAP')) 274 ... .get_scanner_data()) 275 (9044, 276 ticker close VWAP 277 0 NASDAQ:AAPL 168.22 168.003333 278 1 NASDAQ:META 296.73 296.336667 279 2 NASDAQ:GOOGL 122.17 121.895233 280 3 NASDAQ:AMD 96.43 96.123333 281 4 NASDAQ:GOOG 123.40 123.100000 282 .. ... ... ... 283 45 NYSE:GD 238.25 238.043333 284 46 NYSE:GOLD 16.33 16.196667 285 47 AMEX:SLV 21.18 21.041667 286 48 AMEX:VXX 27.08 26.553333 287 49 NYSE:SLB 55.83 55.676667 288 [50 rows x 3 columns]) 289 290 Let's find all the stocks that the price is between the EMA 5 and 20, and the type is a stock 291 or fund 292 >>> (Query() 293 ... .select('close', 'volume', 'EMA5', 'EMA20', 'type') 294 ... .where( 295 ... Column('close').between(Column('EMA5'), Column('EMA20')), 296 ... Column('type').isin(['stock', 'fund']) 297 ... ) 298 ... .get_scanner_data()) 299 (1730, 300 ticker close volume EMA5 EMA20 type 301 0 NASDAQ:AMZN 127.74 125309313 125.033517 127.795142 stock 302 1 AMEX:HYG 72.36 35621800 72.340776 72.671058 fund 303 2 AMEX:LQD 99.61 21362859 99.554272 100.346388 fund 304 3 NASDAQ:IEF 90.08 11628236 89.856804 90.391503 fund 305 4 NYSE:SYK 261.91 3783608 261.775130 266.343290 stock 306 .. ... ... ... ... ... ... 307 45 NYSE:EMN 72.58 1562328 71.088034 72.835394 stock 308 46 NYSE:KIM 16.87 6609420 16.858920 17.096582 fund 309 47 NASDAQ:COLM 71.34 1516675 71.073116 71.658864 stock 310 48 NYSE:AOS 67.81 1586796 67.561619 67.903168 stock 311 49 NASDAQ:IGIB 47.81 2073538 47.761338 48.026795 fund 312 [50 rows x 6 columns]) 313 314 There are also the `ORDER BY`, `OFFSET`, and `LIMIT` statements. 315 Let's select all the tickers with a market cap between 1M and 50M, that have a relative volume 316 bigger than 1.2, and that the MACD is positive 317 >>> (Query() 318 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 319 ... .where( 320 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 321 ... Column('relative_volume_10d_calc') > 1.2, 322 ... Column('MACD.macd') >= Column('MACD.signal') 323 ... ) 324 ... .order_by('volume', ascending=False) 325 ... .offset(5) 326 ... .limit(15) 327 ... .get_scanner_data()) 328 (393, 329 ticker name close volume relative_volume_10d_calc 330 0 OTC:YCRM YCRM 0.0120 19626514 1.887942 331 1 OTC:PLPL PLPL 0.0002 17959914 3.026059 332 2 NASDAQ:ABVC ABVC 1.3800 16295824 1.967505 333 3 OTC:TLSS TLSS 0.0009 15671157 1.877976 334 4 OTC:GVSI GVSI 0.0128 14609774 2.640792 335 5 OTC:IGEX IGEX 0.0012 14285592 1.274861 336 6 OTC:EEGI EEGI 0.0004 12094000 2.224749 337 7 NASDAQ:GLG GLG 0.0591 9811974 1.990526 338 8 NASDAQ:TCRT TCRT 0.0890 8262894 2.630553 339 9 OTC:INKW INKW 0.0027 7196404 1.497134) 340 341 To avoid rewriting the same query again and again, you can save the query to a variable and 342 just call `get_scanner_data()` again and again to get the latest data: 343 >>> top_50_bullish = (Query() 344 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 345 ... .where( 346 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 347 ... Column('relative_volume_10d_calc') > 1.2, 348 ... Column('MACD.macd') >= Column('MACD.signal') 349 ... ) 350 ... .order_by('volume', ascending=False) 351 ... .limit(50)) 352 >>> top_50_bullish.get_scanner_data() 353 (393, 354 ticker name close volume relative_volume_10d_calc 355 0 OTC:BEGI BEGI 0.001050 127874055 3.349924 356 1 OTC:HCMC HCMC 0.000100 126992562 1.206231 357 2 OTC:HEMP HEMP 0.000150 101382713 1.775458 358 3 OTC:SONG SONG 0.000800 92640779 1.805721 359 4 OTC:APRU APRU 0.001575 38104499 29.028958 360 .. ... ... ... ... ... 361 45 OTC:BSHPF BSHPF 0.001000 525000 1.280899 362 46 OTC:GRHI GRHI 0.033000 507266 1.845738 363 47 OTC:OMGGF OMGGF 0.035300 505000 4.290059 364 48 NASDAQ:GBNH GBNH 0.273000 500412 9.076764 365 49 OTC:CLRMF CLRMF 0.032500 496049 17.560935 366 [50 rows x 5 columns]) 367 """ 368 369 def __init__(self, market: str = 'america') -> None: 370 # noinspection PyTypeChecker 371 self.query: QueryDict = copy.deepcopy(STOCKS_QUERY) 372 self.query['markets'] = [market] 373 self.url = URL.format(market=market) 374 375 def select(self, *columns: Column | str) -> Self: 376 self.query['columns'] = [ 377 col.name if isinstance(col, Column) else Column(col).name for col in columns 378 ] 379 return self 380 381 def where(self, *expressions: FilterOperationDict) -> Self: 382 """ 383 Filter screener (expressions are joined with the AND operator) 384 """ 385 self.query['filter'] = list(expressions) # convert tuple[dict] -> list[dict] 386 return self 387 388 def where2(self, operation: OperationDict) -> Self: 389 """ 390 Filter screener using AND/OR operators (nested expressions also allowed) 391 392 Rules: 393 1. The argument passed to `where2()` **must** be wrapped in `And()` or `Or()`. 394 2. `And()` and `Or()` can accept one or more conditions as arguments. 395 3. Conditions can be simple (e.g., `Column('field') == 'value'`) or complex, allowing nesting of `And()` and `Or()` to create intricate logical filters. 396 4. Unlike the `where()` method, which only supports chaining conditions with the `AND` operator, `where2()` allows mixing and nesting of `AND` and `OR` operators. 397 398 Examples: 399 400 1. **Combining conditions with `OR` and nested `AND`:** 401 ```python 402 Query() 403 .select('type', 'typespecs') 404 .where2( 405 Or( 406 And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])), 407 And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])), 408 Column('type') == 'dr', 409 ) 410 ) 411 ``` 412 413 This query filters entities where: 414 - The `type` is `'stock'` and `typespecs` contains `'common'` or `'preferred'`, **OR** 415 - The `type` is `'fund'` and `typespecs` does not contain `'etf'`, **OR** 416 - The `type` is `'dr'`. 417 418 2. **Mixing conditions with `OR`:** 419 ```python 420 Query().where2( 421 Or( 422 And(col('type') == 'stock', col('typespecs').has(['common'])), 423 col('type') == 'fund', 424 ) 425 ) 426 ``` 427 This query filters entities where: 428 - The `type` is `'stock'` and `typespecs` contains `'common'`, **OR** 429 - The `type` is `'fund'`. 430 431 3. **Combining conditions with `AND`:** 432 ```python 433 Query() 434 .set_markets('crypto') 435 .where2( 436 And( 437 col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']), 438 col('currency_id') == 'USD', 439 ) 440 ) 441 ``` 442 This query filters entities in the `'crypto'` market where: 443 - The `exchange` is one of `'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff'`, **AND** 444 - The `currency_id` is `'USD'`. 445 """ 446 self.query['filter2'] = operation['operation'] 447 return self 448 449 def order_by( 450 self, column: Column | str, ascending: bool = True, nulls_first: bool = False 451 ) -> Self: 452 """ 453 Applies sorting to the query results based on the specified column. 454 455 Examples: 456 457 >>> Query().order_by('volume', ascending=False) # sort descending 458 >>> Query().order_by('close', ascending=True) 459 >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False) 460 461 :param column: Either a `Column` object or a string with the column name. 462 :param ascending: Set to True for ascending order (default), or False for descending. 463 :param nulls_first: If True, places `None` values at the beginning of the results. Defaults 464 to False. 465 :return: The updated query object. 466 """ 467 dct: SortByDict = { 468 'sortBy': column.name if isinstance(column, Column) else column, 469 'sortOrder': 'asc' if ascending else 'desc', 470 'nullsFirst': nulls_first, 471 } 472 self.query['sort'] = dct 473 return self 474 475 def limit(self, limit: int) -> Self: 476 self.query.setdefault('range', DEFAULT_RANGE.copy())[1] = limit 477 return self 478 479 def offset(self, offset: int) -> Self: 480 self.query.setdefault('range', DEFAULT_RANGE.copy())[0] = offset 481 return self 482 483 def set_markets(self, *markets: str) -> Self: 484 """ 485 This method allows you to select the market/s which you want to query. 486 487 By default, the screener will only scan US equities, but you can change it to scan any 488 market or country, that includes a list of 67 countries, and also the following 489 asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`, 490 `futures`, `options`. 491 492 You may choose any value from `tradingview_screener.constants.MARKETS`. 493 494 If you select multiple countries, you might want to 495 496 Examples: 497 498 By default, the screener will show results from the `america` market, but you can 499 change it (note the difference between `market` and `country`) 500 >>> columns = ['close', 'market', 'country', 'currency'] 501 >>> (Query() 502 ... .select(*columns) 503 ... .set_markets('italy') 504 ... .get_scanner_data()) 505 (2346, 506 ticker close market country currency 507 0 MIL:UCG 23.9150 italy Italy EUR 508 1 MIL:ISP 2.4910 italy Italy EUR 509 2 MIL:STLAM 17.9420 italy Netherlands EUR 510 3 MIL:ENEL 6.0330 italy Italy EUR 511 4 MIL:ENI 15.4800 italy Italy EUR 512 .. ... ... ... ... ... 513 45 MIL:UNI 5.1440 italy Italy EUR 514 46 MIL:3OIS 0.4311 italy Ireland EUR 515 47 MIL:3SIL 35.2300 italy Ireland EUR 516 48 MIL:IWDE 69.1300 italy Ireland EUR 517 49 MIL:QQQS 19.2840 italy Ireland EUR 518 [50 rows x 5 columns]) 519 520 You can also select multiple markets 521 >>> (Query() 522 ... .select(*columns) 523 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 524 ... .get_scanner_data()) 525 (23964, 526 ticker close market country currency 527 0 AMEX:SPY 420.1617 america United States USD 528 1 NASDAQ:TSLA 201.2000 america United States USD 529 2 NASDAQ:NVDA 416.7825 america United States USD 530 3 NASDAQ:AMD 106.6600 america United States USD 531 4 NASDAQ:QQQ 353.7985 america United States USD 532 .. ... ... ... ... ... 533 45 NASDAQ:GOOGL 124.9200 america United States USD 534 46 HKEX:1211 233.2000 hongkong China HKD 535 47 TASE:ALHE 1995.0000 israel Israel ILA 536 48 AMEX:BIL 91.4398 america United States USD 537 49 NASDAQ:GOOG 126.1500 america United States USD 538 [50 rows x 5 columns]) 539 540 You may also select different financial instruments 541 >>> (Query() 542 ... .select('close', 'market') 543 ... .set_markets('cfd', 'crypto', 'forex', 'futures') 544 ... .get_scanner_data()) 545 (118076, 546 ticker ... market 547 0 UNISWAP3ETH:JUSTICEUSDT ... crypto 548 1 UNISWAP3ETH:UAHGUSDT ... crypto 549 2 UNISWAP3ETH:KENDUWETH ... crypto 550 3 UNISWAP3ETH:MATICSTMATIC ... crypto 551 4 UNISWAP3ETH:WETHETHM ... crypto 552 .. ... ... ... 553 45 UNISWAP:MUSICAIWETH_1F5304.USD ... crypto 554 46 CRYPTOCAP:FIL ... cfd 555 47 CRYPTOCAP:SUI ... cfd 556 48 CRYPTOCAP:ARBI ... cfd 557 49 CRYPTOCAP:OP ... cfd 558 [50 rows x 3 columns]) 559 560 :param markets: one or more markets from `tradingview_screener.constants.MARKETS` 561 :return: Self 562 """ 563 if len(markets) == 1: 564 market = markets[0] 565 self.url = URL.format(market=market) 566 self.query['markets'] = [market] 567 else: # len(markets) == 0 or len(markets) > 1 568 self.url = URL.format(market='global') 569 self.query['markets'] = list(markets) 570 571 return self 572 573 def set_tickers(self, *tickers: str) -> Self: 574 """ 575 Set the tickers you wish to receive information on. 576 577 Note that this resets the markets and sets the URL market to `global`. 578 579 Examples: 580 581 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 582 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 583 (1, 584 ticker name market close volume VWAP MACD.macd 585 0 NASDAQ:TSLA TSLA america 186 3519931 185.53 2.371601) 586 587 To set tickers from multiple markets we need to update the markets that include them: 588 >>> (Query() 589 ... .set_markets('america', 'italy', 'vietnam') 590 ... .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX') 591 ... .get_scanner_data()) 592 (4, 593 ticker name close volume market_cap_basic 594 0 HOSE:VIX VIX 16700.00 33192500 4.568961e+08 595 1 AMEX:SPY SPY 544.35 1883562 NaN 596 2 NYSE:GME GME 23.80 3116758 1.014398e+10 597 3 MIL:RACE RACE 393.30 122878 1.006221e+11) 598 599 :param tickers: One or more tickers, syntax: `exchange:symbol` 600 :return: Self 601 """ 602 self.query.setdefault('symbols', {})['tickers'] = list(tickers) 603 self.set_markets() 604 return self 605 606 def set_index(self, *indexes: str) -> Self: 607 """ 608 Scan only the equities that are in the given index (or indexes). 609 610 Note that this resets the markets and sets the URL market to `global`. 611 612 Examples: 613 614 >>> Query().set_index('SYML:SP;SPX').get_scanner_data() 615 (503, 616 ticker name close volume market_cap_basic 617 0 NASDAQ:NVDA NVDA 1208.88 41238122 2.973644e+12 618 1 NASDAQ:AAPL AAPL 196.89 53103705 3.019127e+12 619 2 NASDAQ:TSLA TSLA 177.48 56244929 5.660185e+11 620 3 NASDAQ:AMD AMD 167.87 44795240 2.713306e+11 621 4 NASDAQ:MSFT MSFT 423.85 13621485 3.150183e+12 622 5 NASDAQ:AMZN AMZN 184.30 28021473 1.917941e+12 623 6 NASDAQ:META META 492.96 9379199 1.250410e+12 624 7 NASDAQ:GOOGL GOOGL 174.46 19660698 2.164346e+12 625 8 NASDAQ:SMCI SMCI 769.11 3444852 4.503641e+10 626 9 NASDAQ:GOOG GOOG 175.95 14716134 2.164346e+12 627 10 NASDAQ:AVGO AVGO 1406.64 1785876 6.518669e+11) 628 629 You can set multiple indices as well, like the NIFTY 50 and UK 100 Index. 630 >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data() 631 (150, 632 ticker name close volume market_cap_basic 633 0 NSE:INFY INFY 1533.600000 24075302 7.623654e+10 634 1 LSE:AZN AZN 12556.000000 2903032 2.489770e+11 635 2 NSE:HDFCBANK HDFCBANK 1573.350000 18356108 1.432600e+11 636 3 NSE:RELIANCE RELIANCE 2939.899900 9279348 2.381518e+11 637 4 LSE:LSEG LSEG 9432.000000 2321053 6.395329e+10 638 5 NSE:BAJFINANCE BAJFINANCE 7191.399900 2984052 5.329685e+10 639 6 LSE:BARC BARC 217.250000 96238723 4.133010e+10 640 7 NSE:SBIN SBIN 829.950010 25061284 8.869327e+10 641 8 NSE:LT LT 3532.500000 5879660 5.816100e+10 642 9 LSE:SHEL SHEL 2732.500000 7448315 2.210064e+11) 643 644 You can find the full list of indices in [`constants.INDICES`](constants.html#INDICES), 645 just note that the syntax is 646 `SYML:{source};{symbol}`. 647 648 :param indexes: One or more strings representing the financial indexes to filter by 649 :return: An instance of the `Query` class with the filter applied 650 """ 651 self.query.setdefault('preset', 'index_components_market_pages') 652 self.query.setdefault('symbols', {})['symbolset'] = list(indexes) 653 # reset markets list and URL to `/global` 654 self.set_markets() 655 return self 656 657 # def set_currency(self, currency: Literal['symbol', 'market'] | str) -> Self: 658 # """ 659 # Change the currency of the screener. 660 # 661 # Note that this changes *only* the currency of the columns of type `fundamental_price`, 662 # for example: `market_cap_basic`, `net_income`, `total_debt`. Other columns like `close` and 663 # `Value.Traded` won't change, because they are of a different type. 664 # 665 # This can be particularly useful if you are querying tickers across different markets. 666 # 667 # Examples: 668 # 669 # >>> Query().set_currency('symbol') # convert to symbol currency 670 # >>> Query().set_currency('market') # convert to market currency 671 # >>> Query().set_currency('usd') # or another currency 672 # >>> Query().set_currency('jpy') 673 # >>> Query().set_currency('eur') 674 # """ 675 # # symbol currency -> self.query['price_conversion'] = {'to_symbol': True} 676 # # market currency -> self.query['price_conversion'] = {'to_symbol': False} 677 # # USD or other currency -> self.query['price_conversion'] = {'to_currency': 'usd'} 678 # if currency == 'symbol': 679 # self.query['price_conversion'] = {'to_symbol': True} 680 # elif currency == 'market': 681 # self.query['price_conversion'] = {'to_symbol': False} 682 # else: 683 # self.query['price_conversion'] = {'to_currency': currency} 684 # return self 685 686 def set_property(self, key: str, value: Any) -> Self: 687 self.query[key] = value 688 return self 689 690 def get_scanner_data_raw(self, **kwargs) -> ScreenerDict | ScreenerDictV2: 691 """ 692 Perform a POST web-request and return the data from the API (dictionary). 693 694 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 695 this can be very useful if you want to pass your own headers/cookies. 696 697 >>> Query().select('close', 'volume').limit(5).get_scanner_data_raw() 698 { 699 'totalCount': 17559, 700 'data': [ 701 {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]}, 702 {'s': 'AMEX:SPY', 'd': [542.04, 52331224]}, 703 {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]}, 704 {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]}, 705 {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]}, 706 ], 707 } 708 """ 709 self.query.setdefault('range', DEFAULT_RANGE.copy()) 710 711 kwargs.setdefault('headers', HEADERS) 712 kwargs.setdefault('timeout', 20) 713 r = requests.post(self.url, json=self.query, **kwargs) 714 715 if not r.ok: 716 # add the body to the error message for debugging purposes 717 r.reason += f'\n Body: {r.text}\n' 718 r.raise_for_status() 719 720 return r.json() 721 722 def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]: 723 """ 724 Perform a POST web-request and return the data from the API as a DataFrame (along with 725 the number of rows/tickers that matched your query). 726 727 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 728 this can be very useful if you want to pass your own headers/cookies. 729 730 ### Live/Delayed data 731 732 Note that to get live-data you have to authenticate, which is done by passing your cookies. 733 Have a look in the README at the "Real-Time Data Access" sections. 734 735 :param kwargs: kwargs to pass to `requests.post()` 736 :return: a tuple consisting of: (total_count, dataframe) 737 """ 738 import pandas as pd 739 740 json_obj = self.get_scanner_data_raw(**kwargs) 741 rows_count = json_obj['totalCount'] 742 743 if '/scan2' in self.url: 744 columns = ['ticker', *json_obj['fields']] 745 rows = json_obj.get('symbols') 746 if rows: 747 df = pd.DataFrame(([row['s'], *row['f']] for row in rows), columns=columns) 748 else: 749 df = pd.DataFrame([], columns=columns) 750 else: 751 rows = json_obj['data'] 752 columns = ['ticker', *self.query.get('columns', ())] # pyright: ignore [reportArgumentType] 753 df = pd.DataFrame(([row['s'], *row['d']] for row in rows), columns=columns) 754 755 return rows_count, df 756 757 def copy(self) -> Query: 758 new = Query() 759 new.query = self.query.copy() 760 return new 761 762 def __repr__(self) -> str: 763 return f'< {pprint.pformat(self.query)}\n url={self.url!r} >' 764 765 def __eq__(self, other) -> bool: 766 return isinstance(other, Query) and self.query == other.query and self.url == other.url 767 768 769# TODO: Query should have no defaults (except limit), and a separate module should have all the 770# default screeners 771# TODO: add all presets
195class Query: 196 """ 197 This class allows you to perform SQL-like queries on the tradingview stock-screener. 198 199 The `Query` object reppresents a query that can be made to the official tradingview API, and it 200 stores all the data as JSON internally. 201 202 Examples: 203 204 To perform a simple query all you have to do is: 205 >>> from tradingview_screener import Query 206 >>> Query().get_scanner_data() 207 (18060, 208 ticker name close volume market_cap_basic 209 0 AMEX:SPY SPY 410.68 107367671 NaN 210 1 NASDAQ:QQQ QQQ 345.31 63475390 NaN 211 2 NASDAQ:TSLA TSLA 207.30 94879471 6.589904e+11 212 3 NASDAQ:NVDA NVDA 405.00 41677185 1.000350e+12 213 4 NASDAQ:AMZN AMZN 127.74 125309313 1.310658e+12 214 .. ... ... ... ... ... 215 45 NYSE:UNH UNH 524.66 2585616 4.859952e+11 216 46 NASDAQ:DXCM DXCM 89.29 14954605 3.449933e+10 217 47 NYSE:MA MA 364.08 3624883 3.429080e+11 218 48 NYSE:ABBV ABBV 138.93 9427212 2.452179e+11 219 49 AMEX:XLK XLK 161.12 8115780 NaN 220 [50 rows x 5 columns]) 221 222 The `get_scanner_data()` method will return a tuple with the first element being the number of 223 records that were found (like a `COUNT(*)`), and the second element contains the data that was 224 found as a DataFrame. 225 226 --- 227 228 By default, the `Query` will select the columns: `name`, `close`, `volume`, `market_cap_basic`, 229 but you override that 230 >>> (Query() 231 ... .select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI', 'Price to Earnings Ratio (TTM)') 232 ... .get_scanner_data()) 233 (18060, 234 ticker open high ... MACD.macd RSI price_earnings_ttm 235 0 AMEX:SPY 414.19 414.600 ... -5.397135 29.113396 NaN 236 1 NASDAQ:QQQ 346.43 348.840 ... -4.321482 34.335449 NaN 237 2 NASDAQ:TSLA 210.60 212.410 ... -12.224250 28.777229 66.752536 238 3 NASDAQ:NVDA 411.30 412.060 ... -8.738986 37.845668 97.835540 239 4 NASDAQ:AMZN 126.20 130.020 ... -2.025378 48.665666 66.697995 240 .. ... ... ... ... ... ... ... 241 45 NYSE:UNH 525.99 527.740 ... 6.448129 54.614775 22.770713 242 46 NASDAQ:DXCM 92.73 92.988 ... -2.376942 52.908093 98.914368 243 47 NYSE:MA 366.49 368.285 ... -7.496065 22.614078 31.711800 244 48 NYSE:ABBV 138.77 143.000 ... -1.708497 27.117232 37.960054 245 49 AMEX:XLK 161.17 162.750 ... -1.520828 36.868658 NaN 246 [50 rows x 8 columns]) 247 248 You can find the 250+ columns available in `tradingview_screener.constants.COLUMNS`. 249 250 Now let's do some queries using the `WHERE` statement, select all the stocks that the `close` is 251 bigger or equal than 350 252 >>> (Query() 253 ... .select('close', 'volume', '52 Week High') 254 ... .where(Column('close') >= 350) 255 ... .get_scanner_data()) 256 (159, 257 ticker close volume price_52_week_high 258 0 AMEX:SPY 410.68 107367671 459.44 259 1 NASDAQ:NVDA 405.00 41677185 502.66 260 2 NYSE:BRK.A 503375.05 7910 566569.97 261 3 AMEX:IVV 412.55 5604525 461.88 262 4 AMEX:VOO 377.32 5638752 422.15 263 .. ... ... ... ... 264 45 NASDAQ:EQIX 710.39 338549 821.63 265 46 NYSE:MCK 448.03 527406 465.90 266 47 NYSE:MTD 976.25 241733 1615.97 267 48 NASDAQ:CTAS 496.41 464631 525.37 268 49 NASDAQ:ROP 475.57 450141 508.90 269 [50 rows x 4 columns]) 270 271 You can even use other columns in these kind of operations 272 >>> (Query() 273 ... .select('close', 'VWAP') 274 ... .where(Column('close') >= Column('VWAP')) 275 ... .get_scanner_data()) 276 (9044, 277 ticker close VWAP 278 0 NASDAQ:AAPL 168.22 168.003333 279 1 NASDAQ:META 296.73 296.336667 280 2 NASDAQ:GOOGL 122.17 121.895233 281 3 NASDAQ:AMD 96.43 96.123333 282 4 NASDAQ:GOOG 123.40 123.100000 283 .. ... ... ... 284 45 NYSE:GD 238.25 238.043333 285 46 NYSE:GOLD 16.33 16.196667 286 47 AMEX:SLV 21.18 21.041667 287 48 AMEX:VXX 27.08 26.553333 288 49 NYSE:SLB 55.83 55.676667 289 [50 rows x 3 columns]) 290 291 Let's find all the stocks that the price is between the EMA 5 and 20, and the type is a stock 292 or fund 293 >>> (Query() 294 ... .select('close', 'volume', 'EMA5', 'EMA20', 'type') 295 ... .where( 296 ... Column('close').between(Column('EMA5'), Column('EMA20')), 297 ... Column('type').isin(['stock', 'fund']) 298 ... ) 299 ... .get_scanner_data()) 300 (1730, 301 ticker close volume EMA5 EMA20 type 302 0 NASDAQ:AMZN 127.74 125309313 125.033517 127.795142 stock 303 1 AMEX:HYG 72.36 35621800 72.340776 72.671058 fund 304 2 AMEX:LQD 99.61 21362859 99.554272 100.346388 fund 305 3 NASDAQ:IEF 90.08 11628236 89.856804 90.391503 fund 306 4 NYSE:SYK 261.91 3783608 261.775130 266.343290 stock 307 .. ... ... ... ... ... ... 308 45 NYSE:EMN 72.58 1562328 71.088034 72.835394 stock 309 46 NYSE:KIM 16.87 6609420 16.858920 17.096582 fund 310 47 NASDAQ:COLM 71.34 1516675 71.073116 71.658864 stock 311 48 NYSE:AOS 67.81 1586796 67.561619 67.903168 stock 312 49 NASDAQ:IGIB 47.81 2073538 47.761338 48.026795 fund 313 [50 rows x 6 columns]) 314 315 There are also the `ORDER BY`, `OFFSET`, and `LIMIT` statements. 316 Let's select all the tickers with a market cap between 1M and 50M, that have a relative volume 317 bigger than 1.2, and that the MACD is positive 318 >>> (Query() 319 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 320 ... .where( 321 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 322 ... Column('relative_volume_10d_calc') > 1.2, 323 ... Column('MACD.macd') >= Column('MACD.signal') 324 ... ) 325 ... .order_by('volume', ascending=False) 326 ... .offset(5) 327 ... .limit(15) 328 ... .get_scanner_data()) 329 (393, 330 ticker name close volume relative_volume_10d_calc 331 0 OTC:YCRM YCRM 0.0120 19626514 1.887942 332 1 OTC:PLPL PLPL 0.0002 17959914 3.026059 333 2 NASDAQ:ABVC ABVC 1.3800 16295824 1.967505 334 3 OTC:TLSS TLSS 0.0009 15671157 1.877976 335 4 OTC:GVSI GVSI 0.0128 14609774 2.640792 336 5 OTC:IGEX IGEX 0.0012 14285592 1.274861 337 6 OTC:EEGI EEGI 0.0004 12094000 2.224749 338 7 NASDAQ:GLG GLG 0.0591 9811974 1.990526 339 8 NASDAQ:TCRT TCRT 0.0890 8262894 2.630553 340 9 OTC:INKW INKW 0.0027 7196404 1.497134) 341 342 To avoid rewriting the same query again and again, you can save the query to a variable and 343 just call `get_scanner_data()` again and again to get the latest data: 344 >>> top_50_bullish = (Query() 345 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 346 ... .where( 347 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 348 ... Column('relative_volume_10d_calc') > 1.2, 349 ... Column('MACD.macd') >= Column('MACD.signal') 350 ... ) 351 ... .order_by('volume', ascending=False) 352 ... .limit(50)) 353 >>> top_50_bullish.get_scanner_data() 354 (393, 355 ticker name close volume relative_volume_10d_calc 356 0 OTC:BEGI BEGI 0.001050 127874055 3.349924 357 1 OTC:HCMC HCMC 0.000100 126992562 1.206231 358 2 OTC:HEMP HEMP 0.000150 101382713 1.775458 359 3 OTC:SONG SONG 0.000800 92640779 1.805721 360 4 OTC:APRU APRU 0.001575 38104499 29.028958 361 .. ... ... ... ... ... 362 45 OTC:BSHPF BSHPF 0.001000 525000 1.280899 363 46 OTC:GRHI GRHI 0.033000 507266 1.845738 364 47 OTC:OMGGF OMGGF 0.035300 505000 4.290059 365 48 NASDAQ:GBNH GBNH 0.273000 500412 9.076764 366 49 OTC:CLRMF CLRMF 0.032500 496049 17.560935 367 [50 rows x 5 columns]) 368 """ 369 370 def __init__(self, market: str = 'america') -> None: 371 # noinspection PyTypeChecker 372 self.query: QueryDict = copy.deepcopy(STOCKS_QUERY) 373 self.query['markets'] = [market] 374 self.url = URL.format(market=market) 375 376 def select(self, *columns: Column | str) -> Self: 377 self.query['columns'] = [ 378 col.name if isinstance(col, Column) else Column(col).name for col in columns 379 ] 380 return self 381 382 def where(self, *expressions: FilterOperationDict) -> Self: 383 """ 384 Filter screener (expressions are joined with the AND operator) 385 """ 386 self.query['filter'] = list(expressions) # convert tuple[dict] -> list[dict] 387 return self 388 389 def where2(self, operation: OperationDict) -> Self: 390 """ 391 Filter screener using AND/OR operators (nested expressions also allowed) 392 393 Rules: 394 1. The argument passed to `where2()` **must** be wrapped in `And()` or `Or()`. 395 2. `And()` and `Or()` can accept one or more conditions as arguments. 396 3. Conditions can be simple (e.g., `Column('field') == 'value'`) or complex, allowing nesting of `And()` and `Or()` to create intricate logical filters. 397 4. Unlike the `where()` method, which only supports chaining conditions with the `AND` operator, `where2()` allows mixing and nesting of `AND` and `OR` operators. 398 399 Examples: 400 401 1. **Combining conditions with `OR` and nested `AND`:** 402 ```python 403 Query() 404 .select('type', 'typespecs') 405 .where2( 406 Or( 407 And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])), 408 And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])), 409 Column('type') == 'dr', 410 ) 411 ) 412 ``` 413 414 This query filters entities where: 415 - The `type` is `'stock'` and `typespecs` contains `'common'` or `'preferred'`, **OR** 416 - The `type` is `'fund'` and `typespecs` does not contain `'etf'`, **OR** 417 - The `type` is `'dr'`. 418 419 2. **Mixing conditions with `OR`:** 420 ```python 421 Query().where2( 422 Or( 423 And(col('type') == 'stock', col('typespecs').has(['common'])), 424 col('type') == 'fund', 425 ) 426 ) 427 ``` 428 This query filters entities where: 429 - The `type` is `'stock'` and `typespecs` contains `'common'`, **OR** 430 - The `type` is `'fund'`. 431 432 3. **Combining conditions with `AND`:** 433 ```python 434 Query() 435 .set_markets('crypto') 436 .where2( 437 And( 438 col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']), 439 col('currency_id') == 'USD', 440 ) 441 ) 442 ``` 443 This query filters entities in the `'crypto'` market where: 444 - The `exchange` is one of `'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff'`, **AND** 445 - The `currency_id` is `'USD'`. 446 """ 447 self.query['filter2'] = operation['operation'] 448 return self 449 450 def order_by( 451 self, column: Column | str, ascending: bool = True, nulls_first: bool = False 452 ) -> Self: 453 """ 454 Applies sorting to the query results based on the specified column. 455 456 Examples: 457 458 >>> Query().order_by('volume', ascending=False) # sort descending 459 >>> Query().order_by('close', ascending=True) 460 >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False) 461 462 :param column: Either a `Column` object or a string with the column name. 463 :param ascending: Set to True for ascending order (default), or False for descending. 464 :param nulls_first: If True, places `None` values at the beginning of the results. Defaults 465 to False. 466 :return: The updated query object. 467 """ 468 dct: SortByDict = { 469 'sortBy': column.name if isinstance(column, Column) else column, 470 'sortOrder': 'asc' if ascending else 'desc', 471 'nullsFirst': nulls_first, 472 } 473 self.query['sort'] = dct 474 return self 475 476 def limit(self, limit: int) -> Self: 477 self.query.setdefault('range', DEFAULT_RANGE.copy())[1] = limit 478 return self 479 480 def offset(self, offset: int) -> Self: 481 self.query.setdefault('range', DEFAULT_RANGE.copy())[0] = offset 482 return self 483 484 def set_markets(self, *markets: str) -> Self: 485 """ 486 This method allows you to select the market/s which you want to query. 487 488 By default, the screener will only scan US equities, but you can change it to scan any 489 market or country, that includes a list of 67 countries, and also the following 490 asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`, 491 `futures`, `options`. 492 493 You may choose any value from `tradingview_screener.constants.MARKETS`. 494 495 If you select multiple countries, you might want to 496 497 Examples: 498 499 By default, the screener will show results from the `america` market, but you can 500 change it (note the difference between `market` and `country`) 501 >>> columns = ['close', 'market', 'country', 'currency'] 502 >>> (Query() 503 ... .select(*columns) 504 ... .set_markets('italy') 505 ... .get_scanner_data()) 506 (2346, 507 ticker close market country currency 508 0 MIL:UCG 23.9150 italy Italy EUR 509 1 MIL:ISP 2.4910 italy Italy EUR 510 2 MIL:STLAM 17.9420 italy Netherlands EUR 511 3 MIL:ENEL 6.0330 italy Italy EUR 512 4 MIL:ENI 15.4800 italy Italy EUR 513 .. ... ... ... ... ... 514 45 MIL:UNI 5.1440 italy Italy EUR 515 46 MIL:3OIS 0.4311 italy Ireland EUR 516 47 MIL:3SIL 35.2300 italy Ireland EUR 517 48 MIL:IWDE 69.1300 italy Ireland EUR 518 49 MIL:QQQS 19.2840 italy Ireland EUR 519 [50 rows x 5 columns]) 520 521 You can also select multiple markets 522 >>> (Query() 523 ... .select(*columns) 524 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 525 ... .get_scanner_data()) 526 (23964, 527 ticker close market country currency 528 0 AMEX:SPY 420.1617 america United States USD 529 1 NASDAQ:TSLA 201.2000 america United States USD 530 2 NASDAQ:NVDA 416.7825 america United States USD 531 3 NASDAQ:AMD 106.6600 america United States USD 532 4 NASDAQ:QQQ 353.7985 america United States USD 533 .. ... ... ... ... ... 534 45 NASDAQ:GOOGL 124.9200 america United States USD 535 46 HKEX:1211 233.2000 hongkong China HKD 536 47 TASE:ALHE 1995.0000 israel Israel ILA 537 48 AMEX:BIL 91.4398 america United States USD 538 49 NASDAQ:GOOG 126.1500 america United States USD 539 [50 rows x 5 columns]) 540 541 You may also select different financial instruments 542 >>> (Query() 543 ... .select('close', 'market') 544 ... .set_markets('cfd', 'crypto', 'forex', 'futures') 545 ... .get_scanner_data()) 546 (118076, 547 ticker ... market 548 0 UNISWAP3ETH:JUSTICEUSDT ... crypto 549 1 UNISWAP3ETH:UAHGUSDT ... crypto 550 2 UNISWAP3ETH:KENDUWETH ... crypto 551 3 UNISWAP3ETH:MATICSTMATIC ... crypto 552 4 UNISWAP3ETH:WETHETHM ... crypto 553 .. ... ... ... 554 45 UNISWAP:MUSICAIWETH_1F5304.USD ... crypto 555 46 CRYPTOCAP:FIL ... cfd 556 47 CRYPTOCAP:SUI ... cfd 557 48 CRYPTOCAP:ARBI ... cfd 558 49 CRYPTOCAP:OP ... cfd 559 [50 rows x 3 columns]) 560 561 :param markets: one or more markets from `tradingview_screener.constants.MARKETS` 562 :return: Self 563 """ 564 if len(markets) == 1: 565 market = markets[0] 566 self.url = URL.format(market=market) 567 self.query['markets'] = [market] 568 else: # len(markets) == 0 or len(markets) > 1 569 self.url = URL.format(market='global') 570 self.query['markets'] = list(markets) 571 572 return self 573 574 def set_tickers(self, *tickers: str) -> Self: 575 """ 576 Set the tickers you wish to receive information on. 577 578 Note that this resets the markets and sets the URL market to `global`. 579 580 Examples: 581 582 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 583 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 584 (1, 585 ticker name market close volume VWAP MACD.macd 586 0 NASDAQ:TSLA TSLA america 186 3519931 185.53 2.371601) 587 588 To set tickers from multiple markets we need to update the markets that include them: 589 >>> (Query() 590 ... .set_markets('america', 'italy', 'vietnam') 591 ... .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX') 592 ... .get_scanner_data()) 593 (4, 594 ticker name close volume market_cap_basic 595 0 HOSE:VIX VIX 16700.00 33192500 4.568961e+08 596 1 AMEX:SPY SPY 544.35 1883562 NaN 597 2 NYSE:GME GME 23.80 3116758 1.014398e+10 598 3 MIL:RACE RACE 393.30 122878 1.006221e+11) 599 600 :param tickers: One or more tickers, syntax: `exchange:symbol` 601 :return: Self 602 """ 603 self.query.setdefault('symbols', {})['tickers'] = list(tickers) 604 self.set_markets() 605 return self 606 607 def set_index(self, *indexes: str) -> Self: 608 """ 609 Scan only the equities that are in the given index (or indexes). 610 611 Note that this resets the markets and sets the URL market to `global`. 612 613 Examples: 614 615 >>> Query().set_index('SYML:SP;SPX').get_scanner_data() 616 (503, 617 ticker name close volume market_cap_basic 618 0 NASDAQ:NVDA NVDA 1208.88 41238122 2.973644e+12 619 1 NASDAQ:AAPL AAPL 196.89 53103705 3.019127e+12 620 2 NASDAQ:TSLA TSLA 177.48 56244929 5.660185e+11 621 3 NASDAQ:AMD AMD 167.87 44795240 2.713306e+11 622 4 NASDAQ:MSFT MSFT 423.85 13621485 3.150183e+12 623 5 NASDAQ:AMZN AMZN 184.30 28021473 1.917941e+12 624 6 NASDAQ:META META 492.96 9379199 1.250410e+12 625 7 NASDAQ:GOOGL GOOGL 174.46 19660698 2.164346e+12 626 8 NASDAQ:SMCI SMCI 769.11 3444852 4.503641e+10 627 9 NASDAQ:GOOG GOOG 175.95 14716134 2.164346e+12 628 10 NASDAQ:AVGO AVGO 1406.64 1785876 6.518669e+11) 629 630 You can set multiple indices as well, like the NIFTY 50 and UK 100 Index. 631 >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data() 632 (150, 633 ticker name close volume market_cap_basic 634 0 NSE:INFY INFY 1533.600000 24075302 7.623654e+10 635 1 LSE:AZN AZN 12556.000000 2903032 2.489770e+11 636 2 NSE:HDFCBANK HDFCBANK 1573.350000 18356108 1.432600e+11 637 3 NSE:RELIANCE RELIANCE 2939.899900 9279348 2.381518e+11 638 4 LSE:LSEG LSEG 9432.000000 2321053 6.395329e+10 639 5 NSE:BAJFINANCE BAJFINANCE 7191.399900 2984052 5.329685e+10 640 6 LSE:BARC BARC 217.250000 96238723 4.133010e+10 641 7 NSE:SBIN SBIN 829.950010 25061284 8.869327e+10 642 8 NSE:LT LT 3532.500000 5879660 5.816100e+10 643 9 LSE:SHEL SHEL 2732.500000 7448315 2.210064e+11) 644 645 You can find the full list of indices in [`constants.INDICES`](constants.html#INDICES), 646 just note that the syntax is 647 `SYML:{source};{symbol}`. 648 649 :param indexes: One or more strings representing the financial indexes to filter by 650 :return: An instance of the `Query` class with the filter applied 651 """ 652 self.query.setdefault('preset', 'index_components_market_pages') 653 self.query.setdefault('symbols', {})['symbolset'] = list(indexes) 654 # reset markets list and URL to `/global` 655 self.set_markets() 656 return self 657 658 # def set_currency(self, currency: Literal['symbol', 'market'] | str) -> Self: 659 # """ 660 # Change the currency of the screener. 661 # 662 # Note that this changes *only* the currency of the columns of type `fundamental_price`, 663 # for example: `market_cap_basic`, `net_income`, `total_debt`. Other columns like `close` and 664 # `Value.Traded` won't change, because they are of a different type. 665 # 666 # This can be particularly useful if you are querying tickers across different markets. 667 # 668 # Examples: 669 # 670 # >>> Query().set_currency('symbol') # convert to symbol currency 671 # >>> Query().set_currency('market') # convert to market currency 672 # >>> Query().set_currency('usd') # or another currency 673 # >>> Query().set_currency('jpy') 674 # >>> Query().set_currency('eur') 675 # """ 676 # # symbol currency -> self.query['price_conversion'] = {'to_symbol': True} 677 # # market currency -> self.query['price_conversion'] = {'to_symbol': False} 678 # # USD or other currency -> self.query['price_conversion'] = {'to_currency': 'usd'} 679 # if currency == 'symbol': 680 # self.query['price_conversion'] = {'to_symbol': True} 681 # elif currency == 'market': 682 # self.query['price_conversion'] = {'to_symbol': False} 683 # else: 684 # self.query['price_conversion'] = {'to_currency': currency} 685 # return self 686 687 def set_property(self, key: str, value: Any) -> Self: 688 self.query[key] = value 689 return self 690 691 def get_scanner_data_raw(self, **kwargs) -> ScreenerDict | ScreenerDictV2: 692 """ 693 Perform a POST web-request and return the data from the API (dictionary). 694 695 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 696 this can be very useful if you want to pass your own headers/cookies. 697 698 >>> Query().select('close', 'volume').limit(5).get_scanner_data_raw() 699 { 700 'totalCount': 17559, 701 'data': [ 702 {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]}, 703 {'s': 'AMEX:SPY', 'd': [542.04, 52331224]}, 704 {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]}, 705 {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]}, 706 {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]}, 707 ], 708 } 709 """ 710 self.query.setdefault('range', DEFAULT_RANGE.copy()) 711 712 kwargs.setdefault('headers', HEADERS) 713 kwargs.setdefault('timeout', 20) 714 r = requests.post(self.url, json=self.query, **kwargs) 715 716 if not r.ok: 717 # add the body to the error message for debugging purposes 718 r.reason += f'\n Body: {r.text}\n' 719 r.raise_for_status() 720 721 return r.json() 722 723 def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]: 724 """ 725 Perform a POST web-request and return the data from the API as a DataFrame (along with 726 the number of rows/tickers that matched your query). 727 728 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 729 this can be very useful if you want to pass your own headers/cookies. 730 731 ### Live/Delayed data 732 733 Note that to get live-data you have to authenticate, which is done by passing your cookies. 734 Have a look in the README at the "Real-Time Data Access" sections. 735 736 :param kwargs: kwargs to pass to `requests.post()` 737 :return: a tuple consisting of: (total_count, dataframe) 738 """ 739 import pandas as pd 740 741 json_obj = self.get_scanner_data_raw(**kwargs) 742 rows_count = json_obj['totalCount'] 743 744 if '/scan2' in self.url: 745 columns = ['ticker', *json_obj['fields']] 746 rows = json_obj.get('symbols') 747 if rows: 748 df = pd.DataFrame(([row['s'], *row['f']] for row in rows), columns=columns) 749 else: 750 df = pd.DataFrame([], columns=columns) 751 else: 752 rows = json_obj['data'] 753 columns = ['ticker', *self.query.get('columns', ())] # pyright: ignore [reportArgumentType] 754 df = pd.DataFrame(([row['s'], *row['d']] for row in rows), columns=columns) 755 756 return rows_count, df 757 758 def copy(self) -> Query: 759 new = Query() 760 new.query = self.query.copy() 761 return new 762 763 def __repr__(self) -> str: 764 return f'< {pprint.pformat(self.query)}\n url={self.url!r} >' 765 766 def __eq__(self, other) -> bool: 767 return isinstance(other, Query) and self.query == other.query and self.url == other.url
This class allows you to perform SQL-like queries on the tradingview stock-screener.
The Query object reppresents a query that can be made to the official tradingview API, and it
stores all the data as JSON internally.
Examples:
To perform a simple query all you have to do is:
>>> from tradingview_screener import Query
>>> Query().get_scanner_data()
(18060,
ticker name close volume market_cap_basic
0 AMEX:SPY SPY 410.68 107367671 NaN
1 NASDAQ:QQQ QQQ 345.31 63475390 NaN
2 NASDAQ:TSLA TSLA 207.30 94879471 6.589904e+11
3 NASDAQ:NVDA NVDA 405.00 41677185 1.000350e+12
4 NASDAQ:AMZN AMZN 127.74 125309313 1.310658e+12
.. ... ... ... ... ...
45 NYSE:UNH UNH 524.66 2585616 4.859952e+11
46 NASDAQ:DXCM DXCM 89.29 14954605 3.449933e+10
47 NYSE:MA MA 364.08 3624883 3.429080e+11
48 NYSE:ABBV ABBV 138.93 9427212 2.452179e+11
49 AMEX:XLK XLK 161.12 8115780 NaN
[50 rows x 5 columns])
The get_scanner_data() method will return a tuple with the first element being the number of
records that were found (like a COUNT(*)), and the second element contains the data that was
found as a DataFrame.
By default, the Query will select the columns: name, close, volume, market_cap_basic,
but you override that
>>> (Query()
... .select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI', 'Price to Earnings Ratio (TTM)')
... .get_scanner_data())
(18060,
ticker open high ... MACD.macd RSI price_earnings_ttm
0 AMEX:SPY 414.19 414.600 ... -5.397135 29.113396 NaN
1 NASDAQ:QQQ 346.43 348.840 ... -4.321482 34.335449 NaN
2 NASDAQ:TSLA 210.60 212.410 ... -12.224250 28.777229 66.752536
3 NASDAQ:NVDA 411.30 412.060 ... -8.738986 37.845668 97.835540
4 NASDAQ:AMZN 126.20 130.020 ... -2.025378 48.665666 66.697995
.. ... ... ... ... ... ... ...
45 NYSE:UNH 525.99 527.740 ... 6.448129 54.614775 22.770713
46 NASDAQ:DXCM 92.73 92.988 ... -2.376942 52.908093 98.914368
47 NYSE:MA 366.49 368.285 ... -7.496065 22.614078 31.711800
48 NYSE:ABBV 138.77 143.000 ... -1.708497 27.117232 37.960054
49 AMEX:XLK 161.17 162.750 ... -1.520828 36.868658 NaN
[50 rows x 8 columns])
You can find the 250+ columns available in tradingview_screener.constants.COLUMNS.
Now let's do some queries using the WHERE statement, select all the stocks that the close is
bigger or equal than 350
>>> (Query()
... .select('close', 'volume', '52 Week High')
... .where(Column('close') >= 350)
... .get_scanner_data())
(159,
ticker close volume price_52_week_high
0 AMEX:SPY 410.68 107367671 459.44
1 NASDAQ:NVDA 405.00 41677185 502.66
2 NYSE:BRK.A 503375.05 7910 566569.97
3 AMEX:IVV 412.55 5604525 461.88
4 AMEX:VOO 377.32 5638752 422.15
.. ... ... ... ...
45 NASDAQ:EQIX 710.39 338549 821.63
46 NYSE:MCK 448.03 527406 465.90
47 NYSE:MTD 976.25 241733 1615.97
48 NASDAQ:CTAS 496.41 464631 525.37
49 NASDAQ:ROP 475.57 450141 508.90
[50 rows x 4 columns])
You can even use other columns in these kind of operations
>>> (Query()
... .select('close', 'VWAP')
... .where(Column('close') >= Column('VWAP'))
... .get_scanner_data())
(9044,
ticker close VWAP
0 NASDAQ:AAPL 168.22 168.003333
1 NASDAQ:META 296.73 296.336667
2 NASDAQ:GOOGL 122.17 121.895233
3 NASDAQ:AMD 96.43 96.123333
4 NASDAQ:GOOG 123.40 123.100000
.. ... ... ...
45 NYSE:GD 238.25 238.043333
46 NYSE:GOLD 16.33 16.196667
47 AMEX:SLV 21.18 21.041667
48 AMEX:VXX 27.08 26.553333
49 NYSE:SLB 55.83 55.676667
[50 rows x 3 columns])
Let's find all the stocks that the price is between the EMA 5 and 20, and the type is a stock or fund
>>> (Query()
... .select('close', 'volume', 'EMA5', 'EMA20', 'type')
... .where(
... Column('close').between(Column('EMA5'), Column('EMA20')),
... Column('type').isin(['stock', 'fund'])
... )
... .get_scanner_data())
(1730,
ticker close volume EMA5 EMA20 type
0 NASDAQ:AMZN 127.74 125309313 125.033517 127.795142 stock
1 AMEX:HYG 72.36 35621800 72.340776 72.671058 fund
2 AMEX:LQD 99.61 21362859 99.554272 100.346388 fund
3 NASDAQ:IEF 90.08 11628236 89.856804 90.391503 fund
4 NYSE:SYK 261.91 3783608 261.775130 266.343290 stock
.. ... ... ... ... ... ...
45 NYSE:EMN 72.58 1562328 71.088034 72.835394 stock
46 NYSE:KIM 16.87 6609420 16.858920 17.096582 fund
47 NASDAQ:COLM 71.34 1516675 71.073116 71.658864 stock
48 NYSE:AOS 67.81 1586796 67.561619 67.903168 stock
49 NASDAQ:IGIB 47.81 2073538 47.761338 48.026795 fund
[50 rows x 6 columns])
There are also the ORDER BY, OFFSET, and LIMIT statements.
Let's select all the tickers with a market cap between 1M and 50M, that have a relative volume
bigger than 1.2, and that the MACD is positive
>>> (Query()
... .select('name', 'close', 'volume', 'relative_volume_10d_calc')
... .where(
... Column('market_cap_basic').between(1_000_000, 50_000_000),
... Column('relative_volume_10d_calc') > 1.2,
... Column('MACD.macd') >= Column('MACD.signal')
... )
... .order_by('volume', ascending=False)
... .offset(5)
... .limit(15)
... .get_scanner_data())
(393,
ticker name close volume relative_volume_10d_calc
0 OTC:YCRM YCRM 0.0120 19626514 1.887942
1 OTC:PLPL PLPL 0.0002 17959914 3.026059
2 NASDAQ:ABVC ABVC 1.3800 16295824 1.967505
3 OTC:TLSS TLSS 0.0009 15671157 1.877976
4 OTC:GVSI GVSI 0.0128 14609774 2.640792
5 OTC:IGEX IGEX 0.0012 14285592 1.274861
6 OTC:EEGI EEGI 0.0004 12094000 2.224749
7 NASDAQ:GLG GLG 0.0591 9811974 1.990526
8 NASDAQ:TCRT TCRT 0.0890 8262894 2.630553
9 OTC:INKW INKW 0.0027 7196404 1.497134)
To avoid rewriting the same query again and again, you can save the query to a variable and
just call get_scanner_data() again and again to get the latest data:
>>> top_50_bullish = (Query()
... .select('name', 'close', 'volume', 'relative_volume_10d_calc')
... .where(
... Column('market_cap_basic').between(1_000_000, 50_000_000),
... Column('relative_volume_10d_calc') > 1.2,
... Column('MACD.macd') >= Column('MACD.signal')
... )
... .order_by('volume', ascending=False)
... .limit(50))
>>> top_50_bullish.get_scanner_data()
(393,
ticker name close volume relative_volume_10d_calc
0 OTC:BEGI BEGI 0.001050 127874055 3.349924
1 OTC:HCMC HCMC 0.000100 126992562 1.206231
2 OTC:HEMP HEMP 0.000150 101382713 1.775458
3 OTC:SONG SONG 0.000800 92640779 1.805721
4 OTC:APRU APRU 0.001575 38104499 29.028958
.. ... ... ... ... ...
45 OTC:BSHPF BSHPF 0.001000 525000 1.280899
46 OTC:GRHI GRHI 0.033000 507266 1.845738
47 OTC:OMGGF OMGGF 0.035300 505000 4.290059
48 NASDAQ:GBNH GBNH 0.273000 500412 9.076764
49 OTC:CLRMF CLRMF 0.032500 496049 17.560935
[50 rows x 5 columns])
382 def where(self, *expressions: FilterOperationDict) -> Self: 383 """ 384 Filter screener (expressions are joined with the AND operator) 385 """ 386 self.query['filter'] = list(expressions) # convert tuple[dict] -> list[dict] 387 return self
Filter screener (expressions are joined with the AND operator)
389 def where2(self, operation: OperationDict) -> Self: 390 """ 391 Filter screener using AND/OR operators (nested expressions also allowed) 392 393 Rules: 394 1. The argument passed to `where2()` **must** be wrapped in `And()` or `Or()`. 395 2. `And()` and `Or()` can accept one or more conditions as arguments. 396 3. Conditions can be simple (e.g., `Column('field') == 'value'`) or complex, allowing nesting of `And()` and `Or()` to create intricate logical filters. 397 4. Unlike the `where()` method, which only supports chaining conditions with the `AND` operator, `where2()` allows mixing and nesting of `AND` and `OR` operators. 398 399 Examples: 400 401 1. **Combining conditions with `OR` and nested `AND`:** 402 ```python 403 Query() 404 .select('type', 'typespecs') 405 .where2( 406 Or( 407 And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])), 408 And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])), 409 Column('type') == 'dr', 410 ) 411 ) 412 ``` 413 414 This query filters entities where: 415 - The `type` is `'stock'` and `typespecs` contains `'common'` or `'preferred'`, **OR** 416 - The `type` is `'fund'` and `typespecs` does not contain `'etf'`, **OR** 417 - The `type` is `'dr'`. 418 419 2. **Mixing conditions with `OR`:** 420 ```python 421 Query().where2( 422 Or( 423 And(col('type') == 'stock', col('typespecs').has(['common'])), 424 col('type') == 'fund', 425 ) 426 ) 427 ``` 428 This query filters entities where: 429 - The `type` is `'stock'` and `typespecs` contains `'common'`, **OR** 430 - The `type` is `'fund'`. 431 432 3. **Combining conditions with `AND`:** 433 ```python 434 Query() 435 .set_markets('crypto') 436 .where2( 437 And( 438 col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']), 439 col('currency_id') == 'USD', 440 ) 441 ) 442 ``` 443 This query filters entities in the `'crypto'` market where: 444 - The `exchange` is one of `'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff'`, **AND** 445 - The `currency_id` is `'USD'`. 446 """ 447 self.query['filter2'] = operation['operation'] 448 return self
Filter screener using AND/OR operators (nested expressions also allowed)
Rules:
- The argument passed to
where2()must be wrapped inAnd()orOr(). And()andOr()can accept one or more conditions as arguments.- Conditions can be simple (e.g.,
Column('field') == 'value') or complex, allowing nesting ofAnd()andOr()to create intricate logical filters. - Unlike the
where()method, which only supports chaining conditions with theANDoperator,where2()allows mixing and nesting ofANDandORoperators.
Examples:
Combining conditions with
ORand nestedAND:Query() .select('type', 'typespecs') .where2( Or( And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])), And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])), Column('type') == 'dr', ) )This query filters entities where:
- The
typeis'stock'andtypespecscontains'common'or'preferred', OR - The
typeis'fund'andtypespecsdoes not contain'etf', OR - The
typeis'dr'.
- The
Mixing conditions with
OR:Query().where2( Or( And(col('type') == 'stock', col('typespecs').has(['common'])), col('type') == 'fund', ) )This query filters entities where:
- The
typeis'stock'andtypespecscontains'common', OR - The
typeis'fund'.
- The
Combining conditions with
AND:Query() .set_markets('crypto') .where2( And( col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']), col('currency_id') == 'USD', ) )This query filters entities in the
'crypto'market where:- The
exchangeis one of'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff', AND - The
currency_idis'USD'.
- The
450 def order_by( 451 self, column: Column | str, ascending: bool = True, nulls_first: bool = False 452 ) -> Self: 453 """ 454 Applies sorting to the query results based on the specified column. 455 456 Examples: 457 458 >>> Query().order_by('volume', ascending=False) # sort descending 459 >>> Query().order_by('close', ascending=True) 460 >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False) 461 462 :param column: Either a `Column` object or a string with the column name. 463 :param ascending: Set to True for ascending order (default), or False for descending. 464 :param nulls_first: If True, places `None` values at the beginning of the results. Defaults 465 to False. 466 :return: The updated query object. 467 """ 468 dct: SortByDict = { 469 'sortBy': column.name if isinstance(column, Column) else column, 470 'sortOrder': 'asc' if ascending else 'desc', 471 'nullsFirst': nulls_first, 472 } 473 self.query['sort'] = dct 474 return self
Applies sorting to the query results based on the specified column.
Examples:
>>> Query().order_by('volume', ascending=False) # sort descending
>>> Query().order_by('close', ascending=True)
>>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False)
Parameters
- column: Either a
Columnobject or a string with the column name. - ascending: Set to True for ascending order (default), or False for descending.
- nulls_first: If True, places
Nonevalues at the beginning of the results. Defaults to False.
Returns
The updated query object.
484 def set_markets(self, *markets: str) -> Self: 485 """ 486 This method allows you to select the market/s which you want to query. 487 488 By default, the screener will only scan US equities, but you can change it to scan any 489 market or country, that includes a list of 67 countries, and also the following 490 asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`, 491 `futures`, `options`. 492 493 You may choose any value from `tradingview_screener.constants.MARKETS`. 494 495 If you select multiple countries, you might want to 496 497 Examples: 498 499 By default, the screener will show results from the `america` market, but you can 500 change it (note the difference between `market` and `country`) 501 >>> columns = ['close', 'market', 'country', 'currency'] 502 >>> (Query() 503 ... .select(*columns) 504 ... .set_markets('italy') 505 ... .get_scanner_data()) 506 (2346, 507 ticker close market country currency 508 0 MIL:UCG 23.9150 italy Italy EUR 509 1 MIL:ISP 2.4910 italy Italy EUR 510 2 MIL:STLAM 17.9420 italy Netherlands EUR 511 3 MIL:ENEL 6.0330 italy Italy EUR 512 4 MIL:ENI 15.4800 italy Italy EUR 513 .. ... ... ... ... ... 514 45 MIL:UNI 5.1440 italy Italy EUR 515 46 MIL:3OIS 0.4311 italy Ireland EUR 516 47 MIL:3SIL 35.2300 italy Ireland EUR 517 48 MIL:IWDE 69.1300 italy Ireland EUR 518 49 MIL:QQQS 19.2840 italy Ireland EUR 519 [50 rows x 5 columns]) 520 521 You can also select multiple markets 522 >>> (Query() 523 ... .select(*columns) 524 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 525 ... .get_scanner_data()) 526 (23964, 527 ticker close market country currency 528 0 AMEX:SPY 420.1617 america United States USD 529 1 NASDAQ:TSLA 201.2000 america United States USD 530 2 NASDAQ:NVDA 416.7825 america United States USD 531 3 NASDAQ:AMD 106.6600 america United States USD 532 4 NASDAQ:QQQ 353.7985 america United States USD 533 .. ... ... ... ... ... 534 45 NASDAQ:GOOGL 124.9200 america United States USD 535 46 HKEX:1211 233.2000 hongkong China HKD 536 47 TASE:ALHE 1995.0000 israel Israel ILA 537 48 AMEX:BIL 91.4398 america United States USD 538 49 NASDAQ:GOOG 126.1500 america United States USD 539 [50 rows x 5 columns]) 540 541 You may also select different financial instruments 542 >>> (Query() 543 ... .select('close', 'market') 544 ... .set_markets('cfd', 'crypto', 'forex', 'futures') 545 ... .get_scanner_data()) 546 (118076, 547 ticker ... market 548 0 UNISWAP3ETH:JUSTICEUSDT ... crypto 549 1 UNISWAP3ETH:UAHGUSDT ... crypto 550 2 UNISWAP3ETH:KENDUWETH ... crypto 551 3 UNISWAP3ETH:MATICSTMATIC ... crypto 552 4 UNISWAP3ETH:WETHETHM ... crypto 553 .. ... ... ... 554 45 UNISWAP:MUSICAIWETH_1F5304.USD ... crypto 555 46 CRYPTOCAP:FIL ... cfd 556 47 CRYPTOCAP:SUI ... cfd 557 48 CRYPTOCAP:ARBI ... cfd 558 49 CRYPTOCAP:OP ... cfd 559 [50 rows x 3 columns]) 560 561 :param markets: one or more markets from `tradingview_screener.constants.MARKETS` 562 :return: Self 563 """ 564 if len(markets) == 1: 565 market = markets[0] 566 self.url = URL.format(market=market) 567 self.query['markets'] = [market] 568 else: # len(markets) == 0 or len(markets) > 1 569 self.url = URL.format(market='global') 570 self.query['markets'] = list(markets) 571 572 return self
This method allows you to select the market/s which you want to query.
By default, the screener will only scan US equities, but you can change it to scan any
market or country, that includes a list of 67 countries, and also the following
asset classes: bonds, cfd, coin, crypto, euronext, forex,
futures, options.
You may choose any value from tradingview_screener.constants.MARKETS.
If you select multiple countries, you might want to
Examples:
By default, the screener will show results from the america market, but you can
change it (note the difference between market and country)
>>> columns = ['close', 'market', 'country', 'currency']
>>> (Query()
... .select(*columns)
... .set_markets('italy')
... .get_scanner_data())
(2346,
ticker close market country currency
0 MIL:UCG 23.9150 italy Italy EUR
1 MIL:ISP 2.4910 italy Italy EUR
2 MIL:STLAM 17.9420 italy Netherlands EUR
3 MIL:ENEL 6.0330 italy Italy EUR
4 MIL:ENI 15.4800 italy Italy EUR
.. ... ... ... ... ...
45 MIL:UNI 5.1440 italy Italy EUR
46 MIL:3OIS 0.4311 italy Ireland EUR
47 MIL:3SIL 35.2300 italy Ireland EUR
48 MIL:IWDE 69.1300 italy Ireland EUR
49 MIL:QQQS 19.2840 italy Ireland EUR
[50 rows x 5 columns])
You can also select multiple markets
>>> (Query()
... .select(*columns)
... .set_markets('america', 'israel', 'hongkong', 'switzerland')
... .get_scanner_data())
(23964,
ticker close market country currency
0 AMEX:SPY 420.1617 america United States USD
1 NASDAQ:TSLA 201.2000 america United States USD
2 NASDAQ:NVDA 416.7825 america United States USD
3 NASDAQ:AMD 106.6600 america United States USD
4 NASDAQ:QQQ 353.7985 america United States USD
.. ... ... ... ... ...
45 NASDAQ:GOOGL 124.9200 america United States USD
46 HKEX:1211 233.2000 hongkong China HKD
47 TASE:ALHE 1995.0000 israel Israel ILA
48 AMEX:BIL 91.4398 america United States USD
49 NASDAQ:GOOG 126.1500 america United States USD
[50 rows x 5 columns])
You may also select different financial instruments
>>> (Query()
... .select('close', 'market')
... .set_markets('cfd', 'crypto', 'forex', 'futures')
... .get_scanner_data())
(118076,
ticker ... market
0 UNISWAP3ETH:JUSTICEUSDT ... crypto
1 UNISWAP3ETH:UAHGUSDT ... crypto
2 UNISWAP3ETH:KENDUWETH ... crypto
3 UNISWAP3ETH:MATICSTMATIC ... crypto
4 UNISWAP3ETH:WETHETHM ... crypto
.. ... ... ...
45 UNISWAP:MUSICAIWETH_1F5304.USD ... crypto
46 CRYPTOCAP:FIL ... cfd
47 CRYPTOCAP:SUI ... cfd
48 CRYPTOCAP:ARBI ... cfd
49 CRYPTOCAP:OP ... cfd
[50 rows x 3 columns])
Parameters
- markets: one or more markets from
tradingview_screener.constants.MARKETS
Returns
Self
574 def set_tickers(self, *tickers: str) -> Self: 575 """ 576 Set the tickers you wish to receive information on. 577 578 Note that this resets the markets and sets the URL market to `global`. 579 580 Examples: 581 582 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 583 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 584 (1, 585 ticker name market close volume VWAP MACD.macd 586 0 NASDAQ:TSLA TSLA america 186 3519931 185.53 2.371601) 587 588 To set tickers from multiple markets we need to update the markets that include them: 589 >>> (Query() 590 ... .set_markets('america', 'italy', 'vietnam') 591 ... .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX') 592 ... .get_scanner_data()) 593 (4, 594 ticker name close volume market_cap_basic 595 0 HOSE:VIX VIX 16700.00 33192500 4.568961e+08 596 1 AMEX:SPY SPY 544.35 1883562 NaN 597 2 NYSE:GME GME 23.80 3116758 1.014398e+10 598 3 MIL:RACE RACE 393.30 122878 1.006221e+11) 599 600 :param tickers: One or more tickers, syntax: `exchange:symbol` 601 :return: Self 602 """ 603 self.query.setdefault('symbols', {})['tickers'] = list(tickers) 604 self.set_markets() 605 return self
Set the tickers you wish to receive information on.
Note that this resets the markets and sets the URL market to global.
Examples:
>>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd')
>>> q.set_tickers('NASDAQ:TSLA').get_scanner_data()
(1,
ticker name market close volume VWAP MACD.macd
0 NASDAQ:TSLA TSLA america 186 3519931 185.53 2.371601)
To set tickers from multiple markets we need to update the markets that include them:
>>> (Query()
... .set_markets('america', 'italy', 'vietnam')
... .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX')
... .get_scanner_data())
(4,
ticker name close volume market_cap_basic
0 HOSE:VIX VIX 16700.00 33192500 4.568961e+08
1 AMEX:SPY SPY 544.35 1883562 NaN
2 NYSE:GME GME 23.80 3116758 1.014398e+10
3 MIL:RACE RACE 393.30 122878 1.006221e+11)
Parameters
- **tickers: One or more tickers, syntax:
exchange**: symbol
Returns
Self
607 def set_index(self, *indexes: str) -> Self: 608 """ 609 Scan only the equities that are in the given index (or indexes). 610 611 Note that this resets the markets and sets the URL market to `global`. 612 613 Examples: 614 615 >>> Query().set_index('SYML:SP;SPX').get_scanner_data() 616 (503, 617 ticker name close volume market_cap_basic 618 0 NASDAQ:NVDA NVDA 1208.88 41238122 2.973644e+12 619 1 NASDAQ:AAPL AAPL 196.89 53103705 3.019127e+12 620 2 NASDAQ:TSLA TSLA 177.48 56244929 5.660185e+11 621 3 NASDAQ:AMD AMD 167.87 44795240 2.713306e+11 622 4 NASDAQ:MSFT MSFT 423.85 13621485 3.150183e+12 623 5 NASDAQ:AMZN AMZN 184.30 28021473 1.917941e+12 624 6 NASDAQ:META META 492.96 9379199 1.250410e+12 625 7 NASDAQ:GOOGL GOOGL 174.46 19660698 2.164346e+12 626 8 NASDAQ:SMCI SMCI 769.11 3444852 4.503641e+10 627 9 NASDAQ:GOOG GOOG 175.95 14716134 2.164346e+12 628 10 NASDAQ:AVGO AVGO 1406.64 1785876 6.518669e+11) 629 630 You can set multiple indices as well, like the NIFTY 50 and UK 100 Index. 631 >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data() 632 (150, 633 ticker name close volume market_cap_basic 634 0 NSE:INFY INFY 1533.600000 24075302 7.623654e+10 635 1 LSE:AZN AZN 12556.000000 2903032 2.489770e+11 636 2 NSE:HDFCBANK HDFCBANK 1573.350000 18356108 1.432600e+11 637 3 NSE:RELIANCE RELIANCE 2939.899900 9279348 2.381518e+11 638 4 LSE:LSEG LSEG 9432.000000 2321053 6.395329e+10 639 5 NSE:BAJFINANCE BAJFINANCE 7191.399900 2984052 5.329685e+10 640 6 LSE:BARC BARC 217.250000 96238723 4.133010e+10 641 7 NSE:SBIN SBIN 829.950010 25061284 8.869327e+10 642 8 NSE:LT LT 3532.500000 5879660 5.816100e+10 643 9 LSE:SHEL SHEL 2732.500000 7448315 2.210064e+11) 644 645 You can find the full list of indices in [`constants.INDICES`](constants.html#INDICES), 646 just note that the syntax is 647 `SYML:{source};{symbol}`. 648 649 :param indexes: One or more strings representing the financial indexes to filter by 650 :return: An instance of the `Query` class with the filter applied 651 """ 652 self.query.setdefault('preset', 'index_components_market_pages') 653 self.query.setdefault('symbols', {})['symbolset'] = list(indexes) 654 # reset markets list and URL to `/global` 655 self.set_markets() 656 return self
Scan only the equities that are in the given index (or indexes).
Note that this resets the markets and sets the URL market to global.
Examples:
>>> Query().set_index('SYML:SP;SPX').get_scanner_data()
(503,
ticker name close volume market_cap_basic
0 NASDAQ:NVDA NVDA 1208.88 41238122 2.973644e+12
1 NASDAQ:AAPL AAPL 196.89 53103705 3.019127e+12
2 NASDAQ:TSLA TSLA 177.48 56244929 5.660185e+11
3 NASDAQ:AMD AMD 167.87 44795240 2.713306e+11
4 NASDAQ:MSFT MSFT 423.85 13621485 3.150183e+12
5 NASDAQ:AMZN AMZN 184.30 28021473 1.917941e+12
6 NASDAQ:META META 492.96 9379199 1.250410e+12
7 NASDAQ:GOOGL GOOGL 174.46 19660698 2.164346e+12
8 NASDAQ:SMCI SMCI 769.11 3444852 4.503641e+10
9 NASDAQ:GOOG GOOG 175.95 14716134 2.164346e+12
10 NASDAQ:AVGO AVGO 1406.64 1785876 6.518669e+11)
You can set multiple indices as well, like the NIFTY 50 and UK 100 Index.
>>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data()
(150,
ticker name close volume market_cap_basic
0 NSE:INFY INFY 1533.600000 24075302 7.623654e+10
1 LSE:AZN AZN 12556.000000 2903032 2.489770e+11
2 NSE:HDFCBANK HDFCBANK 1573.350000 18356108 1.432600e+11
3 NSE:RELIANCE RELIANCE 2939.899900 9279348 2.381518e+11
4 LSE:LSEG LSEG 9432.000000 2321053 6.395329e+10
5 NSE:BAJFINANCE BAJFINANCE 7191.399900 2984052 5.329685e+10
6 LSE:BARC BARC 217.250000 96238723 4.133010e+10
7 NSE:SBIN SBIN 829.950010 25061284 8.869327e+10
8 NSE:LT LT 3532.500000 5879660 5.816100e+10
9 LSE:SHEL SHEL 2732.500000 7448315 2.210064e+11)
You can find the full list of indices in constants.INDICES,
just note that the syntax is
SYML:{source};{symbol}.
Parameters
- indexes: One or more strings representing the financial indexes to filter by
Returns
An instance of the
Queryclass with the filter applied
691 def get_scanner_data_raw(self, **kwargs) -> ScreenerDict | ScreenerDictV2: 692 """ 693 Perform a POST web-request and return the data from the API (dictionary). 694 695 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 696 this can be very useful if you want to pass your own headers/cookies. 697 698 >>> Query().select('close', 'volume').limit(5).get_scanner_data_raw() 699 { 700 'totalCount': 17559, 701 'data': [ 702 {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]}, 703 {'s': 'AMEX:SPY', 'd': [542.04, 52331224]}, 704 {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]}, 705 {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]}, 706 {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]}, 707 ], 708 } 709 """ 710 self.query.setdefault('range', DEFAULT_RANGE.copy()) 711 712 kwargs.setdefault('headers', HEADERS) 713 kwargs.setdefault('timeout', 20) 714 r = requests.post(self.url, json=self.query, **kwargs) 715 716 if not r.ok: 717 # add the body to the error message for debugging purposes 718 r.reason += f'\n Body: {r.text}\n' 719 r.raise_for_status() 720 721 return r.json()
Perform a POST web-request and return the data from the API (dictionary).
Note that you can pass extra keyword-arguments that will be forwarded to requests.post(),
this can be very useful if you want to pass your own headers/cookies.
>>> Query().select('close', 'volume').limit(5).get_scanner_data_raw()
{
'totalCount': 17559,
'data': [
{'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]},
{'s': 'AMEX:SPY', 'd': [542.04, 52331224]},
{'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]},
{'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]},
{'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]},
],
}
723 def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]: 724 """ 725 Perform a POST web-request and return the data from the API as a DataFrame (along with 726 the number of rows/tickers that matched your query). 727 728 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 729 this can be very useful if you want to pass your own headers/cookies. 730 731 ### Live/Delayed data 732 733 Note that to get live-data you have to authenticate, which is done by passing your cookies. 734 Have a look in the README at the "Real-Time Data Access" sections. 735 736 :param kwargs: kwargs to pass to `requests.post()` 737 :return: a tuple consisting of: (total_count, dataframe) 738 """ 739 import pandas as pd 740 741 json_obj = self.get_scanner_data_raw(**kwargs) 742 rows_count = json_obj['totalCount'] 743 744 if '/scan2' in self.url: 745 columns = ['ticker', *json_obj['fields']] 746 rows = json_obj.get('symbols') 747 if rows: 748 df = pd.DataFrame(([row['s'], *row['f']] for row in rows), columns=columns) 749 else: 750 df = pd.DataFrame([], columns=columns) 751 else: 752 rows = json_obj['data'] 753 columns = ['ticker', *self.query.get('columns', ())] # pyright: ignore [reportArgumentType] 754 df = pd.DataFrame(([row['s'], *row['d']] for row in rows), columns=columns) 755 756 return rows_count, df
Perform a POST web-request and return the data from the API as a DataFrame (along with the number of rows/tickers that matched your query).
Note that you can pass extra keyword-arguments that will be forwarded to requests.post(),
this can be very useful if you want to pass your own headers/cookies.
Live/Delayed data
Note that to get live-data you have to authenticate, which is done by passing your cookies. Have a look in the README at the "Real-Time Data Access" sections.
Parameters
- kwargs: kwargs to pass to
requests.post()
Returns
a tuple consisting of: (total_count, dataframe)