tradingview_screener.query

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

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

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

Examples:

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

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

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


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

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

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

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

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

You can even use other columns in these kind of operations

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

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

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

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

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

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

>>> top_50_bullish = (Query()
...  .select('name', 'close', 'volume', 'relative_volume_10d_calc')
...  .where(
...      Column('market_cap_basic').between(1_000_000, 50_000_000),
...      Column('relative_volume_10d_calc') > 1.2,
...      Column('MACD.macd') >= Column('MACD.signal')
...  )
...  .order_by('volume', ascending=False)
...  .limit(50))
>>> top_50_bullish.get_scanner_data()
(393,
          ticker   name     close     volume  relative_volume_10d_calc
 0      OTC:BEGI   BEGI  0.001050  127874055                  3.349924
 1      OTC:HCMC   HCMC  0.000100  126992562                  1.206231
 2      OTC:HEMP   HEMP  0.000150  101382713                  1.775458
 3      OTC:SONG   SONG  0.000800   92640779                  1.805721
 4      OTC:APRU   APRU  0.001575   38104499                 29.028958
 ..          ...    ...       ...        ...                       ...
 45    OTC:BSHPF  BSHPF  0.001000     525000                  1.280899
 46     OTC:GRHI   GRHI  0.033000     507266                  1.845738
 47    OTC:OMGGF  OMGGF  0.035300     505000                  4.290059
 48  NASDAQ:GBNH   GBNH  0.273000     500412                  9.076764
 49    OTC:CLRMF  CLRMF  0.032500     496049                 17.560935
 [50 rows x 5 columns])
Query(market: str = 'america')
370    def __init__(self, market: str = 'america') -> None:
371        # noinspection PyTypeChecker
372        self.query: QueryDict = copy.deepcopy(STOCKS_QUERY)
373        self.query['markets'] = [market]
374        self.url = URL.format(market=market)
url
def select(self, *columns: tradingview_screener.column.Column | str) -> Self:
376    def select(self, *columns: Column | str) -> Self:
377        self.query['columns'] = [
378            col.name if isinstance(col, Column) else Column(col).name for col in columns
379        ]
380        return self
def where( self, *expressions: tradingview_screener.models.FilterOperationDict) -> Self:
382    def where(self, *expressions: FilterOperationDict) -> Self:
383        """
384        Filter screener (expressions are joined with the AND operator)
385        """
386        self.query['filter'] = list(expressions)  # convert tuple[dict] -> list[dict]
387        return self

Filter screener (expressions are joined with the AND operator)

def where2(self, operation: tradingview_screener.models.OperationDict) -> Self:
389    def where2(self, operation: OperationDict) -> Self:
390        """
391        Filter screener using AND/OR operators (nested expressions also allowed)
392
393        Rules:
394        1. The argument passed to `where2()` **must** be wrapped in `And()` or `Or()`.
395        2. `And()` and `Or()` can accept one or more conditions as arguments.
396        3. Conditions can be simple (e.g., `Column('field') == 'value'`) or complex, allowing nesting of `And()` and `Or()` to create intricate logical filters.
397        4. Unlike the `where()` method, which only supports chaining conditions with the `AND` operator, `where2()` allows mixing and nesting of `AND` and `OR` operators.
398
399        Examples:
400
401        1. **Combining conditions with `OR` and nested `AND`:**
402           ```python
403           Query()
404           .select('type', 'typespecs')
405           .where2(
406               Or(
407                   And(Column('type') == 'stock', Column('typespecs').has(['common', 'preferred'])),
408                   And(Column('type') == 'fund', Column('typespecs').has_none_of(['etf'])),
409                   Column('type') == 'dr',
410               )
411           )
412           ```
413
414           This query filters entities where:
415           - The `type` is `'stock'` and `typespecs` contains `'common'` or `'preferred'`, **OR**
416           - The `type` is `'fund'` and `typespecs` does not contain `'etf'`, **OR**
417           - The `type` is `'dr'`.
418
419        2. **Mixing conditions with `OR`:**
420           ```python
421           Query().where2(
422               Or(
423                   And(col('type') == 'stock', col('typespecs').has(['common'])),
424                   col('type') == 'fund',
425               )
426           )
427           ```
428           This query filters entities where:
429           - The `type` is `'stock'` and `typespecs` contains `'common'`, **OR**
430           - The `type` is `'fund'`.
431
432        3. **Combining conditions with `AND`:**
433           ```python
434           Query()
435           .set_markets('crypto')
436           .where2(
437               And(
438                   col('exchange').isin(['UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff']),
439                   col('currency_id') == 'USD',
440               )
441           )
442           ```
443           This query filters entities in the `'crypto'` market where:
444           - The `exchange` is one of `'UNISWAP3POLYGON', 'VERSEETH', 'a', 'fffffffff'`, **AND**
445           - The `currency_id` is `'USD'`.
446        """
447        self.query['filter2'] = operation['operation']
448        return self

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

