tradingview_screener.query

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

This class allows you to perform SQL-like queries on the tradingview stock-screener.

The Query object reppresents a query that can be made to the official tradingview API, and it stores all the data as JSON internally.

Examples:

To perform a simple query all you have to do is:

>>> from tradingview_screener import Query
>>> Query().get_scanner_data()
(18060,
          ticker  name   close     volume  market_cap_basic
 0      AMEX:SPY   SPY  410.68  107367671               NaN
 1    NASDAQ:QQQ   QQQ  345.31   63475390               NaN
 2   NASDAQ:TSLA  TSLA  207.30   94879471      6.589904e+11
 3   NASDAQ:NVDA  NVDA  405.00   41677185      1.000350e+12
 4   NASDAQ:AMZN  AMZN  127.74  125309313      1.310658e+12
 ..          ...   ...     ...        ...               ...
 45     NYSE:UNH   UNH  524.66    2585616      4.859952e+11
 46  NASDAQ:DXCM  DXCM   89.29   14954605      3.449933e+10
 47      NYSE:MA    MA  364.08    3624883      3.429080e+11
 48    NYSE:ABBV  ABBV  138.93    9427212      2.452179e+11
 49     AMEX:XLK   XLK  161.12    8115780               NaN
 [50 rows x 5 columns])

The get_scanner_data() method will return a tuple with the first element being the number of records that were found (like a COUNT(*)), and the second element contains the data that was found as a DataFrame.


By default, the Query will select the columns: name, close, volume, market_cap_basic, but you override that

>>> (Query()
...  .select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI', 'Price to Earnings Ratio (TTM)')
...  .get_scanner_data())
(18060,
          ticker    open     high  ...  MACD.macd        RSI  price_earnings_ttm
 0      AMEX:SPY  414.19  414.600  ...  -5.397135  29.113396                 NaN
 1    NASDAQ:QQQ  346.43  348.840  ...  -4.321482  34.335449                 NaN
 2   NASDAQ:TSLA  210.60  212.410  ... -12.224250  28.777229           66.752536
 3   NASDAQ:NVDA  411.30  412.060  ...  -8.738986  37.845668           97.835540
 4   NASDAQ:AMZN  126.20  130.020  ...  -2.025378  48.665666           66.697995
 ..          ...     ...      ...  ...        ...        ...                 ...
 45     NYSE:UNH  525.99  527.740  ...   6.448129  54.614775           22.770713
 46  NASDAQ:DXCM   92.73   92.988  ...  -2.376942  52.908093           98.914368
 47      NYSE:MA  366.49  368.285  ...  -7.496065  22.614078           31.711800
 48    NYSE:ABBV  138.77  143.000  ...  -1.708497  27.117232           37.960054
 49     AMEX:XLK  161.17  162.750  ...  -1.520828  36.868658                 NaN
 [50 rows x 8 columns])

You can find the 250+ columns available in tradingview_screener.constants.COLUMNS.

Now let's do some queries using the WHERE statement, select all the stocks that the close is bigger or equal than 350

>>> (Query()
...  .select('close', 'volume', '52 Week High')
...  .where(Column('close') >= 350)
...  .get_scanner_data())
(159,
          ticker      close     volume  price_52_week_high
 0      AMEX:SPY     410.68  107367671              459.44
 1   NASDAQ:NVDA     405.00   41677185              502.66
 2    NYSE:BRK.A  503375.05       7910           566569.97
 3      AMEX:IVV     412.55    5604525              461.88
 4      AMEX:VOO     377.32    5638752              422.15
 ..          ...        ...        ...                 ...
 45  NASDAQ:EQIX     710.39     338549              821.63
 46     NYSE:MCK     448.03     527406              465.90
 47     NYSE:MTD     976.25     241733             1615.97
 48  NASDAQ:CTAS     496.41     464631              525.37
 49   NASDAQ:ROP     475.57     450141              508.90
 [50 rows x 4 columns])

You can even use other columns in these kind of operations

