Skill: subway-chenyan/tvscreener (version 1.0.0)

--- name: tvscreener description: Query TradingView screener data for HK, A-share, A-share ETF, and US symbols with deepentropy/tvscreener. Use for stock lookup, technical indicators (price/change/RSI/MACD/volume), symbol filtering, and custom field/filter-based market queries. --- # tvscreener Use this skill for market queries with simple scripts first, then native Python when needed. ## Install ```bash python3 -m pip install -U tvscreener ``` Python must be `>=3.10`. ## Quick commands (run from skill root) > Use Python 3.10+ in your preferred environment (venv/pyenv/system Python). ```bash # Preset single-symbol output (recommended) python3 scripts/query_symbol.py --symbol HKEX:700 --market HONGKONG # Custom query (fields + filters) bash scripts/run_query.sh \ --market CHINA \ --symbol SHSE:600519 \ --fields 'NAME,PRICE,CHANGE_PERCENT,VOLUME,RELATIVE_STRENGTH_INDEX_14,MACD_LEVEL_12_26,MACD_SIGNAL_12_26,MACD_HIST,SIMPLE_MOVING_AVERAGE_20,SIMPLE_MOVING_AVERAGE_50,SIMPLE_MOVING_AVERAGE_200,EXPONENTIAL_MOVING_AVERAGE_20,EXPONENTIAL_MOVING_AVERAGE_50,EXPONENTIAL_MOVING_AVERAGE_200,BOLLINGER_UPPER_BAND_20,BOLLINGER_LOWER_BAND_20,STOCHASTIC_PERCENTK_14_3_3,STOCHASTIC_PERCENTD_14_3_3,AVERAGE_TRUE_RANGE_14,MOVING_AVERAGES_RATING' \ --filter 'NAME=600519' # Field discovery python3 scripts/discover_fields.py --keyword macd --limit 20 ``` ### Shell quoting notes - Wrap `--fields` and `--filter` in single quotes. - If you use interval syntax like `FIELD|60`, quoting is mandatory to avoid shell pipe parsing. ## Query rules - Core technical set (recommended): `PRICE`, `CHANGE_PERCENT`, `VOLUME`, `RELATIVE_STRENGTH_INDEX_14`, `MACD_LEVEL_12_26`, `MACD_SIGNAL_12_26`, `MACD_HIST`, `SIMPLE_MOVING_AVERAGE_20/50/200`, [REDACTED_SECRET], `BOLLINGER_UPPER_BAND_20`, `BOLLINGER_LOWER_BAND_20`, `STOCHASTIC_PERCENTK_14_3_3`, `STOCHASTIC_PERCENTD_14_3_3`, `AVERAGE_TRUE_RANGE_14`, `MOVING_AVERAGES_RATING` - Interval fields syntax: `FIELD|60` / `FIELD|240` (example: `RELATIVE_STRENGTH_INDEX_14|60`) - **Current caveat**: interval fields may fail in `scripts/custom_query.py` with `FieldWithInterval` attribute errors in some tvscreener versions. - Workaround: run without interval fields, or use `scripts/query_symbol.py` for stable single-symbol technical snapshots. - Filters: `=`, `!=`, `>`, `<`, `>=`, `<=` ## Troubleshooting - `ImportError: cannot import name 'Market' from 'tvscreener'` - Usually caused by mismatched Python/site-packages or multiple Python environments. - Fix: ensure commands and installation use the same Python (3.10+), then reinstall: - `python3 -m pip install -U tvscreener` - `zsh: command not found: 60,...` - Cause: unquoted `FIELD|60` interpreted as shell pipes. - Fix: single-quote the full `--fields` string. ## References - Workflow + patterns: `references/README_USAGE.md` - API details: - `references/api/screeners.md` - `references/api/fields.md` - `references/api/filters.md` - `references/api/enums.md` If scripts are insufficient, read references and write direct Python using tvscreener native API. ## Regression test ```bash bash scripts/test_markets.sh ``` Covers Tencent (HK), Moutai (A), A-share ETF (510300), and BIDU (US).

path: references/README_USAGE.md
content: # README_USAGE

Practical usage guide for this skill + mapping to API references.

Environment requirement: use `python3` with version `>=3.10` (recommended: conda `workshop` env).

## 1) Preferred execution order

1. Use `scripts/query_symbol.py` for a fast single-symbol snapshot.
2. Use `scripts/custom_query.py` for custom fields and filters.
3. Use `scripts/discover_fields.py` when field names are unknown.
4. If still insufficient, write direct Python using the native tvscreener API.

## 2) Script examples

