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
class 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])
query: tradingview_screener.query.QueryDict
url
def set_markets(self, *markets: str) -> Query:
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
Returns

Self

def set_tickers(self, *tickers: str) -> Query:
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

def select( self, *columns: Column | str) -> Query:
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
def where( self, *expressions: tradingview_screener.query.FilterOperationDict) -> Query:
627    def where(self, *expressions: FilterOperationDict) -> Query:
628        self.query['filter'] = list(expressions)  # convert tuple[dict] -> list[dict]
629        return self
def order_by( self, column: Column | str, ascending: bool = True) -> Query:
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
def offset(self, offset: int) -> Query:
638    def offset(self, offset: int) -> Query:
639        self.query['range'][0] = offset
640        return self
def limit(self, limit: int) -> Query:
642    def limit(self, limit: int) -> Query:
643        self.query['range'][1] = limit
644        return self
def get_scanner_data(self, **kwargs) -> tuple[int, pandas.core.frame.DataFrame]:
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)

def copy(self) -> Query:
681    def copy(self) -> Query:
682        new = Query()
683        new.query = self.query.copy()
684        return new
class Column:
 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%'`
Column(name: str)
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
name
def crosses(self, other) -> tradingview_screener.query.FilterOperationDict:
139    def crosses(self, other) -> FilterOperationDict:
140        return FilterOperationDict(
141            left=self.name, operation='crosses', right=self._extract_value(other)
142        )
def crosses_above(self, other) -> tradingview_screener.query.FilterOperationDict:
144    def crosses_above(self, other) -> FilterOperationDict:
145        return FilterOperationDict(
146            left=self.name, operation='crosses_above', right=self._extract_value(other)
147        )
def crosses_below(self, other) -> tradingview_screener.query.FilterOperationDict:
149    def crosses_below(self, other) -> FilterOperationDict:
150        return FilterOperationDict(
151            left=self.name, operation='crosses_below', right=self._extract_value(other)
152        )
def between(self, left, right) -> tradingview_screener.query.FilterOperationDict:
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        )
def not_between(self, left, right) -> tradingview_screener.query.FilterOperationDict:
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        )
def isin(self, values) -> tradingview_screener.query.FilterOperationDict:
168    def isin(self, values) -> FilterOperationDict:
169        return FilterOperationDict(left=self.name, operation='in_range', right=list(values))
def not_in(self, values: Iterable) -> tradingview_screener.query.FilterOperationDict:
171    def not_in(self, values: Iterable) -> FilterOperationDict:
172        return {'left': self.name, 'operation': 'not_in_range', 'right': list(values)}
def has(self, values: Iterable) -> tradingview_screener.query.FilterOperationDict:
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)

def has_none_of(self, values: Iterable) -> tradingview_screener.query.FilterOperationDict:
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)

def above_pct( self, column: Column | str, pct: float) -> tradingview_screener.query.FilterOperationDict:
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)
def below_pct( self, column: Column | str, pct: float) -> tradingview_screener.query.FilterOperationDict:
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)
def between_pct( self, column: Column | str, pct1: float, pct2: Optional[float] = None) -> tradingview_screener.query.FilterOperationDict:
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)
def not_between_pct( self, column: Column | str, pct1: float, pct2: Optional[float] = None) -> tradingview_screener.query.FilterOperationDict:
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)
def like(self, other) -> tradingview_screener.query.FilterOperationDict:
249    def like(self, other) -> FilterOperationDict:
250        return FilterOperationDict(
251            left=self.name, operation='match', right=self._extract_value(other)
252        )