>>> (Query()
...  .select('close', 'VWAP')
...  .where(Column('close') >= Column('VWAP'))
...  .get_scanner_data())
(9044,
           ticker   close        VWAP
 0    NASDAQ:AAPL  168.22  168.003333
 1    NASDAQ:META  296.73  296.336667
 2   NASDAQ:GOOGL  122.17  121.895233
 3     NASDAQ:AMD   96.43   96.123333
 4    NASDAQ:GOOG  123.40  123.100000
 ..           ...     ...         ...
 45       NYSE:GD  238.25  238.043333
 46     NYSE:GOLD   16.33   16.196667
 47      AMEX:SLV   21.18   21.041667
 48      AMEX:VXX   27.08   26.553333
 49      NYSE:SLB   55.83   55.676667
 [50 rows x 3 columns])

Let's find all the stocks that the price is between the EMA 5 and 20, and the type is a stock or fund

>>> (Query()
...  .select('close', 'volume', 'EMA5', 'EMA20', 'type')
...  .where(
...     Column('close').between(Column('EMA5'), Column('EMA20')),
...     Column('type').isin(['stock', 'fund'])
...  )
...  .get_scanner_data())
(1730,
          ticker   close     volume        EMA5       EMA20   type
 0   NASDAQ:AMZN  127.74  125309313  125.033517  127.795142  stock
 1      AMEX:HYG   72.36   35621800   72.340776   72.671058   fund
 2      AMEX:LQD   99.61   21362859   99.554272  100.346388   fund
 3    NASDAQ:IEF   90.08   11628236   89.856804   90.391503   fund
 4      NYSE:SYK  261.91    3783608  261.775130  266.343290  stock
 ..          ...     ...        ...         ...         ...    ...
 45     NYSE:EMN   72.58    1562328   71.088034   72.835394  stock
 46     NYSE:KIM   16.87    6609420   16.858920   17.096582   fund
 47  NASDAQ:COLM   71.34    1516675   71.073116   71.658864  stock
 48     NYSE:AOS   67.81    1586796   67.561619   67.903168  stock
 49  NASDAQ:IGIB   47.81    2073538   47.761338   48.026795   fund
 [50 rows x 6 columns])

There are also the ORDER BY, OFFSET, and LIMIT statements. Let's select all the tickers with a market cap between 1M and 50M, that have a relative volume bigger than 1.2, and that the MACD is positive

>>> (Query()
...  .select('name', 'close', 'volume', 'relative_volume_10d_calc')
...  .where(
...      Column('market_cap_basic').between(1_000_000, 50_000_000),
...      Column('relative_volume_10d_calc') > 1.2,
...      Column('MACD.macd') >= Column('MACD.signal')
...  )
...  .order_by('volume', ascending=False)
...  .offset(5)
...  .limit(15)
...  .get_scanner_data())
(393,
         ticker  name   close    volume  relative_volume_10d_calc
 0     OTC:YCRM  YCRM  0.0120  19626514                  1.887942
 1     OTC:PLPL  PLPL  0.0002  17959914                  3.026059
 2  NASDAQ:ABVC  ABVC  1.3800  16295824                  1.967505
 3     OTC:TLSS  TLSS  0.0009  15671157                  1.877976
 4     OTC:GVSI  GVSI  0.0128  14609774                  2.640792
 5     OTC:IGEX  IGEX  0.0012  14285592                  1.274861
 6     OTC:EEGI  EEGI  0.0004  12094000                  2.224749
 7   NASDAQ:GLG   GLG  0.0591   9811974                  1.990526
 8  NASDAQ:TCRT  TCRT  0.0890   8262894                  2.630553
 9     OTC:INKW  INKW  0.0027   7196404                  1.497134)

To avoid rewriting the same query again and again, you can save the query to a variable and just call get_scanner_data() again and again to get the latest data:

>>> top_50_bullish = (Query()
...  .select('name', 'close', 'volume', 'relative_volume_10d_calc')
...  .where(
...      Column('market_cap_basic').between(1_000_000, 50_000_000),
...      Column('relative_volume_10d_calc') > 1.2,
...      Column('MACD.macd') >= Column('MACD.signal')
...  )
...  .order_by('volume', ascending=False)
...  .limit(50))
>>> top_50_bullish.get_scanner_data()
(393,
          ticker   name     close     volume  relative_volume_10d_calc
 0      OTC:BEGI   BEGI  0.001050  127874055                  3.349924
 1      OTC:HCMC   HCMC  0.000100  126992562                  1.206231
 2      OTC:HEMP   HEMP  0.000150  101382713                  1.775458
 3      OTC:SONG   SONG  0.000800   92640779                  1.805721
 4      OTC:APRU   APRU  0.001575   38104499                 29.028958
 ..          ...    ...       ...        ...                       ...
 45    OTC:BSHPF  BSHPF  0.001000     525000                  1.280899
 46     OTC:GRHI   GRHI  0.033000     507266                  1.845738
 47    OTC:OMGGF  OMGGF  0.035300     505000                  4.290059
 48  NASDAQ:GBNH   GBNH  0.273000     500412                  9.076764
 49    OTC:CLRMF  CLRMF  0.032500     496049                 17.560935
 [50 rows x 5 columns])