```bash
# Tencent (HK)
python3 scripts/query_symbol.py --symbol HKEX:700 --market HONGKONG

# Moutai (A-share)
python3 scripts/custom_query.py --market CHINA --symbol SHSE:600519 --fields NAME,PRICE,CHANGE_PERCENT,VOLUME,RELATIVE_STRENGTH_INDEX_14,MACD_LEVEL_12_26,MACD_SIGNAL_12_26,MACD_HIST,SIMPLE_MOVING_AVERAGE_20,SIMPLE_MOVING_AVERAGE_50,SIMPLE_MOVING_AVERAGE_200,EXPONENTIAL_MOVING_AVERAGE_20,EXPONENTIAL_MOVING_AVERAGE_50,EXPONENTIAL_MOVING_AVERAGE_200,BOLLINGER_UPPER_BAND_20,BOLLINGER_LOWER_BAND_20,STOCHASTIC_PERCENTK_14_3_3,STOCHASTIC_PERCENTD_14_3_3,AVERAGE_TRUE_RANGE_14,MOVING_AVERAGES_RATING --filter "NAME=600519"

# CSI300 ETF (A-share ETF)
python3 scripts/custom_query.py --market CHINA --symbol SHSE:510300 --filter "NAME=510300"

# BIDU (US)
python3 scripts/custom_query.py --market AMERICA --symbol NASDAQ:BIDU --filter "NAME=BIDU"
```

## 3) Field/filter conventions

- Field example (full technical template): `PRICE`, `CHANGE_PERCENT`, `VOLUME`, `RELATIVE_STRENGTH_INDEX_14`, `MACD_LEVEL_12_26`, `MACD_SIGNAL_12_26`, `MACD_HIST`, `SIMPLE_MOVING_AVERAGE_20/50/200`, `EXPONENTIAL_MOVING_AVERAGE_20/50/200`, `BOLLINGER_UPPER_BAND_20`, `BOLLINGER_LOWER_BAND_20`, `STOCHASTIC_PERCENTK_14_3_3`, `STOCHASTIC_PERCENTD_14_3_3`, `AVERAGE_TRUE_RANGE_14`, `MOVING_AVERAGES_RATING`
- Interval field: `RELATIVE_STRENGTH_INDEX_14|60`
- Filter ops: `=`, `!=`, `>`, `<`, `>=`, `<=`
- String-like fields (`NAME`, `ACTIVE_SYMBOL`, `EXCHANGE`) are treated as strings.

## 4) Native Python fallback

Use this when scripts cannot express the requested logic.

```python
from tvscreener import StockScreener, StockField, Market

ss = StockScreener()
ss.set_markets(Market.HONGKONG)
ss.set_range(0, 200)

ss.select(
    StockField.NAME,
    StockField.PRICE,
    StockField.CHANGE_PERCENT,
    StockField.RELATIVE_STRENGTH_INDEX_14,
    StockField.MACD_LEVEL_12_26,
)
ss.where(StockField.NAME == "700")

print(ss.get().to_json(orient="records", force_ascii=False, indent=2))
```

## 5) API reference map

- Screeners: `references/api/screeners.md`
- Fields: `references/api/fields.md`
- Filters: `references/api/filters.md`
- Enums/Markets: `references/api/enums.md`

## 6) Known caveats

- Exchange prefix may differ in returned symbol (`SHSE:600519` vs `SSE:600519`).
- If `with_interval()` combinations fail, fallback to base daily fields first.


path: references/api/enums.md
content: # Enums API Reference

Enumeration types for type-safe configuration.

## IndexSymbol

Major stock indices for filtering.

```python
from tvscreener import IndexSymbol
```

### Major US Indices

| Index | Symbol |
|-------|--------|
| S&P 500 | `IndexSymbol.SP500` |
| NASDAQ 100 | `IndexSymbol.NASDAQ_100` |
| Dow Jones Industrial | `IndexSymbol.DOW_JONES` |
| Russell 2000 | `IndexSymbol.RUSSELL_2000` |
| Russell 1000 | `IndexSymbol.RUSSELL_1000` |

### Sector Indices

| Index | Symbol |
|-------|--------|
| S&P 500 Technology | `IndexSymbol.SP500_INFORMATION_TECHNOLOGY` |
| S&P 500 Healthcare | `IndexSymbol.SP500_HEALTH_CARE` |
| S&P 500 Financials | `IndexSymbol.SP500_FINANCIALS` |
| S&P 500 Energy | `IndexSymbol.SP500_ENERGY` |
| PHLX Semiconductor | `IndexSymbol.PHLX_SEMICONDUCTOR` |

### Usage

```python
from tvscreener import StockScreener, IndexSymbol

ss = StockScreener()

# Single index
ss.set_index(IndexSymbol.SP500)

# Multiple indices
ss.set_index(IndexSymbol.NASDAQ_100, IndexSymbol.DOW_JONES)
```

### Search

```python
# Find indices by name
results = IndexSymbol.search("technology")
for idx in results:
    print(idx.name, idx.value)
```

---

## Market

Geographic market regions.

```python
from tvscreener import Market
```

### Available Markets

| Market | Description |
|--------|-------------|
| `Market.ALL` | All markets |
| `Market.AMERICA` | United States |
| `Market.UK` | United Kingdom |
| `Market.GERMANY` | Germany |
| `Market.FRANCE` | France |
| `Market.JAPAN` | Japan |
| `Market.CHINA` | China |
| `Market.HONG_KONG` | Hong Kong |
| `Market.INDIA` | India |
| `Market.BRAZIL` | Brazil |
| `Market.CANADA` | Canada |
| `Market.AUSTRALIA` | Australia |

### Usage

```python
ss = StockScreener()

# Single market
ss.set_markets(Market.AMERICA)

# Multiple markets
ss.set_markets(Market.AMERICA, Market.UK, Market.GERMANY)
```