Rules:

  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:
450    def order_by(
451        self, column: Column | str, ascending: bool = True, nulls_first: bool = False
452    ) -> Self:
453        """
454        Applies sorting to the query results based on the specified column.
455
456        Examples:
457
458        >>> Query().order_by('volume', ascending=False)  # sort descending
459        >>> Query().order_by('close', ascending=True)
460        >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False)
461
462        :param column: Either a `Column` object or a string with the column name.
463        :param ascending: Set to True for ascending order (default), or False for descending.
464        :param nulls_first: If True, places `None` values at the beginning of the results. Defaults
465        to False.
466        :return: The updated query object.
467        """
468        dct: SortByDict = {
469            'sortBy': column.name if isinstance(column, Column) else column,
470            'sortOrder': 'asc' if ascending else 'desc',
471            'nullsFirst': nulls_first,
472        }
473        self.query['sort'] = dct
474        return self

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

Examples:

>>> Query().order_by('volume', ascending=False)  # sort descending
>>> Query().order_by('close', ascending=True)
>>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False)
Parameters
  • column: Either a 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:
476    def limit(self, limit: int) -> Self:
477        self.query.setdefault('range', DEFAULT_RANGE.copy())[1] = limit
478        return self
def offset(self, offset: int) -> Self:
480    def offset(self, offset: int) -> Self:
481        self.query.setdefault('range', DEFAULT_RANGE.copy())[0] = offset
482        return self
def set_markets(self, *markets: str) -> Self:
484    def set_markets(self, *markets: str) -> Self:
485        """
486        This method allows you to select the market/s which you want to query.
487
488        By default, the screener will only scan US equities, but you can change it to scan any
489        market or country, that includes a list of 67 countries, and also the following
490        asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`,
491        `futures`, `options`.
492
493        You may choose any value from `tradingview_screener.constants.MARKETS`.
494
495        If you select multiple countries, you might want to
496
497        Examples:
498
499        By default, the screener will show results from the `america` market, but you can
500        change it (note the difference between `market` and `country`)
501        >>> columns = ['close', 'market', 'country', 'currency']
502        >>> (Query()
503        ...  .select(*columns)
504        ...  .set_markets('italy')
505        ...  .get_scanner_data())
506        (2346,
507                ticker    close market      country currency
508         0     MIL:UCG  23.9150  italy        Italy      EUR
509         1     MIL:ISP   2.4910  italy        Italy      EUR
510         2   MIL:STLAM  17.9420  italy  Netherlands      EUR
511         3    MIL:ENEL   6.0330  italy        Italy      EUR
512         4     MIL:ENI  15.4800  italy        Italy      EUR
513         ..        ...      ...    ...          ...      ...
514         45    MIL:UNI   5.1440  italy        Italy      EUR
515         46   MIL:3OIS   0.4311  italy      Ireland      EUR
516         47   MIL:3SIL  35.2300  italy      Ireland      EUR
517         48   MIL:IWDE  69.1300  italy      Ireland      EUR
518         49   MIL:QQQS  19.2840  italy      Ireland      EUR
519         [50 rows x 5 columns])
520
521        You can also select multiple markets
522        >>> (Query()
523        ...  .select(*columns)
524        ...  .set_markets('america', 'israel', 'hongkong', 'switzerland')
525        ...  .get_scanner_data())
526        (23964,
527                   ticker      close    market        country currency
528         0       AMEX:SPY   420.1617   america  United States      USD
529         1    NASDAQ:TSLA   201.2000   america  United States      USD
530         2    NASDAQ:NVDA   416.7825   america  United States      USD
531         3     NASDAQ:AMD   106.6600   america  United States      USD
532         4     NASDAQ:QQQ   353.7985   america  United States      USD
533         ..           ...        ...       ...            ...      ...
534         45  NASDAQ:GOOGL   124.9200   america  United States      USD
535         46     HKEX:1211   233.2000  hongkong          China      HKD
536         47     TASE:ALHE  1995.0000    israel         Israel      ILA
537         48      AMEX:BIL    91.4398   america  United States      USD
538         49   NASDAQ:GOOG   126.1500   america  United States      USD
539         [50 rows x 5 columns])
540
541        You may also select different financial instruments
542        >>> (Query()
543        ...  .select('close', 'market')
544        ...  .set_markets('cfd', 'crypto', 'forex', 'futures')
545        ...  .get_scanner_data())
546        (118076,
547                                    ticker  ...  market
548         0          UNISWAP3ETH:JUSTICEUSDT  ...  crypto
549         1             UNISWAP3ETH:UAHGUSDT  ...  crypto
550         2            UNISWAP3ETH:KENDUWETH  ...  crypto
551         3         UNISWAP3ETH:MATICSTMATIC  ...  crypto
552         4             UNISWAP3ETH:WETHETHM  ...  crypto
553         ..                             ...  ...     ...
554         45  UNISWAP:MUSICAIWETH_1F5304.USD  ...  crypto
555         46                   CRYPTOCAP:FIL  ...     cfd
556         47                   CRYPTOCAP:SUI  ...     cfd
557         48                  CRYPTOCAP:ARBI  ...     cfd
558         49                    CRYPTOCAP:OP  ...     cfd
559         [50 rows x 3 columns])
560
561        :param markets: one or more markets from `tradingview_screener.constants.MARKETS`
562        :return: Self
563        """
564        if len(markets) == 1:
565            market = markets[0]
566            self.url = URL.format(market=market)
567            self.query['markets'] = [market]
568        else:  # len(markets) == 0 or len(markets) > 1
569            self.url = URL.format(market='global')
570            self.query['markets'] = list(markets)
571
572        return self

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

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

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

