tradingview_screener.query

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

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

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

Examples:

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

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

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


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

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

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

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

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

You can even use other columns in these kind of operations

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

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

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

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

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

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

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

Filter screener (expressions are joined with the AND operator)

def where2(self, operation: tradingview_screener.models.OperationDict) -> Self:
270    def where2(self, operation: OperationDict) -> Self:
271        """
272        """
273        self.query['filter2'] = operation['operation']
274        return self
def order_by( self, column: tradingview_screener.column.Column | str, ascending: bool = True, nulls_first: bool = False) -> Self:
276    def order_by(
277        self, column: Column | str, ascending: bool = True, nulls_first: bool = False
278    ) -> Self:
279        """
280        Applies sorting to the query results based on the specified column.
281
282        Examples:
283
284        >>> Query().order_by('volume', ascending=False)  # sort descending
285        >>> Query().order_by('close', ascending=True)
286        >>> Query().order_by('dividends_yield_current', ascending=False, nulls_first=False)
287
288        :param column: Either a `Column` object or a string with the column name.
289        :param ascending: Set to True for ascending order (default), or False for descending.
290        :param nulls_first: If True, places `None` values at the beginning of the results. Defaults
291        to False.
292        :return: The updated query object.
293        """
294        dct: SortByDict = {
295            'sortBy': column.name if isinstance(column, Column) else column,
296            'sortOrder': 'asc' if ascending else 'desc',
297            'nullsFirst': nulls_first,
298        }
299        self.query['sort'] = dct
300        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:
302    def limit(self, limit: int) -> Self:
303        self.query.setdefault('range', DEFAULT_RANGE.copy())[1] = limit
304        return self
def offset(self, offset: int) -> Self:
306    def offset(self, offset: int) -> Self:
307        self.query.setdefault('range', DEFAULT_RANGE.copy())[0] = offset
308        return self
def set_markets(self, *markets: str) -> Self:
310    def set_markets(self, *markets: str) -> Self:
311        """
312        This method allows you to select the market/s which you want to query.
313
314        By default, the screener will only scan US equities, but you can change it to scan any
315        market or country, that includes a list of 67 countries, and also the following
316        asset classes: `bonds`, `cfd`, `coin`, `crypto`, `euronext`, `forex`,
317        `futures`, `options`.
318
319        You may choose any value from `tradingview_screener.constants.MARKETS`.
320
321        If you select multiple countries, you might want to
322
323        Examples:
324
325        By default, the screener will show results from the `america` market, but you can
326        change it (note the difference between `market` and `country`)
327        >>> columns = ['close', 'market', 'country', 'currency']
328        >>> (Query()
329        ...  .select(*columns)
330        ...  .set_markets('italy')
331        ...  .get_scanner_data())
332        (2346,
333                ticker    close market      country currency
334         0     MIL:UCG  23.9150  italy        Italy      EUR
335         1     MIL:ISP   2.4910  italy        Italy      EUR
336         2   MIL:STLAM  17.9420  italy  Netherlands      EUR
337         3    MIL:ENEL   6.0330  italy        Italy      EUR
338         4     MIL:ENI  15.4800  italy        Italy      EUR
339         ..        ...      ...    ...          ...      ...
340         45    MIL:UNI   5.1440  italy        Italy      EUR
341         46   MIL:3OIS   0.4311  italy      Ireland      EUR
342         47   MIL:3SIL  35.2300  italy      Ireland      EUR
343         48   MIL:IWDE  69.1300  italy      Ireland      EUR
344         49   MIL:QQQS  19.2840  italy      Ireland      EUR
345         [50 rows x 5 columns])
346
347        You can also select multiple markets
348        >>> (Query()
349        ...  .select(*columns)
350        ...  .set_markets('america', 'israel', 'hongkong', 'switzerland')
351        ...  .get_scanner_data())
352        (23964,
353                   ticker      close    market        country currency
354         0       AMEX:SPY   420.1617   america  United States      USD
355         1    NASDAQ:TSLA   201.2000   america  United States      USD
356         2    NASDAQ:NVDA   416.7825   america  United States      USD
357         3     NASDAQ:AMD   106.6600   america  United States      USD
358         4     NASDAQ:QQQ   353.7985   america  United States      USD
359         ..           ...        ...       ...            ...      ...
360         45  NASDAQ:GOOGL   124.9200   america  United States      USD
361         46     HKEX:1211   233.2000  hongkong          China      HKD
362         47     TASE:ALHE  1995.0000    israel         Israel      ILA
363         48      AMEX:BIL    91.4398   america  United States      USD
364         49   NASDAQ:GOOG   126.1500   america  United States      USD
365         [50 rows x 5 columns])
366
367        You may also select different financial instruments
368        >>> (Query()
369        ...  .select('close', 'market')
370        ...  .set_markets('cfd', 'crypto', 'forex', 'futures')
371        ...  .get_scanner_data())
372        (118076,
373                                    ticker  ...  market
374         0          UNISWAP3ETH:JUSTICEUSDT  ...  crypto
375         1             UNISWAP3ETH:UAHGUSDT  ...  crypto
376         2            UNISWAP3ETH:KENDUWETH  ...  crypto
377         3         UNISWAP3ETH:MATICSTMATIC  ...  crypto
378         4             UNISWAP3ETH:WETHETHM  ...  crypto
379         ..                             ...  ...     ...
380         45  UNISWAP:MUSICAIWETH_1F5304.USD  ...  crypto
381         46                   CRYPTOCAP:FIL  ...     cfd
382         47                   CRYPTOCAP:SUI  ...     cfd
383         48                  CRYPTOCAP:ARBI  ...     cfd
384         49                    CRYPTOCAP:OP  ...     cfd
385         [50 rows x 3 columns])
386
387        :param markets: one or more markets from `tradingview_screener.constants.MARKETS`
388        :return: Self
389        """
390        if len(markets) == 1:
391            market = markets[0]
392            self.url = URL.format(market=market)
393            self.query['markets'] = [market]
394        else:  # len(markets) == 0 or len(markets) > 1
395            self.url = URL.format(market='global')
396            self.query['markets'] = list(markets)
397
398        return self

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

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

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