url
def select(self, *columns: tradingview_screener.column.Column | str) -> Self:
257    def select(self, *columns: Column | str) -> Self:
258        self.query['columns'] = [
259            col.name if isinstance(col, Column) else Column(col).name for col in columns
260        ]
261        return self
def where( self, *expressions: tradingview_screener.models.FilterOperationDict) -> Self:
263    def where(self, *expressions: FilterOperationDict) -> Self:
264        """
265        Filter screener (expressions are joined with the AND operator)
266        """
267        self.query['filter'] = list(expressions)  # convert tuple[dict] -> list[dict]
268        return self

Filter screener (expressions are joined with the AND operator)

def where2(self, operation: tradingview_screener.models.OperationDict) -> Self:
270    def where2(self, operation: OperationDict) -> Self:
271        """
272        Filter screener using AND/OR operators (nested expressions also allowed)
273
274        Rules:
275        1. The argument passed to `where2()` **must** be wrapped in `And()` or `Or()`.
276        2. `And()` and `Or()` can accept one or more conditions as arguments.
277        3. Conditions can be simple (e.g., `Column('field') == 'value'`) or complex, allowing nesting of `And()` and `Or()` to create intricate logical filters.
278        4. Unlike the `where()` method, which only supports chaining conditions with the `AND` operator, `where2()` allows mixing and nesting of `AND` and `OR` operators.
279
280        Examples:
281
282        1. **Combining conditions with `OR` and nested `AND`:**
283           ```python
284           Query()
285           .select('type', 'typespecs')
286           .where2(
287               Or(
288                   And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])),
289                   And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])),
290                   Column('type') == 'dr',
291               )
292           )
293           ```
294
295           This query filters entities where:
296           - The `type` is `'stock'` and `typespecs` contains `'common'` or `'preferred'`, **OR**
297           - The `type` is `'fund'` and `typespecs` does not contain `'etf'`, **OR**
298           - The `type` is `'dr'`.
299
300        2. **Mixing conditions with `OR`:**
301           ```python
302           Query().where2(
303               Or(
304                   And(col('type') == 'stock', col('typespecs').has(['common'])),
305                   col('type') == 'fund',
306               )
307           )
308           ```
309           This query filters entities where:
310           - The `type` is `'stock'` and `typespecs` contains `'common'`, **OR**
311           - The `type` is `'fund'`.
312
313        3. **Combining conditions with `AND`:**
314           ```python
315           Query()
316           .set_markets('crypto')
317           .where2(
318               And(
319                   col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']),
320                   col('currency_id') == 'USD',
321               )
322           )
323           ```
324           This query filters entities in the `'crypto'` market where:
325           - The `exchange` is one of `'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff'`, **AND**
326           - The `currency_id` is `'USD'`.
327        """
328        self.query['filter2'] = operation['operation']
329        return self

Filter screener using AND/OR operators (nested expressions also allowed)

Rules:

  1. The argument passed to where2() must be wrapped in And() or Or().
  2. And() and Or() can accept one or more conditions as arguments.
  3. Conditions can be simple (e.g., Column('field') == 'value') or complex, allowing nesting of And() and Or() to create intricate logical filters.
  4. Unlike the where() method, which only supports chaining conditions with the AND operator, where2() allows mixing and nesting of AND and OR operators.

