├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── test_columns.py │ ├── test_util.py │ ├── test_fields.py │ ├── test_stream.py │ ├── test_ta.py │ ├── test_beauty.py │ └── test_field_conditions.py └── functional │ ├── __init__.py │ ├── test_cryptoscreener.py │ ├── test_forexscreener.py │ └── test_stockscreener.py ├── tvscreener ├── py.typed ├── core │ ├── __init__.py │ ├── bond.py │ ├── futures.py │ ├── coin.py │ ├── forex.py │ ├── crypto.py │ └── stock.py ├── exceptions.py ├── filter.py ├── __init__.py ├── ta │ └── __init__.py └── util.py ├── .github ├── img │ ├── logo.png │ ├── dataframe.png │ └── tradingview-screener.png └── workflows │ ├── codecov.yml │ ├── pip-publish.yml │ └── deploy-pages.yml ├── .dev └── codegen │ ├── data │ ├── submarket.json │ ├── exchange.json │ ├── main.json │ ├── symbol type.json │ ├── sector.json │ ├── patterns.json │ ├── index.json │ ├── country.json │ ├── time_intervals.json │ └── industry.json │ ├── code │ ├── submarket.py.generated │ ├── exchange.py.generated │ ├── symbol_type.py.generated │ ├── sector.py.generated │ ├── index.py.generated │ ├── country.py.generated │ └── industry.py.generated │ ├── Generate.ipynb │ └── Generate FilterFields.ipynb ├── requirements.txt ├── pyproject.toml ├── docs ├── getting-started │ ├── installation.md │ ├── quickstart.md │ └── code-generator.md ├── changelog.md ├── stylesheets │ └── extra.css ├── screeners │ ├── bond.md │ ├── coin.md │ ├── crypto.md │ ├── futures.md │ ├── forex.md │ └── stock.md ├── guide │ ├── styled-output.md │ ├── sorting-pagination.md │ ├── streaming.md │ ├── time-intervals.md │ ├── selecting-fields.md │ └── filtering.md ├── index.md ├── api │ ├── filters.md │ ├── screeners.md │ ├── fields.md │ └── enums.md └── examples │ └── crypto-strategies.md ├── mkdocs.yml ├── .gitignore └── app └── js └── code-generator.js /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tvscreener/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tvscreener/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepentropy/tvscreener/HEAD/.github/img/logo.png -------------------------------------------------------------------------------- /.github/img/dataframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepentropy/tvscreener/HEAD/.github/img/dataframe.png -------------------------------------------------------------------------------- /.dev/codegen/data/submarket.json: -------------------------------------------------------------------------------- 1 | { 2 | "Submarket": [ 3 | "Any", 4 | "OTCQB", 5 | "OTCQX", 6 | "PINK" 7 | ] 8 | } -------------------------------------------------------------------------------- /.github/img/tradingview-screener.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepentropy/tvscreener/HEAD/.github/img/tradingview-screener.png -------------------------------------------------------------------------------- /.dev/codegen/data/exchange.json: -------------------------------------------------------------------------------- 1 | { 2 | "Exchange": [ 3 | "Any", 4 | "NYSE ARCA", 5 | "NASDAQ", 6 | "NYSE", 7 | "OTC" 8 | ] 9 | } -------------------------------------------------------------------------------- /.dev/codegen/data/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": [ 3 | "logoid", 4 | "name", 5 | "description", 6 | "type", 7 | "subtype", 8 | "currency", 9 | "fundamental_currency_code" 10 | ] 11 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Runtime dependencies - These are also specified in pyproject.toml 2 | # This file is for convenience during development 3 | pandas>=1.3.0 4 | requests>=2.27.1 5 | 6 | # Development dependencies 7 | # To install with dev dependencies, use: pip install -e ".[dev]" 8 | -------------------------------------------------------------------------------- /.dev/codegen/code/submarket.py.generated: -------------------------------------------------------------------------------- 1 | 2 | # ---------------------- 3 | # Generated file 4 | # 2023-08-11 07:05:13.153890 5 | # ---------------------- 6 | from enum import Enum 7 | 8 | 9 | class Submarket(Enum): 10 | OTCQB = 'OTCQB' 11 | OTCQX = 'OTCQX' 12 | PINK = 'PINK' 13 | -------------------------------------------------------------------------------- /tests/functional/test_cryptoscreener.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tvscreener import CryptoScreener 4 | 5 | 6 | class TestScreener(unittest.TestCase): 7 | 8 | def test_range(self): 9 | ss = CryptoScreener() 10 | df = ss.get() 11 | self.assertEqual(150, len(df)) 12 | -------------------------------------------------------------------------------- /.dev/codegen/code/exchange.py.generated: -------------------------------------------------------------------------------- 1 | 2 | # ---------------------- 3 | # Generated file 4 | # 2023-08-11 07:05:13.153328 5 | # ---------------------- 6 | from enum import Enum 7 | 8 | 9 | class Exchange(Enum): 10 | NYSE_ARCA = 'NYSE ARCA' 11 | NASDAQ = 'NASDAQ' 12 | NYSE = 'NYSE' 13 | OTC = 'OTC' 14 | -------------------------------------------------------------------------------- /.dev/codegen/data/symbol type.json: -------------------------------------------------------------------------------- 1 | { 2 | "Symbol Type": [ 3 | "Any", 4 | "Closed-end fund", 5 | "Common Stock", 6 | "Depositary Receipt", 7 | "ETF", 8 | "ETN", 9 | "Mutual fund", 10 | "Preferred Stock", 11 | "REIT", 12 | "Structured", 13 | "Trust fund", 14 | "UIT" 15 | ] 16 | } -------------------------------------------------------------------------------- /tvscreener/exceptions.py: -------------------------------------------------------------------------------- 1 | class MalformedRequestException(Exception): 2 | def __init__(self, code, response_msg, url, payload): 3 | message = f"Error: {code}: {response_msg}\n" 4 | message += f"Request: {url}\n" 5 | message += "Payload:\n" 6 | message += payload 7 | super().__init__(message) 8 | -------------------------------------------------------------------------------- /tvscreener/core/bond.py: -------------------------------------------------------------------------------- 1 | from tvscreener.core.base import Screener 2 | from tvscreener.field.bond import BondField, DEFAULT_BOND_FIELDS 3 | from tvscreener.util import get_url 4 | 5 | 6 | class BondScreener(Screener): 7 | """Bond screener for querying bonds from TradingView.""" 8 | 9 | _field_type = BondField 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.url = get_url("bond") 14 | self.specific_fields = DEFAULT_BOND_FIELDS 15 | self.sort_by(BondField.VOLUME, False) 16 | -------------------------------------------------------------------------------- /tvscreener/core/futures.py: -------------------------------------------------------------------------------- 1 | from tvscreener.core.base import Screener 2 | from tvscreener.field.futures import FuturesField, DEFAULT_FUTURES_FIELDS 3 | from tvscreener.util import get_url 4 | 5 | 6 | class FuturesScreener(Screener): 7 | """Futures screener for querying futures from TradingView.""" 8 | 9 | _field_type = FuturesField 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.url = get_url("futures") 14 | self.specific_fields = DEFAULT_FUTURES_FIELDS 15 | self.sort_by(FuturesField.VOLUME, False) 16 | -------------------------------------------------------------------------------- /.dev/codegen/code/symbol_type.py.generated: -------------------------------------------------------------------------------- 1 | 2 | # ---------------------- 3 | # Generated file 4 | # 2023-08-11 07:05:13.152605 5 | # ---------------------- 6 | from enum import Enum 7 | 8 | 9 | class SymbolType(Enum): 10 | CLOSEDEND_FUND = 'Closed-end fund' 11 | COMMON_STOCK = 'Common Stock' 12 | DEPOSITARY_RECEIPT = 'Depositary Receipt' 13 | ETF = 'ETF' 14 | ETN = 'ETN' 15 | MUTUAL_FUND = 'Mutual fund' 16 | PREFERRED_STOCK = 'Preferred Stock' 17 | REIT = 'REIT' 18 | STRUCTURED = 'Structured' 19 | TRUST_FUND = 'Trust fund' 20 | UIT = 'UIT' 21 | -------------------------------------------------------------------------------- /tvscreener/core/coin.py: -------------------------------------------------------------------------------- 1 | from tvscreener.core.base import Screener 2 | from tvscreener.field.coin import CoinField, DEFAULT_COIN_FIELDS 3 | from tvscreener.util import get_url 4 | 5 | 6 | class CoinScreener(Screener): 7 | """Coin screener for querying coins (CEX and DEX) from TradingView.""" 8 | 9 | _field_type = CoinField 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.url = get_url("coin") 14 | self.specific_fields = DEFAULT_COIN_FIELDS 15 | self.sort_by(CoinField.MARKET_CAP, False) 16 | self.add_misc("price_conversion", {"to_symbol": False}) 17 | -------------------------------------------------------------------------------- /.dev/codegen/data/sector.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sector": [ 3 | "Any", 4 | "Commercial Services", 5 | "Communications", 6 | "Consumer Durables", 7 | "Consumer Non-Durables", 8 | "Consumer Services", 9 | "Distribution Services", 10 | "Electronic Technology", 11 | "Energy Minerals", 12 | "Finance", 13 | "Government", 14 | "Health Services", 15 | "Health Technology", 16 | "Industrial Services", 17 | "Miscellaneous", 18 | "Non-Energy Minerals", 19 | "Process Industries", 20 | "Producer Manufacturing", 21 | "Retail Trade", 22 | "Technology Services", 23 | "Transportation", 24 | "Utilities" 25 | ] 26 | } -------------------------------------------------------------------------------- /tests/functional/test_forexscreener.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tvscreener import ForexScreener, ForexField, FilterOperator 4 | from tvscreener.field import Region 5 | 6 | 7 | class TestForexScreener(unittest.TestCase): 8 | 9 | def test_len(self): 10 | fs = ForexScreener() 11 | df = fs.get() 12 | self.assertEqual(150, len(df)) 13 | 14 | def test_region(self): 15 | fs = ForexScreener() 16 | fs.add_filter(ForexField.REGION, FilterOperator.EQUAL, Region.AFRICA) 17 | df = fs.get() 18 | self.assertEqual(49, len(df)) 19 | 20 | self.assertEqual(df.loc[0, "Symbol"], "FX_IDC:GHSNGN") 21 | self.assertEqual(df.loc[0, "Name"], "GHSNGN") 22 | -------------------------------------------------------------------------------- /tvscreener/core/forex.py: -------------------------------------------------------------------------------- 1 | from tvscreener.core.base import Screener, default_sort_forex 2 | from tvscreener.field.forex import ForexField, DEFAULT_FOREX_FIELDS 3 | from tvscreener.util import get_url 4 | 5 | 6 | class ForexScreener(Screener): 7 | """Forex screener for querying forex/currency pairs from TradingView.""" 8 | 9 | _field_type = ForexField 10 | 11 | def __init__(self): 12 | super().__init__() 13 | subtype = "forex" 14 | self.url = get_url(subtype) 15 | self.markets = {subtype} # Fixed: set literal instead of set(string) 16 | self.specific_fields = DEFAULT_FOREX_FIELDS # Use default fields (set to ForexField for all 2900+ fields) 17 | self.sort_by(default_sort_forex) 18 | self.add_misc("symbols", {"query": {"types": ["forex"]}}) 19 | -------------------------------------------------------------------------------- /tvscreener/core/crypto.py: -------------------------------------------------------------------------------- 1 | from tvscreener.core.base import Screener, default_sort_crypto 2 | from tvscreener.field.crypto import CryptoField, DEFAULT_CRYPTO_FIELDS 3 | from tvscreener.util import get_url 4 | 5 | 6 | class CryptoScreener(Screener): 7 | """Crypto screener for querying cryptocurrencies from TradingView.""" 8 | 9 | _field_type = CryptoField 10 | 11 | def __init__(self): 12 | super().__init__() 13 | subtype = "crypto" 14 | self.markets = {subtype} # Fixed: set literal instead of set(string) 15 | self.url = get_url(subtype) 16 | self.specific_fields = DEFAULT_CRYPTO_FIELDS # Use default fields (set to CryptoField for all 3000+ fields) 17 | self.sort_by(default_sort_crypto, False) 18 | self.add_misc("price_conversion", {"to_symbol": False}) 19 | -------------------------------------------------------------------------------- /.dev/codegen/data/patterns.json: -------------------------------------------------------------------------------- 1 | { 2 | "patterns": [ 3 | "Candle.AbandonedBaby.Bearish", 4 | "Candle.AbandonedBaby.Bullish", 5 | "Candle.Engulfing.Bearish", 6 | "Candle.Harami.Bearish", 7 | "Candle.Engulfing.Bullish", 8 | "Candle.Harami.Bullish", 9 | "Candle.Doji", 10 | "Candle.Doji.Dragonfly", 11 | "Candle.EveningStar", 12 | "Candle.Doji.Gravestone", 13 | "Candle.Hammer", 14 | "Candle.HangingMan", 15 | "Candle.InvertedHammer", 16 | "Candle.Kicking.Bearish", 17 | "Candle.Kicking.Bullish", 18 | "Candle.LongShadow.Lower", 19 | "Candle.LongShadow.Upper", 20 | "Candle.Marubozu.Black", 21 | "Candle.Marubozu.White", 22 | "Candle.MorningStar", 23 | "Candle.ShootingStar", 24 | "Candle.SpinningTop.Black", 25 | "Candle.SpinningTop.White", 26 | "Candle.3BlackCrows", 27 | "Candle.3WhiteSoldiers", 28 | "Candle.TriStar.Bearish", 29 | "Candle.TriStar.Bullish" 30 | ] 31 | } -------------------------------------------------------------------------------- /tests/unit/test_columns.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tvscreener import StockField 4 | from tvscreener.field import add_historical 5 | from tvscreener.util import format_historical_field 6 | 7 | 8 | class TestColumns(unittest.TestCase): 9 | 10 | def test_hist_1(self): 11 | field = format_historical_field(StockField.NEGATIVE_DIRECTIONAL_INDICATOR_14) 12 | self.assertEqual("ADX-DI[1]", field) 13 | 14 | def test_hist_2(self): 15 | field = format_historical_field(StockField.NEGATIVE_DIRECTIONAL_INDICATOR_14, 2) 16 | self.assertEqual("ADX-DI[2]", field) 17 | 18 | def test_add_historical(self): 19 | field = add_historical(StockField.POSITIVE_DIRECTIONAL_INDICATOR_14.field_name) 20 | self.assertEqual("ADX+DI[1]", field) 21 | 22 | def test_add_historical_2(self): 23 | field = add_historical(StockField.POSITIVE_DIRECTIONAL_INDICATOR_14.field_name, 2) 24 | self.assertEqual("ADX+DI[2]", field) 25 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - main 8 | 9 | permissions: 10 | contents: read # Only need to read repository contents 11 | 12 | jobs: 13 | run: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.x' 21 | - name: Generate coverage report 22 | run: | 23 | pip install -r requirements.txt 24 | pip install pytest 25 | pip install pytest-cov 26 | pytest --cov=./ --cov-report=xml 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v4 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | env_vars: OS,PYTHON 32 | fail_ci_if_error: true 33 | files: ./coverage.xml 34 | flags: unittests 35 | name: codecov-umbrella 36 | verbose: true -------------------------------------------------------------------------------- /.dev/codegen/code/sector.py.generated: -------------------------------------------------------------------------------- 1 | 2 | # ---------------------- 3 | # Generated file 4 | # 2023-08-11 07:05:13.154481 5 | # ---------------------- 6 | from enum import Enum 7 | 8 | 9 | class Sector(Enum): 10 | COMMERCIAL_SERVICES = 'Commercial Services' 11 | COMMUNICATIONS = 'Communications' 12 | CONSUMER_DURABLES = 'Consumer Durables' 13 | CONSUMER_NONDURABLES = 'Consumer Non-Durables' 14 | CONSUMER_SERVICES = 'Consumer Services' 15 | DISTRIBUTION_SERVICES = 'Distribution Services' 16 | ELECTRONIC_TECHNOLOGY = 'Electronic Technology' 17 | ENERGY_MINERALS = 'Energy Minerals' 18 | FINANCE = 'Finance' 19 | GOVERNMENT = 'Government' 20 | HEALTH_SERVICES = 'Health Services' 21 | HEALTH_TECHNOLOGY = 'Health Technology' 22 | INDUSTRIAL_SERVICES = 'Industrial Services' 23 | MISCELLANEOUS = 'Miscellaneous' 24 | NONENERGY_MINERALS = 'Non-Energy Minerals' 25 | PROCESS_INDUSTRIES = 'Process Industries' 26 | PRODUCER_MANUFACTURING = 'Producer Manufacturing' 27 | RETAIL_TRADE = 'Retail Trade' 28 | TECHNOLOGY_SERVICES = 'Technology Services' 29 | TRANSPORTATION = 'Transportation' 30 | UTILITIES = 'Utilities' 31 | -------------------------------------------------------------------------------- /.dev/codegen/data/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "Index": [ 3 | "Any", 4 | "Dow Jones Composite Average", 5 | "Dow Jones Industrial Average", 6 | "Dow Jones Transportation Average", 7 | "Dow Jones Utility Average", 8 | "KBW NASDAQ BANK INDEX", 9 | "MINI RUSSELL 2000 INDEX", 10 | "NASDAQ 100", 11 | "NASDAQ 100 TECHNOLOGY SECTOR", 12 | "NASDAQ BANK", 13 | "NASDAQ BIOTECHNOLOGY", 14 | "NASDAQ COMPOSITE", 15 | "NASDAQ COMPUTER", 16 | "NASDAQ GOLDEN DRAGON CHINA INDEX", 17 | "NASDAQ INDUSTRIAL", 18 | "NASDAQ INSURANCE", 19 | "NASDAQ OTHER FINANCE", 20 | "NASDAQ TELECOMMUNICATIONS", 21 | "NASDAQ TRANSPORTATION", 22 | "NASDAQ US BENCHMARK FOOD PRODUCERS INDEX", 23 | "NYSE ARCA MAJOR MARKET", 24 | "PHLX GOLD AND SILVER SECTOR INDEX", 25 | "PHLX HOUSING SECTOR", 26 | "PHLX OIL SERVICE SECTOR", 27 | "PHLX SEMICONDUCTOR", 28 | "PHLX UTILITY SECTOR", 29 | "RUSSELL 1000", 30 | "RUSSELL 2000", 31 | "RUSSELL 3000", 32 | "S&P 100", 33 | "S&P 400", 34 | "S&P 500", 35 | "S&P 500 Communication Services", 36 | "S&P 500 Consumer Discretionary", 37 | "S&P 500 Consumer Staples", 38 | "S&P 500 ESG INDEX", 39 | "S&P 500 Energy", 40 | "S&P 500 Financials", 41 | "S&P 500 Health Care", 42 | "S&P 500 Industrials", 43 | "S&P 500 Information Technology", 44 | "S&P 500 Materials", 45 | "S&P 500 Real Estate", 46 | "S&P 500 Utilities" 47 | ] 48 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "tvscreener" 7 | version = "0.1.0" 8 | description = "TradingView Screener API" 9 | readme = "README.md" 10 | authors = [ 11 | { name = "DeepEntropy" } 12 | ] 13 | license = "MIT" 14 | requires-python = ">=3.10" 15 | keywords = ["finance", "tradingview", "technical-analysis"] 16 | dependencies = [ 17 | "pandas>=1.3.0", 18 | "requests>=2.27.1" 19 | ] 20 | classifiers = [ 21 | "Development Status :: 4 - Beta", 22 | "Intended Audience :: Developers", 23 | "Intended Audience :: Financial and Insurance Industry", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | "Operating System :: OS Independent", 29 | "Topic :: Office/Business :: Financial", 30 | "Topic :: Software Development :: Libraries :: Python Modules" 31 | ] 32 | 33 | [project.urls] 34 | Homepage = "https://github.com/deepentropy/tvscreener" 35 | Repository = "https://github.com/deepentropy/tvscreener" 36 | Documentation = "https://github.com/deepentropy/tvscreener" 37 | "Bug Tracker" = "https://github.com/deepentropy/tvscreener/issues" 38 | 39 | [project.optional-dependencies] 40 | dev = [ 41 | "pytest>=7.0.0", 42 | "pytest-cov>=4.0.0", 43 | "build>=1.0.0", 44 | "twine>=4.0.0" 45 | ] 46 | 47 | [tool.setuptools.packages.find] 48 | include = ["tvscreener*"] 49 | exclude = ["notebooks", "scripts", "tests"] 50 | 51 | [tool.setuptools.package-data] 52 | tvscreener = ["py.typed"] 53 | -------------------------------------------------------------------------------- /.dev/codegen/data/country.json: -------------------------------------------------------------------------------- 1 | { 2 | "Country": [ 3 | "Any", 4 | "Albania", 5 | "Argentina", 6 | "Australia", 7 | "Austria", 8 | "Azerbaijan", 9 | "Bahamas", 10 | "Barbados", 11 | "Belgium", 12 | "Bermuda", 13 | "Brazil", 14 | "British Virgin Islands", 15 | "Cambodia", 16 | "Canada", 17 | "Cayman Islands", 18 | "Chile", 19 | "China", 20 | "Colombia", 21 | "Costa Rica", 22 | "Cyprus", 23 | "Czech Republic", 24 | "Denmark", 25 | "Dominican Republic", 26 | "Egypt", 27 | "Faroe Islands", 28 | "Finland", 29 | "France", 30 | "Germany", 31 | "Gibraltar", 32 | "Greece", 33 | "Hong Kong", 34 | "Hungary", 35 | "Iceland", 36 | "India", 37 | "Indonesia", 38 | "Ireland", 39 | "Israel", 40 | "Italy", 41 | "Jamaica", 42 | "Japan", 43 | "Jordan", 44 | "Kazakhstan", 45 | "Luxembourg", 46 | "Macau", 47 | "Macedonia", 48 | "Malaysia", 49 | "Malta", 50 | "Mauritius", 51 | "Mexico", 52 | "Monaco", 53 | "Mongolia", 54 | "Montenegro", 55 | "Netherlands", 56 | "New Zealand", 57 | "Norway", 58 | "Panama", 59 | "Peru", 60 | "Philippines", 61 | "Poland", 62 | "Portugal", 63 | "Puerto Rico", 64 | "Romania", 65 | "Russian Federation", 66 | "Singapore", 67 | "South Africa", 68 | "South Korea", 69 | "Spain", 70 | "Sweden", 71 | "Switzerland", 72 | "Taiwan", 73 | "Tanzania", 74 | "Thailand", 75 | "Turkey", 76 | "U.S. Virgin Islands", 77 | "United Arab Emirates", 78 | "United Kingdom", 79 | "United States", 80 | "Uruguay", 81 | "Vietnam" 82 | ] 83 | } -------------------------------------------------------------------------------- /.github/workflows/pip-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Python package to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Triggers on version tags like v1.0.0 7 | 8 | jobs: 9 | build: 10 | name: Build distribution 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read # Minimal permissions for checking out code 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.10" 23 | 24 | - name: Install build dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install build 28 | 29 | - name: Build package 30 | run: python -m build 31 | 32 | - name: Test built package 33 | run: | 34 | pip install dist/*.whl 35 | python -c "import tvscreener; print('Package imported successfully!')" 36 | 37 | - name: Store the distribution packages 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: python-package-distributions 41 | path: dist/ 42 | 43 | publish-to-pypi: 44 | name: Publish to PyPI 45 | needs: 46 | - build 47 | runs-on: ubuntu-latest 48 | environment: 49 | name: pypi 50 | url: https://pypi.org/p/tvscreener 51 | permissions: 52 | id-token: write # IMPORTANT: mandatory for trusted publishing 53 | 54 | steps: 55 | - name: Download all the dists 56 | uses: actions/download-artifact@v4 57 | with: 58 | name: python-package-distributions 59 | path: dist/ 60 | 61 | - name: Publish distribution to PyPI 62 | uses: pypa/gh-action-pypi-publish@release/v1 63 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Requirements 4 | 5 | - Python 3.8 or higher 6 | - pandas 7 | - requests 8 | 9 | ## Install from PyPI 10 | 11 | ```bash 12 | pip install tvscreener 13 | ``` 14 | 15 | ## Install from Source 16 | 17 | ```bash 18 | git clone https://github.com/deepentropy/tvscreener.git 19 | cd tvscreener 20 | pip install -e . 21 | ``` 22 | 23 | ## Verify Installation 24 | 25 | ```python 26 | import tvscreener as tvs 27 | 28 | ss = tvs.StockScreener() 29 | df = ss.get() 30 | print(f"Retrieved {len(df)} stocks") 31 | ``` 32 | 33 | ## Upgrade 34 | 35 | ```bash 36 | pip install --upgrade tvscreener 37 | ``` 38 | 39 | ## Dependencies 40 | 41 | tvscreener automatically installs these dependencies: 42 | 43 | | Package | Purpose | 44 | |---------|---------| 45 | | `pandas` | DataFrames for results | 46 | | `requests` | HTTP requests to TradingView | 47 | 48 | ## Optional Dependencies 49 | 50 | For styled output (colored tables): 51 | 52 | ```bash 53 | pip install rich 54 | ``` 55 | 56 | For Jupyter notebook integration: 57 | 58 | ```bash 59 | pip install ipywidgets 60 | ``` 61 | 62 | ## Troubleshooting 63 | 64 | ### Import Error 65 | 66 | If you get an import error, ensure you're using Python 3.8+: 67 | 68 | ```bash 69 | python --version 70 | ``` 71 | 72 | ### Connection Issues 73 | 74 | tvscreener connects to TradingView's public API. If you experience timeouts: 75 | 76 | 1. Check your internet connection 77 | 2. Try again (TradingView may be rate-limiting) 78 | 3. Use a VPN if TradingView is blocked in your region 79 | 80 | ### No Data Returned 81 | 82 | If `ss.get()` returns an empty DataFrame: 83 | 84 | 1. Check your filters - they may be too restrictive 85 | 2. Verify field names are correct (use autocomplete in your IDE) 86 | 3. Try without filters first: `StockScreener().get()` 87 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | # Deploy Code Generator app and Documentation to GitHub Pages 2 | name: Deploy to GitHub Pages 3 | 4 | on: 5 | # Run on pushes to main that affect app or docs 6 | push: 7 | branches: ["main"] 8 | paths: 9 | - "app/**" 10 | - "docs/**" 11 | - "mkdocs.yml" 12 | - ".github/workflows/deploy-pages.yml" 13 | 14 | # Allow manual trigger 15 | workflow_dispatch: 16 | 17 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 18 | permissions: 19 | contents: read 20 | pages: write 21 | id-token: write 22 | 23 | # Allow only one concurrent deployment 24 | concurrency: 25 | group: "pages" 26 | cancel-in-progress: false 27 | 28 | jobs: 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: '3.11' 39 | 40 | - name: Install MkDocs and dependencies 41 | run: | 42 | pip install mkdocs-material mkdocstrings[python] mkdocs-git-revision-date-localized-plugin 43 | 44 | - name: Build documentation 45 | run: mkdocs build --site-dir site/docs 46 | 47 | - name: Copy app to site 48 | run: | 49 | mkdir -p site 50 | cp -r app/* site/ 51 | 52 | - name: Setup Pages 53 | uses: actions/configure-pages@v4 54 | 55 | - name: Upload artifact 56 | uses: actions/upload-pages-artifact@v3 57 | with: 58 | path: './site' 59 | 60 | deploy: 61 | environment: 62 | name: github-pages 63 | url: ${{ steps.deployment.outputs.page_url }} 64 | runs-on: ubuntu-latest 65 | needs: build 66 | steps: 67 | - name: Deploy to GitHub Pages 68 | id: deployment 69 | uses: actions/deploy-pages@v4 70 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to tvscreener. 4 | 5 | ## [0.1.0] - 2024 6 | 7 | ### Added 8 | 9 | - **Pythonic Comparison Operators** - Use `>`, `<`, `>=`, `<=`, `==`, `!=` directly on fields 10 | ```python 11 | ss.where(StockField.PRICE > 100) 12 | ss.where(StockField.VOLUME >= 1_000_000) 13 | ``` 14 | 15 | - **Range Methods** - `between()` and `not_between()` for range filtering 16 | ```python 17 | ss.where(StockField.PE_RATIO_TTM.between(10, 25)) 18 | ss.where(StockField.RSI.not_between(30, 70)) 19 | ``` 20 | 21 | - **List Methods** - `isin()` and `not_in()` for list matching 22 | ```python 23 | ss.where(StockField.SECTOR.isin(['Technology', 'Healthcare'])) 24 | ``` 25 | 26 | - **Fluent API** - All methods return `self` for chaining 27 | ```python 28 | df = StockScreener().select(...).where(...).sort_by(...).get() 29 | ``` 30 | 31 | - **Index Filtering** - Filter by index constituents 32 | ```python 33 | ss.set_index(IndexSymbol.SP500) 34 | ``` 35 | 36 | - **Select All** - Retrieve all ~3,500 fields 37 | ```python 38 | ss.select_all() 39 | ``` 40 | 41 | - **Time Intervals** - Use indicators on multiple timeframes 42 | ```python 43 | rsi_1h = StockField.RSI.with_interval('60') 44 | ``` 45 | 46 | - **All 6 Screeners** - Stock, Crypto, Forex, Bond, Futures, Coin 47 | 48 | ### Changed 49 | 50 | - Improved type safety with Field enums 51 | - Better error messages for invalid field types 52 | 53 | ### Backward Compatibility 54 | 55 | - Legacy `where(field, operator, value)` syntax still supported 56 | - All existing code continues to work 57 | 58 | --- 59 | 60 | ## [0.0.x] - Previous Versions 61 | 62 | Initial releases with basic screening functionality. 63 | 64 | ### Features 65 | 66 | - Basic screener classes 67 | - Filter by field and operator 68 | - Select specific fields 69 | - Sorting and pagination 70 | - Streaming updates 71 | - Styled output with `beautify()` 72 | -------------------------------------------------------------------------------- /tests/unit/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import math 3 | 4 | from tvscreener import StockField, get_columns_to_request, get_recommendation, millify 5 | from tvscreener.util import _is_nan 6 | 7 | 8 | class TestUtil(unittest.TestCase): 9 | 10 | def test_get_columns_type(self): 11 | columns = get_columns_to_request(StockField) 12 | self.assertIsInstance(columns, dict) 13 | # StockField has 3509 fields after expanding technical indicators with intervals 14 | self.assertGreater(len(columns), 3000) 15 | 16 | def test_get_columns_len(self): 17 | columns = get_columns_to_request(StockField) 18 | self.assertIsInstance(columns, dict) 19 | 20 | def test_get_recommendation(self): 21 | self.assertEqual("S", get_recommendation(-1)) 22 | self.assertEqual("N", get_recommendation(0)) 23 | self.assertEqual("B", get_recommendation(1)) 24 | 25 | def test_millify(self): 26 | self.assertEqual("1.000M", millify(10 ** 6)) 27 | self.assertEqual("10.000M", millify(10 ** 7)) 28 | self.assertEqual("1.000B", millify(10 ** 9)) 29 | 30 | def test_millify_thousands(self): 31 | self.assertEqual("1.000K", millify(10 ** 3)) 32 | self.assertEqual("10.000K", millify(10 ** 4)) 33 | self.assertEqual("100.000K", millify(10 ** 5)) 34 | 35 | def test_millify_negative(self): 36 | self.assertEqual("-1.000M", millify(-10 ** 6)) 37 | self.assertEqual("-1.000K", millify(-10 ** 3)) 38 | self.assertEqual("-1.000B", millify(-10 ** 9)) 39 | 40 | def test_millify_small(self): 41 | self.assertEqual("100.000", millify(100)) 42 | self.assertEqual("1.000", millify(1)) 43 | 44 | def test_is_nan_with_nan(self): 45 | self.assertTrue(_is_nan(float('nan'))) 46 | self.assertTrue(_is_nan(math.nan)) 47 | 48 | def test_is_nan_with_number(self): 49 | self.assertFalse(_is_nan(1)) 50 | self.assertFalse(_is_nan(0)) 51 | self.assertFalse(_is_nan(-1)) 52 | self.assertFalse(_is_nan(1.5)) 53 | 54 | def test_is_nan_with_string(self): 55 | self.assertFalse(_is_nan("hello")) 56 | self.assertFalse(_is_nan("")) 57 | 58 | def test_is_nan_with_none(self): 59 | self.assertFalse(_is_nan(None)) 60 | -------------------------------------------------------------------------------- /tests/unit/test_fields.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tvscreener import StockField 4 | 5 | 6 | class TestFields(unittest.TestCase): 7 | 8 | def test_recommendation(self): 9 | self.assertTrue(StockField.BULL_BEAR_POWER.has_recommendation()) 10 | 11 | def test_historical(self): 12 | self.assertTrue(StockField.NEGATIVE_DIRECTIONAL_INDICATOR_14.historical) 13 | self.assertTrue(StockField.COMMODITY_CHANNEL_INDEX_20.historical) 14 | self.assertTrue(StockField.MOMENTUM_10.historical) 15 | 16 | def test_interval(self): 17 | self.assertTrue(StockField.VOLUME.interval) 18 | self.assertTrue(StockField.AVERAGE_DIRECTIONAL_INDEX_14.interval) 19 | self.assertTrue(StockField.NEGATIVE_DIRECTIONAL_INDICATOR_14.interval) 20 | self.assertTrue(StockField.POSITIVE_DIRECTIONAL_INDICATOR_14.interval) 21 | 22 | def test_format_computed_reco(self): 23 | self.assertEqual("computed_recommendation", StockField.AVERAGE_DIRECTIONAL_INDEX_14.format) 24 | self.assertEqual("computed_recommendation", StockField.BOLLINGER_LOWER_BAND_20.format) 25 | self.assertEqual("computed_recommendation", StockField.BOLLINGER_UPPER_BAND_20.format) 26 | self.assertEqual("computed_recommendation", StockField.SIMPLE_MOVING_AVERAGE_10.format) 27 | 28 | def test_rec_label(self): 29 | self.assertEqual(None, StockField.AVERAGE_DIRECTIONAL_INDEX_14.get_rec_label()) 30 | self.assertEqual("Reco. Bull Bear Power", StockField.BULL_BEAR_POWER.get_rec_label()) 31 | self.assertEqual("Reco. Hull Moving Average (9)", StockField.HULL_MOVING_AVERAGE_9.get_rec_label()) 32 | self.assertEqual("Reco. Ultimate Oscillator (7, 14, 28)", 33 | StockField.ULTIMATE_OSCILLATOR_7_14_28.get_rec_label()) 34 | 35 | def test_get_rec_field(self): 36 | self.assertEqual(None, StockField.AVERAGE_DIRECTIONAL_INDEX_14.get_rec_field()) 37 | self.assertEqual("Rec.BBPower", StockField.BULL_BEAR_POWER.get_rec_field()) 38 | self.assertEqual("Rec.HullMA9", StockField.HULL_MOVING_AVERAGE_9.get_rec_field()) 39 | self.assertEqual("Rec.UO", StockField.ULTIMATE_OSCILLATOR_7_14_28.get_rec_field()) 40 | 41 | def test_get_by_label(self): 42 | self.assertEqual(StockField.VOLUME, StockField.get_by_label(StockField, StockField.VOLUME.label)) 43 | -------------------------------------------------------------------------------- /tvscreener/filter.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from tvscreener.field import Field 4 | 5 | 6 | class FilterOperator(Enum): 7 | BELOW = "less" 8 | BELOW_OR_EQUAL = "eless" 9 | ABOVE = "greater" 10 | ABOVE_OR_EQUAL = "egreater" 11 | CROSSES = "crosses" 12 | CROSSES_UP = "crosses_above" 13 | CROSSES_DOWN = "crosses_below" 14 | IN_RANGE = "in_range" 15 | NOT_IN_RANGE = "not_in_range" 16 | EQUAL = "equal" 17 | NOT_EQUAL = "nequal" 18 | MATCH = "match" 19 | 20 | 21 | class ExtraFilter(Enum): 22 | CURRENT_TRADING_DAY = "active_symbol" 23 | SEARCH = "name,description" 24 | PRIMARY = "is_primary" 25 | 26 | def __init__(self, value): 27 | self.field_name = value 28 | 29 | 30 | class FieldCondition: 31 | """ 32 | Represents a comparison condition on a field. 33 | 34 | This class enables Pythonic comparison syntax like: 35 | StockField.PRICE > 100 36 | StockField.VOLUME.between(1e6, 10e6) 37 | StockField.SECTOR == 'Technology' 38 | 39 | Example: 40 | >>> condition = StockField.PRICE > 100 41 | >>> ss.where(condition) 42 | """ 43 | 44 | def __init__(self, field, operation: FilterOperator, value): 45 | self.field = field 46 | self.operation = operation 47 | self.value = value 48 | 49 | def to_filter(self) -> 'Filter': 50 | """Convert this condition to a Filter object.""" 51 | return Filter(self.field, self.operation, self.value) 52 | 53 | def __repr__(self): 54 | return f"FieldCondition({self.field.name}, {self.operation.name}, {self.value})" 55 | 56 | 57 | class Filter: 58 | def __init__(self, field: Field | ExtraFilter, operation: FilterOperator, values): 59 | self.field = field 60 | self.operation = operation 61 | self.values = values if isinstance(values, list) else [values] 62 | 63 | # def name(self): 64 | # return self.field.field_name if isinstance(self.field, Field) else self.field.value 65 | 66 | def to_dict(self): 67 | right = [filter_enum.value if isinstance(filter_enum, Enum) else filter_enum for filter_enum in self.values] 68 | right = right[0] if len(right) == 1 else right 69 | left = self.field.field_name 70 | return {"left": left, "operation": self.operation.value, "right": right} 71 | -------------------------------------------------------------------------------- /.dev/codegen/code/index.py.generated: -------------------------------------------------------------------------------- 1 | 2 | # ---------------------- 3 | # Generated file 4 | # 2023-08-11 07:05:13.155095 5 | # ---------------------- 6 | from enum import Enum 7 | 8 | 9 | class Index(Enum): 10 | DOW_JONES_COMPOSITE_AVERAGE = 'Dow Jones Composite Average' 11 | DOW_JONES_INDUSTRIAL_AVERAGE = 'Dow Jones Industrial Average' 12 | DOW_JONES_TRANSPORTATION_AVERAGE = 'Dow Jones Transportation Average' 13 | DOW_JONES_UTILITY_AVERAGE = 'Dow Jones Utility Average' 14 | KBW_NASDAQ_BANK_INDEX = 'KBW NASDAQ BANK INDEX' 15 | MINI_RUSSELL_2000_INDEX = 'MINI RUSSELL 2000 INDEX' 16 | NASDAQ_100 = 'NASDAQ 100' 17 | NASDAQ_100_TECHNOLOGY_SECTOR = 'NASDAQ 100 TECHNOLOGY SECTOR' 18 | NASDAQ_BANK = 'NASDAQ BANK' 19 | NASDAQ_BIOTECHNOLOGY = 'NASDAQ BIOTECHNOLOGY' 20 | NASDAQ_COMPOSITE = 'NASDAQ COMPOSITE' 21 | NASDAQ_COMPUTER = 'NASDAQ COMPUTER' 22 | NASDAQ_GOLDEN_DRAGON_CHINA_INDEX = 'NASDAQ GOLDEN DRAGON CHINA INDEX' 23 | NASDAQ_INDUSTRIAL = 'NASDAQ INDUSTRIAL' 24 | NASDAQ_INSURANCE = 'NASDAQ INSURANCE' 25 | NASDAQ_OTHER_FINANCE = 'NASDAQ OTHER FINANCE' 26 | NASDAQ_TELECOMMUNICATIONS = 'NASDAQ TELECOMMUNICATIONS' 27 | NASDAQ_TRANSPORTATION = 'NASDAQ TRANSPORTATION' 28 | NASDAQ_US_BENCHMARK_FOOD_PRODUCERS_INDEX = 'NASDAQ US BENCHMARK FOOD PRODUCERS INDEX' 29 | NYSE_ARCA_MAJOR_MARKET = 'NYSE ARCA MAJOR MARKET' 30 | PHLX_GOLD_AND_SILVER_SECTOR_INDEX = 'PHLX GOLD AND SILVER SECTOR INDEX' 31 | PHLX_HOUSING_SECTOR = 'PHLX HOUSING SECTOR' 32 | PHLX_OIL_SERVICE_SECTOR = 'PHLX OIL SERVICE SECTOR' 33 | PHLX_SEMICONDUCTOR = 'PHLX SEMICONDUCTOR' 34 | PHLX_UTILITY_SECTOR = 'PHLX UTILITY SECTOR' 35 | RUSSELL_1000 = 'RUSSELL 1000' 36 | RUSSELL_2000 = 'RUSSELL 2000' 37 | RUSSELL_3000 = 'RUSSELL 3000' 38 | SANDP_100 = 'S&P 100' 39 | SANDP_400 = 'S&P 400' 40 | SANDP_500 = 'S&P 500' 41 | SANDP_500_COMMUNICATION_SERVICES = 'S&P 500 Communication Services' 42 | SANDP_500_CONSUMER_DISCRETIONARY = 'S&P 500 Consumer Discretionary' 43 | SANDP_500_CONSUMER_STAPLES = 'S&P 500 Consumer Staples' 44 | SANDP_500_ESG_INDEX = 'S&P 500 ESG INDEX' 45 | SANDP_500_ENERGY = 'S&P 500 Energy' 46 | SANDP_500_FINANCIALS = 'S&P 500 Financials' 47 | SANDP_500_HEALTH_CARE = 'S&P 500 Health Care' 48 | SANDP_500_INDUSTRIALS = 'S&P 500 Industrials' 49 | SANDP_500_INFORMATION_TECHNOLOGY = 'S&P 500 Information Technology' 50 | SANDP_500_MATERIALS = 'S&P 500 Materials' 51 | SANDP_500_REAL_ESTATE = 'S&P 500 Real Estate' 52 | SANDP_500_UTILITIES = 'S&P 500 Utilities' 53 | -------------------------------------------------------------------------------- /.dev/codegen/code/country.py.generated: -------------------------------------------------------------------------------- 1 | 2 | # ---------------------- 3 | # Generated file 4 | # 2023-08-11 07:05:13.156337 5 | # ---------------------- 6 | from enum import Enum 7 | 8 | 9 | class Country(Enum): 10 | ALBANIA = 'Albania' 11 | ARGENTINA = 'Argentina' 12 | AUSTRALIA = 'Australia' 13 | AUSTRIA = 'Austria' 14 | AZERBAIJAN = 'Azerbaijan' 15 | BAHAMAS = 'Bahamas' 16 | BARBADOS = 'Barbados' 17 | BELGIUM = 'Belgium' 18 | BERMUDA = 'Bermuda' 19 | BRAZIL = 'Brazil' 20 | BRITISH_VIRGIN_ISLANDS = 'British Virgin Islands' 21 | CAMBODIA = 'Cambodia' 22 | CANADA = 'Canada' 23 | CAYMAN_ISLANDS = 'Cayman Islands' 24 | CHILE = 'Chile' 25 | CHINA = 'China' 26 | COLOMBIA = 'Colombia' 27 | COSTA_RICA = 'Costa Rica' 28 | CYPRUS = 'Cyprus' 29 | CZECH_REPUBLIC = 'Czech Republic' 30 | DENMARK = 'Denmark' 31 | DOMINICAN_REPUBLIC = 'Dominican Republic' 32 | EGYPT = 'Egypt' 33 | FAROE_ISLANDS = 'Faroe Islands' 34 | FINLAND = 'Finland' 35 | FRANCE = 'France' 36 | GERMANY = 'Germany' 37 | GIBRALTAR = 'Gibraltar' 38 | GREECE = 'Greece' 39 | HONG_KONG = 'Hong Kong' 40 | HUNGARY = 'Hungary' 41 | ICELAND = 'Iceland' 42 | INDIA = 'India' 43 | INDONESIA = 'Indonesia' 44 | IRELAND = 'Ireland' 45 | ISRAEL = 'Israel' 46 | ITALY = 'Italy' 47 | JAMAICA = 'Jamaica' 48 | JAPAN = 'Japan' 49 | JORDAN = 'Jordan' 50 | KAZAKHSTAN = 'Kazakhstan' 51 | LUXEMBOURG = 'Luxembourg' 52 | MACAU = 'Macau' 53 | MACEDONIA = 'Macedonia' 54 | MALAYSIA = 'Malaysia' 55 | MALTA = 'Malta' 56 | MAURITIUS = 'Mauritius' 57 | MEXICO = 'Mexico' 58 | MONACO = 'Monaco' 59 | MONGOLIA = 'Mongolia' 60 | MONTENEGRO = 'Montenegro' 61 | NETHERLANDS = 'Netherlands' 62 | NEW_ZEALAND = 'New Zealand' 63 | NORWAY = 'Norway' 64 | PANAMA = 'Panama' 65 | PERU = 'Peru' 66 | PHILIPPINES = 'Philippines' 67 | POLAND = 'Poland' 68 | PORTUGAL = 'Portugal' 69 | PUERTO_RICO = 'Puerto Rico' 70 | ROMANIA = 'Romania' 71 | RUSSIAN_FEDERATION = 'Russian Federation' 72 | SINGAPORE = 'Singapore' 73 | SOUTH_AFRICA = 'South Africa' 74 | SOUTH_KOREA = 'South Korea' 75 | SPAIN = 'Spain' 76 | SWEDEN = 'Sweden' 77 | SWITZERLAND = 'Switzerland' 78 | TAIWAN = 'Taiwan' 79 | TANZANIA = 'Tanzania' 80 | THAILAND = 'Thailand' 81 | TURKEY = 'Turkey' 82 | U_S__VIRGIN_ISLANDS = 'U.S. Virgin Islands' 83 | UNITED_ARAB_EMIRATES = 'United Arab Emirates' 84 | UNITED_KINGDOM = 'United Kingdom' 85 | UNITED_STATES = 'United States' 86 | URUGUAY = 'Uruguay' 87 | VIETNAM = 'Vietnam' 88 | -------------------------------------------------------------------------------- /docs/getting-started/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick Start Guide 2 | 3 | Get up and running with tvscreener in 5 minutes. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | pip install tvscreener 9 | ``` 10 | 11 | ## Basic Usage 12 | 13 | ```python 14 | import tvscreener as tvs 15 | 16 | # Create a screener and get data 17 | ss = tvs.StockScreener() 18 | df = ss.get() # Returns pandas DataFrame with 150 stocks 19 | ``` 20 | 21 | ## Filtering Stocks 22 | 23 | Use Python comparison operators directly on fields: 24 | 25 | ```python 26 | from tvscreener import StockScreener, StockField 27 | 28 | ss = StockScreener() 29 | 30 | # Filter by price, volume, and market cap 31 | ss.where(StockField.PRICE > 10) 32 | ss.where(StockField.VOLUME >= 1_000_000) 33 | ss.where(StockField.MARKET_CAPITALIZATION.between(1e9, 50e9)) 34 | 35 | df = ss.get() 36 | ``` 37 | 38 | ## Selecting Fields 39 | 40 | Choose which data columns to retrieve: 41 | 42 | ```python 43 | ss = StockScreener() 44 | ss.select( 45 | StockField.NAME, 46 | StockField.PRICE, 47 | StockField.CHANGE_PERCENT, 48 | StockField.VOLUME, 49 | StockField.PE_RATIO_TTM 50 | ) 51 | df = ss.get() 52 | ``` 53 | 54 | Or get all ~3,500 available fields: 55 | 56 | ```python 57 | ss = StockScreener() 58 | ss.select_all() 59 | df = ss.get() 60 | ``` 61 | 62 | ## Index Constituents 63 | 64 | Filter to stocks in major indices: 65 | 66 | ```python 67 | from tvscreener import StockScreener, IndexSymbol 68 | 69 | ss = StockScreener() 70 | ss.set_index(IndexSymbol.SP500) 71 | ss.set_range(0, 500) 72 | df = ss.get() # S&P 500 stocks only 73 | ``` 74 | 75 | ## Specific Symbols 76 | 77 | Query specific tickers: 78 | 79 | ```python 80 | ss = StockScreener() 81 | ss.symbols = { 82 | "query": {"types": []}, 83 | "tickers": ["NASDAQ:AAPL", "NASDAQ:MSFT", "NYSE:JPM"] 84 | } 85 | df = ss.get() 86 | ``` 87 | 88 | ## Other Screeners 89 | 90 | ```python 91 | import tvscreener as tvs 92 | 93 | # Forex 94 | fs = tvs.ForexScreener() 95 | df = fs.get() 96 | 97 | # Crypto 98 | cs = tvs.CryptoScreener() 99 | df = cs.get() 100 | 101 | # Bonds 102 | bs = tvs.BondScreener() 103 | df = bs.get() 104 | 105 | # Futures 106 | futs = tvs.FuturesScreener() 107 | df = futs.get() 108 | ``` 109 | 110 | ## Next Steps 111 | 112 | - [Filtering Guide](../guide/filtering.md) - Complete filtering reference 113 | - [Stock Screening Examples](../examples/stock-screening.md) - Value, momentum, dividend strategies 114 | - [Technical Analysis Examples](../examples/technical-analysis.md) - RSI, MACD, multi-timeframe 115 | - [API Reference](../api/fields.md) - All available fields 116 | -------------------------------------------------------------------------------- /tvscreener/__init__.py: -------------------------------------------------------------------------------- 1 | from .core.base import Screener, ScreenerDataFrame 2 | from .core.bond import BondScreener 3 | from .core.coin import CoinScreener 4 | from .core.crypto import CryptoScreener 5 | from .core.forex import ForexScreener 6 | from .core.futures import FuturesScreener 7 | from .core.stock import StockScreener 8 | from .exceptions import MalformedRequestException 9 | from .field import Field, FieldWithInterval, FieldWithHistory, IndexSymbol 10 | from .field import * 11 | from .field.stock import StockField 12 | from .field.forex import ForexField 13 | from .field.crypto import CryptoField 14 | from .field.bond import BondField 15 | from .field.futures import FuturesField 16 | from .field.coin import CoinField 17 | from .field.presets import ( 18 | get_preset, list_presets, 19 | STOCK_PRICE_FIELDS, STOCK_VOLUME_FIELDS, STOCK_VALUATION_FIELDS, 20 | STOCK_DIVIDEND_FIELDS, STOCK_PROFITABILITY_FIELDS, STOCK_PERFORMANCE_FIELDS, 21 | STOCK_OSCILLATOR_FIELDS, STOCK_MOVING_AVERAGE_FIELDS, STOCK_EARNINGS_FIELDS, 22 | CRYPTO_PRICE_FIELDS, CRYPTO_VOLUME_FIELDS, CRYPTO_PERFORMANCE_FIELDS, CRYPTO_TECHNICAL_FIELDS, 23 | FOREX_PRICE_FIELDS, FOREX_PERFORMANCE_FIELDS, FOREX_TECHNICAL_FIELDS, 24 | BOND_BASIC_FIELDS, BOND_YIELD_FIELDS, BOND_MATURITY_FIELDS, 25 | FUTURES_PRICE_FIELDS, FUTURES_TECHNICAL_FIELDS, 26 | COIN_PRICE_FIELDS, COIN_MARKET_FIELDS, 27 | ) 28 | from .filter import Filter, FilterOperator, ExtraFilter, FieldCondition 29 | from .util import * 30 | from .beauty import beautify 31 | 32 | __all__ = [ 33 | "Screener", "ScreenerDataFrame", 34 | "StockScreener", "ForexScreener", "CryptoScreener", 35 | "BondScreener", "FuturesScreener", "CoinScreener", 36 | "MalformedRequestException", 37 | "Field", "Filter", "FilterOperator", "ExtraFilter", "FieldCondition", 38 | "StockField", "ForexField", "CryptoField", "BondField", "FuturesField", "CoinField", 39 | "Market", "Exchange", "Country", "Sector", "Industry", "IndexSymbol", 40 | "beautify", 41 | # Field presets 42 | "get_preset", "list_presets", 43 | "STOCK_PRICE_FIELDS", "STOCK_VOLUME_FIELDS", "STOCK_VALUATION_FIELDS", 44 | "STOCK_DIVIDEND_FIELDS", "STOCK_PROFITABILITY_FIELDS", "STOCK_PERFORMANCE_FIELDS", 45 | "STOCK_OSCILLATOR_FIELDS", "STOCK_MOVING_AVERAGE_FIELDS", "STOCK_EARNINGS_FIELDS", 46 | "CRYPTO_PRICE_FIELDS", "CRYPTO_VOLUME_FIELDS", "CRYPTO_PERFORMANCE_FIELDS", "CRYPTO_TECHNICAL_FIELDS", 47 | "FOREX_PRICE_FIELDS", "FOREX_PERFORMANCE_FIELDS", "FOREX_TECHNICAL_FIELDS", 48 | "BOND_BASIC_FIELDS", "BOND_YIELD_FIELDS", "BOND_MATURITY_FIELDS", 49 | "FUTURES_PRICE_FIELDS", "FUTURES_TECHNICAL_FIELDS", 50 | "COIN_PRICE_FIELDS", "COIN_MARKET_FIELDS", 51 | ] 52 | -------------------------------------------------------------------------------- /tests/unit/test_stream.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | 4 | from tvscreener import StockScreener 5 | 6 | 7 | class TestStream(unittest.TestCase): 8 | 9 | @patch.object(StockScreener, 'get') 10 | def test_stream_max_iterations(self, mock_get): 11 | mock_get.return_value = MagicMock() 12 | ss = StockScreener() 13 | results = list(ss.stream(interval=1.0, max_iterations=3)) 14 | self.assertEqual(len(results), 3) 15 | self.assertEqual(mock_get.call_count, 3) 16 | 17 | @patch.object(StockScreener, 'get') 18 | @patch('tvscreener.core.base.time.sleep') 19 | def test_stream_minimum_interval(self, mock_sleep, mock_get): 20 | mock_get.return_value = MagicMock() 21 | ss = StockScreener() 22 | # Even if user passes 0.1, it should use minimum of 1.0 23 | list(ss.stream(interval=0.1, max_iterations=2)) 24 | # Should sleep with minimum interval of 1.0 25 | mock_sleep.assert_called_with(1.0) 26 | 27 | @patch.object(StockScreener, 'get') 28 | @patch('tvscreener.core.base.time.sleep') 29 | def test_stream_callback(self, mock_sleep, mock_get): 30 | mock_df = MagicMock() 31 | mock_get.return_value = mock_df 32 | callback = MagicMock() 33 | 34 | ss = StockScreener() 35 | list(ss.stream(interval=1.0, max_iterations=2, on_update=callback)) 36 | 37 | self.assertEqual(callback.call_count, 2) 38 | callback.assert_called_with(mock_df) 39 | 40 | @patch.object(StockScreener, 'get') 41 | @patch('tvscreener.core.base.time.sleep') 42 | def test_stream_handles_errors(self, mock_sleep, mock_get): 43 | mock_get.side_effect = [Exception("API Error"), MagicMock()] 44 | ss = StockScreener() 45 | results = list(ss.stream(interval=1.0, max_iterations=2)) 46 | self.assertEqual(len(results), 2) 47 | self.assertIsNone(results[0]) # First result is None due to error 48 | 49 | @patch.object(StockScreener, 'get') 50 | @patch('tvscreener.core.base.time.sleep') 51 | def test_stream_no_sleep_after_last_iteration(self, mock_sleep, mock_get): 52 | mock_get.return_value = MagicMock() 53 | ss = StockScreener() 54 | list(ss.stream(interval=1.0, max_iterations=1)) 55 | # Should not sleep after the last iteration 56 | mock_sleep.assert_not_called() 57 | 58 | @patch.object(StockScreener, 'get') 59 | @patch('tvscreener.core.base.time.sleep') 60 | def test_stream_custom_interval(self, mock_sleep, mock_get): 61 | mock_get.return_value = MagicMock() 62 | ss = StockScreener() 63 | list(ss.stream(interval=5.0, max_iterations=2)) 64 | # Should sleep with the custom interval 65 | mock_sleep.assert_called_with(5.0) 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /tests/unit/test_ta.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from tvscreener.field import Rating 4 | import tvscreener.ta as ta 5 | 6 | 7 | class TestTechnicalAnalysis(unittest.TestCase): 8 | 9 | def test_adx_buy(self): 10 | # +DI crosses above -DI with ADX > 20 11 | result = ta.adx(adx_value=25, dminus=10, dplus=15, dminus_old=12, dplus_old=8) 12 | self.assertEqual(Rating.BUY, result) 13 | 14 | def test_adx_sell(self): 15 | # -DI crosses above +DI with ADX > 20 16 | result = ta.adx(adx_value=25, dminus=15, dplus=10, dminus_old=8, dplus_old=12) 17 | self.assertEqual(Rating.SELL, result) 18 | 19 | def test_adx_neutral_low_adx(self): 20 | # ADX <= 20 should return neutral 21 | result = ta.adx(adx_value=18, dminus=10, dplus=15, dminus_old=12, dplus_old=8) 22 | self.assertEqual(Rating.NEUTRAL, result) 23 | 24 | def test_adx_neutral_no_cross(self): 25 | # No cross, should return neutral 26 | result = ta.adx(adx_value=25, dminus=10, dplus=15, dminus_old=10, dplus_old=15) 27 | self.assertEqual(Rating.NEUTRAL, result) 28 | 29 | def test_ao_bullish_cross(self): 30 | # AO crosses above zero 31 | result = ta.ao(ao_value=5, ao_old_1=-2, ao_old_2=-5) 32 | self.assertEqual(Rating.BUY, result) 33 | 34 | def test_ao_bearish_cross(self): 35 | # AO crosses below zero 36 | result = ta.ao(ao_value=-5, ao_old_1=2, ao_old_2=5) 37 | self.assertEqual(Rating.SELL, result) 38 | 39 | def test_ao_bullish_saucer(self): 40 | # Bullish saucer: AO > 0, green bar after red bars 41 | result = ta.ao(ao_value=10, ao_old_1=5, ao_old_2=8) 42 | self.assertEqual(Rating.BUY, result) 43 | 44 | def test_ao_bearish_saucer(self): 45 | # Bearish saucer: AO < 0, red bar after green bars 46 | result = ta.ao(ao_value=-10, ao_old_1=-5, ao_old_2=-8) 47 | self.assertEqual(Rating.SELL, result) 48 | 49 | def test_ao_neutral(self): 50 | # No pattern detected 51 | result = ta.ao(ao_value=5, ao_old_1=6, ao_old_2=7) 52 | self.assertEqual(Rating.NEUTRAL, result) 53 | 54 | def test_bb_lower_buy(self): 55 | # Close below lower band = buy signal 56 | result = ta.bb_lower(low_limit=100, close=95) 57 | self.assertEqual(Rating.BUY, result) 58 | 59 | def test_bb_lower_neutral(self): 60 | # Close above lower band = neutral 61 | result = ta.bb_lower(low_limit=100, close=105) 62 | self.assertEqual(Rating.NEUTRAL, result) 63 | 64 | def test_bb_upper_sell(self): 65 | # Close above upper band = sell signal 66 | result = ta.bb_upper(up_limit=100, close=105) 67 | self.assertEqual(Rating.SELL, result) 68 | 69 | def test_bb_upper_neutral(self): 70 | # Close below upper band = neutral 71 | result = ta.bb_upper(up_limit=100, close=95) 72 | self.assertEqual(Rating.NEUTRAL, result) 73 | 74 | 75 | if __name__ == '__main__': 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | /* Custom styles for tvscreener documentation */ 2 | 3 | /* TradingView-inspired accent color */ 4 | :root { 5 | --md-primary-fg-color: #2962ff; 6 | --md-primary-fg-color--light: #5c8bff; 7 | --md-primary-fg-color--dark: #0039cb; 8 | --md-accent-fg-color: #2962ff; 9 | } 10 | 11 | /* Dark mode adjustments */ 12 | [data-md-color-scheme="slate"] { 13 | --md-default-bg-color: #1e222d; 14 | --md-default-fg-color--light: #787b86; 15 | } 16 | 17 | /* Code block styling */ 18 | .md-typeset code { 19 | background-color: rgba(41, 98, 255, 0.1); 20 | border-radius: 4px; 21 | padding: 0.1em 0.3em; 22 | } 23 | 24 | .md-typeset pre code { 25 | background-color: transparent; 26 | padding: 0; 27 | } 28 | 29 | /* Table styling */ 30 | .md-typeset table:not([class]) { 31 | border-radius: 8px; 32 | overflow: hidden; 33 | } 34 | 35 | .md-typeset table:not([class]) th { 36 | background-color: rgba(41, 98, 255, 0.1); 37 | } 38 | 39 | /* Button styling */ 40 | .md-button { 41 | border-radius: 8px !important; 42 | font-weight: 600 !important; 43 | } 44 | 45 | .md-button--primary { 46 | background-color: #2962ff !important; 47 | } 48 | 49 | .md-button--primary:hover { 50 | background-color: #0039cb !important; 51 | } 52 | 53 | /* Card grid styling */ 54 | .grid.cards > ul > li { 55 | border-radius: 8px; 56 | transition: transform 0.2s, box-shadow 0.2s; 57 | } 58 | 59 | .grid.cards > ul > li:hover { 60 | transform: translateY(-2px); 61 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 62 | } 63 | 64 | /* Admonition styling */ 65 | .md-typeset .admonition, 66 | .md-typeset details { 67 | border-radius: 8px; 68 | } 69 | 70 | /* Navigation improvements */ 71 | .md-nav__link { 72 | font-weight: 500; 73 | } 74 | 75 | .md-nav__item--active > .md-nav__link { 76 | color: #2962ff; 77 | } 78 | 79 | /* Footer link color */ 80 | .md-footer-meta__inner a { 81 | color: #2962ff; 82 | } 83 | 84 | /* Search highlight */ 85 | .md-search-result mark { 86 | background-color: rgba(41, 98, 255, 0.3); 87 | } 88 | 89 | /* Copy button styling */ 90 | .md-clipboard { 91 | border-radius: 4px; 92 | } 93 | 94 | /* Mobile responsiveness */ 95 | @media screen and (max-width: 76.1875em) { 96 | .md-nav--primary .md-nav__title { 97 | background-color: #2962ff; 98 | } 99 | } 100 | 101 | /* Feature highlight boxes */ 102 | .feature-box { 103 | background: linear-gradient(135deg, rgba(41, 98, 255, 0.1), rgba(41, 98, 255, 0.05)); 104 | border-radius: 12px; 105 | padding: 1.5rem; 106 | margin: 1rem 0; 107 | } 108 | 109 | /* Badge styling */ 110 | .badge { 111 | display: inline-block; 112 | padding: 0.25em 0.6em; 113 | font-size: 0.75em; 114 | font-weight: 600; 115 | border-radius: 4px; 116 | background-color: #2962ff; 117 | color: white; 118 | } 119 | 120 | .badge--new { 121 | background-color: #00c853; 122 | } 123 | 124 | .badge--beta { 125 | background-color: #ff6d00; 126 | } 127 | -------------------------------------------------------------------------------- /tests/unit/test_beauty.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pandas as pd 3 | import math 4 | 5 | from tvscreener import beautify, StockField 6 | from tvscreener.beauty import ( 7 | _get_recommendation, _percent_colors, _rating_colors, _rating_letter, 8 | BUY_CHAR, SELL_CHAR, NEUTRAL_CHAR, 9 | COLOR_RED_NEGATIVE, COLOR_GREEN_POSITIVE, COLOR_BLUE_BUY, COLOR_RED_SELL, COLOR_GRAY_NEUTRAL 10 | ) 11 | from tvscreener.field import Rating 12 | 13 | 14 | class TestBeautifyHelpers(unittest.TestCase): 15 | 16 | def test_get_recommendation_positive(self): 17 | self.assertEqual(Rating.BUY, _get_recommendation(1)) 18 | self.assertEqual(Rating.BUY, _get_recommendation(0.5)) 19 | self.assertEqual(Rating.BUY, _get_recommendation(100)) 20 | 21 | def test_get_recommendation_negative(self): 22 | self.assertEqual(Rating.SELL, _get_recommendation(-1)) 23 | self.assertEqual(Rating.SELL, _get_recommendation(-0.5)) 24 | self.assertEqual(Rating.SELL, _get_recommendation(-100)) 25 | 26 | def test_get_recommendation_neutral(self): 27 | self.assertEqual(Rating.NEUTRAL, _get_recommendation(0)) 28 | 29 | def test_percent_colors_positive(self): 30 | self.assertEqual(COLOR_GREEN_POSITIVE, _percent_colors("1.50%")) 31 | self.assertEqual(COLOR_GREEN_POSITIVE, _percent_colors("0.00%")) 32 | 33 | def test_percent_colors_negative(self): 34 | self.assertEqual(COLOR_RED_NEGATIVE, _percent_colors("-1.50%")) 35 | self.assertEqual(COLOR_RED_NEGATIVE, _percent_colors("-0.01%")) 36 | 37 | def test_rating_colors_buy(self): 38 | self.assertEqual(COLOR_BLUE_BUY, _rating_colors(f"1.5 {BUY_CHAR}")) 39 | 40 | def test_rating_colors_sell(self): 41 | self.assertEqual(COLOR_RED_SELL, _rating_colors(f"1.5 {SELL_CHAR}")) 42 | 43 | def test_rating_colors_neutral(self): 44 | self.assertEqual(COLOR_GRAY_NEUTRAL, _rating_colors(f"1.5 {NEUTRAL_CHAR}")) 45 | 46 | def test_rating_colors_non_string(self): 47 | """Test that non-string values return neutral color.""" 48 | self.assertEqual(COLOR_GRAY_NEUTRAL, _rating_colors(123)) 49 | self.assertEqual(COLOR_GRAY_NEUTRAL, _rating_colors(None)) 50 | self.assertEqual(COLOR_GRAY_NEUTRAL, _rating_colors(1.5)) 51 | 52 | def test_rating_letter_buy(self): 53 | self.assertEqual(BUY_CHAR, _rating_letter(Rating.BUY)) 54 | self.assertEqual(BUY_CHAR, _rating_letter(Rating.STRONG_BUY)) 55 | 56 | def test_rating_letter_sell(self): 57 | self.assertEqual(SELL_CHAR, _rating_letter(Rating.SELL)) 58 | self.assertEqual(SELL_CHAR, _rating_letter(Rating.STRONG_SELL)) 59 | 60 | def test_rating_letter_neutral(self): 61 | self.assertEqual(NEUTRAL_CHAR, _rating_letter(Rating.NEUTRAL)) 62 | 63 | 64 | class TestBeautifyFunction(unittest.TestCase): 65 | 66 | def test_beautify_returns_styler(self): 67 | # Create a simple DataFrame with percent column 68 | df = pd.DataFrame({ 69 | 'change': [1.5, -2.3, 0.0] 70 | }) 71 | # Mock the StockField to test basic functionality 72 | # Note: beautify expects a ScreenerDataFrame, so we test with minimal setup 73 | # This is a simple smoke test 74 | self.assertTrue(callable(beautify)) 75 | 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /docs/screeners/bond.md: -------------------------------------------------------------------------------- 1 | # Bond Screener 2 | 3 | Screen government and corporate bonds. 4 | 5 | ## Quick Start 6 | 7 | ```python 8 | from tvscreener import BondScreener, BondField 9 | 10 | bs = BondScreener() 11 | df = bs.get() 12 | ``` 13 | 14 | ## Field Count 15 | 16 | The Bond Screener has access to **~201 fields** covering: 17 | 18 | - Yield and price data 19 | - Maturity information 20 | - Credit ratings 21 | - Technical indicators 22 | 23 | ## Common Fields 24 | 25 | ### Price & Yield 26 | 27 | ```python 28 | BondField.PRICE # Current price 29 | BondField.YIELD # Current yield 30 | BondField.CHANGE_PERCENT # Daily change 31 | BondField.OPEN # Day open 32 | BondField.HIGH # Day high 33 | BondField.LOW # Day low 34 | ``` 35 | 36 | ### Technical 37 | 38 | ```python 39 | BondField.RELATIVE_STRENGTH_INDEX_14 # RSI(14) 40 | BondField.MACD_LEVEL_12_26 # MACD 41 | BondField.SIMPLE_MOVING_AVERAGE_50 # SMA 50 42 | BondField.SIMPLE_MOVING_AVERAGE_200 # SMA 200 43 | ``` 44 | 45 | ### Performance 46 | 47 | ```python 48 | BondField.PERFORMANCE_1_WEEK # 1 week 49 | BondField.PERFORMANCE_1_MONTH # 1 month 50 | BondField.PERFORMANCE_YTD # Year to date 51 | BondField.PERFORMANCE_1_YEAR # 1 year 52 | ``` 53 | 54 | ## Example Screens 55 | 56 | ### All Bonds by Yield 57 | 58 | ```python 59 | bs = BondScreener() 60 | bs.sort_by(BondField.YIELD, ascending=False) 61 | bs.set_range(0, 50) 62 | bs.select( 63 | BondField.NAME, 64 | BondField.PRICE, 65 | BondField.YIELD, 66 | BondField.CHANGE_PERCENT 67 | ) 68 | 69 | df = bs.get() 70 | ``` 71 | 72 | ### Top Movers 73 | 74 | ```python 75 | bs = BondScreener() 76 | bs.sort_by(BondField.CHANGE_PERCENT, ascending=False) 77 | bs.set_range(0, 20) 78 | bs.select( 79 | BondField.NAME, 80 | BondField.PRICE, 81 | BondField.YIELD, 82 | BondField.CHANGE_PERCENT 83 | ) 84 | 85 | df = bs.get() 86 | ``` 87 | 88 | ### RSI Analysis 89 | 90 | ```python 91 | bs = BondScreener() 92 | bs.where(BondField.RELATIVE_STRENGTH_INDEX_14 < 40) 93 | bs.select( 94 | BondField.NAME, 95 | BondField.YIELD, 96 | BondField.RELATIVE_STRENGTH_INDEX_14 97 | ) 98 | 99 | df = bs.get() 100 | ``` 101 | 102 | ## Specific Bonds 103 | 104 | Query specific bonds: 105 | 106 | ```python 107 | bs = BondScreener() 108 | bs.symbols = { 109 | "query": {"types": []}, 110 | "tickers": ["TVC:US10Y", "TVC:US02Y", "TVC:US30Y"] 111 | } 112 | bs.select_all() 113 | 114 | df = bs.get() 115 | ``` 116 | 117 | ## Common Bond Symbols 118 | 119 | | Bond | Symbol | 120 | |------|--------| 121 | | US 10-Year | `TVC:US10Y` | 122 | | US 2-Year | `TVC:US02Y` | 123 | | US 30-Year | `TVC:US30Y` | 124 | | US 5-Year | `TVC:US05Y` | 125 | | German 10-Year | `TVC:DE10Y` | 126 | | UK 10-Year | `TVC:GB10Y` | 127 | | Japan 10-Year | `TVC:JP10Y` | 128 | 129 | ## All Fields 130 | 131 | ```python 132 | bs = BondScreener() 133 | bs.select_all() 134 | bs.set_range(0, 100) 135 | 136 | df = bs.get() 137 | print(f"Columns: {len(df.columns)}") # ~201 138 | ``` 139 | 140 | ## Notes 141 | 142 | - Bond yields move inversely to prices 143 | - Government bonds are often used as safe-haven indicators 144 | - Monitor yield spreads (e.g., 10Y-2Y) for economic signals 145 | -------------------------------------------------------------------------------- /.dev/codegen/data/time_intervals.json: -------------------------------------------------------------------------------- 1 | { 2 | "columns": [ 3 | "ADR", 4 | "ADX", 5 | "ADX+DI", 6 | "ADX-DI", 7 | "AO", 8 | "ATR", 9 | "Aroon.Down", 10 | "Aroon.Up", 11 | "BB.lower", 12 | "BB.upper", 13 | "BBPower", 14 | "CCI20", 15 | "DonchCh20.Lower", 16 | "DonchCh20.Upper", 17 | "EMA10", 18 | "EMA100", 19 | "EMA20", 20 | "EMA200", 21 | "EMA30", 22 | "EMA5", 23 | "EMA50", 24 | "HullMA9", 25 | "Ichimoku.BLine", 26 | "Ichimoku.CLine", 27 | "Ichimoku.Lead1", 28 | "Ichimoku.Lead2", 29 | "KltChnl.lower", 30 | "KltChnl.upper", 31 | "MACD.macd", 32 | "MACD.signal", 33 | "Mom", 34 | "P.SAR", 35 | "Pivot.M.Camarilla.Middle", 36 | "Pivot.M.Camarilla.R1", 37 | "Pivot.M.Camarilla.R2", 38 | "Pivot.M.Camarilla.R3", 39 | "Pivot.M.Camarilla.S1", 40 | "Pivot.M.Camarilla.S2", 41 | "Pivot.M.Camarilla.S3", 42 | "Pivot.M.Classic.Middle", 43 | "Pivot.M.Classic.R1", 44 | "Pivot.M.Classic.R2", 45 | "Pivot.M.Classic.R3", 46 | "Pivot.M.Classic.S1", 47 | "Pivot.M.Classic.S2", 48 | "Pivot.M.Classic.S3", 49 | "Pivot.M.Demark.Middle", 50 | "Pivot.M.Demark.R1", 51 | "Pivot.M.Demark.S1", 52 | "Pivot.M.Fibonacci.Middle", 53 | "Pivot.M.Fibonacci.R1", 54 | "Pivot.M.Fibonacci.R2", 55 | "Pivot.M.Fibonacci.R3", 56 | "Pivot.M.Fibonacci.S1", 57 | "Pivot.M.Fibonacci.S2", 58 | "Pivot.M.Fibonacci.S3", 59 | "Pivot.M.Woodie.Middle", 60 | "Pivot.M.Woodie.R1", 61 | "Pivot.M.Woodie.R2", 62 | "Pivot.M.Woodie.R3", 63 | "Pivot.M.Woodie.S1", 64 | "Pivot.M.Woodie.S2", 65 | "Pivot.M.Woodie.S3", 66 | "ROC", 67 | "RSI", 68 | "RSI7", 69 | "Recommend.All", 70 | "Recommend.MA", 71 | "Recommend.Other", 72 | "SMA10", 73 | "SMA100", 74 | "SMA20", 75 | "SMA200", 76 | "SMA30", 77 | "SMA5", 78 | "SMA50", 79 | "Stoch.D", 80 | "Stoch.K", 81 | "Stoch.RSI.D", 82 | "Stoch.RSI.K", 83 | "UO", 84 | "VWAP", 85 | "VWMA", 86 | "W.R", 87 | "change", 88 | "close", 89 | "gap", 90 | "high", 91 | "low", 92 | "open", 93 | "MoneyFlow", 94 | "Value.Traded", 95 | "relative_volume_10d_calc", 96 | "candlestick", 97 | "Candle.AbandonedBaby.Bearish", 98 | "Candle.AbandonedBaby.Bullish", 99 | "Candle.Engulfing.Bearish", 100 | "Candle.Harami.Bearish", 101 | "Candle.Engulfing.Bullish", 102 | "Candle.Harami.Bullish", 103 | "Candle.Doji", 104 | "Candle.Doji.Dragonfly", 105 | "Candle.EveningStar", 106 | "Candle.Doji.Gravestone", 107 | "Candle.Hammer", 108 | "Candle.HangingMan", 109 | "Candle.InvertedHammer", 110 | "Candle.Kicking.Bearish", 111 | "Candle.Kicking.Bullish", 112 | "Candle.LongShadow.Lower", 113 | "Candle.LongShadow.Upper", 114 | "Candle.Marubozu.Black", 115 | "Candle.Marubozu.White", 116 | "Candle.MorningStar", 117 | "Candle.ShootingStar", 118 | "Candle.SpinningTop.Black", 119 | "Candle.SpinningTop.White", 120 | "Candle.3BlackCrows", 121 | "Candle.3WhiteSoldiers", 122 | "Candle.TriStar.Bearish", 123 | "Candle.TriStar.Bullish", 124 | "change_abs", 125 | "change_from_open", 126 | "change_from_open_abs", 127 | "volume" 128 | ] 129 | } -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: tvscreener 2 | site_description: Python library to retrieve data from TradingView Screener 3 | site_url: https://deepentropy.github.io/tvscreener/docs/ 4 | repo_url: https://github.com/deepentropy/tvscreener 5 | repo_name: deepentropy/tvscreener 6 | 7 | theme: 8 | name: material 9 | palette: 10 | # Dark mode (default) 11 | - scheme: slate 12 | primary: indigo 13 | accent: blue 14 | toggle: 15 | icon: material/brightness-4 16 | name: Switch to light mode 17 | # Light mode 18 | - scheme: default 19 | primary: indigo 20 | accent: blue 21 | toggle: 22 | icon: material/brightness-7 23 | name: Switch to dark mode 24 | features: 25 | - navigation.instant 26 | - navigation.tracking 27 | - navigation.sections 28 | - navigation.expand 29 | - navigation.top 30 | - search.suggest 31 | - search.highlight 32 | - content.code.copy 33 | - content.code.annotate 34 | - content.tabs.link 35 | icon: 36 | repo: fontawesome/brands/github 37 | 38 | plugins: 39 | - search 40 | - mkdocstrings: 41 | handlers: 42 | python: 43 | options: 44 | show_source: true 45 | show_root_heading: true 46 | heading_level: 2 47 | members_order: source 48 | docstring_style: google 49 | 50 | markdown_extensions: 51 | - pymdownx.highlight: 52 | anchor_linenums: true 53 | line_spans: __span 54 | pygments_lang_class: true 55 | - pymdownx.inlinehilite 56 | - pymdownx.snippets 57 | - pymdownx.superfences 58 | - pymdownx.tabbed: 59 | alternate_style: true 60 | - pymdownx.details 61 | - pymdownx.mark 62 | - admonition 63 | - attr_list 64 | - md_in_html 65 | - tables 66 | - toc: 67 | permalink: true 68 | 69 | extra: 70 | social: 71 | - icon: fontawesome/brands/github 72 | link: https://github.com/deepentropy/tvscreener 73 | - icon: fontawesome/brands/python 74 | link: https://pypi.org/project/tvscreener/ 75 | 76 | extra_css: 77 | - stylesheets/extra.css 78 | 79 | nav: 80 | - Home: index.md 81 | - Getting Started: 82 | - Installation: getting-started/installation.md 83 | - Quick Start: getting-started/quickstart.md 84 | - Code Generator: getting-started/code-generator.md 85 | - User Guide: 86 | - Filtering: guide/filtering.md 87 | - Selecting Fields: guide/selecting-fields.md 88 | - Sorting & Pagination: guide/sorting-pagination.md 89 | - Time Intervals: guide/time-intervals.md 90 | - Streaming: guide/streaming.md 91 | - Styled Output: guide/styled-output.md 92 | - Screeners: 93 | - Stock Screener: screeners/stock.md 94 | - Crypto Screener: screeners/crypto.md 95 | - Forex Screener: screeners/forex.md 96 | - Bond Screener: screeners/bond.md 97 | - Futures Screener: screeners/futures.md 98 | - Coin Screener: screeners/coin.md 99 | - Examples: 100 | - Stock Screening: examples/stock-screening.md 101 | - Technical Analysis: examples/technical-analysis.md 102 | - Crypto Strategies: examples/crypto-strategies.md 103 | - API Reference: 104 | - Screeners: api/screeners.md 105 | - Fields: api/fields.md 106 | - Filters: api/filters.md 107 | - Enums: api/enums.md 108 | - Changelog: changelog.md 109 | -------------------------------------------------------------------------------- /docs/guide/styled-output.md: -------------------------------------------------------------------------------- 1 | # Styled Output 2 | 3 | Display results with TradingView-style formatting. 4 | 5 | ## Overview 6 | 7 | Use `beautify()` to display results with colored formatting: 8 | 9 | ```python 10 | from tvscreener import StockScreener, beautify 11 | 12 | ss = StockScreener() 13 | df = ss.get() 14 | 15 | beautify(df) 16 | ``` 17 | 18 | ## Features 19 | 20 | - **Colored changes**: Green for positive, red for negative 21 | - **Formatted numbers**: Currency symbols, percentages, large numbers 22 | - **Aligned columns**: Clean tabular display 23 | - **Terminal-friendly**: Works in most terminal emulators 24 | 25 | ## Installation 26 | 27 | Styled output requires the `rich` library: 28 | 29 | ```bash 30 | pip install rich 31 | ``` 32 | 33 | ## Basic Usage 34 | 35 | ```python 36 | from tvscreener import StockScreener, StockField, beautify 37 | 38 | ss = StockScreener() 39 | ss.select( 40 | StockField.NAME, 41 | StockField.PRICE, 42 | StockField.CHANGE_PERCENT, 43 | StockField.VOLUME, 44 | StockField.MARKET_CAPITALIZATION 45 | ) 46 | ss.set_range(0, 20) 47 | 48 | df = ss.get() 49 | beautify(df) 50 | ``` 51 | 52 | Output shows: 53 | - Price in currency format ($XXX.XX) 54 | - Change % in green/red with percentage symbol 55 | - Volume with K/M/B suffixes 56 | - Market cap with B/T suffixes 57 | 58 | ## Column Formatting 59 | 60 | The `beautify()` function automatically detects and formats: 61 | 62 | | Column Type | Format Example | 63 | |-------------|----------------| 64 | | Price/Currency | $150.25 | 65 | | Change % | +2.5% (green) / -1.2% (red) | 66 | | Volume | 1.5M | 67 | | Market Cap | 2.5T | 68 | | P/E Ratio | 25.3 | 69 | | Percentages | 15.2% | 70 | 71 | ## Jupyter Notebooks 72 | 73 | In Jupyter, `beautify()` displays as a styled HTML table: 74 | 75 | ```python 76 | from tvscreener import StockScreener, beautify 77 | 78 | ss = StockScreener() 79 | df = ss.get() 80 | beautify(df) # Renders as styled HTML in Jupyter 81 | ``` 82 | 83 | ## Customization 84 | 85 | ### Limit Rows 86 | 87 | ```python 88 | beautify(df, max_rows=10) 89 | ``` 90 | 91 | ### Select Columns 92 | 93 | ```python 94 | beautify(df[['name', 'close', 'change']]) 95 | ``` 96 | 97 | ## Without beautify() 98 | 99 | Standard pandas display: 100 | 101 | ```python 102 | ss = StockScreener() 103 | df = ss.get() 104 | print(df) # Plain pandas output 105 | ``` 106 | 107 | Or use pandas styling: 108 | 109 | ```python 110 | df.style.format({ 111 | 'close': '${:.2f}', 112 | 'change': '{:+.2f}%', 113 | 'volume': '{:,.0f}' 114 | }) 115 | ``` 116 | 117 | ## Terminal Colors 118 | 119 | For best results, use a terminal that supports ANSI colors: 120 | 121 | - **Windows**: Windows Terminal, PowerShell 122 | - **macOS**: Terminal, iTerm2 123 | - **Linux**: Most terminal emulators 124 | 125 | If colors don't display correctly: 126 | 127 | ```python 128 | # Force plain text output 129 | import os 130 | os.environ['NO_COLOR'] = '1' 131 | ``` 132 | 133 | ## Streaming with Styled Output 134 | 135 | ```python 136 | from tvscreener import StockScreener, StockField, beautify 137 | 138 | ss = StockScreener() 139 | ss.set_range(0, 10) 140 | 141 | for df in ss.stream(interval=5): 142 | print("\033[2J\033[H") # Clear terminal 143 | beautify(df) 144 | ``` 145 | 146 | ## Export to HTML 147 | 148 | Save styled output as HTML: 149 | 150 | ```python 151 | from tvscreener import StockScreener 152 | 153 | ss = StockScreener() 154 | df = ss.get() 155 | 156 | # Using pandas 157 | html = df.to_html() 158 | with open('screener.html', 'w') as f: 159 | f.write(html) 160 | ``` 161 | -------------------------------------------------------------------------------- /tvscreener/core/stock.py: -------------------------------------------------------------------------------- 1 | from tvscreener.core.base import Screener, default_market, default_sort_stocks 2 | from tvscreener.field import Market, Type, SymbolType 3 | from tvscreener.field.stock import StockField, DEFAULT_STOCK_FIELDS 4 | from tvscreener.filter import FilterOperator 5 | from tvscreener.util import get_url 6 | 7 | # Mapping from SymbolType to Type for efficient lookup 8 | SYMBOL_TYPE_TO_TYPE_MAP = { 9 | SymbolType.COMMON_STOCK: Type.STOCK, 10 | SymbolType.DEPOSITORY_RECEIPT: Type.DEPOSITORY_RECEIPT, 11 | SymbolType.ETF: Type.FUND, 12 | SymbolType.MUTUAL_FUND: Type.FUND, 13 | SymbolType.REIT: Type.FUND, 14 | SymbolType.PREFERRED_STOCK: Type.STOCK, 15 | SymbolType.ETN: Type.STRUCTURED, 16 | SymbolType.STRUCTURED: Type.STRUCTURED, 17 | SymbolType.UIT: Type.FUND, 18 | } 19 | 20 | 21 | class StockScreener(Screener): 22 | """Stock screener for querying stocks from TradingView.""" 23 | 24 | _field_type = StockField 25 | 26 | def __init__(self): 27 | super().__init__() 28 | self.markets = [default_market] 29 | self.url = get_url("global") 30 | self.specific_fields = DEFAULT_STOCK_FIELDS # Use default 424 fields (set to StockField for all 3500+) 31 | self.sort_by(default_sort_stocks, False) 32 | 33 | def _build_payload(self, requested_columns_): 34 | payload = super()._build_payload(requested_columns_) 35 | if self.markets: 36 | payload["markets"] = [market.value for market in self.markets] 37 | return payload 38 | 39 | def _add_types(self, *types: Type): 40 | if len(types) > 1: 41 | operator = FilterOperator.IN_RANGE 42 | else: 43 | operator = FilterOperator.EQUAL 44 | 45 | for type_ in types: 46 | self.add_filter(StockField.TYPE, operator, type_.value) 47 | 48 | def set_symbol_types(self, *symbol_types: SymbolType): 49 | """ 50 | Set the symbol types to be scanned. 51 | 52 | :param symbol_types: Symbol types to include in the screener results 53 | :raises ValueError: If an unknown symbol type is provided 54 | """ 55 | # Validate all symbol types are known 56 | unknown_types = [st for st in symbol_types if st not in SYMBOL_TYPE_TO_TYPE_MAP] 57 | if unknown_types: 58 | raise ValueError(f"Unknown symbol types: {unknown_types}") 59 | 60 | # Collect unique Type values from symbol_types 61 | types_to_add = {SYMBOL_TYPE_TO_TYPE_MAP[symbol_type] for symbol_type in symbol_types} 62 | 63 | # Add Type filters (use IN_RANGE if multiple types, otherwise EQUAL) 64 | if types_to_add: 65 | self._add_types(*types_to_add) 66 | 67 | # Special case: COMMON_STOCK automatically includes DEPOSITORY_RECEIPT 68 | # This maintains backward compatibility with existing behavior 69 | symbol_types_list = list(symbol_types) 70 | if SymbolType.COMMON_STOCK in symbol_types and SymbolType.DEPOSITORY_RECEIPT not in symbol_types: 71 | symbol_types_list.append(SymbolType.DEPOSITORY_RECEIPT) 72 | 73 | # Add subtype filters 74 | for symbol_type in symbol_types_list: 75 | self.add_filter(StockField.SUBTYPE, FilterOperator.IN_RANGE, symbol_type.value.copy()) 76 | 77 | def set_markets(self, *markets: Market): 78 | """ 79 | Set the markets to be scanned 80 | :param markets: list of markets 81 | :return: None 82 | """ 83 | if Market.ALL in markets: 84 | self.markets = [market for market in Market] 85 | else: 86 | self.markets = [market for market in markets] 87 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # tvscreener 2 | 3 | **Python library to retrieve data from TradingView Screener** 4 | 5 | [![PyPI version](https://badge.fury.io/py/tvscreener.svg)](https://badge.fury.io/py/tvscreener) 6 | [![Downloads](https://pepy.tech/badge/tvscreener)](https://pepy.tech/project/tvscreener) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | --- 10 | 11 | ## Features 12 | 13 | - **6 Screener Types**: Stock, Crypto, Forex, Bond, Futures, and Coin 14 | - **Pythonic Filtering**: Use `>`, `<`, `>=`, `<=`, `==`, `!=` operators directly on fields 15 | - **3,500+ Fields**: Access all TradingView fields including fundamentals, technicals, and more 16 | - **Index Filtering**: Filter by S&P 500, NASDAQ 100, Dow Jones, and 50+ other indices 17 | - **Multi-Timeframe**: Use any technical indicator with different timeframes 18 | - **Real-time Streaming**: Stream live data updates 19 | - **Styled Output**: Beautiful formatted tables matching TradingView's style 20 | 21 | ## Quick Example 22 | 23 | ```python 24 | from tvscreener import StockScreener, StockField, IndexSymbol 25 | 26 | ss = StockScreener() 27 | 28 | # Filter S&P 500 stocks 29 | ss.set_index(IndexSymbol.SP500) 30 | 31 | # Pythonic filtering 32 | ss.where(StockField.PRICE > 50) 33 | ss.where(StockField.PE_RATIO_TTM.between(10, 25)) 34 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 40) 35 | 36 | # Select fields 37 | ss.select( 38 | StockField.NAME, 39 | StockField.PRICE, 40 | StockField.CHANGE_PERCENT, 41 | StockField.PE_RATIO_TTM, 42 | StockField.RELATIVE_STRENGTH_INDEX_14 43 | ) 44 | 45 | # Sort and limit 46 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 47 | ss.set_range(0, 100) 48 | 49 | df = ss.get() 50 | ``` 51 | 52 | ## Try the Code Generator 53 | 54 | Build screener queries visually with our web app: 55 | 56 | [:material-rocket-launch: Launch Code Generator](https://deepentropy.github.io/tvscreener/){ .md-button .md-button--primary } 57 | 58 | ## Installation 59 | 60 | ```bash 61 | pip install tvscreener 62 | ``` 63 | 64 | ## Documentation 65 | 66 |
67 | 68 | - :material-clock-fast:{ .lg .middle } __Quick Start__ 69 | 70 | --- 71 | 72 | Get up and running in 5 minutes 73 | 74 | [:octicons-arrow-right-24: Getting Started](getting-started/quickstart.md) 75 | 76 | - :material-filter:{ .lg .middle } __Filtering Guide__ 77 | 78 | --- 79 | 80 | Complete reference for filtering stocks 81 | 82 | [:octicons-arrow-right-24: Filtering](guide/filtering.md) 83 | 84 | - :material-chart-line:{ .lg .middle } __Stock Screening__ 85 | 86 | --- 87 | 88 | Value, dividend, momentum strategies 89 | 90 | [:octicons-arrow-right-24: Examples](examples/stock-screening.md) 91 | 92 | - :material-chart-timeline:{ .lg .middle } __Technical Analysis__ 93 | 94 | --- 95 | 96 | RSI, MACD, multi-timeframe screens 97 | 98 | [:octicons-arrow-right-24: Technical](examples/technical-analysis.md) 99 | 100 |
101 | 102 | ## Why tvscreener? 103 | 104 | | Feature | tvscreener | Others | 105 | |---------|------------|--------| 106 | | **Type-safe fields** | `StockField.PRICE` | `"close"` strings | 107 | | **Pythonic syntax** | `field > 100` | SQL-like builders | 108 | | **Index filtering** | `set_index(SP500)` | Manual symbol lists | 109 | | **Field discovery** | `search("rsi")` | Read docs | 110 | | **All fields** | `select_all()` | List each one | 111 | | **Streaming** | Built-in | DIY | 112 | 113 | ## License 114 | 115 | MIT License - See [LICENSE](https://github.com/deepentropy/tvscreener/blob/main/LICENSE) for details. 116 | 117 | --- 118 | 119 | **Not affiliated with TradingView.** This library accesses publicly available data from the TradingView website. 120 | -------------------------------------------------------------------------------- /docs/screeners/coin.md: -------------------------------------------------------------------------------- 1 | # Coin Screener 2 | 3 | Screen coins from CoinGecko data. 4 | 5 | ## Quick Start 6 | 7 | ```python 8 | from tvscreener import CoinScreener, CoinField 9 | 10 | cs = CoinScreener() 11 | df = cs.get() 12 | ``` 13 | 14 | ## Field Count 15 | 16 | The Coin Screener has access to **~3,026 fields** covering: 17 | 18 | - Price & market data 19 | - On-chain metrics 20 | - Technical indicators 21 | - Performance data 22 | 23 | ## Difference from Crypto Screener 24 | 25 | | Feature | CoinScreener | CryptoScreener | 26 | |---------|--------------|----------------| 27 | | Data Source | CoinGecko-style | TradingView exchanges | 28 | | Focus | Coins/tokens | Trading pairs | 29 | | Metrics | Market cap, supply | Exchange volume | 30 | 31 | ## Common Fields 32 | 33 | ### Price & Market 34 | 35 | ```python 36 | CoinField.PRICE # Current price 37 | CoinField.MARKET_CAPITALIZATION # Market cap 38 | CoinField.CHANGE_PERCENT # 24h change 39 | CoinField.VOLUME # 24h volume 40 | ``` 41 | 42 | ### Technical 43 | 44 | ```python 45 | CoinField.RELATIVE_STRENGTH_INDEX_14 # RSI(14) 46 | CoinField.MACD_LEVEL_12_26 # MACD 47 | CoinField.SIMPLE_MOVING_AVERAGE_50 # SMA 50 48 | CoinField.SIMPLE_MOVING_AVERAGE_200 # SMA 200 49 | ``` 50 | 51 | ### Performance 52 | 53 | ```python 54 | CoinField.PERFORMANCE_1_WEEK # 7d change 55 | CoinField.PERFORMANCE_1_MONTH # 30d change 56 | CoinField.PERFORMANCE_YTD # Year to date 57 | CoinField.PERFORMANCE_1_YEAR # 1 year 58 | ``` 59 | 60 | ## Example Screens 61 | 62 | ### Top by Market Cap 63 | 64 | ```python 65 | cs = CoinScreener() 66 | cs.sort_by(CoinField.MARKET_CAPITALIZATION, ascending=False) 67 | cs.set_range(0, 100) 68 | cs.select( 69 | CoinField.NAME, 70 | CoinField.PRICE, 71 | CoinField.MARKET_CAPITALIZATION, 72 | CoinField.CHANGE_PERCENT 73 | ) 74 | 75 | df = cs.get() 76 | ``` 77 | 78 | ### Top Gainers 79 | 80 | ```python 81 | cs = CoinScreener() 82 | cs.where(CoinField.CHANGE_PERCENT > 10) 83 | cs.where(CoinField.MARKET_CAPITALIZATION > 10_000_000) # Min $10M cap 84 | cs.sort_by(CoinField.CHANGE_PERCENT, ascending=False) 85 | cs.set_range(0, 50) 86 | 87 | df = cs.get() 88 | ``` 89 | 90 | ### Large Cap Oversold 91 | 92 | ```python 93 | cs = CoinScreener() 94 | cs.where(CoinField.MARKET_CAPITALIZATION > 1e9) # $1B+ market cap 95 | cs.where(CoinField.RELATIVE_STRENGTH_INDEX_14 < 35) 96 | cs.select( 97 | CoinField.NAME, 98 | CoinField.PRICE, 99 | CoinField.MARKET_CAPITALIZATION, 100 | CoinField.RELATIVE_STRENGTH_INDEX_14 101 | ) 102 | 103 | df = cs.get() 104 | ``` 105 | 106 | ### Weekly Momentum 107 | 108 | ```python 109 | cs = CoinScreener() 110 | cs.where(CoinField.PERFORMANCE_1_WEEK > 20) 111 | cs.where(CoinField.MARKET_CAPITALIZATION > 100_000_000) 112 | cs.sort_by(CoinField.PERFORMANCE_1_WEEK, ascending=False) 113 | 114 | df = cs.get() 115 | ``` 116 | 117 | ## Multi-Timeframe 118 | 119 | ```python 120 | cs = CoinScreener() 121 | 122 | # Daily RSI moderate 123 | cs.where(CoinField.RELATIVE_STRENGTH_INDEX_14.between(40, 60)) 124 | 125 | # 4-hour RSI oversold 126 | rsi_4h = CoinField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') 127 | cs.where(rsi_4h < 35) 128 | 129 | cs.select( 130 | CoinField.NAME, 131 | CoinField.PRICE, 132 | CoinField.RELATIVE_STRENGTH_INDEX_14, 133 | rsi_4h 134 | ) 135 | 136 | df = cs.get() 137 | ``` 138 | 139 | ## All Fields 140 | 141 | ```python 142 | cs = CoinScreener() 143 | cs.select_all() 144 | cs.set_range(0, 100) 145 | 146 | df = cs.get() 147 | print(f"Columns: {len(df.columns)}") # ~3,026 148 | ``` 149 | 150 | ## Notes 151 | 152 | - CoinScreener focuses on individual coins/tokens 153 | - Use CryptoScreener for exchange-specific trading pairs 154 | - Market cap and supply data may differ from exchange data 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .tmp/ 6 | .claude/ 7 | nul 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | /ignore -------------------------------------------------------------------------------- /docs/guide/sorting-pagination.md: -------------------------------------------------------------------------------- 1 | # Sorting & Pagination 2 | 3 | Control the order and number of results returned. 4 | 5 | ## Sorting 6 | 7 | ### Sort by Field 8 | 9 | ```python 10 | from tvscreener import StockScreener, StockField 11 | 12 | ss = StockScreener() 13 | 14 | # Largest market cap first (descending) 15 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 16 | 17 | df = ss.get() 18 | ``` 19 | 20 | ### Sort Ascending 21 | 22 | ```python 23 | ss = StockScreener() 24 | 25 | # Lowest P/E first (ascending) 26 | ss.sort_by(StockField.PE_RATIO_TTM, ascending=True) 27 | 28 | df = ss.get() 29 | ``` 30 | 31 | ### Common Sort Fields 32 | 33 | ```python 34 | # By market cap (largest first) 35 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 36 | 37 | # Top gainers 38 | ss.sort_by(StockField.CHANGE_PERCENT, ascending=False) 39 | 40 | # Most active (by volume) 41 | ss.sort_by(StockField.VOLUME, ascending=False) 42 | 43 | # Lowest P/E 44 | ss.sort_by(StockField.PE_RATIO_TTM, ascending=True) 45 | 46 | # Highest dividend yield 47 | ss.sort_by(StockField.DIVIDEND_YIELD_FY, ascending=False) 48 | 49 | # Most oversold (lowest RSI) 50 | ss.sort_by(StockField.RELATIVE_STRENGTH_INDEX_14, ascending=True) 51 | ``` 52 | 53 | ## Pagination 54 | 55 | ### Set Range 56 | 57 | Use `set_range(from, to)` to control which results are returned: 58 | 59 | ```python 60 | ss = StockScreener() 61 | 62 | # First 100 results 63 | ss.set_range(0, 100) 64 | 65 | df = ss.get() 66 | ``` 67 | 68 | ### Pagination Examples 69 | 70 | ```python 71 | # First 50 results 72 | ss.set_range(0, 50) 73 | 74 | # Results 51-100 75 | ss.set_range(50, 100) 76 | 77 | # Results 101-150 78 | ss.set_range(100, 150) 79 | 80 | # First 500 results 81 | ss.set_range(0, 500) 82 | 83 | # Maximum results (up to 5000) 84 | ss.set_range(0, 5000) 85 | ``` 86 | 87 | ### Default Behavior 88 | 89 | Without `set_range()`, the default is 150 results: 90 | 91 | ```python 92 | ss = StockScreener() 93 | df = ss.get() 94 | print(len(df)) # 150 (default) 95 | ``` 96 | 97 | ## Combining Sort and Pagination 98 | 99 | Get the top 100 stocks by market cap: 100 | 101 | ```python 102 | ss = StockScreener() 103 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 104 | ss.set_range(0, 100) 105 | 106 | df = ss.get() 107 | ``` 108 | 109 | ## Practical Examples 110 | 111 | ### Top 50 Gainers 112 | 113 | ```python 114 | ss = StockScreener() 115 | ss.where(StockField.VOLUME >= 1_000_000) # Liquid stocks only 116 | ss.sort_by(StockField.CHANGE_PERCENT, ascending=False) 117 | ss.set_range(0, 50) 118 | 119 | df = ss.get() 120 | ``` 121 | 122 | ### Bottom 50 Losers 123 | 124 | ```python 125 | ss = StockScreener() 126 | ss.where(StockField.VOLUME >= 1_000_000) 127 | ss.sort_by(StockField.CHANGE_PERCENT, ascending=True) 128 | ss.set_range(0, 50) 129 | 130 | df = ss.get() 131 | ``` 132 | 133 | ### Most Active Stocks 134 | 135 | ```python 136 | ss = StockScreener() 137 | ss.sort_by(StockField.VOLUME, ascending=False) 138 | ss.set_range(0, 100) 139 | 140 | df = ss.get() 141 | ``` 142 | 143 | ### All S&P 500 Stocks 144 | 145 | ```python 146 | from tvscreener import IndexSymbol 147 | 148 | ss = StockScreener() 149 | ss.set_index(IndexSymbol.SP500) 150 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 151 | ss.set_range(0, 500) # S&P 500 has ~500 constituents 152 | 153 | df = ss.get() 154 | ``` 155 | 156 | ### Paginated Iteration 157 | 158 | Process results in batches: 159 | 160 | ```python 161 | ss = StockScreener() 162 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 163 | 164 | all_results = [] 165 | batch_size = 100 166 | 167 | for offset in range(0, 1000, batch_size): 168 | ss.set_range(offset, offset + batch_size) 169 | batch = ss.get() 170 | 171 | if batch.empty: 172 | break 173 | 174 | all_results.append(batch) 175 | print(f"Fetched {offset + len(batch)} stocks") 176 | 177 | import pandas as pd 178 | full_df = pd.concat(all_results, ignore_index=True) 179 | ``` 180 | 181 | ## Limits 182 | 183 | | Parameter | Min | Max | Default | 184 | |-----------|-----|-----|---------| 185 | | `from` | 0 | - | 0 | 186 | | `to` | 1 | 5000 | 150 | 187 | | Results per request | - | 5000 | 150 | 188 | -------------------------------------------------------------------------------- /docs/getting-started/code-generator.md: -------------------------------------------------------------------------------- 1 | # Code Generator 2 | 3 | The **tvscreener Code Generator** is a visual web app that lets you build screener queries without writing code. 4 | 5 | [:material-rocket-launch: Launch Code Generator](https://deepentropy.github.io/tvscreener/){ .md-button .md-button--primary } 6 | 7 | ## Features 8 | 9 | - **Visual Filter Builder**: Add filters with dropdowns instead of writing code 10 | - **All 6 Screeners**: Stock, Crypto, Forex, Bond, Futures, and Coin 11 | - **3,500+ Fields**: Browse and select from all available fields 12 | - **Real-time Preview**: See the generated Python code as you configure 13 | - **One-click Copy**: Copy code to clipboard and run locally 14 | 15 | ## How It Works 16 | 17 | ```mermaid 18 | graph LR 19 | A[Configure UI] --> B[Generated Code] 20 | B --> C[Copy to Clipboard] 21 | C --> D[Run in Python] 22 | D --> E[Get Data] 23 | ``` 24 | 25 | 1. **Select a Screener**: Choose Stock, Crypto, Forex, Bond, Futures, or Coin 26 | 2. **Add Filters**: Use the filter builder to add conditions 27 | 3. **Select Fields**: Choose which data columns you want 28 | 4. **Configure Options**: Set index filter, sorting, and limit 29 | 5. **Copy Code**: Click "Copy Code" and paste into your Python environment 30 | 6. **Run**: Execute the code to get your data 31 | 32 | ## Example Workflow 33 | 34 | ### 1. Select Screener 35 | 36 | Click on "Stock" to screen stocks (the default). 37 | 38 | ### 2. Add Filters 39 | 40 | Click "+ Add Filter" and configure: 41 | 42 | - **Field**: Price 43 | - **Operator**: Greater than (>) 44 | - **Value**: 50 45 | 46 | Add more filters as needed: 47 | 48 | - Volume >= 1,000,000 49 | - RSI(14) < 30 50 | 51 | ### 3. Select Fields 52 | 53 | Use "Select All" for all ~3,500 fields, or pick specific ones: 54 | 55 | - Name 56 | - Price 57 | - Change % 58 | - Volume 59 | - RSI(14) 60 | 61 | ### 4. Configure Options 62 | 63 | - **Index**: S&P 500 (optional) 64 | - **Sort by**: Market Cap (descending) 65 | - **Limit**: 100 66 | 67 | ### 5. Copy and Run 68 | 69 | The generated code will look like: 70 | 71 | ```python 72 | from tvscreener import StockScreener, StockField, IndexSymbol 73 | 74 | ss = StockScreener() 75 | 76 | # Filters 77 | ss.where(StockField.PRICE > 50) 78 | ss.where(StockField.VOLUME >= 1_000_000) 79 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 30) 80 | 81 | # Fields 82 | ss.select( 83 | StockField.NAME, 84 | StockField.PRICE, 85 | StockField.CHANGE_PERCENT, 86 | StockField.VOLUME, 87 | StockField.RELATIVE_STRENGTH_INDEX_14 88 | ) 89 | 90 | # Index 91 | ss.set_index(IndexSymbol.SP500) 92 | 93 | # Sort & Limit 94 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 95 | ss.set_range(0, 100) 96 | 97 | df = ss.get() 98 | print(df) 99 | ``` 100 | 101 | ## Presets 102 | 103 | The Code Generator includes presets for common strategies: 104 | 105 | ### Stock Presets 106 | 107 | - **Basic**: Name, Price, Change, Volume 108 | - **Valuation**: P/E, P/B, P/S, Market Cap 109 | - **Dividends**: Yield, Payout Ratio, Ex-Date 110 | - **Technical**: RSI, MACD, Moving Averages 111 | - **Momentum**: Performance metrics 112 | - **Financial**: Revenue, Margins, Ratios 113 | 114 | ### Crypto Presets 115 | 116 | - **Basic**: Name, Price, Change, Volume 117 | - **Market**: Market Cap, Circulating Supply 118 | - **Technical**: RSI, MACD, Volatility 119 | 120 | ## Tips 121 | 122 | !!! tip "Use Select All for Exploration" 123 | When exploring available data, use "Select All" to see all ~3,500 fields. You can then narrow down to the fields you need. 124 | 125 | !!! tip "Start Simple" 126 | Begin with one or two filters, verify results, then add more conditions. 127 | 128 | !!! tip "Check Field Types" 129 | Numeric fields support `>`, `<`, `between`. Text fields support `==`, `isin`. 130 | 131 | ## Limitations 132 | 133 | - The Code Generator creates Python code - you still need Python installed to run it 134 | - No backend - all processing happens in your browser 135 | - Generated code requires the `tvscreener` package to be installed 136 | 137 | ## Feedback 138 | 139 | Found a bug or have a feature request? 140 | 141 | [Open an Issue on GitHub](https://github.com/deepentropy/tvscreener/issues){ .md-button } 142 | -------------------------------------------------------------------------------- /.dev/codegen/data/industry.json: -------------------------------------------------------------------------------- 1 | { 2 | "Industry": [ 3 | "Any", 4 | "Advertising/Marketing Services", 5 | "Aerospace & Defense", 6 | "Agricultural Commodities/Milling", 7 | "Air Freight/Couriers", 8 | "Airlines", 9 | "Alternative Power Generation", 10 | "Aluminum", 11 | "Apparel/Footwear", 12 | "Apparel/Footwear Retail", 13 | "Auto Parts: OEM", 14 | "Automotive Aftermarket", 15 | "Beverages: Alcoholic", 16 | "Beverages: Non-Alcoholic", 17 | "Biotechnology", 18 | "Broadcasting", 19 | "Building Products", 20 | "Cable/Satellite TV", 21 | "Casinos/Gaming", 22 | "Catalog/Specialty Distribution", 23 | "Chemicals: Agricultural", 24 | "Chemicals: Major Diversified", 25 | "Chemicals: Specialty", 26 | "Coal", 27 | "Commercial Printing/Forms", 28 | "Computer Communications", 29 | "Computer Peripherals", 30 | "Computer Processing Hardware", 31 | "Construction Materials", 32 | "Consumer Sundries", 33 | "Containers/Packaging", 34 | "Contract Drilling", 35 | "Data Processing Services", 36 | "Department Stores", 37 | "Discount Stores", 38 | "Drugstore Chains", 39 | "Electric Utilities", 40 | "Electrical Products", 41 | "Electronic Components", 42 | "Electronic Equipment/Instruments", 43 | "Electronic Production Equipment", 44 | "Electronics Distributors", 45 | "Electronics/Appliance Stores", 46 | "Electronics/Appliances", 47 | "Engineering & Construction", 48 | "Environmental Services", 49 | "Finance/Rental/Leasing", 50 | "Financial Conglomerates", 51 | "Financial Publishing/Services", 52 | "Food Distributors", 53 | "Food Retail", 54 | "Food: Major Diversified", 55 | "Food: Meat/Fish/Dairy", 56 | "Food: Specialty/Candy", 57 | "Forest Products", 58 | "Gas Distributors", 59 | "General Government", 60 | "Home Furnishings", 61 | "Home Improvement Chains", 62 | "Homebuilding", 63 | "Hospital/Nursing Management", 64 | "Hotels/Resorts/Cruise lines", 65 | "Household/Personal Care", 66 | "Industrial Conglomerates", 67 | "Industrial Machinery", 68 | "Industrial Specialties", 69 | "Information Technology Services", 70 | "Insurance Brokers/Services", 71 | "Integrated Oil", 72 | "Internet Retail", 73 | "Internet Software/Services", 74 | "Investment Banks/Brokers", 75 | "Investment Managers", 76 | "Investment Trusts/Mutual Funds", 77 | "Life/Health Insurance", 78 | "Major Banks", 79 | "Major Telecommunications", 80 | "Managed Health Care", 81 | "Marine Shipping", 82 | "Media Conglomerates", 83 | "Medical Distributors", 84 | "Medical Specialties", 85 | "Medical/Nursing Services", 86 | "Metal Fabrication", 87 | "Miscellaneous", 88 | "Miscellaneous Commercial Services", 89 | "Miscellaneous Manufacturing", 90 | "Motor Vehicles", 91 | "Movies/Entertainment", 92 | "Multi-Line Insurance", 93 | "Office Equipment/Supplies", 94 | "Oil & Gas Pipelines", 95 | "Oil & Gas Production", 96 | "Oil Refining/Marketing", 97 | "Oilfield Services/Equipment", 98 | "Other Consumer Services", 99 | "Other Consumer Specialties", 100 | "Other Metals/Minerals", 101 | "Other Transportation", 102 | "Packaged Software", 103 | "Personnel Services", 104 | "Pharmaceuticals: Generic", 105 | "Pharmaceuticals: Major", 106 | "Pharmaceuticals: Other", 107 | "Precious Metals", 108 | "Property/Casualty Insurance", 109 | "Publishing: Books/Magazines", 110 | "Publishing: Newspapers", 111 | "Pulp & Paper", 112 | "Railroads", 113 | "Real Estate Development", 114 | "Real Estate Investment Trusts", 115 | "Recreational Products", 116 | "Regional Banks", 117 | "Restaurants", 118 | "Savings Banks", 119 | "Semiconductors", 120 | "Services to the Health Industry", 121 | "Specialty Insurance", 122 | "Specialty Stores", 123 | "Specialty Telecommunications", 124 | "Steel", 125 | "Telecommunications Equipment", 126 | "Textiles", 127 | "Tobacco", 128 | "Tools & Hardware", 129 | "Trucking", 130 | "Trucks/Construction/Farm Machinery", 131 | "Water Utilities", 132 | "Wholesale Distributors", 133 | "Wireless Telecommunications" 134 | ] 135 | } -------------------------------------------------------------------------------- /tvscreener/ta/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Technical Analysis helpers for computing recommendations based on indicators. 3 | 4 | This module provides functions to calculate buy/sell/neutral recommendations 5 | for various technical indicators such as ADX, Awesome Oscillator, and Bollinger Bands. 6 | """ 7 | 8 | from tvscreener.field import Rating 9 | 10 | 11 | def _crosses_up(x1, x2, y1, y2): 12 | """ 13 | Check if x crossed above y. 14 | 15 | :param x1: Current x value 16 | :param x2: Previous x value 17 | :param y1: Current y value 18 | :param y2: Previous y value 19 | :return: True if x crossed above y 20 | """ 21 | return x1 > y1 and x2 < y2 22 | 23 | 24 | def adx(adx_value, dminus, dplus, dminus_old, dplus_old): 25 | """ 26 | Calculate ADX (Average Directional Index) recommendation. 27 | 28 | :param adx_value: Current ADX value 29 | :param dminus: Current -DI value 30 | :param dplus: Current +DI value 31 | :param dminus_old: Previous -DI value 32 | :param dplus_old: Previous +DI value 33 | :return: Rating enum (BUY, SELL, or NEUTRAL) 34 | """ 35 | if _crosses_up(dplus, dplus_old, dminus, dminus_old) and adx_value > 20: 36 | return Rating.BUY 37 | elif _crosses_up(dminus, dminus_old, dplus, dplus_old) and adx_value > 20: 38 | return Rating.SELL 39 | return Rating.NEUTRAL 40 | 41 | 42 | def _is_ao_bearish_cross(ao_value, ao_old_1, ao_old_2): 43 | """ 44 | Check for AO bearish zero line cross. 45 | 46 | When AO crosses below the Zero Line, short term momentum is now falling faster 47 | than the long term momentum. This can present a bearish selling opportunity. 48 | """ 49 | return ao_value < 0 < ao_old_1 and ao_old_2 > 0 50 | 51 | 52 | def _is_ao_bullish_cross(ao_value, ao_old_1, ao_old_2): 53 | """ 54 | Check for AO bullish zero line cross. 55 | 56 | When AO crosses above the Zero Line, short term momentum is now rising faster 57 | than the long term momentum. This can present a bullish buying opportunity. 58 | """ 59 | return ao_value > 0 > ao_old_1 and ao_old_2 < 0 60 | 61 | 62 | def _is_ao_bullish_saucer(ao_value, ao_old_1, ao_old_2): 63 | """ 64 | Check for AO bullish saucer setup. 65 | 66 | A Bullish Saucer setup occurs when the AO is above the Zero Line. It entails 67 | two consecutive red bars (with the second bar being lower than the first bar) 68 | being followed by a green Bar. 69 | """ 70 | return ao_value > 0 and ao_value > ao_old_1 and ao_old_1 < ao_old_2 71 | 72 | 73 | def _is_ao_bearish_saucer(ao_value, ao_old_1, ao_old_2): 74 | """ 75 | Check for AO bearish saucer setup. 76 | 77 | A Bearish Saucer setup occurs when the AO is below the Zero Line. It entails 78 | two consecutive green bars (with the second bar being higher than the first bar) 79 | being followed by a red bar. 80 | """ 81 | return ao_value < 0 and ao_value < ao_old_1 and ao_old_1 > ao_old_2 82 | 83 | 84 | def ao(ao_value, ao_old_1, ao_old_2): 85 | """ 86 | Calculate Awesome Oscillator recommendation. 87 | 88 | :param ao_value: Current AO value 89 | :param ao_old_1: Previous AO value (1 period back) 90 | :param ao_old_2: Previous AO value (2 periods back) 91 | :return: Rating enum (BUY, SELL, or NEUTRAL) 92 | """ 93 | if _is_ao_bullish_saucer(ao_value, ao_old_1, ao_old_2) or \ 94 | _is_ao_bullish_cross(ao_value, ao_old_1, ao_old_2): 95 | return Rating.BUY 96 | elif _is_ao_bearish_saucer(ao_value, ao_old_1, ao_old_2) or \ 97 | _is_ao_bearish_cross(ao_value, ao_old_1, ao_old_2): 98 | return Rating.SELL 99 | return Rating.NEUTRAL 100 | 101 | 102 | def bb_lower(low_limit, close): 103 | """ 104 | Calculate Bollinger Bands lower band recommendation. 105 | 106 | :param low_limit: Lower Bollinger Band value 107 | :param close: Current close price 108 | :return: Rating enum (BUY or NEUTRAL) 109 | """ 110 | if close < low_limit: 111 | return Rating.BUY 112 | return Rating.NEUTRAL 113 | 114 | 115 | def bb_upper(up_limit, close): 116 | """ 117 | Calculate Bollinger Bands upper band recommendation. 118 | 119 | :param up_limit: Upper Bollinger Band value 120 | :param close: Current close price 121 | :return: Rating enum (SELL or NEUTRAL) 122 | """ 123 | if close > up_limit: 124 | return Rating.SELL 125 | return Rating.NEUTRAL 126 | -------------------------------------------------------------------------------- /docs/screeners/crypto.md: -------------------------------------------------------------------------------- 1 | # Crypto Screener 2 | 3 | Screen cryptocurrencies across major exchanges. 4 | 5 | ## Quick Start 6 | 7 | ```python 8 | from tvscreener import CryptoScreener, CryptoField 9 | 10 | cs = CryptoScreener() 11 | df = cs.get() 12 | ``` 13 | 14 | ## Field Count 15 | 16 | The Crypto Screener has access to **~3,108 fields** covering: 17 | 18 | - Price & Volume data 19 | - Market metrics (market cap, circulating supply) 20 | - Technical indicators 21 | - Performance metrics 22 | 23 | ## Common Fields 24 | 25 | ### Price & Volume 26 | 27 | ```python 28 | CryptoField.PRICE # Current price 29 | CryptoField.OPEN # Day open 30 | CryptoField.HIGH # Day high 31 | CryptoField.LOW # Day low 32 | CryptoField.VOLUME # 24h volume 33 | CryptoField.CHANGE_PERCENT # 24h change 34 | ``` 35 | 36 | ### Market Data 37 | 38 | ```python 39 | CryptoField.MARKET_CAPITALIZATION # Market cap 40 | CryptoField.CIRCULATING_SUPPLY # Circulating supply 41 | CryptoField.TOTAL_SUPPLY # Total supply 42 | ``` 43 | 44 | ### Technical 45 | 46 | ```python 47 | CryptoField.RELATIVE_STRENGTH_INDEX_14 # RSI(14) 48 | CryptoField.MACD_LEVEL_12_26 # MACD 49 | CryptoField.SIMPLE_MOVING_AVERAGE_50 # SMA 50 50 | CryptoField.SIMPLE_MOVING_AVERAGE_200 # SMA 200 51 | CryptoField.AVERAGE_TRUE_RANGE_14 # ATR 52 | CryptoField.VOLATILITY_DAY # Daily volatility 53 | ``` 54 | 55 | ### Performance 56 | 57 | ```python 58 | CryptoField.PERFORMANCE_1_WEEK # 7d change 59 | CryptoField.PERFORMANCE_1_MONTH # 30d change 60 | CryptoField.PERFORMANCE_3_MONTH # 90d change 61 | CryptoField.PERFORMANCE_YTD # Year to date 62 | CryptoField.PERFORMANCE_1_YEAR # 1 year 63 | ``` 64 | 65 | ## Example Screens 66 | 67 | ### Top Market Cap 68 | 69 | ```python 70 | cs = CryptoScreener() 71 | cs.sort_by(CryptoField.MARKET_CAPITALIZATION, ascending=False) 72 | cs.set_range(0, 100) 73 | cs.select( 74 | CryptoField.NAME, 75 | CryptoField.PRICE, 76 | CryptoField.MARKET_CAPITALIZATION, 77 | CryptoField.CHANGE_PERCENT 78 | ) 79 | 80 | df = cs.get() 81 | ``` 82 | 83 | ### High Volume Gainers 84 | 85 | ```python 86 | cs = CryptoScreener() 87 | cs.where(CryptoField.CHANGE_PERCENT > 10) 88 | cs.where(CryptoField.VOLUME > 10_000_000) 89 | cs.sort_by(CryptoField.CHANGE_PERCENT, ascending=False) 90 | cs.set_range(0, 50) 91 | 92 | df = cs.get() 93 | ``` 94 | 95 | ### Oversold RSI 96 | 97 | ```python 98 | cs = CryptoScreener() 99 | cs.where(CryptoField.RELATIVE_STRENGTH_INDEX_14 < 30) 100 | cs.where(CryptoField.MARKET_CAPITALIZATION > 100_000_000) 101 | cs.sort_by(CryptoField.RELATIVE_STRENGTH_INDEX_14, ascending=True) 102 | 103 | df = cs.get() 104 | ``` 105 | 106 | ### Large Cap with Low Volatility 107 | 108 | ```python 109 | cs = CryptoScreener() 110 | cs.where(CryptoField.MARKET_CAPITALIZATION > 1e9) 111 | cs.where(CryptoField.VOLATILITY_DAY < 5) 112 | cs.sort_by(CryptoField.MARKET_CAPITALIZATION, ascending=False) 113 | 114 | df = cs.get() 115 | ``` 116 | 117 | ### Weekly Momentum 118 | 119 | ```python 120 | cs = CryptoScreener() 121 | cs.where(CryptoField.PERFORMANCE_1_WEEK > 20) 122 | cs.where(CryptoField.VOLUME > 5_000_000) 123 | cs.sort_by(CryptoField.PERFORMANCE_1_WEEK, ascending=False) 124 | 125 | df = cs.get() 126 | ``` 127 | 128 | ## Multi-Timeframe Analysis 129 | 130 | ```python 131 | cs = CryptoScreener() 132 | 133 | # Daily RSI oversold 134 | cs.where(CryptoField.RELATIVE_STRENGTH_INDEX_14 < 35) 135 | 136 | # 4-hour RSI also oversold 137 | rsi_4h = CryptoField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') 138 | cs.where(rsi_4h < 30) 139 | 140 | cs.select( 141 | CryptoField.NAME, 142 | CryptoField.PRICE, 143 | CryptoField.RELATIVE_STRENGTH_INDEX_14, 144 | rsi_4h 145 | ) 146 | 147 | df = cs.get() 148 | ``` 149 | 150 | ## Specific Cryptos 151 | 152 | Query specific cryptocurrencies: 153 | 154 | ```python 155 | cs = CryptoScreener() 156 | cs.symbols = { 157 | "query": {"types": []}, 158 | "tickers": ["BINANCE:BTCUSDT", "BINANCE:ETHUSDT", "BINANCE:SOLUSDT"] 159 | } 160 | cs.select_all() 161 | 162 | df = cs.get() 163 | ``` 164 | 165 | ## All Fields 166 | 167 | ```python 168 | cs = CryptoScreener() 169 | cs.select_all() 170 | cs.set_range(0, 100) 171 | 172 | df = cs.get() 173 | print(f"Columns: {len(df.columns)}") # ~3,108 174 | ``` 175 | 176 | ## Notes 177 | 178 | - Crypto markets trade 24/7 - volume is typically 24h volume 179 | - Prices are quoted in various currencies depending on the exchange pair 180 | - Use exchange prefix for specific pairs (e.g., `BINANCE:BTCUSDT`) 181 | -------------------------------------------------------------------------------- /tvscreener/util.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import Type 3 | 4 | from tvscreener.field import Field, add_historical, add_rec, add_rec_to_label, \ 5 | add_historical_to_label 6 | 7 | 8 | def format_historical_field(field_, historical=1): 9 | """ 10 | Format the technical field to include historical offset 11 | :param field_: Field to format 12 | :param historical: Historical offset (default 1) 13 | :return: Formatted field name 14 | :raises ValueError: If field is not a historical field 15 | """ 16 | # Fixed: Use proper exception instead of assert 17 | if not field_.historical: 18 | raise ValueError(f"{field_} is not a historical field") 19 | formatted_technical_field = add_historical(field_.field_name, historical) 20 | 21 | return formatted_technical_field 22 | 23 | 24 | def get_columns_to_request(fields_: Type[Field]): 25 | """ 26 | Assemble the technical columns for the request 27 | :param fields_: type of fields to be requested (StockField, ForexField, CryptoField) 28 | :return: 29 | """ 30 | 31 | # Build a dict of technical label and field label 32 | columns = {field.field_name: field.label for field in fields_} 33 | 34 | # Drop column that starts with "pattern" 35 | columns = {k: v for k, v in columns.items() if not k.startswith("candlestick")} 36 | 37 | # Add the update mode column to every request 38 | columns["update_mode"] = "Update Mode" 39 | 40 | # Format the fields that embed the time interval in the name 41 | columns = {_format_timed_fields(k): v for k, v in columns.items()} 42 | 43 | # Add the recommendation columns 44 | rec_columns = {add_rec(field.field_name): add_rec_to_label(field.field_name) 45 | for field in fields_ if field.has_recommendation()} 46 | 47 | # Add the historical columns 48 | hist_columns = {format_historical_field(field): add_historical_to_label(field.label) 49 | for field in fields_ if field.historical} 50 | 51 | # Merge the dicts 52 | columns = {**columns, **rec_columns, **hist_columns} 53 | 54 | return columns 55 | 56 | 57 | def _format_timed_fields(field_): 58 | """Format fields that embed the time interval in the name 59 | e.g. 'change.1W' -> 'change|1W'""" 60 | # Split the field by '.' 61 | if (field_.startswith("change") or field_.startswith("relative_volume_intraday")) and '.' in field_: 62 | num = field_.split('.')[1] 63 | # is num a number? 64 | if num.isdigit(): 65 | return field_.replace('.', '|') 66 | elif num in ['1W', '1M']: 67 | return field_.replace('.', '|') 68 | return field_ 69 | 70 | 71 | def is_status_code_ok(response): 72 | """Check if HTTP response status code indicates success.""" 73 | return response.ok # Simplified: use built-in ok property 74 | 75 | 76 | def get_url(subtype): 77 | return f"https://scanner.tradingview.com/{subtype}/scan" 78 | 79 | 80 | # Use proper abbreviations including K for thousands 81 | millnames = ['', 'K', 'M', 'B', 'T'] 82 | 83 | 84 | def millify(n): 85 | """ 86 | Convert a number to abbreviated form (e.g., 1000 -> 1.000K, 1000000 -> 1.000M). 87 | 88 | :param n: Number to convert 89 | :return: String representation with abbreviation 90 | """ 91 | n = float(n) 92 | # Handle negative numbers 93 | is_negative = n < 0 94 | n = abs(n) 95 | 96 | millidx = max(0, min(len(millnames) - 1, 97 | int(math.floor(0 if n == 0 else math.log10(n) / 3)))) 98 | 99 | result = '{:.3f}{}'.format(n / 10 ** (3 * millidx), millnames[millidx]) 100 | return '-' + result if is_negative else result 101 | 102 | 103 | def _is_nan(value): 104 | """ 105 | Check if a value is NaN (Not a Number). 106 | 107 | :param value: Value to check 108 | :return: True if value is NaN, False otherwise 109 | """ 110 | try: 111 | return math.isnan(float(value)) 112 | except (TypeError, ValueError): 113 | return False 114 | 115 | 116 | def get_recommendation(rating): 117 | """ 118 | Convert a numeric rating to a recommendation string. 119 | 120 | :param rating: Numeric rating value 121 | :return: "S" (Sell) for negative, "N" (Neutral) for zero, "B" (Buy) for positive 122 | :raises ValueError: If rating is not a valid number 123 | """ 124 | try: 125 | rating = float(rating) 126 | except (TypeError, ValueError): 127 | raise ValueError(f"Invalid rating: {rating}. Rating should be a number.") 128 | 129 | if rating < 0: 130 | return "S" # Sell 131 | elif rating == 0: 132 | return "N" # Neutral 133 | else: # rating > 0 134 | return "B" # Buy 135 | -------------------------------------------------------------------------------- /docs/api/filters.md: -------------------------------------------------------------------------------- 1 | # Filters API Reference 2 | 3 | Filter operators and conditions for screening. 4 | 5 | ## FieldCondition 6 | 7 | Created automatically when using comparison operators on fields. 8 | 9 | ```python 10 | from tvscreener import StockField 11 | 12 | # These create FieldCondition objects 13 | condition1 = StockField.PRICE > 100 14 | condition2 = StockField.PE_RATIO_TTM.between(10, 25) 15 | condition3 = StockField.SECTOR.isin(['Technology', 'Healthcare']) 16 | ``` 17 | 18 | ### Properties 19 | 20 | | Property | Type | Description | 21 | |----------|------|-------------| 22 | | `field` | `Field` | The field being compared | 23 | | `operation` | `FilterOperator` | The comparison operator | 24 | | `value` | `Any` | The comparison value | 25 | 26 | ### Methods 27 | 28 | #### `to_filter()` 29 | 30 | Convert to TradingView API filter format. 31 | 32 | ```python 33 | condition = StockField.PRICE > 100 34 | filter_dict = condition.to_filter() 35 | # {'left': 'close', 'operation': 'greater', 'right': 100} 36 | ``` 37 | 38 | ## FilterOperator 39 | 40 | Enum of available filter operations. 41 | 42 | ```python 43 | from tvscreener import FilterOperator 44 | ``` 45 | 46 | ### Comparison Operators 47 | 48 | | Operator | Python Syntax | FilterOperator | 49 | |----------|---------------|----------------| 50 | | Greater than | `>` | `FilterOperator.ABOVE` | 51 | | Greater or equal | `>=` | `FilterOperator.ABOVE_OR_EQUAL` | 52 | | Less than | `<` | `FilterOperator.BELOW` | 53 | | Less or equal | `<=` | `FilterOperator.BELOW_OR_EQUAL` | 54 | | Equal | `==` | `FilterOperator.EQUAL` | 55 | | Not equal | `!=` | `FilterOperator.NOT_EQUAL` | 56 | 57 | ### Range Operators 58 | 59 | | Method | FilterOperator | 60 | |--------|----------------| 61 | | `.between(min, max)` | `FilterOperator.IN_RANGE` | 62 | | `.not_between(min, max)` | `FilterOperator.NOT_IN_RANGE` | 63 | 64 | ### List Operators 65 | 66 | | Method | FilterOperator | 67 | |--------|----------------| 68 | | `.isin(values)` | `FilterOperator.IN_RANGE` | 69 | | `.not_in(values)` | `FilterOperator.NOT_IN_RANGE` | 70 | 71 | ## ExtraFilter 72 | 73 | Special filters not tied to specific fields. 74 | 75 | ```python 76 | from tvscreener import ExtraFilter, FilterOperator 77 | ``` 78 | 79 | ### Available Extra Filters 80 | 81 | | Filter | Description | 82 | |--------|-------------| 83 | | `ExtraFilter.PRIMARY` | Primary listing only | 84 | | `ExtraFilter.CURRENT_TRADING_DAY` | Currently trading | 85 | 86 | ### Usage 87 | 88 | ```python 89 | ss = StockScreener() 90 | ss.add_filter(ExtraFilter.PRIMARY, FilterOperator.EQUAL, True) 91 | ss.add_filter(ExtraFilter.CURRENT_TRADING_DAY, FilterOperator.EQUAL, True) 92 | ``` 93 | 94 | ## Legacy Syntax 95 | 96 | The original filter syntax is still supported: 97 | 98 | ```python 99 | from tvscreener import StockScreener, StockField, FilterOperator 100 | 101 | ss = StockScreener() 102 | 103 | # Legacy syntax 104 | ss.where(StockField.PRICE, FilterOperator.ABOVE, 100) 105 | ss.where(StockField.VOLUME, FilterOperator.ABOVE_OR_EQUAL, 1_000_000) 106 | 107 | # Equivalent new syntax 108 | ss.where(StockField.PRICE > 100) 109 | ss.where(StockField.VOLUME >= 1_000_000) 110 | ``` 111 | 112 | ## Filter Chaining 113 | 114 | All filters are combined with AND logic: 115 | 116 | ```python 117 | ss = StockScreener() 118 | 119 | # All conditions must be true 120 | ss.where(StockField.PRICE > 50) # AND 121 | ss.where(StockField.PRICE < 500) # AND 122 | ss.where(StockField.VOLUME >= 1_000_000) # AND 123 | ss.where(StockField.PE_RATIO_TTM.between(10, 30)) 124 | 125 | df = ss.get() # Returns stocks matching ALL conditions 126 | ``` 127 | 128 | ## Type Validation 129 | 130 | Filters are validated against the screener type: 131 | 132 | ```python 133 | ss = StockScreener() 134 | 135 | # Valid - StockField with StockScreener 136 | ss.where(StockField.PRICE > 100) 137 | 138 | # Invalid - CryptoField with StockScreener 139 | # ss.where(CryptoField.PRICE > 100) # Raises error 140 | ``` 141 | 142 | ## Complex Filters Example 143 | 144 | ```python 145 | from tvscreener import StockScreener, StockField, IndexSymbol 146 | 147 | ss = StockScreener() 148 | 149 | # Index filter 150 | ss.set_index(IndexSymbol.SP500) 151 | 152 | # Price range 153 | ss.where(StockField.PRICE.between(20, 500)) 154 | 155 | # Volume filter 156 | ss.where(StockField.VOLUME >= 500_000) 157 | 158 | # Valuation filters 159 | ss.where(StockField.PE_RATIO_TTM.between(5, 25)) 160 | ss.where(StockField.PRICE_TO_BOOK_FY < 5) 161 | 162 | # Sector filter 163 | ss.where(StockField.SECTOR.not_in(['Finance', 'Utilities'])) 164 | 165 | # Technical filter 166 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14.between(30, 70)) 167 | 168 | # Performance filter 169 | ss.where(StockField.CHANGE_PERCENT > 0) 170 | 171 | df = ss.get() 172 | ``` 173 | -------------------------------------------------------------------------------- /docs/screeners/futures.md: -------------------------------------------------------------------------------- 1 | # Futures Screener 2 | 3 | Screen futures contracts across commodities, indices, and currencies. 4 | 5 | ## Quick Start 6 | 7 | ```python 8 | from tvscreener import FuturesScreener, FuturesField 9 | 10 | fs = FuturesScreener() 11 | df = fs.get() 12 | ``` 13 | 14 | ## Field Count 15 | 16 | The Futures Screener has access to **~393 fields** covering: 17 | 18 | - Price data (open, high, low, close) 19 | - Volume and open interest 20 | - Technical indicators 21 | - Performance metrics 22 | 23 | ## Common Fields 24 | 25 | ### Price & Volume 26 | 27 | ```python 28 | FuturesField.PRICE # Current price 29 | FuturesField.OPEN # Day open 30 | FuturesField.HIGH # Day high 31 | FuturesField.LOW # Day low 32 | FuturesField.VOLUME # Trading volume 33 | FuturesField.CHANGE_PERCENT # Daily change 34 | ``` 35 | 36 | ### Technical 37 | 38 | ```python 39 | FuturesField.RELATIVE_STRENGTH_INDEX_14 # RSI(14) 40 | FuturesField.MACD_LEVEL_12_26 # MACD 41 | FuturesField.SIMPLE_MOVING_AVERAGE_50 # SMA 50 42 | FuturesField.SIMPLE_MOVING_AVERAGE_200 # SMA 200 43 | FuturesField.AVERAGE_TRUE_RANGE_14 # ATR 44 | ``` 45 | 46 | ### Performance 47 | 48 | ```python 49 | FuturesField.PERFORMANCE_1_WEEK # 1 week 50 | FuturesField.PERFORMANCE_1_MONTH # 1 month 51 | FuturesField.PERFORMANCE_YTD # Year to date 52 | FuturesField.PERFORMANCE_1_YEAR # 1 year 53 | ``` 54 | 55 | ## Example Screens 56 | 57 | ### All Futures by Volume 58 | 59 | ```python 60 | fs = FuturesScreener() 61 | fs.sort_by(FuturesField.VOLUME, ascending=False) 62 | fs.set_range(0, 50) 63 | fs.select( 64 | FuturesField.NAME, 65 | FuturesField.PRICE, 66 | FuturesField.CHANGE_PERCENT, 67 | FuturesField.VOLUME 68 | ) 69 | 70 | df = fs.get() 71 | ``` 72 | 73 | ### Top Gainers 74 | 75 | ```python 76 | fs = FuturesScreener() 77 | fs.where(FuturesField.CHANGE_PERCENT > 2) 78 | fs.sort_by(FuturesField.CHANGE_PERCENT, ascending=False) 79 | fs.select( 80 | FuturesField.NAME, 81 | FuturesField.PRICE, 82 | FuturesField.CHANGE_PERCENT 83 | ) 84 | 85 | df = fs.get() 86 | ``` 87 | 88 | ### Oversold RSI 89 | 90 | ```python 91 | fs = FuturesScreener() 92 | fs.where(FuturesField.RELATIVE_STRENGTH_INDEX_14 < 30) 93 | fs.select( 94 | FuturesField.NAME, 95 | FuturesField.PRICE, 96 | FuturesField.RELATIVE_STRENGTH_INDEX_14 97 | ) 98 | fs.sort_by(FuturesField.RELATIVE_STRENGTH_INDEX_14, ascending=True) 99 | 100 | df = fs.get() 101 | ``` 102 | 103 | ### High Volatility 104 | 105 | ```python 106 | fs = FuturesScreener() 107 | fs.where(FuturesField.AVERAGE_TRUE_RANGE_14 > 0) 108 | fs.sort_by(FuturesField.AVERAGE_TRUE_RANGE_14, ascending=False) 109 | fs.select( 110 | FuturesField.NAME, 111 | FuturesField.PRICE, 112 | FuturesField.AVERAGE_TRUE_RANGE_14, 113 | FuturesField.VOLATILITY_DAY 114 | ) 115 | fs.set_range(0, 20) 116 | 117 | df = fs.get() 118 | ``` 119 | 120 | ## Specific Futures 121 | 122 | Query specific contracts: 123 | 124 | ```python 125 | fs = FuturesScreener() 126 | fs.symbols = { 127 | "query": {"types": []}, 128 | "tickers": ["CME:ES1!", "CME:NQ1!", "COMEX:GC1!", "NYMEX:CL1!"] 129 | } 130 | fs.select_all() 131 | 132 | df = fs.get() 133 | ``` 134 | 135 | ## Common Futures Symbols 136 | 137 | ### Index Futures 138 | 139 | | Contract | Symbol | 140 | |----------|--------| 141 | | E-mini S&P 500 | `CME:ES1!` | 142 | | E-mini NASDAQ | `CME:NQ1!` | 143 | | E-mini Dow | `CBOT:YM1!` | 144 | | E-mini Russell | `CME:RTY1!` | 145 | 146 | ### Commodity Futures 147 | 148 | | Contract | Symbol | 149 | |----------|--------| 150 | | Gold | `COMEX:GC1!` | 151 | | Silver | `COMEX:SI1!` | 152 | | Crude Oil | `NYMEX:CL1!` | 153 | | Natural Gas | `NYMEX:NG1!` | 154 | | Corn | `CBOT:ZC1!` | 155 | | Soybeans | `CBOT:ZS1!` | 156 | 157 | ### Currency Futures 158 | 159 | | Contract | Symbol | 160 | |----------|--------| 161 | | Euro FX | `CME:6E1!` | 162 | | British Pound | `CME:6B1!` | 163 | | Japanese Yen | `CME:6J1!` | 164 | 165 | ## Multi-Timeframe 166 | 167 | ```python 168 | fs = FuturesScreener() 169 | 170 | # Daily trend 171 | fs.where(FuturesField.PRICE > FuturesField.SIMPLE_MOVING_AVERAGE_50) 172 | 173 | # 4-hour RSI 174 | rsi_4h = FuturesField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') 175 | fs.where(rsi_4h.between(40, 60)) 176 | 177 | fs.select( 178 | FuturesField.NAME, 179 | FuturesField.PRICE, 180 | FuturesField.RELATIVE_STRENGTH_INDEX_14, 181 | rsi_4h 182 | ) 183 | 184 | df = fs.get() 185 | ``` 186 | 187 | ## All Fields 188 | 189 | ```python 190 | fs = FuturesScreener() 191 | fs.select_all() 192 | fs.set_range(0, 100) 193 | 194 | df = fs.get() 195 | print(f"Columns: {len(df.columns)}") # ~393 196 | ``` 197 | 198 | ## Notes 199 | 200 | - Futures symbols typically end with `1!` for the front-month contract 201 | - Volume and open interest are key metrics for futures 202 | - Different exchanges have different trading hours 203 | -------------------------------------------------------------------------------- /docs/guide/streaming.md: -------------------------------------------------------------------------------- 1 | # Streaming 2 | 3 | Get real-time updates with the streaming feature. 4 | 5 | ## Overview 6 | 7 | Use `stream()` to receive continuous updates as data changes: 8 | 9 | ```python 10 | from tvscreener import StockScreener, StockField 11 | 12 | ss = StockScreener() 13 | ss.select(StockField.NAME, StockField.PRICE, StockField.CHANGE_PERCENT) 14 | ss.set_range(0, 10) 15 | 16 | for df in ss.stream(interval=5): 17 | print(df[['name', 'close', 'change']]) 18 | ``` 19 | 20 | ## Parameters 21 | 22 | | Parameter | Type | Default | Description | 23 | |-----------|------|---------|-------------| 24 | | `interval` | int | 10 | Seconds between updates | 25 | 26 | ## Basic Example 27 | 28 | Stream top 10 gainers every 5 seconds: 29 | 30 | ```python 31 | ss = StockScreener() 32 | ss.where(StockField.VOLUME >= 1_000_000) 33 | ss.sort_by(StockField.CHANGE_PERCENT, ascending=False) 34 | ss.set_range(0, 10) 35 | ss.select( 36 | StockField.NAME, 37 | StockField.PRICE, 38 | StockField.CHANGE_PERCENT, 39 | StockField.VOLUME 40 | ) 41 | 42 | for df in ss.stream(interval=5): 43 | print("\n--- Update ---") 44 | print(df[['name', 'close', 'change', 'volume']]) 45 | ``` 46 | 47 | ## Stopping the Stream 48 | 49 | ### Keyboard Interrupt 50 | 51 | Press `Ctrl+C` to stop: 52 | 53 | ```python 54 | try: 55 | for df in ss.stream(interval=5): 56 | print(df) 57 | except KeyboardInterrupt: 58 | print("Stream stopped") 59 | ``` 60 | 61 | ### After N Updates 62 | 63 | ```python 64 | count = 0 65 | max_updates = 10 66 | 67 | for df in ss.stream(interval=5): 68 | count += 1 69 | print(f"Update {count}: {len(df)} rows") 70 | 71 | if count >= max_updates: 72 | break 73 | ``` 74 | 75 | ### Conditional Stop 76 | 77 | ```python 78 | for df in ss.stream(interval=5): 79 | # Stop if any stock drops more than 10% 80 | if (df['change'] < -10).any(): 81 | print("Alert: Stock dropped >10%!") 82 | break 83 | ``` 84 | 85 | ## Practical Examples 86 | 87 | ### Price Alert 88 | 89 | Monitor for price changes: 90 | 91 | ```python 92 | ss = StockScreener() 93 | ss.symbols = { 94 | "query": {"types": []}, 95 | "tickers": ["NASDAQ:AAPL", "NASDAQ:MSFT", "NASDAQ:GOOGL"] 96 | } 97 | ss.select(StockField.NAME, StockField.PRICE, StockField.CHANGE_PERCENT) 98 | 99 | target_price = {"AAPL": 200, "MSFT": 450, "GOOGL": 180} 100 | 101 | for df in ss.stream(interval=10): 102 | for _, row in df.iterrows(): 103 | symbol = row['name'] 104 | price = row['close'] 105 | if symbol in target_price and price >= target_price[symbol]: 106 | print(f"ALERT: {symbol} reached ${price:.2f}!") 107 | ``` 108 | 109 | ### Volume Spike Detection 110 | 111 | ```python 112 | ss = StockScreener() 113 | ss.where(StockField.VOLUME >= 1_000_000) 114 | ss.sort_by(StockField.RELATIVE_VOLUME, ascending=False) 115 | ss.set_range(0, 20) 116 | ss.select( 117 | StockField.NAME, 118 | StockField.PRICE, 119 | StockField.VOLUME, 120 | StockField.RELATIVE_VOLUME 121 | ) 122 | 123 | for df in ss.stream(interval=30): 124 | # Filter for unusual volume (3x average) 125 | spikes = df[df['Relative Volume'] > 3] 126 | if not spikes.empty: 127 | print("\n=== VOLUME SPIKES ===") 128 | print(spikes[['name', 'close', 'volume', 'Relative Volume']]) 129 | ``` 130 | 131 | ### RSI Monitor 132 | 133 | Watch for oversold conditions: 134 | 135 | ```python 136 | ss = StockScreener() 137 | ss.set_index(IndexSymbol.SP500) 138 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 30) 139 | ss.select( 140 | StockField.NAME, 141 | StockField.PRICE, 142 | StockField.RELATIVE_STRENGTH_INDEX_14, 143 | StockField.CHANGE_PERCENT 144 | ) 145 | 146 | for df in ss.stream(interval=60): # Check every minute 147 | if not df.empty: 148 | print(f"\n{len(df)} oversold S&P 500 stocks:") 149 | print(df[['name', 'close', 'RSI']]) 150 | else: 151 | print("No oversold stocks currently") 152 | ``` 153 | 154 | ## With Jupyter Notebooks 155 | 156 | For Jupyter, use IPython display: 157 | 158 | ```python 159 | from IPython.display import clear_output, display 160 | 161 | ss = StockScreener() 162 | ss.set_range(0, 10) 163 | 164 | for df in ss.stream(interval=5): 165 | clear_output(wait=True) 166 | display(df) 167 | ``` 168 | 169 | ## Interval Recommendations 170 | 171 | | Use Case | Interval | 172 | |----------|----------| 173 | | High-frequency monitoring | 1-5 seconds | 174 | | Active trading | 5-15 seconds | 175 | | General monitoring | 30-60 seconds | 176 | | End-of-day review | 300+ seconds | 177 | 178 | !!! warning "Rate Limiting" 179 | Very short intervals may hit TradingView's rate limits. Use intervals of at least 5 seconds. 180 | 181 | ## Best Practices 182 | 183 | 1. **Limit results**: Use `set_range(0, 50)` or less for faster updates 184 | 2. **Select only needed fields**: Reduces data transfer 185 | 3. **Use appropriate intervals**: Balance freshness vs. API load 186 | 4. **Handle errors**: Wrap in try/except for network issues 187 | 5. **Clean up**: Always have a way to stop the stream 188 | -------------------------------------------------------------------------------- /docs/guide/time-intervals.md: -------------------------------------------------------------------------------- 1 | # Time Intervals 2 | 3 | Use technical indicators on different timeframes with `with_interval()`. 4 | 5 | ## Overview 6 | 7 | By default, technical indicators use daily data. Use `with_interval()` to analyze on different timeframes: 8 | 9 | ```python 10 | from tvscreener import StockScreener, StockField 11 | 12 | ss = StockScreener() 13 | 14 | # Hourly RSI 15 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 16 | ss.where(rsi_1h < 30) 17 | 18 | df = ss.get() 19 | ``` 20 | 21 | ## Available Intervals 22 | 23 | | Timeframe | Code | Description | 24 | |-----------|------|-------------| 25 | | 1 minute | `'1'` | Intraday | 26 | | 5 minutes | `'5'` | Intraday | 27 | | 15 minutes | `'15'` | Intraday | 28 | | 30 minutes | `'30'` | Intraday | 29 | | 1 hour | `'60'` | Intraday | 30 | | 2 hours | `'120'` | Intraday | 31 | | 4 hours | `'240'` | Intraday | 32 | | Daily | `'1D'` | Default | 33 | | Weekly | `'1W'` | Longer term | 34 | | Monthly | `'1M'` | Longer term | 35 | 36 | ## Syntax 37 | 38 | ```python 39 | field.with_interval('interval_code') 40 | ``` 41 | 42 | ## Examples 43 | 44 | ### RSI on Different Timeframes 45 | 46 | ```python 47 | ss = StockScreener() 48 | 49 | # Default daily RSI 50 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 40) 51 | 52 | # 1-hour RSI 53 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 54 | ss.where(rsi_1h < 30) 55 | 56 | # 4-hour RSI 57 | rsi_4h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') 58 | ss.where(rsi_4h < 35) 59 | 60 | df = ss.get() 61 | ``` 62 | 63 | ### MACD on Multiple Timeframes 64 | 65 | ```python 66 | ss = StockScreener() 67 | 68 | # Daily MACD bullish 69 | ss.where(StockField.MACD_LEVEL_12_26 > 0) 70 | 71 | # Hourly MACD also bullish 72 | macd_1h = StockField.MACD_LEVEL_12_26.with_interval('60') 73 | ss.where(macd_1h > 0) 74 | 75 | df = ss.get() 76 | ``` 77 | 78 | ### Moving Averages 79 | 80 | ```python 81 | ss = StockScreener() 82 | 83 | # Price above weekly 50 SMA 84 | sma50_weekly = StockField.SIMPLE_MOVING_AVERAGE_50.with_interval('1W') 85 | ss.where(StockField.PRICE > sma50_weekly) 86 | 87 | df = ss.get() 88 | ``` 89 | 90 | ## Multi-Timeframe Analysis 91 | 92 | ### Higher Timeframe Confirmation 93 | 94 | Confirm signals on multiple timeframes: 95 | 96 | ```python 97 | ss = StockScreener() 98 | 99 | # Daily trend: above 50 SMA 100 | ss.where(StockField.PRICE > StockField.SIMPLE_MOVING_AVERAGE_50) 101 | 102 | # Hourly momentum: RSI oversold 103 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 104 | ss.where(rsi_1h < 40) 105 | 106 | # 15-minute entry: RSI bouncing 107 | rsi_15m = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('15') 108 | ss.where(rsi_15m.between(30, 50)) 109 | 110 | ss.select( 111 | StockField.NAME, 112 | StockField.PRICE, 113 | StockField.RELATIVE_STRENGTH_INDEX_14, # Daily 114 | rsi_1h, # Hourly 115 | rsi_15m # 15-minute 116 | ) 117 | 118 | df = ss.get() 119 | ``` 120 | 121 | ### Weekly + Daily Confirmation 122 | 123 | ```python 124 | ss = StockScreener() 125 | 126 | # Weekly RSI not overbought 127 | rsi_weekly = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('1W') 128 | ss.where(rsi_weekly < 60) 129 | 130 | # Daily RSI oversold (entry signal) 131 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 30) 132 | 133 | df = ss.get() 134 | ``` 135 | 136 | ### 4-Hour + 1-Hour MACD 137 | 138 | ```python 139 | ss = StockScreener() 140 | 141 | # 4-hour MACD positive (trend) 142 | macd_4h = StockField.MACD_LEVEL_12_26.with_interval('240') 143 | ss.where(macd_4h > 0) 144 | 145 | # 1-hour MACD crossing up (entry) 146 | macd_1h = StockField.MACD_LEVEL_12_26.with_interval('60') 147 | signal_1h = StockField.MACD_SIGNAL_12_26_9.with_interval('60') 148 | ss.where(macd_1h > signal_1h) 149 | 150 | df = ss.get() 151 | ``` 152 | 153 | ## Selecting Interval Fields 154 | 155 | Include interval fields in your selection: 156 | 157 | ```python 158 | ss = StockScreener() 159 | 160 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 161 | rsi_4h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') 162 | 163 | ss.select( 164 | StockField.NAME, 165 | StockField.PRICE, 166 | StockField.RELATIVE_STRENGTH_INDEX_14, # Daily (default) 167 | rsi_1h, # Hourly 168 | rsi_4h # 4-hour 169 | ) 170 | 171 | df = ss.get() 172 | ``` 173 | 174 | ## Which Fields Support Intervals? 175 | 176 | Technical indicators support intervals: 177 | 178 | - RSI, MACD, Stochastic, CCI, etc. 179 | - Moving averages (SMA, EMA) 180 | - Bollinger Bands 181 | - ATR, ADX 182 | - Volume indicators 183 | 184 | Fundamental fields do NOT support intervals: 185 | 186 | - P/E, P/B, Market Cap (these are point-in-time values) 187 | - Revenue, Earnings, Margins 188 | - Dividend data 189 | 190 | ## Best Practices 191 | 192 | !!! tip "Start with Higher Timeframes" 193 | Use higher timeframes for trend direction, lower timeframes for entry timing. 194 | 195 | !!! tip "Avoid Over-Filtering" 196 | More timeframes = fewer results. Start with 2-3 timeframes. 197 | 198 | !!! tip "Match Your Trading Style" 199 | - Day trading: 1min, 5min, 15min 200 | - Swing trading: 1h, 4h, 1D 201 | - Position trading: 1D, 1W, 1M 202 | -------------------------------------------------------------------------------- /docs/screeners/forex.md: -------------------------------------------------------------------------------- 1 | # Forex Screener 2 | 3 | Screen currency pairs from the foreign exchange market. 4 | 5 | ## Quick Start 6 | 7 | ```python 8 | from tvscreener import ForexScreener, ForexField 9 | 10 | fs = ForexScreener() 11 | df = fs.get() 12 | ``` 13 | 14 | ## Field Count 15 | 16 | The Forex Screener has access to **~2,965 fields** covering: 17 | 18 | - Price & Volume data 19 | - Technical indicators 20 | - Performance metrics 21 | - Currency-specific metrics 22 | 23 | ## Common Fields 24 | 25 | ### Price 26 | 27 | ```python 28 | ForexField.PRICE # Current price 29 | ForexField.OPEN # Day open 30 | ForexField.HIGH # Day high 31 | ForexField.LOW # Day low 32 | ForexField.CHANGE_PERCENT # Daily change 33 | ``` 34 | 35 | ### Technical 36 | 37 | ```python 38 | ForexField.RELATIVE_STRENGTH_INDEX_14 # RSI(14) 39 | ForexField.MACD_LEVEL_12_26 # MACD 40 | ForexField.MACD_SIGNAL_12_26_9 # MACD Signal 41 | ForexField.SIMPLE_MOVING_AVERAGE_50 # SMA 50 42 | ForexField.SIMPLE_MOVING_AVERAGE_200 # SMA 200 43 | ForexField.EXPONENTIAL_MOVING_AVERAGE_20 # EMA 20 44 | ForexField.AVERAGE_TRUE_RANGE_14 # ATR 45 | ForexField.STOCHASTIC_K_14_3_3 # Stochastic %K 46 | ForexField.STOCHASTIC_D_14_3_3 # Stochastic %D 47 | ``` 48 | 49 | ### Performance 50 | 51 | ```python 52 | ForexField.PERFORMANCE_1_WEEK # 1 week change 53 | ForexField.PERFORMANCE_1_MONTH # 1 month change 54 | ForexField.PERFORMANCE_3_MONTH # 3 month change 55 | ForexField.PERFORMANCE_YTD # Year to date 56 | ForexField.PERFORMANCE_1_YEAR # 1 year 57 | ``` 58 | 59 | ## Example Screens 60 | 61 | ### Major Pairs Only 62 | 63 | ```python 64 | fs = ForexScreener() 65 | fs.search("USD") # Pairs containing USD 66 | fs.set_range(0, 50) 67 | fs.select( 68 | ForexField.NAME, 69 | ForexField.PRICE, 70 | ForexField.CHANGE_PERCENT 71 | ) 72 | 73 | df = fs.get() 74 | ``` 75 | 76 | ### Top Movers 77 | 78 | ```python 79 | fs = ForexScreener() 80 | fs.sort_by(ForexField.CHANGE_PERCENT, ascending=False) 81 | fs.set_range(0, 20) 82 | fs.select( 83 | ForexField.NAME, 84 | ForexField.PRICE, 85 | ForexField.CHANGE_PERCENT, 86 | ForexField.HIGH, 87 | ForexField.LOW 88 | ) 89 | 90 | df = fs.get() 91 | ``` 92 | 93 | ### Oversold RSI 94 | 95 | ```python 96 | fs = ForexScreener() 97 | fs.where(ForexField.RELATIVE_STRENGTH_INDEX_14 < 30) 98 | fs.select( 99 | ForexField.NAME, 100 | ForexField.PRICE, 101 | ForexField.RELATIVE_STRENGTH_INDEX_14, 102 | ForexField.CHANGE_PERCENT 103 | ) 104 | fs.sort_by(ForexField.RELATIVE_STRENGTH_INDEX_14, ascending=True) 105 | 106 | df = fs.get() 107 | ``` 108 | 109 | ### Golden Cross Setup 110 | 111 | Price above moving averages: 112 | 113 | ```python 114 | fs = ForexScreener() 115 | fs.where(ForexField.PRICE > ForexField.SIMPLE_MOVING_AVERAGE_50) 116 | fs.where(ForexField.SIMPLE_MOVING_AVERAGE_50 > ForexField.SIMPLE_MOVING_AVERAGE_200) 117 | fs.select( 118 | ForexField.NAME, 119 | ForexField.PRICE, 120 | ForexField.SIMPLE_MOVING_AVERAGE_50, 121 | ForexField.SIMPLE_MOVING_AVERAGE_200 122 | ) 123 | 124 | df = fs.get() 125 | ``` 126 | 127 | ### High ATR (Volatility) 128 | 129 | ```python 130 | fs = ForexScreener() 131 | fs.where(ForexField.AVERAGE_TRUE_RANGE_14 > 0.01) 132 | fs.sort_by(ForexField.AVERAGE_TRUE_RANGE_14, ascending=False) 133 | fs.select( 134 | ForexField.NAME, 135 | ForexField.PRICE, 136 | ForexField.AVERAGE_TRUE_RANGE_14, 137 | ForexField.VOLATILITY_DAY 138 | ) 139 | 140 | df = fs.get() 141 | ``` 142 | 143 | ## Multi-Timeframe Analysis 144 | 145 | ```python 146 | fs = ForexScreener() 147 | 148 | # Daily RSI 149 | fs.where(ForexField.RELATIVE_STRENGTH_INDEX_14.between(40, 60)) 150 | 151 | # 4-hour RSI oversold 152 | rsi_4h = ForexField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') 153 | fs.where(rsi_4h < 35) 154 | 155 | # 1-hour MACD bullish 156 | macd_1h = ForexField.MACD_LEVEL_12_26.with_interval('60') 157 | fs.where(macd_1h > 0) 158 | 159 | fs.select( 160 | ForexField.NAME, 161 | ForexField.PRICE, 162 | ForexField.RELATIVE_STRENGTH_INDEX_14, 163 | rsi_4h, 164 | macd_1h 165 | ) 166 | 167 | df = fs.get() 168 | ``` 169 | 170 | ## Specific Currency Pairs 171 | 172 | Query specific pairs: 173 | 174 | ```python 175 | fs = ForexScreener() 176 | fs.symbols = { 177 | "query": {"types": []}, 178 | "tickers": ["FX:EURUSD", "FX:GBPUSD", "FX:USDJPY", "FX:AUDUSD"] 179 | } 180 | fs.select_all() 181 | 182 | df = fs.get() 183 | ``` 184 | 185 | ## Major Pairs 186 | 187 | Common major currency pairs: 188 | 189 | | Pair | Exchange Symbol | 190 | |------|-----------------| 191 | | EUR/USD | `FX:EURUSD` | 192 | | GBP/USD | `FX:GBPUSD` | 193 | | USD/JPY | `FX:USDJPY` | 194 | | USD/CHF | `FX:USDCHF` | 195 | | AUD/USD | `FX:AUDUSD` | 196 | | USD/CAD | `FX:USDCAD` | 197 | | NZD/USD | `FX:NZDUSD` | 198 | 199 | ## All Fields 200 | 201 | ```python 202 | fs = ForexScreener() 203 | fs.select_all() 204 | fs.set_range(0, 100) 205 | 206 | df = fs.get() 207 | print(f"Columns: {len(df.columns)}") # ~2,965 208 | ``` 209 | 210 | ## Notes 211 | 212 | - Forex markets trade 24/5 (closed on weekends) 213 | - Price represents exchange rate (base/quote) 214 | - ATR and volatility help identify trading opportunities 215 | -------------------------------------------------------------------------------- /docs/guide/selecting-fields.md: -------------------------------------------------------------------------------- 1 | # Selecting Fields 2 | 3 | Control which data columns are returned in your results. 4 | 5 | ## Basic Selection 6 | 7 | Use `select()` to choose specific fields: 8 | 9 | ```python 10 | from tvscreener import StockScreener, StockField 11 | 12 | ss = StockScreener() 13 | ss.select( 14 | StockField.NAME, 15 | StockField.PRICE, 16 | StockField.CHANGE_PERCENT, 17 | StockField.VOLUME 18 | ) 19 | df = ss.get() 20 | ``` 21 | 22 | ## Select All Fields 23 | 24 | Retrieve all ~3,500 available fields: 25 | 26 | ```python 27 | ss = StockScreener() 28 | ss.select_all() 29 | df = ss.get() 30 | 31 | print(f"Columns: {len(df.columns)}") # ~3,500 columns 32 | ``` 33 | 34 | !!! note "Performance" 35 | `select_all()` returns a large DataFrame. Use it for exploration, then narrow down to needed fields. 36 | 37 | ## Field Discovery 38 | 39 | ### Search by Name 40 | 41 | ```python 42 | # Find RSI-related fields 43 | matches = StockField.search("rsi") 44 | for field in matches[:10]: 45 | print(field.name) 46 | ``` 47 | 48 | ### Technical Indicators 49 | 50 | ```python 51 | # Get all technical indicator fields 52 | technicals = StockField.technicals() 53 | for field in technicals[:10]: 54 | print(field.name) 55 | ``` 56 | 57 | ### Recommendations 58 | 59 | ```python 60 | # Get analyst recommendation fields 61 | recs = StockField.recommendations() 62 | for field in recs: 63 | print(field.name) 64 | ``` 65 | 66 | ## Field Presets 67 | 68 | Pre-defined groups of commonly used fields: 69 | 70 | ```python 71 | # Valuation fields 72 | ss.select(*StockField.valuations()) 73 | 74 | # Dividend fields 75 | ss.select(*StockField.dividends()) 76 | 77 | # Oscillator fields 78 | ss.select(*StockField.oscillators()) 79 | 80 | # Moving averages 81 | ss.select(*StockField.moving_averages()) 82 | 83 | # Performance fields 84 | ss.select(*StockField.performance()) 85 | ``` 86 | 87 | ## Field Properties 88 | 89 | Each field has properties you can inspect: 90 | 91 | ```python 92 | field = StockField.PRICE 93 | 94 | print(field.label) # Human-readable name 95 | print(field.field_name) # API field name 96 | print(field.format) # Data format (currency, percent, etc.) 97 | ``` 98 | 99 | ## Common Field Categories 100 | 101 | ### Price & Volume 102 | 103 | ```python 104 | ss.select( 105 | StockField.PRICE, 106 | StockField.OPEN, 107 | StockField.HIGH, 108 | StockField.LOW, 109 | StockField.CLOSE, 110 | StockField.VOLUME 111 | ) 112 | ``` 113 | 114 | ### Valuation 115 | 116 | ```python 117 | ss.select( 118 | StockField.PE_RATIO_TTM, 119 | StockField.PRICE_TO_BOOK_FY, 120 | StockField.PRICE_TO_SALES_FY, 121 | StockField.EV_TO_EBITDA_TTM, 122 | StockField.MARKET_CAPITALIZATION 123 | ) 124 | ``` 125 | 126 | ### Dividends 127 | 128 | ```python 129 | ss.select( 130 | StockField.DIVIDEND_YIELD_FY, 131 | StockField.DIVIDENDS_PER_SHARE_FY, 132 | StockField.PAYOUT_RATIO_TTM, 133 | StockField.EX_DIVIDEND_DATE 134 | ) 135 | ``` 136 | 137 | ### Performance 138 | 139 | ```python 140 | ss.select( 141 | StockField.CHANGE_PERCENT, 142 | StockField.PERFORMANCE_1_WEEK, 143 | StockField.PERFORMANCE_1_MONTH, 144 | StockField.PERFORMANCE_3_MONTH, 145 | StockField.PERFORMANCE_6_MONTH, 146 | StockField.PERFORMANCE_YTD, 147 | StockField.PERFORMANCE_1_YEAR 148 | ) 149 | ``` 150 | 151 | ### Technical Indicators 152 | 153 | ```python 154 | ss.select( 155 | StockField.RELATIVE_STRENGTH_INDEX_14, 156 | StockField.MACD_LEVEL_12_26, 157 | StockField.MACD_SIGNAL_12_26_9, 158 | StockField.SIMPLE_MOVING_AVERAGE_50, 159 | StockField.SIMPLE_MOVING_AVERAGE_200, 160 | StockField.EXPONENTIAL_MOVING_AVERAGE_20, 161 | StockField.AVERAGE_TRUE_RANGE_14, 162 | StockField.BOLLINGER_UPPER_BAND_20, 163 | StockField.BOLLINGER_LOWER_BAND_20 164 | ) 165 | ``` 166 | 167 | ### Profitability 168 | 169 | ```python 170 | ss.select( 171 | StockField.RETURN_ON_EQUITY_TTM, 172 | StockField.RETURN_ON_ASSETS_TTM, 173 | StockField.GROSS_MARGIN_TTM, 174 | StockField.NET_MARGIN_TTM, 175 | StockField.OPERATING_MARGIN_TTM 176 | ) 177 | ``` 178 | 179 | ## Combining Selection with Filtering 180 | 181 | ```python 182 | ss = StockScreener() 183 | 184 | # Filter 185 | ss.where(StockField.PRICE > 10) 186 | ss.where(StockField.MARKET_CAPITALIZATION > 1e9) 187 | 188 | # Select (order doesn't matter) 189 | ss.select( 190 | StockField.NAME, 191 | StockField.PRICE, 192 | StockField.MARKET_CAPITALIZATION 193 | ) 194 | 195 | df = ss.get() 196 | ``` 197 | 198 | ## Chaining Methods 199 | 200 | ```python 201 | df = ( 202 | StockScreener() 203 | .select(StockField.NAME, StockField.PRICE, StockField.VOLUME) 204 | .where(StockField.PRICE > 100) 205 | .sort_by(StockField.VOLUME, ascending=False) 206 | .get() 207 | ) 208 | ``` 209 | 210 | ## Default Fields 211 | 212 | If you don't call `select()`, a default set of fields is returned: 213 | 214 | ```python 215 | ss = StockScreener() 216 | df = ss.get() 217 | print(df.columns.tolist()) 218 | ``` 219 | 220 | ## Field Count by Screener 221 | 222 | | Screener | Field Count | 223 | |----------|-------------| 224 | | Stock | ~3,526 | 225 | | Crypto | ~3,108 | 226 | | Forex | ~2,965 | 227 | | Bond | ~201 | 228 | | Futures | ~393 | 229 | | Coin | ~3,026 | 230 | -------------------------------------------------------------------------------- /.dev/codegen/Generate.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "initial_id", 7 | "metadata": { 8 | "collapsed": true, 9 | "ExecuteTime": { 10 | "end_time": "2023-08-08T09:09:20.755952003Z", 11 | "start_time": "2023-08-08T09:09:20.755639510Z" 12 | } 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "import json\n", 17 | "from generate import write, scrap_columns, generate_columns, fill_template\n", 18 | "\n", 19 | "%load_ext autoreload\n", 20 | "%autoreload 2" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 3, 26 | "outputs": [], 27 | "source": [ 28 | "def save_columns(url, name):\n", 29 | " selenium_columns = scrap_columns(url)\n", 30 | " with open(f'{name}.json', 'w') as f:\n", 31 | " json.dump(selenium_columns, f)\n", 32 | " \n", 33 | "def generate_code_files(name):\n", 34 | " with open(f'{name.lower()}.json') as f:\n", 35 | " columns = json.load(f)\n", 36 | " formatted_columns = generate_columns(columns)\n", 37 | " template = fill_template(name, formatted_columns)\n", 38 | " write(name.lower(), template)" 39 | ], 40 | "metadata": { 41 | "collapsed": false, 42 | "ExecuteTime": { 43 | "end_time": "2023-08-08T09:09:27.879269457Z", 44 | "start_time": "2023-08-08T09:09:27.872493388Z" 45 | } 46 | }, 47 | "id": "d5fa1f7d53053c3e" 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "outputs": [], 53 | "source": [ 54 | "stock_url = \"https://www.tradingview.com/screener/\"\n", 55 | "save_columns(stock_url, 'stock')" 56 | ], 57 | "metadata": { 58 | "collapsed": false 59 | }, 60 | "id": "54db65ab7c0ba6e6" 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "outputs": [], 66 | "source": [ 67 | "crypto_url = \"https://www.tradingview.com/crypto-screener/\"\n", 68 | "save_columns(crypto_url, 'crypto')" 69 | ], 70 | "metadata": { 71 | "collapsed": false 72 | }, 73 | "id": "5842322bb724a00b" 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "outputs": [], 79 | "source": [ 80 | "forex_url = \"https://www.tradingview.com/forex-screener/\"\n", 81 | "save_columns(forex_url, 'forex')" 82 | ], 83 | "metadata": { 84 | "collapsed": false 85 | }, 86 | "id": "25c9bfa6c3bd8da4" 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "outputs": [], 92 | "source": [ 93 | "etf_url = \"https://www.tradingview.com/etf-screener/\"\n", 94 | "save_columns(etf_url, 'etf')" 95 | ], 96 | "metadata": { 97 | "collapsed": false 98 | }, 99 | "id": "462e7fa9db2bd99a" 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 4, 104 | "outputs": [], 105 | "source": [ 106 | "generate_code_files('Stock')" 107 | ], 108 | "metadata": { 109 | "collapsed": false, 110 | "ExecuteTime": { 111 | "end_time": "2023-08-08T09:09:30.637092897Z", 112 | "start_time": "2023-08-08T09:09:30.629107433Z" 113 | } 114 | }, 115 | "id": "9f576a5a2aa8f757" 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 5, 120 | "outputs": [ 121 | { 122 | "name": "stdout", 123 | "output_type": "stream", 124 | "text": [ 125 | "Column MoneyFlow not found in the interval columns\n", 126 | "Column Value.Traded not found in the interval columns\n" 127 | ] 128 | } 129 | ], 130 | "source": [ 131 | "generate_code_files('Crypto')" 132 | ], 133 | "metadata": { 134 | "collapsed": false, 135 | "ExecuteTime": { 136 | "end_time": "2023-08-08T09:09:31.219942802Z", 137 | "start_time": "2023-08-08T09:09:31.212637210Z" 138 | } 139 | }, 140 | "id": "933ca7d35da3c8cf" 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 6, 145 | "outputs": [ 146 | { 147 | "name": "stdout", 148 | "output_type": "stream", 149 | "text": [ 150 | "Column MoneyFlow not found in the interval columns\n", 151 | "Column Value.Traded not found in the interval columns\n", 152 | "Column relative_volume_10d_calc not found in the interval columns\n", 153 | "Column volume not found in the interval columns\n" 154 | ] 155 | } 156 | ], 157 | "source": [ 158 | "generate_code_files('Forex')" 159 | ], 160 | "metadata": { 161 | "collapsed": false, 162 | "ExecuteTime": { 163 | "end_time": "2023-08-08T09:09:31.683820525Z", 164 | "start_time": "2023-08-08T09:09:31.676674132Z" 165 | } 166 | }, 167 | "id": "d70eeeff7c42494a" 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": null, 172 | "outputs": [], 173 | "source": [ 174 | "common = [\"logoid\",\n", 175 | " \"name\",\n", 176 | " \"description\",\n", 177 | " \"type\",\n", 178 | " \"subtype\"]\n", 179 | "\n", 180 | "with open(f'main.json', 'w') as f:\n", 181 | " json.dump({\"main\": common}, f)" 182 | ], 183 | "metadata": { 184 | "collapsed": false 185 | }, 186 | "id": "23094b57d7f195da" 187 | } 188 | ], 189 | "metadata": { 190 | "kernelspec": { 191 | "display_name": "Python 3", 192 | "language": "python", 193 | "name": "python3" 194 | }, 195 | "language_info": { 196 | "codemirror_mode": { 197 | "name": "ipython", 198 | "version": 2 199 | }, 200 | "file_extension": ".py", 201 | "mimetype": "text/x-python", 202 | "name": "python", 203 | "nbconvert_exporter": "python", 204 | "pygments_lexer": "ipython2", 205 | "version": "2.7.6" 206 | } 207 | }, 208 | "nbformat": 4, 209 | "nbformat_minor": 5 210 | } 211 | -------------------------------------------------------------------------------- /docs/api/screeners.md: -------------------------------------------------------------------------------- 1 | # Screeners API Reference 2 | 3 | All screener classes share a common base API. 4 | 5 | ## Available Screeners 6 | 7 | | Class | Import | Fields Class | 8 | |-------|--------|--------------| 9 | | `StockScreener` | `from tvscreener import StockScreener` | `StockField` | 10 | | `CryptoScreener` | `from tvscreener import CryptoScreener` | `CryptoField` | 11 | | `ForexScreener` | `from tvscreener import ForexScreener` | `ForexField` | 12 | | `BondScreener` | `from tvscreener import BondScreener` | `BondField` | 13 | | `FuturesScreener` | `from tvscreener import FuturesScreener` | `FuturesField` | 14 | | `CoinScreener` | `from tvscreener import CoinScreener` | `CoinField` | 15 | 16 | ## Common Methods 17 | 18 | ### `get()` 19 | 20 | Execute the query and return results as a pandas DataFrame. 21 | 22 | ```python 23 | ss = StockScreener() 24 | df = ss.get() 25 | ``` 26 | 27 | **Returns:** `pandas.DataFrame` 28 | 29 | --- 30 | 31 | ### `where(condition)` 32 | 33 | Add a filter condition. 34 | 35 | ```python 36 | # New syntax (v0.1.0+) 37 | ss.where(StockField.PRICE > 100) 38 | ss.where(StockField.PE_RATIO_TTM.between(10, 25)) 39 | 40 | # Legacy syntax 41 | ss.where(StockField.PRICE, FilterOperator.ABOVE, 100) 42 | ``` 43 | 44 | **Parameters:** 45 | - `condition`: A `FieldCondition` from comparison operators 46 | 47 | **Returns:** `self` (for chaining) 48 | 49 | --- 50 | 51 | ### `select(*fields)` 52 | 53 | Specify which fields to include in results. 54 | 55 | ```python 56 | ss.select( 57 | StockField.NAME, 58 | StockField.PRICE, 59 | StockField.VOLUME 60 | ) 61 | ``` 62 | 63 | **Parameters:** 64 | - `*fields`: One or more Field enum values 65 | 66 | **Returns:** `self` (for chaining) 67 | 68 | --- 69 | 70 | ### `select_all()` 71 | 72 | Select all available fields (~3,500 for stocks). 73 | 74 | ```python 75 | ss.select_all() 76 | ``` 77 | 78 | **Returns:** `self` (for chaining) 79 | 80 | --- 81 | 82 | ### `sort_by(field, ascending=True)` 83 | 84 | Sort results by a field. 85 | 86 | ```python 87 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 88 | ``` 89 | 90 | **Parameters:** 91 | - `field`: Field enum value to sort by 92 | - `ascending`: `True` for ascending, `False` for descending 93 | 94 | **Returns:** `self` (for chaining) 95 | 96 | --- 97 | 98 | ### `set_range(from_index, to_index)` 99 | 100 | Set pagination range. 101 | 102 | ```python 103 | ss.set_range(0, 100) # First 100 results 104 | ss.set_range(100, 200) # Results 101-200 105 | ``` 106 | 107 | **Parameters:** 108 | - `from_index`: Starting index (0-based) 109 | - `to_index`: Ending index (exclusive), max 5000 110 | 111 | **Returns:** `self` (for chaining) 112 | 113 | --- 114 | 115 | ### `search(query)` 116 | 117 | Text search across name and description. 118 | 119 | ```python 120 | ss.search('semiconductor') 121 | ``` 122 | 123 | **Parameters:** 124 | - `query`: Search string 125 | 126 | **Returns:** `self` (for chaining) 127 | 128 | --- 129 | 130 | ### `stream(interval=10)` 131 | 132 | Stream results with periodic updates. 133 | 134 | ```python 135 | for df in ss.stream(interval=5): 136 | print(df) 137 | ``` 138 | 139 | **Parameters:** 140 | - `interval`: Seconds between updates (default: 10) 141 | 142 | **Yields:** `pandas.DataFrame` on each update 143 | 144 | --- 145 | 146 | ## StockScreener-Specific Methods 147 | 148 | ### `set_index(*indices)` 149 | 150 | Filter to index constituents (Stock only). 151 | 152 | ```python 153 | from tvscreener import IndexSymbol 154 | 155 | ss.set_index(IndexSymbol.SP500) 156 | ss.set_index(IndexSymbol.NASDAQ_100, IndexSymbol.DOW_JONES) 157 | ``` 158 | 159 | **Parameters:** 160 | - `*indices`: One or more `IndexSymbol` values 161 | 162 | **Returns:** `self` (for chaining) 163 | 164 | --- 165 | 166 | ### `set_markets(*markets)` 167 | 168 | Filter by market region. 169 | 170 | ```python 171 | from tvscreener import Market 172 | 173 | ss.set_markets(Market.AMERICA) 174 | ss.set_markets(Market.JAPAN) 175 | ``` 176 | 177 | **Parameters:** 178 | - `*markets`: One or more `Market` values 179 | 180 | **Returns:** `self` (for chaining) 181 | 182 | --- 183 | 184 | ### `set_symbol_types(*types)` 185 | 186 | Filter by security type. 187 | 188 | ```python 189 | from tvscreener import SymbolType 190 | 191 | ss.set_symbol_types(SymbolType.COMMON_STOCK) 192 | ss.set_symbol_types(SymbolType.ETF) 193 | ``` 194 | 195 | **Parameters:** 196 | - `*types`: One or more `SymbolType` values 197 | 198 | **Returns:** `self` (for chaining) 199 | 200 | --- 201 | 202 | ## Properties 203 | 204 | ### `symbols` 205 | 206 | Direct access to symbol configuration. 207 | 208 | ```python 209 | ss.symbols = { 210 | "query": {"types": []}, 211 | "tickers": ["NASDAQ:AAPL", "NYSE:IBM"] 212 | } 213 | ``` 214 | 215 | --- 216 | 217 | ## Method Chaining 218 | 219 | All configuration methods return `self`, enabling fluent syntax: 220 | 221 | ```python 222 | df = ( 223 | StockScreener() 224 | .select(StockField.NAME, StockField.PRICE) 225 | .where(StockField.PRICE > 100) 226 | .sort_by(StockField.VOLUME, ascending=False) 227 | .set_range(0, 50) 228 | .get() 229 | ) 230 | ``` 231 | 232 | --- 233 | 234 | ## Full Example 235 | 236 | ```python 237 | from tvscreener import StockScreener, StockField, IndexSymbol 238 | 239 | ss = StockScreener() 240 | 241 | # Filter to S&P 500 242 | ss.set_index(IndexSymbol.SP500) 243 | 244 | # Add conditions 245 | ss.where(StockField.PRICE.between(50, 500)) 246 | ss.where(StockField.PE_RATIO_TTM.between(10, 30)) 247 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 50) 248 | 249 | # Select fields 250 | ss.select( 251 | StockField.NAME, 252 | StockField.PRICE, 253 | StockField.PE_RATIO_TTM, 254 | StockField.RELATIVE_STRENGTH_INDEX_14, 255 | StockField.MARKET_CAPITALIZATION 256 | ) 257 | 258 | # Sort and paginate 259 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 260 | ss.set_range(0, 100) 261 | 262 | # Execute 263 | df = ss.get() 264 | ``` 265 | -------------------------------------------------------------------------------- /tests/functional/test_stockscreener.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | from unittest.mock import patch 4 | 5 | import pandas as pd 6 | 7 | from tvscreener import StockScreener, MalformedRequestException, \ 8 | ExtraFilter, FilterOperator, StockField 9 | from tvscreener.field import SymbolType, Market, SubMarket, Country, Exchange 10 | 11 | 12 | class TestScreener(unittest.TestCase): 13 | 14 | @unittest.mock.patch('sys.stdout', new_callable=io.StringIO) 15 | def test_stdout(self, mock_stdout): 16 | ss = StockScreener() 17 | ss.get(print_request=True) 18 | self.assertIn("filter", mock_stdout.getvalue()) 19 | 20 | def test_malformed_request(self): 21 | ss = StockScreener() 22 | ss.add_filter(StockField.TYPE, FilterOperator.ABOVE_OR_EQUAL, "test") 23 | with self.assertRaises(MalformedRequestException): 24 | ss.get() 25 | 26 | def test_range(self): 27 | ss = StockScreener() 28 | df = ss.get() 29 | self.assertEqual(150, len(df)) 30 | 31 | def test_search(self): 32 | ss = StockScreener() 33 | ss.set_symbol_types(SymbolType.COMMON_STOCK) 34 | ss.search('AA') 35 | df = ss.get() 36 | self.assertGreater(len(df), 80) 37 | 38 | self.assertEqual(df.loc[0, "Symbol"], "NASDAQ:AAPL") 39 | self.assertEqual(df.loc[0, "Name"], "AAPL") 40 | 41 | def test_column_order(self): 42 | ss = StockScreener() 43 | df = ss.get() 44 | 45 | self.assertEqual(df.columns[0], "Symbol") 46 | self.assertEqual(df.columns[1], "Name") 47 | self.assertEqual(df.columns[2], "Description") 48 | 49 | self.assertEqual(df.loc[0, "Symbol"], "NASDAQ:AAPL") 50 | self.assertEqual(df.loc[0, "Name"], "AAPL") 51 | 52 | def test_not_multiindex(self): 53 | ss = StockScreener() 54 | df = ss.get() 55 | self.assertIsInstance(df.index, pd.Index) 56 | 57 | self.assertEqual("Symbol", df.columns[0]) 58 | self.assertEqual("Name", df.columns[1]) 59 | self.assertEqual("Description", df.columns[2]) 60 | 61 | self.assertEqual("NASDAQ:AAPL", df.loc[0, "Symbol"]) 62 | self.assertEqual("AAPL", df.loc[0, "Name"]) 63 | 64 | def test_multiindex(self): 65 | ss = StockScreener() 66 | df = ss.get() 67 | df.set_technical_columns() 68 | self.assertNotIsInstance(df.index, pd.MultiIndex) 69 | 70 | self.assertEqual(("symbol", "Symbol"), df.columns[0]) 71 | self.assertEqual(("name", "Name"), df.columns[1]) 72 | self.assertEqual(("description", "Description"), df.columns[2]) 73 | 74 | self.assertEqual("NASDAQ:AAPL", df.loc[0, ("symbol", "Symbol")]) 75 | self.assertEqual("AAPL", df.loc[0, ("name", "Name")]) 76 | 77 | def test_technical_index(self): 78 | ss = StockScreener() 79 | df = ss.get() 80 | df.set_technical_columns(only=True) 81 | self.assertIsInstance(df.index, pd.Index) 82 | 83 | self.assertEqual(df.columns[0], "symbol") 84 | self.assertEqual(df.columns[1], "name") 85 | self.assertEqual(df.columns[2], "description") 86 | 87 | self.assertEqual("NASDAQ:AAPL", df.loc[0, "symbol"]) 88 | self.assertEqual("AAPL", df.loc[0, "name"]) 89 | 90 | def test_primary_filter(self): 91 | ss = StockScreener() 92 | ss.add_filter(ExtraFilter.PRIMARY, FilterOperator.EQUAL, True) 93 | df = ss.get() 94 | self.assertEqual(150, len(df)) 95 | 96 | self.assertEqual("NASDAQ:AAPL", df.loc[0, "Symbol"]) 97 | self.assertEqual("AAPL", df.loc[0, "Name"]) 98 | 99 | def test_market(self): 100 | ss = StockScreener() 101 | ss.set_markets(Market.ARGENTINA) 102 | df = ss.get() 103 | self.assertEqual(150, len(df)) 104 | 105 | # WARNING: Order is not guaranteed 106 | self.assertIn("BCBA:AA", df.loc[0, "Symbol"], ) 107 | self.assertIn("AA", df.loc[0, "Name"]) 108 | 109 | def test_submarket(self): 110 | ss = StockScreener() 111 | ss.add_filter(StockField.SUBMARKET, FilterOperator.EQUAL, SubMarket.OTCQB) 112 | df = ss.get() 113 | self.assertEqual(150, len(df)) 114 | 115 | self.assertEqual("OTC:PLDGP", df.loc[0, "Symbol"]) 116 | self.assertEqual("PLDGP", df.loc[0, "Name"]) 117 | 118 | def test_submarket_pink(self): 119 | ss = StockScreener() 120 | ss.add_filter(StockField.SUBMARKET, FilterOperator.EQUAL, SubMarket.PINK) 121 | df = ss.get() 122 | self.assertEqual(150, len(df)) 123 | 124 | # WARNING: Order is not guaranteed 125 | self.assertIn("OTC:LVM", df.loc[0, "Symbol"]) 126 | self.assertIn("LVM", df.loc[0, "Name"]) 127 | 128 | def test_country(self): 129 | ss = StockScreener() 130 | ss.add_filter(StockField.COUNTRY, FilterOperator.EQUAL, Country.ARGENTINA) 131 | df = ss.get() 132 | self.assertEqual(17, len(df)) 133 | 134 | self.assertEqual("NYSE:YPF", df.loc[0, "Symbol"]) 135 | self.assertEqual("YPF", df.loc[0, "Name"]) 136 | 137 | def test_countries(self): 138 | ss = StockScreener() 139 | ss.add_filter(StockField.COUNTRY, FilterOperator.EQUAL, Country.ARGENTINA) 140 | ss.add_filter(StockField.COUNTRY, FilterOperator.EQUAL, Country.BERMUDA) 141 | df = ss.get() 142 | self.assertEqual(106, len(df)) 143 | 144 | # WARNING: Order is not guaranteed 145 | # self.assertEqual("NASDAQ:ACGL", df.loc[0, "Symbol"]) 146 | # self.assertEqual("ACGL", df.loc[0, "Name"]) 147 | 148 | def test_exchange(self): 149 | ss = StockScreener() 150 | ss.add_filter(StockField.EXCHANGE, FilterOperator.EQUAL, Exchange.NYSE_ARCA) 151 | df = ss.get() 152 | self.assertEqual(150, len(df)) 153 | 154 | self.assertEqual("AMEX:LNG", df.loc[0, "Symbol"]) 155 | self.assertEqual("LNG", df.loc[0, "Name"]) 156 | 157 | def test_current_trading_day(self): 158 | ss = StockScreener() 159 | ss.add_filter(ExtraFilter.CURRENT_TRADING_DAY, FilterOperator.EQUAL, True) 160 | df = ss.get() 161 | self.assertEqual(150, len(df)) 162 | 163 | self.assertEqual("NASDAQ:AAPL", df.loc[0, "Symbol"]) 164 | self.assertEqual("AAPL", df.loc[0, "Name"]) 165 | -------------------------------------------------------------------------------- /docs/api/fields.md: -------------------------------------------------------------------------------- 1 | # Fields API Reference 2 | 3 | Field enums provide type-safe access to screener data. 4 | 5 | ## Field Classes 6 | 7 | | Class | Screener | Field Count | 8 | |-------|----------|-------------| 9 | | `StockField` | `StockScreener` | ~3,526 | 10 | | `CryptoField` | `CryptoScreener` | ~3,108 | 11 | | `ForexField` | `ForexScreener` | ~2,965 | 12 | | `BondField` | `BondScreener` | ~201 | 13 | | `FuturesField` | `FuturesScreener` | ~393 | 14 | | `CoinField` | `CoinScreener` | ~3,026 | 15 | 16 | ## Import 17 | 18 | ```python 19 | from tvscreener import StockField 20 | from tvscreener.field import StockField, CryptoField, ForexField 21 | ``` 22 | 23 | ## Comparison Operators 24 | 25 | Fields support Python comparison operators for filtering: 26 | 27 | ### Numeric Comparisons 28 | 29 | ```python 30 | StockField.PRICE > 100 # Greater than 31 | StockField.PRICE >= 100 # Greater than or equal 32 | StockField.PRICE < 100 # Less than 33 | StockField.PRICE <= 100 # Less than or equal 34 | StockField.PRICE == 100 # Equal to 35 | StockField.PRICE != 100 # Not equal to 36 | ``` 37 | 38 | ### Range Methods 39 | 40 | ```python 41 | StockField.PRICE.between(50, 200) # Value in range [50, 200] 42 | StockField.PRICE.not_between(50, 200) # Value outside range 43 | ``` 44 | 45 | ### List Methods 46 | 47 | ```python 48 | StockField.SECTOR.isin(['Technology', 'Healthcare']) 49 | StockField.SECTOR.not_in(['Finance', 'Utilities']) 50 | ``` 51 | 52 | ## Field Properties 53 | 54 | Each field has accessible properties: 55 | 56 | ```python 57 | field = StockField.PRICE 58 | 59 | field.name # Enum name: 'PRICE' 60 | field.value # Field definition tuple 61 | field.label # Human-readable label 62 | field.field_name # API field name 63 | field.format # Data format type 64 | ``` 65 | 66 | ### Format Types 67 | 68 | | Format | Description | Example Fields | 69 | |--------|-------------|----------------| 70 | | `currency` | Monetary values | PRICE, MARKET_CAP | 71 | | `percent` | Percentage | CHANGE_PERCENT, ROE | 72 | | `float` | Decimal number | PE_RATIO, RSI | 73 | | `number_group` | Large numbers | VOLUME, SHARES | 74 | | `text` | Text string | NAME, SECTOR | 75 | | `bool` | True/False | IS_PRIMARY | 76 | | `date` | Date | EX_DIVIDEND_DATE | 77 | 78 | ## Time Intervals 79 | 80 | Technical fields support multiple timeframes: 81 | 82 | ```python 83 | # Default (daily) 84 | StockField.RELATIVE_STRENGTH_INDEX_14 85 | 86 | # With specific interval 87 | StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') # 1 hour 88 | StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') # 4 hours 89 | StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('1W') # Weekly 90 | ``` 91 | 92 | ### Available Intervals 93 | 94 | | Code | Timeframe | 95 | |------|-----------| 96 | | `'1'` | 1 minute | 97 | | `'5'` | 5 minutes | 98 | | `'15'` | 15 minutes | 99 | | `'30'` | 30 minutes | 100 | | `'60'` | 1 hour | 101 | | `'120'` | 2 hours | 102 | | `'240'` | 4 hours | 103 | | `'1D'` | Daily | 104 | | `'1W'` | Weekly | 105 | | `'1M'` | Monthly | 106 | 107 | ## Historical Data 108 | 109 | Some fields support historical lookback: 110 | 111 | ```python 112 | # Previous day's RSI 113 | StockField.RELATIVE_STRENGTH_INDEX_14.with_history(1) 114 | 115 | # RSI from 5 days ago 116 | StockField.RELATIVE_STRENGTH_INDEX_14.with_history(5) 117 | ``` 118 | 119 | ## Field Discovery 120 | 121 | ### Search 122 | 123 | Find fields by name: 124 | 125 | ```python 126 | matches = StockField.search("dividend") 127 | for field in matches[:10]: 128 | print(f"{field.name}: {field.label}") 129 | ``` 130 | 131 | ### Presets 132 | 133 | Get grouped fields: 134 | 135 | ```python 136 | # Technical indicators 137 | technicals = StockField.technicals() 138 | 139 | # Oscillators 140 | oscillators = StockField.oscillators() 141 | 142 | # Moving averages 143 | mas = StockField.moving_averages() 144 | 145 | # Valuations 146 | valuations = StockField.valuations() 147 | 148 | # Dividends 149 | dividends = StockField.dividends() 150 | 151 | # Performance 152 | performance = StockField.performance() 153 | 154 | # Recommendations 155 | recommendations = StockField.recommendations() 156 | ``` 157 | 158 | ## Common Fields Reference 159 | 160 | ### Price & Volume 161 | 162 | | Field | Description | 163 | |-------|-------------| 164 | | `PRICE` | Current price | 165 | | `OPEN` | Day's open | 166 | | `HIGH` | Day's high | 167 | | `LOW` | Day's low | 168 | | `CLOSE` | Previous close | 169 | | `VOLUME` | Trading volume | 170 | | `RELATIVE_VOLUME` | Volume vs average | 171 | 172 | ### Valuation 173 | 174 | | Field | Description | 175 | |-------|-------------| 176 | | `PE_RATIO_TTM` | Price/Earnings (TTM) | 177 | | `PRICE_TO_BOOK_FY` | Price/Book | 178 | | `PRICE_TO_SALES_FY` | Price/Sales | 179 | | `EV_TO_EBITDA_TTM` | EV/EBITDA | 180 | | `PRICE_EARNINGS_TO_GROWTH_TTM` | PEG Ratio | 181 | | `MARKET_CAPITALIZATION` | Market Cap | 182 | 183 | ### Dividends 184 | 185 | | Field | Description | 186 | |-------|-------------| 187 | | `DIVIDEND_YIELD_FY` | Dividend Yield % | 188 | | `DIVIDENDS_PER_SHARE_FY` | Dividend Per Share | 189 | | `PAYOUT_RATIO_TTM` | Payout Ratio % | 190 | | `EX_DIVIDEND_DATE` | Ex-Dividend Date | 191 | 192 | ### Technical 193 | 194 | | Field | Description | 195 | |-------|-------------| 196 | | `RELATIVE_STRENGTH_INDEX_14` | RSI (14) | 197 | | `MACD_LEVEL_12_26` | MACD Line | 198 | | `MACD_SIGNAL_12_26_9` | MACD Signal | 199 | | `SIMPLE_MOVING_AVERAGE_50` | SMA 50 | 200 | | `SIMPLE_MOVING_AVERAGE_200` | SMA 200 | 201 | | `EXPONENTIAL_MOVING_AVERAGE_20` | EMA 20 | 202 | | `AVERAGE_TRUE_RANGE_14` | ATR (14) | 203 | | `AVERAGE_DIRECTIONAL_INDEX_14` | ADX (14) | 204 | | `STOCHASTIC_K_14_3_3` | Stochastic %K | 205 | | `STOCHASTIC_D_14_3_3` | Stochastic %D | 206 | | `BOLLINGER_UPPER_BAND_20` | BB Upper | 207 | | `BOLLINGER_LOWER_BAND_20` | BB Lower | 208 | 209 | ### Performance 210 | 211 | | Field | Description | 212 | |-------|-------------| 213 | | `CHANGE_PERCENT` | Today's Change % | 214 | | `PERFORMANCE_1_WEEK` | 1 Week Change % | 215 | | `PERFORMANCE_1_MONTH` | 1 Month Change % | 216 | | `PERFORMANCE_3_MONTH` | 3 Month Change % | 217 | | `PERFORMANCE_6_MONTH` | 6 Month Change % | 218 | | `PERFORMANCE_YTD` | Year-to-Date % | 219 | | `PERFORMANCE_1_YEAR` | 1 Year Change % | 220 | 221 | ### Profitability 222 | 223 | | Field | Description | 224 | |-------|-------------| 225 | | `RETURN_ON_EQUITY_TTM` | ROE % | 226 | | `RETURN_ON_ASSETS_TTM` | ROA % | 227 | | `GROSS_MARGIN_TTM` | Gross Margin % | 228 | | `NET_MARGIN_TTM` | Net Margin % | 229 | | `OPERATING_MARGIN_TTM` | Operating Margin % | 230 | -------------------------------------------------------------------------------- /docs/guide/filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering Guide 2 | 3 | Complete reference for filtering screener results. 4 | 5 | ## Comparison Operators 6 | 7 | Use Python operators directly on fields: 8 | 9 | | Operator | Example | Description | 10 | |----------|---------|-------------| 11 | | `>` | `StockField.PRICE > 100` | Greater than | 12 | | `>=` | `StockField.VOLUME >= 1e6` | Greater than or equal | 13 | | `<` | `StockField.RSI < 30` | Less than | 14 | | `<=` | `StockField.PE_RATIO_TTM <= 15` | Less than or equal | 15 | | `==` | `StockField.COUNTRY == 'United States'` | Equal to | 16 | | `!=` | `StockField.SECTOR != 'Finance'` | Not equal to | 17 | 18 | ```python 19 | from tvscreener import StockScreener, StockField 20 | 21 | ss = StockScreener() 22 | ss.where(StockField.PRICE > 50) 23 | ss.where(StockField.PRICE < 200) 24 | ss.where(StockField.VOLUME >= 500_000) 25 | ``` 26 | 27 | ## Range Filters 28 | 29 | ### between(min, max) 30 | Values within a range (inclusive): 31 | 32 | ```python 33 | ss.where(StockField.PRICE.between(50, 200)) 34 | ss.where(StockField.PE_RATIO_TTM.between(10, 25)) 35 | ss.where(StockField.MARKET_CAPITALIZATION.between(1e9, 10e9)) # $1B - $10B 36 | ``` 37 | 38 | ### not_between(min, max) 39 | Values outside a range: 40 | 41 | ```python 42 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14.not_between(30, 70)) # Overbought or oversold 43 | ``` 44 | 45 | ## List Filters 46 | 47 | ### isin(values) 48 | Match any value in a list: 49 | 50 | ```python 51 | from tvscreener import Exchange, Sector 52 | 53 | ss.where(StockField.EXCHANGE.isin([Exchange.NASDAQ, Exchange.NYSE])) 54 | ss.where(StockField.SECTOR.isin(['Electronic Technology', 'Health Technology'])) 55 | ``` 56 | 57 | ### not_in(values) 58 | Exclude values in a list: 59 | 60 | ```python 61 | ss.where(StockField.SECTOR.not_in(['Finance', 'Utilities'])) 62 | ``` 63 | 64 | ## Chaining Filters 65 | 66 | All filters are combined with AND logic: 67 | 68 | ```python 69 | ss = StockScreener() 70 | ss.where(StockField.PRICE > 10) # AND 71 | ss.where(StockField.PRICE < 100) # AND 72 | ss.where(StockField.VOLUME >= 1_000_000) # AND 73 | ss.where(StockField.MARKET_CAPITALIZATION.between(1e9, 50e9)) # AND 74 | ss.where(StockField.PE_RATIO_TTM.between(5, 25)) 75 | 76 | df = ss.get() 77 | ``` 78 | 79 | ## Market Filters 80 | 81 | ### By Exchange 82 | 83 | ```python 84 | from tvscreener import Exchange 85 | 86 | ss.where(StockField.EXCHANGE == Exchange.NASDAQ) 87 | # Or multiple: 88 | ss.where(StockField.EXCHANGE.isin([Exchange.NASDAQ, Exchange.NYSE])) 89 | ``` 90 | 91 | ### By Country 92 | 93 | ```python 94 | ss.where(StockField.COUNTRY == 'United States') 95 | ``` 96 | 97 | ### By Market Region 98 | 99 | ```python 100 | from tvscreener import Market 101 | 102 | ss = StockScreener() 103 | ss.set_markets(Market.AMERICA) # US stocks 104 | # Or: 105 | ss.set_markets(Market.JAPAN) 106 | ss.set_markets(Market.GERMANY) 107 | ss.set_markets(Market.ALL) # Global 108 | ``` 109 | 110 | ## Index Filters 111 | 112 | Filter to index constituents: 113 | 114 | ```python 115 | from tvscreener import IndexSymbol 116 | 117 | ss = StockScreener() 118 | ss.set_index(IndexSymbol.SP500) 119 | ss.set_range(0, 500) 120 | ``` 121 | 122 | Available indices: 123 | 124 | | Index | Symbol | 125 | |-------|--------| 126 | | S&P 500 | `IndexSymbol.SP500` | 127 | | NASDAQ 100 | `IndexSymbol.NASDAQ_100` | 128 | | Dow Jones | `IndexSymbol.DOW_JONES` | 129 | | Russell 2000 | `IndexSymbol.RUSSELL_2000` | 130 | | Russell 1000 | `IndexSymbol.RUSSELL_1000` | 131 | 132 | Sector indices: 133 | 134 | ```python 135 | ss.set_index(IndexSymbol.SP500_INFORMATION_TECHNOLOGY) 136 | ss.set_index(IndexSymbol.SP500_HEALTH_CARE) 137 | ss.set_index(IndexSymbol.PHLX_SEMICONDUCTOR) 138 | ``` 139 | 140 | Multiple indices: 141 | 142 | ```python 143 | ss.set_index(IndexSymbol.SP500, IndexSymbol.NASDAQ_100) 144 | ``` 145 | 146 | ## Symbol Type Filters 147 | 148 | Filter by security type: 149 | 150 | ```python 151 | from tvscreener import SymbolType 152 | 153 | ss = StockScreener() 154 | ss.set_symbol_types(SymbolType.COMMON_STOCK) 155 | # Or multiple: 156 | ss.set_symbol_types(SymbolType.COMMON_STOCK, SymbolType.ETF) 157 | ``` 158 | 159 | Available types: 160 | - `SymbolType.COMMON_STOCK` 161 | - `SymbolType.ETF` 162 | - `SymbolType.PREFERRED_STOCK` 163 | - `SymbolType.REIT` 164 | - `SymbolType.CLOSED_END_FUND` 165 | - `SymbolType.MUTUAL_FUND` 166 | 167 | ## Extra Filters 168 | 169 | ### Primary Listing Only 170 | 171 | ```python 172 | from tvscreener import ExtraFilter, FilterOperator 173 | 174 | ss.add_filter(ExtraFilter.PRIMARY, FilterOperator.EQUAL, True) 175 | ``` 176 | 177 | ### Currently Trading 178 | 179 | ```python 180 | ss.add_filter(ExtraFilter.CURRENT_TRADING_DAY, FilterOperator.EQUAL, True) 181 | ``` 182 | 183 | ## Search 184 | 185 | Text search across name and description: 186 | 187 | ```python 188 | ss = StockScreener() 189 | ss.search('semiconductor') 190 | df = ss.get() 191 | ``` 192 | 193 | ## Sorting 194 | 195 | ```python 196 | ss = StockScreener() 197 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) # Largest first 198 | ss.sort_by(StockField.CHANGE_PERCENT, ascending=False) # Top gainers 199 | ss.sort_by(StockField.VOLUME, ascending=False) # Most active 200 | ``` 201 | 202 | ## Pagination 203 | 204 | ```python 205 | ss = StockScreener() 206 | ss.set_range(0, 100) # First 100 results 207 | ss.set_range(100, 200) # Next 100 results 208 | ss.set_range(0, 1000) # First 1000 results 209 | ``` 210 | 211 | ## Complete Example 212 | 213 | ```python 214 | from tvscreener import StockScreener, StockField, IndexSymbol, Exchange 215 | 216 | ss = StockScreener() 217 | 218 | # S&P 500 stocks only 219 | ss.set_index(IndexSymbol.SP500) 220 | 221 | # Price and volume filters 222 | ss.where(StockField.PRICE.between(20, 500)) 223 | ss.where(StockField.VOLUME >= 500_000) 224 | 225 | # Valuation filters 226 | ss.where(StockField.PE_RATIO_TTM.between(5, 30)) 227 | ss.where(StockField.PRICE_TO_BOOK_FY < 5) 228 | 229 | # Performance filter 230 | ss.where(StockField.CHANGE_PERCENT > 0) # Up today 231 | 232 | # Technical filter 233 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14.between(40, 60)) 234 | 235 | # Select fields 236 | ss.select( 237 | StockField.NAME, 238 | StockField.PRICE, 239 | StockField.CHANGE_PERCENT, 240 | StockField.VOLUME, 241 | StockField.PE_RATIO_TTM, 242 | StockField.RELATIVE_STRENGTH_INDEX_14 243 | ) 244 | 245 | # Sort by market cap 246 | ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False) 247 | 248 | # Get up to 500 results 249 | ss.set_range(0, 500) 250 | 251 | df = ss.get() 252 | ``` 253 | -------------------------------------------------------------------------------- /docs/screeners/stock.md: -------------------------------------------------------------------------------- 1 | # Stock Screener 2 | 3 | Screen stocks from global exchanges including NYSE, NASDAQ, and international markets. 4 | 5 | ## Quick Start 6 | 7 | ```python 8 | from tvscreener import StockScreener, StockField 9 | 10 | ss = StockScreener() 11 | df = ss.get() 12 | ``` 13 | 14 | ## Field Count 15 | 16 | The Stock Screener has access to **~3,526 fields** covering: 17 | 18 | - Price & Volume data 19 | - Fundamental metrics (valuation, profitability, dividends) 20 | - Technical indicators (oscillators, moving averages, patterns) 21 | - Analyst ratings and recommendations 22 | 23 | ## Unique Features 24 | 25 | ### Index Filtering 26 | 27 | Filter to index constituents: 28 | 29 | ```python 30 | from tvscreener import IndexSymbol 31 | 32 | ss = StockScreener() 33 | ss.set_index(IndexSymbol.SP500) 34 | ss.set_range(0, 500) 35 | df = ss.get() 36 | ``` 37 | 38 | Available indices: 39 | 40 | | Index | Symbol | 41 | |-------|--------| 42 | | S&P 500 | `IndexSymbol.SP500` | 43 | | NASDAQ 100 | `IndexSymbol.NASDAQ_100` | 44 | | Dow Jones | `IndexSymbol.DOW_JONES` | 45 | | Russell 2000 | `IndexSymbol.RUSSELL_2000` | 46 | | Russell 1000 | `IndexSymbol.RUSSELL_1000` | 47 | 48 | Sector indices: 49 | 50 | ```python 51 | ss.set_index(IndexSymbol.SP500_INFORMATION_TECHNOLOGY) 52 | ss.set_index(IndexSymbol.SP500_HEALTH_CARE) 53 | ss.set_index(IndexSymbol.PHLX_SEMICONDUCTOR) 54 | ``` 55 | 56 | Multiple indices: 57 | 58 | ```python 59 | ss.set_index(IndexSymbol.SP500, IndexSymbol.NASDAQ_100) 60 | ``` 61 | 62 | Search for indices: 63 | 64 | ```python 65 | results = IndexSymbol.search("technology") 66 | for idx in results: 67 | print(idx.name) 68 | ``` 69 | 70 | ### Market Filtering 71 | 72 | ```python 73 | from tvscreener import Market 74 | 75 | ss = StockScreener() 76 | ss.set_markets(Market.AMERICA) # US stocks 77 | ss.set_markets(Market.JAPAN) # Japanese stocks 78 | ss.set_markets(Market.GERMANY) # German stocks 79 | ss.set_markets(Market.ALL) # Global 80 | ``` 81 | 82 | ### Symbol Type Filtering 83 | 84 | ```python 85 | from tvscreener import SymbolType 86 | 87 | ss = StockScreener() 88 | ss.set_symbol_types(SymbolType.COMMON_STOCK) 89 | ss.set_symbol_types(SymbolType.ETF) 90 | ss.set_symbol_types(SymbolType.PREFERRED_STOCK) 91 | ss.set_symbol_types(SymbolType.REIT) 92 | ``` 93 | 94 | ## Common Fields 95 | 96 | ### Price & Volume 97 | 98 | ```python 99 | StockField.PRICE # Current price 100 | StockField.OPEN # Day open 101 | StockField.HIGH # Day high 102 | StockField.LOW # Day low 103 | StockField.CLOSE # Previous close 104 | StockField.VOLUME # Trading volume 105 | StockField.RELATIVE_VOLUME # Volume vs average 106 | ``` 107 | 108 | ### Valuation 109 | 110 | ```python 111 | StockField.PE_RATIO_TTM # Price/Earnings 112 | StockField.PRICE_TO_BOOK_FY # Price/Book 113 | StockField.PRICE_TO_SALES_FY # Price/Sales 114 | StockField.EV_TO_EBITDA_TTM # EV/EBITDA 115 | StockField.PRICE_EARNINGS_TO_GROWTH_TTM # PEG Ratio 116 | StockField.MARKET_CAPITALIZATION # Market Cap 117 | ``` 118 | 119 | ### Dividends 120 | 121 | ```python 122 | StockField.DIVIDEND_YIELD_FY # Yield % 123 | StockField.DIVIDENDS_PER_SHARE_FY # DPS 124 | StockField.PAYOUT_RATIO_TTM # Payout Ratio 125 | StockField.EX_DIVIDEND_DATE # Ex-Date 126 | ``` 127 | 128 | ### Profitability 129 | 130 | ```python 131 | StockField.RETURN_ON_EQUITY_TTM # ROE 132 | StockField.RETURN_ON_ASSETS_TTM # ROA 133 | StockField.GROSS_MARGIN_TTM # Gross Margin 134 | StockField.NET_MARGIN_TTM # Net Margin 135 | StockField.OPERATING_MARGIN_TTM # Operating Margin 136 | ``` 137 | 138 | ### Technical 139 | 140 | ```python 141 | StockField.RELATIVE_STRENGTH_INDEX_14 # RSI(14) 142 | StockField.MACD_LEVEL_12_26 # MACD 143 | StockField.MACD_SIGNAL_12_26_9 # MACD Signal 144 | StockField.SIMPLE_MOVING_AVERAGE_50 # SMA 50 145 | StockField.SIMPLE_MOVING_AVERAGE_200 # SMA 200 146 | StockField.EXPONENTIAL_MOVING_AVERAGE_20 # EMA 20 147 | StockField.AVERAGE_TRUE_RANGE_14 # ATR 148 | StockField.AVERAGE_DIRECTIONAL_INDEX_14 # ADX 149 | ``` 150 | 151 | ### Performance 152 | 153 | ```python 154 | StockField.CHANGE_PERCENT # Today's change 155 | StockField.PERFORMANCE_1_WEEK # 1 week 156 | StockField.PERFORMANCE_1_MONTH # 1 month 157 | StockField.PERFORMANCE_3_MONTH # 3 months 158 | StockField.PERFORMANCE_6_MONTH # 6 months 159 | StockField.PERFORMANCE_YTD # Year to date 160 | StockField.PERFORMANCE_1_YEAR # 1 year 161 | ``` 162 | 163 | ## Example Screens 164 | 165 | ### Value Stocks 166 | 167 | ```python 168 | ss = StockScreener() 169 | ss.set_index(IndexSymbol.SP500) 170 | ss.where(StockField.PE_RATIO_TTM.between(5, 15)) 171 | ss.where(StockField.PRICE_TO_BOOK_FY < 3) 172 | ss.where(StockField.MARKET_CAPITALIZATION > 10e9) 173 | ss.sort_by(StockField.PE_RATIO_TTM, ascending=True) 174 | 175 | df = ss.get() 176 | ``` 177 | 178 | ### High Dividend Yield 179 | 180 | ```python 181 | ss = StockScreener() 182 | ss.where(StockField.DIVIDEND_YIELD_FY > 4) 183 | ss.where(StockField.PAYOUT_RATIO_TTM.between(20, 80)) 184 | ss.where(StockField.MARKET_CAPITALIZATION > 5e9) 185 | ss.sort_by(StockField.DIVIDEND_YIELD_FY, ascending=False) 186 | 187 | df = ss.get() 188 | ``` 189 | 190 | ### Momentum 191 | 192 | ```python 193 | ss = StockScreener() 194 | ss.set_index(IndexSymbol.SP500) 195 | ss.where(StockField.PERFORMANCE_3_MONTH > 20) 196 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14.between(50, 70)) 197 | ss.sort_by(StockField.PERFORMANCE_3_MONTH, ascending=False) 198 | 199 | df = ss.get() 200 | ``` 201 | 202 | ### Oversold RSI 203 | 204 | ```python 205 | ss = StockScreener() 206 | ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 30) 207 | ss.where(StockField.VOLUME >= 500_000) 208 | ss.where(StockField.PRICE > 5) 209 | ss.sort_by(StockField.RELATIVE_STRENGTH_INDEX_14, ascending=True) 210 | 211 | df = ss.get() 212 | ``` 213 | 214 | ## Field Discovery 215 | 216 | ```python 217 | # Search for specific fields 218 | matches = StockField.search("dividend") 219 | for field in matches[:10]: 220 | print(field.name) 221 | 222 | # Get all technical fields 223 | technicals = StockField.technicals() 224 | 225 | # Get all recommendations 226 | recommendations = StockField.recommendations() 227 | 228 | # Get all valuations 229 | valuations = StockField.valuations() 230 | ``` 231 | 232 | ## All Fields 233 | 234 | Use `select_all()` to get all ~3,526 fields: 235 | 236 | ```python 237 | ss = StockScreener() 238 | ss.set_index(IndexSymbol.SP500) 239 | ss.select_all() 240 | ss.set_range(0, 500) 241 | 242 | df = ss.get() 243 | print(f"Columns: {len(df.columns)}") 244 | ``` 245 | -------------------------------------------------------------------------------- /docs/api/enums.md: -------------------------------------------------------------------------------- 1 | # Enums API Reference 2 | 3 | Enumeration types for type-safe configuration. 4 | 5 | ## IndexSymbol 6 | 7 | Major stock indices for filtering. 8 | 9 | ```python 10 | from tvscreener import IndexSymbol 11 | ``` 12 | 13 | ### Major US Indices 14 | 15 | | Index | Symbol | 16 | |-------|--------| 17 | | S&P 500 | `IndexSymbol.SP500` | 18 | | NASDAQ 100 | `IndexSymbol.NASDAQ_100` | 19 | | Dow Jones Industrial | `IndexSymbol.DOW_JONES` | 20 | | Russell 2000 | `IndexSymbol.RUSSELL_2000` | 21 | | Russell 1000 | `IndexSymbol.RUSSELL_1000` | 22 | 23 | ### Sector Indices 24 | 25 | | Index | Symbol | 26 | |-------|--------| 27 | | S&P 500 Technology | `IndexSymbol.SP500_INFORMATION_TECHNOLOGY` | 28 | | S&P 500 Healthcare | `IndexSymbol.SP500_HEALTH_CARE` | 29 | | S&P 500 Financials | `IndexSymbol.SP500_FINANCIALS` | 30 | | S&P 500 Energy | `IndexSymbol.SP500_ENERGY` | 31 | | PHLX Semiconductor | `IndexSymbol.PHLX_SEMICONDUCTOR` | 32 | 33 | ### Usage 34 | 35 | ```python 36 | from tvscreener import StockScreener, IndexSymbol 37 | 38 | ss = StockScreener() 39 | 40 | # Single index 41 | ss.set_index(IndexSymbol.SP500) 42 | 43 | # Multiple indices 44 | ss.set_index(IndexSymbol.NASDAQ_100, IndexSymbol.DOW_JONES) 45 | ``` 46 | 47 | ### Search 48 | 49 | ```python 50 | # Find indices by name 51 | results = IndexSymbol.search("technology") 52 | for idx in results: 53 | print(idx.name, idx.value) 54 | ``` 55 | 56 | --- 57 | 58 | ## Market 59 | 60 | Geographic market regions. 61 | 62 | ```python 63 | from tvscreener import Market 64 | ``` 65 | 66 | ### Available Markets 67 | 68 | | Market | Description | 69 | |--------|-------------| 70 | | `Market.ALL` | All markets | 71 | | `Market.AMERICA` | United States | 72 | | `Market.UK` | United Kingdom | 73 | | `Market.GERMANY` | Germany | 74 | | `Market.FRANCE` | France | 75 | | `Market.JAPAN` | Japan | 76 | | `Market.CHINA` | China | 77 | | `Market.HONG_KONG` | Hong Kong | 78 | | `Market.INDIA` | India | 79 | | `Market.BRAZIL` | Brazil | 80 | | `Market.CANADA` | Canada | 81 | | `Market.AUSTRALIA` | Australia | 82 | 83 | ### Usage 84 | 85 | ```python 86 | ss = StockScreener() 87 | 88 | # Single market 89 | ss.set_markets(Market.AMERICA) 90 | 91 | # Multiple markets 92 | ss.set_markets(Market.AMERICA, Market.UK, Market.GERMANY) 93 | ``` 94 | 95 | --- 96 | 97 | ## SymbolType 98 | 99 | Security types for filtering. 100 | 101 | ```python 102 | from tvscreener import SymbolType 103 | ``` 104 | 105 | ### Available Types 106 | 107 | | Type | Description | 108 | |------|-------------| 109 | | `SymbolType.COMMON_STOCK` | Common shares | 110 | | `SymbolType.ETF` | Exchange-Traded Funds | 111 | | `SymbolType.PREFERRED_STOCK` | Preferred shares | 112 | | `SymbolType.REIT` | Real Estate Investment Trusts | 113 | | `SymbolType.CLOSED_END_FUND` | Closed-end funds | 114 | | `SymbolType.MUTUAL_FUND` | Mutual funds | 115 | | `SymbolType.ADR` | American Depositary Receipts | 116 | 117 | ### Usage 118 | 119 | ```python 120 | ss = StockScreener() 121 | 122 | # Single type 123 | ss.set_symbol_types(SymbolType.COMMON_STOCK) 124 | 125 | # Multiple types 126 | ss.set_symbol_types(SymbolType.COMMON_STOCK, SymbolType.ETF) 127 | ``` 128 | 129 | ### ETF Screening Example 130 | 131 | ```python 132 | ss = StockScreener() 133 | ss.set_symbol_types(SymbolType.ETF) 134 | ss.where(StockField.VOLUME > 1_000_000) 135 | ss.sort_by(StockField.VOLUME, ascending=False) 136 | 137 | df = ss.get() 138 | ``` 139 | 140 | --- 141 | 142 | ## Exchange 143 | 144 | Stock exchanges. 145 | 146 | ```python 147 | from tvscreener import Exchange 148 | ``` 149 | 150 | ### Available Exchanges 151 | 152 | | Exchange | Symbol | 153 | |----------|--------| 154 | | NASDAQ | `Exchange.NASDAQ` | 155 | | NYSE | `Exchange.NYSE` | 156 | | NYSE American | `Exchange.NYSE_AMERICAN` | 157 | | NYSE Arca | `Exchange.NYSE_ARCA` | 158 | 159 | ### Usage with Filters 160 | 161 | ```python 162 | ss = StockScreener() 163 | ss.where(StockField.EXCHANGE == Exchange.NASDAQ) 164 | 165 | # Or multiple 166 | ss.where(StockField.EXCHANGE.isin([Exchange.NASDAQ, Exchange.NYSE])) 167 | ``` 168 | 169 | --- 170 | 171 | ## FilterOperator 172 | 173 | Filter operations for comparisons. 174 | 175 | ```python 176 | from tvscreener import FilterOperator 177 | ``` 178 | 179 | ### Available Operators 180 | 181 | | Operator | Value | Python Equivalent | 182 | |----------|-------|-------------------| 183 | | `FilterOperator.ABOVE` | `'greater'` | `>` | 184 | | `FilterOperator.ABOVE_OR_EQUAL` | `'egreater'` | `>=` | 185 | | `FilterOperator.BELOW` | `'less'` | `<` | 186 | | `FilterOperator.BELOW_OR_EQUAL` | `'eless'` | `<=` | 187 | | `FilterOperator.EQUAL` | `'equal'` | `==` | 188 | | `FilterOperator.NOT_EQUAL` | `'nequal'` | `!=` | 189 | | `FilterOperator.IN_RANGE` | `'in_range'` | `.between()` | 190 | | `FilterOperator.NOT_IN_RANGE` | `'not_in_range'` | `.not_between()` | 191 | 192 | ### Legacy Usage 193 | 194 | ```python 195 | ss = StockScreener() 196 | ss.where(StockField.PRICE, FilterOperator.ABOVE, 100) 197 | ``` 198 | 199 | ### Modern Syntax (Preferred) 200 | 201 | ```python 202 | ss = StockScreener() 203 | ss.where(StockField.PRICE > 100) 204 | ``` 205 | 206 | --- 207 | 208 | ## TimeInterval 209 | 210 | Time intervals for technical indicators. 211 | 212 | ### Available Intervals 213 | 214 | | Interval | Code | 215 | |----------|------| 216 | | 1 minute | `'1'` | 217 | | 5 minutes | `'5'` | 218 | | 15 minutes | `'15'` | 219 | | 30 minutes | `'30'` | 220 | | 1 hour | `'60'` | 221 | | 2 hours | `'120'` | 222 | | 4 hours | `'240'` | 223 | | Daily | `'1D'` | 224 | | Weekly | `'1W'` | 225 | | Monthly | `'1M'` | 226 | 227 | ### Usage 228 | 229 | ```python 230 | # RSI on 1-hour timeframe 231 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 232 | 233 | # MACD on 4-hour timeframe 234 | macd_4h = StockField.MACD_LEVEL_12_26.with_interval('240') 235 | 236 | # SMA on weekly timeframe 237 | sma_weekly = StockField.SIMPLE_MOVING_AVERAGE_50.with_interval('1W') 238 | ``` 239 | 240 | --- 241 | 242 | ## Complete Example 243 | 244 | ```python 245 | from tvscreener import ( 246 | StockScreener, 247 | StockField, 248 | IndexSymbol, 249 | Market, 250 | SymbolType, 251 | Exchange 252 | ) 253 | 254 | ss = StockScreener() 255 | 256 | # Market and type filters 257 | ss.set_markets(Market.AMERICA) 258 | ss.set_symbol_types(SymbolType.COMMON_STOCK) 259 | ss.set_index(IndexSymbol.SP500) 260 | 261 | # Exchange filter 262 | ss.where(StockField.EXCHANGE.isin([Exchange.NASDAQ, Exchange.NYSE])) 263 | 264 | # Price and volume 265 | ss.where(StockField.PRICE.between(50, 500)) 266 | ss.where(StockField.VOLUME >= 1_000_000) 267 | 268 | # Technical with interval 269 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 270 | ss.where(rsi_1h < 40) 271 | 272 | df = ss.get() 273 | ``` 274 | -------------------------------------------------------------------------------- /.dev/codegen/code/industry.py.generated: -------------------------------------------------------------------------------- 1 | 2 | # ---------------------- 3 | # Generated file 4 | # 2023-08-11 07:05:13.155735 5 | # ---------------------- 6 | from enum import Enum 7 | 8 | 9 | class Industry(Enum): 10 | ADVERTISINGMARKETING_SERVICES = 'Advertising/Marketing Services' 11 | AEROSPACE_AND_DEFENSE = 'Aerospace & Defense' 12 | AGRICULTURAL_COMMODITIESMILLING = 'Agricultural Commodities/Milling' 13 | AIR_FREIGHTCOURIERS = 'Air Freight/Couriers' 14 | AIRLINES = 'Airlines' 15 | ALTERNATIVE_POWER_GENERATION = 'Alternative Power Generation' 16 | ALUMINUM = 'Aluminum' 17 | APPARELFOOTWEAR = 'Apparel/Footwear' 18 | APPARELFOOTWEAR_RETAIL = 'Apparel/Footwear Retail' 19 | AUTO_PARTS_OEM = 'Auto Parts: OEM' 20 | AUTOMOTIVE_AFTERMARKET = 'Automotive Aftermarket' 21 | BEVERAGES_ALCOHOLIC = 'Beverages: Alcoholic' 22 | BEVERAGES_NONALCOHOLIC = 'Beverages: Non-Alcoholic' 23 | BIOTECHNOLOGY = 'Biotechnology' 24 | BROADCASTING = 'Broadcasting' 25 | BUILDING_PRODUCTS = 'Building Products' 26 | CABLESATELLITE_TV = 'Cable/Satellite TV' 27 | CASINOSGAMING = 'Casinos/Gaming' 28 | CATALOGSPECIALTY_DISTRIBUTION = 'Catalog/Specialty Distribution' 29 | CHEMICALS_AGRICULTURAL = 'Chemicals: Agricultural' 30 | CHEMICALS_MAJOR_DIVERSIFIED = 'Chemicals: Major Diversified' 31 | CHEMICALS_SPECIALTY = 'Chemicals: Specialty' 32 | COAL = 'Coal' 33 | COMMERCIAL_PRINTINGFORMS = 'Commercial Printing/Forms' 34 | COMPUTER_COMMUNICATIONS = 'Computer Communications' 35 | COMPUTER_PERIPHERALS = 'Computer Peripherals' 36 | COMPUTER_PROCESSING_HARDWARE = 'Computer Processing Hardware' 37 | CONSTRUCTION_MATERIALS = 'Construction Materials' 38 | CONSUMER_SUNDRIES = 'Consumer Sundries' 39 | CONTAINERSPACKAGING = 'Containers/Packaging' 40 | CONTRACT_DRILLING = 'Contract Drilling' 41 | DATA_PROCESSING_SERVICES = 'Data Processing Services' 42 | DEPARTMENT_STORES = 'Department Stores' 43 | DISCOUNT_STORES = 'Discount Stores' 44 | DRUGSTORE_CHAINS = 'Drugstore Chains' 45 | ELECTRIC_UTILITIES = 'Electric Utilities' 46 | ELECTRICAL_PRODUCTS = 'Electrical Products' 47 | ELECTRONIC_COMPONENTS = 'Electronic Components' 48 | ELECTRONIC_EQUIPMENTINSTRUMENTS = 'Electronic Equipment/Instruments' 49 | ELECTRONIC_PRODUCTION_EQUIPMENT = 'Electronic Production Equipment' 50 | ELECTRONICS_DISTRIBUTORS = 'Electronics Distributors' 51 | ELECTRONICSAPPLIANCE_STORES = 'Electronics/Appliance Stores' 52 | ELECTRONICSAPPLIANCES = 'Electronics/Appliances' 53 | ENGINEERING_AND_CONSTRUCTION = 'Engineering & Construction' 54 | ENVIRONMENTAL_SERVICES = 'Environmental Services' 55 | FINANCERENTALLEASING = 'Finance/Rental/Leasing' 56 | FINANCIAL_CONGLOMERATES = 'Financial Conglomerates' 57 | FINANCIAL_PUBLISHINGSERVICES = 'Financial Publishing/Services' 58 | FOOD_DISTRIBUTORS = 'Food Distributors' 59 | FOOD_RETAIL = 'Food Retail' 60 | FOOD_MAJOR_DIVERSIFIED = 'Food: Major Diversified' 61 | FOOD_MEATFISHDAIRY = 'Food: Meat/Fish/Dairy' 62 | FOOD_SPECIALTYCANDY = 'Food: Specialty/Candy' 63 | FOREST_PRODUCTS = 'Forest Products' 64 | GAS_DISTRIBUTORS = 'Gas Distributors' 65 | GENERAL_GOVERNMENT = 'General Government' 66 | HOME_FURNISHINGS = 'Home Furnishings' 67 | HOME_IMPROVEMENT_CHAINS = 'Home Improvement Chains' 68 | HOMEBUILDING = 'Homebuilding' 69 | HOSPITALNURSING_MANAGEMENT = 'Hospital/Nursing Management' 70 | HOTELSRESORTSCRUISE_LINES = 'Hotels/Resorts/Cruise lines' 71 | HOUSEHOLDPERSONAL_CARE = 'Household/Personal Care' 72 | INDUSTRIAL_CONGLOMERATES = 'Industrial Conglomerates' 73 | INDUSTRIAL_MACHINERY = 'Industrial Machinery' 74 | INDUSTRIAL_SPECIALTIES = 'Industrial Specialties' 75 | INFORMATION_TECHNOLOGY_SERVICES = 'Information Technology Services' 76 | INSURANCE_BROKERSSERVICES = 'Insurance Brokers/Services' 77 | INTEGRATED_OIL = 'Integrated Oil' 78 | INTERNET_RETAIL = 'Internet Retail' 79 | INTERNET_SOFTWARESERVICES = 'Internet Software/Services' 80 | INVESTMENT_BANKSBROKERS = 'Investment Banks/Brokers' 81 | INVESTMENT_MANAGERS = 'Investment Managers' 82 | INVESTMENT_TRUSTSMUTUAL_FUNDS = 'Investment Trusts/Mutual Funds' 83 | LIFEHEALTH_INSURANCE = 'Life/Health Insurance' 84 | MAJOR_BANKS = 'Major Banks' 85 | MAJOR_TELECOMMUNICATIONS = 'Major Telecommunications' 86 | MANAGED_HEALTH_CARE = 'Managed Health Care' 87 | MARINE_SHIPPING = 'Marine Shipping' 88 | MEDIA_CONGLOMERATES = 'Media Conglomerates' 89 | MEDICAL_DISTRIBUTORS = 'Medical Distributors' 90 | MEDICAL_SPECIALTIES = 'Medical Specialties' 91 | MEDICALNURSING_SERVICES = 'Medical/Nursing Services' 92 | METAL_FABRICATION = 'Metal Fabrication' 93 | MISCELLANEOUS = 'Miscellaneous' 94 | MISCELLANEOUS_COMMERCIAL_SERVICES = 'Miscellaneous Commercial Services' 95 | MISCELLANEOUS_MANUFACTURING = 'Miscellaneous Manufacturing' 96 | MOTOR_VEHICLES = 'Motor Vehicles' 97 | MOVIESENTERTAINMENT = 'Movies/Entertainment' 98 | MULTILINE_INSURANCE = 'Multi-Line Insurance' 99 | OFFICE_EQUIPMENTSUPPLIES = 'Office Equipment/Supplies' 100 | OIL_AND_GAS_PIPELINES = 'Oil & Gas Pipelines' 101 | OIL_AND_GAS_PRODUCTION = 'Oil & Gas Production' 102 | OIL_REFININGMARKETING = 'Oil Refining/Marketing' 103 | OILFIELD_SERVICESEQUIPMENT = 'Oilfield Services/Equipment' 104 | OTHER_CONSUMER_SERVICES = 'Other Consumer Services' 105 | OTHER_CONSUMER_SPECIALTIES = 'Other Consumer Specialties' 106 | OTHER_METALSMINERALS = 'Other Metals/Minerals' 107 | OTHER_TRANSPORTATION = 'Other Transportation' 108 | PACKAGED_SOFTWARE = 'Packaged Software' 109 | PERSONNEL_SERVICES = 'Personnel Services' 110 | PHARMACEUTICALS_GENERIC = 'Pharmaceuticals: Generic' 111 | PHARMACEUTICALS_MAJOR = 'Pharmaceuticals: Major' 112 | PHARMACEUTICALS_OTHER = 'Pharmaceuticals: Other' 113 | PRECIOUS_METALS = 'Precious Metals' 114 | PROPERTYCASUALTY_INSURANCE = 'Property/Casualty Insurance' 115 | PUBLISHING_BOOKSMAGAZINES = 'Publishing: Books/Magazines' 116 | PUBLISHING_NEWSPAPERS = 'Publishing: Newspapers' 117 | PULP_AND_PAPER = 'Pulp & Paper' 118 | RAILROADS = 'Railroads' 119 | REAL_ESTATE_DEVELOPMENT = 'Real Estate Development' 120 | REAL_ESTATE_INVESTMENT_TRUSTS = 'Real Estate Investment Trusts' 121 | RECREATIONAL_PRODUCTS = 'Recreational Products' 122 | REGIONAL_BANKS = 'Regional Banks' 123 | RESTAURANTS = 'Restaurants' 124 | SAVINGS_BANKS = 'Savings Banks' 125 | SEMICONDUCTORS = 'Semiconductors' 126 | SERVICES_TO_THE_HEALTH_INDUSTRY = 'Services to the Health Industry' 127 | SPECIALTY_INSURANCE = 'Specialty Insurance' 128 | SPECIALTY_STORES = 'Specialty Stores' 129 | SPECIALTY_TELECOMMUNICATIONS = 'Specialty Telecommunications' 130 | STEEL = 'Steel' 131 | TELECOMMUNICATIONS_EQUIPMENT = 'Telecommunications Equipment' 132 | TEXTILES = 'Textiles' 133 | TOBACCO = 'Tobacco' 134 | TOOLS_AND_HARDWARE = 'Tools & Hardware' 135 | TRUCKING = 'Trucking' 136 | TRUCKSCONSTRUCTIONFARM_MACHINERY = 'Trucks/Construction/Farm Machinery' 137 | WATER_UTILITIES = 'Water Utilities' 138 | WHOLESALE_DISTRIBUTORS = 'Wholesale Distributors' 139 | WIRELESS_TELECOMMUNICATIONS = 'Wireless Telecommunications' 140 | -------------------------------------------------------------------------------- /.dev/codegen/Generate FilterFields.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "initial_id", 7 | "metadata": { 8 | "collapsed": true, 9 | "ExecuteTime": { 10 | "end_time": "2023-08-11T06:52:01.213849587Z", 11 | "start_time": "2023-08-11T06:52:01.172112997Z" 12 | } 13 | }, 14 | "outputs": [ 15 | { 16 | "name": "stdout", 17 | "output_type": "stream", 18 | "text": [ 19 | "The autoreload extension is already loaded. To reload it, use:\n", 20 | " %reload_ext autoreload\n" 21 | ] 22 | } 23 | ], 24 | "source": [ 25 | "from generate import scrap_field_values\n", 26 | "import json\n", 27 | "\n", 28 | "%load_ext autoreload\n", 29 | "%autoreload 2" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 3, 35 | "outputs": [], 36 | "source": [ 37 | "from generate import fill_filter_template, generate_filter_columns\n", 38 | "from generate import write\n", 39 | "\n", 40 | "\n", 41 | "def save_columns(url):\n", 42 | " fields_dict = scrap_field_values(url)\n", 43 | " for key, value in fields_dict.items():\n", 44 | " with open(f'data/{key.lower()}.json', 'w') as f:\n", 45 | " json.dump({key: value}, f)\n", 46 | " return fields_dict\n", 47 | " \n", 48 | "def generate_code_files(name):\n", 49 | " with open(f'data/{name.lower()}.json') as f:\n", 50 | " filters_ = json.load(f)\n", 51 | " formatted_columns = generate_filter_columns(filters_[name])\n", 52 | " template = fill_filter_template(name.replace(' ', ''), formatted_columns)\n", 53 | " write(name.replace(' ', '_').lower(), template)" 54 | ], 55 | "metadata": { 56 | "collapsed": false, 57 | "ExecuteTime": { 58 | "end_time": "2023-08-11T06:52:03.190052866Z", 59 | "start_time": "2023-08-11T06:52:03.186114291Z" 60 | } 61 | }, 62 | "id": "d4c4d0ddb0c0119b" 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 4, 67 | "outputs": [ 68 | { 69 | "name": "stdout", 70 | "output_type": "stream", 71 | "text": [ 72 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[1]/div[1] ...\n", 73 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[2]/div[1] ...\n", 74 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[3]/div[1] ...\n", 75 | "TimeoutException on field: Primary Listing\n", 76 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[4]/div[1] ...\n", 77 | "TimeoutException on field: Current trading day\n", 78 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[5]/div[1] ...\n", 79 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[6]/div[1] ...\n", 80 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[7]/div[1] ...\n", 81 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[8]/div[1] ...\n", 82 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[9]/div[1] ...\n", 83 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[10]/div[1] ...\n", 84 | "NoSuchElementException on field: Market Capitalization\n", 85 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[11]/div[1] ...\n", 86 | "NoSuchElementException on field: Volume\n", 87 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[12]/div[1] ...\n", 88 | "NoSuchElementException on field: Average Volume (10 day)\n", 89 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[13]/div[1] ...\n", 90 | "NoSuchElementException on field: Average Volume (30 day)\n", 91 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[14]/div[1] ...\n", 92 | "NoSuchElementException on field: Average Volume (60 day)\n", 93 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[15]/div[1] ...\n", 94 | "NoSuchElementException on field: Average Volume (90 day)\n", 95 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[16]/div[1] ...\n", 96 | "TimeoutException on field: Relative Volume\n", 97 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[17]/div[1] ...\n", 98 | "TimeoutException on field: Relative Volume at Time\n", 99 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[18]/div[1] ...\n", 100 | "TimeoutException on field: Change %\n", 101 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[19]/div[1] ...\n", 102 | "TimeoutException on field: Change 1m, %\n", 103 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[20]/div[1] ...\n", 104 | "TimeoutException on field: Change 5m, %\n", 105 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[21]/div[1] ...\n", 106 | "TimeoutException on field: Change 15m, %\n", 107 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[22]/div[1] ...\n", 108 | "TimeoutException on field: Change 1h, %\n", 109 | "Search field on xpath: /html/body/div[10]/div/div/div/div[3]/div[1]/div/div/div[23]/div[1] ...\n" 110 | ] 111 | } 112 | ], 113 | "source": [ 114 | "stock_url = \"https://www.tradingview.com/screener/\"\n", 115 | "filters = save_columns(stock_url)" 116 | ], 117 | "metadata": { 118 | "collapsed": false, 119 | "ExecuteTime": { 120 | "end_time": "2023-08-11T06:52:42.294915919Z", 121 | "start_time": "2023-08-11T06:52:04.108834356Z" 122 | } 123 | }, 124 | "id": "e4eabbd29fc14556" 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 7, 129 | "outputs": [], 130 | "source": [ 131 | "for key in filters.keys():\n", 132 | " generate_code_files(key)" 133 | ], 134 | "metadata": { 135 | "collapsed": false, 136 | "ExecuteTime": { 137 | "end_time": "2023-08-11T07:05:13.158804479Z", 138 | "start_time": "2023-08-11T07:05:13.148339900Z" 139 | } 140 | }, 141 | "id": "fce7ec0c7477cb3c" 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "outputs": [], 147 | "source": [], 148 | "metadata": { 149 | "collapsed": false 150 | }, 151 | "id": "c8b265fdafe0ea0" 152 | } 153 | ], 154 | "metadata": { 155 | "kernelspec": { 156 | "display_name": "Python 3", 157 | "language": "python", 158 | "name": "python3" 159 | }, 160 | "language_info": { 161 | "codemirror_mode": { 162 | "name": "ipython", 163 | "version": 2 164 | }, 165 | "file_extension": ".py", 166 | "mimetype": "text/x-python", 167 | "name": "python", 168 | "nbconvert_exporter": "python", 169 | "pygments_lexer": "ipython2", 170 | "version": "2.7.6" 171 | } 172 | }, 173 | "nbformat": 4, 174 | "nbformat_minor": 5 175 | } 176 | -------------------------------------------------------------------------------- /docs/examples/crypto-strategies.md: -------------------------------------------------------------------------------- 1 | # Crypto Trading Strategies 2 | 3 | Screen cryptocurrencies using various trading strategies. 4 | 5 | ## Market Cap Tiers 6 | 7 | ### Large Cap Only 8 | 9 | Focus on established cryptocurrencies: 10 | 11 | ```python 12 | from tvscreener import CryptoScreener, CryptoField 13 | 14 | cs = CryptoScreener() 15 | cs.where(CryptoField.MARKET_CAPITALIZATION > 10e9) # >$10B 16 | cs.sort_by(CryptoField.MARKET_CAPITALIZATION, ascending=False) 17 | cs.select( 18 | CryptoField.NAME, 19 | CryptoField.PRICE, 20 | CryptoField.MARKET_CAPITALIZATION, 21 | CryptoField.CHANGE_PERCENT 22 | ) 23 | 24 | df = cs.get() 25 | ``` 26 | 27 | ### Mid Cap Discovery 28 | 29 | Find emerging projects: 30 | 31 | ```python 32 | cs = CryptoScreener() 33 | cs.where(CryptoField.MARKET_CAPITALIZATION.between(100e6, 1e9)) 34 | cs.where(CryptoField.VOLUME > 5_000_000) 35 | cs.sort_by(CryptoField.CHANGE_PERCENT, ascending=False) 36 | 37 | df = cs.get() 38 | ``` 39 | 40 | ### Small Cap with Volume 41 | 42 | High risk, high potential: 43 | 44 | ```python 45 | cs = CryptoScreener() 46 | cs.where(CryptoField.MARKET_CAPITALIZATION.between(10e6, 100e6)) 47 | cs.where(CryptoField.VOLUME > 1_000_000) # Liquidity filter 48 | cs.sort_by(CryptoField.PERFORMANCE_1_WEEK, ascending=False) 49 | 50 | df = cs.get() 51 | ``` 52 | 53 | ## Momentum Strategies 54 | 55 | ### Daily Gainers 56 | 57 | ```python 58 | cs = CryptoScreener() 59 | cs.where(CryptoField.CHANGE_PERCENT > 10) 60 | cs.where(CryptoField.VOLUME > 10_000_000) 61 | cs.sort_by(CryptoField.CHANGE_PERCENT, ascending=False) 62 | cs.set_range(0, 50) 63 | 64 | df = cs.get() 65 | ``` 66 | 67 | ### Weekly Momentum 68 | 69 | ```python 70 | cs = CryptoScreener() 71 | cs.where(CryptoField.PERFORMANCE_1_WEEK > 30) 72 | cs.where(CryptoField.MARKET_CAPITALIZATION > 100e6) 73 | cs.sort_by(CryptoField.PERFORMANCE_1_WEEK, ascending=False) 74 | 75 | df = cs.get() 76 | ``` 77 | 78 | ### Monthly Breakouts 79 | 80 | ```python 81 | cs = CryptoScreener() 82 | cs.where(CryptoField.PERFORMANCE_1_MONTH > 50) 83 | cs.where(CryptoField.VOLUME > 5_000_000) 84 | cs.sort_by(CryptoField.PERFORMANCE_1_MONTH, ascending=False) 85 | 86 | df = cs.get() 87 | ``` 88 | 89 | ## Technical Analysis 90 | 91 | ### RSI Oversold 92 | 93 | ```python 94 | cs = CryptoScreener() 95 | cs.where(CryptoField.RELATIVE_STRENGTH_INDEX_14 < 30) 96 | cs.where(CryptoField.MARKET_CAPITALIZATION > 100e6) 97 | cs.select( 98 | CryptoField.NAME, 99 | CryptoField.PRICE, 100 | CryptoField.RELATIVE_STRENGTH_INDEX_14, 101 | CryptoField.CHANGE_PERCENT 102 | ) 103 | cs.sort_by(CryptoField.RELATIVE_STRENGTH_INDEX_14, ascending=True) 104 | 105 | df = cs.get() 106 | ``` 107 | 108 | ### RSI Overbought 109 | 110 | Potential reversal candidates: 111 | 112 | ```python 113 | cs = CryptoScreener() 114 | cs.where(CryptoField.RELATIVE_STRENGTH_INDEX_14 > 70) 115 | cs.where(CryptoField.VOLUME > 10_000_000) 116 | cs.sort_by(CryptoField.RELATIVE_STRENGTH_INDEX_14, ascending=False) 117 | 118 | df = cs.get() 119 | ``` 120 | 121 | ### MACD Bullish 122 | 123 | ```python 124 | cs = CryptoScreener() 125 | cs.where(CryptoField.MACD_LEVEL_12_26 > 0) 126 | cs.where(CryptoField.MACD_LEVEL_12_26 > CryptoField.MACD_SIGNAL_12_26_9) 127 | cs.where(CryptoField.VOLUME > 5_000_000) 128 | 129 | df = cs.get() 130 | ``` 131 | 132 | ### Above Moving Averages 133 | 134 | ```python 135 | cs = CryptoScreener() 136 | cs.where(CryptoField.PRICE > CryptoField.SIMPLE_MOVING_AVERAGE_50) 137 | cs.where(CryptoField.SIMPLE_MOVING_AVERAGE_50 > CryptoField.SIMPLE_MOVING_AVERAGE_200) 138 | cs.where(CryptoField.MARKET_CAPITALIZATION > 1e9) 139 | 140 | df = cs.get() 141 | ``` 142 | 143 | ## Volume Analysis 144 | 145 | ### Volume Spike 146 | 147 | Unusual volume activity: 148 | 149 | ```python 150 | cs = CryptoScreener() 151 | cs.where(CryptoField.RELATIVE_VOLUME > 3) # 3x normal volume 152 | cs.where(CryptoField.MARKET_CAPITALIZATION > 50e6) 153 | cs.sort_by(CryptoField.RELATIVE_VOLUME, ascending=False) 154 | 155 | df = cs.get() 156 | ``` 157 | 158 | ### High Volume Movers 159 | 160 | ```python 161 | cs = CryptoScreener() 162 | cs.where(CryptoField.VOLUME > 100_000_000) 163 | cs.where(CryptoField.CHANGE_PERCENT.not_between(-2, 2)) # Significant move 164 | cs.sort_by(CryptoField.VOLUME, ascending=False) 165 | 166 | df = cs.get() 167 | ``` 168 | 169 | ## Multi-Timeframe 170 | 171 | ### Daily + Hourly RSI 172 | 173 | ```python 174 | cs = CryptoScreener() 175 | 176 | # Daily RSI moderate 177 | cs.where(CryptoField.RELATIVE_STRENGTH_INDEX_14.between(35, 50)) 178 | 179 | # Hourly RSI oversold 180 | rsi_1h = CryptoField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 181 | cs.where(rsi_1h < 30) 182 | 183 | cs.where(CryptoField.MARKET_CAPITALIZATION > 500e6) 184 | cs.select( 185 | CryptoField.NAME, 186 | CryptoField.PRICE, 187 | CryptoField.RELATIVE_STRENGTH_INDEX_14, 188 | rsi_1h, 189 | CryptoField.CHANGE_PERCENT 190 | ) 191 | 192 | df = cs.get() 193 | ``` 194 | 195 | ### 4-Hour Trend Confirmation 196 | 197 | ```python 198 | cs = CryptoScreener() 199 | 200 | # Daily trend up 201 | cs.where(CryptoField.PRICE > CryptoField.SIMPLE_MOVING_AVERAGE_50) 202 | 203 | # 4-hour MACD bullish 204 | macd_4h = CryptoField.MACD_LEVEL_12_26.with_interval('240') 205 | cs.where(macd_4h > 0) 206 | 207 | # 4-hour RSI not overbought 208 | rsi_4h = CryptoField.RELATIVE_STRENGTH_INDEX_14.with_interval('240') 209 | cs.where(rsi_4h < 65) 210 | 211 | df = cs.get() 212 | ``` 213 | 214 | ## Volatility Screens 215 | 216 | ### High Volatility 217 | 218 | For active traders: 219 | 220 | ```python 221 | cs = CryptoScreener() 222 | cs.where(CryptoField.VOLATILITY_DAY > 5) 223 | cs.where(CryptoField.VOLUME > 10_000_000) 224 | cs.sort_by(CryptoField.VOLATILITY_DAY, ascending=False) 225 | 226 | df = cs.get() 227 | ``` 228 | 229 | ### Low Volatility Large Cap 230 | 231 | For conservative positions: 232 | 233 | ```python 234 | cs = CryptoScreener() 235 | cs.where(CryptoField.MARKET_CAPITALIZATION > 5e9) 236 | cs.where(CryptoField.VOLATILITY_DAY < 3) 237 | cs.sort_by(CryptoField.MARKET_CAPITALIZATION, ascending=False) 238 | 239 | df = cs.get() 240 | ``` 241 | 242 | ## Recovery Screens 243 | 244 | ### Bouncing from Lows 245 | 246 | ```python 247 | cs = CryptoScreener() 248 | cs.where(CryptoField.PERFORMANCE_1_MONTH < -30) # Down 30%+ monthly 249 | cs.where(CryptoField.CHANGE_PERCENT > 5) # Up today 250 | cs.where(CryptoField.VOLUME > 5_000_000) 251 | cs.sort_by(CryptoField.CHANGE_PERCENT, ascending=False) 252 | 253 | df = cs.get() 254 | ``` 255 | 256 | ### RSI Reversal Setup 257 | 258 | ```python 259 | cs = CryptoScreener() 260 | cs.where(CryptoField.RELATIVE_STRENGTH_INDEX_14.between(30, 40)) 261 | cs.where(CryptoField.CHANGE_PERCENT > 0) 262 | cs.where(CryptoField.MARKET_CAPITALIZATION > 100e6) 263 | 264 | df = cs.get() 265 | ``` 266 | 267 | ## Specific Cryptos 268 | 269 | Track specific assets: 270 | 271 | ```python 272 | cs = CryptoScreener() 273 | cs.symbols = { 274 | "query": {"types": []}, 275 | "tickers": [ 276 | "BINANCE:BTCUSDT", 277 | "BINANCE:ETHUSDT", 278 | "BINANCE:SOLUSDT", 279 | "BINANCE:ADAUSDT", 280 | "BINANCE:DOTUSDT" 281 | ] 282 | } 283 | cs.select_all() 284 | 285 | df = cs.get() 286 | ``` 287 | 288 | ## Streaming Monitor 289 | 290 | Real-time updates: 291 | 292 | ```python 293 | cs = CryptoScreener() 294 | cs.where(CryptoField.MARKET_CAPITALIZATION > 1e9) 295 | cs.sort_by(CryptoField.CHANGE_PERCENT, ascending=False) 296 | cs.set_range(0, 20) 297 | cs.select( 298 | CryptoField.NAME, 299 | CryptoField.PRICE, 300 | CryptoField.CHANGE_PERCENT, 301 | CryptoField.VOLUME 302 | ) 303 | 304 | for df in cs.stream(interval=10): 305 | print("\n=== Top 20 Large Cap Movers ===") 306 | print(df) 307 | ``` 308 | 309 | ## Notes 310 | 311 | - Crypto markets are 24/7 - different dynamics than stocks 312 | - Higher volatility requires stricter risk management 313 | - Volume is crucial for entry/exit execution 314 | - Consider timezone for volume patterns 315 | -------------------------------------------------------------------------------- /app/js/code-generator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Code Generator for tvscreener 3 | * Generates Python code from UI configuration 4 | */ 5 | 6 | const CodeGenerator = { 7 | /** 8 | * Generate complete Python code from configuration 9 | * @param {Object} config - Configuration object 10 | * @returns {string} Generated Python code 11 | */ 12 | generate(config) { 13 | const lines = []; 14 | const imports = this.generateImports(config); 15 | 16 | lines.push(...imports); 17 | lines.push(''); 18 | lines.push(this.generateScreenerCreation(config)); 19 | 20 | // Filters 21 | if (config.filters && config.filters.length > 0) { 22 | lines.push(''); 23 | lines.push('# Filters'); 24 | for (const filter of config.filters) { 25 | const filterLine = this.generateFilter(filter, config); 26 | if (filterLine) { 27 | lines.push(filterLine); 28 | } 29 | } 30 | } 31 | 32 | // Fields 33 | if (config.selectAll) { 34 | lines.push(''); 35 | lines.push('# Select all available fields'); 36 | lines.push('ss.select_all()'); 37 | } else if (config.fields && config.fields.length > 0) { 38 | lines.push(''); 39 | lines.push('# Fields to retrieve'); 40 | lines.push(this.generateSelect(config.fields, config)); 41 | } 42 | 43 | // Index (only for stock screener) 44 | if (config.index && config.screenerConfig?.hasIndex) { 45 | lines.push(''); 46 | lines.push('# Filter by index'); 47 | lines.push(`ss.set_index(IndexSymbol.${config.index})`); 48 | } 49 | 50 | // Sort 51 | if (config.sortField) { 52 | lines.push(''); 53 | lines.push('# Sorting'); 54 | const ascending = config.sortOrder === 'asc' ? 'True' : 'False'; 55 | const fieldClass = config.screenerConfig?.fieldClass || 'StockField'; 56 | lines.push(`ss.sort_by(${fieldClass}.${config.sortField}, ascending=${ascending})`); 57 | } 58 | 59 | // Limit 60 | if (config.limit && config.limit !== 150) { 61 | lines.push(''); 62 | lines.push('# Result limit'); 63 | lines.push(`ss.set_range(0, ${config.limit})`); 64 | } 65 | 66 | // Get data 67 | lines.push(''); 68 | lines.push('# Execute query'); 69 | lines.push('df = ss.get()'); 70 | lines.push('print(f"Found {len(df)} results")'); 71 | lines.push('df.head(20)'); 72 | 73 | return lines.join('\n'); 74 | }, 75 | 76 | /** 77 | * Generate import statements 78 | */ 79 | generateImports(config) { 80 | const items = []; 81 | const screenerClass = config.screenerConfig?.class || 'StockScreener'; 82 | const fieldClass = config.screenerConfig?.fieldClass || 'StockField'; 83 | 84 | // Screener class 85 | items.push(screenerClass); 86 | 87 | // Field class (if used for fields, filters, or sorting) 88 | const needsFieldClass = 89 | (config.fields && config.fields.length > 0) || 90 | (config.filters && config.filters.length > 0) || 91 | config.sortField || 92 | config.selectAll; 93 | 94 | if (needsFieldClass) { 95 | items.push(fieldClass); 96 | } 97 | 98 | // IndexSymbol if used 99 | if (config.index && config.screenerConfig?.hasIndex) { 100 | items.push('IndexSymbol'); 101 | } 102 | 103 | return [`from tvscreener import ${items.join(', ')}`]; 104 | }, 105 | 106 | /** 107 | * Generate screener creation line 108 | */ 109 | generateScreenerCreation(config) { 110 | const screenerClass = config.screenerConfig?.class || 'StockScreener'; 111 | return `ss = ${screenerClass}()`; 112 | }, 113 | 114 | /** 115 | * Generate a single filter line 116 | */ 117 | generateFilter(filter, config) { 118 | if (!filter.field || !filter.operator || filter.value === '') { 119 | return null; 120 | } 121 | 122 | const fieldClass = config.screenerConfig?.fieldClass || 'StockField'; 123 | const fieldRef = `${fieldClass}.${filter.field}`; 124 | 125 | switch (filter.operator) { 126 | case '>': 127 | return `ss.where(${fieldRef} > ${this.formatValue(filter.value, filter.format)})`; 128 | case '>=': 129 | return `ss.where(${fieldRef} >= ${this.formatValue(filter.value, filter.format)})`; 130 | case '<': 131 | return `ss.where(${fieldRef} < ${this.formatValue(filter.value, filter.format)})`; 132 | case '<=': 133 | return `ss.where(${fieldRef} <= ${this.formatValue(filter.value, filter.format)})`; 134 | case '==': 135 | return `ss.where(${fieldRef} == ${this.formatValue(filter.value, filter.format)})`; 136 | case '!=': 137 | return `ss.where(${fieldRef} != ${this.formatValue(filter.value, filter.format)})`; 138 | case 'between': 139 | return `ss.where(${fieldRef}.between(${this.formatValue(filter.value, filter.format)}, ${this.formatValue(filter.value2, filter.format)}))`; 140 | case 'isin': 141 | const values = filter.value.split(',').map(v => this.formatValue(v.trim(), filter.format)); 142 | return `ss.where(${fieldRef}.isin([${values.join(', ')}]))`; 143 | default: 144 | return null; 145 | } 146 | }, 147 | 148 | /** 149 | * Format a value based on its type 150 | */ 151 | formatValue(value, format) { 152 | if (value === null || value === undefined || value === '') { 153 | return 'None'; 154 | } 155 | 156 | // Check if it's a number 157 | const num = parseFloat(value); 158 | if (!isNaN(num) && format !== 'text') { 159 | // Format large numbers with underscores for readability 160 | if (Math.abs(num) >= 1000000) { 161 | return this.formatLargeNumber(num); 162 | } 163 | return String(num); 164 | } 165 | 166 | // String value - escape backslashes first, then quotes 167 | return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; 168 | }, 169 | 170 | /** 171 | * Format large numbers with underscores 172 | */ 173 | formatLargeNumber(num) { 174 | if (num >= 1e12) { 175 | return `${num / 1e12}e12`; 176 | } else if (num >= 1e9) { 177 | const billions = num / 1e9; 178 | if (Number.isInteger(billions)) { 179 | return `${billions}e9`; 180 | } 181 | return `${billions}e9`; 182 | } else if (num >= 1e6) { 183 | const millions = num / 1e6; 184 | if (Number.isInteger(millions)) { 185 | return `${millions}_000_000`; 186 | } 187 | return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, '_'); 188 | } 189 | return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, '_'); 190 | }, 191 | 192 | /** 193 | * Generate select statement 194 | */ 195 | generateSelect(fields, config) { 196 | if (fields.length === 0) { 197 | return '# Using default fields'; 198 | } 199 | 200 | const fieldClass = config.screenerConfig?.fieldClass || 'StockField'; 201 | 202 | if (fields.length <= 3) { 203 | const fieldRefs = fields.map(f => `${fieldClass}.${f}`).join(', '); 204 | return `ss.select(${fieldRefs})`; 205 | } 206 | 207 | // Multi-line for many fields 208 | const lines = ['ss.select(']; 209 | for (let i = 0; i < fields.length; i++) { 210 | const comma = i < fields.length - 1 ? ',' : ''; 211 | lines.push(` ${fieldClass}.${fields[i]}${comma}`); 212 | } 213 | lines.push(')'); 214 | return lines.join('\n'); 215 | } 216 | }; 217 | 218 | // Export for use in app.js 219 | if (typeof module !== 'undefined' && module.exports) { 220 | module.exports = CodeGenerator; 221 | } 222 | -------------------------------------------------------------------------------- /tests/unit/test_field_conditions.py: -------------------------------------------------------------------------------- 1 | """Tests for the new Pythonic comparison operator syntax on Fields.""" 2 | import pytest 3 | from tvscreener import StockScreener, StockField, FilterOperator 4 | from tvscreener.filter import FieldCondition 5 | 6 | 7 | class TestFieldCondition: 8 | """Test FieldCondition class.""" 9 | 10 | def test_greater_than(self): 11 | """Test > operator.""" 12 | cond = StockField.PRICE > 100 13 | assert isinstance(cond, FieldCondition) 14 | assert cond.field == StockField.PRICE 15 | assert cond.operation == FilterOperator.ABOVE 16 | assert cond.value == 100 17 | 18 | def test_greater_than_or_equal(self): 19 | """Test >= operator.""" 20 | cond = StockField.VOLUME >= 1_000_000 21 | assert isinstance(cond, FieldCondition) 22 | assert cond.field == StockField.VOLUME 23 | assert cond.operation == FilterOperator.ABOVE_OR_EQUAL 24 | assert cond.value == 1_000_000 25 | 26 | def test_less_than(self): 27 | """Test < operator.""" 28 | cond = StockField.RELATIVE_STRENGTH_INDEX_14 < 30 29 | assert isinstance(cond, FieldCondition) 30 | assert cond.field == StockField.RELATIVE_STRENGTH_INDEX_14 31 | assert cond.operation == FilterOperator.BELOW 32 | assert cond.value == 30 33 | 34 | def test_less_than_or_equal(self): 35 | """Test <= operator.""" 36 | cond = StockField.PRICE <= 50 37 | assert isinstance(cond, FieldCondition) 38 | assert cond.field == StockField.PRICE 39 | assert cond.operation == FilterOperator.BELOW_OR_EQUAL 40 | assert cond.value == 50 41 | 42 | def test_equal(self): 43 | """Test == operator with value comparison.""" 44 | cond = StockField.SECTOR == 'Technology' 45 | assert isinstance(cond, FieldCondition) 46 | assert cond.field == StockField.SECTOR 47 | assert cond.operation == FilterOperator.EQUAL 48 | assert cond.value == 'Technology' 49 | 50 | def test_not_equal(self): 51 | """Test != operator with value comparison.""" 52 | cond = StockField.SECTOR != 'Finance' 53 | assert isinstance(cond, FieldCondition) 54 | assert cond.field == StockField.SECTOR 55 | assert cond.operation == FilterOperator.NOT_EQUAL 56 | assert cond.value == 'Finance' 57 | 58 | def test_between(self): 59 | """Test between() method.""" 60 | cond = StockField.MARKET_CAPITALIZATION.between(1e9, 10e9) 61 | assert isinstance(cond, FieldCondition) 62 | assert cond.field == StockField.MARKET_CAPITALIZATION 63 | assert cond.operation == FilterOperator.IN_RANGE 64 | assert cond.value == [1e9, 10e9] 65 | 66 | def test_not_between(self): 67 | """Test not_between() method.""" 68 | cond = StockField.PRICE.not_between(50, 100) 69 | assert isinstance(cond, FieldCondition) 70 | assert cond.field == StockField.PRICE 71 | assert cond.operation == FilterOperator.NOT_IN_RANGE 72 | assert cond.value == [50, 100] 73 | 74 | def test_isin(self): 75 | """Test isin() method.""" 76 | cond = StockField.SECTOR.isin(['Technology', 'Healthcare']) 77 | assert isinstance(cond, FieldCondition) 78 | assert cond.field == StockField.SECTOR 79 | assert cond.operation == FilterOperator.IN_RANGE 80 | assert cond.value == ['Technology', 'Healthcare'] 81 | 82 | def test_not_in(self): 83 | """Test not_in() method.""" 84 | cond = StockField.SECTOR.not_in(['Finance', 'Utilities']) 85 | assert isinstance(cond, FieldCondition) 86 | assert cond.field == StockField.SECTOR 87 | assert cond.operation == FilterOperator.NOT_IN_RANGE 88 | assert cond.value == ['Finance', 'Utilities'] 89 | 90 | def test_enum_equality_preserved(self): 91 | """Test that enum-to-enum equality still works.""" 92 | assert StockField.PRICE == StockField.PRICE 93 | assert StockField.PRICE != StockField.VOLUME 94 | 95 | def test_to_filter(self): 96 | """Test to_filter() method.""" 97 | cond = StockField.PRICE > 100 98 | filter_ = cond.to_filter() 99 | assert filter_.field == StockField.PRICE 100 | assert filter_.operation == FilterOperator.ABOVE 101 | assert filter_.values == [100] 102 | 103 | def test_repr(self): 104 | """Test string representation.""" 105 | cond = StockField.PRICE > 100 106 | assert 'PRICE' in repr(cond) 107 | assert 'ABOVE' in repr(cond) 108 | assert '100' in repr(cond) 109 | 110 | 111 | class TestFieldWithIntervalConditions: 112 | """Test comparison operators on FieldWithInterval.""" 113 | 114 | def test_greater_than(self): 115 | """Test > operator on FieldWithInterval.""" 116 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 117 | cond = rsi_1h > 70 118 | assert isinstance(cond, FieldCondition) 119 | assert cond.operation == FilterOperator.ABOVE 120 | assert cond.value == 70 121 | assert cond.field.field_name == 'RSI|60' 122 | 123 | def test_between(self): 124 | """Test between() on FieldWithInterval.""" 125 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 126 | cond = rsi_1h.between(30, 70) 127 | assert isinstance(cond, FieldCondition) 128 | assert cond.operation == FilterOperator.IN_RANGE 129 | assert cond.value == [30, 70] 130 | 131 | 132 | class TestFieldWithHistoryConditions: 133 | """Test comparison operators on FieldWithHistory.""" 134 | 135 | def test_greater_than(self): 136 | """Test > operator on FieldWithHistory.""" 137 | # Use a field that supports historical lookback 138 | prev_rsi = StockField.RELATIVE_STRENGTH_INDEX_14.with_history(1) 139 | cond = prev_rsi > 70 140 | assert isinstance(cond, FieldCondition) 141 | assert cond.operation == FilterOperator.ABOVE 142 | assert cond.value == 70 143 | 144 | 145 | class TestScreenerWhereMethod: 146 | """Test Screener.where() with new syntax.""" 147 | 148 | def test_where_with_condition(self): 149 | """Test where() with FieldCondition.""" 150 | ss = StockScreener() 151 | ss.where(StockField.PRICE > 100) 152 | 153 | assert len(ss.filters) == 1 154 | filter_dict = ss.filters[0].to_dict() 155 | assert filter_dict['left'] == 'close' 156 | assert filter_dict['operation'] == 'greater' 157 | assert filter_dict['right'] == 100 158 | 159 | def test_where_with_legacy_syntax(self): 160 | """Test where() with legacy (field, operator, value) syntax.""" 161 | ss = StockScreener() 162 | ss.where(StockField.PRICE, FilterOperator.ABOVE, 100) 163 | 164 | assert len(ss.filters) == 1 165 | filter_dict = ss.filters[0].to_dict() 166 | assert filter_dict['left'] == 'close' 167 | assert filter_dict['operation'] == 'greater' 168 | assert filter_dict['right'] == 100 169 | 170 | def test_where_chaining(self): 171 | """Test method chaining with where().""" 172 | ss = StockScreener() 173 | result = ss.where(StockField.PRICE > 100).where(StockField.VOLUME > 1e6) 174 | 175 | assert result is ss # Returns self for chaining 176 | assert len(ss.filters) == 2 177 | 178 | def test_where_mixed_syntax(self): 179 | """Test mixing new and legacy syntax.""" 180 | ss = StockScreener() 181 | ss.where(StockField.PRICE > 100) # New syntax 182 | ss.where(StockField.VOLUME, FilterOperator.ABOVE, 1e6) # Legacy syntax 183 | 184 | assert len(ss.filters) == 2 185 | 186 | def test_where_with_between(self): 187 | """Test where() with between() condition.""" 188 | ss = StockScreener() 189 | ss.where(StockField.MARKET_CAPITALIZATION.between(1e9, 10e9)) 190 | 191 | assert len(ss.filters) == 1 192 | filter_dict = ss.filters[0].to_dict() 193 | assert filter_dict['operation'] == 'in_range' 194 | assert filter_dict['right'] == [1e9, 10e9] 195 | 196 | def test_where_with_interval_field(self): 197 | """Test where() with FieldWithInterval.""" 198 | ss = StockScreener() 199 | rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60') 200 | ss.where(rsi_1h > 70) 201 | 202 | assert len(ss.filters) == 1 203 | filter_dict = ss.filters[0].to_dict() 204 | assert filter_dict['left'] == 'RSI|60' 205 | assert filter_dict['operation'] == 'greater' 206 | assert filter_dict['right'] == 70 207 | --------------------------------------------------------------------------------