---

## SymbolType

Security types for filtering.

```python
from tvscreener import SymbolType
```

### Available Types

| Type | Description |
|------|-------------|
| `SymbolType.COMMON_STOCK` | Common shares |
| `SymbolType.ETF` | Exchange-Traded Funds |
| `SymbolType.PREFERRED_STOCK` | Preferred shares |
| `SymbolType.REIT` | Real Estate Investment Trusts |
| `SymbolType.CLOSED_END_FUND` | Closed-end funds |
| `SymbolType.MUTUAL_FUND` | Mutual funds |
| `SymbolType.ADR` | American Depositary Receipts |

### Usage

```python
ss = StockScreener()

# Single type
ss.set_symbol_types(SymbolType.COMMON_STOCK)

# Multiple types
ss.set_symbol_types(SymbolType.COMMON_STOCK, SymbolType.ETF)
```

### ETF Screening Example

```python
ss = StockScreener()
ss.set_symbol_types(SymbolType.ETF)
ss.where(StockField.VOLUME > 1_000_000)
ss.sort_by(StockField.VOLUME, ascending=False)

df = ss.get()
```

---

## Exchange

Stock exchanges.

```python
from tvscreener import Exchange
```

### Available Exchanges

| Exchange | Symbol |
|----------|--------|
| NASDAQ | `Exchange.NASDAQ` |
| NYSE | `Exchange.NYSE` |
| NYSE American | `Exchange.NYSE_AMERICAN` |
| NYSE Arca | `Exchange.NYSE_ARCA` |

### Usage with Filters

```python
ss = StockScreener()
ss.where(StockField.EXCHANGE == Exchange.NASDAQ)

# Or multiple
ss.where(StockField.EXCHANGE.isin([Exchange.NASDAQ, Exchange.NYSE]))
```

---

## FilterOperator

Filter operations for comparisons.

```python
from tvscreener import FilterOperator
```

### Available Operators

| Operator | Value | Python Equivalent |
|----------|-------|-------------------|
| `FilterOperator.ABOVE` | `'greater'` | `>` |
| `FilterOperator.ABOVE_OR_EQUAL` | `'egreater'` | `>=` |
| `FilterOperator.BELOW` | `'less'` | `<` |
| `FilterOperator.BELOW_OR_EQUAL` | `'eless'` | `<=` |
| `FilterOperator.EQUAL` | `'equal'` | `==` |
| `FilterOperator.NOT_EQUAL` | `'nequal'` | `!=` |
| `FilterOperator.IN_RANGE` | `'in_range'` | `.between()` |
| `FilterOperator.NOT_IN_RANGE` | `'not_in_range'` | `.not_between()` |

### Legacy Usage

```python
ss = StockScreener()
ss.where(StockField.PRICE, FilterOperator.ABOVE, 100)
```

### Modern Syntax (Preferred)

```python
ss = StockScreener()
ss.where(StockField.PRICE > 100)
```

---

## TimeInterval

Time intervals for technical indicators.

### Available Intervals

| Interval | Code |
|----------|------|
| 1 minute | `'1'` |
| 5 minutes | `'5'` |
| 15 minutes | `'15'` |
| 30 minutes | `'30'` |
| 1 hour | `'60'` |
| 2 hours | `'120'` |
| 4 hours | `'240'` |
| Daily | `'1D'` |
| Weekly | `'1W'` |
| Monthly | `'1M'` |

### Usage

```python
# RSI on 1-hour timeframe
rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60')

# MACD on 4-hour timeframe
macd_4h = StockField.MACD_LEVEL_12_26.with_interval('240')

# SMA on weekly timeframe
sma_weekly = StockField.SIMPLE_MOVING_AVERAGE_50.with_interval('1W')
```

---

## Complete Example

```python
from tvscreener import (
    StockScreener,
    StockField,
    IndexSymbol,
    Market,
    SymbolType,
    Exchange
)

ss = StockScreener()

# Market and type filters
ss.set_markets(Market.AMERICA)
ss.set_symbol_types(SymbolType.COMMON_STOCK)
ss.set_index(IndexSymbol.SP500)

# Exchange filter
ss.where(StockField.EXCHANGE.isin([Exchange.NASDAQ, Exchange.NYSE]))

# Price and volume
ss.where(StockField.PRICE.between(50, 500))
ss.where(StockField.VOLUME >= 1_000_000)

# Technical with interval
rsi_1h = StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60')
ss.where(rsi_1h < 40)

df = ss.get()
```


path: references/api/fields.md
content: # Fields API Reference

Field enums provide type-safe access to screener data.

## Field Classes

| Class | Screener | Field Count |
|-------|----------|-------------|
| `StockField` | `StockScreener` | ~3,526 |
| `CryptoField` | `CryptoScreener` | ~3,108 |
| `ForexField` | `ForexScreener` | ~2,965 |
| `BondField` | `BondScreener` | ~201 |
| `FuturesField` | `FuturesScreener` | ~393 |
| `CoinField` | `CoinScreener` | ~3,026 |

## Import

```python
from tvscreener import StockField
from tvscreener.field import StockField, CryptoField, ForexField
```

## Comparison Operators

