You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
"""
|
|
Stock Screener endpoints
|
|
|
|
Condition-based stock filtering via yfinance EquityQuery + screen().
|
|
Results are cached in Redis for 5 minutes (TTL=300).
|
|
"""
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Response
|
|
import logging
|
|
|
|
from app.services.screener_service import screener_service
|
|
from app.utils.cache import with_cache
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger("app.api.v1.screener")
|
|
|
|
|
|
@router.get("/stocks", summary="Screen stocks by financial criteria")
|
|
@with_cache(
|
|
namespace="screener:stocks",
|
|
ttl=300,
|
|
key_params=[
|
|
"market_cap_min", "market_cap_max", "exchange", "min_avg_volume",
|
|
"exclude_types", "sector", "pe_min", "pe_max", "price_min", "price_max",
|
|
"page", "page_size", "sort_by", "sort_ascending",
|
|
],
|
|
)
|
|
async def screen_stocks(
|
|
response: Response,
|
|
market_cap_min: Optional[float] = Query(
|
|
None, ge=0, description="Minimum market cap in USD (e.g. 500000000 for $500M)"
|
|
),
|
|
market_cap_max: Optional[float] = Query(
|
|
None, ge=0, description="Maximum market cap in USD (e.g. 10000000000 for $10B)"
|
|
),
|
|
exchange: Optional[str] = Query(
|
|
None,
|
|
description="Comma-separated exchange names: NYSE, NASDAQ, AMEX, NYSE_ARCA. "
|
|
"Omit for all US exchanges.",
|
|
),
|
|
min_avg_volume: Optional[int] = Query(
|
|
None, ge=0, description="Minimum 3-month average daily volume (e.g. 500000)"
|
|
),
|
|
exclude_types: Optional[str] = Query(
|
|
None,
|
|
description="Comma-separated quote types to exclude (e.g. ETF,FUND). "
|
|
"Only EQUITY results are kept when specified.",
|
|
),
|
|
sector: Optional[str] = Query(
|
|
None,
|
|
description="Filter by sector (e.g. Technology, Healthcare, 'Financial Services'). "
|
|
"Note: sector is not returned per-stock in the response.",
|
|
),
|
|
pe_min: Optional[float] = Query(None, ge=0, description="Minimum trailing P/E ratio"),
|
|
pe_max: Optional[float] = Query(None, ge=0, description="Maximum trailing P/E ratio"),
|
|
price_min: Optional[float] = Query(None, ge=0, description="Minimum stock price in USD"),
|
|
price_max: Optional[float] = Query(None, ge=0, description="Maximum stock price in USD"),
|
|
page: int = Query(1, ge=1, description="Page number (1-based)"),
|
|
page_size: int = Query(
|
|
100, ge=1, le=250, description="Results per page (max 250, Yahoo API limit)"
|
|
),
|
|
sort_by: str = Query(
|
|
"market_cap",
|
|
description="Sort field: market_cap, volume, avg_volume, price, pe_ratio, "
|
|
"change_percent, name, eps, dividend_yield, forward_pe, price_to_book",
|
|
),
|
|
sort_ascending: bool = Query(False, description="Sort ascending (default: descending)"),
|
|
force_refresh: bool = Query(False, description="Bypass cache and fetch fresh data"),
|
|
):
|
|
"""
|
|
Screen stocks based on financial criteria using yfinance.
|
|
|
|
Filters stocks from US exchanges (NYSE, NASDAQ, AMEX, NYSE_ARCA) by market cap,
|
|
volume, price, P/E ratio, sector, and more. Results are paginated and cached for
|
|
5 minutes.
|
|
|
|
**Exchange mapping**:
|
|
- `NYSE` → NYQ
|
|
- `NASDAQ` → NMS, NGM, NCM
|
|
- `AMEX` → ASE
|
|
- `NYSE_ARCA` → PCX
|
|
|
|
**Important limitations**:
|
|
- `page_size` maximum is 250 (Yahoo Finance API limit)
|
|
- `sector` filtering works but sector is NOT returned per-stock in the response
|
|
- Results reflect real-time Yahoo Finance data
|
|
|
|
**Example**:
|
|
```
|
|
GET /screener/stocks?market_cap_min=500000000&market_cap_max=10000000000
|
|
&exchange=NYSE,NASDAQ&min_avg_volume=500000&exclude_types=ETF,FUND
|
|
&sort_by=market_cap&page=1&page_size=100
|
|
```
|
|
"""
|
|
try:
|
|
result = await screener_service.screen_stocks(
|
|
market_cap_min=market_cap_min,
|
|
market_cap_max=market_cap_max,
|
|
exchange=exchange,
|
|
min_avg_volume=min_avg_volume,
|
|
exclude_types=exclude_types,
|
|
sector=sector,
|
|
pe_min=pe_min,
|
|
pe_max=pe_max,
|
|
price_min=price_min,
|
|
price_max=price_max,
|
|
page=page,
|
|
page_size=page_size,
|
|
sort_by=sort_by,
|
|
sort_ascending=sort_ascending,
|
|
)
|
|
logger.info(
|
|
"Screener returned %d stocks (total_available=%s, page=%d)",
|
|
result["returned_count"],
|
|
result["total_available"],
|
|
result["page"],
|
|
)
|
|
return result
|
|
|
|
except RuntimeError as e:
|
|
raise HTTPException(status_code=503, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Screener error: %s", e, exc_info=True)
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Screener query failed: {str(e)}",
|
|
)
|
|
|
|
|
|
@router.get("/fields", summary="Available screener filter options and valid values")
|
|
async def get_screener_fields():
|
|
"""
|
|
Return metadata about available screener filter options.
|
|
|
|
Useful for building dynamic filter UIs — lists all valid exchange names,
|
|
sectors, sort fields, and parameter descriptions.
|
|
"""
|
|
return {
|
|
"exchanges": {
|
|
"values": ["NYSE", "NASDAQ", "AMEX", "NYSE_ARCA"],
|
|
"description": "US stock exchange names (comma-separate multiple values)",
|
|
"yfinance_codes": {
|
|
"NYSE": ["NYQ"],
|
|
"NASDAQ": ["NMS", "NGM", "NCM"],
|
|
"AMEX": ["ASE"],
|
|
"NYSE_ARCA": ["PCX"],
|
|
},
|
|
},
|
|
"sectors": {
|
|
"values": [
|
|
"Technology",
|
|
"Healthcare",
|
|
"Financial Services",
|
|
"Consumer Cyclical",
|
|
"Industrials",
|
|
"Consumer Defensive",
|
|
"Energy",
|
|
"Basic Materials",
|
|
"Real Estate",
|
|
"Utilities",
|
|
"Communication Services",
|
|
],
|
|
"description": "Yahoo Finance sector names (exact match required). "
|
|
"Note: sector is NOT returned per-stock in responses.",
|
|
},
|
|
"sort_fields": {
|
|
"values": [
|
|
"market_cap",
|
|
"volume",
|
|
"avg_volume",
|
|
"price",
|
|
"pe_ratio",
|
|
"change_percent",
|
|
"name",
|
|
"eps",
|
|
"dividend_yield",
|
|
"forward_pe",
|
|
"price_to_book",
|
|
],
|
|
"default": "market_cap",
|
|
"description": "Fields available for sorting results",
|
|
},
|
|
"filters": {
|
|
"market_cap_min": "Minimum market cap in USD",
|
|
"market_cap_max": "Maximum market cap in USD",
|
|
"exchange": "Comma-separated exchange names",
|
|
"min_avg_volume": "Minimum 3-month average daily volume",
|
|
"exclude_types": "Quote types to exclude (e.g. ETF,FUND)",
|
|
"sector": "Yahoo Finance sector name (exact match)",
|
|
"pe_min": "Minimum trailing P/E ratio",
|
|
"pe_max": "Maximum trailing P/E ratio",
|
|
"price_min": "Minimum stock price in USD",
|
|
"price_max": "Maximum stock price in USD",
|
|
},
|
|
"pagination": {
|
|
"page": "Page number, 1-based (default: 1)",
|
|
"page_size": "Results per page, max 250 (default: 100)",
|
|
},
|
|
"limitations": [
|
|
"page_size maximum is 250 (Yahoo Finance API limit)",
|
|
"sector filter works but sector field is not returned per-stock",
|
|
"Results reflect real-time Yahoo Finance data with 5-minute Redis cache",
|
|
],
|
|
}
|