Examples:

  1. Combining conditions with OR and nested AND:

    Query()
    .select('type', 'typespecs')
    .where2(
        Or(
            And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])),
            And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])),
            Column('type') == 'dr',
        )
    )
    

    This query filters entities where:

    • The type is 'stock' and typespecs contains 'common' or 'preferred', OR
    • The type is 'fund' and typespecs does not contain 'etf', OR
    • The type is 'dr'.
  2. Mixing conditions with OR:

    Query().where2(
        Or(
            And(col('type') == 'stock', col('typespecs').has(['common'])),
            col('type') == 'fund',
        )
    )
    

    This query filters entities where:

    • The type is 'stock' and typespecs contains 'common', OR
    • The type is 'fund'.
  3. Combining conditions with AND:

    Query()
    .set_markets('crypto')
    .where2(
        And(
            col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']),
            col('currency_id') == 'USD',
        )
    )
    

    This query filters entities in the 'crypto' market where:

    • The exchange is one of 'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff', AND
    • The currency_id is 'USD'.
def order_by( self, column: tradingview_screener.column.Column | str, ascending: bool = True, nulls_first: bool = False) -> Self:
331    def order_by(
332        self, column: Column | str, ascending: bool = True, nulls_first: bool = False
333    ) -> Self:
334        """
335        Applies sorting to the query results based on the specified column.
336
337        Examples:
338
339        >>> Query().order_by('volume', ascending=False)  # sort descending
340        >>> Query().order_by('close', ascending=True)
341        >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False)
342
343        :param column: Either a `Column` object or a string with the column name.
344        :param ascending: Set to True for ascending order (default), or False for descending.
345        :param nulls_first: If True, places `None` values at the beginning of the results. Defaults
346        to False.
347        :return: The updated query object.
348        """
349        dct: SortByDict = {
350            'sortBy': column.name if isinstance(column, Column) else column,
351            'sortOrder': 'asc' if ascending else 'desc',
352            'nullsFirst': nulls_first,
353        }
354        self.query['sort'] = dct
355        return self

Applies sorting to the query results based on the specified column.

Examples:

>>> Query().order_by('volume', ascending=False)  # sort descending
>>> Query().order_by('close', ascending=True)
>>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False)
Parameters
  • column: Either a Column object or a string with the column name.
  • ascending: Set to True for ascending order (default), or False for descending.
  • nulls_first: If True, places None values at the beginning of the results. Defaults to False.
Returns

The updated query object.