Fields support Python comparison operators for filtering:

### Numeric Comparisons

```python
StockField.PRICE > 100          # Greater than
StockField.PRICE >= 100         # Greater than or equal
StockField.PRICE < 100          # Less than
StockField.PRICE <= 100         # Less than or equal
StockField.PRICE == 100         # Equal to
StockField.PRICE != 100         # Not equal to
```

### Range Methods

```python
StockField.PRICE.between(50, 200)       # Value in range [50, 200]
StockField.PRICE.not_between(50, 200)   # Value outside range
```

### List Methods

```python
StockField.SECTOR.isin(['Technology', 'Healthcare'])
StockField.SECTOR.not_in(['Finance', 'Utilities'])
```

## Field Properties

Each field has accessible properties:

```python
field = StockField.PRICE

field.name          # Enum name: 'PRICE'
field.value         # Field definition tuple
field.label         # Human-readable label
field.field_name    # API field name
field.format        # Data format type
```

### Format Types

| Format | Description | Example Fields |
|--------|-------------|----------------|
| `currency` | Monetary values | PRICE, MARKET_CAP |
| `percent` | Percentage | CHANGE_PERCENT, ROE |
| `float` | Decimal number | PE_RATIO, RSI |
| `number_group` | Large numbers | VOLUME, SHARES |
| `text` | Text string | NAME, SECTOR |
| `bool` | True/False | IS_PRIMARY |
| `date` | Date | EX_DIVIDEND_DATE |

## Time Intervals

Technical fields support multiple timeframes:

```python
# Default (daily)
StockField.RELATIVE_STRENGTH_INDEX_14

# With specific interval
StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('60')   # 1 hour
StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('240')  # 4 hours
StockField.RELATIVE_STRENGTH_INDEX_14.with_interval('1W')   # Weekly
```

### Available Intervals

| Code | Timeframe |
|------|-----------|
| `'1'` | 1 minute |
| `'5'` | 5 minutes |
| `'15'` | 15 minutes |
| `'30'` | 30 minutes |
| `'60'` | 1 hour |
| `'120'` | 2 hours |
| `'240'` | 4 hours |
| `'1D'` | Daily |
| `'1W'` | Weekly |
| `'1M'` | Monthly |

## Historical Data

Some fields support historical lookback:

```python
# Previous day's RSI
StockField.RELATIVE_STRENGTH_INDEX_14.with_history(1)

# RSI from 5 days ago
StockField.RELATIVE_STRENGTH_INDEX_14.with_history(5)
```

## Field Discovery

### Search

Find fields by name:

```python
matches = StockField.search("dividend")
for field in matches[:10]:
    print(f"{field.name}: {field.label}")
```

### Presets

Get grouped fields:

```python
# Technical indicators
technicals = StockField.technicals()

# Oscillators
oscillators = StockField.oscillators()

# Moving averages
mas = StockField.moving_averages()

# Valuations
valuations = StockField.valuations()

# Dividends
dividends = StockField.dividends()

# Performance
performance = StockField.performance()

# Recommendations
recommendations = StockField.recommendations()
```

## Common Fields Reference

### Price & Volume

| Field | Description |
|-------|-------------|
| `PRICE` | Current price |
| `OPEN` | Day's open |
| `HIGH` | Day's high |
| `LOW` | Day's low |
| `CLOSE` | Previous close |
| `VOLUME` | Trading volume |
| `RELATIVE_VOLUME` | Volume vs average |

### Valuation

| Field | Description |
|-------|-------------|
| `PE_RATIO_TTM` | Price/Earnings (TTM) |
| `PRICE_TO_BOOK_FY` | Price/Book |
| `PRICE_TO_SALES_FY` | Price/Sales |
| `EV_TO_EBITDA_TTM` | EV/EBITDA |
| `PRICE_EARNINGS_TO_GROWTH_TTM` | PEG Ratio |
| `MARKET_CAPITALIZATION` | Market Cap |

### Dividends

| Field | Description |
|-------|-------------|
| `DIVIDEND_YIELD_FY` | Dividend Yield % |
| `DIVIDENDS_PER_SHARE_FY` | Dividend Per Share |
| `PAYOUT_RATIO_TTM` | Payout Ratio % |
| `EX_DIVIDEND_DATE` | Ex-Dividend Date |

### Technical

| Field | Description |
|-------|-------------|
| `RELATIVE_STRENGTH_INDEX_14` | RSI (14) |
| `MACD_LEVEL_12_26` | MACD Line |
| `MACD_SIGNAL_12_26` | MACD Signal |
| `SIMPLE_MOVING_AVERAGE_50` | SMA 50 |
| `SIMPLE_MOVING_AVERAGE_200` | SMA 200 |
| `EXPONENTIAL_MOVING_AVERAGE_20` | EMA 20 |
| `AVERAGE_TRUE_RANGE_14` | ATR (14) |
| `AVERAGE_DIRECTIONAL_INDEX_14` | ADX (14) |
| `STOCHASTIC_K_14_3_3` | Stochastic %K |
| `STOCHASTIC_D_14_3_3` | Stochastic %D |
| `BOLLINGER_UPPER_BAND_20` | BB Upper |
| `BOLLINGER_LOWER_BAND_20` | BB Lower |

