量化投资体系之二:为 Web 看板集成公众号/财经原始数据

先看效果图





教程:

教程目标 :在现有 web_app.py 的「数据收集 → 公众号/财经」Tab 中,集成腾讯实时行情、AkShare 原始指标、药师宏观原始数据,无需二次计算,纯展示原始数据。
前置知识 :Python 基础、会用 pip install、能看懂 DataFrame 列名
涉及技术:Streamlit、pandas、requests、mootdx(已有代码复用)


一、背景与数据说明

1.1 为什么做这个?

web_app.py 的「数据收集 → 公众号/财经」Tab 原本只有一个占位文字,没有实际数据:

python 复制代码
# 旧代码(第863行附近)
with tab_d4:
    st.subheader("📱 公众号/财经数据")
    st.info("药师等公众号数据、腾讯财经、同花顺、东方财富(待集成)")

同时,系统中已有 3 个完整的数据采集脚本:

  • yaoshi_api.py --- 药师API(股债收益差、汇率等)
  • akshare_backup.py --- AkShare(沪深300PE、国债10年、融资融券)
  • tencent_realtime.py --- 腾讯财经实时行情

但这些数据各自存在不同格式的 Excel/CSV 文件里,web_app 没有统一读取方式。本次任务是新建一个统一读取层,让 web_app 的各 Tab 可以标准化的方式读取数据。

1.2 最终效果预览

截图文件位置: 开发记录/2026年05月04日,公众号财经数据集成/效果图/

文件名 内容 数据验证
01_首页.png 投资看板首页 主页正常加载
02_数据收集全貌.png 数据收集Tab(含通达信/自选股等) 各子Tab正常
03_公众号财经默认.png 公众号财经Tab(切换前) 占位文字已替换
04_腾讯实时.png 主要指数+ETF列表 上证4112.16(+1.37%) 沪深300 4807.31(+0.76%) 创业板3677.15(+3.83%)
05_AkShare.png 沪深300PE+国债10年+融资融券 PE=13.85 日期2026-04-21
06_药师宏观.png 股债收益差+汇率 股债差=5.11% 百分位=48.2% 日期2026-04-23

上传到 CSDN 时,将上述截图拖入编辑器即可获得 URL,再替换到 markdown 中。


二、环境准备

2.1 安装依赖

已有脚本依赖,检测是否已安装:

bash 复制代码
pip show streamlit pandas requests openpyxl

缺少的安装:

bash 复制代码
pip install streamlit pandas openpyxl requests

2.2 目录结构

复制代码
02_股票金融分析/
├── 07_信息研究/
│   └── market_data/
│       └── reader.py        ← 新建:统一读取层
├── 08_知识智能体/
│   └── web_app.py           ← 修改:tab_d4 重构
└── 股市各类数据获取/
    ├── tencent_realtime.py  ← 已有:腾讯实时
    ├── akshare_backup.py    ← 已有:AkShare指标
    └── yaoshi_api.py         ← 已有:药师宏观

数据来源:
F:\吾股丰登\观察标的数据库\
├── HS300PE_history.xlsx           # 沪深300PE
├── china_bond_10y_history.xlsx    # 国债10年
├── margin_history_full.xlsx       # 融资融券
├── stock_bond_yield_history.xlsx # 股债收益差
├── exchange_rate_history.xlsx     # 汇率
└── market_sentiment_history.xlsx  # 市场情绪

三、第一步:新建统一读取层 reader.py

3.1 为什么需要读取层?

F:\吾股丰登\观察标的数据库\ 下的 Excel 文件列名是中文乱码格式(``、PE_TTM 这种),每个文件的列顺序也不一致。如果每个 web_app Tab 都自己读文件、做列名映射,代码会非常冗余。