def limit(self, limit: int) -> Self:
357    def limit(self, limit: int) -> Self:
358        self.query.setdefault('range', DEFAULT_RANGE.copy())[1] = limit
359        return self
def offset(self, offset: int) -> Self:
361    def offset(self, offset: int) -> Self:
362        self.query.setdefault('range', DEFAULT_RANGE.copy())[0] = offset
363        return self
def set_markets(self, *markets: str) -> Self:
365    def set_markets(self, *markets: str) -> Self:
366        """
367        This method allows you to select the market/s which you want to query.
368
369        By default, the screener will only scan US equities, but you can change it to scan any
370        market or country, that includes a list of 67 countries, and also the following
371        asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`,
372        `futures`, `options`.
373
374        You may choose any value from `tradingview_screener.constants.MARKETS`.
375
376        If you select multiple countries, you might want to
377
378        Examples:
379
380        By default, the screener will show results from the `america` market, but you can
381        change it (note the difference between `market` and `country`)
382        >>> columns = ['close', 'market', 'country', 'currency']
383        >>> (Query()
384        ...  .select(*columns)
385        ...  .set_markets('italy')
386        ...  .get_scanner_data())
387        (2346,
388                ticker    close market      country currency
389         0     MIL:UCG  23.9150  italy        Italy      EUR
390         1     MIL:ISP   2.4910  italy        Italy      EUR
391         2   MIL:STLAM  17.9420  italy  Netherlands      EUR
392         3    MIL:ENEL   6.0330  italy        Italy      EUR
393         4     MIL:ENI  15.4800  italy        Italy      EUR
394         ..        ...      ...    ...          ...      ...
395         45    MIL:UNI   5.1440  italy        Italy      EUR
396         46   MIL:3OIS   0.4311  italy      Ireland      EUR
397         47   MIL:3SIL  35.2300  italy      Ireland      EUR
398         48   MIL:IWDE  69.1300  italy      Ireland      EUR
399         49   MIL:QQQS  19.2840  italy      Ireland      EUR
400         [50 rows x 5 columns])
401
402        You can also select multiple markets
403        >>> (Query()
404        ...  .select(*columns)
405        ...  .set_markets('america', 'israel', 'hongkong', 'switzerland')
406        ...  .get_scanner_data())
407        (23964,
408                   ticker      close    market        country currency
409         0       AMEX:SPY   420.1617   america  United States      USD
410         1    NASDAQ:TSLA   201.2000   america  United States      USD
411         2    NASDAQ:NVDA   416.7825   america  United States      USD
412         3     NASDAQ:AMD   106.6600   america  United States      USD
413         4     NASDAQ:QQQ   353.7985   america  United States      USD
414         ..           ...        ...       ...            ...      ...
415         45  NASDAQ:GOOGL   124.9200   america  United States      USD
416         46     HKEX:1211   233.2000  hongkong          China      HKD
417         47     TASE:ALHE  1995.0000    israel         Israel      ILA
418         48      AMEX:BIL    91.4398   america  United States      USD
419         49   NASDAQ:GOOG   126.1500   america  United States      USD
420         [50 rows x 5 columns])
421
422        You may also select different financial instruments
423        >>> (Query()
424        ...  .select('close', 'market')
425        ...  .set_markets('cfd', 'crypto', 'forex', 'futures')
426        ...  .get_scanner_data())
427        (118076,
428                                    ticker  ...  market
429         0          UNISWAP3ETH:JUSTICEUSDT  ...  crypto
430         1             UNISWAP3ETH:UAHGUSDT  ...  crypto
431         2            UNISWAP3ETH:KENDUWETH  ...  crypto
432         3         UNISWAP3ETH:MATICSTMATIC  ...  crypto
433         4             UNISWAP3ETH:WETHETHM  ...  crypto
434         ..                             ...  ...     ...
435         45  UNISWAP:MUSICAIWETH_1F5304.USD  ...  crypto
436         46                   CRYPTOCAP:FIL  ...     cfd
437         47                   CRYPTOCAP:SUI  ...     cfd
438         48                  CRYPTOCAP:ARBI  ...     cfd
439         49                    CRYPTOCAP:OP  ...     cfd
440         [50 rows x 3 columns])
441
442        :param markets: one or more markets from `tradingview_screener.constants.MARKETS`
443        :return: Self
444        """
445        if len(markets) == 1:
446            market = markets[0]
447            self.url = URL.format(market=market)
448            self.query['markets'] = [market]
449        else:  # len(markets) == 0 or len(markets) > 1
450            self.url = URL.format(market='global')
451            self.query['markets'] = list(markets)
452
453        return self

This method allows you to select the market/s which you want to query.

By default, the screener will only scan US equities, but you can change it to scan any market or country, that includes a list of 67 countries, and also the following asset classes: bonds, cfd, coin, crypto, euronext, forex, futures, options.

You may choose any value from tradingview_screener.constants.MARKETS.

If you select multiple countries, you might want to

Examples:

By default, the screener will show results from the america market, but you can change it (note the difference between market and country)

>>> columns = ['close', 'market', 'country', 'currency']
>>> (Query()
...  .select(*columns)
...  .set_markets('italy')
...  .get_scanner_data())
(2346,
        ticker    close market      country currency
 0     MIL:UCG  23.9150  italy        Italy      EUR
 1     MIL:ISP   2.4910  italy        Italy      EUR
 2   MIL:STLAM  17.9420  italy  Netherlands      EUR
 3    MIL:ENEL   6.0330  italy        Italy      EUR
 4     MIL:ENI  15.4800  italy        Italy      EUR
 ..        ...      ...    ...          ...      ...
 45    MIL:UNI   5.1440  italy        Italy      EUR
 46   MIL:3OIS   0.4311  italy      Ireland      EUR
 47   MIL:3SIL  35.2300  italy      Ireland      EUR
 48   MIL:IWDE  69.1300  italy      Ireland      EUR
 49   MIL:QQQS  19.2840  italy      Ireland      EUR
 [50 rows x 5 columns])

You can also select multiple markets

>>> (Query()
...  .select(*columns)
...  .set_markets('america', 'israel', 'hongkong', 'switzerland')
...  .get_scanner_data())
(23964,
           ticker      close    market        country currency
 0       AMEX:SPY   420.1617   america  United States      USD
 1    NASDAQ:TSLA   201.2000   america  United States      USD
 2    NASDAQ:NVDA   416.7825   america  United States      USD
 3     NASDAQ:AMD   106.6600   america  United States      USD
 4     NASDAQ:QQQ   353.7985   america  United States      USD
 ..           ...        ...       ...            ...      ...
 45  NASDAQ:GOOGL   124.9200   america  United States      USD
 46     HKEX:1211   233.2000  hongkong          China      HKD
 47     TASE:ALHE  1995.0000    israel         Israel      ILA
 48      AMEX:BIL    91.4398   america  United States      USD
 49   NASDAQ:GOOG   126.1500   america  United States      USD
 [50 rows x 5 columns])