If you select multiple countries, you might want to

Examples:

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

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

You can also select multiple markets

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

You may also select different financial instruments

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

Self

def set_tickers(self, *tickers: str) -> Self:
400    def set_tickers(self, *tickers: str) -> Self:
401        """
402        Set the tickers you wish to receive information on.
403
404        Note that this resets the markets and sets the URL market to `global`.
405
406        Examples:
407
408        >>> q = Query().select('name', 'market', 'close', 'volume', 'VWAP', 'MACD.macd')
409        >>> q.set_tickers('NASDAQ:TSLA').get_scanner_data()
410        (1,
411                 ticker  name   market  close   volume    VWAP  MACD.macd
412         0  NASDAQ:TSLA  TSLA  america    186  3519931  185.53   2.371601)
413
414        To set tickers from multiple markets we need to update the markets that include them:
415        >>> (Query()
416        ...  .set_markets('america', 'italy', 'vietnam')
417        ...  .set_tickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX')
418        ...  .get_scanner_data())
419        (4,
420              ticker  name     close    volume  market_cap_basic
421         0  HOSE:VIX   VIX  16700.00  33192500      4.568961e+08
422         1  AMEX:SPY   SPY    544.35   1883562               NaN
423         2  NYSE:GME   GME     23.80   3116758      1.014398e+10
424         3  MIL:RACE  RACE    393.30    122878      1.006221e+11)
425
426        :param tickers: One or more tickers, syntax: `exchange:symbol`
427        :return: Self
428        """
429        self.query.setdefault('symbols', {})['tickers'] = list(tickers)
430        self.set_markets()
431        return self

Set the tickers you wish to receive information on.

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

Examples:

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

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

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

Self

def set_index(self, *indexes: str) -> Self:
433    def set_index(self, *indexes: str) -> Self:
434        """
435        Scan only the equities that are in in the given index (or indexes).
436
437        Note that this resets the markets and sets the URL market to `global`.
438
439        Examples:
440
441        >>> Query().set_index('SYML:SP;SPX').get_scanner_data()
442        (503,
443                   ticker   name    close    volume  market_cap_basic
444         0    NASDAQ:NVDA   NVDA  1208.88  41238122      2.973644e+12
445         1    NASDAQ:AAPL   AAPL   196.89  53103705      3.019127e+12
446         2    NASDAQ:TSLA   TSLA   177.48  56244929      5.660185e+11
447         3     NASDAQ:AMD    AMD   167.87  44795240      2.713306e+11
448         4    NASDAQ:MSFT   MSFT   423.85  13621485      3.150183e+12
449         5    NASDAQ:AMZN   AMZN   184.30  28021473      1.917941e+12
450         6    NASDAQ:META   META   492.96   9379199      1.250410e+12
451         7   NASDAQ:GOOGL  GOOGL   174.46  19660698      2.164346e+12
452         8    NASDAQ:SMCI   SMCI   769.11   3444852      4.503641e+10
453         9    NASDAQ:GOOG   GOOG   175.95  14716134      2.164346e+12
454         10   NASDAQ:AVGO   AVGO  1406.64   1785876      6.518669e+11)
455
456        You can set multiple indices as well, like the NIFTY 50 and UK 100 Index.
457        >>> Query().set_index('SYML:NSE;NIFTY', 'SYML:TVC;UKX').get_scanner_data()
458        (150,
459                     ticker        name         close     volume  market_cap_basic
460         0         NSE:INFY        INFY   1533.600000   24075302      7.623654e+10
461         1          LSE:AZN         AZN  12556.000000    2903032      2.489770e+11
462         2     NSE:HDFCBANK    HDFCBANK   1573.350000   18356108      1.432600e+11
463         3     NSE:RELIANCE    RELIANCE   2939.899900    9279348      2.381518e+11
464         4         LSE:LSEG        LSEG   9432.000000    2321053      6.395329e+10
465         5   NSE:BAJFINANCE  BAJFINANCE   7191.399900    2984052      5.329685e+10
466         6         LSE:BARC        BARC    217.250000   96238723      4.133010e+10
467         7         NSE:SBIN        SBIN    829.950010   25061284      8.869327e+10
468         8           NSE:LT          LT   3532.500000    5879660      5.816100e+10
469         9         LSE:SHEL        SHEL   2732.500000    7448315      2.210064e+11)
470
471        You can find the full list of indices in [`constants.INDICES`](constants.html#INDICES),
472        just note that the syntax is
473        `SYML:{source};{symbol}`.
474
475        :param indexes: One or more strings representing the financial indexes to filter by
476        :return: An instance of the `Query` class with the filter applied
477        """
478        self.query.setdefault('preset', 'index_components_market_pages')
479        self.query.setdefault('symbols', {})['symbolset'] = list(indexes)
480        # reset markets list and URL to `/global`
481        self.set_markets()
482        return self

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

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

