trading/demo_tushare_data.py

620 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# =============================================================================
# Real Data Acquisition Demo — Tushare Pro
# 真实数据获取演示 — Tushare Pro 版
# =============================================================================
#
# Tushare Pro 是国内最成熟的金融数据 API 之一,数据质量和稳定性
# 优于 AKShare适合策略验证通过后切换到生产级数据。
#
# 与 AKShare 的核心差异:
# • 需要注册获取 Token (免费注册,基础接口免费)
# • 积分系统: 注册送 120 分,部分接口需要更高积分
# • 股票代码格式: "000001.SZ" (而非纯数字)
# • 数据质量更高、接口更稳定
# • 支持基本面数据 (PE/PB/ROE) 和因子数据
#
# Prerequisites / 前置准备:
# 1. 注册 Tushare: https://tushare.pro/register
# 2. 获取 Token: 登录后 → 个人主页 → 接口 Token
# 3. 安装: pip install tushare pandas numpy
#
# Topics covered / 涵盖主题:
# §1 Tushare 注册与 Token 配置
# §2 获取 A 股个股日线数据
# §3 获取指数日线数据
# §4 获取股票基础信息 (上市日期/行业/市值)
# §5 获取财务数据 (PE/PB/ROE — 价值因子的数据源)
# §6 获取指数成分股权重 (组合优化的输入)
# §7 获取行业分类 (申万行业)
# §8 数据清洗与对齐
# §9 构建本地数据库
# §10 AKShare vs Tushare 对比总结
# =============================================================================
from __future__ import annotations
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import os
import time
from datetime import datetime
# ── 检查 tushare 是否安装 ──
try:
import tushare as ts
print(f"✓ Tushare 版本: {ts.__version__}")
except ImportError:
print("❌ Tushare 未安装。请在 trading conda 环境中执行:")
print(" conda activate trading")
print(" pip install tushare")
raise
print("=" * 68)
print(" 真实数据获取演示 — Tushare Pro 版")
print(" Real Data Acquisition Demo with Tushare Pro")
print("=" * 68)
# ══════════════════════════════════════════════════════════════════════
# §1 Tushare 注册与 Token 配置
# ══════════════════════════════════════════════════════════════════════
#
# 使用步骤:
# 1. 访问 https://tushare.pro/register 注册 (用手机号即可)
# 2. 登录后 → 个人主页 → 接口 Token → 复制那一长串字符
# 3. 把 Token 粘贴到下面 (或设置环境变量 TUSHARE_TOKEN)
#
# 积分说明:
# 注册: 120 分 (基础接口可用)
# 完善个人信息: +20 分
# 推荐他人注册: 各 +50 分
# 捐赠: 200 RMB = 3000 分 (解锁全部接口)
#
# 日常使用建议:
# - 将 Token 存为环境变量而非硬编码在代码中
# - 每个接口都标注所需最低积分,避免超限调用
# =============================================================================
print("\n[§1] Tushare Token 配置")
TUSHARE_TOKEN = os.environ.get("TUSHARE_TOKEN", "")
if not TUSHARE_TOKEN:
print(" ⚠ 未检测到 TUSHARE_TOKEN 环境变量")
print(" ── 获取 Token 的步骤 ──")
print(" 1. 访问 https://tushare.pro/register 注册")
print(" 2. 登录后进入个人主页 → 接口 Token")
print(" 3. 复制 Token 后,设置环境变量:")
print(" export TUSHARE_TOKEN='你的token'")
print()
print(" 或者直接在下方代码中填入 (仅本地测试用):")
print(' TUSHARE_TOKEN = "你的token"')
print()
print(" ⚠ 本 Demo 将继续运行,但数据获取部分会跳过或使用模拟数据")
print(" 您仍然可以看到完整的代码结构和数据清洗逻辑。")
print()
# 尝试初始化 Pro API
PRO = None
TOKEN_VALID = False
if TUSHARE_TOKEN:
try:
PRO = ts.pro_api(TUSHARE_TOKEN)
# 用最简单接口测试 Token 是否有效
test = PRO.trade_cal(exchange='SSE', start_date='20250601', end_date='20250605')
TOKEN_VALID = len(test) > 0
print(f" ✓ Token 验证成功")
print(f" ✓ Pro API 就绪")
except Exception as e:
print(f" ✗ Token 验证失败: {e}")
else:
print(f" - Token 未设置,跳过远程数据获取")
print(f" - 代码结构完整,可阅读学习流程逻辑")
# ══════════════════════════════════════════════════════════════════════
# §2 获取 A 股个股日线数据
# ══════════════════════════════════════════════════════════════════════
#
# 接口: pro.daily()
# 所需积分: 120 分 (注册即送)
# 每日调用上限: 200 次
# 单次最多返回: 约 5000 条记录
#
# 参数:
# ts_code — 股票代码 "000001.SZ" / "600000.SH"
# trade_date— 交易日期 "YYYYMMDD"
# start_date/end_date — 日期范围
#
# 注意: Tushare 的日期格式是 "YYYYMMDD" (无连字符)
# =============================================================================
print("\n[§2] 获取个股日线数据")
# ── 构建 A 股代表性标的池 ──
STOCK_POOL = {
"000001.SZ": "平安银行",
"000002.SZ": "万科A",
"000858.SZ": "五粮液",
"600519.SH": "贵州茅台",
"600036.SH": "招商银行",
"600276.SH": "恒瑞医药",
"300750.SZ": "宁德时代",
"601318.SH": "中国平安",
}
# ── 日期范围转换 ──
# Demo 系列用 "YYYY-MM-DD"Tushare 用 "YYYYMMDD"
START_DATE = "20190101"
END_DATE = "20250601"
def fetch_daily_safe(pro, ts_code, start, end, max_retries=3):
"""
安全获取个股日线数据。
Tushare 每次调用返回约 5000 条,单只股票 6 年约 1500 个交易日,
一次调用即可覆盖。如果有更多股票/更长区间,需要分多次调用并拼接。
"""
for attempt in range(max_retries):
try:
df = pro.daily(
ts_code=ts_code,
start_date=start,
end_date=end,
fields='ts_code,trade_date,open,high,low,close,pre_close,'
'change,pct_chg,vol,amount'
)
time.sleep(0.3) # 频率控制: Tushare 每分钟上限约 200 次
return df
except Exception as e:
print(f"{ts_code}{attempt+1} 次失败: {e}")
if attempt < max_retries - 1:
time.sleep(2.0)
return None
if TOKEN_VALID and PRO:
stock_daily_data = {}
for code, name in STOCK_POOL.items():
print(f" 获取 {code} ({name})...", end=" ")
df = fetch_daily_safe(PRO, code, START_DATE, END_DATE)
if df is not None and len(df) > 0:
stock_daily_data[code] = df
print(f"{len(df)} 行, {df['trade_date'].iloc[-1]} ~ {df['trade_date'].iloc[0]}")
else:
print("无数据")
if stock_daily_data:
# ── 构建价格面板 ──
stock_price_panel = pd.DataFrame()
for code, df in stock_daily_data.items():
# Tushare 返回的 trade_date 是 YYYYMMDD 格式
df = df.copy()
df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')
df = df.sort_values('trade_date')
s = df.set_index('trade_date')['close'].copy()
s.name = code
stock_price_panel = pd.concat([stock_price_panel, s], axis=1)
print(f"\n 个股价格面板: {stock_price_panel.shape}")
# 展示最新 3 行
print(f" 最新 3 个交易日:")
print(stock_price_panel.tail(3).to_string())
else:
print(" (跳过 — Token 未配置)")
print(f" 演示: 如果 Token 有效,会拉取以下 {len(STOCK_POOL)} 只股票的数据")
for code, name in STOCK_POOL.items():
print(f" {code} ({name})")
# ══════════════════════════════════════════════════════════════════════
# §3 获取指数日线数据
# ══════════════════════════════════════════════════════════════════════
#
# 接口: pro.index_daily()
# 所需积分: 120 分
#
# 指数代码:
# 000300.SH — 沪深300
# 000905.SH — 中证500
# 000016.SH — 上证50
# 399006.SZ — 创业板指
# =============================================================================
print("\n[§3] 获取指数日线数据")
INDEX_CODES = {
"000300.SH": "沪深300",
"000905.SH": "中证500",
"000016.SH": "上证50",
"399006.SZ": "创业板指",
}
if TOKEN_VALID and PRO:
index_data = {}
for code, name in INDEX_CODES.items():
print(f" 获取 {code} ({name})...", end=" ")
try:
df = PRO.index_daily(
ts_code=code,
start_date=START_DATE,
end_date=END_DATE,
fields='ts_code,trade_date,close,open,high,low,vol,amount,pct_chg'
)
time.sleep(0.3)
if df is not None and len(df) > 0:
index_data[code] = df
print(f"{len(df)}")
else:
print("无数据")
except Exception as e:
print(f"失败: {e}")
if index_data:
index_panel = pd.DataFrame()
for code, df in index_data.items():
df = df.copy()
df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')
df = df.sort_values('trade_date')
s = df.set_index('trade_date')['close']
s.name = code
index_panel = pd.concat([index_panel, s], axis=1)
print(f"\n 指数面板: {index_panel.shape}")
# 年化收益对比
ann_rets = (index_panel.iloc[-1] / index_panel.iloc[0]) ** (252.0 / len(index_panel)) - 1
print(" 年化收益率 (CAGR):")
for code, val in ann_rets.items():
name = INDEX_CODES.get(code, code)
print(f" {name}: {val:+.2%}")
else:
print(" (跳过 — Token 未配置)")
# ══════════════════════════════════════════════════════════════════════
# §4 获取股票基础信息
# ══════════════════════════════════════════════════════════════════════
#
# 接口: pro.stock_basic()
# 所需积分: 120 分
# 功能: 获取股票列表,包含股票代码、名称、上市日期、退市日期、行业、地区等
# =============================================================================
print("\n[§4] 获取股票基础信息")
if TOKEN_VALID and PRO:
try:
# 获取沪深两市全部 A 股列表
stock_basic = PRO.stock_basic(
exchange='',
list_status='L', # L=上市, D=退市, P=暂停上市
fields='ts_code,symbol,name,area,industry,market,list_date,'
'delist_date,curr_type,list_status,enname'
)
if stock_basic is not None and len(stock_basic) > 0:
print(f" 当前上市股票: {len(stock_basic)}")
# 退市股票 (用于幸存者偏差检查!)
# 如果只拉当前上市股票,回测会漏掉已退市的垃圾公司
# → 幸存者偏差 (Survivorship Bias) ← 这是实盘最大的坑之一
delisted = PRO.stock_basic(
exchange='',
list_status='D', # 退市
fields='ts_code,symbol,name,list_date,delist_date'
)
if delisted is not None and len(delisted) > 0:
print(f" 历史退市股票: {len(delisted)} 只 ← 回测必须包含!")
print(f" 退市样本 (前 5 只):")
for _, row in delisted.head(5).iterrows():
print(f" {row['ts_code']} {row['name']} "
f"{row['list_date']} ~ {row['delist_date']}")
# 行业分布
if 'industry' in stock_basic.columns:
industry_counts = stock_basic['industry'].value_counts().head(10)
print(f"\n 行业分布 Top 10:")
for ind, cnt in industry_counts.items():
print(f" {ind}: {cnt}")
except Exception as e:
print(f" 获取失败: {e}")
else:
print(" (跳过 — Token 未配置)")
print(" 提示: stock_basic() 可获取全市场股票列表和退市记录")
print(" 退市数据对消除幸存者偏差至关重要")
# ══════════════════════════════════════════════════════════════════════
# §5 获取财务数据 (价值因子的数据源)
# ══════════════════════════════════════════════════════════════════════
#
# 这是 Tushare 相比 AKShare 最大的优势领域 ——
# AKShare 也有部分财务接口,但覆盖度和稳定性不如 Tushare。
#
# 接口: pro.daily_basic() — 每日指标
# 所需积分: 300 分 (需升级)
# 字段: pe, pe_ttm, pb, ps, ps_ttm, dv_ratio, total_share, float_share,
# total_mv, circ_mv
#
# 接口: pro.fina_indicator() — 财务指标
# 所需积分: 600 分 (需进一步升级)
# 字段: roe, roa, grossprofit_margin, netprofit_margin, debt_to_assets 等
# =============================================================================
print("\n[§5] 获取财务数据")
if TOKEN_VALID and PRO:
try:
# daily_basic: 日频估值指标 (PE/PB/市值)
daily_basic = PRO.daily_basic(
ts_code='000001.SZ',
start_date='20240101',
end_date='20250601',
fields='ts_code,trade_date,close,pe,pe_ttm,pb,ps,total_mv,circ_mv'
)
if daily_basic is not None and len(daily_basic) > 0:
print(f" 平安银行 (000001.SZ) 日频估值: {len(daily_basic)}")
print(f" 字段: {list(daily_basic.columns)}")
print(f" 最新 PE_TTM / PB / 市值:")
latest = daily_basic.iloc[0]
print(f" PE(TTM): {latest.get('pe_ttm', 'N/A')}")
print(f" PB: {latest.get('pb', 'N/A')}")
print(f" 总市值: {latest.get('total_mv', 'N/A')} 万元")
print(f"\n ⚠ 注意: 如果返回的 pe_ttm 都是 NaN, 说明积分不足 (需 300 分)")
else:
print(" 未获取到数据 (积分可能不足)")
except Exception as e:
print(f" 获取失败: {e}")
print(f" 提示: daily_basic 需要 300 积分,注册仅送 120 分")
else:
print(" (跳过 — Token 未配置)")
print(" 提示: Tushare 的 daily_basic() 和 fina_indicator() 可获取")
print(" PE/PB/ROE/ROA/毛利率/资产负债率等基本面数据")
print(" 这些都是价值因子 (Value Factor) 的核心输入")
# ══════════════════════════════════════════════════════════════════════
# §6 获取指数成分股权重
# ══════════════════════════════════════════════════════════════════════
#
# 接口: pro.index_weight() — 指数成分股权重
# 所需积分: 120 分
# 用途: 组合优化 (Black-Litterman 的市场均衡权重)
# =============================================================================
print("\n[§6] 获取指数成分股权重")
if TOKEN_VALID and PRO:
try:
# 沪深300 成分股权重 (每月更新)
index_weights = PRO.index_weight(
index_code='000300.SH',
start_date='20240101',
end_date='20250601',
)
if index_weights is not None and len(index_weights) > 0:
print(f" 沪深300 成分股权重: {len(index_weights)} 条记录")
print(f" 字段: {list(index_weights.columns)}")
# 最新一期 Top 10
latest_date = index_weights['trade_date'].max() if 'trade_date' in index_weights.columns else None
if latest_date is not None:
latest_weights = index_weights[
index_weights['trade_date'] == latest_date
].nlargest(10, 'weight') if 'weight' in index_weights.columns else None
if latest_weights is not None and len(latest_weights) > 0:
print(f"\n {latest_date} 前 10 大权重股:")
for _, row in latest_weights.iterrows():
print(f" {row['con_code']} {row.get('con_name', '')}: "
f"{row['weight']:.2%}")
else:
print(" 未获取到数据")
except Exception as e:
print(f" 获取失败: {e}")
else:
print(" (跳过 — Token 未配置)")
print(" 提示: index_weight() 可获取沪深300/中证500等指数的成分股权重")
print(" 这是 Black-Litterman 模型市场均衡组合的必需输入")
# ══════════════════════════════════════════════════════════════════════
# §7 获取行业分类 (申万行业)
# ══════════════════════════════════════════════════════════════════════
#
# 申万行业分类是中国 A 股投研最常用的行业分类标准。
# 在组合优化中 (demo_05), 行业约束需要知道每只股票属于哪个行业。
#
# 接口: pro.index_classify() — 申万行业分类
# 所需积分: 120 分
# =============================================================================
print("\n[§7] 获取行业分类")
if TOKEN_VALID and PRO:
try:
# 获取申万一级行业分类 (level='L1')
sw_classify = PRO.index_classify(
level='L1',
src='SW2021' # 申万2021版行业分类
)
if sw_classify is not None and len(sw_classify) > 0:
print(f" 申万一级行业: {sw_classify['industry_name'].nunique()}")
print(f" 各行业成分股数量 Top 10:")
industry_counts = sw_classify.groupby('industry_name').size().sort_values(ascending=False)
for name, cnt in industry_counts.head(10).items():
print(f" {name}: {cnt}")
else:
print(" 未获取到数据 (接口可能已变更)")
except Exception as e:
print(f" 获取失败: {e}")
print(f" 提示: 申万行业分类接口可能已更新,建议查阅最新文档")
else:
print(" (跳过 — Token 未配置)")
# ══════════════════════════════════════════════════════════════════════
# §8 数据清洗与对齐 (Tushare 特有)
# ══════════════════════════════════════════════════════════════════════
# =============================================================================
print("\n[§8] 数据清洗与对齐")
if TOKEN_VALID and PRO and 'stock_daily_data' in dir() and stock_daily_data:
# ── 缺失值分析 ──
missing = stock_price_panel.isna().sum()
print(f" 各标的缺失天数:")
for code, cnt in missing.items():
name = STOCK_POOL.get(code, code)
print(f" {code} ({name}): {cnt} 天 ({cnt/len(stock_price_panel):.1%})")
# ── ffill + 丢弃行首 ──
clean_prices = stock_price_panel.ffill().dropna(how='any')
print(f"\n 清洗前: {stock_price_panel.shape[0]}")
print(f" 清洗后: {clean_prices.shape[0]} 天 (丢弃 {stock_price_panel.shape[0] - clean_prices.shape[0]} 天)")
# ── 收益率面板 ──
returns = clean_prices.pct_change().dropna()
print(f" 收益率面板: {returns.shape}")
# ── 相关性矩阵 ──
corr = returns.corr()
print(f"\n 收益率相关性 (仅展示对角外最高相关的 3 对):")
corr_unstack = corr.where(
~np.eye(len(corr), dtype=bool)
).unstack().dropna()
top_pairs = corr_unstack.abs().nlargest(6)
for (s1, s2), val in top_pairs.items():
print(f" {s1}{s2}: {val:+.3f}")
else:
print(" (跳过 — 无数据可清洗)")
print(" 提示: 清洗流程与 demo_akshare_data.py 相同,核心原则:")
print(" 1. 只用 ffill(), 禁止 bfill()")
print(" 2. 填充后 dropna(how='any') 丢弃仍有 NaN 的行")
print(" 3. pct_change() 后再 dropna() 得到收益率面板")
# ══════════════════════════════════════════════════════════════════════
# §9 构建本地数据库
# ══════════════════════════════════════════════════════════════════════
# =============================================================================
print("\n[§9] 构建本地数据库")
DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "tushare")
os.makedirs(DATA_DIR, exist_ok=True)
if TOKEN_VALID and PRO:
saved_files = []
if 'stock_daily_data' in dir() and stock_daily_data:
# 保存清洗后价格面板
clean_path = os.path.join(DATA_DIR, "stock_price_clean.csv")
clean_prices.to_csv(clean_path)
saved_files.append(clean_path)
# 保存收益率
ret_path = os.path.join(DATA_DIR, "stock_returns.csv")
returns.to_csv(ret_path)
saved_files.append(ret_path)
if 'index_panel' in dir() and len(index_panel) > 0:
idx_path = os.path.join(DATA_DIR, "index_prices.csv")
index_panel.to_csv(idx_path)
saved_files.append(idx_path)
# 保存拉取元信息
meta_path = os.path.join(DATA_DIR, "fetch_metadata.txt")
with open(meta_path, "w", encoding="utf-8") as f:
f.write(f"数据源: Tushare Pro\n")
f.write(f"拉取时间: {datetime.now().isoformat()}\n")
f.write(f"数据范围: {START_DATE} ~ {END_DATE}\n")
f.write(f"标的数量: {len(STOCK_POOL)} 只个股, {len(INDEX_CODES)} 个指数\n")
saved_files.append(meta_path)
for f in saved_files:
print(f"{f}")
print(f"\n 数据目录: {DATA_DIR}")
print(f" 文件列表: {os.listdir(DATA_DIR) if os.path.exists(DATA_DIR) else ''}")
else:
print(" (跳过 — 无数据可保存)")
# ══════════════════════════════════════════════════════════════════════
# §10 AKShare vs Tushare 对比总结
# ══════════════════════════════════════════════════════════════════════
# =============================================================================
print("\n" + "=" * 68)
print(" §10 AKShare vs Tushare 对比总结")
print("=" * 68)
print("""
┌───────────────────┬─────────────────────┬─────────────────────┐
│ 维度 │ AKShare │ Tushare Pro │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 费用 │ 完全免费 │ 基础免费(120分) │
│ │ │ 高级需积分/捐赠 │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 注册 │ 不需要 │ 需要手机号注册 │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 数据覆盖 │ 极广 (股/基/期/外/ │ 聚焦 A 股/指数/ │
│ │ 宏/另类/舆情等) │ 基金/财务/因子 │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 数据稳定性 │ 中等 (依赖爬虫, │ 高 (独立数据源, │
│ │ 接口偶发变更) │ 专业维护) │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 基本面数据 │ 有限 (PE/PB 基本) │ 丰富 (PE/PB/ROE/ │
│ │ │ 财务报表等) │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 退市股票数据 │ 获取较难 │ stock_basic() 直接 │
│ │ │ 包含退市记录 │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 因子数据 │ 无 │ 有 (Barra 风格因子) │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 股票代码格式 │ "000001" (纯数字) │ "000001.SZ" (含市场)│
├───────────────────┼─────────────────────┼─────────────────────┤
│ 调用频率限制 │ 无硬限制 (建议礼貌) │ 有 (按积分等级) │
├───────────────────┼─────────────────────┼─────────────────────┤
│ 最佳使用场景 │ 研究探索、快速验证 │ 策略验证通过后的 │
│ │ 数据覆盖第一选择 │ 生产级数据源 │
└───────────────────┴─────────────────────┴─────────────────────┘
推荐路径:
① 研究阶段 → AKShare (零成本、快速验证想法)
② 策略定型 → Tushare (数据质量更高、接口更稳定)
③ 实盘运行 → Tushare + 本地缓存数据库 (减少 API 依赖)
关键提醒:
• AKShare 接口偶尔变更,升级版本前记得检查 changelog
• Tushare 积分用完会拒绝请求,注意剩余调用次数
• 两个数据源的收盘价可能存在微小差异 (取整/复权方式)
回测结果因此会有细微不同,这是正常的
• 长期存储请用 Parquet 格式 (比 CSV 更快更省空间):
df.to_parquet("data.parquet")
df = pd.read_parquet("data.parquet")
""")
print("=" * 68)
print(" ✓ Tushare Pro 数据获取 Demo 完成")
print("=" * 68)
print(f"""
关键收获:
1. Tushare 需要 Token (免费注册),积分系统决定可用接口范围
2. 股票代码格式为 "000001.SZ" / "600000.SH" (含交易所后缀)
3. Tushare 最大优势: 财务数据 (PE/PB/ROE) + 退市数据 + 稳定性
4. daily_basic() 和 fina_indicator() 是价值因子的核心数据源
5. index_weight() 提供 Black-Litterman 所需的市场权重
6. stock_basic(list_status='D') 是消除幸存者偏差的关键
7. 研究阶段用 AKShare定型后用 Tushare 生产化
下一步:
- 将 Tushare 拉取的数据写入 demo_04 (Alpha 因子)
用真实 PE/PB/ROE 替代合成因子
- 用真实退市记录修正回测的幸存者偏差
""")