You may also select different financial instruments

>>> (Query()
...  .select('close', 'market')
...  .set_markets('cfd', 'crypto', 'forex', 'futures')
...  .get_scanner_data())
(118076,
                            ticker  ...  market
 0          UNISWAP3ETH:JUSTICEUSDT  ...  crypto
 1             UNISWAP3ETH:UAHGUSDT  ...  crypto
 2            UNISWAP3ETH:KENDUWETH  ...  crypto
 3         UNISWAP3ETH:MATICSTMATIC  ...  crypto
 4             UNISWAP3ETH:WETHETHM  ...  crypto
 ..                             ...  ...     ...
 45  UNISWAP:MUSICAIWETH_1F5304.USD  ...  crypto
 46                   CRYPTOCAP:FIL  ...     cfd
 47                   CRYPTOCAP:SUI  ...     cfd
 48                  CRYPTOCAP:ARBI  ...     cfd
 49                    CRYPTOCAP:OP  ...     cfd
 [50 rows x 3 columns])
Parameters
  • markets: one or more markets from tradingview_screener.constants.MARKETS
Returns

Self

def set_tickers(self, *tickers: str) -> Self:
455    def set_tickers(self, *tickers: str) -> Self:
456        """
457        Set the tickers you wish to receive information on.
458
459        Note that this resets the markets and sets the URL market to `global`.
460
461        Examples:
462
463        >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd')
464        >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data()
465        (1,
466                 ticker  name   market  close   volume    VWAP  MACD.macd
467         0  NASDAQ:TSLA  TSLA  america    186  3519931  185.53   2.371601)
468
469        To set tickers from multiple markets we need to update the markets that include them:
470        >>> (Query()
471        ...  .set_markets('america', 'italy', 'vietnam')
472        ...  .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX')
473        ...  .get_scanner_data())
474        (4,
475              ticker  name     close    volume  market_cap_basic
476         0  HOSE:VIX   VIX  16700.00  33192500      4.568961e+08
477         1  AMEX:SPY   SPY    544.35   1883562               NaN
478         2  NYSE:GME   GME     23.80   3116758      1.014398e+10
479         3  MIL:RACE  RACE    393.30    122878      1.006221e+11)
480
481        :param tickers: One or more tickers, syntax: `exchange:symbol`
482        :return: Self
483        """
484        self.query.setdefault('symbols', {})['tickers'] = list(tickers)
485        self.set_markets()
486        return self

Set the tickers you wish to receive information on.

Note that this resets the markets and sets the URL market to global.

Examples:

>>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd')
>>> q.set_tickers('NASDAQ:TSLA').get_scanner_data()
(1,
         ticker  name   market  close   volume    VWAP  MACD.macd
 0  NASDAQ:TSLA  TSLA  america    186  3519931  185.53   2.371601)

To set tickers from multiple markets we need to update the markets that include them:

>>> (Query()
...  .set_markets('america', 'italy', 'vietnam')
...  .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX')
...  .get_scanner_data())
(4,
      ticker  name     close    volume  market_cap_basic
 0  HOSE:VIX   VIX  16700.00  33192500      4.568961e+08
 1  AMEX:SPY   SPY    544.35   1883562               NaN
 2  NYSE:GME   GME     23.80   3116758      1.014398e+10
 3  MIL:RACE  RACE    393.30    122878      1.006221e+11)
Parameters
  • **tickers: One or more tickers, syntax: exchange**: symbol
Returns

Self