### Performance

| Field | Description |
|-------|-------------|
| `CHANGE_PERCENT` | Today's Change % |
| `WEEKLY_PERFORMANCE` | 1 Week Change % |
| `MONTHLY_PERFORMANCE` | 1 Month Change % |
| `PERFORMANCE_3_MONTH` | 3 Month Change % |
| `PERFORMANCE_6_MONTH` | 6 Month Change % |
| `PERFORMANCE_YTD` | Year-to-Date % |
| `PERFORMANCE_1_YEAR` | 1 Year Change % |

### Profitability

| Field | Description |
|-------|-------------|
| `RETURN_ON_EQUITY_TTM` | ROE % |
| `RETURN_ON_ASSETS_TTM` | ROA % |
| `GROSS_MARGIN_TTM` | Gross Margin % |
| `NET_MARGIN_TTM` | Net Margin % |
| `OPERATING_MARGIN_TTM` | Operating Margin % |


path: references/api/filters.md
content: # Filters API Reference

Filter operators and conditions for screening.

## FieldCondition

Created automatically when using comparison operators on fields.

```python
from tvscreener import StockField

# These create FieldCondition objects
condition1 = StockField.PRICE > 100
condition2 = StockField.PE_RATIO_TTM.between(10, 25)
condition3 = StockField.SECTOR.isin(['Technology', 'Healthcare'])
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `field` | `Field` | The field being compared |
| `operation` | `FilterOperator` | The comparison operator |
| `value` | `Any` | The comparison value |

### Methods

#### `to_filter()`

Convert to TradingView API filter format.

```python
condition = StockField.PRICE > 100
filter_dict = condition.to_filter()
# {'left': 'close', 'operation': 'greater', 'right': 100}
```

## FilterOperator

Enum of available filter operations.

```python
from tvscreener import FilterOperator
```

### Comparison Operators

| Operator | Python Syntax | FilterOperator |
|----------|---------------|----------------|
| Greater than | `>` | `FilterOperator.ABOVE` |
| Greater or equal | `>=` | `FilterOperator.ABOVE_OR_EQUAL` |
| Less than | `<` | `FilterOperator.BELOW` |
| Less or equal | `<=` | `FilterOperator.BELOW_OR_EQUAL` |
| Equal | `==` | `FilterOperator.EQUAL` |
| Not equal | `!=` | `FilterOperator.NOT_EQUAL` |

### Range Operators

| Method | FilterOperator |
|--------|----------------|
| `.between(min, max)` | `FilterOperator.IN_RANGE` |
| `.not_between(min, max)` | `FilterOperator.NOT_IN_RANGE` |

### List Operators

| Method | FilterOperator |
|--------|----------------|
| `.isin(values)` | `FilterOperator.IN_RANGE` |
| `.not_in(values)` | `FilterOperator.NOT_IN_RANGE` |

## ExtraFilter

Special filters not tied to specific fields.

```python
from tvscreener import ExtraFilter, FilterOperator
```

### Available Extra Filters

| Filter | Description |
|--------|-------------|
| `ExtraFilter.PRIMARY` | Primary listing only |
| `ExtraFilter.CURRENT_TRADING_DAY` | Currently trading |

### Usage

```python
ss = StockScreener()
ss.add_filter(ExtraFilter.PRIMARY, FilterOperator.EQUAL, True)
ss.add_filter(ExtraFilter.CURRENT_TRADING_DAY, FilterOperator.EQUAL, True)
```

## Legacy Syntax

The original filter syntax is still supported:

```python
from tvscreener import StockScreener, StockField, FilterOperator

ss = StockScreener()

# Legacy syntax
ss.where(StockField.PRICE, FilterOperator.ABOVE, 100)
ss.where(StockField.VOLUME, FilterOperator.ABOVE_OR_EQUAL, 1_000_000)

# Equivalent new syntax
ss.where(StockField.PRICE > 100)
ss.where(StockField.VOLUME >= 1_000_000)
```

## Filter Chaining

All filters are combined with AND logic:

```python
ss = StockScreener()

# All conditions must be true
ss.where(StockField.PRICE > 50)           # AND
ss.where(StockField.PRICE < 500)          # AND
ss.where(StockField.VOLUME >= 1_000_000)  # AND
ss.where(StockField.PE_RATIO_TTM.between(10, 30))

df = ss.get()  # Returns stocks matching ALL conditions
```

## Type Validation

Filters are validated against the screener type:

```python
ss = StockScreener()

# Valid - StockField with StockScreener
ss.where(StockField.PRICE > 100)

# Invalid - CryptoField with StockScreener
# ss.where(CryptoField.PRICE > 100)  # Raises error
```

## Complex Filters Example

```python
from tvscreener import StockScreener, StockField, IndexSymbol

ss = StockScreener()

# Index filter
ss.set_index(IndexSymbol.SP500)

# Price range
ss.where(StockField.PRICE.between(20, 500))

# Volume filter
ss.where(StockField.VOLUME >= 500_000)

# Valuation filters
ss.where(StockField.PE_RATIO_TTM.between(5, 25))
ss.where(StockField.PRICE_TO_BOOK_FY < 5)

