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_earnings_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 the [Fields](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html) page. 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', 'price_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 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](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'stock'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) contains `'common'` or `'preferred'`, **OR** 416 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'fund'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) does not contain `'etf'`, **OR** 417 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) 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](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'stock'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) contains `'common'`, **OR** 430 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) 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 """ 448 self.query['filter2'] = operation['operation'] 449 return self 450 451 def order_by( 452 self, column: Column | str, ascending: bool = True, nulls_first: bool = False 453 ) -> Self: 454 """ 455 Applies sorting to the query results based on the specified column. 456 457 Examples: 458 459 >>> Query().order_by('volume', ascending=False) # sort descending 460 >>> Query().order_by('close', ascending=True) 461 >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False) 462 463 :param column: Either a `Column` object or a string with the column name. 464 :param ascending: Set to True for ascending order (default), or False for descending. 465 :param nulls_first: If True, places `None` values at the beginning of the results. Defaults 466 to False. 467 :return: The updated query object. 468 """ 469 dct: SortByDict = { 470 'sortBy': column.name if isinstance(column, Column) else column, 471 'sortOrder': 'asc' if ascending else 'desc', 472 'nullsFirst': nulls_first, 473 } 474 self.query['sort'] = dct 475 return self 476 477 def limit(self, limit: int) -> Self: 478 self.query.setdefault('range', DEFAULT_RANGE.copy())[1] = limit 479 return self 480 481 def offset(self, offset: int) -> Self: 482 self.query.setdefault('range', DEFAULT_RANGE.copy())[0] = offset 483 return self 484 485 def set_markets(self, *markets: str) -> Self: 486 """ 487 This method allows you to select the market/s which you want to query. 488 489 By default, the screener will only scan US equities, but you can change it to scan any 490 market or [country](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=country&expand), that includes a list of 67 countries, and also the following 491 asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`, 492 `futures`, `options`. 493 494 You may choose any value from the [Markets](https://shner-elmo.github.io/TradingView-Screener/markets.html) page. 495 496 If you select multiple countries, you might want to 497 498 Examples: 499 500 By default, the screener will show results from the `america` market, but you can 501 change it (note the difference between `market` and `country`) 502 >>> columns = ['close', 'market', 'country', 'currency'] 503 >>> (Query() 504 ... .select(*columns) 505 ... .set_markets('italy') 506 ... .get_scanner_data()) 507 (2346, 508 ticker close market country currency 509 0 MIL:UCG 23.9150 italy Italy EUR 510 1 MIL:ISP 2.4910 italy Italy EUR 511 2 MIL:STLAM 17.9420 italy Netherlands EUR 512 3 MIL:ENEL 6.0330 italy Italy EUR 513 4 MIL:ENI 15.4800 italy Italy EUR 514 .. ... ... ... ... ... 515 45 MIL:UNI 5.1440 italy Italy EUR 516 46 MIL:3OIS 0.4311 italy Ireland EUR 517 47 MIL:3SIL 35.2300 italy Ireland EUR 518 48 MIL:IWDE 69.1300 italy Ireland EUR 519 49 MIL:QQQS 19.2840 italy Ireland EUR 520 [50 rows x 5 columns]) 521 522 You can also select multiple markets 523 >>> (Query() 524 ... .select(*columns) 525 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 526 ... .get_scanner_data()) 527 (23964, 528 ticker close market country currency 529 0 AMEX:SPY 420.1617 america United States USD 530 1 NASDAQ:TSLA 201.2000 america United States USD 531 2 NASDAQ:NVDA 416.7825 america United States USD 532 3 NASDAQ:AMD 106.6600 america United States USD 533 4 NASDAQ:QQQ 353.7985 america United States USD 534 .. ... ... ... ... ... 535 45 NASDAQ:GOOGL 124.9200 america United States USD 536 46 HKEX:1211 233.2000 hongkong China HKD 537 47 TASE:ALHE 1995.0000 israel Israel ILA 538 48 AMEX:BIL 91.4398 america United States USD 539 49 NASDAQ:GOOG 126.1500 america United States USD 540 [50 rows x 5 columns]) 541 542 You may also select different financial instruments 543 >>> (Query() 544 ... .select('close', 'market') 545 ... .set_markets('cfd', 'crypto', 'forex', 'futures') 546 ... .get_scanner_data()) 547 (118076, 548 ticker ... market 549 0 UNISWAP3ETH:JUSTICEUSDT ... crypto 550 1 UNISWAP3ETH:UAHGUSDT ... crypto 551 2 UNISWAP3ETH:KENDUWETH ... crypto 552 3 UNISWAP3ETH:MATICSTMATIC ... crypto 553 4 UNISWAP3ETH:WETHETHM ... crypto 554 .. ... ... ... 555 45 UNISWAP:MUSICAIWETH_1F5304.USD ... crypto 556 46 CRYPTOCAP:FIL ... cfd 557 47 CRYPTOCAP:SUI ... cfd 558 48 CRYPTOCAP:ARBI ... cfd 559 49 CRYPTOCAP:OP ... cfd 560 [50 rows x 3 columns]) 561 562 :param markets: one or more markets 563 :return: Self 564 """ 565 if len(markets) == 1: 566 market = markets[0] 567 self.url = URL.format(market=market) 568 self.query['markets'] = [market] 569 else: # len(markets) == 0 or len(markets) > 1 570 self.url = URL.format(market='global') 571 self.query['markets'] = list(markets) 572 573 return self 574 575 def set_tickers(self, *tickers: str) -> Self: 576 """ 577 Set the tickers you wish to receive information on. 578 579 Note that this resets the markets and sets the URL market to `global`. 580 581 Examples: 582 583 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 584 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 585 (1, 586 ticker name market close volume VWAP MACD.macd 587 0 NASDAQ:TSLA TSLA america 186 3519931 185.53 2.371601) 588 589 To set multiple tickers: 590 >>> (Query() 591 ... .select('name', 'close', 'volume', 'market_cap_basic') 592 ... .set_tickers('NYSE:GME', 'MIL:RACE', 'HOSE:VIX') 593 ... .get_scanner_data()) 594 (3, 595 ticker name close volume market_cap_basic 596 0 MIL:RACE RACE 300.4 351982 8.161551e+10 597 1 NYSE:GME GME 21.8 6429236 9.781469e+09 598 2 HOSE:VIX VIX 17750.0 26524292 1.666686e+09) 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 Note that the syntax to specify an index is `SYML:{source};{symbol}`. 646 647 :param indexes: One or more strings representing the financial indexes to filter by 648 :return: An instance of the `Query` class with the filter applied 649 """ 650 self.query.setdefault('preset', 'index_components_market_pages') 651 self.query.setdefault('symbols', {})['symbolset'] = list(indexes) 652 # reset markets list and URL to `/global` 653 self.set_markets() 654 return self 655 656 # def set_currency(self, currency: Literal['symbol', 'market'] | str) -> Self: 657 # """ 658 # Change the currency of the screener. 659 # 660 # Note that this changes *only* the currency of the columns of type `fundamental_price`, 661 # for example: `market_cap_basic`, `net_income`, `total_debt`. Other columns like `close` and 662 # `Value.Traded` won't change, because they are of a different type. 663 # 664 # This can be particularly useful if you are querying tickers across different markets. 665 # 666 # Examples: 667 # 668 # >>> Query().set_currency('symbol') # convert to symbol currency 669 # >>> Query().set_currency('market') # convert to market currency 670 # >>> Query().set_currency('usd') # or another currency 671 # >>> Query().set_currency('jpy') 672 # >>> Query().set_currency('eur') 673 # """ 674 # # symbol currency -> self.query['price_conversion'] = {'to_symbol': True} 675 # # market currency -> self.query['price_conversion'] = {'to_symbol': False} 676 # # USD or other currency -> self.query['price_conversion'] = {'to_currency': 'usd'} 677 # if currency == 'symbol': 678 # self.query['price_conversion'] = {'to_symbol': True} 679 # elif currency == 'market': 680 # self.query['price_conversion'] = {'to_symbol': False} 681 # else: 682 # self.query['price_conversion'] = {'to_currency': currency} 683 # return self 684 685 def set_property(self, key: str, value: Any) -> Self: 686 self.query[key] = value 687 return self 688 689 def get_scanner_data_raw(self, **kwargs) -> ScreenerDict | ScreenerDictV2: 690 """ 691 Perform a POST web-request and return the data from the API (dictionary). 692 693 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 694 this can be very useful if you want to pass your own headers/cookies. 695 696 >>> Query().select('close', 'volume').limit(5).get_scanner_data_raw() 697 { 698 'totalCount': 17559, 699 'data': [ 700 {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]}, 701 {'s': 'AMEX:SPY', 'd': [542.04, 52331224]}, 702 {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]}, 703 {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]}, 704 {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]}, 705 ], 706 } 707 """ 708 self.query.setdefault('range', DEFAULT_RANGE.copy()) 709 710 kwargs.setdefault('headers', HEADERS) 711 kwargs.setdefault('timeout', 20) 712 r = requests.post(self.url, json=self.query, **kwargs) 713 714 if not r.ok: 715 # add the body to the error message for debugging purposes 716 r.reason += f'\n Body: {r.text}\n' 717 r.raise_for_status() 718 719 return r.json() 720 721 def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]: 722 """ 723 Perform a POST web-request and return the data from the API as a DataFrame (along with 724 the number of rows/tickers that matched your query). 725 726 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 727 this can be very useful if you want to pass your own headers/cookies. 728 729 ### Live/Delayed data 730 731 Note that to get live-data you have to authenticate, which is done by passing your cookies. 732 Have a look in the README at the "Real-Time Data Access" sections. 733 734 :param kwargs: kwargs to pass to `requests.post()` 735 :return: a tuple consisting of: (total_count, dataframe) 736 """ 737 import pandas as pd 738 739 json_obj = self.get_scanner_data_raw(**kwargs) 740 rows_count = json_obj['totalCount'] 741 742 if '/scan2' in self.url: 743 columns = ['ticker', *json_obj['fields']] 744 rows = json_obj.get('symbols') 745 if rows: 746 df = pd.DataFrame(([row['s'], *row['f']] for row in rows), columns=columns) 747 else: 748 df = pd.DataFrame([], columns=columns) 749 else: 750 rows = json_obj['data'] 751 columns = ['ticker', *self.query.get('columns', ())] # pyright: ignore [reportArgumentType] 752 df = pd.DataFrame(([row['s'], *row['d']] for row in rows), columns=columns) 753 754 return rows_count, df 755 756 def copy(self) -> Query: 757 new = Query() 758 new.query = self.query.copy() 759 return new 760 761 def __repr__(self) -> str: 762 return f'< {pprint.pformat(self.query)}\n url={self.url!r} >' 763 764 def __eq__(self, other) -> bool: 765 return isinstance(other, Query) and self.query == other.query and self.url == other.url 766 767 768# TODO: Query should have no defaults (except limit), and a separate module should have all the 769# default screeners 770# 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_earnings_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 the [Fields](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html) page. 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', 'price_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 371 def __init__(self, market: str = 'america') -> None: 372 # noinspection PyTypeChecker 373 self.query: QueryDict = copy.deepcopy(STOCKS_QUERY) 374 self.query['markets'] = [market] 375 self.url = URL.format(market=market) 376 377 def select(self, *columns: Column | str) -> Self: 378 self.query['columns'] = [ 379 col.name if isinstance(col, Column) else Column(col).name for col in columns 380 ] 381 return self 382 383 def where(self, *expressions: FilterOperationDict) -> Self: 384 """ 385 Filter screener (expressions are joined with the AND operator) 386 """ 387 self.query['filter'] = list(expressions) # convert tuple[dict] -> list[dict] 388 return self 389 390 def where2(self, operation: OperationDict) -> Self: 391 """ 392 Filter screener using AND/OR operators (nested expressions also allowed) 393 394 Rules: 395 1. The argument passed to `where2()` **must** be wrapped in `And()` or `Or()`. 396 2. `And()` and `Or()` can accept one or more conditions as arguments. 397 3. Conditions can be simple (e.g., `Column('field') == 'value'`) or complex, allowing nesting of `And()` and `Or()` to create intricate logical filters. 398 4. Unlike the `where()` method, which only supports chaining conditions with the `AND` operator, `where2()` allows mixing and nesting of `AND` and `OR` operators. 399 400 Examples: 401 402 1. **Combining conditions with `OR` and nested `AND`:** 403 ```python 404 Query() 405 .select('type', 'typespecs') 406 .where2( 407 Or( 408 And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])), 409 And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])), 410 Column('type') == 'dr', 411 ) 412 ) 413 ``` 414 415 This query filters entities where: 416 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'stock'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) contains `'common'` or `'preferred'`, **OR** 417 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'fund'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) does not contain `'etf'`, **OR** 418 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'dr'`. 419 420 2. **Mixing conditions with `OR`:** 421 ```python 422 Query().where2( 423 Or( 424 And(col('type') == 'stock', col('typespecs').has(['common'])), 425 col('type') == 'fund', 426 ) 427 ) 428 ``` 429 This query filters entities where: 430 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'stock'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) contains `'common'`, **OR** 431 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'fund'`. 432 433 3. **Combining conditions with `AND`:** 434 ```python 435 Query() 436 .set_markets('crypto') 437 .where2( 438 And( 439 col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']), 440 col('currency_id') == 'USD', 441 ) 442 ) 443 ``` 444 This query filters entities in the `'crypto'` market where: 445 - The `exchange` is one of `'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff'`, **AND** 446 - The `currency_id` is `'USD'`. 447 448 """ 449 self.query['filter2'] = operation['operation'] 450 return self 451 452 def order_by( 453 self, column: Column | str, ascending: bool = True, nulls_first: bool = False 454 ) -> Self: 455 """ 456 Applies sorting to the query results based on the specified column. 457 458 Examples: 459 460 >>> Query().order_by('volume', ascending=False) # sort descending 461 >>> Query().order_by('close', ascending=True) 462 >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False) 463 464 :param column: Either a `Column` object or a string with the column name. 465 :param ascending: Set to True for ascending order (default), or False for descending. 466 :param nulls_first: If True, places `None` values at the beginning of the results. Defaults 467 to False. 468 :return: The updated query object. 469 """ 470 dct: SortByDict = { 471 'sortBy': column.name if isinstance(column, Column) else column, 472 'sortOrder': 'asc' if ascending else 'desc', 473 'nullsFirst': nulls_first, 474 } 475 self.query['sort'] = dct 476 return self 477 478 def limit(self, limit: int) -> Self: 479 self.query.setdefault('range', DEFAULT_RANGE.copy())[1] = limit 480 return self 481 482 def offset(self, offset: int) -> Self: 483 self.query.setdefault('range', DEFAULT_RANGE.copy())[0] = offset 484 return self 485 486 def set_markets(self, *markets: str) -> Self: 487 """ 488 This method allows you to select the market/s which you want to query. 489 490 By default, the screener will only scan US equities, but you can change it to scan any 491 market or [country](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=country&expand), that includes a list of 67 countries, and also the following 492 asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`, 493 `futures`, `options`. 494 495 You may choose any value from the [Markets](https://shner-elmo.github.io/TradingView-Screener/markets.html) page. 496 497 If you select multiple countries, you might want to 498 499 Examples: 500 501 By default, the screener will show results from the `america` market, but you can 502 change it (note the difference between `market` and `country`) 503 >>> columns = ['close', 'market', 'country', 'currency'] 504 >>> (Query() 505 ... .select(*columns) 506 ... .set_markets('italy') 507 ... .get_scanner_data()) 508 (2346, 509 ticker close market country currency 510 0 MIL:UCG 23.9150 italy Italy EUR 511 1 MIL:ISP 2.4910 italy Italy EUR 512 2 MIL:STLAM 17.9420 italy Netherlands EUR 513 3 MIL:ENEL 6.0330 italy Italy EUR 514 4 MIL:ENI 15.4800 italy Italy EUR 515 .. ... ... ... ... ... 516 45 MIL:UNI 5.1440 italy Italy EUR 517 46 MIL:3OIS 0.4311 italy Ireland EUR 518 47 MIL:3SIL 35.2300 italy Ireland EUR 519 48 MIL:IWDE 69.1300 italy Ireland EUR 520 49 MIL:QQQS 19.2840 italy Ireland EUR 521 [50 rows x 5 columns]) 522 523 You can also select multiple markets 524 >>> (Query() 525 ... .select(*columns) 526 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 527 ... .get_scanner_data()) 528 (23964, 529 ticker close market country currency 530 0 AMEX:SPY 420.1617 america United States USD 531 1 NASDAQ:TSLA 201.2000 america United States USD 532 2 NASDAQ:NVDA 416.7825 america United States USD 533 3 NASDAQ:AMD 106.6600 america United States USD 534 4 NASDAQ:QQQ 353.7985 america United States USD 535 .. ... ... ... ... ... 536 45 NASDAQ:GOOGL 124.9200 america United States USD 537 46 HKEX:1211 233.2000 hongkong China HKD 538 47 TASE:ALHE 1995.0000 israel Israel ILA 539 48 AMEX:BIL 91.4398 america United States USD 540 49 NASDAQ:GOOG 126.1500 america United States USD 541 [50 rows x 5 columns]) 542 543 You may also select different financial instruments 544 >>> (Query() 545 ... .select('close', 'market') 546 ... .set_markets('cfd', 'crypto', 'forex', 'futures') 547 ... .get_scanner_data()) 548 (118076, 549 ticker ... market 550 0 UNISWAP3ETH:JUSTICEUSDT ... crypto 551 1 UNISWAP3ETH:UAHGUSDT ... crypto 552 2 UNISWAP3ETH:KENDUWETH ... crypto 553 3 UNISWAP3ETH:MATICSTMATIC ... crypto 554 4 UNISWAP3ETH:WETHETHM ... crypto 555 .. ... ... ... 556 45 UNISWAP:MUSICAIWETH_1F5304.USD ... crypto 557 46 CRYPTOCAP:FIL ... cfd 558 47 CRYPTOCAP:SUI ... cfd 559 48 CRYPTOCAP:ARBI ... cfd 560 49 CRYPTOCAP:OP ... cfd 561 [50 rows x 3 columns]) 562 563 :param markets: one or more markets 564 :return: Self 565 """ 566 if len(markets) == 1: 567 market = markets[0] 568 self.url = URL.format(market=market) 569 self.query['markets'] = [market] 570 else: # len(markets) == 0 or len(markets) > 1 571 self.url = URL.format(market='global') 572 self.query['markets'] = list(markets) 573 574 return self 575 576 def set_tickers(self, *tickers: str) -> Self: 577 """ 578 Set the tickers you wish to receive information on. 579 580 Note that this resets the markets and sets the URL market to `global`. 581 582 Examples: 583 584 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 585 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 586 (1, 587 ticker name market close volume VWAP MACD.macd 588 0 NASDAQ:TSLA TSLA america 186 3519931 185.53 2.371601) 589 590 To set multiple tickers: 591 >>> (Query() 592 ... .select('name', 'close', 'volume', 'market_cap_basic') 593 ... .set_tickers('NYSE:GME', 'MIL:RACE', 'HOSE:VIX') 594 ... .get_scanner_data()) 595 (3, 596 ticker name close volume market_cap_basic 597 0 MIL:RACE RACE 300.4 351982 8.161551e+10 598 1 NYSE:GME GME 21.8 6429236 9.781469e+09 599 2 HOSE:VIX VIX 17750.0 26524292 1.666686e+09) 600 601 :param tickers: One or more tickers, syntax: `exchange:symbol` 602 :return: Self 603 """ 604 self.query.setdefault('symbols', {})['tickers'] = list(tickers) 605 self.set_markets() 606 return self 607 608 def set_index(self, *indexes: str) -> Self: 609 """ 610 Scan only the equities that are in the given index (or indexes). 611 612 Note that this resets the markets and sets the URL market to `global`. 613 614 Examples: 615 616 >>> Query().set_index('SYML:SP;SPX').get_scanner_data() 617 (503, 618 ticker name close volume market_cap_basic 619 0 NASDAQ:NVDA NVDA 1208.88 41238122 2.973644e+12 620 1 NASDAQ:AAPL AAPL 196.89 53103705 3.019127e+12 621 2 NASDAQ:TSLA TSLA 177.48 56244929 5.660185e+11 622 3 NASDAQ:AMD AMD 167.87 44795240 2.713306e+11 623 4 NASDAQ:MSFT MSFT 423.85 13621485 3.150183e+12 624 5 NASDAQ:AMZN AMZN 184.30 28021473 1.917941e+12 625 6 NASDAQ:META META 492.96 9379199 1.250410e+12 626 7 NASDAQ:GOOGL GOOGL 174.46 19660698 2.164346e+12 627 8 NASDAQ:SMCI SMCI 769.11 3444852 4.503641e+10 628 9 NASDAQ:GOOG GOOG 175.95 14716134 2.164346e+12 629 10 NASDAQ:AVGO AVGO 1406.64 1785876 6.518669e+11) 630 631 You can set multiple indices as well, like the NIFTY 50 and UK 100 Index. 632 >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data() 633 (150, 634 ticker name close volume market_cap_basic 635 0 NSE:INFY INFY 1533.600000 24075302 7.623654e+10 636 1 LSE:AZN AZN 12556.000000 2903032 2.489770e+11 637 2 NSE:HDFCBANK HDFCBANK 1573.350000 18356108 1.432600e+11 638 3 NSE:RELIANCE RELIANCE 2939.899900 9279348 2.381518e+11 639 4 LSE:LSEG LSEG 9432.000000 2321053 6.395329e+10 640 5 NSE:BAJFINANCE BAJFINANCE 7191.399900 2984052 5.329685e+10 641 6 LSE:BARC BARC 217.250000 96238723 4.133010e+10 642 7 NSE:SBIN SBIN 829.950010 25061284 8.869327e+10 643 8 NSE:LT LT 3532.500000 5879660 5.816100e+10 644 9 LSE:SHEL SHEL 2732.500000 7448315 2.210064e+11) 645 646 Note that the syntax to specify an index is `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
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_earnings_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 the Fields page.
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', 'price_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])
383 def where(self, *expressions: FilterOperationDict) -> Self: 384 """ 385 Filter screener (expressions are joined with the AND operator) 386 """ 387 self.query['filter'] = list(expressions) # convert tuple[dict] -> list[dict] 388 return self
Filter screener (expressions are joined with the AND operator)
390 def where2(self, operation: OperationDict) -> Self: 391 """ 392 Filter screener using AND/OR operators (nested expressions also allowed) 393 394 Rules: 395 1. The argument passed to `where2()` **must** be wrapped in `And()` or `Or()`. 396 2. `And()` and `Or()` can accept one or more conditions as arguments. 397 3. Conditions can be simple (e.g., `Column('field') == 'value'`) or complex, allowing nesting of `And()` and `Or()` to create intricate logical filters. 398 4. Unlike the `where()` method, which only supports chaining conditions with the `AND` operator, `where2()` allows mixing and nesting of `AND` and `OR` operators. 399 400 Examples: 401 402 1. **Combining conditions with `OR` and nested `AND`:** 403 ```python 404 Query() 405 .select('type', 'typespecs') 406 .where2( 407 Or( 408 And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])), 409 And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])), 410 Column('type') == 'dr', 411 ) 412 ) 413 ``` 414 415 This query filters entities where: 416 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'stock'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) contains `'common'` or `'preferred'`, **OR** 417 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'fund'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) does not contain `'etf'`, **OR** 418 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'dr'`. 419 420 2. **Mixing conditions with `OR`:** 421 ```python 422 Query().where2( 423 Or( 424 And(col('type') == 'stock', col('typespecs').has(['common'])), 425 col('type') == 'fund', 426 ) 427 ) 428 ``` 429 This query filters entities where: 430 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'stock'` and [typespecs](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=typespecs&expand) contains `'common'`, **OR** 431 - The [type](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=type&expand) is `'fund'`. 432 433 3. **Combining conditions with `AND`:** 434 ```python 435 Query() 436 .set_markets('crypto') 437 .where2( 438 And( 439 col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']), 440 col('currency_id') == 'USD', 441 ) 442 ) 443 ``` 444 This query filters entities in the `'crypto'` market where: 445 - The `exchange` is one of `'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff'`, **AND** 446 - The `currency_id` is `'USD'`. 447 448 """ 449 self.query['filter2'] = operation['operation'] 450 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:
Mixing conditions with
OR:Query().where2( Or( And(col('type') == 'stock', col('typespecs').has(['common'])), col('type') == 'fund', ) )This query filters entities where:
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
452 def order_by( 453 self, column: Column | str, ascending: bool = True, nulls_first: bool = False 454 ) -> Self: 455 """ 456 Applies sorting to the query results based on the specified column. 457 458 Examples: 459 460 >>> Query().order_by('volume', ascending=False) # sort descending 461 >>> Query().order_by('close', ascending=True) 462 >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False) 463 464 :param column: Either a `Column` object or a string with the column name. 465 :param ascending: Set to True for ascending order (default), or False for descending. 466 :param nulls_first: If True, places `None` values at the beginning of the results. Defaults 467 to False. 468 :return: The updated query object. 469 """ 470 dct: SortByDict = { 471 'sortBy': column.name if isinstance(column, Column) else column, 472 'sortOrder': 'asc' if ascending else 'desc', 473 'nullsFirst': nulls_first, 474 } 475 self.query['sort'] = dct 476 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.
486 def set_markets(self, *markets: str) -> Self: 487 """ 488 This method allows you to select the market/s which you want to query. 489 490 By default, the screener will only scan US equities, but you can change it to scan any 491 market or [country](https://shner-elmo.github.io/TradingView-Screener/fields/stocks.html?field=country&expand), that includes a list of 67 countries, and also the following 492 asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`, 493 `futures`, `options`. 494 495 You may choose any value from the [Markets](https://shner-elmo.github.io/TradingView-Screener/markets.html) page. 496 497 If you select multiple countries, you might want to 498 499 Examples: 500 501 By default, the screener will show results from the `america` market, but you can 502 change it (note the difference between `market` and `country`) 503 >>> columns = ['close', 'market', 'country', 'currency'] 504 >>> (Query() 505 ... .select(*columns) 506 ... .set_markets('italy') 507 ... .get_scanner_data()) 508 (2346, 509 ticker close market country currency 510 0 MIL:UCG 23.9150 italy Italy EUR 511 1 MIL:ISP 2.4910 italy Italy EUR 512 2 MIL:STLAM 17.9420 italy Netherlands EUR 513 3 MIL:ENEL 6.0330 italy Italy EUR 514 4 MIL:ENI 15.4800 italy Italy EUR 515 .. ... ... ... ... ... 516 45 MIL:UNI 5.1440 italy Italy EUR 517 46 MIL:3OIS 0.4311 italy Ireland EUR 518 47 MIL:3SIL 35.2300 italy Ireland EUR 519 48 MIL:IWDE 69.1300 italy Ireland EUR 520 49 MIL:QQQS 19.2840 italy Ireland EUR 521 [50 rows x 5 columns]) 522 523 You can also select multiple markets 524 >>> (Query() 525 ... .select(*columns) 526 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 527 ... .get_scanner_data()) 528 (23964, 529 ticker close market country currency 530 0 AMEX:SPY 420.1617 america United States USD 531 1 NASDAQ:TSLA 201.2000 america United States USD 532 2 NASDAQ:NVDA 416.7825 america United States USD 533 3 NASDAQ:AMD 106.6600 america United States USD 534 4 NASDAQ:QQQ 353.7985 america United States USD 535 .. ... ... ... ... ... 536 45 NASDAQ:GOOGL 124.9200 america United States USD 537 46 HKEX:1211 233.2000 hongkong China HKD 538 47 TASE:ALHE 1995.0000 israel Israel ILA 539 48 AMEX:BIL 91.4398 america United States USD 540 49 NASDAQ:GOOG 126.1500 america United States USD 541 [50 rows x 5 columns]) 542 543 You may also select different financial instruments 544 >>> (Query() 545 ... .select('close', 'market') 546 ... .set_markets('cfd', 'crypto', 'forex', 'futures') 547 ... .get_scanner_data()) 548 (118076, 549 ticker ... market 550 0 UNISWAP3ETH:JUSTICEUSDT ... crypto 551 1 UNISWAP3ETH:UAHGUSDT ... crypto 552 2 UNISWAP3ETH:KENDUWETH ... crypto 553 3 UNISWAP3ETH:MATICSTMATIC ... crypto 554 4 UNISWAP3ETH:WETHETHM ... crypto 555 .. ... ... ... 556 45 UNISWAP:MUSICAIWETH_1F5304.USD ... crypto 557 46 CRYPTOCAP:FIL ... cfd 558 47 CRYPTOCAP:SUI ... cfd 559 48 CRYPTOCAP:ARBI ... cfd 560 49 CRYPTOCAP:OP ... cfd 561 [50 rows x 3 columns]) 562 563 :param markets: one or more markets 564 :return: Self 565 """ 566 if len(markets) == 1: 567 market = markets[0] 568 self.url = URL.format(market=market) 569 self.query['markets'] = [market] 570 else: # len(markets) == 0 or len(markets) > 1 571 self.url = URL.format(market='global') 572 self.query['markets'] = list(markets) 573 574 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 the Markets page.
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
Returns
Self
576 def set_tickers(self, *tickers: str) -> Self: 577 """ 578 Set the tickers you wish to receive information on. 579 580 Note that this resets the markets and sets the URL market to `global`. 581 582 Examples: 583 584 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 585 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 586 (1, 587 ticker name market close volume VWAP MACD.macd 588 0 NASDAQ:TSLA TSLA america 186 3519931 185.53 2.371601) 589 590 To set multiple tickers: 591 >>> (Query() 592 ... .select('name', 'close', 'volume', 'market_cap_basic') 593 ... .set_tickers('NYSE:GME', 'MIL:RACE', 'HOSE:VIX') 594 ... .get_scanner_data()) 595 (3, 596 ticker name close volume market_cap_basic 597 0 MIL:RACE RACE 300.4 351982 8.161551e+10 598 1 NYSE:GME GME 21.8 6429236 9.781469e+09 599 2 HOSE:VIX VIX 17750.0 26524292 1.666686e+09) 600 601 :param tickers: One or more tickers, syntax: `exchange:symbol` 602 :return: Self 603 """ 604 self.query.setdefault('symbols', {})['tickers'] = list(tickers) 605 self.set_markets() 606 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 multiple tickers:
>>> (Query()
... .select('name', 'close', 'volume', 'market_cap_basic')
... .set_tickers('NYSE:GME', 'MIL:RACE', 'HOSE:VIX')
... .get_scanner_data())
(3,
ticker name close volume market_cap_basic
0 MIL:RACE RACE 300.4 351982 8.161551e+10
1 NYSE:GME GME 21.8 6429236 9.781469e+09
2 HOSE:VIX VIX 17750.0 26524292 1.666686e+09)
Parameters
- **tickers: One or more tickers, syntax:
exchange**: symbol
Returns
Self
608 def set_index(self, *indexes: str) -> Self: 609 """ 610 Scan only the equities that are in the given index (or indexes). 611 612 Note that this resets the markets and sets the URL market to `global`. 613 614 Examples: 615 616 >>> Query().set_index('SYML:SP;SPX').get_scanner_data() 617 (503, 618 ticker name close volume market_cap_basic 619 0 NASDAQ:NVDA NVDA 1208.88 41238122 2.973644e+12 620 1 NASDAQ:AAPL AAPL 196.89 53103705 3.019127e+12 621 2 NASDAQ:TSLA TSLA 177.48 56244929 5.660185e+11 622 3 NASDAQ:AMD AMD 167.87 44795240 2.713306e+11 623 4 NASDAQ:MSFT MSFT 423.85 13621485 3.150183e+12 624 5 NASDAQ:AMZN AMZN 184.30 28021473 1.917941e+12 625 6 NASDAQ:META META 492.96 9379199 1.250410e+12 626 7 NASDAQ:GOOGL GOOGL 174.46 19660698 2.164346e+12 627 8 NASDAQ:SMCI SMCI 769.11 3444852 4.503641e+10 628 9 NASDAQ:GOOG GOOG 175.95 14716134 2.164346e+12 629 10 NASDAQ:AVGO AVGO 1406.64 1785876 6.518669e+11) 630 631 You can set multiple indices as well, like the NIFTY 50 and UK 100 Index. 632 >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data() 633 (150, 634 ticker name close volume market_cap_basic 635 0 NSE:INFY INFY 1533.600000 24075302 7.623654e+10 636 1 LSE:AZN AZN 12556.000000 2903032 2.489770e+11 637 2 NSE:HDFCBANK HDFCBANK 1573.350000 18356108 1.432600e+11 638 3 NSE:RELIANCE RELIANCE 2939.899900 9279348 2.381518e+11 639 4 LSE:LSEG LSEG 9432.000000 2321053 6.395329e+10 640 5 NSE:BAJFINANCE BAJFINANCE 7191.399900 2984052 5.329685e+10 641 6 LSE:BARC BARC 217.250000 96238723 4.133010e+10 642 7 NSE:SBIN SBIN 829.950010 25061284 8.869327e+10 643 8 NSE:LT LT 3532.500000 5879660 5.816100e+10 644 9 LSE:SHEL SHEL 2732.500000 7448315 2.210064e+11) 645 646 Note that the syntax to specify an index is `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
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)
Note that the syntax to specify an index 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
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()
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]},
],
}
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
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)