1. 认识baostock与估值数据
baostock是一个免费、开源的证券数据接口库,主要提供A股市场的历史行情和基本面数据。对于量化投资者、金融数据分析师来说,获取准确的估值数据是进行投资分析的基础工作。估值数据主要包括市盈率(PE)、市净率(PB)、市销率(PS)等指标,这些数据能够帮助我们判断股票的相对价值。
baostock相比其他商业数据接口有几个明显优势:首先是完全免费,不需要支付高昂的数据订阅费用;其次数据质量较好,经过了一定程度的清洗和校验;再者接口设计简单明了,Python调用非常方便。不过需要注意的是,baostock对调用频率有一定限制,单用户每分钟请求次数不宜过高,否则可能会被临时限制访问。
提示:虽然baostock是免费接口,但建议在程序中加入适当的延时,避免频繁请求导致IP被封禁。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与接口安装
2.1 Python环境配置
要使用baostock接口,首先需要确保你的Python环境已经准备就绪。推荐使用Python 3.6及以上版本,这个版本的稳定性和兼容性都比较好。可以通过以下命令检查Python版本:
bash复制python --version
如果还没有安装Python,可以从官网下载安装包。建议使用Anaconda来管理Python环境,这样可以避免各种依赖冲突问题。
2.2 安装baostock库
baostock库的安装非常简单,直接使用pip命令即可:
bash复制pip install baostock
安装完成后,可以通过以下代码测试是否安装成功:
python复制import baostock as bs
print(bs.__version__)
如果能够正常输出版本号,说明安装已经成功。目前最新稳定版本是0.8.8,建议使用这个版本进行开发。
2.3 登录baostock系统
在使用baostock接口前,需要先进行登录操作。登录过程会建立一个会话连接,后续的查询都在这个会话中进行。登录代码如下:
python复制import baostock as bs
# 登录系统
lg = bs.login()
# 显示登录返回信息
print('login respond error_code:'+lg.error_code)
print('login respond error_msg:'+lg.error_msg)
登录成功后,error_code会返回0,error_msg返回"success"。如果出现错误,可以根据错误信息进行排查。常见错误包括网络连接问题、服务器维护等。
3. 获取估值数据的具体方法
3.1 查询单只股票的估值数据
获取单只股票的估值数据是baostock最常用的功能之一。以查询贵州茅台(600519)的市盈率为例:
python复制# 查询贵州茅台市盈率
rs = bs.query_history_k_data("sh.600519",
"date,code,close,peTTM",
start_date='2023-01-01',
end_date='2023-12-31',
frequency="d", adjustflag="3")
# 打印结果集
data_list = []
while (rs.error_code == '0') & rs.next():
# 获取一条记录,将记录合并在一起
data_list.append(rs.get_row_data())
# 转换为DataFrame
import pandas as pd
result = pd.DataFrame(data_list, columns=rs.fields)
print(result.head())
这段代码会返回贵州茅台在2023年全年的每日收盘价和动态市盈率(peTTM)。其中关键参数说明:
- "sh.600519":股票代码,沪市股票前缀sh,深市前缀sz
- "date,code,close,peTTM":需要查询的字段,可以按需增减
- start_date/end_date:查询的时间范围
- frequency:数据频率,d表示日线,w周线,m月线
- adjustflag:复权类型,3表示后复权
3.2 批量获取多只股票估值数据
实际分析中,我们往往需要获取多只股票的估值数据进行对比。baostock提供了批量查询的接口:
python复制# 获取沪深300成分股列表
rs = bs.query_hs300_stocks()
hs300_stocks = []
while (rs.error_code == '0') & rs.next():
hs300_stocks.append(rs.get_row_data()[1]) # 获取股票代码
# 批量查询这些股票的估值数据
valuation_data = []
for stock in hs300_stocks[:10]: # 示例只查询前10只
rs = bs.query_history_k_data(stock,
"date,code,close,peTTM,pbMRQ",
start_date='2023-12-01',
end_date='2023-12-31')
while (rs.error_code == '0') & rs.next():
valuation_data.append(rs.get_row_data())
# 转换为DataFrame
result = pd.DataFrame(valuation_data, columns=["date","code","close","peTTM","pbMRQ"])
print(result.head())
这个例子先获取了沪深300成分股列表,然后批量查询这些股票在2023年12月的估值数据。实际使用时,可以根据需要调整股票列表和时间范围。
3.3 获取特定日期的全市场估值数据
有时候我们需要获取特定交易日全市场的估值数据分布情况,可以使用以下方法:
python复制# 查询2023-12-29全市场A股的估值数据
rs = bs.query_all_stock(day="2023-12-29")
all_stocks = []
while (rs.error_code == '0') & rs.next():
all_stocks.append(rs.get_row_data()[0]) # 获取股票代码
# 由于股票数量较多,这里只查询前100只作为示例
valuation_data = []
for stock in all_stocks[:100]:
try:
rs = bs.query_history_k_data(stock,
"date,code,close,peTTM,pbMRQ,psTTM",
start_date='2023-12-29',
end_date='2023-12-29')
if rs.error_code == '0' and rs.next():
valuation_data.append(rs.get_row_data())
except:
continue
# 转换为DataFrame并计算基本统计量
result = pd.DataFrame(valuation_data, columns=["date","code","close","peTTM","pbMRQ","psTTM"])
result[["peTTM","pbMRQ","psTTM"]] = result[["peTTM","pbMRQ","psTTM"]].apply(pd.to_numeric)
print("市盈率统计:")
print(result["peTTM"].describe())
print("\n市净率统计:")
print(result["pbMRQ"].describe())
这段代码先获取了指定交易日所有A股股票的代码,然后批量查询这些股票的估值数据,最后计算了市盈率、市净率等指标的基本统计量。实际应用中,可以根据需要调整查询的股票数量和估值指标。
4. 估值数据的处理与分析
4.1 数据清洗与异常值处理
从baostock获取的原始数据通常需要进行清洗才能用于分析。常见的清洗工作包括:
python复制# 转换数据类型
result[["close","peTTM","pbMRQ"]] = result[["close","peTTM","pbMRQ"]].apply(pd.to_numeric)
# 处理缺失值
result = result.dropna() # 删除包含缺失值的行
# 处理异常值 - 例如市盈率为负或极高的股票
result = result[(result["peTTM"] > 0) & (result["peTTM"] < 100)]
result = result[(result["pbMRQ"] > 0) & (result["pbMRQ"] < 20)]
# 添加行业分类信息(需要额外的行业数据)
# 这里假设我们已经有一个行业分类的DataFrame:industry_df
result = pd.merge(result, industry_df, on="code", how="left")
数据清洗后,我们可以进行更可靠的分析。例如计算各行业的平均估值水平:
python复制industry_valuation = result.groupby("industry")[["peTTM","pbMRQ"]].mean()
print(industry_valuation.sort_values("peTTM"))
4.2 估值数据的可视化分析
可视化是理解估值数据分布的有效方法。使用matplotlib和seaborn可以创建各种图表:
python复制import matplotlib.pyplot as plt
import seaborn as sns
# 设置绘图风格
sns.set(style="whitegrid")
# 市盈率分布直方图
plt.figure(figsize=(10,6))
sns.histplot(data=result, x="peTTM", bins=50, kde=True)
plt.title("A股市盈率分布")
plt.xlabel("市盈率(PE)")
plt.ylabel("股票数量")
plt.show()
# 市净率箱线图(按行业)
plt.figure(figsize=(12,6))
sns.boxplot(data=result, x="industry", y="pbMRQ")
plt.xticks(rotation=90)
plt.title("各行业市净率比较")
plt.show()
这些图表可以帮助我们直观地了解市场估值水平和行业差异。
4.3 构建简单的估值策略
基于估值数据,我们可以构建一些简单的投资策略。例如,选择低市盈率、低市净率的"双低"股票:
python复制# 计算市盈率和市净率的百分位
result["pe_rank"] = result["peTTM"].rank(pct=True)
result["pb_rank"] = result["pbMRQ"].rank(pct=True)
# 综合得分 = (市盈率百分位 + 市净率百分位)/2
result["value_score"] = (result["pe_rank"] + result["pb_rank"])/2
# 选择得分最低的20只股票
value_stocks = result.sort_values("value_score").head(20)
print(value_stocks[["code","close","peTTM","pbMRQ","value_score"]])
这只是一个简单的示例,实际策略需要考虑更多因素,如行业分布、财务质量等。
5. 常见问题与优化建议
5.1 处理baostock调用限制
baostock对API调用有一定限制,如果请求过于频繁,可能会遇到以下错误:
code复制error_code:10002002
error_msg:request too frequently, please try again later
为了避免这个问题,可以采取以下措施:
- 在循环查询中添加延时:
python复制import time
for stock in stock_list:
# 查询代码...
time.sleep(0.5) # 添加0.5秒延时
- 使用try-except捕获异常并重试:
python复制max_retries = 3
for stock in stock_list:
retry_count = 0
while retry_count < max_retries:
try:
# 查询代码...
break
except Exception as e:
print(f"查询{stock}出错:{str(e)}")
retry_count += 1
time.sleep(2) # 出错后等待更长时间
- 对于大批量查询,可以考虑将任务分散到不同时间段执行,或者使用多个账号轮询。
5.2 数据更新与维护
baostock的数据通常会在交易日结束后更新。为了确保使用最新数据,建议:
- 设置自动更新机制,每天收盘后定时运行数据更新脚本
- 在程序中添加数据更新时间检查:
python复制# 查询数据更新时间
rs = bs.query_trade_dates(start_date="2023-12-29")
while (rs.error_code == '0') & rs.next():
print(rs.get_row_data())
- 建立本地数据库存储历史数据,避免每次都从baostock查询全部历史
5.3 数据质量验证
虽然baostock数据质量总体不错,但仍建议进行交叉验证:
- 对比不同来源的同一指标,如将baostock的市盈率与其他数据源对比
- 检查极端值是否合理,如市盈率超过1000或为负值的情况
- 验证数据连续性,检查是否有异常缺失的交易日
可以编写简单的验证函数:
python复制def validate_data(df):
# 检查缺失值
if df.isnull().sum().sum() > 0:
print("警告:数据中存在缺失值")
# 检查极端值
high_pe = df[df["peTTM"] > 100]
if len(high_pe) > 0:
print(f"警告:有{len(high_pe)}条记录的市盈率超过100")
# 其他验证逻辑...
return True
5.4 性能优化建议
当需要处理大量股票或长时间序列数据时,可以考虑以下优化措施:
- 使用多线程/多进程并行查询:
python复制from concurrent.futures import ThreadPoolExecutor
def query_stock(stock):
# 查询单个股票的代码...
return data
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(query_stock, stock_list))
- 减少查询字段,只获取必要的指标
- 适当增大查询时间范围,减少查询次数
- 将常用数据缓存到本地数据库或文件中
6. 扩展应用与进阶技巧
6.1 结合财务数据综合分析
单纯的估值指标有时会失真,结合财务数据能获得更全面的分析:
python复制# 查询财务数据
rs = bs.query_profit_data(code="sh.600519", year=2023, quarter=4)
profit_data = []
while (rs.error_code == '0') & rs.next():
profit_data.append(rs.get_row_data())
# 查询成长性指标
rs = bs.query_growth_data(code="sh.600519", year=2023, quarter=4)
growth_data = []
while (rs.error_code == '0') & rs.next():
growth_data.append(rs.get_row_data())
# 将估值数据与财务数据合并分析
# ...
这种综合分析可以帮助识别"估值陷阱"——那些看起来便宜但实际上存在问题的股票。
6.2 构建估值时间序列分析
通过分析估值指标的历史变化,可以更好地理解当前估值水平:
python复制# 获取某股票5年的估值数据
rs = bs.query_history_k_data("sh.600519",
"date,peTTM,pbMRQ",
start_date='2019-01-01',
end_date='2023-12-31',
frequency="m") # 月度数据
# 转换为DataFrame并处理
valuation_ts = pd.DataFrame([rs.get_row_data() for _ in rs], columns=rs.fields)
valuation_ts[["peTTM","pbMRQ"]] = valuation_ts[["peTTM","pbMRQ"]].apply(pd.to_numeric)
valuation_ts["date"] = pd.to_datetime(valuation_ts["date"])
# 计算滚动百分位
valuation_ts["pe_percentile"] = valuation_ts["peTTM"].rolling(60).rank(pct=True)
valuation_ts["pb_percentile"] = valuation_ts["pbMRQ"].rolling(60).rank(pct=True)
# 绘制趋势图
plt.figure(figsize=(12,6))
plt.plot(valuation_ts["date"], valuation_ts["pe_percentile"], label="PE百分位")
plt.plot(valuation_ts["date"], valuation_ts["pb_percentile"], label="PB百分位")
plt.axhline(0.8, color="red", linestyle="--", label="高估阈值")
plt.axhline(0.2, color="green", linestyle="--", label="低估阈值")
plt.legend()
plt.title("贵州茅台估值历史百分位")
plt.show()
这种分析可以帮助判断当前估值在历史上的位置,为投资决策提供参考。
6.3 行业相对估值分析
不同行业的估值水平差异很大,直接比较绝对值可能没有意义。行业相对估值分析更有参考价值:
python复制# 获取各行业估值中位数
industry_median = result.groupby("industry")[["peTTM","pbMRQ"]].median().reset_index()
# 计算个股估值与行业中位数的比值
result = pd.merge(result, industry_median, on="industry", suffixes=("","_median"))
result["pe_relative"] = result["peTTM"] / result["peTTM_median"]
result["pb_relative"] = result["pbMRQ"] / result["pbMRQ_median"]
# 找出行业内相对低估的股票
undervalued_in_industry = result[
(result["pe_relative"] < 0.8) &
(result["pb_relative"] < 0.9)
].sort_values("pe_relative")
print(undervalued_in_industry[["code","industry","peTTM","peTTM_median","pe_relative"]].head(10))
这种方法可以帮我们在每个行业内寻找相对低估的机会,避免简单的跨行业比较。