# Sector filter
ss.where(StockField.SECTOR.not_in(['Finance', 'Utilities']))

# Technical filter
ss.where(StockField.RELATIVE_STRENGTH_INDEX_14.between(30, 70))

# Performance filter
ss.where(StockField.CHANGE_PERCENT > 0)

df = ss.get()
```


path: references/api/screeners.md
content: # Screeners API Reference

All screener classes share a common base API.

## Available Screeners

| Class | Import | Fields Class |
|-------|--------|--------------|
| `StockScreener` | `from tvscreener import StockScreener` | `StockField` |
| `CryptoScreener` | `from tvscreener import CryptoScreener` | `CryptoField` |
| `ForexScreener` | `from tvscreener import ForexScreener` | `ForexField` |
| `BondScreener` | `from tvscreener import BondScreener` | `BondField` |
| `FuturesScreener` | `from tvscreener import FuturesScreener` | `FuturesField` |
| `CoinScreener` | `from tvscreener import CoinScreener` | `CoinField` |

## Common Methods

### `get()`

Execute the query and return results as a pandas DataFrame.

```python
ss = StockScreener()
df = ss.get()
```

**Returns:** `pandas.DataFrame`

---

### `where(condition)`

Add a filter condition.

```python
# New syntax (v0.1.0+)
ss.where(StockField.PRICE > 100)
ss.where(StockField.PE_RATIO_TTM.between(10, 25))

# Legacy syntax
ss.where(StockField.PRICE, FilterOperator.ABOVE, 100)
```

**Parameters:**
- `condition`: A `FieldCondition` from comparison operators

**Returns:** `self` (for chaining)

---

### `select(*fields)`

Specify which fields to include in results.

```python
ss.select(
    StockField.NAME,
    StockField.PRICE,
    StockField.VOLUME
)
```

**Parameters:**
- `*fields`: One or more Field enum values

**Returns:** `self` (for chaining)

---

### `select_all()`

Select all available fields (~3,500 for stocks).

```python
ss.select_all()
```

**Returns:** `self` (for chaining)

---

### `sort_by(field, ascending=True)`

Sort results by a field.

```python
ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False)
```

**Parameters:**
- `field`: Field enum value to sort by
- `ascending`: `True` for ascending, `False` for descending

**Returns:** `self` (for chaining)

---

### `set_range(from_index, to_index)`

Set pagination range.

```python
ss.set_range(0, 100)  # First 100 results
ss.set_range(100, 200)  # Results 101-200
```

**Parameters:**
- `from_index`: Starting index (0-based)
- `to_index`: Ending index (exclusive), max 5000

**Returns:** `self` (for chaining)

---

### `search(query)`

Text search across name and description.

```python
ss.search('semiconductor')
```

**Parameters:**
- `query`: Search string

**Returns:** `self` (for chaining)

---

### `stream(interval=10)`

Stream results with periodic updates.

```python
for df in ss.stream(interval=5):
    print(df)
```

**Parameters:**
- `interval`: Seconds between updates (default: 10)

**Yields:** `pandas.DataFrame` on each update

---

## StockScreener-Specific Methods

### `set_index(*indices)`

Filter to index constituents (Stock only).

```python
from tvscreener import IndexSymbol

ss.set_index(IndexSymbol.SP500)
ss.set_index(IndexSymbol.NASDAQ_100, IndexSymbol.DOW_JONES)
```

**Parameters:**
- `*indices`: One or more `IndexSymbol` values

**Returns:** `self` (for chaining)

---

### `set_markets(*markets)`

Filter by market region.

```python
from tvscreener import Market

ss.set_markets(Market.AMERICA)
ss.set_markets(Market.JAPAN)
```

**Parameters:**
- `*markets`: One or more `Market` values

**Returns:** `self` (for chaining)

---

### `set_symbol_types(*types)`

Filter by security type.

```python
from tvscreener import SymbolType

ss.set_symbol_types(SymbolType.COMMON_STOCK)
ss.set_symbol_types(SymbolType.ETF)
```

**Parameters:**
- `*types`: One or more `SymbolType` values

**Returns:** `self` (for chaining)

---

## Properties

### `symbols`

Direct access to symbol configuration.

```python
ss.symbols = {
    "query": {"types": []},
    "tickers": ["NASDAQ:AAPL", "NYSE:IBM"]
}
```

---

## Method Chaining

All configuration methods return `self`, enabling fluent syntax:

```python
df = (
    StockScreener()
    .select(StockField.NAME, StockField.PRICE)
    .where(StockField.PRICE > 100)
    .sort_by(StockField.VOLUME, ascending=False)
    .set_range(0, 50)
    .get()
)
```

---

## Full Example

```python
from tvscreener import StockScreener, StockField, IndexSymbol

ss = StockScreener()

# Filter to S&P 500
ss.set_index(IndexSymbol.SP500)

# Add conditions
ss.where(StockField.PRICE.between(50, 500))
ss.where(StockField.PE_RATIO_TTM.between(10, 30))
ss.where(StockField.RELATIVE_STRENGTH_INDEX_14 < 50)

# Select fields
ss.select(
    StockField.NAME,
    StockField.PRICE,
    StockField.PE_RATIO_TTM,
    StockField.RELATIVE_STRENGTH_INDEX_14,
    StockField.MARKET_CAPITALIZATION
)

