Fundamental Factor Screening
Purpose
Filter stocks using fundamental financial data (PE/PB/ROE, etc.) to build value or growth screen signals for backtesting. Supports multiple markets with different data sources.
Market Support
| Market |
Data Source |
Method |
Supported Metrics |
| A-shares |
tushare daily_basic |
extra_fields in config.json |
pe, pb, pe_ttm, ps_ttm, dv_ttm, total_mv, circ_mv, roe |
| US stocks |
yfinance Ticker.info |
Direct API call |
trailingPE, forwardPE, priceToBook, returnOnEquity, marketCap, dividendYield |
| HK stocks |
yfinance Ticker.info |
Direct API call |
trailingPE, priceToBook, returnOnEquity, marketCap |
Signal Logic
Value Filter (Default)
- PE < pe_max AND PE > 0 (exclude loss-making stocks)
- PB < pb_max
- ROE > roe_min
- All conditions met → long (1), otherwise → flat (0)
Growth Filter (Optional)
- PE_TTM within reasonable range (0 < PE_TTM < pe_ttm_max)
- ROE > roe_min (profitability floor)
- Market cap > mv_min (exclude micro-caps)
A-Share Usage (tushare)
config.json
{
"source": "tushare",
"codes": ["000001.SZ", "600036.SH", "000858.SZ"],
"start_date": "2023-01-01",
"end_date": "2024-12-31",
"extra_fields": ["pe", "pb", "pe_ttm", "roe", "total_mv"],
"initial_cash": 1000000,
"commission": 0.001
}
The extra_fields columns are automatically merged into the daily DataFrame by the DataLoader.
HK/US Stock Usage (yfinance)
For HK/US stocks, fundamental data is not available as daily time-series via the backtest loader. Instead, use yfinance Ticker info for point-in-time screening:
import yfinance as yf
def screen_us_stocks(tickers, criteria):
"""Screen US/HK stocks by fundamental criteria."""
passed = []
for symbol in tickers:
info = yf.Ticker(symbol).info
pe = info.get("trailingPE")
pb = info.get("priceToBook")
roe = info.get("returnOnEquity") # Decimal (e.g., 0.25 = 25%)
mcap = info.get("marketCap")
if pe is None or pb is None or roe is None:
continue # Skip stocks with missing data
if (0 < pe < criteria["pe_max"]
and pb < criteria["pb_max"]
and roe > criteria["roe_min"]
and (mcap or 0) > criteria.get("mcap_min", 0)):
passed.append({
"symbol": symbol,
"pe": pe,
"pb": pb,
"roe": round(roe * 100, 1), # Convert to percentage
"mcap": mcap,
})
return passed
# Example: screen S&P 500 components
criteria = {"pe_max": 20, "pb_max": 3.0, "roe_min": 0.08, "mcap_min": 10_000_000_000}
results = screen_us_stocks(["AAPL", "MSFT", "JNJ", "JPM", "XOM"], criteria)
HK Stock Screening
# HK stocks use the same yfinance interface
hk_tickers = ["0700.HK", "9988.HK", "1810.HK", "2318.HK", "0005.HK"]
results = screen_us_stocks(hk_tickers, criteria) # Same function works
Parameters
| Parameter |
Default |
Description |
| pe_max |
20.0 |
PE ceiling (exclude overvalued) |
| pb_max |
3.0 |
PB ceiling |
| roe_min |
8.0 |
ROE floor (%), exclude low-profitability |
| pe_min |
0.0 |
PE floor (exclude loss-making stocks) |
| mcap_min |
0 |
Market cap floor (for US/HK, in USD) |
Common Pitfalls
extra_fields columns may contain NaN (new listings, ST stocks) — must fillna or dropna
- Negative PE means loss-making — always filter with
pe > 0
- ROE units differ: tushare uses percentage (e.g., 15 = 15%), yfinance uses decimal (e.g., 0.15 = 15%)
- For portfolio strategies: N stocks passing the screen each get weight 1/N
- yfinance
Ticker.info is a point-in-time snapshot, not historical time-series — cannot directly use for daily rebalancing backtests on US/HK stocks
- For US/HK daily fundamental backtests, consider using the screening results as a stock universe, then applying technical signals within that universe
Dependencies
pip install pandas numpy yfinance
Signal Convention
1/N = selected for long (N = number of stocks passing the screen), 0 = not selected
1---2name: fundamental-filter3description: Fundamental factor screening — filter stocks by PE/PB/ROE and other financial metrics for value or growth selection. Supports A-shares (via tushare extra_fields) and HK/US stocks (via yfinance Ticker info).4---5# Fundamental Factor Screening67## Purpose89Filter stocks using fundamental financial data (PE/PB/ROE, etc.) to build value or growth screen signals for backtesting. Supports multiple markets with different data sources.1011## Market Support1213| Market | Data Source | Method | Supported Metrics |14|--------|-----------|--------|------------------|15| A-shares | tushare `daily_basic` | `extra_fields` in config.json | pe, pb, pe_ttm, ps_ttm, dv_ttm, total_mv, circ_mv, roe |16| US stocks | yfinance `Ticker.info` | Direct API call | trailingPE, forwardPE, priceToBook, returnOnEquity, marketCap, dividendYield |17| HK stocks | yfinance `Ticker.info` | Direct API call | trailingPE, priceToBook, returnOnEquity, marketCap |1819## Signal Logic2021### Value Filter (Default)22231. PE < pe_max AND PE > 0 (exclude loss-making stocks)242. PB < pb_max253. ROE > roe_min264. All conditions met → long (1), otherwise → flat (0)2728### Growth Filter (Optional)29301. PE_TTM within reasonable range (0 < PE_TTM < pe_ttm_max)312. ROE > roe_min (profitability floor)323. Market cap > mv_min (exclude micro-caps)3334## A-Share Usage (tushare)3536### config.json3738```json39{40 "source": "tushare",41 "codes": ["000001.SZ", "600036.SH", "000858.SZ"],42 "start_date": "2023-01-01",43 "end_date": "2024-12-31",44 "extra_fields": ["pe", "pb", "pe_ttm", "roe", "total_mv"],45 "initial_cash": 1000000,46 "commission": 0.00147}48```4950The `extra_fields` columns are automatically merged into the daily DataFrame by the DataLoader.5152## HK/US Stock Usage (yfinance)5354For HK/US stocks, fundamental data is not available as daily time-series via the backtest loader. Instead, use `yfinance` Ticker info for point-in-time screening:5556```python57import yfinance as yf5859def screen_us_stocks(tickers, criteria):60 """Screen US/HK stocks by fundamental criteria."""61 passed = []62 for symbol in tickers:63 info = yf.Ticker(symbol).info64 pe = info.get("trailingPE")65 pb = info.get("priceToBook")66 roe = info.get("returnOnEquity") # Decimal (e.g., 0.25 = 25%)67 mcap = info.get("marketCap")6869 if pe is None or pb is None or roe is None:70 continue # Skip stocks with missing data7172 if (0 < pe < criteria["pe_max"]73 and pb < criteria["pb_max"]74 and roe > criteria["roe_min"]75 and (mcap or 0) > criteria.get("mcap_min", 0)):76 passed.append({77 "symbol": symbol,78 "pe": pe,79 "pb": pb,80 "roe": round(roe * 100, 1), # Convert to percentage81 "mcap": mcap,82 })8384 return passed8586# Example: screen S&P 500 components87criteria = {"pe_max": 20, "pb_max": 3.0, "roe_min": 0.08, "mcap_min": 10_000_000_000}88results = screen_us_stocks(["AAPL", "MSFT", "JNJ", "JPM", "XOM"], criteria)89```9091### HK Stock Screening9293```python94# HK stocks use the same yfinance interface95hk_tickers = ["0700.HK", "9988.HK", "1810.HK", "2318.HK", "0005.HK"]96results = screen_us_stocks(hk_tickers, criteria) # Same function works97```9899## Parameters100101| Parameter | Default | Description |102|-----------|---------|-------------|103| pe_max | 20.0 | PE ceiling (exclude overvalued) |104| pb_max | 3.0 | PB ceiling |105| roe_min | 8.0 | ROE floor (%), exclude low-profitability |106| pe_min | 0.0 | PE floor (exclude loss-making stocks) |107| mcap_min | 0 | Market cap floor (for US/HK, in USD) |108109## Common Pitfalls110111- `extra_fields` columns may contain NaN (new listings, ST stocks) — must `fillna` or `dropna`112- Negative PE means loss-making — always filter with `pe > 0`113- ROE units differ: tushare uses percentage (e.g., 15 = 15%), yfinance uses decimal (e.g., 0.15 = 15%)114- For portfolio strategies: N stocks passing the screen each get weight 1/N115- yfinance `Ticker.info` is a point-in-time snapshot, not historical time-series — cannot directly use for daily rebalancing backtests on US/HK stocks116- For US/HK daily fundamental backtests, consider using the screening results as a stock universe, then applying technical signals within that universe117118## Dependencies119120```bash121pip install pandas numpy yfinance122```123124## Signal Convention125126- `1/N` = selected for long (N = number of stocks passing the screen), `0` = not selected