统一读取层的职责:

  1. 读取文件
  2. 标准化列名(乱码 → 中文)
  3. 标准化日期格式(YYYY-MM-DD
  4. 不做任何计算(只做数据搬运)

3.2 核心代码

路径:07_信息研究/market_data/reader.py

python 复制代码
# -*- coding: utf-8 -*-
"""
市场数据统一读取层
路径: F:/吾股丰登/观察标的数据库/
职责: 把不同格式文件标准化为一致的 DataFrame
原则: 只做列名标准化和日期格式化,不做指标计算
"""

import os
import warnings
from pathlib import Path
from typing import Optional

import pandas as pd

BASE_DIR = Path(r"F:\吾股丰登\观察标的数据库")
warnings.filterwarnings("ignore", category=DeprecationWarning)


def _read_excel(file: Path, usecols: Optional[list] = None) -> pd.DataFrame:
    """读取 Excel,不存在则返回空 DataFrame"""
    if not file.exists():
        return pd.DataFrame()
    try:
        return pd.read_excel(file, usecols=usecols)
    except Exception:
        return pd.DataFrame()

3.3 AkShare 数据读取函数

python 复制代码
# 沪深300 PE-TTM
def read_hs300_pe() -> pd.DataFrame:
    """文件: HS300PE_history.xlsx → [日期, PE_TTM, PE_STATIC]"""
    file = BASE_DIR / "HS300PE_history.xlsx"
    df = _read_excel(file)
    if df.empty:
        return df
    df = df.rename(columns={df.columns[0]: "日期", df.columns[1]: "PE_TTM"})
    if len(df.columns) >= 3:
        df = df.rename(columns={df.columns[2]: "PE_STATIC"})
    df["日期"] = pd.to_datetime(df["日期"], errors="coerce")
    return df.sort_values("日期").reset_index(drop=True)


# 国债10年收益率
def read_bond_10y() -> pd.DataFrame:
    """文件: china_bond_10y_history.xlsx → [日期, 国债10年]"""
    file = BASE_DIR / "china_bond_10y_history.xlsx"
    df = _read_excel(file)
    if df.empty:
        return df
    df = df.rename(columns={df.columns[0]: "日期", df.columns[1]: "国债10年"})
    df["日期"] = pd.to_datetime(df["日期"], errors="coerce")
    return df.sort_values("日期").reset_index(drop=True)


# 融资融券
def read_margin() -> pd.DataFrame:
    """文件: margin_history_full.xlsx → [日期, 融资余额, 融券余额, ...]"""
    file = BASE_DIR / "margin_history_full.xlsx"
    df = _read_excel(file)
    if df.empty:
        return df
    rename = {df.columns[0]: "日期", df.columns[1]: "融资余额", df.columns[2]: "融券余额"}
    df = df.rename(columns=rename)
    df["日期"] = pd.to_datetime(df["日期"], errors="coerce")
    return df.sort_values("日期").reset_index(drop=True)

3.4 药师宏观数据读取函数

python 复制代码
# 股债收益差
def read_stock_bond_yield() -> pd.DataFrame:
    """文件: stock_bond_yield_history.xlsx
    → [日期, PE_TTM, PE百分位, 国债10年, 股债收益差, 股债收益差百分位, 沪深300点位]"""
    file = BASE_DIR / "stock_bond_yield_history.xlsx"
    df = _read_excel(file)
    if df.empty:
        return df
    col_names = {
        df.columns[0]: "日期", df.columns[1]: "PE_TTM", df.columns[2]: "PE百分位",
        df.columns[3]: "国债10年", df.columns[4]: "股债收益差",
        df.columns[5]: "股债收益差百分位", df.columns[6]: "沪深300点位",
    }
    df = df.rename(columns=col_names)
    df["日期"] = pd.to_datetime(df["日期"], errors="coerce")
    return df.sort_values("日期").reset_index(drop=True)


# 人民币汇率中间价
def read_exchange_rate() -> pd.DataFrame:
    """文件: exchange_rate_history.xlsx → [日期, 汇率, 百分位, 沪深300点位]"""
    file = BASE_DIR / "exchange_rate_history.xlsx"
    df = _read_excel(file)
    if df.empty:
        return df
    col_names = {
        df.columns[0]: "日期", df.columns[1]: "汇率",
        df.columns[2]: "百分位", df.columns[3]: "沪深300点位",
    }
    df = df.rename(columns=col_names)
    df["日期"] = pd.to_datetime(df["日期"], errors="coerce")
    return df.sort_values("日期").reset_index(drop=True)


# 市场情绪
def read_market_sentiment() -> pd.DataFrame:
    """文件: market_sentiment_history.xlsx → [日期, 收盘价, 成交额, 成交额_20日均]"""
    file = BASE_DIR / "market_sentiment_history.xlsx"
    df = _read_excel(file)
    if df.empty:
        return df
    col_names = {
        df.columns[0]: "日期", df.columns[1]: "收盘价",
        df.columns[2]: "成交额", df.columns[3]: "成交额_20日均",
    }
    df = df.rename(columns=col_names)
    df["日期"] = pd.to_datetime(df["日期"], errors="coerce")
    return df.sort_values("日期").reset_index(drop=True)

3.5 腾讯实时行情(HTTP 实时获取)

python 复制代码
def read_tencent_realtime(symbols: list[str]) -> dict:
    """
    获取腾讯财经实时行情(HTTP,需联网)
    symbols: 如 ['sh000300', 'sz399006', 'sh510300']
    返回: {sym: {名称, 代码, 价格, 涨跌幅, 成交额_万元}}
    """
    import requests

    if not symbols:
        return {}

    # 清除代理(否则内地网络无法访问 qt.gtimg.cn)
    for k in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]:
        if k in os.environ:
            del os.environ[k]

    codes = ",".join(symbols)
    try:
        r = requests.get(
            f"https://qt.gtimg.cn/q={codes}",
            timeout=10,
            proxies={"http": None, "https": None},
        )
    except Exception:
        return {}

    results = {}
    for line in r.text.split(";"):
        if "=" not in line:
            continue
        key, val = line.split("=", 1)
        sym = key.replace("v_", "")
        parts = val.replace('"', "").split("~")

        if len(parts) < 40:
            continue

        results[sym] = {
            "名称": parts[1] if len(parts) > 1 else "",
            "代码": parts[2] if len(parts) > 2 else "",
            "价格": float(parts[3]) if parts[3] else 0,
            "涨跌幅": float(parts[38]) if parts[38] else 0,
            "成交额_万元": float(parts[37]) if parts[37].replace(".", "").replace("-", "").isdigit() else 0,
        }

    return results