def set_index(self, *indexes: str) -> Self:
488    def set_index(self, *indexes: str) -> Self:
489        """
490        Scan only the equities that are in in the given index (or indexes).
491
492        Note that this resets the markets and sets the URL market to `global`.
493
494        Examples:
495
496        >>> Query().set_index('SYML:SP;SPX').get_scanner_data()
497        (503,
498                   ticker   name    close    volume  market_cap_basic
499         0    NASDAQ:NVDA   NVDA  1208.88  41238122      2.973644e+12
500         1    NASDAQ:AAPL   AAPL   196.89  53103705      3.019127e+12
501         2    NASDAQ:TSLA   TSLA   177.48  56244929      5.660185e+11
502         3     NASDAQ:AMD    AMD   167.87  44795240      2.713306e+11
503         4    NASDAQ:MSFT   MSFT   423.85  13621485      3.150183e+12
504         5    NASDAQ:AMZN   AMZN   184.30  28021473      1.917941e+12
505         6    NASDAQ:META   META   492.96   9379199      1.250410e+12
506         7   NASDAQ:GOOGL  GOOGL   174.46  19660698      2.164346e+12
507         8    NASDAQ:SMCI   SMCI   769.11   3444852      4.503641e+10
508         9    NASDAQ:GOOG   GOOG   175.95  14716134      2.164346e+12
509         10   NASDAQ:AVGO   AVGO  1406.64   1785876      6.518669e+11)
510
511        You can set multiple indices as well, like the NIFTY 50 and UK 100 Index.
512        >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data()
513        (150,
514                     ticker        name         close     volume  market_cap_basic
515         0         NSE:INFY        INFY   1533.600000   24075302      7.623654e+10
516         1          LSE:AZN         AZN  12556.000000    2903032      2.489770e+11
517         2     NSE:HDFCBANK    HDFCBANK   1573.350000   18356108      1.432600e+11
518         3     NSE:RELIANCE    RELIANCE   2939.899900    9279348      2.381518e+11
519         4         LSE:LSEG        LSEG   9432.000000    2321053      6.395329e+10
520         5   NSE:BAJFINANCE  BAJFINANCE   7191.399900    2984052      5.329685e+10
521         6         LSE:BARC        BARC    217.250000   96238723      4.133010e+10
522         7         NSE:SBIN        SBIN    829.950010   25061284      8.869327e+10
523         8           NSE:LT          LT   3532.500000    5879660      5.816100e+10
524         9         LSE:SHEL        SHEL   2732.500000    7448315      2.210064e+11)
525
526        You can find the full list of indices in [`constants.INDICES`](constants.html#INDICES),
527        just note that the syntax is
528        `SYML:{source};{symbol}`.
529
530        :param indexes: One or more strings representing the financial indexes to filter by
531        :return: An instance of the `Query` class with the filter applied
532        """
533        self.query.setdefault('preset', 'index_components_market_pages')
534        self.query.setdefault('symbols', {})['symbolset'] = list(indexes)
535        # reset markets list and URL to `/global`
536        self.set_markets()
537        return self

Scan only the equities that are in in the given index (or indexes).

Note that this resets the markets and sets the URL market to global.

Examples:

>>> Query().set_index('SYML:SP;SPX').get_scanner_data()
(503,
           ticker   name    close    volume  market_cap_basic
 0    NASDAQ:NVDA   NVDA  1208.88  41238122      2.973644e+12
 1    NASDAQ:AAPL   AAPL   196.89  53103705      3.019127e+12
 2    NASDAQ:TSLA   TSLA   177.48  56244929      5.660185e+11
 3     NASDAQ:AMD    AMD   167.87  44795240      2.713306e+11
 4    NASDAQ:MSFT   MSFT   423.85  13621485      3.150183e+12
 5    NASDAQ:AMZN   AMZN   184.30  28021473      1.917941e+12
 6    NASDAQ:META   META   492.96   9379199      1.250410e+12
 7   NASDAQ:GOOGL  GOOGL   174.46  19660698      2.164346e+12
 8    NASDAQ:SMCI   SMCI   769.11   3444852      4.503641e+10
 9    NASDAQ:GOOG   GOOG   175.95  14716134      2.164346e+12
 10   NASDAQ:AVGO   AVGO  1406.64   1785876      6.518669e+11)

You can set multiple indices as well, like the NIFTY 50 and UK 100 Index.