If you select multiple countries, you might want to

Examples:

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

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

You can also select multiple markets

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

You may also select different financial instruments

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

Self

def set_tickers(self, *tickers: str) -> Self:
574    def set_tickers(self, *tickers: str) -> Self:
575        """
576        Set the tickers you wish to receive information on.
577
578        Note that this resets the markets and sets the URL market to `global`.
579
580        Examples:
581
582        >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd')
583        >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data()
584        (1,
585                 ticker  name   market  close   volume    VWAP  MACD.macd
586         0  NASDAQ:TSLA  TSLA  america    186  3519931  185.53   2.371601)
587
588        To set tickers from multiple markets we need to update the markets that include them:
589        >>> (Query()
590        ...  .set_markets('america', 'italy', 'vietnam')
591        ...  .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX')
592        ...  .get_scanner_data())
593        (4,
594              ticker  name     close    volume  market_cap_basic
595         0  HOSE:VIX   VIX  16700.00  33192500      4.568961e+08
596         1  AMEX:SPY   SPY    544.35   1883562               NaN
597         2  NYSE:GME   GME     23.80   3116758      1.014398e+10
598         3  MIL:RACE  RACE    393.30    122878      1.006221e+11)
599
600        :param tickers: One or more tickers, syntax: `exchange:symbol`
601        :return: Self
602        """
603        self.query.setdefault('symbols', {})['tickers'] = list(tickers)
604        self.set_markets()
605        return self

Set the tickers you wish to receive information on.

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

Examples:

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

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

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

Self

def set_index(self, *indexes: str) -> Self:
607    def set_index(self, *indexes: str) -> Self:
608        """
609        Scan only the equities that are in the given index (or indexes).
610
611        Note that this resets the markets and sets the URL market to `global`.
612
613        Examples:
614
615        >>> Query().set_index('SYML:SP;SPX').get_scanner_data()
616        (503,
617                   ticker   name    close    volume  market_cap_basic
618         0    NASDAQ:NVDA   NVDA  1208.88  41238122      2.973644e+12
619         1    NASDAQ:AAPL   AAPL   196.89  53103705      3.019127e+12
620         2    NASDAQ:TSLA   TSLA   177.48  56244929      5.660185e+11
621         3     NASDAQ:AMD    AMD   167.87  44795240      2.713306e+11
622         4    NASDAQ:MSFT   MSFT   423.85  13621485      3.150183e+12
623         5    NASDAQ:AMZN   AMZN   184.30  28021473      1.917941e+12
624         6    NASDAQ:META   META   492.96   9379199      1.250410e+12
625         7   NASDAQ:GOOGL  GOOGL   174.46  19660698      2.164346e+12
626         8    NASDAQ:SMCI   SMCI   769.11   3444852      4.503641e+10
627         9    NASDAQ:GOOG   GOOG   175.95  14716134      2.164346e+12
628         10   NASDAQ:AVGO   AVGO  1406.64   1785876      6.518669e+11)
629
630        You can set multiple indices as well, like the NIFTY 50 and UK 100 Index.
631        >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data()
632        (150,
633                     ticker        name         close     volume  market_cap_basic
634         0         NSE:INFY        INFY   1533.600000   24075302      7.623654e+10
635         1          LSE:AZN         AZN  12556.000000    2903032      2.489770e+11
636         2     NSE:HDFCBANK    HDFCBANK   1573.350000   18356108      1.432600e+11
637         3     NSE:RELIANCE    RELIANCE   2939.899900    9279348      2.381518e+11
638         4         LSE:LSEG        LSEG   9432.000000    2321053      6.395329e+10
639         5   NSE:BAJFINANCE  BAJFINANCE   7191.399900    2984052      5.329685e+10
640         6         LSE:BARC        BARC    217.250000   96238723      4.133010e+10
641         7         NSE:SBIN        SBIN    829.950010   25061284      8.869327e+10
642         8           NSE:LT          LT   3532.500000    5879660      5.816100e+10
643         9         LSE:SHEL        SHEL   2732.500000    7448315      2.210064e+11)
644
645        You can find the full list of indices in [`constants.INDICES`](constants.html#INDICES),
646        just note that the syntax is
647        `SYML:{source};{symbol}`.
648
649        :param indexes: One or more strings representing the financial indexes to filter by
650        :return: An instance of the `Query` class with the filter applied
651        """
652        self.query.setdefault('preset', 'index_components_market_pages')
653        self.query.setdefault('symbols', {})['symbolset'] = list(indexes)
654        # reset markets list and URL to `/global`
655        self.set_markets()
656        return self

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

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

