1. 从零开始认识baostock数据平台
baostock作为一个开放的金融数据平台,为投资者和量化研究人员提供了丰富的证券市场数据接口。不同于其他商业数据平台,baostock的最大特点是完全免费且无需注册即可使用,这使其成为个人开发者和研究机构的理想选择。
我第一次接触baostock是在2019年做量化策略研究时,当时被它简洁的API设计和稳定的数据质量所吸引。经过三年多的实际使用,我可以负责任地说,对于A股市场的历史数据和基本面分析,baostock完全可以满足大多数个人投资者的需求。
baostock的数据覆盖范围包括:
- 股票行情数据(日线、周线、月线、5分钟线等)
- 财务数据(资产负债表、利润表、现金流量表)
- 估值指标数据(PE、PB、PS等)
- 宏观经济数据(GDP、CPI、利率等)
注意:虽然baostock数据免费,但服务器资源有限,建议合理控制请求频率,避免短时间内发起大量请求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 估值类数据获取全流程解析
2.1 环境准备与库安装
在开始获取估值数据前,需要确保Python环境已正确配置。我推荐使用Python 3.7及以上版本,这个版本区间与baostock的兼容性最好。
安装baostock库非常简单,只需执行:
bash复制pip install baostock
如果你像我一样喜欢使用conda管理环境,也可以:
bash复制conda install -c conda-forge baostock
我习惯在Jupyter Notebook中进行数据探索,因此还会安装:
bash复制pip install jupyter pandas matplotlib
2.2 登录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。我建议把这个登录过程封装成函数,方便在多个 notebook 中复用:
python复制def bs_login():
lg = bs.login()
if lg.error_code != '0':
raise ConnectionError(f"登录失败: {lg.error_msg}")
return True
2.3 理解估值数据接口参数
baostock提供了query_history_k_data_plus()接口来获取估值数据,其核心参数包括:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | str | 是 | 股票代码,格式如"sh.600000" |
| fields | str | 是 | 需要返回的字段,多个用逗号分隔 |
| start_date | str | 是 | 开始日期,格式"YYYY-MM-DD" |
| end_date | str | 是 | 结束日期,格式"YYYY-MM-DD" |
| frequency | str | 否 | 数据频率,d-日k线,w-周,m-月 |
| adjustflag | str | 否 | 复权类型,1-后复权,2-前复权,3-不复权 |
对于估值数据,我们最常查询的字段包括:
- peTTM:滚动市盈率
- pbMRQ:市净率(最新财报)
- psTTM:滚动市销率
- pcfNcfTTM:滚动市现率
3. 实战:获取多只股票的估值数据
3.1 单只股票估值获取示例
让我们以贵州茅台(sh.600519)为例,获取其2022年全年的估值数据:
python复制import pandas as pd
rs = bs.query_history_k_data_plus("sh.600519",
"date,code,peTTM,pbMRQ,psTTM,pcfNcfTTM",
start_date='2022-01-01',
end_date='2022-12-31',
frequency="d",
adjustflag="3")
data_list = []
while (rs.error_code == '0') & rs.next():
data_list.append(rs.get_row_data())
df = pd.DataFrame(data_list, columns=rs.fields)
df['date'] = pd.to_datetime(df['date'])
df[['peTTM','pbMRQ','psTTM','pcfNcfTTM']] = df[['peTTM','pbMRQ','psTTM','pcfNcfTTM']].apply(pd.to_numeric)
这段代码会返回一个包含日期和四种估值指标的DataFrame。我通常会添加一些数据处理步骤:
python复制# 计算估值指标的百分位
for col in ['peTTM','pbMRQ','psTTM','pcfNcfTTM']:
df[f'{col}_percentile'] = df[col].rank(pct=True)
# 设置日期为索引
df.set_index('date', inplace=True)
3.2 批量获取多只股票估值数据
实际分析中,我们往往需要比较多只股票的估值情况。下面是我常用的批量获取方法:
python复制def get_multiple_stocks_valuation(stock_list, start_date, end_date):
all_data = []
for code in stock_list:
rs = bs.query_history_k_data_plus(code,
"date,code,peTTM,pbMRQ,psTTM",
start_date=start_date,
end_date=end_date,
frequency="d",
adjustflag="3")
while (rs.error_code == '0') & rs.next():
all_data.append(rs.get_row_data())
df = pd.DataFrame(all_data, columns=['date','code','peTTM','pbMRQ','psTTM'])
return df
# 示例使用
stock_list = ['sh.600519', 'sz.000858', 'sh.601318']
df = get_multiple_stocks_valuation(stock_list, '2022-01-01', '2022-12-31')
提示:批量获取时建议在每次请求后添加time.sleep(1),避免触发baostock的调用限制。
3.3 估值数据可视化分析
获取数据后,可视化能帮助我们更直观地理解估值变化。这是我常用的估值分析图表代码:
python复制import matplotlib.pyplot as plt
plt.figure(figsize=(12, 8))
for code, group in df.groupby('code'):
plt.plot(pd.to_datetime(group['date']), group['peTTM'], label=code)
plt.title('PE Ratio Comparison')
plt.xlabel('Date')
plt.ylabel('PE(TTM)')
plt.legend()
plt.grid(True)
plt.show()
对于更专业的分析,可以使用seaborn绘制分面图:
python复制import seaborn as sns
g = sns.FacetGrid(df, col='code', col_wrap=2, height=4)
g.map(plt.plot, 'date', 'pbMRQ')
g.set_xticklabels(rotation=45)
plt.tight_layout()
4. 高级技巧与性能优化
4.1 处理baostock调用限制
baostock虽然没有严格的API调用次数限制,但当请求过于频繁时,可能会遇到连接问题。我的经验法则是:
- 单线程情况下,每秒不超过3次请求
- 批量获取时,每10次请求后暂停2秒
- 长时间运行脚本时,每小时重新登录一次
优化后的批量获取函数示例:
python复制import time
def safe_get_valuation(code, start_date, end_date, retry=3):
for i in range(retry):
try:
rs = bs.query_history_k_data_plus(code,
"date,code,peTTM,pbMRQ",
start_date=start_date,
end_date=end_date)
return rs
except Exception as e:
if i == retry - 1:
raise
time.sleep(2)
return None
4.2 数据缓存策略
为了避免重复请求相同数据,我通常会实现一个简单的缓存机制:
python复制from pathlib import Path
import pickle
CACHE_DIR = Path('baostock_cache')
def get_with_cache(code, start_date, end_date, force_update=False):
cache_file = CACHE_DIR / f"{code}_{start_date}_{end_date}.pkl"
if not force_update and cache_file.exists():
with open(cache_file, 'rb') as f:
return pickle.load(f)
data = get_valuation_data(code, start_date, end_date)
CACHE_DIR.mkdir(exist_ok=True)
with open(cache_file, 'wb') as f:
pickle.dump(data, f)
return data
4.3 异常数据处理
baostock返回的估值数据有时会有异常值(如PE为0或负数),需要特别处理:
python复制def clean_valuation_data(df):
# 处理缺失值
df = df.replace('', np.nan)
# 替换不合理值
for col in ['peTTM', 'pbMRQ', 'psTTM']:
df[col] = pd.to_numeric(df[col], errors='coerce')
df[col] = df[col].replace([0, -1], np.nan)
# 前向填充
df.fillna(method='ffill', inplace=True)
return df
5. 估值数据分析实战案例
5.1 行业估值对比分析
通过baostock获取整个行业的估值数据,可以帮我们发现投资机会。以下是分析银行业估值的示例:
python复制# 银行业股票列表
bank_stocks = ['sh.601398', 'sh.601288', 'sh.601988', 'sh.601328', 'sz.000001']
# 获取2022年数据
bank_data = get_multiple_stocks_valuation(bank_stocks, '2022-01-01', '2022-12-31')
# 计算每日行业平均PE
daily_pe = bank_data.groupby('date')['peTTM'].mean()
# 可视化
plt.figure(figsize=(10,6))
daily_pe.plot()
plt.title('Banking Industry Average PE (2022)')
plt.ylabel('PE Ratio')
plt.xlabel('Date')
plt.grid(True)
5.2 估值百分位分析
估值百分位是判断股票估值高低的重要指标:
python复制def analyze_valuation_percentile(code, start_date, end_date):
data = get_valuation_data(code, start_date, end_date)
data['pe_percentile'] = data['peTTM'].rank(pct=True)
# 找出PE处于历史低位的日期
low_pe_days = data[data['pe_percentile'] < 0.2]
return {
'code': code,
'avg_pe': data['peTTM'].mean(),
'low_pe_days_count': len(low_pe_days),
'recent_pe_percentile': data['pe_percentile'].iloc[-1]
}
5.3 估值与股价关系分析
理解估值指标与股价的实际关系非常重要:
python复制# 获取股价和PE数据
rs = bs.query_history_k_data_plus("sh.600519",
"date,close,peTTM",
start_date='2020-01-01',
end_date='2022-12-31')
data = pd.DataFrame([rs.get_row_data() for _ in range(rs.row_count)],
columns=['date','close','peTTM'])
# 计算相关性
correlation = data[['close','peTTM']].corr()
print(f"股价与PE的相关系数: {correlation.iloc[0,1]:.2f}")
# 散点图
plt.scatter(data['peTTM'], data['close'], alpha=0.5)
plt.xlabel('PE Ratio')
plt.ylabel('Stock Price')
plt.title('PE vs Price')
6. 常见问题与解决方案
6.1 数据获取失败处理
当遇到数据获取失败时,可以按照以下步骤排查:
- 检查网络连接是否正常
- 确认股票代码格式正确(如"sh.600000")
- 验证日期格式为"YYYY-MM-DD"
- 检查baostock服务是否正常(可以访问官网查看)
- 如果频繁失败,尝试减少单次请求的数据量
我通常会这样封装健壮的数据获取函数:
python复制def robust_query(api_func, *args, max_retry=3, **kwargs):
for i in range(max_retry):
try:
return api_func(*args, **kwargs)
except Exception as e:
if i == max_retry - 1:
raise
time.sleep(2 * (i + 1))
6.2 数据缺失处理
baostock的数据偶尔会有缺失,特别是在非交易日。处理方法包括:
- 前向填充:用前一天的数据填充
- 线性插值:适用于连续数值
- 删除缺失值:如果缺失不多
我的处理函数示例:
python复制def handle_missing_data(df, method='ffill'):
if method == 'ffill':
return df.fillna(method='ffill')
elif method == 'interpolate':
return df.interpolate()
elif method == 'drop':
return df.dropna()
else:
raise ValueError("Unknown method")
6.3 数据一致性验证
为确保数据准确性,我通常会交叉验证几个关键点:
- 检查极值:PE是否在合理范围内(通常0-100)
- 验证数据连续性:是否有异常的缺失日期
- 对比其他数据源:如新浪财经或东方财富的数据
验证函数示例:
python复制def validate_data(df):
# 检查PE范围
abnormal_pe = df[(df['peTTM'] < 0) | (df['peTTM'] > 200)]
if not abnormal_pe.empty:
print(f"发现异常PE值: {len(abnormal_pe)}条")
# 检查日期连续性
df['date'] = pd.to_datetime(df['date'])
date_diff = df['date'].diff().dt.days
gap_days = date_diff[date_diff > 1]
if not gap_days.empty:
print(f"发现日期间隔: {gap_days.tolist()}")
return len(abnormal_pe) == 0 and len(gap_days) == 0
7. 性能优化实战建议
7.1 并行获取数据
对于大量股票的数据获取,可以使用多线程加速:
python复制from concurrent.futures import ThreadPoolExecutor
def parallel_get_data(stock_list, start_date, end_date, max_workers=5):
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
for code in stock_list:
futures.append(executor.submit(
get_valuation_data, code, start_date, end_date))
results = []
for future in futures:
try:
results.append(future.result())
except Exception as e:
print(f"获取数据失败: {e}")
return pd.concat(results, ignore_index=True)
重要提示:并行请求时务必控制并发数量,建议不超过5个并发,避免给baostock服务器造成过大压力。
7.2 数据存储优化
对于长期积累的数据,合理的存储方式能显著提高后续分析效率:
- 使用Parquet格式存储,它比CSV更高效
- 按股票代码分目录存储
- 添加元数据记录获取时间等信息
存储函数示例:
python复制import pyarrow.parquet as pq
import os
def save_as_parquet(df, code, data_type='valuation'):
path = f"data/{data_type}/{code[:2]}" # 按市场分目录
os.makedirs(path, exist_ok=True)
file_path = f"{path}/{code}.parquet"
df.to_parquet(file_path)
print(f"数据已保存至: {file_path}")
7.3 增量更新策略
对于定期更新的分析,增量获取数据比全量更新更高效:
python复制def update_valuation_data(code, existing_end_date):
# 获取现有数据的最新日期
last_date = pd.to_datetime(existing_end_date)
# 只获取新数据
new_data = get_valuation_data(code,
(last_date + pd.Timedelta(days=1)).strftime('%Y-%m-%d'),
datetime.now().strftime('%Y-%m-%d'))
if not new_data.empty:
# 合并新旧数据
updated_data = pd.concat([existing_data, new_data])
return updated_data.drop_duplicates('date')
return existing_data
8. 扩展应用:构建估值监控系统
基于baostock的估值数据,我们可以构建一个简单的估值监控系统。
8.1 核心功能设计
- 每日自动获取重点股票估值数据
- 计算各项指标的百分位
- 对异常估值发出提醒
- 生成定期估值报告
8.2 关键实现代码
python复制class ValuationMonitor:
def __init__(self, watch_list):
self.watch_list = watch_list
self.historical_data = {}
def update_data(self):
for code in self.watch_list:
# 获取最近3年数据
new_data = get_valuation_data(code,
(datetime.now() - timedelta(days=3*365)).strftime('%Y-%m-%d'),
datetime.now().strftime('%Y-%m-%d'))
if code in self.historical_data:
self.historical_data[code] = pd.concat([self.historical_data[code], new_data])
else:
self.historical_data[code] = new_data
# 去重
self.historical_data[code] = self.historical_data[code].drop_duplicates('date')
def check_alert(self):
alerts = []
for code, data in self.historical_data.items():
latest = data.iloc[-1]
# 计算百分位
for col in ['peTTM', 'pbMRQ']:
percentile = (data[col] < latest[col]).mean()
if percentile < 0.1:
alerts.append(f"{code} {col}处于历史低位(百分位:{percentile:.1%})")
elif percentile > 0.9:
alerts.append(f"{code} {col}处于历史高位(百分位:{percentile:.1%})")
return alerts
8.3 定时任务设置
使用APScheduler设置每日自动运行:
python复制from apscheduler.schedulers.blocking import BlockingScheduler
def job():
monitor = ValuationMonitor(['sh.600519', 'sz.000858'])
monitor.update_data()
alerts = monitor.check_alert()
if alerts:
send_email_alerts(alerts)
scheduler = BlockingScheduler()
scheduler.add_job(job, 'cron', hour=18, minute=0) # 每天18:00运行
scheduler.start()
9. 与其他数据源对比
虽然baostock很好用,但了解其局限性也很重要。
9.1 数据质量对比
| 指标 | baostock | 商业数据源 | 交易所数据 |
|---|---|---|---|
| 数据准确性 | 高 | 极高 | 最高 |
| 更新频率 | 日级 | 实时/分钟级 | 实时 |
| 历史深度 | 10年+ | 20年+ | 完整历史 |
| 数据字段 | 适中 | 丰富 | 最全 |
9.2 适用场景建议
根据我的经验:
- 学术研究、个人学习:优先使用baostock
- 量化交易回测:baostock+部分商业数据校验
- 实盘交易系统:建议使用商业数据源
- 高频交易:必须使用专业数据服务
9.3 数据补充策略
当baostock数据不足时,我的常用补充方案:
- 使用tushare pro补充更细粒度数据
- 通过爬虫获取交易所公开数据
- 购买商业数据的必要部分
python复制def enhance_with_tushare(baostock_data, code):
import tushare as ts
pro = ts.pro_api('your_token')
ts_data = pro.daily(ts_code=code.replace('.',''),
start_date=baostock_data['date'].min(),
end_date=baostock_data['date'].max())
merged = pd.merge(baostock_data, ts_data,
left_on='date', right_on='trade_date')
return merged
10. 最佳实践总结
经过长期使用baostock获取估值数据的实践,我总结了以下经验:
-
数据获取方面:
- 每次会话前先登录,结束后及时退出
- 批量获取时添加合理延迟
- 对重要数据实现本地缓存
-
数据处理方面:
- 始终验证数据完整性和合理性
- 对异常值进行标记或修正
- 建立数据质量检查流程
-
分析应用方面:
- 结合多个估值指标综合判断
- 关注估值的历史百分位而非绝对值
- 不同行业采用不同的估值标准
-
系统设计方面:
- 将数据获取与业务逻辑分离
- 实现自动化更新机制
- 建立数据版本控制
最后分享一个我常用的估值分析模板:
python复制def full_valuation_analysis(code):
# 获取数据
data = get_valuation_data(code, '2010-01-01', datetime.now().strftime('%Y-%m-%d'))
# 数据处理
data = clean_valuation_data(data)
# 计算指标
metrics = {
'current_pe': data['peTTM'].iloc[-1],
'pe_percentile': data['peTTM'].rank(pct=True).iloc[-1],
'avg_pe_3y': data['peTTM'].rolling('1095D').mean().iloc[-1],
'pb_ratio': data['pbMRQ'].iloc[-1]
}
# 生成报告
report = f"""
{code}估值分析报告:
- 当前PE: {metrics['current_pe']:.2f}
- 历史百分位: {metrics['pe_percentile']:.1%}
- 三年平均PE: {metrics['avg_pe_3y']:.2f}
- 当前PB: {metrics['pb_ratio']:.2f}
"""
return report