3.6 辅助函数

python 复制代码
def get_latest_row(df: pd.DataFrame) -> dict:
    """返回 DataFrame 最后一行,无数据则返回空 dict"""
    if df is None or df.empty:
        return {}
    row = df.iloc[-1]
    return {col: row[col] for col in df.columns}


def fmt_date(dt) -> str:
    """标准化日期格式为 YYYY-MM-DD 字符串"""
    if pd.isna(dt):
        return ""
    if isinstance(dt, str):
        return dt[:10]
    return pd.to_datetime(dt).strftime("%Y-%m-%d")

四、第二步:修改 web_app.py 的 tab_d4

4.1 添加 import

web_app.py 顶部(约第28~40行)增加两段 import:

python 复制代码
# 统一市场数据读取层(新增)
sys.path.insert(0, str(Path(__file__).parent.parent / "07_信息研究" / "market_data"))
from reader import (
    read_hs300_pe, read_bond_10y, read_margin,
    read_stock_bond_yield, read_exchange_rate, read_market_sentiment,
    read_tencent_realtime, get_latest_row, fmt_date,
)

# 腾讯实时行情(已有脚本,引用)
STOCK_DATA_DIR = Path(__file__).parent.parent / "股市各类数据获取"
sys.path.insert(0, str(STOCK_DATA_DIR))
import tencent_realtime as tr_realtime

4.2 重构 tab_d4

替换原有的占位文字(第863~865行):

python 复制代码
# 旧代码
with tab_d4:
    st.subheader("📱 公众号/财经数据")
    st.info("药师等公众号数据、腾讯财经、同花顺、东方财富(待集成)")

替换为三子Tab结构:

python 复制代码
with tab_d4:
    st.subheader("📱 公众号/财经数据(原始数据,非计算指标)")

    sub_a, sub_b, sub_c = st.tabs(["📈 腾讯实时", "💹 AkShare", "🔗 药师宏观"])

    # ── 子Tab1: 腾讯实时行情 ─────────────────────────────────────────
    with sub_a:
        col_rt1, col_rt2 = st.columns([1, 4])
        with col_rt1:
            refresh_rt = st.button("🔄 刷新", use_container_width=True)
        with col_rt2:
            st.caption("数据源: qt.gtimg.cn(需联网)")

        st.markdown("**主要指数**")
        idx_data = tr_realtime.get_major_indices()
        if idx_data:
            idx_rows = [{
                "代码": sym, "名称": d['名称'],
                "价格": f"{d['价格']:.2f}",
                "涨跌幅": f"{d['涨跌幅']:+.2f}%",
                "PE": f"{d['PE']:.2f}" if d['PE'] else "N/A",
                "成交额(万)": f"{d['成交额_万元']:,.0f}",
            } for sym, d in idx_data.items()]
            st.dataframe(pd.DataFrame(idx_rows), use_container_width=True, hide_index=True)
        else:
            st.warning("无法获取指数数据(网络问题或未开盘)")

        st.markdown("**ETF 列表**")
        etf_data = tr_realtime.get_etf_list()
        if etf_data:
            etf_rows = [{
                "代码": sym, "名称": d['名称'], "价格": f"{d['价格']:.3f}",
                "涨跌幅": f"{d['涨跌幅']:+.2f}%", "成交额(万)": f"{d['成交额_万元']:,.0f}",
            } for sym, d in sorted(etf_data.items(), key=lambda x: -x[1]['成交额_万元'])]
            st.dataframe(pd.DataFrame(etf_rows), use_container_width=True, hide_index=True)
            if refresh_rt:
                try:
                    tr_realtime.save_daily_snapshot()
                    st.success("快照已保存")
                except Exception as e:
                    st.warning(f"保存失败: {e}")

    # ── 子Tab2: AkShare 原始指标 ─────────────────────────────────────
    with sub_b:
        df_pe = read_hs300_pe()
        st.markdown("**沪深300 PE-TTM**")
        if not df_pe.empty:
            last = df_pe.iloc[-1]
            c1, c2, c3 = st.columns(3)
            c1.metric("最新PE", f"{last['PE_TTM']:.2f}")
            c2.metric("日期", fmt_date(last['日期']))
            st.dataframe(df_pe[['日期', 'PE_TTM']].tail(10).pipe(
                lambda x: x.assign(日期=x['日期'].apply(fmt_date))
            ).sort_values('日期', ascending=False).reset_index(drop=True),
                use_container_width=True, hide_index=True)

        df_bond = read_bond_10y()
        st.markdown("**国债10年收益率**")
        if not df_bond.empty:
            last_b = df_bond.iloc[-1]
            st.metric("最新利率", f"{last_b['国债10年']:.4f}%")
            st.dataframe(df_bond.tail(10).pipe(
                lambda x: x.assign(日期=x['日期'].apply(fmt_date))
            ).sort_values('日期', ascending=False).reset_index(drop=True),
                use_container_width=True, hide_index=True)

        df_mg = read_margin()
        st.markdown("**融资融券**")
        if not df_mg.empty:
            last_mg = df_mg.iloc[-1]
            st.metric("融资余额", f"{last_mg['融资余额']/10000:.2f}万亿")
            disp = df_mg[['日期', '融资余额', '融券余额']].tail(10).copy()
            disp['融资余额'] = disp['融资余额'].apply(lambda x: f"{x/10000:.2f}万亿")
            disp['融券余额'] = disp['融券余额'].apply(lambda x: f"{x/10000:.4f}万亿")
            disp['日期'] = disp['日期'].apply(fmt_date)
            st.dataframe(disp.sort_values('日期', ascending=False).reset_index(drop=True),
                use_container_width=True, hide_index=True)

    # ── 子Tab3: 药师宏观原始指标 ─────────────────────────────────────
    with sub_c:
        df_sb = read_stock_bond_yield()
        st.markdown("**股债收益差**")
        if not df_sb.empty:
            last_sb = df_sb.iloc[-1]
            c1, c2, c3, c4 = st.columns(4)
            c1.metric("股债差", f"{last_sb['股债收益差']:.2f}%")
            c2.metric("百分位", f"{last_sb['股债收益差百分位']:.1f}%")
            c3.metric("PE(TTM)", f"{last_sb['PE_TTM']:.2f}")
            c4.metric("日期", fmt_date(last_sb['日期']))
            disp = df_sb[['日期', 'PE_TTM', '国债10年', '股债收益差', '股债收益差百分位']].tail(10)
            disp['日期'] = disp['日期'].apply(fmt_date)
            st.dataframe(disp.sort_values('日期', ascending=False).reset_index(drop=True),
                use_container_width=True, hide_index=True)

        df_fx = read_exchange_rate()
        st.markdown("**人民币汇率(USD/CNY)**")
        if not df_fx.empty:
            last_fx = df_fx.iloc[-1]
            c1, c2, c3 = st.columns(3)
            c1.metric("汇率", f"{last_fx['汇率']:.4f}")
            c2.metric("百分位", f"{last_fx['百分位']:.1f}%")
            c3.metric("日期", fmt_date(last_fx['日期']))
            disp = df_fx[['日期', '汇率', '百分位', '沪深300点位']].tail(10)
            disp['日期'] = disp['日期'].apply(fmt_date)
            st.dataframe(disp.sort_values('日期', ascending=False).reset_index(drop=True),
                use_container_width=True, hide_index=True)

