A股行情与财务数据读取的终极整合技能。融合Baostock/Akshare双数据源的全部最佳实践、 已知踩坑经验、单位转换、代码格式处理、缓存策略、错误处理与降级策略、研发费用查询、 公司注册属地查询等核心场景。一站式解决A股数据读取的所有问题。 本技能将分散在多个文档中的经验融合为一份自包含的完整指南。
---
name: astock-unified-financial
version: 2.0
description: >
A股行情与财务数据读取的终极整合技能。融合Baostock/Akshare双数据源的全部最佳实践、
已知踩坑经验、单位转换、代码格式处理、缓存策略、错误处理与降级策略、研发费用查询、
公司注册属地查询等核心场景。一站式解决A股数据读取的所有问题。
本技能将分散在多个文档中的经验融合为一份自包含的完整指南。
agent_created: true
tags: [akshare, baostock, A股, 财务数据, 行情数据, 研发费用, 单位转换, 踩坑经验, 降级策略]
---
# A股金融数据整合技能(终极版)
> **融合自**: astock-financial-data-guide / akshare-financial-data-traps / A股行情与财务数据读取经验手册
> **最后融合日期**: 2026-05-18
---
## 目录
1. [数据源总览与选择策略](#1-数据源总览与选择策略)
2. [推荐数据源组合 & 降级链](#2-推荐数据源组合--降级链)
3. [登录模式对照](#3-登录模式对照)
4. [Baostock 接口详解](#4-baostock-接口详解)
5. [Akshare 接口详解](#5-akshare-接口详解)
6. [废弃/不可用接口一览](#6-废弃不可用接口一览)
7. [研发费用查询专项指南](#7-研发费用查询专项指南)
8. [公司注册属地查询](#8-公司注册属地查询)
9. [股票代码格式处理](#9-股票代码格式处理)
10. [核心工具函数](#10-核心工具函数)
11. [单位转换速查](#11-单位转换速查)
12. [缓存策略](#12-缓存策略)
13. [错误处理与重试](#13-错误处理与重试)
14. [数据源降级策略](#14-数据源降级策略)
15. [最佳实践模板](#15-最佳实践模板)
16. [完整踩坑记录](#16-完整踩坑记录)
17. [附录:各接口列名速查](#17-附录各接口列名速查)
---
## 1. 数据源总览与选择策略
### 1.1 数据源对比
| 数据源 | 类型 | 行情 | 财报 | 分红 | 行业 | 优势 | 劣势 | 是否需要API Key |
|--------|------|------|------|------|------|------|------|----------------|
| **Baostock** | 证券宝 | ✅日/60分K线 | ✅五大季报 | ✅ | ✅ | 稳定、免费、无需Key、数据规范 | 字段单位需确认、登录/登出模式 | ❌ |
| **Akshare-同花顺** | Akshare | ❌ | ✅摘要/现金流/利润表 | ✅巨潮 | ❌ | 数据最全、带单位标注 | 列名可能随版本变 | ❌ |
| **Akshare-新浪** | Akshare | ❌ | ✅财务指标/利润表 | ❌ | ❌ | 覆盖面广、研发费数据完整 | 单位不确定(元/万元)、列名含(%)需判断 | ❌ |
| **Akshare-东方财富** | Akshare | ✅历史行情 | ✅个股信息 | ❌ | ✅ | 行情+基本面一体 | **服务器无法访问**⚠️ | ❌ |
| **Tushare** | 第三方 | ✅ | ✅ | ✅ | ✅ | 数据全面、质量高 | **需积分+API Key** | ✅ |
| **通达信** | 本地 | ✅ | ❌ | ❌ | ❌ | 速度极快(本地读取) | 仅行情、需安装通达信 | ❌ |
### 1.2 数据源能力速查矩阵
| 功能场景 | 推荐数据源(优先级) | 备注 |
|---------|-------------------|------|
| K线行情 | Baostock(主) → 东方财富(备) → 通达信(备) | Baostock稳定免费 |
| 财务摘要 | 同花顺财务摘要(主) → 新浪财务指标(备) | 同花顺带单位标注 |
| 利润表(含研发费) | 同花顺利润表(主) → 新浪利润表(备) | 同花顺带单位,新浪纯数字(元) |
| 研发费用 | 同花顺利润表(主) → 新浪利润表(备) → 四级降级策略 | ⚠️见第7章 |
| 分红数据 | 巨潮分红(主) → Baostock(备) | 巨潮"每10股",Baostock"元/股" |
| 行业分类 | Baostock(主) → 东方财富(备,可能不可达) | Baostock含industry和province |
| 公司注册属地 | 巨潮公司概况(主) | `stock_profile_cninfo` |
| 实时行情 | 东方财富全市场实时行情(备) | 新浪实时行情替代方案 |
| 技术指标计算 | Baostock K线 + Numba加速 | 数据量大时用磁盘缓存 |
### 1.3 踩坑优先级通道
```
Level 1: 同花顺数据源(首选) → parse_amount/parse_percent
Level 2: 新浪数据源(备用) → safe_float + ÷1亿调单位
Level 3: Baostock(兜底) → safe_float + convert_share
Level 4: 东方财富(降级) → ⚠️当前服务器不可达
```
---
## 2. 推荐数据源组合 & 降级链
### 2.1 主力推荐组合
```
主力: Akshare(同花顺/新浪) + Baostock(补充)
行情: Baostock K线 (稳定可靠)
财报: Akshare同花顺(主) → 新浪(备) → Baostock(兜底)
研发费: 同花顺利润表(主) → 新浪利润表(备) → 四级降级
分红: Akshare巨潮(主) → Baostock(备)
行业: Baostock query_stock_industry
注册属地: 巨潮 stock_profile_cninfo
```
### 2.2 降级链速查
| 目标数据 | 降级链 |
|---------|--------|
| 研发费用 | 同花顺利润表"研发费用" → 新浪利润表"研发费用" → 同花顺备用列名 → 新浪备用列名 |
| 净利润 | 同花顺利润表 → 新浪利润表 → Baostock profit |
| 营收 | 同花顺利润表 → 新浪利润表 → Baostock profit |
| 分红 | 巨潮分红 → Baostock dividend |
| 行业 | Baostock industry → 东方财富(可能不可达) |
| 注册地 | 巨潮 profile_cninfo → (无备用) |
---
## 3. 登录模式对照
| 数据源 | 登录方式 | 生命周期 |
|--------|---------|---------|
| Baostock | `bs.login()` / `bs.logout()` | 需显式管理, 查询前必须登录 |
| Akshare | 无需登录 | 直接调用 |
| Tushare | `ts.pro_api('your_token')` | Token一次性设置 |
**Baostock登录建议**: 在程序入口登录, 程序出口登出, 中间复用连接。换股时无需登出, 仅在切换完全不同股票或退出时登出。
---
## 4. Baostock 接口详解
### 4.1 登录/登出
```python
import baostock as bs
# 登录
lg = bs.login()
if lg.error_code != '0':
print(f"登录失败: {lg.error_msg}")
# 登出
bs.logout()
# ⚠️: 查询前必须登录, 查询后无需立即登出(可复用连接)
```
### 4.2 K线数据
```python
# 日线
rs = bs.query_history_k_data_plus(
code="sh.600000", # baostock格式: sh.600000 / sz.000001 / bj.430047
fields="date,close,open,high,low,volume,amount,turn",
start_date="2025-01-01",
end_date="2025-12-31",
frequency="d", # d=日线, w=周线, m=月线
adjustflag="3" # 1=后复权, 2=前复权, 3=不复权
)
# 60分钟线
rs = bs.query_history_k_data_plus(
code="sh.600000",
fields="date,time,close,open,high,low,volume,amount",
start_date="2025-01-01",
end_date="2025-12-31",
frequency="60", # 5/15/30/60分钟
adjustflag="3"
)
# 遍历结果
def bs_query_to_df(rs):
"""将baostock ResultData转为DataFrame"""
rows = []
while (rs.error_code == '0') and rs.next():
rows.append(rs.get_row_data())
if not rows:
return None
import pandas as pd
return pd.DataFrame(rows, columns=rs.fields)
```
**⚠️ 踩坑**:
- K线返回的 close/volume 等字段都是**字符串类型**, 需手动转float
- 60分钟线需 `fields` 含 `time` 字段
- `adjustflag="3"` 不复权时, 价格为原始价格
### 4.3 财务数据(五大季报)
```python
# 盈利数据
rs = bs.query_profit_data(code="sh.600000", year=2024, quarter=4)
# 关键字段: totalShare, liqaShare, epsTTM, gpMargin, netProfit, roeAvg, roeDiluted
# 成长数据
rs = bs.query_growth_data(code="sh.600000", year=2024, quarter=4)
# 关键字段: YOYEquity, YOYAsset, YOYNI, YOYEPSBasic, YOYPNI, YOYOR, YOYGR
# 运营数据
rs = bs.query_operation_data(code="sh.600000", year=2024, quarter=4)
# 关键字段: NetProfitGrowRate, OperatingProfitGrowRate, TotalRevenueGrowRate
# 现金流数据
rs = bs.query_cash_flow_data(code="sh.600000", year=2024, quarter=4)
# 关键字段: CAToAsset, NCAToAsset, CFOToOR
# 资产负债数据
rs = bs.query_balance_data(code="sh.600000", year=2024, quarter=4)
# 关键字段: totalAssets, totalLiab, totalEquity, currentAssets, currentLiab
```
**⚠️ 踩坑**:
- **totalShare单位不确定**: 实测某些版本返回"万股"而非"股", 用 `convert_share()` 自动检测
- **所有比率字段都是小数形式**, 需×100转百分比显示
- **quarter参数**: 1-4对应Q1-Q4, 年报为Q4
- **空字符串/None**: 字段可能返回空字符串''或None, 需用 `safe_float()` 统一处理
- **这些函数不接受yearType参数**
- **totalShare无province/area/city/region字段** — 不能用于查注册属地
### 4.4 分红数据
```python
rs = bs.query_dividend_data(
code="sh.600000",
year=2024,
yearType="report" # "report"=按报告期, "operate"=按运营期
)
# 关键字段: dividCashPsBeforeTax(元/股), dividPreistNoTax(每10股送股), dividAistNoTax(每10股转增)
```
### 4.5 行业/基本信息
```python
# 行业分类
rs = bs.query_stock_industry(code="sh.600000")
# 字段: industry, industryClassification, province(省份)
# 注意: baostock的province字段只存在于query_stock_basic中?
# 实测: query_stock_industry返回字段: updateDate, code, code_name, industry, industryClassification
# 并没有province字段!注册属地请用巨潮profile_cninfo
# 股票基本信息
rs = bs.query_stock_basic(code="sh.600000")
# 字段: code, code_name, ipoDate, outDate, type, status
# 交易日查询
rs = bs.query_trade_dates(start_date="2025-01-01", end_date="2025-12-31")
# 全部股票列表
rs = bs.query_all_stock(day="2025-05-12")
```
### 4.6 Baostock 字段速查
**盈利数据(query_profit_data)**:
| 字段 | 含义 | 单位 | 转换 |
|------|------|------|------|
| totalShare | 总股本 | 股⚠️ | `convert_share()` → 亿股 |
| liqaShare | 流通股本 | 股⚠️ | `convert_share()` → 亿股 |
| epsTTM | 每股收益TTM | 元/股 | 直接使用 |
| gpMargin | 毛利率 | 小数 | ×100→% |
| netProfit | 净利润 | 元 | ÷1亿→亿元 |
| roeAvg | 平均ROE | 小数 | ×100→% |
| roeDiluted | 稀释ROE | 小数 | ×100→% |
| operatingRevenue | 营业收入 | 元 | ÷1亿→亿元 |
**成长数据(query_growth_data)**:
| 字段 | 含义 | 单位 | 转换 |
|------|------|------|------|
| YOYEquity | 净资产同比增长率 | 小数 | ×100→% |
| YOYAsset | 总资产同比增长率 | 小数 | ×100→% |
| YOYNI | 净利润同比增长率 | 小数 | ×100→% |
| YOYPNI | 归母净利润同比增长率 | 小数 | ×100→% |
| YOYOR | 营业收入同比增长率 | 小数 | ×100→% |
| YOYGR | 营业总收入同比增长率 | 小数 | ×100→% |
**分红数据(query_dividend_data)**:
| 字段 | 含义 | 单位 |
|------|------|------|
| dividCashPsBeforeTax | 税前每股现金分红 | 元/股 |
| dividPreistNoTax | 每10股送股数 | 股 |
| dividAistNoTax | 每10股转增股数 | 股 |
---
## 5. Akshare 接口详解
### 5.1 同花顺财务摘要 (`stock_financial_abstract_ths`)
```python
import akshare as ak
# 按年度
df = ak.stock_financial_abstract_ths(symbol="600000", indicator="按年度")
# 按单季度
df = ak.stock_financial_abstract_ths(symbol="600000", indicator="按单季度")
```
**⚠️ 踩坑**:
- 金额列带单位: 如 "1483.91亿"、"4.82万亿"、"3391.23万", 需用 `parse_amount()` 解析
- 比率列带%号: 如 "6.22%", 需用 `parse_percent()` 解析
- 返回DataFrame最新行在**最后**, 遍历时注意顺序
- 列名可能因akshare版本更新而变化
- **⚠️ 此接口没有"研发费用"列!** 研发费需从利润表获取
### 5.2 同花顺现金流量表 (`stock_financial_cash_ths`)
```python
df = ak.stock_financial_cash_ths(symbol="600000", indicator="按年度")
```
**⚠️ 踩坑**:
- 列名极长且包含标点符号, 硬编码容易出错
- 同一列名在不同akshare版本中可能有细微差异(空格/标点)
### 5.3 同花顺利润表 (`stock_financial_benefit_ths`) ✅含研发费
```python
df = ak.stock_financial_benefit_ths(symbol="600000", indicator="按年度")
# ⚠️ 注意: 接口名是 stock_financial_benefit_ths, 不是 stock_profit_sheet_ths(已废弃)
```
**⚠️ 踩坑**:
- 金额列带单位: 如 "1.90亿"、"227.55亿"、"6192.32万", 需用 `parse_amount` 解析
- **研发费用列在此接口有!** 列名固定为 `"研发费用"`
- 2017年以前的数据研发费列值可能为 `False`(布尔值), 表示该年度未单独披露
- 报告期格式: "2025"(纯年份), 匹配用 `str(year) in rp`
- 列名可能为: "研发费用"/"研究开发费用"/"研究与开发费用", 需多列名尝试
### 5.4 新浪利润表 (`stock_financial_report_sina` symbol="利润表") ✅含研发费
```python
df = ak.stock_financial_report_sina(stock="000063", symbol="利润表")
```
**⚠️ 踩坑**:
- **金额列单位为"元"**, 是纯数字(float), 如 `22754978000.0`, 需÷1亿转亿元
- 报告日格式: `"20251231"`, 匹配年报用 `f"{year}1231" in rp`
- 该接口同时返回季报和年报数据, 按报告日筛选即可
- **研发费用列在此接口有!** 列名固定为 `"研发费用"`
- 数据非常完整, 2017年以前的研发费也有(同花顺利润表可能返回False)
### 5.5 新浪财务指标 (`stock_financial_analysis_indicator`)
```python
df = ak.stock_financial_analysis_indicator(symbol="600000", start_year="2022")
# 按日期索引, 每行一个报告期
```
**⚠️ 踩坑**:
- **列名含(%)的毛利率列**: 返回值可能是百分比(如92.15)也可能是小数(如0.92), 需用 `convert_sina_percent()` 或 `abs(f) < 2` 判断
- **列名含(元)的金额列**: 实际单位可能是"元"也可能是"万元", 不确定! 优先用 `parse_amount` 处理, `parse_amount` 返回 None 时按"元"÷1亿
- 新浪数据通过日期索引访问: `df.loc[f"{year}-12-31"]`
- **❌ 此接口没有"研发费用"列!** 研发费需从利润表获取
### 5.6 巨潮分红数据 (`stock_dividend_cninfo`)
```python
df = ak.stock_dividend_cninfo(symbol="600000")
# 关键列: 报告时间, 派息比例(每10股派息额)
# 报告时间格式: "2024年年报"
```
**⚠️ 踩坑**:
- 派息比例是"每10股"的金额, 需÷10转为每股分红
- 匹配年报: `f'{year}年报' in str(row['报告时间'])`
### 5.7 东方财富个股信息 (`stock_individual_info_em` ⚠️服务器不可达)
```python
# ⚠️ 以下接口从当前服务器无法访问, 仅作降级参考
df = ak.stock_individual_info_em(symbol="600000") # 个股基本信息
df = ak.stock_zh_a_hist(symbol="600000", period="daily",
start_date="20250101", end_date="20251231") # 历史行情
```
### 5.8 巨潮公司概况 (`stock_profile_cninfo`) ✅推荐用于注册地
```python
df = ak.stock_profile_cninfo(symbol="600519")
# 含"注册地址"列, 值为完整地址如"贵州省仁怀市茅台镇"
```
### 5.9 新浪实时行情 (`stock_zh_a_spot_em` 替代方案)
```python
df = ak.stock_zh_a_spot_em() # 全市场实时行情
```
### 5.10 Akshare 安全调用封装
```python
def ak_safe_call(fn, *args, default=None, **kwargs):
"""安全调用akshare接口"""
try:
result = fn(*args, **kwargs)
if result is None or (hasattr(result, 'empty') and result.empty):
return default
return result
except Exception as e:
print(f" [AK] 接口异常: {e}")
return default
```
---
## 6. 废弃/不可用接口一览
> ⚠️ **直接从历史踩坑中整理, 不要再用**
| 接口名 | 状态 | 替代方案 |
|--------|------|---------|
| `stock_profit_sheet_ths` | ❌ 已废弃(不存在) | `stock_financial_benefit_ths` |
| `stock_financial_abstract_ths` | ❌ 无研发费列 | `stock_financial_benefit_ths` |
| `stock_financial_analysis_indicator` | ❌ 无研发费列 | `stock_financial_report_sina` |
| `stock_individual_info_em` | ❌ 服务器不可达(2026.05) | `stock_profile_cninfo` |
| `stock_info_sz_name_code(indicator=...)` | ❌ 不接受indicator参数 | 不使用 |
### 接口名易混提醒
| 易混淆名 | 正确名 |
|---------|--------|
| ❌ `stock_profit_sheet_ths` | ✅ `stock_financial_benefit_ths` |
| — | ✅ `stock_financial_report_sina(stock="000063", symbol="利润表")` |
---
## 7. 研发费用查询专项指南
### 7.1 正确查询路径
```
研发费用查询只能从利润表获取
❌ 财务摘要(stock_financial_abstract_ths) → 无研发费列
❌ 新浪财务指标(stock_financial_analysis_indicator) → 无研发费列
✅ 同花顺利润表(stock_financial_benefit_ths) → 有"研发费用"列
✅ 新浪利润表(stock_financial_report_sina symbol="利润表") → 有"研发费用"列
```
### 7.2 四级降级策略
```
Level 1: 同花顺利润表 → "研发费用"列(首选)
Level 2: 新浪利润表 → "研发费用"列
Level 3: 同花顺利润表 → 备用列名("研究开发费用"/"研究与开发费用")
Level 4: 新浪利润表 → 备用列名(如列名有变化)
```
### 7.3 同花顺利润表获取研发费
```python
df = ak.stock_financial_benefit_ths(symbol="000063", indicator="按年度")
for _, row in df.iterrows():
rp = str(row.get('报告期', ''))
if '2025' in rp:
rd = row.get('研发费用', '') # 返回如 "227.55亿"
if rd is not False: # 早期年份可能为False
p = parse_amount(rd) # → 227.55 (亿元)
break
```
### 7.4 新浪利润表获取研发费
```python
df = ak.stock_financial_report_sina(stock="000063", symbol="利润表")
for _, row in df.iterrows():
rp = str(row.get('报告日', ''))
if '20251231' in rp:
raw_val = row.get('研发费用', '') # 返回纯数字如 22754978000.0
f = safe_float(raw_val)
rd_yi = f / 100000000 # → 227.55 亿元
break
```
### 7.5 研发费用完整容错代码
```python
def get_rd_expense(pure_code, year):
"""获取指定股票指定年度的研发费用(亿元)
降级链: 同花顺利润表 → 新浪利润表
"""
# Level 1: 同花顺利润表
df = ak_safe_call(ak.stock_financial_benefit_ths, symbol=pure_code, indicator="按年度")
if df is not None:
rd_cols = ['研发费用', '研究开发费用', '研究与开发费用']
for _, row in df.iterrows():
if str(year) in str(row.get('报告期', '')):
for col in rd_cols:
val = row.get(col)
if val is not False and val is not None:
p = parse_amount(val)
if p is not None:
return p
# Level 2: 新浪利润表
df = ak_safe_call(ak.stock_financial_report_sina, stock=pure_code, symbol="利润表")
if df is not None:
target = f"{year}1231"
for _, row in df.iterrows():
if target in str(row.get('报告日', '')):
val = row.get('研发费用')
f = safe_float(val)
if f != NO_DATA:
return f / 100000000
return NO_DATA
```
### 7.6 验证数据
| 股票 | 同花顺利润表 | 新浪利润表 | 一致性 |
|------|------------|-----------|--------|
| 中兴通讯000063 | 227.55亿 | 227.55亿 | ✅ |
| 茅台600519 | 1.90亿 | 1.90亿 | ✅ |
---
## 8. 公司注册属地查询
### 8.1 正确查询方法
```python
import akshare as ak
import re
# 巨潮公司概况(首选, 唯一可靠来源)
df = ak.stock_profile_cninfo(symbol="600519")
# 含"注册地址"列, 值为完整地址如"贵州省仁怀市茅台镇"
# ⚠️ baostock的query_stock_industry无province字段!
# 不要用baostock查注册属地
```
### 8.2 省份提取函数
```python
def extract_province(addr):
"""从完整注册地址提取省份"""
if not addr:
return ''
# 直辖市
for city in ['北京', '上海', '天津', '重庆']:
if city in addr:
return city + '市'
# 省/自治区/特别行政区
m = re.search(r'([\u4e00-\u9fa5]+省|[\u4e00-\u9fa5]+自治区|[\u4e00-\u9fa5]+特别行政区)', addr)
if m:
return m.group(1)
return addr
```
### 8.3 验证数据
| 股票 | 巨潮注册地址 | 提取省份 |
|------|------------|---------|
| 茅台600519 | 贵州省仁怀市茅台镇 | 贵州省 ✅ |
| 中兴通讯000063 | 广东省深圳市南山区 | 广东省 ✅ |
| 宁德时代300750 | 福建省宁德市 | 福建省 ✅ |
| 隆基绿能601012 | 陕西省西安市 | 陕西省 ✅ |
| 平安银行000001 | 广东省深圳市 | 广东省 ✅ |
---
## 9. 股票代码格式处理
### 9.1 格式对照
| 格式 | 示例 | 使用场景 |
|------|------|---------|
| 纯代码 | `600000` | 用户输入、akshare接口 |
| Baostock格式 | `sh.600000` / `sz.000001` / `bj.430047` | baostock全部接口 |
| 东方财富格式 | `600000` (纯代码) | akshare东方财富接口 |
| Tushare格式 | `600000.SH` | tushare接口 |
### 9.2 市场前缀规则
| 开头数字 | 市场 | Baostock前缀 | Tushare后缀 | 说明 |
|---------|------|-------------|------------|------|
| 6, 9 | 上海证券交易所 | sh. | .SH | 9开头为科创板 |
| 0, 3 | 深圳证券交易所 | sz. | .SZ | 3开头为创业板 |
| 8, 4 | 北京证券交易所 | bj. | .BJ | 北交所 |
### 9.3 代码识别与转换
```python
def normalize_stock_code(raw):
"""统一识别股票代码, 返回 (baostock格式, 纯代码)
支持输入: 600000 / sh.600000 / SH600000 / 430047 / bj.430047
"""
raw = raw.strip().replace(' ', '').replace('\uff0e', '.').replace('\u3002', '.')
if '.' in raw:
parts = raw.split('.')
bs_code = parts[0].lower() + '.' + parts[1]
pure_code = parts[1]
elif raw.startswith('6') or raw.startswith('9'):
bs_code = 'sh.' + raw
pure_code = raw
elif raw.startswith('0') or raw.startswith('3'):
bs_code = 'sz.' + raw
pure_code = raw
elif raw.startswith('8') or raw.startswith('4'):
bs_code = 'bj.' + raw # 北交所
pure_code = raw
else:
raise ValueError(f"无法识别代码格式: {raw}")
return bs_code, pure_code
```
### 9.4 注意事项
1. **北交所代码(8/4开头)**: 必须映射为 `bj.` 前缀, 不能用 `sh.` 或 `sz.`
2. **全角句号**: 用户可能输入全角句号(`.`/`。`), 需先替换为半角(`.`)
3. **大小写**: `SH.600000` 和 `sh.600000` 等价, 统一转小写
4. **排序陷阱**: 对"3.10"等字符串排序时不能用默认字符串排序, 需按数字排序:
```python
sorted(selected, key=lambda k: tuple(int(x) for x in k.split('.')))
```
---
## 10. 核心工具函数
```python
import re
import math
NO_DATA = "无" # 统一无数据标记
# ── safe_float ──
def safe_float(val, default=NO_DATA):
"""安全转浮点, 处理None/空串/NaN/False/超限值"""
if val is None or val == '' or val == 'False' or val == 'NaN':
return default
try:
f = float(val)
if math.isnan(f) or math.isinf(f) or abs(f) > 1e15:
return default
return f
except (ValueError, TypeError):
return default
# ── parse_amount ──
def parse_amount(s):
"""解析带单位的金额字符串, 统一转为亿元
'1483.91亿' → 1483.91
'4.82万亿' → 48200
'3391.23万' → 0.339123
纯数字(元) → 原值(调用者需自行÷1亿)
无法解析 → None
"""
if s is None or s == '' or s == 'False':
return None
s = str(s).strip()
m = re.match(r'^([-\d.]+)\s*万亿$', s)
if m:
return float(m.group(1)) * 10000
m = re.match(r'^([-\d.]+)\s*亿$', s)
if m:
return float(m.group(1))
m = re.match(r'^([-\d.]+)\s*万$', s)
if m:
return float(m.group(1)) / 10000
try:
return float(s) # 纯数字, 单位需调用者判断
except (ValueError, TypeError):
return None
# ── parse_percent ──
def parse_percent(s):
"""解析百分比字符串
'6.22%' → 6.22
纯数字 → 原值(可能是小数也可能是百分比)
"""
if s is None or s == '' or s == 'False':
return None
s = str(s).strip()
m = re.match(r'^([-\d.]+)\s*%$', s)
if m:
return float(m.group(1))
try:
return float(s)
except (ValueError, TypeError):
return None
# ── convert_share ──
def convert_share(val):
"""Baostock totalShare/liqaShare 自动单位检测与转换(→亿股)"""
f = safe_float(val)
if f == NO_DATA:
return NO_DATA
if f < 10000 and f > 0:
return f / 10000 # 万股→亿股
else:
return f / 100000000 # 股→亿股
# ── convert_sina_percent ──
def convert_sina_percent(val):
"""新浪(%)列: 判断是小数还是百分比, 统一转为百分比"""
f = safe_float(val)
if f == NO_DATA:
return NO_DATA
if abs(f) < 2: # 小数形式如0.92
f = f * 100
return f # 已是百分比如92.15
# ── extract_province ──
def extract_province(addr):
"""从完整注册地址提取省份"""
if not addr:
return ''
for city in ['北京', '上海', '天津', '重庆']:
if city in addr:
return city + '市'
m = re.search(r'([\u4e00-\u9fa5]+省|[\u4e00-\u9fa5]+自治区|[\u4e00-\u9fa5]+特别行政区)', addr)
if m:
return m.group(1)
return addr
```
---
## 11. 单位转换速查
| 源 | 原始单位 | 目标单位 | 转换方法 | 备注 |
|----|---------|---------|---------|------|
| 同花顺金额 | "1483.91亿" | 亿元 | `parse_amount()` | 自动处理万亿/亿/万 |
| 同花顺利润表 False值 | 布尔False | — | `if val is not False:` | 视为无数据, 跳过 |
| 新浪金额(元)列 | 不确定 | 亿元 | 先`parse_amount()`, 返回None时÷1亿 | ⚠️单位可能为万元 |
| 新浪利润表金额 | 元(纯数字) | 亿元 | `val / 100000000` | 纯float, 必须÷1亿 |
| Baostock金额 | 元 | 亿元 | `val / 100000000` | netProfit等 |
| Baostock股本 | 股/万股 | 亿股 | `convert_share()` | 自动检测如果val<10000为万股 |
| Baostock比率 | 小数 | 百分比 | `val * 100` | gpMargin/roeAvg等 |
| 新浪比率(%)列 | 不确定 | 百分比 | `convert_sina_percent()` | `abs(f)<2`判断 |
| 同花顺比率 | "6.22%" | 百分比 | `parse_percent()` | 带%号 |
| 巨潮分红 | 每10股派息 | 每股分红 | `val / 10` | 派息比例列 |
---
## 12. 缓存策略
### 12.1 策略选择
| 场景 | 推荐策略 | 理由 |
|------|---------|------|
| 单股票多指标查询 | 内存字典缓存 | 数据量小, 生命周期短 |
| 多股票回测 | 磁盘CSV缓存 | 数据量大, 需跨运行复用 |
| 实时行情 | 不缓存 | 数据时效性要求高 |
| 静态财务数据 | 磁盘缓存+过期时间 | 年报季度更新, 可缓存较长时间 |
### 12.2 内存缓存(财务查询场景)
```python
class FinancialDataFetcher:
def __init__(self):
self._cache = {}
def bs_query(self, func_name, year, quarter):
cache_key = f"bs_{func_name}_{year}_{quarter}"
if cache_key in self._cache:
return self._cache[cache_key]
# ... 查询逻辑 ...
self._cache[cache_key] = result
return result
```
### 12.3 磁盘缓存(回测场景)
```python
import os
import pandas as pd
CACHE_DIR = "data_cache"
def get_cache_path(code, start_date, end_date, freq):
os.makedirs(CACHE_DIR, exist_ok=True)
return os.path.join(CACHE_DIR, f"{code}_{start_date}_{end_date}_{freq}.csv")
def load_cache(path):
if os.path.exists(path):
return pd.read_csv(path, dtype=str)
return None
def save_cache(path, df):
df.to_csv(path, index=False)
# 使用示例
cache_path = get_cache_path("sh.600000", "2025-01-01", "2025-12-31", "d")
df = load_cache(cache_path)
if df is None:
df = fetch_from_baostock(...)
save_cache(cache_path, df)
```
---
## 13. 错误处理与重试
### 13.1 Baostock 带重试查询
```python
import time
import socket
def bs_query_with_retry(fn, max_retries=3, wait_seconds=2):
"""带重试的baostock查询"""
for attempt in range(max_retries):
try:
rs = fn()
if rs.error_code == '0':
return rs
else:
print(f" [BS] 查询错误: {rs.error_msg}, 重试 {attempt+1}/{max_retries}")
except (socket.timeout, TimeoutError, OSError) as e:
print(f" [BS] 网络异常: {e}, 重试 {attempt+1}/{max_retries}")
time.sleep(wait_seconds)
return None
```
### 13.2 Akshare 安全调用
```python
def ak_safe_call(fn, *args, default=None, **kwargs):
"""安全调用akshare接口"""
try:
result = fn(*args, **kwargs)
if result is None or (hasattr(result, 'empty') and result.empty):
return default
return result
except Exception as e:
print(f" [AK] 接口异常: {e}")
return default
```
### 13.3 Baostock 结果转DataFrame
```python
def bs_query_to_df(rs):
"""将baostock ResultData转为DataFrame"""
rows = []
while (rs.error_code == '0') and rs.next():
rows.append(rs.get_row_data())
if not rows:
return None
import pandas as pd
return pd.DataFrame(rows, columns=rs.fields)
```
---
## 14. 数据源降级策略
### 14.1 通用降级函数
```python
def fetch_with_fallback(primary_fn, fallback_fn1=None, fallback_fn2=None):
"""带降级的数据获取: 主数据源 → 备用1 → 备用2 → "无" """
result = primary_fn()
if result is not None and result != NO_DATA:
return result
if fallback_fn1:
result = fallback_fn1()
if result is not None and result != NO_DATA:
return result
if fallback_fn2:
result = fallback_fn2()
if result is not None and result != NO_DATA:
return result
return NO_DATA
```
### 14.2 推荐降级链
| 目标 | 降级链 |
|------|--------|
| 财报 | Akshare同花顺 → 新浪 → Baostock |
| 分红 | Akshare巨潮 → Baostock |
| 行业 | Baostock → 东方财富(可能不可达) |
| 研发费 | 同花顺利润表 → 新浪利润表 → 同花顺备用列名 → 新浪备用列名 |
---
## 15. 最佳实践模板
### 15.1 FinancialDataFetcher 完整类模板
```python
import datetime
import baostock as bs
class FinancialDataFetcher:
"""财务数据获取器 - 主力+备用双源模式"""
def __init__(self, bs_code, pure_code):
self.bs_code = bs_code
self.pure_code = pure_code
self._cache = {}
self._bs_logged_in = False
self.now = datetime.datetime.now()
# 判断"去年": 5月前年报可能未出, 取前年
self.last_year = self.now.year - 2 if self.now.month < 5 else self.now.year - 1
self.prev_year = self.last_year - 1
self.year_before = self.last_year - 2
# ── 登录管理 ──
def bs_login(self):
if not self._bs_logged_in:
lg = bs.login()
if lg.error_code != '0':
return False
self._bs_logged_in = True
return True
def bs_logout(self):
if self._bs_logged_in:
bs.logout()
self._bs_logged_in = False
def _ensure_bs_login(self):
"""确保已登录(在所有baostock查询前调用)"""
if not self._bs_logged_in:
self.bs_login()
# ── 通用查询(带缓存) ──
def bs_query(self, func_name, year=None, quarter=None):
self._ensure_bs_login()
cache_key = f"bs_{func_name}_{year}_{quarter}"
if cache_key in self._cache:
return self._cache[cache_key]
# ... 实际查询逻辑 ...
# self._cache[cache_key] = result
# return result
```
### 15.2 主循环模板
```python
def main():
fetcher = None
bs_code = None
while True:
if bs_code is None:
bs_code, pure_code = input_stock_code()
fetcher = None # 换股时重置
if fetcher is None:
fetcher = FinancialDataFetcher(bs_code, pure_code)
fetcher.bs_login()
# ... 查询逻辑 ...
action = wait_key_prompt()
if action == 'exit':
fetcher.bs_logout()
break
elif action == 'new_stock':
fetcher.bs_logout()
fetcher = None
bs_code = None
# 'same_stock': 保持fetcher复用缓存
# 关键原则:
# - fetcher在循环外创建, 换股时才重置
# - 换股时先bs_logout()再重建
# - 退出时bs_logout()
# - 同一股票多次查询时复用缓存
```
### 15.3 Numba加速技术指标模板
```python
from numba import jit
import numpy as np
@jit(nopython=True, cache=True, fastmath=True)
def calc_kd(close_prices, n=9, m1=3, m2=3):
"""计算KD指标 - Numba加速版"""
length = len(close_prices)
k_values = np.zeros(length)
d_values = np.zeros(length)
# ... 计算逻辑 ...
return k_values, d_values
@jit(nopython=True, cache=True, fastmath=True)
def calc_rsi(close_prices, period=8):
"""计算RSI - Numba加速版"""
# ... 计算逻辑 ...
return rsi_values
# 关键参数: RSI周期=8, KD参数 n=9, m1=3, m2=3
```
---
## 16. 完整踩坑记录
### 16.1 单位陷阱
| 日期 | 踩坑内容 | 影响 | 解决方案 |
|------|---------|------|---------|
| 2026-05-12 | 新浪毛利率(%)列返回值可能是百分比也可能是小数 | 毛利率9215%而非92.15% | `abs(f)<2` 判断 |
| 2026-05-12 | Baostock roeAvg是小数而非百分比 | ROE显示0.15%而非15% | `f*100` |
| 2026-05-12 | Baostock totalShare单位可能是"万股" | 股本偏小10000倍 | 自动检测 `convert_share()` |
| 2026-05-12 | 新浪金额列(元)实际单位可能是"万元" | 研发费用/营收偏差1万倍 | 优先用parse_amount |
| 2026-05-12 | 巨潮分红"派息比例"是每10股 | 每股分红偏大10倍 | ÷10 |
| 2026-05-13 | 同花顺利润表早期年份研发费返回False(布尔值) | parse_amount解析False失败 | 加`val is not False`前置判断 |
| 2026-05-13 | 新浪利润表金额单位为元(纯数字) | 直接当亿元使用偏小1亿倍 | ÷1亿转亿元 |
| 2026-05-13 | Baostock股本单位: 股可能为万股 | 股本偏差 | `if f<10000 and f>0`检测 |
### 16.2 API陷阱
| 日期 | 踩坑内容 | 影响 | 解决方案 |
|------|---------|------|---------|
| 2026-05-12 | 东方财富API从服务器无法访问 | stock_individual_info_em超时 | 降级到其他数据源 |
| 2026-05-12 | 同花顺现金流表列名硬编码 | akshare升级后列名变化导致查不到 | 加try-except和模糊匹配 |
| 2026-05-12 | baostock query_profit_data等不接受yearType参数 | 传参报错 | 不传yearType |
| 2026-05-12 | baostock需login后才能查询 | 未登录直接查询报错 | 查询前检查登录状态 |
| 2026-05-12 | baostock K线返回值全是字符串类型 | 数值比较/计算出错 | 手动float()转换 |
| 2026-05-13 | 同花顺财务摘要无研发费列 | 研发费始终返回NO_DATA | 改用同花顺利润表 |
| 2026-05-13 | 新浪财务分析指标无研发费列 | 同上 | 改用新浪利润表 |
| 2026-05-13 | 同花顺利润表早期年份研发费返回False | parse_amount解析失败 | 加`val is not False`判断 |
| 2026-05-13 | stock_profit_sheet_ths接口不存在 | AttributeError | 正确接口是stock_financial_benefit_ths |
| 2026-05-13 | baostock query_stock_industry无province字段 | 无法查注册属地 | 改用巨潮 profile_cninfo |
| 2026-05-18 | 公司查注册属地无备用方案 | profile_cninfo失败则无替代 | 暂无备用, 注意异常处理 |
### 16.3 逻辑陷阱
| 日期 | 踩坑内容 | 影响 | 解决方案 |
|------|---------|------|---------|
| 2026-05-12 | sorted()对"3.10"字符串排序不正确 | 3.10排在3.2前面 | 按数字排序 `key=lambda k: tuple(int(x) for x in k.split('.'))` |
| 2026-05-12 | 分红终止条件逻辑错误 | 第1年就break,只取1年数据 | 删除提前终止条件 |
| 2026-05-12 | PEG中g_val==0无法匹配字符串"0" | ZeroDivisionError | 先float()再判断 |
| 2026-05-12 | 北交所代码(8/4开头)未处理 | 北交所股票无法查询 | 增加bj.前缀 |
---
## 17. 附录:各接口列名速查
### A. 同花顺财务摘要 (`stock_financial_abstract_ths`)
| 列名 | 含义 | 格式 |
|------|------|------|
| 报告期 | 年报/季报日期 | "2024-12-31" |
| 基本每股收益 | EPS | 纯数字(元) |
| 每股净资产 | BPS | 纯数字(元) |
| 每股经营现金流 | OCF/Share | 纯数字(元) |
| 净利润 | 净利润 | "1483.91亿" |
| 营业总收入 | 营收 | "4.82万亿" |
| 净资产收益率 | ROE | "6.22%" |
| 净利润同比增长率 | 净利润增速 | "15.3%" |
| 营业总收入同比增长率 | 营收增速 | "12.1%" |
> ⚠️ **此接口没有"研发费用"列!**
### B. 同花顺利润表 (`stock_financial_benefit_ths`) ✅含研发费
| 列名 | 含义 | 格式 | 备注 |
|------|------|------|------|
| 报告期 | 年报/季报 | "2025"(纯年份) | 匹配: `str(year) in rp` |
| 一、营业总收入 | 营收 | "1720.54亿" | 带单位 |
| 其中:营业收入 | 营业收入 | "1688.38亿" | 带单位 |
| 二、营业总成本 | 营业总成本 | "573.71亿" | 带单位 |
| 其中:营业成本 | 营业成本 | "114.89亿" | 带单位 |
| 销售费用 | 销售费用 | "725.35亿" | 带单位 |
| 管理费用 | 管理费用 | "83.20亿" | 带单位 |
| **研发费用** | **研发费用** | **"1.90亿"** | **✅带单位, 早期年份可能为False** |
| 财务费用 | 财务费用 | "-0.82亿" | 可能为负 |
| 三、营业利润 | 营业利润 | "1148.09亿" | 带单位 |
| 五、净利润 | 净利润 | "853.10亿" | 带单位 |
| 归属于母公司所有者的净利润 | 归母净利润 | "823.20亿" | 带单位 |
> ⚠️ 接口名是 `stock_financial_benefit_ths`, **不是** `stock_profit_sheet_ths`(已废弃)
### C. 新浪利润表 (`stock_financial_report_sina` symbol="利润表") ✅含研发费
| 列名 | 含义 | 格式 | 备注 |
|------|------|------|------|
| 报告日 | 报告日期 | "20251231" | 匹配: `f"{year}1231" in rp` |
| 营业总收入 | 营收 | 172054171890.91 | 纯数字(元) |
| 营业收入 | 营业收入 | 168838102514.79 | 纯数字(元) |
| 营业成本 | 营业成本 | 114892277570.91 | 纯数字(元) |
| 销售费用 | 销售费用 | 72534996000.68 | 纯数字(元) |
| 管理费用 | 管理费用 | 8320061659.66 | 纯数字(元) |
| **研发费用** | **研发费用** | **190112246.58** | **✅纯数字(元), ÷1亿→亿元** |
| 财务费用 | 财务费用 | -815240284.72 | 可能为负 |
| 营业利润 | 营业利润 | 114808950164.24 | 纯数字(元) |
| 净利润 | 净利润 | 85310324833.67 | 纯数字(元) |
| 归属于母公司所有者的净利润 | 归母净利润 | 82320067101.68 | 纯数字(元) |
> ⚠️ 所有金额列单位均为**元**(纯float), 需÷1亿转亿元
> ⚠️ 该接口同时返回季报+年报, 按报告日筛选即可
### D. 新浪财务指标 (`stock_financial_analysis_indicator`)
| 列名 | 含义 | ⚠️单位注意 |
|------|------|-----------|
| 销售毛利率(%) | 毛利率 | 可能是百分比或小数 |
| 净资产收益率(%) | ROE | 同上 |
| 每股经营性现金流(元) | OCF/Share | 元/股 |
| 每股净资产_调整前(元) | BPS | 元/股 |
| 主营业务收入增长率(%) | 营收增速 | 可能是百分比或小数 |
| 净利润增长率(%) | 净利润增速 | 同上 |
| 营业收入(元) | 营收 | 元(也可能是万元⚠️) |
| 研发费用(元) | 研发 | 元(也可能是万元⚠️) |
> ⚠️ **此接口没有"研发费用"列!** 上表中"研发费用(元)"仅作参考, 实测可能有或无
### E. 同花顺现金流量表 (`stock_financial_cash_ths`)
| 列名 | 含义 | 格式 |
|------|------|------|
| 经营活动产生的现金流量净额 | OCF | 带单位 |
| 购建固定资产、无形资产和其他长期资产支付的现金 | CAPEX | 带单位 |
### F. 巨潮分红 (`stock_dividend_cninfo`)
| 列名 | 含义 | 格式 |
|------|------|------|
| 报告时间 | 报告期 | "2024年年报" |
| 派息比例 | 每10股派息额 | 需÷10→每股分红 |
---
## 本技能维护说明
### 经验积累机制
当你经过多次尝试才得出正确结果时(例如参数格式试错、接口选择调整、发现文档未明示的约束等), 必须将经验简要追加到第16章的踩坑记录中。
**记录标准**:
- 只记录经过**2次及以上尝试**才成功的情况
- 格式: `| 日期 | 踩坑内容 | 影响 | 解决方案 |`
- 内容简明, 聚焦"下次遇到同样情况该怎么做"
### 数据源状态更新
- 东方财富API等不可用状态可能随时间变化, 每隔一段时间应重新验证
- Akshare接口列名可能因版本变化, 使用前建议先打印列名验证
---
*文档结束 - 持续更新中*
don't have the plugin yet? install it then click "run inline in claude" again.