# Sort and paginate
ss.sort_by(StockField.MARKET_CAPITALIZATION, ascending=False)
ss.set_range(0, 100)

# Execute
df = ss.get()
```


path: scripts/custom_query.py
content: #!/usr/bin/env python3
import argparse
import json
import re
import sys
from typing import List

from tvscreener import Market, StockField, StockScreener


OPS = [">=", "<=", "!=", ">", "<", "="]


def parse_field(token: str):
    # Supports: PRICE or RELATIVE_STRENGTH_INDEX_14|60
    if "|" in token:
        name, interval = token.split("|", 1)
        base = getattr(StockField, name)
        return base.with_interval(interval)
    return getattr(StockField, token)


def parse_value(v: str, force_string: bool = False):
    if force_string:
        return v
    if re.fullmatch(r"-?\d+", v):
        return int(v)
    if re.fullmatch(r"-?\d+\.\d+", v):
        return float(v)
    return v


def apply_filter(ss: StockScreener, expr: str):
    # FIELD>=123, FIELD=abc, FIELD<10.5
    op = next((o for o in OPS if o in expr), None)
    if not op:
        raise ValueError(f"Invalid filter: {expr}")
    left, right = expr.split(op, 1)
    left = left.strip()
    force_string = left in {"NAME", "ACTIVE_SYMBOL", "EXCHANGE"}
    right = parse_value(right.strip(), force_string=force_string)
    field = parse_field(left)

    if op == "=":
        ss.where(field == right)
    elif op == "!=":
        ss.where(field != right)
    elif op == ">":
        ss.where(field > right)
    elif op == "<":
        ss.where(field < right)
    elif op == ">=":
        ss.where(field >= right)
    elif op == "<=":
        ss.where(field <= right)


def main() -> int:
    p = argparse.ArgumentParser(description="Flexible tvscreener query")
    p.add_argument("--market", default="HONGKONG", help="e.g. HONGKONG/CHINA/AMERICA")
    p.add_argument("--symbol", default="", help="Exact symbol preference (e.g. HKEX:700)")
    p.add_argument("--fields", default="NAME,PRICE,CHANGE_PERCENT,VOLUME,RELATIVE_STRENGTH_INDEX_14,MACD_LEVEL_12_26,MACD_SIGNAL_12_26,MACD_HIST,SIMPLE_MOVING_AVERAGE_20,SIMPLE_MOVING_AVERAGE_50,SIMPLE_MOVING_AVERAGE_200,EXPONENTIAL_MOVING_AVERAGE_20,EXPONENTIAL_MOVING_AVERAGE_50,EXPONENTIAL_MOVING_AVERAGE_200,BOLLINGER_UPPER_BAND_20,BOLLINGER_LOWER_BAND_20,STOCHASTIC_PERCENTK_14_3_3,STOCHASTIC_PERCENTD_14_3_3,AVERAGE_TRUE_RANGE_14,MOVING_AVERAGES_RATING", help="Comma-separated StockField names; supports interval as FIELD|60")
    p.add_argument("--filter", action="append", default=[], help="Filter expression, e.g. PRICE>100 or NAME=600519")
    p.add_argument("--limit", type=int, default=100)
    p.add_argument("--csv", default="")
    args = p.parse_args()

    try:
        market = getattr(Market, args.market)
    except AttributeError:
        print(f"Invalid market: {args.market}", file=sys.stderr)
        return 2

    ss = StockScreener()
    ss.set_markets(market)
    ss.set_range(0, args.limit)

    field_tokens = [x.strip() for x in args.fields.split(",") if x.strip()]
    selected = [parse_field(x) for x in field_tokens]
    ss.select(*selected)

    for f in args.filter:
        apply_filter(ss, f)

    df = ss.get()

    if args.symbol:
        [REDACTED_SECRET]":")[-1]
        row = df[df["Symbol"] == args.symbol]
        if row.empty and "Name" in df.columns:
            row = df[df["Name"].astype(str) == token]
        df = row

    if df.empty:
        print(json.dumps({"found": False}, ensure_ascii=False))
        return 1

    if args.csv:
        df.to_csv(args.csv, index=False)

    print(df.to_json(orient="records", force_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/discover_fields.py
content: #!/usr/bin/env python3
import argparse
import json

from tvscreener import StockField


def main() -> int:
    p = argparse.ArgumentParser(description="Discover tvscreener StockField constants")
    p.add_argument("--keyword", default="", help="keyword to search, e.g. rsi/macd/volume")
    p.add_argument("--limit", type=int, default=30)
    args = p.parse_args()

    if args.keyword:
        fields = StockField.search(args.keyword)
    else:
        fields = StockField.technicals()

    out = []
    for f in fields[: args.limit]:
        out.append({"name": f.name, "label": getattr(f, "label", "")})

    print(json.dumps(out, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/query_symbol.py
content: #!/usr/bin/env python3
import argparse
import json
import sys

from tvscreener import Market, StockField, StockScreener


def main() -> int:
    p = argparse.ArgumentParser(description="Query one stock symbol from tvscreener")
    p.add_argument("--symbol", default="HKEX:700", help="e.g. HKEX:700")
    p.add_argument("--market", default="HONGKONG", help="Market enum name, e.g. HONGKONG")
    p.add_argument("--csv", default="", help="Optional CSV output path")
    args = p.parse_args()

    try:
        market = getattr(Market, args.market)
    except AttributeError:
        print(f"Invalid market: {args.market}", file=sys.stderr)
        return 2

    ss = StockScreener()
    ss.set_markets(market)
    ss.set_range(0, 500)
    ss.select(
        StockField.NAME,
        StockField.PRICE,
        StockField.CHANGE_PERCENT,
        StockField.VOLUME,
        StockField.RELATIVE_STRENGTH_INDEX_14,
        StockField.MACD_LEVEL_12_26,
        StockField.MACD_SIGNAL_12_26,
        StockField.MACD_HIST,
        StockField.SIMPLE_MOVING_AVERAGE_20,
        StockField.SIMPLE_MOVING_AVERAGE_50,
        StockField.SIMPLE_MOVING_AVERAGE_200,
        StockField.EXPONENTIAL_MOVING_AVERAGE_20,
        StockField.EXPONENTIAL_MOVING_AVERAGE_50,
        StockField.EXPONENTIAL_MOVING_AVERAGE_200,
        StockField.BOLLINGER_UPPER_BAND_20,
        StockField.BOLLINGER_LOWER_BAND_20,
        StockField.STOCHASTIC_PERCENTK_14_3_3,
        StockField.STOCHASTIC_PERCENTD_14_3_3,
        StockField.AVERAGE_TRUE_RANGE_14,
        StockField.MOVING_AVERAGES_RATING,
        StockField.RECOMMENDATION_MARK,
    )

    # Server-side narrow down by ticker part to avoid missing symbols outside top-N page.
    [REDACTED_SECRET]":")[-1]
    ss.where(StockField.NAME == token)
    df = ss.get()

    row = df[df["Symbol"] == args.symbol]
    if row.empty:
        # Exchange prefix may differ (e.g. SHSE:600519 vs SSE:600519), fallback by token.
        row = df[df["Name"].astype(str) == token]

    if row.empty:
        print(json.dumps({"symbol": args.symbol, "found": False}, ensure_ascii=False))
        return 1

    payload = row.iloc[0].to_dict()
    print(json.dumps(payload, ensure_ascii=False, default=str, indent=2))

    if args.csv:
        row.to_csv(args.csv, index=False)

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/run_query.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PYTHON_BIN="${PYTHON_BIN:-python3}"

"$PYTHON_BIN" - <<'PY'
import sys
if sys.version_info < (3, 10):
    raise SystemExit("Python>=3.10 required. Please activate your conda workshop env, then rerun.")
PY

"$PYTHON_BIN" - <<'PY' >/dev/null 2>&1 || "$PYTHON_BIN" -m pip install -q -U tvscreener
from tvscreener import Market, StockField, StockScreener
PY

exec "$PYTHON_BIN" "$SCRIPT_DIR/custom_query.py" "$@"


path: scripts/test_markets.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PYTHON_BIN="${PYTHON_BIN:-python3}"

"$PYTHON_BIN" - <<'PY'
import sys
if sys.version_info < (3, 10):
    raise SystemExit("Python>=3.10 required. Please activate your conda workshop env, then rerun.")
PY

"$PYTHON_BIN" - <<'PY' >/dev/null 2>&1 || "$PYTHON_BIN" -m pip install -q -U tvscreener
from tvscreener import Market, StockField, StockScreener
PY

"$PYTHON_BIN" "$SCRIPT_DIR/query_symbol.py" --symbol HKEX:700 --market HONGKONG
"$PYTHON_BIN" "$SCRIPT_DIR/query_symbol.py" --symbol SHSE:600519 --market CHINA
"$PYTHON_BIN" "$SCRIPT_DIR/query_symbol.py" --symbol SHSE:510300 --market CHINA
"$PYTHON_BIN" "$SCRIPT_DIR/query_symbol.py" --symbol NASDAQ:BIDU --market AMERICA

# custom query path (caller-defined fields)
"$PYTHON_BIN" "$SCRIPT_DIR/custom_query.py" --market HONGKONG --symbol HKEX:700 --fields NAME,PRICE,RELATIVE_STRENGTH_INDEX_14,MACD_LEVEL_12_26 --filter "NAME=700" >/dev/null
"$PYTHON_BIN" "$SCRIPT_DIR/custom_query.py" --market CHINA --symbol SHSE:600519 --fields NAME,PRICE,CHANGE_PERCENT --filter "NAME=600519" >/dev/null
"$PYTHON_BIN" "$SCRIPT_DIR/custom_query.py" --market CHINA --symbol SHSE:510300 --fields NAME,PRICE,VOLUME --filter "NAME=510300" >/dev/null
"$PYTHON_BIN" "$SCRIPT_DIR/custom_query.py" --market AMERICA --symbol NASDAQ:BIDU --fields NAME,PRICE,CHANGE_PERCENT --filter "NAME=BIDU" >/dev/null

echo "PASS: HK/A-share/A-share ETF/US symbols all resolved (preset + custom fields)."


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)