五、第三步:验证

5.1 语法检查

bash 复制代码
python -m py_compile 07_信息研究/market_data/reader.py
python -m py_compile 08_知识智能体/web_app.py

5.2 独立运行 reader 测试

bash 复制代码
cd D:\python学习历程\自制项目\02_股票金融分析
python 07_信息研究/market_data/reader.py

输出:

复制代码
=== market_data reader 测试 ===
[OK] 沪深300PE: 5111条, 最新=2026-04-21, 最新值=13.8500
[OK] 国债10年: 6072条, 最新=2026-04-24, 最新值=1.7601
[OK] 融资融券: 3296条, 最新=2026-04-29, 最新值=27124.5532
[OK] 股债收益差: 576条, 最新=2026-04-23, 最新值=14.5500
[OK] 汇率: 2014条, 最新=2026-04-23, 最新值=6.8674
[OK] 市场情绪: 5154条, 最新=2026-04-23, 最新值=6271.1736

=== 腾讯实时行情测试 ===
  [OK] sh000300: 沪深300 价格=4807.31 涨跌幅=+0.76%
  [OK] sz399006: 创业板指 价格=3677.15 涨跌幅=+3.83%
  [OK] sh000001: 上证指数 价格=4112.16 涨跌幅=+1.37%

5.3 启动 Web 界面验证

bash 复制代码
cd 08_知识智能体
streamlit run web_app.py --server.port 8503

浏览器打开 http://localhost:8503 → 左侧导航选「📥 数据收集」→ 切换到「📱 公众号/财经」→ 三个子 Tab 均能正常显示数据。

5.4 验证结果表格

指标 数据源 预期值 实测值 状态
沪深300PE行数 HS300PE_history.xlsx >5000 5111
国债10年行数 china_bond_10y_history.xlsx >5000 6072
融资融券行数 margin_history_full.xlsx >3000 3296
股债收益差行数 stock_bond_yield_history.xlsx >500 576
汇率行数 exchange_rate_history.xlsx >2000 2014
市场情绪行数 market_sentiment_history.xlsx >5000 5154
腾讯实时-沪深300 qt.gtimg.cn 价格>0 4807.31
腾讯实时-创业板 qt.gtimg.cn 价格>0 3677.15
reader.py语法 py_compile 无错误 无错误
web_app.py语法 py_compile 无错误 无错误

六、常见问题汇总(Q&A)