Examples:

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

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

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

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

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

An instance of the Query class with the filter applied

def set_property(self, key: str, value: Any) -> Self:
513    def set_property(self, key: str, value: Any) -> Self:
514        self.query[key] = value
515        return self
def get_scanner_data_raw(self, **kwargs) -> tradingview_screener.models.ScreenerDict:
517    def get_scanner_data_raw(self, **kwargs) -> ScreenerDict:
518        """
519        Perform a POST web-request and return the data from the API (dictionary).
520
521        Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`,
522        this can be very useful if you want to pass your own headers/cookies.
523
524        >>> Query().select('close', 'volume').limit(5).get_scanner_data_raw()
525        {
526            'totalCount': 17559,
527            'data': [
528                {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]},
529                {'s': 'AMEX:SPY', 'd': [542.04, 52331224]},
530                {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]},
531                {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]},
532                {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]},
533            ],
534        }
535        """
536        self.query.setdefault('range', DEFAULT_RANGE.copy())
537
538        kwargs.setdefault('headers', HEADERS)
539        kwargs.setdefault('timeout', 20)
540        r = requests.post(self.url, json=self.query, **kwargs)
541
542        if not r.ok:
543            # add the body to the error message for debugging purposes
544            r.reason += f'\n Body: {r.text}\n'
545            r.raise_for_status()
546
547        return r.json()

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

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

>>> Query().select('close', 'volume').limit(5).get_scanner_data_raw()
{
    'totalCount': 17559,
    'data': [
        {'s': 'NASDAQ:NVDA', 'd': [116.14, 312636630]},
        {'s': 'AMEX:SPY', 'd': [542.04, 52331224]},
        {'s': 'NASDAQ:QQQ', 'd': [462.58, 40084156]},
        {'s': 'NASDAQ:TSLA', 'd': [207.83, 76247251]},
        {'s': 'NASDAQ:SBUX', 'd': [95.9, 157211696]},
    ],
}
def get_scanner_data(self, **kwargs) -> tuple[int, pandas.core.frame.DataFrame]:
549    def get_scanner_data(self, **kwargs) -> tuple[int, pd.DataFrame]:
550        """
551        Perform a POST web-request and return the data from the API as a DataFrame (along with
552        the number of rows/tickers that matched your query).
553
554        Note that you can pass extra keyword-arguments that will be forwarded to `requests.post()`,
555        this can be very useful if you want to pass your own headers/cookies.
556
557        ### Live/Delayed data
558
559        Note that to get live-data you have to authenticate, which is done by passing your cookies.
560        Have a look in the README at the "Real-Time Data Access" sections.
561
562        :param kwargs: kwargs to pass to `requests.post()`
563        :return: a tuple consisting of: (total_count, dataframe)
564        """
565        import pandas as pd
566
567        json_obj = self.get_scanner_data_raw(**kwargs)
568        rows_count = json_obj['totalCount']
569        data = json_obj['data']
570
571        df = pd.DataFrame(
572            data=([row['s'], *row['d']] for row in data),
573            columns=['ticker', *self.query.get('columns', ())],  # pyright: ignore [reportArgumentType]
574        )
575        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:
577    def copy(self) -> Query:
578        new = Query()
579        new.query = self.query.copy()
580        return new