>>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data()
(150,
             ticker        name         close     volume  market_cap_basic
 0         NSE:INFY        INFY   1533.600000   24075302      7.623654e+10
 1          LSE:AZN         AZN  12556.000000    2903032      2.489770e+11
 2     NSE:HDFCBANK    HDFCBANK   1573.350000   18356108      1.432600e+11
 3     NSE:RELIANCE    RELIANCE   2939.899900    9279348      2.381518e+11
 4         LSE:LSEG        LSEG   9432.000000    2321053      6.395329e+10
 5   NSE:BAJFINANCE  BAJFINANCE   7191.399900    2984052      5.329685e+10
 6         LSE:BARC        BARC    217.250000   96238723      4.133010e+10
 7         NSE:SBIN        SBIN    829.950010   25061284      8.869327e+10
 8           NSE:LT          LT   3532.500000    5879660      5.816100e+10
 9         LSE:SHEL        SHEL   2732.500000    7448315      2.210064e+11)

You can find the full list of indices in constants.INDICES, just note that the syntax is SYML:{source};{symbol}.

Parameters
  • indexes: One or more strings representing the financial indexes to filter by
Returns

An instance of the Query class with the filter applied

def set_property(self, key: str, value: Any) -> Self:
568    def set_property(self, key: str, value: Any) -> Self:
569        self.query[key] = value
570        return self
def get_scanner_data_raw(self, **kwargs) -> tradingview_screener.models.ScreenerDict:
572    def get_scanner_data_raw(self, **kwargs) -> ScreenerDict:
573        """
574        Perform a POST web-request and return the data from the API (dictionary).
575
576        Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`,
577        this can be very useful if you want to pass your own headers/cookies.
578
579        >>> Query().select('close', 'volume').limit(5).get_scanner_data_raw()
580        {
581            'totalCount': 17559,
582            'data': [
583                {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]},
584                {'s': 'AMEX:SPY', 'd': [542.04, 52331224]},
585                {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]},
586                {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]},
587                {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]},
588            ],
589        }
590        """
591        self.query.setdefault('range', DEFAULT_RANGE.copy())
592
593        kwargs.setdefault('headers', HEADERS)
594        kwargs.setdefault('timeout', 20)
595        r = requests.post(self.url, json=self.query, **kwargs)
596
597        if not r.ok:
598            # add the body to the error message for debugging purposes
599            r.reason += f'\n Body: {r.text}\n'
600            r.raise_for_status()
601
602        return r.json()

Perform a POST web-request and return the data from the API (dictionary).

Note that you can pass extra keyword-arguments that will be forwarded to requests.post(), this can be very useful if you want to pass your own headers/cookies.

>>> Query().select('close', 'volume').limit(5).get_scanner_data_raw()
{
    'totalCount': 17559,
    'data': [
        {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]},
        {'s': 'AMEX:SPY', 'd': [542.04, 52331224]},
        {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]},
        {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]},
        {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]},
    ],
}
def get_scanner_data(self, **kwargs) -> tuple[int, pandas.core.frame.DataFrame]:
604    def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]:
605        """
606        Perform a POST web-request and return the data from the API as a DataFrame (along with
607        the number of rows/tickers that matched your query).
608
609        Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`,
610        this can be very useful if you want to pass your own headers/cookies.
611
612        ### Live/Delayed data
613
614        Note that to get live-data you have to authenticate, which is done by passing your cookies.
615        Have a look in the README at the "Real-Time Data Access" sections.
616
617        :param kwargs: kwargs to pass to `requests.post()`
618        :return: a tuple consisting of: (total_count, dataframe)
619        """
620        import pandas as pd
621
622        json_obj = self.get_scanner_data_raw(**kwargs)
623        rows_count = json_obj['totalCount']
624        data = json_obj['data']
625
626        df = pd.DataFrame(
627            data=([row['s'], *row['d']] for row in data),
628            columns=['ticker', *self.query.get('columns', ())],  # pyright: ignore [reportArgumentType]
629        )
630        return rows_count, df

Perform a POST web-request and return the data from the API as a DataFrame (along with the number of rows/tickers that matched your query).

Note that you can pass extra keyword-arguments that will be forwarded to requests.post(), this can be very useful if you want to pass your own headers/cookies.

Live/Delayed data

Note that to get live-data you have to authenticate, which is done by passing your cookies. Have a look in the README at the "Real-Time Data Access" sections.

Parameters
  • kwargs: kwargs to pass to requests.post()
Returns

a tuple consisting of: (total_count, dataframe)

def copy(self) -> Query:
632    def copy(self) -> Query:
633        new = Query()
634        new.query = self.query.copy()
635        return new