# 问题现象 根本原因 解决方案
Q1 NameError: name 'os' is not definedread_tencent_realtime reader.py 顶部忘记 import os,但函数体内用了 os.environ 在文件顶部加 import os
Q2 Excel 列名显示为乱码(``、PE_TTM openpyxl 默认编码与 Excel 文件编码不一致 warnings.filterwarnings("ignore") 抑制警告;列名用 df.columns[0] 位置访问而非字面值
Q3 腾讯实时无法获取(返回空 dict) Windows 代理设置干扰内地访问 qt.gtimg.cn 函数开头清除所有代理环境变量:for k in ['HTTP_PROXY', 'HTTPS_PROXY', ...]: del os.environ[k]
Q4 融资融券数据量级异常(显示几万亿) 原始数据单位是元,不是亿元 读取后除以 10000 转为万亿:f"{融资余额/10000:.2f}万亿"
Q5 web_app 启动时报 ModuleNotFoundError web_app.py 中跨目录 import 没有配套 sys.path.insert 在 import 语句前加 sys.path.insert(0, str(目标目录))
Q6 日期格式不统一(有时 2026-04-21,有时 2026/04/21 不同 Excel 文件的日期格式不一致 统一用 pd.to_datetime() + strftime("%Y-%m-%d") 转换为字符串
Q7 read_stock_bond_yield() 数据条数较少(576条 vs 5000+) 股债收益差数据更新频率低(周频),不是每天更新 正常现象,确认最新日期是最新即可

七、核心经验总结

  1. 跨目录 import 必须配套 sys.path.insertweb_app.py 引用 07_信息研究/market_data/reader.py 时,必须在 import 前加路径注入,不能依赖隐式当前目录。

  2. Excel 列名乱码用位置访问 :不用字面值匹配,改用 df.columns[0] / df.columns[1] 按位置重命名,避免乱码问题。

  3. 内地 HTTP 请求必须清除代理:Windows 系统代理会干扰 requests 访问 qt.gtimg.cn,在请求前删除所有代理环境变量。

  4. 统一读取层只做标准化,不做计算reader.py 的职责是列名映射和日期格式化,不做百分位计算、指标推导等二次处理(留给 web_app 的风险控制 Tab)。

  5. 数据单位必须确认:融资余额原始数据是"元",转"万亿"要除以 10000;成交额原始数据是"元",转"亿"要除以 1 亿,不能搞混。


八、数据架构图

复制代码
数据采集脚本(独立运行,按日更新)
  yaoshi_api.py      → stock_bond_yield_history.xlsx / exchange_rate_history.xlsx
  akshare_backup.py  → HS300PE_history.xlsx / china_bond_10y_history.xlsx / margin_history_full.xlsx
  tencent_realtime.py → HTTP实时 qt.gtimg.cn
         ↓
  统一读取层 07_信息研究/market_data/reader.py
         ↓
  web_app.py tab_d4(展示原始数据,不计算)
  web_app.py tab_rk1~rk5(展示计算后的分析指标)
相关推荐
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_13:多媒体嵌入 —— 视频与音频
前端·css·笔记·ui·html·音视频
Karl_wei1 小时前
LangChain Agent 实战接入
aigc·agent·ai编程
之歆1 小时前
DAY12_CSS3选择器全攻略 + 盒子新特性完全指南(上)
前端·css·css3
之歆2 小时前
DAY13_CSS3进阶完全指南 —— 背景、边框、文本、渐变、滤镜与 Web 字体(上)
前端·c#·css3
幸福巡礼2 小时前
【 LangChain 1.2 实战(四)】构建一个模块化的天气查询 Agent
java·前端·langchain
小满zs3 小时前
Next.js精通SEO第四章(JSON-LD + web vitals)
前端·seo·next.js
怕浪猫10 小时前
决定命运的,从来不是市场,而是你看待市场的方式
aigc·ai编程
云水一下10 小时前
从零开始!VMware安装Fedora Workstation 44桌面系统完整教程
前端
小碗细面11 小时前
13种Agent、129套设计系统:Open Design 开源项目完全指南
aigc·ai编程