tradingview_screener.query

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

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

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

Examples:

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

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

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


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

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

You can find the 250+ columns available in the Fields page.

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

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

You can even use other columns in these kind of operations

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

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

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

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

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

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

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

Filter screener (expressions are joined with the AND operator)

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

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

Rules:

  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:

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

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

Examples:

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

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

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

You may choose any value from the Markets page.

If you select multiple countries, you might want to

Examples:

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

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

You can also select multiple markets

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

You may also select different financial instruments

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

Self

def set_tickers(self, *tickers: str) -> Self:
576    def set_tickers(self, *tickers: str) -> Self:
577        """
578        Set the tickers you wish to receive information on.
579
580        Note that this resets the markets and sets the URL market to `global`.
581
582        Examples:
583
584        >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd')
585        >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data()
586        (1,
587                 ticker  name   market  close   volume    VWAP  MACD.macd
588         0  NASDAQ:TSLA  TSLA  america    186  3519931  185.53   2.371601)
589
590        To set multiple tickers:
591        >>> (Query()
592        ...  .select('name', 'close', 'volume', 'market_cap_basic')
593        ...  .set_tickers('NYSE:GME', 'MIL:RACE', 'HOSE:VIX')
594        ...  .get_scanner_data())
595        (3,
596            ticker  name    close    volume  market_cap_basic
597        0  MIL:RACE  RACE    300.4    351982      8.161551e+10
598        1  NYSE:GME   GME     21.8   6429236      9.781469e+09
599        2  HOSE:VIX   VIX  17750.0  26524292      1.666686e+09)
600 
601        :param tickers: One or more tickers, syntax: `exchange:symbol`
602        :return: Self
603        """
604        self.query.setdefault('symbols', {})['tickers'] = list(tickers)
605        self.set_markets()
606        return self

Set the tickers you wish to receive information on.

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

Examples:

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

To set multiple tickers:

>>> (Query()
...  .select('name', 'close', 'volume', 'market_cap_basic')
...  .set_tickers('NYSE:GME', 'MIL:RACE', 'HOSE:VIX')
...  .get_scanner_data())
(3,
    ticker  name    close    volume  market_cap_basic
0  MIL:RACE  RACE    300.4    351982      8.161551e+10
1  NYSE:GME   GME     21.8   6429236      9.781469e+09
2  HOSE:VIX   VIX  17750.0  26524292      1.666686e+09)
Parameters
  • **tickers: One or more tickers, syntax: exchange**: symbol
Returns

Self

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

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

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

Examples:

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

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

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

Note that the syntax to specify an index is SYML:{source};{symbol}.

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

An instance of the Query class with the filter applied

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

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

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

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

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

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

Live/Delayed data

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

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

a tuple consisting of: (total_count, dataframe)

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