Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Value investing analysis tool for Chinese A-share stocks with screening, financial analysis, industry comparison, and DCF valuation.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/stock_screener.py
1#!/usr/bin/env python32"""3A股股票筛选器4根据多种财务指标筛选符合条件的股票56依赖: pip install akshare pandas numpy7"""89import argparse10import json11import sys12import time13from datetime import datetime14from typing import List, Dict15from functools import wraps1617try:18import akshare as ak19import pandas as pd20import numpy as np21except ImportError:22print("错误: 请先安装依赖库")23print("pip install akshare pandas numpy")24sys.exit(1)252627def retry_on_failure(max_retries: int = 3, delay: float = 1.0):28"""网络请求重试装饰器"""29def decorator(func):30@wraps(func)31def wrapper(*args, **kwargs):32last_error = None33for attempt in range(max_retries):34try:35return func(*args, **kwargs)36except Exception as e:37last_error = e38if attempt < max_retries - 1:39print(f" 重试 ({attempt + 1}/{max_retries})...")40time.sleep(delay * (attempt + 1))41raise last_error42return wrapper43return decorator444546INDEX_CODE_MAP = {47"hs300": "000300",48"zz500": "000905",49"zz1000": "000852",50"cyb": "399006",51"kcb": "000688"52}535455class StockScreener:56"""股票筛选器"""5758def __init__(self):59self.all_stocks_data = None6061def load_stock_data(self, scope: str = "hs300", custom_codes: List[str] = None) -> pd.DataFrame:62"""加载股票数据"""63print(f"正在加载股票数据 (范围: {scope})...")6465try:66if scope == "all":67df = ak.stock_zh_a_spot_em()68elif scope in INDEX_CODE_MAP:69df = self._get_index_stocks_data(INDEX_CODE_MAP[scope])70elif scope.startswith("custom:") or custom_codes:71codes = custom_codes or scope.replace("custom:", "").split(",")72df = self._get_custom_stocks_data(codes)73else:74df = ak.stock_zh_a_spot_em()7576self.all_stocks_data = df77print(f"已加载 {len(df)} 只股票数据")78return df7980except Exception as e:81print(f"加载数据失败: {e}")82return pd.DataFrame()8384@retry_on_failure(max_retries=3, delay=2.0)85def _get_all_stocks_realtime(self) -> pd.DataFrame:86"""获取全部A股实时数据(带重试)"""87return ak.stock_zh_a_spot_em()8889@retry_on_failure(max_retries=3, delay=2.0)90def _get_index_constituents(self, index_code: str) -> list:91"""获取指数成分股列表(带重试)"""92df = ak.index_stock_cons(symbol=index_code)93return df['品种代码'].tolist()9495def _get_index_stocks_data(self, index_code: str) -> pd.DataFrame:96"""获取指数成分股数据"""97try:98# 获取成分股列表99print(f" 获取指数 {index_code} 成分股...")100codes = self._get_index_constituents(index_code)101print(f" 成分股数量: {len(codes)}")102103# 获取实时数据104print(" 获取实时行情...")105all_stocks = self._get_all_stocks_realtime()106df = all_stocks[all_stocks['代码'].isin(codes)]107return df108except Exception as e:109print(f"获取指数成分股失败: {e}")110return pd.DataFrame()111112def _get_custom_stocks_data(self, codes: List[str]) -> pd.DataFrame:113"""获取自定义股票列表数据"""114try:115all_stocks = self._get_all_stocks_realtime()116df = all_stocks[all_stocks['代码'].isin(codes)]117return df118except Exception as e:119print(f"获取自定义股票数据失败: {e}")120return pd.DataFrame()121122def _apply_numeric_filter(self, df: pd.DataFrame, column: str,123min_val: float = None, max_val: float = None) -> pd.DataFrame:124"""应用数值筛选条件"""125if column not in df.columns:126return df127128numeric_col = pd.to_numeric(df[column], errors='coerce')129if min_val is not None:130df = df[numeric_col >= min_val]131if max_val is not None:132df = df[numeric_col <= max_val]133return df134135def _find_column(self, df: pd.DataFrame, candidates: List[str]) -> str:136"""从候选列名中找到存在的列"""137for col in candidates:138if col in df.columns:139return col140return None141142def apply_filters(self, df: pd.DataFrame, filters: Dict) -> pd.DataFrame:143"""应用筛选条件"""144filtered = df.copy()145146# PE筛选147filtered = self._apply_numeric_filter(148filtered, '市盈率-动态',149min_val=filters.get('pe_min'),150max_val=filters.get('pe_max')151)152153# PB筛选154filtered = self._apply_numeric_filter(155filtered, '市净率',156min_val=filters.get('pb_min'),157max_val=filters.get('pb_max')158)159160# ROE筛选161if filters.get('roe_min') is not None:162roe_col = self._find_column(filtered, ['净资产收益率', 'ROE', '加权净资产收益率'])163if roe_col:164filtered = self._apply_numeric_filter(filtered, roe_col, min_val=filters['roe_min'])165166# 资产负债率筛选167filtered = self._apply_numeric_filter(168filtered, '资产负债率',169max_val=filters.get('debt_ratio_max')170)171172# 总市值筛选(转换为亿)173if '总市值' in filtered.columns:174if filters.get('market_cap_min') is not None or filters.get('market_cap_max') is not None:175filtered['总市值_亿'] = pd.to_numeric(filtered['总市值'], errors='coerce') / 1e8176filtered = self._apply_numeric_filter(177filtered, '总市值_亿',178min_val=filters.get('market_cap_min'),179max_val=filters.get('market_cap_max')180)181182return filtered183184def _get_numeric_value(self, row: pd.Series, column: str) -> float:185"""从行中获取数值,无效返回 NaN"""186return pd.to_numeric(row.get(column, np.nan), errors='coerce')187188def calculate_score(self, row: pd.Series) -> float:189"""计算综合评分 (0-100)"""190score = 50191192try:193# PE评分 (越低越好, 负数除外)194pe = self._get_numeric_value(row, '市盈率-动态')195if not np.isnan(pe) and pe > 0:196if pe < 10:197score += 15198elif pe < 15:199score += 10200elif pe < 20:201score += 5202elif pe > 50:203score -= 10204205# PB评分206pb = self._get_numeric_value(row, '市净率')207if not np.isnan(pb) and pb > 0:208if 0.5 < pb < 1.5:209score += 10210elif 1.5 <= pb < 3:211score += 5212elif pb > 5:213score -= 5214215# ROE评分216roe_col = self._find_column(row.index.to_frame(), ['净资产收益率', 'ROE', '加权净资产收益率'])217if roe_col:218roe = self._get_numeric_value(row, roe_col)219if not np.isnan(roe):220if roe > 20:221score += 15222elif roe > 15:223score += 10224elif roe > 10:225score += 5226elif roe < 5:227score -= 5228229# 涨跌幅评分 (下跌可能是机会)230change = self._get_numeric_value(row, '涨跌幅')231if not np.isnan(change):232if -5 < change < 0:233score += 3234elif change < -5:235score += 5236237except Exception:238pass239240return max(0, min(100, score))241242def screen(self, scope: str = "hs300", filters: Dict = None,243sort_by: str = "score", top_n: int = None) -> List[Dict]:244"""执行筛选"""245# 加载数据246if scope.startswith("custom:"):247codes = scope.replace("custom:", "").split(",")248df = self.load_stock_data(scope="custom", custom_codes=codes)249else:250df = self.load_stock_data(scope=scope)251252if df.empty:253return []254255# 应用筛选条件256if filters:257df = self.apply_filters(df, filters)258259if df.empty:260return []261262# 计算评分263df['评分'] = df.apply(self.calculate_score, axis=1)264265# 排序266if sort_by == "score":267df = df.sort_values('评分', ascending=False)268elif sort_by == "pe":269pe_col = '市盈率-动态' if '市盈率-动态' in df.columns else None270if pe_col:271df = df.sort_values(pe_col, ascending=True)272elif sort_by == "pb":273if '市净率' in df.columns:274df = df.sort_values('市净率', ascending=True)275elif sort_by == "market_cap":276if '总市值' in df.columns:277df = df.sort_values('总市值', ascending=False)278279# 限制数量280if top_n:281df = df.head(top_n)282283# 转换为结果列表284results = []285for _, row in df.iterrows():286result = {287"代码": row.get('代码', ''),288"名称": row.get('名称', ''),289"最新价": row.get('最新价', ''),290"涨跌幅": row.get('涨跌幅', ''),291"市盈率": row.get('市盈率-动态', ''),292"市净率": row.get('市净率', ''),293"总市值(亿)": round(float(row.get('总市值', 0)) / 100000000, 2) if row.get('总市值') else '',294"评分": row.get('评分', 50)295}296results.append(result)297298return results299300301def main():302parser = argparse.ArgumentParser(description="A股股票筛选器")303parser.add_argument("--scope", type=str, default="hs300",304help="筛选范围: all/hs300/zz500/zz1000/cyb/kcb/custom:代码1,代码2")305parser.add_argument("--pe-max", type=float, help="最大PE")306parser.add_argument("--pe-min", type=float, help="最小PE")307parser.add_argument("--pb-max", type=float, help="最大PB")308parser.add_argument("--pb-min", type=float, help="最小PB")309parser.add_argument("--roe-min", type=float, help="最小ROE (%)")310parser.add_argument("--debt-ratio-max", type=float, help="最大资产负债率 (%)")311parser.add_argument("--dividend-min", type=float, help="最小股息率 (%)")312parser.add_argument("--market-cap-min", type=float, help="最小市值 (亿)")313parser.add_argument("--market-cap-max", type=float, help="最大市值 (亿)")314parser.add_argument("--sort-by", type=str, default="score",315choices=["score", "pe", "pb", "market_cap"],316help="排序方式")317parser.add_argument("--top", type=int, default=50, help="返回前N只股票")318parser.add_argument("--output", type=str, help="输出文件路径 (JSON)")319320args = parser.parse_args()321322# 构建筛选条件323filter_keys = [324'pe_max', 'pe_min', 'pb_max', 'pb_min', 'roe_min',325'debt_ratio_max', 'dividend_min', 'market_cap_min', 'market_cap_max'326]327filters = {328k: getattr(args, k.replace('-', '_'))329for k in filter_keys330if getattr(args, k.replace('-', '_')) is not None331}332333# 执行筛选334screener = StockScreener()335results = screener.screen(336scope=args.scope,337filters=filters if filters else None,338sort_by=args.sort_by,339top_n=args.top340)341342# 输出结果343output = {344"screen_time": datetime.now().isoformat(),345"scope": args.scope,346"filters": filters,347"count": len(results),348"results": results349}350351output_json = json.dumps(output, ensure_ascii=False, indent=2, default=str)352353if args.output:354with open(args.output, 'w', encoding='utf-8') as f:355f.write(output_json)356print(f"筛选结果已保存到: {args.output}")357print(f"共筛选出 {len(results)} 只股票")358else:359print(output_json)360361362if __name__ == "__main__":363main()364