tradingview_screener.query
1from __future__ import annotations 2 3__all__ = ['Query', 'Column'] 4 5import pprint 6from typing import TypedDict, Any, Literal, Optional, Iterable 7 8import requests 9import pandas as pd 10 11from tradingview_screener.constants import COLUMNS, MARKETS, HEADERS, URL 12 13 14class FilterOperationDict(TypedDict): 15 left: str 16 operation: Literal[ 17 'greater', 18 'egreater', 19 'less', 20 'eless', 21 'equal', 22 'nequal', 23 'in_range', 24 'not_in_range', 25 'match', # the same as: `LOWER(col) LIKE '%pattern%'` 26 'crosses', 27 'crosses_above', 28 'crosses_below', 29 'above%', 30 'below%', 31 'in_range%', 32 'not_in_range%', 33 'has', # set must contain one of the values 34 'has_none_of', # set must NOT contain ANY of the values 35 ] 36 right: Any 37 38 39class SortByDict(TypedDict): 40 sortBy: str 41 sortOrder: Literal['asc', 'desc'] 42 43 44class QueryDict(TypedDict): 45 """ 46 The fields that can be passed to the tradingview scan API 47 """ 48 49 # TODO: test which optional ... 50 markets: list[str] 51 symbols: dict 52 options: dict 53 columns: list[str] 54 filter: list[FilterOperationDict] 55 sort: SortByDict 56 range: list[int] # a with two integers, i.e. `[0, 100]` 57 58 59class Column: 60 """ 61 A Column object represents a field in the tradingview stock screener, 62 and it's used in SELECT queries and WHERE queries with the `Query` object. 63 64 A `Column` supports all the comparison operations: 65 `<`, `<=`, `>`, `>=`, `==`, `!=`, and also other methods like `between()`, `isin()`, etc. 66 67 Examples: 68 69 Some of the operations that you can do with `Column` objects: 70 >>> Column('close') >= 2.5 71 >>> Column('close').between(2.5, 15) 72 >>> Column('high') > Column('VWAP') 73 >>> Column('close').between(Column('EMA5'), Column('EMA20') 74 >>> Column('type').isin(['stock', 'fund']) 75 >>> Column('description').like('apple') # the same as `description LIKE '%apple%'` 76 """ 77 78 def __init__(self, name: str) -> None: 79 """ 80 Create a column object from a given column name 81 82 :param name: string, should be either a key or a value from the `COLUMNS` dictionary 83 """ 84 # if `name` is a dictionary key: get its value. otherwise make sure that it's a 85 # dictionary value. 86 self.name = COLUMNS.get(name, name) 87 88 # disable this method and do the column/field validation through the server 89 # @classmethod 90 # def from_unknown_name(cls, name: str) -> Column: 91 # """ 92 # Create a column object from a column name that isn't in the `COLUMNS` dictionary 93 # 94 # :param name: string, column name 95 # :return: Column 96 # """ 97 # # close is just a temporary column, so it won't raise an error at `__init__` 98 # column = cls(name='close') 99 # column.name = name 100 # return column 101 102 @staticmethod 103 def _extract_value(obj) -> ...: 104 if isinstance(obj, Column): 105 return obj.name 106 return obj 107 108 def __gt__(self, other) -> FilterOperationDict: 109 return FilterOperationDict( 110 left=self.name, operation='greater', right=self._extract_value(other) 111 ) 112 113 def __ge__(self, other) -> FilterOperationDict: 114 return FilterOperationDict( 115 left=self.name, operation='egreater', right=self._extract_value(other) 116 ) 117 118 def __lt__(self, other) -> FilterOperationDict: 119 return FilterOperationDict( 120 left=self.name, operation='less', right=self._extract_value(other) 121 ) 122 123 def __le__(self, other) -> FilterOperationDict: 124 return FilterOperationDict( 125 left=self.name, operation='eless', right=self._extract_value(other) 126 ) 127 128 def __eq__(self, other) -> FilterOperationDict: 129 return FilterOperationDict( 130 left=self.name, operation='equal', right=self._extract_value(other) 131 ) 132 133 def __ne__(self, other) -> FilterOperationDict: 134 return FilterOperationDict( 135 left=self.name, operation='nequal', right=self._extract_value(other) 136 ) 137 138 def crosses(self, other) -> FilterOperationDict: 139 return FilterOperationDict( 140 left=self.name, operation='crosses', right=self._extract_value(other) 141 ) 142 143 def crosses_above(self, other) -> FilterOperationDict: 144 return FilterOperationDict( 145 left=self.name, operation='crosses_above', right=self._extract_value(other) 146 ) 147 148 def crosses_below(self, other) -> FilterOperationDict: 149 return FilterOperationDict( 150 left=self.name, operation='crosses_below', right=self._extract_value(other) 151 ) 152 153 def between(self, left, right) -> FilterOperationDict: 154 return FilterOperationDict( 155 left=self.name, 156 operation='in_range', 157 right=[self._extract_value(left), self._extract_value(right)], 158 ) 159 160 def not_between(self, left, right) -> FilterOperationDict: 161 return FilterOperationDict( 162 left=self.name, 163 operation='not_in_range', 164 right=[self._extract_value(left), self._extract_value(right)], 165 ) 166 167 def isin(self, values) -> FilterOperationDict: 168 return FilterOperationDict(left=self.name, operation='in_range', right=list(values)) 169 170 def not_in(self, values: Iterable) -> FilterOperationDict: 171 return {'left': self.name, 'operation': 'not_in_range', 'right': list(values)} 172 173 def has(self, values: Iterable) -> FilterOperationDict: 174 """ 175 Field contains any of the values 176 177 (it's the same as `isin()`, except that it works on fields of type `set`) 178 """ 179 return {'left': self.name, 'operation': 'has', 'right': list(values)} 180 181 def has_none_of(self, values: Iterable) -> FilterOperationDict: 182 """ 183 Field doesn't contain any of the values 184 185 (it's the same as `not_in()`, except that it works on fields of type `set`) 186 """ 187 return {'left': self.name, 'operation': 'has_none_of', 'right': list(values)} 188 189 def above_pct(self, column: Column | str, pct: float) -> FilterOperationDict: 190 """ 191 Examples: 192 193 The closing price is higher than the VWAP by more than 3% 194 >>> Column('close').above_pct('VWAP', 1.03) 195 196 closing price is above the 52-week-low by more than 150% 197 >>> Column('close').above_pct('price_52_week_low', 2.5) 198 """ 199 return { 200 'left': self.name, 201 'operation': 'above%', 202 'right': [self._extract_value(column), pct], 203 } 204 205 def below_pct(self, column: Column | str, pct: float) -> FilterOperationDict: 206 """ 207 Examples: 208 209 The closing price is lower than the VWAP by 3% or more 210 >>> Column('close').below_pct('VWAP', 1.03) 211 """ 212 return { 213 'left': self.name, 214 'operation': 'below%', 215 'right': [self._extract_value(column), pct], 216 } 217 218 def between_pct( 219 self, column: Column | str, pct1: float, pct2: Optional[float] = None 220 ) -> FilterOperationDict: 221 """ 222 Examples: 223 224 The percentage change between the Close and the EMA is between 20% and 50% 225 >>> Column('close').between_pct('EMA200', 1.2, 1.5) 226 """ 227 return { 228 'left': self.name, 229 'operation': 'in_range%', 230 'right': [self._extract_value(column), pct1, pct2], 231 } 232 233 def not_between_pct( 234 self, column: Column | str, pct1: float, pct2: Optional[float] = None 235 ) -> FilterOperationDict: 236 """ 237 Examples: 238 239 The percentage change between the Close and the EMA is between 20% and 50% 240 >>> Column('close').not_between_pct('EMA200', 1.2, 1.5) 241 """ 242 return { 243 'left': self.name, 244 'operation': 'not_in_range%', 245 'right': [self._extract_value(column), pct1, pct2], 246 } 247 248 def like(self, other) -> FilterOperationDict: 249 return FilterOperationDict( 250 left=self.name, operation='match', right=self._extract_value(other) 251 ) 252 253 def __repr__(self) -> str: 254 return f'< Column({self.name!r}) >' 255 256 257class Query: 258 """ 259 This class allows you to perform SQL-like queries on the tradingview stock-screener. 260 261 The `Query` object reppresents a query that can be made to the official tradingview API, and it 262 stores all the data as JSON internally. 263 264 Examples: 265 266 To perform a simple query all you have to do is: 267 >>> from tradingview_screener import Query 268 >>> Query().get_scanner_data() 269 (18060, 270 ticker name close volume market_cap_basic 271 0 AMEX:SPY SPY 410.68 107367671 NaN 272 1 NASDAQ:QQQ QQQ 345.31 63475390 NaN 273 2 NASDAQ:TSLA TSLA 207.30 94879471 6.589904e+11 274 3 NASDAQ:NVDA NVDA 405.00 41677185 1.000350e+12 275 4 NASDAQ:AMZN AMZN 127.74 125309313 1.310658e+12 276 .. ... ... ... ... ... 277 45 NYSE:UNH UNH 524.66 2585616 4.859952e+11 278 46 NASDAQ:DXCM DXCM 89.29 14954605 3.449933e+10 279 47 NYSE:MA MA 364.08 3624883 3.429080e+11 280 48 NYSE:ABBV ABBV 138.93 9427212 2.452179e+11 281 49 AMEX:XLK XLK 161.12 8115780 NaN 282 [50 rows x 5 columns]) 283 284 The `get_scanner_data()` method will return a tuple with the first element being the number of 285 records that were found (like a `COUNT(*)`), and the second element contains the data that was 286 found as a DataFrame. 287 288 --- 289 290 By default, the `Query` will select the columns: `name`, `close`, `volume`, `market_cap_basic`, 291 but you override that 292 >>> (Query() 293 ... .select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI', 'Price to Earnings Ratio (TTM)') 294 ... .get_scanner_data()) 295 (18060, 296 ticker open high ... MACD.macd RSI price_earnings_ttm 297 0 AMEX:SPY 414.19 414.600 ... -5.397135 29.113396 NaN 298 1 NASDAQ:QQQ 346.43 348.840 ... -4.321482 34.335449 NaN 299 2 NASDAQ:TSLA 210.60 212.410 ... -12.224250 28.777229 66.752536 300 3 NASDAQ:NVDA 411.30 412.060 ... -8.738986 37.845668 97.835540 301 4 NASDAQ:AMZN 126.20 130.020 ... -2.025378 48.665666 66.697995 302 .. ... ... ... ... ... ... ... 303 45 NYSE:UNH 525.99 527.740 ... 6.448129 54.614775 22.770713 304 46 NASDAQ:DXCM 92.73 92.988 ... -2.376942 52.908093 98.914368 305 47 NYSE:MA 366.49 368.285 ... -7.496065 22.614078 31.711800 306 48 NYSE:ABBV 138.77 143.000 ... -1.708497 27.117232 37.960054 307 49 AMEX:XLK 161.17 162.750 ... -1.520828 36.868658 NaN 308 [50 rows x 8 columns]) 309 310 You can find the 250+ columns available in `tradingview_screener.constants.COLUMNS`. 311 312 Now let's do some queries using the `WHERE` statement, select all the stocks that the `close` is 313 bigger or equal than 350 314 >>> (Query() 315 ... .select('close', 'volume', '52 Week High') 316 ... .where(Column('close') >= 350) 317 ... .get_scanner_data()) 318 (159, 319 ticker close volume price_52_week_high 320 0 AMEX:SPY 410.68 107367671 459.44 321 1 NASDAQ:NVDA 405.00 41677185 502.66 322 2 NYSE:BRK.A 503375.05 7910 566569.97 323 3 AMEX:IVV 412.55 5604525 461.88 324 4 AMEX:VOO 377.32 5638752 422.15 325 .. ... ... ... ... 326 45 NASDAQ:EQIX 710.39 338549 821.63 327 46 NYSE:MCK 448.03 527406 465.90 328 47 NYSE:MTD 976.25 241733 1615.97 329 48 NASDAQ:CTAS 496.41 464631 525.37 330 49 NASDAQ:ROP 475.57 450141 508.90 331 [50 rows x 4 columns]) 332 333 You can even use other columns in these kind of operations 334 >>> (Query() 335 ... .select('close', 'VWAP') 336 ... .where(Column('close') >= Column('VWAP')) 337 ... .get_scanner_data()) 338 (9044, 339 ticker close VWAP 340 0 NASDAQ:AAPL 168.22 168.003333 341 1 NASDAQ:META 296.73 296.336667 342 2 NASDAQ:GOOGL 122.17 121.895233 343 3 NASDAQ:AMD 96.43 96.123333 344 4 NASDAQ:GOOG 123.40 123.100000 345 .. ... ... ... 346 45 NYSE:GD 238.25 238.043333 347 46 NYSE:GOLD 16.33 16.196667 348 47 AMEX:SLV 21.18 21.041667 349 48 AMEX:VXX 27.08 26.553333 350 49 NYSE:SLB 55.83 55.676667 351 [50 rows x 3 columns]) 352 353 Let's find all the stocks that the price is between the EMA 5 and 20, and the type is a stock 354 or fund 355 >>> (Query() 356 ... .select('close', 'volume', 'EMA5', 'EMA20', 'type') 357 ... .where( 358 ... Column('close').between(Column('EMA5'), Column('EMA20')), 359 ... Column('type').isin(['stock', 'fund']) 360 ... ) 361 ... .get_scanner_data()) 362 (1730, 363 ticker close volume EMA5 EMA20 type 364 0 NASDAQ:AMZN 127.74 125309313 125.033517 127.795142 stock 365 1 AMEX:HYG 72.36 35621800 72.340776 72.671058 fund 366 2 AMEX:LQD 99.61 21362859 99.554272 100.346388 fund 367 3 NASDAQ:IEF 90.08 11628236 89.856804 90.391503 fund 368 4 NYSE:SYK 261.91 3783608 261.775130 266.343290 stock 369 .. ... ... ... ... ... ... 370 45 NYSE:EMN 72.58 1562328 71.088034 72.835394 stock 371 46 NYSE:KIM 16.87 6609420 16.858920 17.096582 fund 372 47 NASDAQ:COLM 71.34 1516675 71.073116 71.658864 stock 373 48 NYSE:AOS 67.81 1586796 67.561619 67.903168 stock 374 49 NASDAQ:IGIB 47.81 2073538 47.761338 48.026795 fund 375 [50 rows x 6 columns]) 376 377 There are also the `ORDER BY`, `OFFSET`, and `LIMIT` statements. 378 Let's select all the tickers with a market cap between 1M and 50M, that have a relative volume 379 bigger than 1.2, and that the MACD is positive 380 >>> (Query() 381 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 382 ... .where( 383 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 384 ... Column('relative_volume_10d_calc') > 1.2, 385 ... Column('MACD.macd') >= Column('MACD.signal') 386 ... ) 387 ... .order_by('volume', ascending=False) 388 ... .offset(5) 389 ... .limit(15) 390 ... .get_scanner_data()) 391 (393, 392 ticker name close volume relative_volume_10d_calc 393 0 OTC:YCRM YCRM 0.0120 19626514 1.887942 394 1 OTC:PLPL PLPL 0.0002 17959914 3.026059 395 2 NASDAQ:ABVC ABVC 1.3800 16295824 1.967505 396 3 OTC:TLSS TLSS 0.0009 15671157 1.877976 397 4 OTC:GVSI GVSI 0.0128 14609774 2.640792 398 5 OTC:IGEX IGEX 0.0012 14285592 1.274861 399 6 OTC:EEGI EEGI 0.0004 12094000 2.224749 400 7 NASDAQ:GLG GLG 0.0591 9811974 1.990526 401 8 NASDAQ:TCRT TCRT 0.0890 8262894 2.630553 402 9 OTC:INKW INKW 0.0027 7196404 1.497134) 403 404 To avoid rewriting the same query again and again, you can save the query to a variable and 405 just call `get_scanner_data()` again and again to get the latest data: 406 >>> top_50_bullish = (Query() 407 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 408 ... .where( 409 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 410 ... Column('relative_volume_10d_calc') > 1.2, 411 ... Column('MACD.macd') >= Column('MACD.signal') 412 ... ) 413 ... .order_by('volume', ascending=False) 414 ... .limit(50)) 415 >>> top_50_bullish.get_scanner_data() 416 (393, 417 ticker name close volume relative_volume_10d_calc 418 0 OTC:BEGI BEGI 0.001050 127874055 3.349924 419 1 OTC:HCMC HCMC 0.000100 126992562 1.206231 420 2 OTC:HEMP HEMP 0.000150 101382713 1.775458 421 3 OTC:SONG SONG 0.000800 92640779 1.805721 422 4 OTC:APRU APRU 0.001575 38104499 29.028958 423 .. ... ... ... ... ... 424 45 OTC:BSHPF BSHPF 0.001000 525000 1.280899 425 46 OTC:GRHI GRHI 0.033000 507266 1.845738 426 47 OTC:OMGGF OMGGF 0.035300 505000 4.290059 427 48 NASDAQ:GBNH GBNH 0.273000 500412 9.076764 428 49 OTC:CLRMF CLRMF 0.032500 496049 17.560935 429 [50 rows x 5 columns]) 430 """ 431 432 def __init__(self) -> None: 433 # noinspection PyTypeChecker 434 self.query: QueryDict = { 435 'markets': ['america'], 436 'symbols': {'query': {'types': []}, 'tickers': []}, 437 'options': {'lang': 'en'}, 438 'columns': ['name', 'close', 'volume', 'market_cap_basic'], 439 # 'filter': ..., 440 'sort': {'sortBy': 'Value.Traded', 'sortOrder': 'desc'}, 441 'range': [0, 50], 442 } 443 self.url = 'https://scanner.tradingview.com/america/scan' 444 445 def set_markets(self, *markets: str) -> Query: 446 """ 447 This method allows you to select the market/s which you want to query. 448 449 By default, the screener will only scan US equities, but you can change it to scan any 450 or even multiple markets, that includes a list of 67 countries, and also the following 451 commodities: `bonds`, `cfd`, `coin`, `crypto`, `economics2`, `euronext`, `forex`, 452 `futures`, `options`. 453 454 You may choose any value from `tradingview_screener.constants.MARKETS`. 455 456 Examples: 457 458 By default, the screener will search the `america` market 459 >>> default_columns = ['close', 'market', 'country', 'currency'] 460 >>> Query().select(*default_columns).get_scanner_data() 461 (17898, 462 ticker close market country currency 463 0 AMEX:SPY 419.9900 america United States USD 464 1 NASDAQ:TSLA 201.7201 america United States USD 465 2 NASDAQ:NVDA 416.3800 america United States USD 466 3 NASDAQ:AMD 106.4499 america United States USD 467 4 NASDAQ:QQQ 353.4000 america United States USD 468 .. ... ... ... ... ... 469 45 NASDAQ:ADBE 538.0000 america United States USD 470 46 NYSE:BA 188.9000 america United States USD 471 47 NASDAQ:SBUX 90.9100 america United States USD 472 48 NYSE:HUM 500.6350 america United States USD 473 49 NYSE:CAT 227.3400 america United States USD 474 [50 rows x 5 columns]) 475 476 But you can change it (note the difference between `market` and `country`) 477 >>> (Query() 478 ... .select(*default_columns) 479 ... .set_markets('italy') 480 ... .get_scanner_data()) 481 (2346, 482 ticker close market country currency 483 0 MIL:UCG 23.9150 italy Italy EUR 484 1 MIL:ISP 2.4910 italy Italy EUR 485 2 MIL:STLAM 17.9420 italy Netherlands EUR 486 3 MIL:ENEL 6.0330 italy Italy EUR 487 4 MIL:ENI 15.4800 italy Italy EUR 488 .. ... ... ... ... ... 489 45 MIL:UNI 5.1440 italy Italy EUR 490 46 MIL:3OIS 0.4311 italy Ireland EUR 491 47 MIL:3SIL 35.2300 italy Ireland EUR 492 48 MIL:IWDE 69.1300 italy Ireland EUR 493 49 MIL:QQQS 19.2840 italy Ireland EUR 494 [50 rows x 5 columns]) 495 496 You can also select multiple markets 497 >>> (Query() 498 ... .select(*default_columns) 499 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 500 ... .get_scanner_data()) 501 (23964, 502 ticker close market country currency 503 0 AMEX:SPY 420.1617 america United States USD 504 1 NASDAQ:TSLA 201.2000 america United States USD 505 2 NASDAQ:NVDA 416.7825 america United States USD 506 3 NASDAQ:AMD 106.6600 america United States USD 507 4 NASDAQ:QQQ 353.7985 america United States USD 508 .. ... ... ... ... ... 509 45 NASDAQ:GOOGL 124.9200 america United States USD 510 46 HKEX:1211 233.2000 hongkong China HKD 511 47 TASE:ALHE 1995.0000 israel Israel ILA 512 48 AMEX:BIL 91.4398 america United States USD 513 49 NASDAQ:GOOG 126.1500 america United States USD 514 [50 rows x 5 columns]) 515 516 You may also select different financial instruments 517 >>> (Query() 518 ... .select('close', 'market') 519 ... .set_markets('cfd', 'crypto', 'futures', 'options') 520 ... .get_scanner_data()) 521 (118076, 522 ticker ... market 523 0 UNISWAP3ETH:WETHVGT ... crypto 524 1 UNISWAP3POLYGON:BONKWMATIC ... crypto 525 2 UNISWAP3ARBITRUM:WETHTROVE ... crypto 526 3 UNISWAP3ETH:USDTBRD ... crypto 527 4 UNISWAP3ETH:WBTCAUSD ... crypto 528 .. ... ... ... 529 45 NSE:IDEAF2024 ... futures 530 46 NSE:INDUSTOWERX2023 ... futures 531 47 NSE:INDUSTOWER1! ... futures 532 48 BIST:XU100 ... cfd 533 49 BYBIT:BTCUSD.P ... crypto 534 [50 rows x 3 columns]) 535 536 To select all the avaialble markets you can do this trick 537 >>> from tradingview_screener.constants import MARKETS 538 >>> len(MARKETS) 539 76 540 >>> (Query() 541 ... .select('close', 'market') 542 ... .set_markets(*MARKETS) 543 ... .get_scanner_data()) # notice how many records we find: over 240k 544 (241514, 545 ticker ... market 546 0 UNISWAP3ETH:WETHVGT ... crypto 547 1 UNISWAP3POLYGON:BONKWMATIC ... crypto 548 2 UNISWAP3ARBITRUM:WETHTROVE ... crypto 549 3 UNISWAP3ETH:USDTBRD ... crypto 550 4 UNISWAP3ETH:WBTCAUSD ... crypto 551 .. ... ... ... 552 45 NSE:IDEAF2024 ... futures 553 46 NSE:INDUSTOWER1! ... futures 554 47 NSE:INDUSTOWERX2023 ... futures 555 48 BIST:XU100 ... cfd 556 49 BYBIT:BTCUSD.P ... crypto 557 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 assert market in MARKETS 566 567 self.url = URL.format(market=market) 568 self.query['markets'] = [market] 569 570 elif len(markets) >= 1: 571 for m in markets: 572 assert m in MARKETS 573 574 self.url = URL.format(market='global') 575 self.query['markets'] = list(markets) 576 577 return self 578 579 def set_tickers(self, *tickers: str) -> Query: 580 """ 581 Set the tickers you wish to receive information on. 582 583 Examples: 584 585 >>> Query().limit(5).get_scanner_data() 586 (17879, 587 ticker name close volume market_cap_basic 588 0 NASDAQ:TSLA TSLA 248.50 118559595 7.887376e+11 589 1 AMEX:SPY SPY 445.52 62066984 NaN 590 2 NASDAQ:NVDA NVDA 455.72 47389801 1.125628e+12 591 3 NASDAQ:QQQ QQQ 372.58 35846281 NaN 592 4 NASDAQ:AAPL AAPL 178.18 65600673 2.785707e+12) 593 594 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 595 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 596 (2, 597 ticker name market close volume VWAP MACD.macd 598 0 NASDAQ:TSLA TSLA america 248.50 118559595 250.563333 0.730376 599 1 NASDAQ:NVDA NVDA america 455.72 47389801 458.163333 7.927189) 600 601 >>> q.set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX').get_scanner_data() 602 (4, 603 ticker name market close volume VWAP MACD.macd 604 0 HOSE:VIX VIX vietnam 19800.00 26292400 19883.333333 1291.359459 605 1 AMEX:SPY SPY america 445.52 62066984 445.720000 0.484263 606 2 NYSE:GME GME america 17.71 4693902 17.853333 -0.660342 607 3 MIL:RACE RACE italy 279.30 246547 279.033327 -1.398701) 608 609 :param tickers: One or more tickers, syntax: `exchange:symbol` 610 :return: Self 611 """ 612 # no need to select the market if we specify the symbol we want 613 # noinspection PyTypedDict 614 self.query.pop('markets', None) 615 616 self.query['symbols'] = {'tickers': list(tickers)} 617 self.url = 'https://scanner.tradingview.com/global/scan' 618 return self 619 620 def select(self, *columns: Column | str) -> Query: 621 self.query['columns'] = [ 622 col.name if isinstance(col, Column) else Column(col).name for col in columns 623 ] 624 return self 625 626 def where(self, *expressions: FilterOperationDict) -> Query: 627 self.query['filter'] = list(expressions) # convert tuple[dict] -> list[dict] 628 return self 629 630 def order_by(self, column: Column | str, ascending: bool = True) -> Query: 631 column = column.name if isinstance(column, Column) else Column(column).name 632 sort_order = 'asc' if ascending else 'desc' 633 # noinspection PyTypeChecker 634 self.query['sort'] = SortByDict(sortBy=column, sortOrder=sort_order) 635 return self 636 637 def offset(self, offset: int) -> Query: 638 self.query['range'][0] = offset 639 return self 640 641 def limit(self, limit: int) -> Query: 642 self.query['range'][1] = limit 643 return self 644 645 # def set_options(self, options) -> None: 646 # raise NotImplementedError 647 648 def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]: 649 """ 650 Perform a POST web-request and return the data from the API as a DataFrame. 651 652 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 653 this can be very useful if you want to pass your own headers/cookies. 654 655 (if you have paid for a live data add-on with TradingView, you want to pass your own 656 headers and cookies to access that real-time data) 657 658 :param kwargs: kwargs to pass to `requests.post()` 659 :return: a tuple consisting of: (total_count, dataframe) 660 """ 661 kwargs.setdefault('headers', HEADERS) 662 kwargs.setdefault('timeout', 20) 663 r = requests.post(self.url, json=self.query, **kwargs) 664 665 if r.status_code >= 400: 666 # add the body to the error message for debugging purposes 667 r.reason += f'\n Body: {r.text}\n' 668 r.raise_for_status() 669 670 json_obj = r.json() 671 rows_count = json_obj['totalCount'] 672 data = json_obj['data'] 673 674 df = pd.DataFrame( 675 data=([row['s'], *row['d']] for row in data), 676 columns=['ticker', *self.query.get('columns', ())], 677 ) 678 return rows_count, df 679 680 def copy(self) -> Query: 681 new = Query() 682 new.query = self.query.copy() 683 return new 684 685 def __repr__(self) -> str: 686 return f'< {pprint.pformat(self.query)} >' 687 688 def __eq__(self, other) -> bool: 689 return isinstance(other, Query) and self.query == other.query
258class Query: 259 """ 260 This class allows you to perform SQL-like queries on the tradingview stock-screener. 261 262 The `Query` object reppresents a query that can be made to the official tradingview API, and it 263 stores all the data as JSON internally. 264 265 Examples: 266 267 To perform a simple query all you have to do is: 268 >>> from tradingview_screener import Query 269 >>> Query().get_scanner_data() 270 (18060, 271 ticker name close volume market_cap_basic 272 0 AMEX:SPY SPY 410.68 107367671 NaN 273 1 NASDAQ:QQQ QQQ 345.31 63475390 NaN 274 2 NASDAQ:TSLA TSLA 207.30 94879471 6.589904e+11 275 3 NASDAQ:NVDA NVDA 405.00 41677185 1.000350e+12 276 4 NASDAQ:AMZN AMZN 127.74 125309313 1.310658e+12 277 .. ... ... ... ... ... 278 45 NYSE:UNH UNH 524.66 2585616 4.859952e+11 279 46 NASDAQ:DXCM DXCM 89.29 14954605 3.449933e+10 280 47 NYSE:MA MA 364.08 3624883 3.429080e+11 281 48 NYSE:ABBV ABBV 138.93 9427212 2.452179e+11 282 49 AMEX:XLK XLK 161.12 8115780 NaN 283 [50 rows x 5 columns]) 284 285 The `get_scanner_data()` method will return a tuple with the first element being the number of 286 records that were found (like a `COUNT(*)`), and the second element contains the data that was 287 found as a DataFrame. 288 289 --- 290 291 By default, the `Query` will select the columns: `name`, `close`, `volume`, `market_cap_basic`, 292 but you override that 293 >>> (Query() 294 ... .select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI', 'Price to Earnings Ratio (TTM)') 295 ... .get_scanner_data()) 296 (18060, 297 ticker open high ... MACD.macd RSI price_earnings_ttm 298 0 AMEX:SPY 414.19 414.600 ... -5.397135 29.113396 NaN 299 1 NASDAQ:QQQ 346.43 348.840 ... -4.321482 34.335449 NaN 300 2 NASDAQ:TSLA 210.60 212.410 ... -12.224250 28.777229 66.752536 301 3 NASDAQ:NVDA 411.30 412.060 ... -8.738986 37.845668 97.835540 302 4 NASDAQ:AMZN 126.20 130.020 ... -2.025378 48.665666 66.697995 303 .. ... ... ... ... ... ... ... 304 45 NYSE:UNH 525.99 527.740 ... 6.448129 54.614775 22.770713 305 46 NASDAQ:DXCM 92.73 92.988 ... -2.376942 52.908093 98.914368 306 47 NYSE:MA 366.49 368.285 ... -7.496065 22.614078 31.711800 307 48 NYSE:ABBV 138.77 143.000 ... -1.708497 27.117232 37.960054 308 49 AMEX:XLK 161.17 162.750 ... -1.520828 36.868658 NaN 309 [50 rows x 8 columns]) 310 311 You can find the 250+ columns available in `tradingview_screener.constants.COLUMNS`. 312 313 Now let's do some queries using the `WHERE` statement, select all the stocks that the `close` is 314 bigger or equal than 350 315 >>> (Query() 316 ... .select('close', 'volume', '52 Week High') 317 ... .where(Column('close') >= 350) 318 ... .get_scanner_data()) 319 (159, 320 ticker close volume price_52_week_high 321 0 AMEX:SPY 410.68 107367671 459.44 322 1 NASDAQ:NVDA 405.00 41677185 502.66 323 2 NYSE:BRK.A 503375.05 7910 566569.97 324 3 AMEX:IVV 412.55 5604525 461.88 325 4 AMEX:VOO 377.32 5638752 422.15 326 .. ... ... ... ... 327 45 NASDAQ:EQIX 710.39 338549 821.63 328 46 NYSE:MCK 448.03 527406 465.90 329 47 NYSE:MTD 976.25 241733 1615.97 330 48 NASDAQ:CTAS 496.41 464631 525.37 331 49 NASDAQ:ROP 475.57 450141 508.90 332 [50 rows x 4 columns]) 333 334 You can even use other columns in these kind of operations 335 >>> (Query() 336 ... .select('close', 'VWAP') 337 ... .where(Column('close') >= Column('VWAP')) 338 ... .get_scanner_data()) 339 (9044, 340 ticker close VWAP 341 0 NASDAQ:AAPL 168.22 168.003333 342 1 NASDAQ:META 296.73 296.336667 343 2 NASDAQ:GOOGL 122.17 121.895233 344 3 NASDAQ:AMD 96.43 96.123333 345 4 NASDAQ:GOOG 123.40 123.100000 346 .. ... ... ... 347 45 NYSE:GD 238.25 238.043333 348 46 NYSE:GOLD 16.33 16.196667 349 47 AMEX:SLV 21.18 21.041667 350 48 AMEX:VXX 27.08 26.553333 351 49 NYSE:SLB 55.83 55.676667 352 [50 rows x 3 columns]) 353 354 Let's find all the stocks that the price is between the EMA 5 and 20, and the type is a stock 355 or fund 356 >>> (Query() 357 ... .select('close', 'volume', 'EMA5', 'EMA20', 'type') 358 ... .where( 359 ... Column('close').between(Column('EMA5'), Column('EMA20')), 360 ... Column('type').isin(['stock', 'fund']) 361 ... ) 362 ... .get_scanner_data()) 363 (1730, 364 ticker close volume EMA5 EMA20 type 365 0 NASDAQ:AMZN 127.74 125309313 125.033517 127.795142 stock 366 1 AMEX:HYG 72.36 35621800 72.340776 72.671058 fund 367 2 AMEX:LQD 99.61 21362859 99.554272 100.346388 fund 368 3 NASDAQ:IEF 90.08 11628236 89.856804 90.391503 fund 369 4 NYSE:SYK 261.91 3783608 261.775130 266.343290 stock 370 .. ... ... ... ... ... ... 371 45 NYSE:EMN 72.58 1562328 71.088034 72.835394 stock 372 46 NYSE:KIM 16.87 6609420 16.858920 17.096582 fund 373 47 NASDAQ:COLM 71.34 1516675 71.073116 71.658864 stock 374 48 NYSE:AOS 67.81 1586796 67.561619 67.903168 stock 375 49 NASDAQ:IGIB 47.81 2073538 47.761338 48.026795 fund 376 [50 rows x 6 columns]) 377 378 There are also the `ORDER BY`, `OFFSET`, and `LIMIT` statements. 379 Let's select all the tickers with a market cap between 1M and 50M, that have a relative volume 380 bigger than 1.2, and that the MACD is positive 381 >>> (Query() 382 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 383 ... .where( 384 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 385 ... Column('relative_volume_10d_calc') > 1.2, 386 ... Column('MACD.macd') >= Column('MACD.signal') 387 ... ) 388 ... .order_by('volume', ascending=False) 389 ... .offset(5) 390 ... .limit(15) 391 ... .get_scanner_data()) 392 (393, 393 ticker name close volume relative_volume_10d_calc 394 0 OTC:YCRM YCRM 0.0120 19626514 1.887942 395 1 OTC:PLPL PLPL 0.0002 17959914 3.026059 396 2 NASDAQ:ABVC ABVC 1.3800 16295824 1.967505 397 3 OTC:TLSS TLSS 0.0009 15671157 1.877976 398 4 OTC:GVSI GVSI 0.0128 14609774 2.640792 399 5 OTC:IGEX IGEX 0.0012 14285592 1.274861 400 6 OTC:EEGI EEGI 0.0004 12094000 2.224749 401 7 NASDAQ:GLG GLG 0.0591 9811974 1.990526 402 8 NASDAQ:TCRT TCRT 0.0890 8262894 2.630553 403 9 OTC:INKW INKW 0.0027 7196404 1.497134) 404 405 To avoid rewriting the same query again and again, you can save the query to a variable and 406 just call `get_scanner_data()` again and again to get the latest data: 407 >>> top_50_bullish = (Query() 408 ... .select('name', 'close', 'volume', 'relative_volume_10d_calc') 409 ... .where( 410 ... Column('market_cap_basic').between(1_000_000, 50_000_000), 411 ... Column('relative_volume_10d_calc') > 1.2, 412 ... Column('MACD.macd') >= Column('MACD.signal') 413 ... ) 414 ... .order_by('volume', ascending=False) 415 ... .limit(50)) 416 >>> top_50_bullish.get_scanner_data() 417 (393, 418 ticker name close volume relative_volume_10d_calc 419 0 OTC:BEGI BEGI 0.001050 127874055 3.349924 420 1 OTC:HCMC HCMC 0.000100 126992562 1.206231 421 2 OTC:HEMP HEMP 0.000150 101382713 1.775458 422 3 OTC:SONG SONG 0.000800 92640779 1.805721 423 4 OTC:APRU APRU 0.001575 38104499 29.028958 424 .. ... ... ... ... ... 425 45 OTC:BSHPF BSHPF 0.001000 525000 1.280899 426 46 OTC:GRHI GRHI 0.033000 507266 1.845738 427 47 OTC:OMGGF OMGGF 0.035300 505000 4.290059 428 48 NASDAQ:GBNH GBNH 0.273000 500412 9.076764 429 49 OTC:CLRMF CLRMF 0.032500 496049 17.560935 430 [50 rows x 5 columns]) 431 """ 432 433 def __init__(self) -> None: 434 # noinspection PyTypeChecker 435 self.query: QueryDict = { 436 'markets': ['america'], 437 'symbols': {'query': {'types': []}, 'tickers': []}, 438 'options': {'lang': 'en'}, 439 'columns': ['name', 'close', 'volume', 'market_cap_basic'], 440 # 'filter': ..., 441 'sort': {'sortBy': 'Value.Traded', 'sortOrder': 'desc'}, 442 'range': [0, 50], 443 } 444 self.url = 'https://scanner.tradingview.com/america/scan' 445 446 def set_markets(self, *markets: str) -> Query: 447 """ 448 This method allows you to select the market/s which you want to query. 449 450 By default, the screener will only scan US equities, but you can change it to scan any 451 or even multiple markets, that includes a list of 67 countries, and also the following 452 commodities: `bonds`, `cfd`, `coin`, `crypto`, `economics2`, `euronext`, `forex`, 453 `futures`, `options`. 454 455 You may choose any value from `tradingview_screener.constants.MARKETS`. 456 457 Examples: 458 459 By default, the screener will search the `america` market 460 >>> default_columns = ['close', 'market', 'country', 'currency'] 461 >>> Query().select(*default_columns).get_scanner_data() 462 (17898, 463 ticker close market country currency 464 0 AMEX:SPY 419.9900 america United States USD 465 1 NASDAQ:TSLA 201.7201 america United States USD 466 2 NASDAQ:NVDA 416.3800 america United States USD 467 3 NASDAQ:AMD 106.4499 america United States USD 468 4 NASDAQ:QQQ 353.4000 america United States USD 469 .. ... ... ... ... ... 470 45 NASDAQ:ADBE 538.0000 america United States USD 471 46 NYSE:BA 188.9000 america United States USD 472 47 NASDAQ:SBUX 90.9100 america United States USD 473 48 NYSE:HUM 500.6350 america United States USD 474 49 NYSE:CAT 227.3400 america United States USD 475 [50 rows x 5 columns]) 476 477 But you can change it (note the difference between `market` and `country`) 478 >>> (Query() 479 ... .select(*default_columns) 480 ... .set_markets('italy') 481 ... .get_scanner_data()) 482 (2346, 483 ticker close market country currency 484 0 MIL:UCG 23.9150 italy Italy EUR 485 1 MIL:ISP 2.4910 italy Italy EUR 486 2 MIL:STLAM 17.9420 italy Netherlands EUR 487 3 MIL:ENEL 6.0330 italy Italy EUR 488 4 MIL:ENI 15.4800 italy Italy EUR 489 .. ... ... ... ... ... 490 45 MIL:UNI 5.1440 italy Italy EUR 491 46 MIL:3OIS 0.4311 italy Ireland EUR 492 47 MIL:3SIL 35.2300 italy Ireland EUR 493 48 MIL:IWDE 69.1300 italy Ireland EUR 494 49 MIL:QQQS 19.2840 italy Ireland EUR 495 [50 rows x 5 columns]) 496 497 You can also select multiple markets 498 >>> (Query() 499 ... .select(*default_columns) 500 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 501 ... .get_scanner_data()) 502 (23964, 503 ticker close market country currency 504 0 AMEX:SPY 420.1617 america United States USD 505 1 NASDAQ:TSLA 201.2000 america United States USD 506 2 NASDAQ:NVDA 416.7825 america United States USD 507 3 NASDAQ:AMD 106.6600 america United States USD 508 4 NASDAQ:QQQ 353.7985 america United States USD 509 .. ... ... ... ... ... 510 45 NASDAQ:GOOGL 124.9200 america United States USD 511 46 HKEX:1211 233.2000 hongkong China HKD 512 47 TASE:ALHE 1995.0000 israel Israel ILA 513 48 AMEX:BIL 91.4398 america United States USD 514 49 NASDAQ:GOOG 126.1500 america United States USD 515 [50 rows x 5 columns]) 516 517 You may also select different financial instruments 518 >>> (Query() 519 ... .select('close', 'market') 520 ... .set_markets('cfd', 'crypto', 'futures', 'options') 521 ... .get_scanner_data()) 522 (118076, 523 ticker ... market 524 0 UNISWAP3ETH:WETHVGT ... crypto 525 1 UNISWAP3POLYGON:BONKWMATIC ... crypto 526 2 UNISWAP3ARBITRUM:WETHTROVE ... crypto 527 3 UNISWAP3ETH:USDTBRD ... crypto 528 4 UNISWAP3ETH:WBTCAUSD ... crypto 529 .. ... ... ... 530 45 NSE:IDEAF2024 ... futures 531 46 NSE:INDUSTOWERX2023 ... futures 532 47 NSE:INDUSTOWER1! ... futures 533 48 BIST:XU100 ... cfd 534 49 BYBIT:BTCUSD.P ... crypto 535 [50 rows x 3 columns]) 536 537 To select all the avaialble markets you can do this trick 538 >>> from tradingview_screener.constants import MARKETS 539 >>> len(MARKETS) 540 76 541 >>> (Query() 542 ... .select('close', 'market') 543 ... .set_markets(*MARKETS) 544 ... .get_scanner_data()) # notice how many records we find: over 240k 545 (241514, 546 ticker ... market 547 0 UNISWAP3ETH:WETHVGT ... crypto 548 1 UNISWAP3POLYGON:BONKWMATIC ... crypto 549 2 UNISWAP3ARBITRUM:WETHTROVE ... crypto 550 3 UNISWAP3ETH:USDTBRD ... crypto 551 4 UNISWAP3ETH:WBTCAUSD ... crypto 552 .. ... ... ... 553 45 NSE:IDEAF2024 ... futures 554 46 NSE:INDUSTOWER1! ... futures 555 47 NSE:INDUSTOWERX2023 ... futures 556 48 BIST:XU100 ... cfd 557 49 BYBIT:BTCUSD.P ... crypto 558 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 assert market in MARKETS 567 568 self.url = URL.format(market=market) 569 self.query['markets'] = [market] 570 571 elif len(markets) >= 1: 572 for m in markets: 573 assert m in MARKETS 574 575 self.url = URL.format(market='global') 576 self.query['markets'] = list(markets) 577 578 return self 579 580 def set_tickers(self, *tickers: str) -> Query: 581 """ 582 Set the tickers you wish to receive information on. 583 584 Examples: 585 586 >>> Query().limit(5).get_scanner_data() 587 (17879, 588 ticker name close volume market_cap_basic 589 0 NASDAQ:TSLA TSLA 248.50 118559595 7.887376e+11 590 1 AMEX:SPY SPY 445.52 62066984 NaN 591 2 NASDAQ:NVDA NVDA 455.72 47389801 1.125628e+12 592 3 NASDAQ:QQQ QQQ 372.58 35846281 NaN 593 4 NASDAQ:AAPL AAPL 178.18 65600673 2.785707e+12) 594 595 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 596 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 597 (2, 598 ticker name market close volume VWAP MACD.macd 599 0 NASDAQ:TSLA TSLA america 248.50 118559595 250.563333 0.730376 600 1 NASDAQ:NVDA NVDA america 455.72 47389801 458.163333 7.927189) 601 602 >>> q.set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX').get_scanner_data() 603 (4, 604 ticker name market close volume VWAP MACD.macd 605 0 HOSE:VIX VIX vietnam 19800.00 26292400 19883.333333 1291.359459 606 1 AMEX:SPY SPY america 445.52 62066984 445.720000 0.484263 607 2 NYSE:GME GME america 17.71 4693902 17.853333 -0.660342 608 3 MIL:RACE RACE italy 279.30 246547 279.033327 -1.398701) 609 610 :param tickers: One or more tickers, syntax: `exchange:symbol` 611 :return: Self 612 """ 613 # no need to select the market if we specify the symbol we want 614 # noinspection PyTypedDict 615 self.query.pop('markets', None) 616 617 self.query['symbols'] = {'tickers': list(tickers)} 618 self.url = 'https://scanner.tradingview.com/global/scan' 619 return self 620 621 def select(self, *columns: Column | str) -> Query: 622 self.query['columns'] = [ 623 col.name if isinstance(col, Column) else Column(col).name for col in columns 624 ] 625 return self 626 627 def where(self, *expressions: FilterOperationDict) -> Query: 628 self.query['filter'] = list(expressions) # convert tuple[dict] -> list[dict] 629 return self 630 631 def order_by(self, column: Column | str, ascending: bool = True) -> Query: 632 column = column.name if isinstance(column, Column) else Column(column).name 633 sort_order = 'asc' if ascending else 'desc' 634 # noinspection PyTypeChecker 635 self.query['sort'] = SortByDict(sortBy=column, sortOrder=sort_order) 636 return self 637 638 def offset(self, offset: int) -> Query: 639 self.query['range'][0] = offset 640 return self 641 642 def limit(self, limit: int) -> Query: 643 self.query['range'][1] = limit 644 return self 645 646 # def set_options(self, options) -> None: 647 # raise NotImplementedError 648 649 def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]: 650 """ 651 Perform a POST web-request and return the data from the API as a DataFrame. 652 653 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 654 this can be very useful if you want to pass your own headers/cookies. 655 656 (if you have paid for a live data add-on with TradingView, you want to pass your own 657 headers and cookies to access that real-time data) 658 659 :param kwargs: kwargs to pass to `requests.post()` 660 :return: a tuple consisting of: (total_count, dataframe) 661 """ 662 kwargs.setdefault('headers', HEADERS) 663 kwargs.setdefault('timeout', 20) 664 r = requests.post(self.url, json=self.query, **kwargs) 665 666 if r.status_code >= 400: 667 # add the body to the error message for debugging purposes 668 r.reason += f'\n Body: {r.text}\n' 669 r.raise_for_status() 670 671 json_obj = r.json() 672 rows_count = json_obj['totalCount'] 673 data = json_obj['data'] 674 675 df = pd.DataFrame( 676 data=([row['s'], *row['d']] for row in data), 677 columns=['ticker', *self.query.get('columns', ())], 678 ) 679 return rows_count, df 680 681 def copy(self) -> Query: 682 new = Query() 683 new.query = self.query.copy() 684 return new 685 686 def __repr__(self) -> str: 687 return f'< {pprint.pformat(self.query)} >' 688 689 def __eq__(self, other) -> bool: 690 return isinstance(other, Query) and self.query == other.query
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])
446 def set_markets(self, *markets: str) -> Query: 447 """ 448 This method allows you to select the market/s which you want to query. 449 450 By default, the screener will only scan US equities, but you can change it to scan any 451 or even multiple markets, that includes a list of 67 countries, and also the following 452 commodities: `bonds`, `cfd`, `coin`, `crypto`, `economics2`, `euronext`, `forex`, 453 `futures`, `options`. 454 455 You may choose any value from `tradingview_screener.constants.MARKETS`. 456 457 Examples: 458 459 By default, the screener will search the `america` market 460 >>> default_columns = ['close', 'market', 'country', 'currency'] 461 >>> Query().select(*default_columns).get_scanner_data() 462 (17898, 463 ticker close market country currency 464 0 AMEX:SPY 419.9900 america United States USD 465 1 NASDAQ:TSLA 201.7201 america United States USD 466 2 NASDAQ:NVDA 416.3800 america United States USD 467 3 NASDAQ:AMD 106.4499 america United States USD 468 4 NASDAQ:QQQ 353.4000 america United States USD 469 .. ... ... ... ... ... 470 45 NASDAQ:ADBE 538.0000 america United States USD 471 46 NYSE:BA 188.9000 america United States USD 472 47 NASDAQ:SBUX 90.9100 america United States USD 473 48 NYSE:HUM 500.6350 america United States USD 474 49 NYSE:CAT 227.3400 america United States USD 475 [50 rows x 5 columns]) 476 477 But you can change it (note the difference between `market` and `country`) 478 >>> (Query() 479 ... .select(*default_columns) 480 ... .set_markets('italy') 481 ... .get_scanner_data()) 482 (2346, 483 ticker close market country currency 484 0 MIL:UCG 23.9150 italy Italy EUR 485 1 MIL:ISP 2.4910 italy Italy EUR 486 2 MIL:STLAM 17.9420 italy Netherlands EUR 487 3 MIL:ENEL 6.0330 italy Italy EUR 488 4 MIL:ENI 15.4800 italy Italy EUR 489 .. ... ... ... ... ... 490 45 MIL:UNI 5.1440 italy Italy EUR 491 46 MIL:3OIS 0.4311 italy Ireland EUR 492 47 MIL:3SIL 35.2300 italy Ireland EUR 493 48 MIL:IWDE 69.1300 italy Ireland EUR 494 49 MIL:QQQS 19.2840 italy Ireland EUR 495 [50 rows x 5 columns]) 496 497 You can also select multiple markets 498 >>> (Query() 499 ... .select(*default_columns) 500 ... .set_markets('america', 'israel', 'hongkong', 'switzerland') 501 ... .get_scanner_data()) 502 (23964, 503 ticker close market country currency 504 0 AMEX:SPY 420.1617 america United States USD 505 1 NASDAQ:TSLA 201.2000 america United States USD 506 2 NASDAQ:NVDA 416.7825 america United States USD 507 3 NASDAQ:AMD 106.6600 america United States USD 508 4 NASDAQ:QQQ 353.7985 america United States USD 509 .. ... ... ... ... ... 510 45 NASDAQ:GOOGL 124.9200 america United States USD 511 46 HKEX:1211 233.2000 hongkong China HKD 512 47 TASE:ALHE 1995.0000 israel Israel ILA 513 48 AMEX:BIL 91.4398 america United States USD 514 49 NASDAQ:GOOG 126.1500 america United States USD 515 [50 rows x 5 columns]) 516 517 You may also select different financial instruments 518 >>> (Query() 519 ... .select('close', 'market') 520 ... .set_markets('cfd', 'crypto', 'futures', 'options') 521 ... .get_scanner_data()) 522 (118076, 523 ticker ... market 524 0 UNISWAP3ETH:WETHVGT ... crypto 525 1 UNISWAP3POLYGON:BONKWMATIC ... crypto 526 2 UNISWAP3ARBITRUM:WETHTROVE ... crypto 527 3 UNISWAP3ETH:USDTBRD ... crypto 528 4 UNISWAP3ETH:WBTCAUSD ... crypto 529 .. ... ... ... 530 45 NSE:IDEAF2024 ... futures 531 46 NSE:INDUSTOWERX2023 ... futures 532 47 NSE:INDUSTOWER1! ... futures 533 48 BIST:XU100 ... cfd 534 49 BYBIT:BTCUSD.P ... crypto 535 [50 rows x 3 columns]) 536 537 To select all the avaialble markets you can do this trick 538 >>> from tradingview_screener.constants import MARKETS 539 >>> len(MARKETS) 540 76 541 >>> (Query() 542 ... .select('close', 'market') 543 ... .set_markets(*MARKETS) 544 ... .get_scanner_data()) # notice how many records we find: over 240k 545 (241514, 546 ticker ... market 547 0 UNISWAP3ETH:WETHVGT ... crypto 548 1 UNISWAP3POLYGON:BONKWMATIC ... crypto 549 2 UNISWAP3ARBITRUM:WETHTROVE ... crypto 550 3 UNISWAP3ETH:USDTBRD ... crypto 551 4 UNISWAP3ETH:WBTCAUSD ... crypto 552 .. ... ... ... 553 45 NSE:IDEAF2024 ... futures 554 46 NSE:INDUSTOWER1! ... futures 555 47 NSE:INDUSTOWERX2023 ... futures 556 48 BIST:XU100 ... cfd 557 49 BYBIT:BTCUSD.P ... crypto 558 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 assert market in MARKETS 567 568 self.url = URL.format(market=market) 569 self.query['markets'] = [market] 570 571 elif len(markets) >= 1: 572 for m in markets: 573 assert m in MARKETS 574 575 self.url = URL.format(market='global') 576 self.query['markets'] = list(markets) 577 578 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
or even multiple markets, that includes a list of 67 countries, and also the following
commodities: bonds
, cfd
, coin
, crypto
, economics2
, euronext
, forex
,
futures
, options
.
You may choose any value from tradingview_screener.constants.MARKETS
.
Examples:
By default, the screener will search the america
market
>>> default_columns = ['close', 'market', 'country', 'currency']
>>> Query().select(*default_columns).get_scanner_data()
(17898,
ticker close market country currency
0 AMEX:SPY 419.9900 america United States USD
1 NASDAQ:TSLA 201.7201 america United States USD
2 NASDAQ:NVDA 416.3800 america United States USD
3 NASDAQ:AMD 106.4499 america United States USD
4 NASDAQ:QQQ 353.4000 america United States USD
.. ... ... ... ... ...
45 NASDAQ:ADBE 538.0000 america United States USD
46 NYSE:BA 188.9000 america United States USD
47 NASDAQ:SBUX 90.9100 america United States USD
48 NYSE:HUM 500.6350 america United States USD
49 NYSE:CAT 227.3400 america United States USD
[50 rows x 5 columns])
But you can change it (note the difference between market
and country
)
>>> (Query()
... .select(*default_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(*default_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', 'futures', 'options')
... .get_scanner_data())
(118076,
ticker ... market
0 UNISWAP3ETH:WETHVGT ... crypto
1 UNISWAP3POLYGON:BONKWMATIC ... crypto
2 UNISWAP3ARBITRUM:WETHTROVE ... crypto
3 UNISWAP3ETH:USDTBRD ... crypto
4 UNISWAP3ETH:WBTCAUSD ... crypto
.. ... ... ...
45 NSE:IDEAF2024 ... futures
46 NSE:INDUSTOWERX2023 ... futures
47 NSE:INDUSTOWER1! ... futures
48 BIST:XU100 ... cfd
49 BYBIT:BTCUSD.P ... crypto
[50 rows x 3 columns])
To select all the avaialble markets you can do this trick
>>> from tradingview_screener.constants import MARKETS
>>> len(MARKETS)
76
>>> (Query()
... .select('close', 'market')
... .set_markets(*MARKETS)
... .get_scanner_data()) # notice how many records we find: over 240k
(241514,
ticker ... market
0 UNISWAP3ETH:WETHVGT ... crypto
1 UNISWAP3POLYGON:BONKWMATIC ... crypto
2 UNISWAP3ARBITRUM:WETHTROVE ... crypto
3 UNISWAP3ETH:USDTBRD ... crypto
4 UNISWAP3ETH:WBTCAUSD ... crypto
.. ... ... ...
45 NSE:IDEAF2024 ... futures
46 NSE:INDUSTOWER1! ... futures
47 NSE:INDUSTOWERX2023 ... futures
48 BIST:XU100 ... cfd
49 BYBIT:BTCUSD.P ... crypto
[50 rows x 3 columns])
Parameters
- markets: one or more markets from
tradingview_screener.constants.MARKETS
Returns
Self
580 def set_tickers(self, *tickers: str) -> Query: 581 """ 582 Set the tickers you wish to receive information on. 583 584 Examples: 585 586 >>> Query().limit(5).get_scanner_data() 587 (17879, 588 ticker name close volume market_cap_basic 589 0 NASDAQ:TSLA TSLA 248.50 118559595 7.887376e+11 590 1 AMEX:SPY SPY 445.52 62066984 NaN 591 2 NASDAQ:NVDA NVDA 455.72 47389801 1.125628e+12 592 3 NASDAQ:QQQ QQQ 372.58 35846281 NaN 593 4 NASDAQ:AAPL AAPL 178.18 65600673 2.785707e+12) 594 595 >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd') 596 >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data() 597 (2, 598 ticker name market close volume VWAP MACD.macd 599 0 NASDAQ:TSLA TSLA america 248.50 118559595 250.563333 0.730376 600 1 NASDAQ:NVDA NVDA america 455.72 47389801 458.163333 7.927189) 601 602 >>> q.set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX').get_scanner_data() 603 (4, 604 ticker name market close volume VWAP MACD.macd 605 0 HOSE:VIX VIX vietnam 19800.00 26292400 19883.333333 1291.359459 606 1 AMEX:SPY SPY america 445.52 62066984 445.720000 0.484263 607 2 NYSE:GME GME america 17.71 4693902 17.853333 -0.660342 608 3 MIL:RACE RACE italy 279.30 246547 279.033327 -1.398701) 609 610 :param tickers: One or more tickers, syntax: `exchange:symbol` 611 :return: Self 612 """ 613 # no need to select the market if we specify the symbol we want 614 # noinspection PyTypedDict 615 self.query.pop('markets', None) 616 617 self.query['symbols'] = {'tickers': list(tickers)} 618 self.url = 'https://scanner.tradingview.com/global/scan' 619 return self
Set the tickers you wish to receive information on.
Examples:
>>> Query().limit(5).get_scanner_data()
(17879,
ticker name close volume market_cap_basic
0 NASDAQ:TSLA TSLA 248.50 118559595 7.887376e+11
1 AMEX:SPY SPY 445.52 62066984 NaN
2 NASDAQ:NVDA NVDA 455.72 47389801 1.125628e+12
3 NASDAQ:QQQ QQQ 372.58 35846281 NaN
4 NASDAQ:AAPL AAPL 178.18 65600673 2.785707e+12)
>>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd')
>>> q.set_tickers('NASDAQ:TSLA').get_scanner_data()
(2,
ticker name market close volume VWAP MACD.macd
0 NASDAQ:TSLA TSLA america 248.50 118559595 250.563333 0.730376
1 NASDAQ:NVDA NVDA america 455.72 47389801 458.163333 7.927189)
>>> q.set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX').get_scanner_data()
(4,
ticker name market close volume VWAP MACD.macd
0 HOSE:VIX VIX vietnam 19800.00 26292400 19883.333333 1291.359459
1 AMEX:SPY SPY america 445.52 62066984 445.720000 0.484263
2 NYSE:GME GME america 17.71 4693902 17.853333 -0.660342
3 MIL:RACE RACE italy 279.30 246547 279.033327 -1.398701)
Parameters
- **tickers: One or more tickers, syntax:
exchange**: symbol
Returns
Self
631 def order_by(self, column: Column | str, ascending: bool = True) -> Query: 632 column = column.name if isinstance(column, Column) else Column(column).name 633 sort_order = 'asc' if ascending else 'desc' 634 # noinspection PyTypeChecker 635 self.query['sort'] = SortByDict(sortBy=column, sortOrder=sort_order) 636 return self
649 def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]: 650 """ 651 Perform a POST web-request and return the data from the API as a DataFrame. 652 653 Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`, 654 this can be very useful if you want to pass your own headers/cookies. 655 656 (if you have paid for a live data add-on with TradingView, you want to pass your own 657 headers and cookies to access that real-time data) 658 659 :param kwargs: kwargs to pass to `requests.post()` 660 :return: a tuple consisting of: (total_count, dataframe) 661 """ 662 kwargs.setdefault('headers', HEADERS) 663 kwargs.setdefault('timeout', 20) 664 r = requests.post(self.url, json=self.query, **kwargs) 665 666 if r.status_code >= 400: 667 # add the body to the error message for debugging purposes 668 r.reason += f'\n Body: {r.text}\n' 669 r.raise_for_status() 670 671 json_obj = r.json() 672 rows_count = json_obj['totalCount'] 673 data = json_obj['data'] 674 675 df = pd.DataFrame( 676 data=([row['s'], *row['d']] for row in data), 677 columns=['ticker', *self.query.get('columns', ())], 678 ) 679 return rows_count, df
Perform a POST web-request and return the data from the API as a DataFrame.
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.
(if you have paid for a live data add-on with TradingView, you want to pass your own headers and cookies to access that real-time data)
Parameters
- kwargs: kwargs to pass to
requests.post()
Returns
a tuple consisting of: (total_count, dataframe)
60class Column: 61 """ 62 A Column object represents a field in the tradingview stock screener, 63 and it's used in SELECT queries and WHERE queries with the `Query` object. 64 65 A `Column` supports all the comparison operations: 66 `<`, `<=`, `>`, `>=`, `==`, `!=`, and also other methods like `between()`, `isin()`, etc. 67 68 Examples: 69 70 Some of the operations that you can do with `Column` objects: 71 >>> Column('close') >= 2.5 72 >>> Column('close').between(2.5, 15) 73 >>> Column('high') > Column('VWAP') 74 >>> Column('close').between(Column('EMA5'), Column('EMA20') 75 >>> Column('type').isin(['stock', 'fund']) 76 >>> Column('description').like('apple') # the same as `description LIKE '%apple%'` 77 """ 78 79 def __init__(self, name: str) -> None: 80 """ 81 Create a column object from a given column name 82 83 :param name: string, should be either a key or a value from the `COLUMNS` dictionary 84 """ 85 # if `name` is a dictionary key: get its value. otherwise make sure that it's a 86 # dictionary value. 87 self.name = COLUMNS.get(name, name) 88 89 # disable this method and do the column/field validation through the server 90 # @classmethod 91 # def from_unknown_name(cls, name: str) -> Column: 92 # """ 93 # Create a column object from a column name that isn't in the `COLUMNS` dictionary 94 # 95 # :param name: string, column name 96 # :return: Column 97 # """ 98 # # close is just a temporary column, so it won't raise an error at `__init__` 99 # column = cls(name='close') 100 # column.name = name 101 # return column 102 103 @staticmethod 104 def _extract_value(obj) -> ...: 105 if isinstance(obj, Column): 106 return obj.name 107 return obj 108 109 def __gt__(self, other) -> FilterOperationDict: 110 return FilterOperationDict( 111 left=self.name, operation='greater', right=self._extract_value(other) 112 ) 113 114 def __ge__(self, other) -> FilterOperationDict: 115 return FilterOperationDict( 116 left=self.name, operation='egreater', right=self._extract_value(other) 117 ) 118 119 def __lt__(self, other) -> FilterOperationDict: 120 return FilterOperationDict( 121 left=self.name, operation='less', right=self._extract_value(other) 122 ) 123 124 def __le__(self, other) -> FilterOperationDict: 125 return FilterOperationDict( 126 left=self.name, operation='eless', right=self._extract_value(other) 127 ) 128 129 def __eq__(self, other) -> FilterOperationDict: 130 return FilterOperationDict( 131 left=self.name, operation='equal', right=self._extract_value(other) 132 ) 133 134 def __ne__(self, other) -> FilterOperationDict: 135 return FilterOperationDict( 136 left=self.name, operation='nequal', right=self._extract_value(other) 137 ) 138 139 def crosses(self, other) -> FilterOperationDict: 140 return FilterOperationDict( 141 left=self.name, operation='crosses', right=self._extract_value(other) 142 ) 143 144 def crosses_above(self, other) -> FilterOperationDict: 145 return FilterOperationDict( 146 left=self.name, operation='crosses_above', right=self._extract_value(other) 147 ) 148 149 def crosses_below(self, other) -> FilterOperationDict: 150 return FilterOperationDict( 151 left=self.name, operation='crosses_below', right=self._extract_value(other) 152 ) 153 154 def between(self, left, right) -> FilterOperationDict: 155 return FilterOperationDict( 156 left=self.name, 157 operation='in_range', 158 right=[self._extract_value(left), self._extract_value(right)], 159 ) 160 161 def not_between(self, left, right) -> FilterOperationDict: 162 return FilterOperationDict( 163 left=self.name, 164 operation='not_in_range', 165 right=[self._extract_value(left), self._extract_value(right)], 166 ) 167 168 def isin(self, values) -> FilterOperationDict: 169 return FilterOperationDict(left=self.name, operation='in_range', right=list(values)) 170 171 def not_in(self, values: Iterable) -> FilterOperationDict: 172 return {'left': self.name, 'operation': 'not_in_range', 'right': list(values)} 173 174 def has(self, values: Iterable) -> FilterOperationDict: 175 """ 176 Field contains any of the values 177 178 (it's the same as `isin()`, except that it works on fields of type `set`) 179 """ 180 return {'left': self.name, 'operation': 'has', 'right': list(values)} 181 182 def has_none_of(self, values: Iterable) -> FilterOperationDict: 183 """ 184 Field doesn't contain any of the values 185 186 (it's the same as `not_in()`, except that it works on fields of type `set`) 187 """ 188 return {'left': self.name, 'operation': 'has_none_of', 'right': list(values)} 189 190 def above_pct(self, column: Column | str, pct: float) -> FilterOperationDict: 191 """ 192 Examples: 193 194 The closing price is higher than the VWAP by more than 3% 195 >>> Column('close').above_pct('VWAP', 1.03) 196 197 closing price is above the 52-week-low by more than 150% 198 >>> Column('close').above_pct('price_52_week_low', 2.5) 199 """ 200 return { 201 'left': self.name, 202 'operation': 'above%', 203 'right': [self._extract_value(column), pct], 204 } 205 206 def below_pct(self, column: Column | str, pct: float) -> FilterOperationDict: 207 """ 208 Examples: 209 210 The closing price is lower than the VWAP by 3% or more 211 >>> Column('close').below_pct('VWAP', 1.03) 212 """ 213 return { 214 'left': self.name, 215 'operation': 'below%', 216 'right': [self._extract_value(column), pct], 217 } 218 219 def between_pct( 220 self, column: Column | str, pct1: float, pct2: Optional[float] = None 221 ) -> FilterOperationDict: 222 """ 223 Examples: 224 225 The percentage change between the Close and the EMA is between 20% and 50% 226 >>> Column('close').between_pct('EMA200', 1.2, 1.5) 227 """ 228 return { 229 'left': self.name, 230 'operation': 'in_range%', 231 'right': [self._extract_value(column), pct1, pct2], 232 } 233 234 def not_between_pct( 235 self, column: Column | str, pct1: float, pct2: Optional[float] = None 236 ) -> FilterOperationDict: 237 """ 238 Examples: 239 240 The percentage change between the Close and the EMA is between 20% and 50% 241 >>> Column('close').not_between_pct('EMA200', 1.2, 1.5) 242 """ 243 return { 244 'left': self.name, 245 'operation': 'not_in_range%', 246 'right': [self._extract_value(column), pct1, pct2], 247 } 248 249 def like(self, other) -> FilterOperationDict: 250 return FilterOperationDict( 251 left=self.name, operation='match', right=self._extract_value(other) 252 ) 253 254 def __repr__(self) -> str: 255 return f'< Column({self.name!r}) >'
A Column object represents a field in the tradingview stock screener,
and it's used in SELECT queries and WHERE queries with the Query
object.
A Column
supports all the comparison operations:
<
, <=
, >
, >=
, ==
, !=
, and also other methods like between()
, isin()
, etc.
Examples:
Some of the operations that you can do with Column
objects:
>>> Column('close') >= 2.5
>>> Column('close').between(2.5, 15)
>>> Column('high') > Column('VWAP')
>>> Column('close').between(Column('EMA5'), Column('EMA20')
>>> Column('type').isin(['stock', 'fund'])
>>> Column('description').like('apple') # the same as `description LIKE '%apple%'`
79 def __init__(self, name: str) -> None: 80 """ 81 Create a column object from a given column name 82 83 :param name: string, should be either a key or a value from the `COLUMNS` dictionary 84 """ 85 # if `name` is a dictionary key: get its value. otherwise make sure that it's a 86 # dictionary value. 87 self.name = COLUMNS.get(name, name)
Create a column object from a given column name
Parameters
- name: string, should be either a key or a value from the
COLUMNS
dictionary
174 def has(self, values: Iterable) -> FilterOperationDict: 175 """ 176 Field contains any of the values 177 178 (it's the same as `isin()`, except that it works on fields of type `set`) 179 """ 180 return {'left': self.name, 'operation': 'has', 'right': list(values)}
Field contains any of the values
(it's the same as isin()
, except that it works on fields of type set
)
182 def has_none_of(self, values: Iterable) -> FilterOperationDict: 183 """ 184 Field doesn't contain any of the values 185 186 (it's the same as `not_in()`, except that it works on fields of type `set`) 187 """ 188 return {'left': self.name, 'operation': 'has_none_of', 'right': list(values)}
Field doesn't contain any of the values
(it's the same as not_in()
, except that it works on fields of type set
)
190 def above_pct(self, column: Column | str, pct: float) -> FilterOperationDict: 191 """ 192 Examples: 193 194 The closing price is higher than the VWAP by more than 3% 195 >>> Column('close').above_pct('VWAP', 1.03) 196 197 closing price is above the 52-week-low by more than 150% 198 >>> Column('close').above_pct('price_52_week_low', 2.5) 199 """ 200 return { 201 'left': self.name, 202 'operation': 'above%', 203 'right': [self._extract_value(column), pct], 204 }
Examples:
The closing price is higher than the VWAP by more than 3%
>>> Column('close').above_pct('VWAP', 1.03)
closing price is above the 52-week-low by more than 150%
>>> Column('close').above_pct('price_52_week_low', 2.5)
206 def below_pct(self, column: Column | str, pct: float) -> FilterOperationDict: 207 """ 208 Examples: 209 210 The closing price is lower than the VWAP by 3% or more 211 >>> Column('close').below_pct('VWAP', 1.03) 212 """ 213 return { 214 'left': self.name, 215 'operation': 'below%', 216 'right': [self._extract_value(column), pct], 217 }
Examples:
The closing price is lower than the VWAP by 3% or more
>>> Column('close').below_pct('VWAP', 1.03)
219 def between_pct( 220 self, column: Column | str, pct1: float, pct2: Optional[float] = None 221 ) -> FilterOperationDict: 222 """ 223 Examples: 224 225 The percentage change between the Close and the EMA is between 20% and 50% 226 >>> Column('close').between_pct('EMA200', 1.2, 1.5) 227 """ 228 return { 229 'left': self.name, 230 'operation': 'in_range%', 231 'right': [self._extract_value(column), pct1, pct2], 232 }
Examples:
The percentage change between the Close and the EMA is between 20% and 50%
>>> Column('close').between_pct('EMA200', 1.2, 1.5)
234 def not_between_pct( 235 self, column: Column | str, pct1: float, pct2: Optional[float] = None 236 ) -> FilterOperationDict: 237 """ 238 Examples: 239 240 The percentage change between the Close and the EMA is between 20% and 50% 241 >>> Column('close').not_between_pct('EMA200', 1.2, 1.5) 242 """ 243 return { 244 'left': self.name, 245 'operation': 'not_in_range%', 246 'right': [self._extract_value(column), pct1, pct2], 247 }
Examples:
The percentage change between the Close and the EMA is between 20% and 50%
>>> Column('close').not_between_pct('EMA200', 1.2, 1.5)