UNPKG

tradingview-screener-ts

Version:

TypeScript port of TradingView Screener with 100% Python parity - Based on the original Python library by shner-elmo (https://github.com/shner-elmo/TradingView-Screener)

460 lines 15.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Query = exports.Or = exports.And = exports.HEADERS = exports.URL = exports.DEFAULT_RANGE = void 0; const axios_1 = __importDefault(require("axios")); const column_1 = require("./column"); /** * Default range for query results */ exports.DEFAULT_RANGE = [0, 50]; /** * TradingView scanner API URL template */ exports.URL = 'https://scanner.tradingview.com/{market}/scan'; /** * Default HTTP headers for API requests */ exports.HEADERS = { authority: 'scanner.tradingview.com', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"', accept: 'text/plain, */*; q=0.01', 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 'sec-ch-ua-mobile': '?0', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)' + 'Chrome/98.0.4758.102 Safari/537.36', 'sec-ch-ua-platform': '"Windows"', origin: 'https://www.tradingview.com', 'sec-fetch-site': 'same-site', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', referer: 'https://www.tradingview.com/', 'accept-language': 'en-US,en;q=0.9,it;q=0.8', }; /** * Implementation for AND/OR chaining of expressions * @param expressions - Array of filter expressions * @param operator - Logical operator ('and' or 'or') * @returns Operation dictionary */ function implAndOrChaining(expressions, operator) { // We want to wrap all the `FilterOperationDict` expressions with `{'expression': expr}`, // to know if it's an instance of `FilterOperationDict` we simply check if it has the `left` key, // which no other interface has. const operands = []; for (const expr of expressions) { if ('left' in expr) { // if it's a FilterOperationDict operands.push({ expression: expr }); } else { operands.push(expr); } } return { operation: { operator, operands } }; } /** * Combines multiple expressions with AND logic * @param expressions - Filter expressions to combine * @returns Combined operation dictionary */ function And(...expressions) { return implAndOrChaining(expressions, 'and'); } exports.And = And; /** * Combines multiple expressions with OR logic * @param expressions - Filter expressions to combine * @returns Combined operation dictionary */ function Or(...expressions) { return implAndOrChaining(expressions, 'or'); } exports.Or = Or; /** * This class allows you to perform SQL-like queries on the TradingView stock screener. * * The `Query` object represents a query that can be made to the official TradingView API, and it * stores all the data as JSON internally. * * @example * To perform a simple query all you have to do is: * ```typescript * import { Query } from 'tradingview-screener-ts'; * * const result = await new Query().getScannerData(); * console.log(result); * // { * // totalCount: 18060, * // data: [ * // { ticker: 'AMEX:SPY', name: 'SPY', close: 410.68, volume: 107367671, market_cap_basic: null }, * // { ticker: 'NASDAQ:QQQ', name: 'QQQ', close: 345.31, volume: 63475390, market_cap_basic: null }, * // // ... more rows * // ] * // } * ``` * * The `getScannerData()` method will return an object with `totalCount` (like a `COUNT(*)`) and * `data` array containing the actual results. * * By default, the `Query` will select the columns: `name`, `close`, `volume`, `market_cap_basic`, * but you can override that: * * @example * ```typescript * const result = await new Query() * .select('open', 'high', 'low', 'VWAP', 'MACD.macd', 'RSI', 'Price to Earnings Ratio (TTM)') * .getScannerData(); * ``` * * You can find 250+ columns available in the TradingView documentation. * * Now let's do some queries using the `WHERE` statement, select all the stocks that the `close` is * bigger or equal than 350: * * @example * ```typescript * import { Query, Column } from 'tradingview-screener-ts'; * * const result = await new Query() * .select('close', 'volume', '52 Week High') * .where(new Column('close').gte(350)) * .getScannerData(); * ``` * * You can even use other columns in these kind of operations: * * @example * ```typescript * const result = await new Query() * .select('close', 'VWAP') * .where(new Column('close').gte(new Column('VWAP'))) * .getScannerData(); * ``` */ class Query { /** * Creates a new Query instance with default configuration */ constructor() { this.query = { markets: ['america'], symbols: { query: { types: [] }, tickers: [] }, options: { lang: 'en' }, columns: ['name', 'close', 'volume', 'market_cap_basic'], sort: { sortBy: 'Value.Traded', sortOrder: 'desc' }, range: [...exports.DEFAULT_RANGE], }; this.url = 'https://scanner.tradingview.com/america/scan'; } /** * Select specific columns to retrieve * @param columns - Column names or Column instances to select * @returns This Query instance for method chaining */ select(...columns) { this.query.columns = columns.map(col => (col instanceof column_1.Column ? col.name : col)); return this; } /** * Filter screener results (expressions are joined with the AND operator) * @param expressions - Filter expressions to apply * @returns This Query instance for method chaining */ where(...expressions) { this.query.filter = [...expressions]; return this; } /** * 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., `new Column('field').eq('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. * * @param operation - Operation dictionary with AND/OR logic * @returns This Query instance for method chaining * * @example * Combining conditions with `OR` and nested `AND`: * ```typescript * import { Query, And, Or, col } from 'tradingview-screener-ts'; * * const result = await new Query() * .select('type', 'typespecs') * .where2( * Or( * And(col('type').eq('stock'), col('typespecs').has(['common', 'preferred'])), * And(col('type').eq('fund'), col('typespecs').hasNoneOf(['etf'])), * col('type').eq('dr') * ) * ) * .getScannerData(); * ``` */ where2(operation) { this.query.filter2 = operation.operation; return this; } /** * Applies sorting to the query results based on the specified column * @param column - Column to sort by * @param ascending - Sort order (true for ascending, false for descending) * @param nullsFirst - Whether to place null values first * @returns This Query instance for method chaining * * @example * ```typescript * new Query().orderBy('volume', false) // sort descending * new Query().orderBy('close', true) * new Query().orderBy('dividends_yield_current', false, false) * ``` */ orderBy(column, ascending = true, nullsFirst = false) { const sortDict = { sortBy: column instanceof column_1.Column ? column.name : column, sortOrder: ascending ? 'asc' : 'desc', nullsFirst, }; this.query.sort = sortDict; return this; } /** * Limit the number of results * @param limit - Maximum number of results to return * @returns This Query instance for method chaining */ limit(limit) { if (!this.query.range) { this.query.range = [...exports.DEFAULT_RANGE]; } this.query.range[1] = limit; return this; } /** * Skip a number of results (pagination) * @param offset - Number of results to skip * @returns This Query instance for method chaining */ offset(offset) { if (!this.query.range) { this.query.range = [...exports.DEFAULT_RANGE]; } this.query.range[0] = offset; return this; } /** * Set the markets to query * @param markets - Market names to include * @returns This Query instance for method chaining * * @example * ```typescript * // Single market * new Query().setMarkets('italy') * * // Multiple markets * new Query().setMarkets('america', 'israel', 'hongkong', 'switzerland') * * // Different asset classes * new Query().setMarkets('cfd', 'crypto', 'forex', 'futures') * ``` */ setMarkets(...markets) { if (markets.length === 1) { const market = markets[0]; this.url = exports.URL.replace('{market}', market); this.query.markets = [market]; } else { this.url = exports.URL.replace('{market}', 'global'); this.query.markets = [...markets]; } return this; } /** * Set specific tickers to query * @param tickers - Ticker symbols to include (format: 'EXCHANGE:SYMBOL') * @returns This Query instance for method chaining * * @example * ```typescript * new Query().setTickers('NASDAQ:TSLA') * new Query().setTickers('NYSE:GME', 'AMEX:SPY', 'MIL:RACE', 'HOSE:VIX') * ``` */ setTickers(...tickers) { if (!this.query.symbols) { this.query.symbols = {}; } this.query.symbols.tickers = [...tickers]; this.setMarkets(); // Reset markets to global return this; } /** * Scan only equities that are in the given index (or indexes) * @param indexes - Index symbols to filter by * @returns This Query instance for method chaining * * @example * ```typescript * new Query().setIndex('SYML:SP;SPX') * new Query().setIndex('SYML:NSE;NIFTY', 'SYML:TVC;UKX') * ``` */ setIndex(...indexes) { this.query.preset = 'index_components_market_pages'; if (!this.query.symbols) { this.query.symbols = {}; } this.query.symbols.symbolset = [...indexes]; this.setMarkets(); // Reset markets to global return this; } /** * Set a custom property on the query * @param key - Property key * @param value - Property value * @returns This Query instance for method chaining */ setProperty(key, value) { this.query[key] = value; return this; } /** * Perform a POST web-request and return the raw data from the API * @param config - Additional request configuration * @returns Raw API response * * @example * ```typescript * const rawData = await new Query() * .select('close', 'volume') * .limit(5) * .getScannerDataRaw(); * * console.log(rawData); * // { * // totalCount: 17559, * // data: [ * // { s: 'NASDAQ:NVDA', d: [116.14, 312636630] }, * // { s: 'AMEX:SPY', d: [542.04, 52331224] }, * // // ... * // ] * // } * ``` */ async getScannerDataRaw(config = {}) { if (!this.query.range) { this.query.range = [...exports.DEFAULT_RANGE]; } const requestConfig = { headers: { ...exports.HEADERS, ...config.headers }, timeout: config.timeout || 20000, ...config, }; // Handle cookies if (config.cookies) { if (typeof config.cookies === 'string') { requestConfig.headers['Cookie'] = config.cookies; } else { const cookieString = Object.entries(config.cookies) .map(([key, value]) => `${key}=${value}`) .join('; '); requestConfig.headers['Cookie'] = cookieString; } } try { const response = await axios_1.default.post(this.url, this.query, requestConfig); return response.data; } catch (error) { if (axios_1.default.isAxiosError(error) && error.response) { const errorMessage = `HTTP ${error.response.status}: ${error.response.statusText}\nBody: ${JSON.stringify(error.response.data)}`; throw new Error(errorMessage); } throw error; } } /** * Perform a POST web-request and return the data from the API as a structured result * @param config - Additional request configuration * @returns Structured result with totalCount and data array * * @example * ```typescript * const result = await new Query() * .select('name', 'close', 'volume') * .where(new Column('close').gte(100)) * .getScannerData(); * * console.log(`Found ${result.totalCount} results`); * result.data.forEach(row => { * console.log(`${row.name}: $${row.close}`); * }); * ``` */ async getScannerData(config = {}) { const jsonObj = await this.getScannerDataRaw(config); const rowsCount = jsonObj.totalCount; const data = jsonObj.data; const columns = this.query.columns || []; const structuredData = data.map(row => { const result = { ticker: row.s }; columns.forEach((column, index) => { result[column] = row.d[index]; }); return result; }); return { totalCount: rowsCount, data: structuredData, }; } /** * Create a copy of this Query * @returns New Query instance with copied configuration */ copy() { const newQuery = new Query(); newQuery.query = JSON.parse(JSON.stringify(this.query)); newQuery.url = this.url; return newQuery; } /** * Get the current query configuration * @returns Current query dictionary */ getQuery() { return JSON.parse(JSON.stringify(this.query)); } /** * Get the current URL * @returns Current API URL */ getUrl() { return this.url; } /** * String representation of the Query * @returns Formatted string representation */ toString() { return `Query(\n ${JSON.stringify(this.query, null, 2)}\n url=${this.url}\n)`; } /** * Check equality with another Query * @param other - Other Query to compare with * @returns True if queries are equal */ equals(other) { return JSON.stringify(this.query) === JSON.stringify(other.query) && this.url === other.url; } } exports.Query = Query; //# sourceMappingURL=query.js.map