Examples:

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

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

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

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

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

An instance of the Query class with the filter applied

def set_property(self, key: str, value: Any) -> Self:
687    def set_property(self, key: str, value: Any) -> Self:
688        self.query[key] = value
689        return self
def get_scanner_data_raw( self, **kwargs) -> tradingview_screener.models.ScreenerDict | tradingview_screener.models.ScreenerDictV2:
691    def get_scanner_data_raw(self, **kwargs) -> ScreenerDict | ScreenerDictV2:
692        """
693        Perform a POST web-request and return the data from the API (dictionary).
694
695        Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`,
696        this can be very useful if you want to pass your own headers/cookies.
697
698        >>> Query().select('close', 'volume').limit(5).get_scanner_data_raw()
699        {
700            'totalCount': 17559,
701            'data': [
702                {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]},
703                {'s': 'AMEX:SPY', 'd': [542.04, 52331224]},
704                {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]},
705                {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]},
706                {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]},
707            ],
708        }
709        """
710        self.query.setdefault('range', DEFAULT_RANGE.copy())
711
712        kwargs.setdefault('headers', HEADERS)
713        kwargs.setdefault('timeout', 20)
714        r = requests.post(self.url, json=self.query, **kwargs)
715
716        if not r.ok:
717            # add the body to the error message for debugging purposes
718            r.reason += f'\n Body: {r.text}\n'
719            r.raise_for_status()
720
721        return r.json()

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

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

>>> Query().select('close', 'volume').limit(5).get_scanner_data_raw()
{
    'totalCount': 17559,
    'data': [
        {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]},
        {'s': 'AMEX:SPY', 'd': [542.04, 52331224]},
        {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]},
        {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]},
        {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]},
    ],
}
def get_scanner_data(self, **kwargs) -> tuple[int, pandas.core.frame.DataFrame]:
723    def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]:
724        """
725        Perform a POST web-request and return the data from the API as a DataFrame (along with
726        the number of rows/tickers that matched your query).
727
728        Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`,
729        this can be very useful if you want to pass your own headers/cookies.
730
731        ### Live/Delayed data
732
733        Note that to get live-data you have to authenticate, which is done by passing your cookies.
734        Have a look in the README at the "Real-Time Data Access" sections.
735
736        :param kwargs: kwargs to pass to `requests.post()`
737        :return: a tuple consisting of: (total_count, dataframe)
738        """
739        import pandas as pd
740
741        json_obj = self.get_scanner_data_raw(**kwargs)
742        rows_count = json_obj['totalCount']
743
744        if '/scan2' in self.url:
745            columns = ['ticker', *json_obj['fields']]
746            rows = json_obj.get('symbols')
747            if rows:
748                df = pd.DataFrame(([row['s'], *row['f']] for row in rows), columns=columns)
749            else:
750                df = pd.DataFrame([], columns=columns)
751        else:
752            rows = json_obj['data']
753            columns = ['ticker', *self.query.get('columns', ())]  # pyright: ignore [reportArgumentType]
754            df = pd.DataFrame(([row['s'], *row['d']] for row in rows), columns=columns)
755
756        return rows_count, df

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

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

Live/Delayed data

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

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

a tuple consisting of: (total_count, dataframe)

def copy(self) -> Query:
758    def copy(self) -> Query:
759        new = Query()
760        new.query = self.query.copy()
761        return new