├── 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 | [](https://badge.fury.io/py/tvscreener)
6 | [](https://pepy.tech/project/tvscreener)
7 | [](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 |
--------------------------------------------------------------------------------