Python在金融中的应用 · 第四部分

第十一节:API、AKShare与事件数据管线

这一节的任务是做出一条可以反复使用的数据准备管线:输入一个股票代码,程序取得行情和新闻,逐项检查数据,生成一段带有时间、来源和边界说明的文本。下一节的大模型只读取这段文本;所以,这一节决定了智能体“看到什么、漏掉什么、能否回溯”。

本节完成后,你应当能够独立完成建立项目与安装依赖认识 AKShare 与 DataFrame取得并检查行情取得并检查新闻把表格转成 Prompt 上下文独立练习

先理解:智能体不是从“提问”开始

如果把一堆未经检查的网页文字直接交给模型,模型可能写出很流畅的解释,却没有可靠的时间、来源和价格依据。本课程把智能体拆成五个可检查的环节。任何一个环节有问题,都应先停下来修正,而不是让模型“猜一下”。

读取行情与新闻
检查列名、日期、空值
计算收益与摘要
压缩表格转短文本
交给模型带边界的 Prompt
金融事件驱动分析的数据管线
整条管线中,模型推理在最后。前面的数据准备工作决定了后面的文字是否可以被追溯。
课程边界:这里讨论的是信息整理与研究练习。程序不得输出买入、卖出、仓位、目标价或确定性预测;新闻与同日价格同时出现,也不能自动证明前者导致后者。

从一个干净的项目目录开始

请在课程虚拟环境已经激活的前提下,新建一个文件夹,例如 finance-event-agent。在 Trae 的资源管理器中打开这个文件夹,再新建 section-11-api.ipynb。本节所有代码都写进这个 Notebook;数据、运行结果与密钥则放进不同位置。

金融智能体项目目录和环境变量的示意
建议的项目结构。Notebook 放计算过程;.env 只保存本机密钥;data 与 outputs 保存可复查的文件。
01确认 Trae 左下角选中的是课程 Python / conda 内核。
02新建一个代码单元,先运行 print("hello")。
03创建 data 文件夹,用于保存原始和清洗后的数据。
04创建 outputs 文件夹,用于保存报告、JSON 与日志。

为什么不把所有内容放在一个 Notebook 旁边?因为后面会同时有源数据、清洗数据、模型响应和图表。分开放能避免误把密钥或临时文件上传到 Git,也便于别人复现你的流程。

逐字读懂三条准备命令

下面三条命令要在 Trae 的终端中运行,不是在 Notebook 的 Python 代码单元中运行。Notebook 单元是 Python;终端是操作系统命令行。每运行一条,先看输出是否出现错误,再进行下一条。

python -m pip install -U akshare pandas requests python-dotenv
python -m pip freeze > requirements-agent.txt
python -c "import akshare, pandas, requests; print(akshare.__version__)"

第一条:安装或更新课程依赖

python
调用当前已激活环境中的 Python 解释器。使用它而不是单独写 pip,可以减少“包装进了 A 环境、Notebook 却在 B 环境运行”的问题。
-m
让 Python 按模块方式运行后面的 pip。可以理解成“请当前 Python 来执行 pip 这个工具”。
pip
Python 的安装包管理工具。它负责从包索引下载文件,并安装到当前环境的 site-packages 文件夹。
install
告诉 pip 要安装列在后面的包;若包尚不存在,就下载并安装。
-U
upgrade 的缩写。若已有旧版本,允许 pip 更新到可获取的较新版本。更新后接口、字段有可能变化,因此后面会检查版本和列名。
akshare
金融数据接口库。本节通过它请求股票代码表、日线行情和全球财经新闻。
pandas
表格数据工具。AKShare 的结果通常就是 Pandas 的 DataFrame;清洗、筛选、收益计算都要用它。
requests
HTTP 请求工具。下一节会用它把整理后的文本发送给大模型 API;本节先理解 GET、POST、状态码与超时。
python-dotenv
读取本机 .env 文件中的环境变量。它让密钥不必写入 Python 源码。
示例输出 Collecting akshare Successfully installed akshare-1.xx.x pandas-2.xx.x requests-2.xx.x python-dotenv-1.xx.x (版本号随安装时间变化;只要没有红色 ERROR,就可进行下一步。)

第二条:把当前环境“拍一张版本快照”

python -m pip freeze
列出当前环境中已经安装的包及其确切版本,例如 pandas==2.2.2。它不是安装命令,只是显示清单。
>
终端的重定向符号。左侧原本会显示在屏幕上的文字,被写进右侧的文件,而不是打印出来。
requirements-agent.txt
生成的文本文件名。将来在另一台电脑上可以运行 python -m pip install -r requirements-agent.txt,尽量复现同一批依赖。
运行后去哪里找文件?在 Trae 左侧资源管理器刷新当前项目目录,会看到 requirements-agent.txt。双击它,应该能看到几十行类似 包名==版本号 的记录。这个文件可以提交到 Git;但 .env 绝对不可以提交。

第三条:用一行代码验证安装位置与版本

python -c
让终端直接运行引号内的一小段 Python 代码。适合做快速检查;真正的分析代码仍写在 Notebook 中。
import akshare, pandas, requests
尝试导入三个包。任何一个找不到,都会出现 ModuleNotFoundError,说明当前 Python 环境没有正确安装它。
print(...)
将圆括号内的结果显示到终端。这里只打印版本,不打印密钥或任何私有数据。
akshare.__version__
AKShare 包提供的版本属性。两侧各有两个下划线,必须完整输入;这是 Python 的特殊属性命名方式。
示例输出 1.18.40

最常见的错误:终端检查成功,但 Notebook 仍提示 “No module named akshare”。原因通常是 Notebook 内核不是刚才安装包的 Python。解决办法是:在 Notebook 右上角重新选择同一个 Python / conda 内核,然后重启内核,再运行 import akshare as ak。

AKShare 是什么,结果为什么是表格

AKShare 是一个开源的 Python 金融数据接口库。它把许多数据源的访问方式封装成 Python 函数:你调用函数,它返回一个 Pandas 表格。它不是“保证正确的数据库”,也不意味着所有接口都无需变动;每次开始分析时,都应先查看返回的列名、前几行、行数和时间范围。

AKShare 在 Jupyter Notebook 中返回代码名称表的示意
第一次调用一个新接口时,先显示 head() 和 columns。不要在没有看过字段的情况下就猜列名。

先运行下面的代码。它从 A 股代码名称表中取前五行,目的是练习“函数返回 DataFrame”这一件事,不是开始做股票分析。

import akshare as ak
import pandas as pd

stocks = ak.stock_info_a_code_name()

print("对象类型:", type(stocks))
print("表格形状(行数,列数):", stocks.shape)
print("列名:", stocks.columns.tolist())
display(stocks.head())
示例输出 对象类型: <class 'pandas.core.frame.DataFrame'> 表格形状(行数,列数): (数千行, 2) 列名: ['code', 'name'] code name 0 000001 平安银行 1 000002 万 科A 2 000004 国华网安
DataFrame
可以把它看成带列名的电子表格;每一列有名称与数据类型,每一行是一条记录。
shape
返回 (行数, 列数)。如果行数为 0,先不要继续分析,要检查接口、网络或参数。
columns.tolist()
把列名转换成普通 Python 列表,便于完整显示和复制。字段名会随接口不同而不同。
head()
默认显示前五行。它不会修改原始表格,只是让你先看清数据长什么样。
display()
Jupyter Notebook 的友好显示方式。若在普通 .py 文件中,可改用 print(stocks.head())。

取得行情:先写清股票、区间和复权口径

行情分析至少要回答三个问题:查的是谁、查哪个时间段、价格是否复权。下面用深市代码 sz000001 演示。不同接口对代码格式的要求不同,不要想当然地删掉前缀;先读该接口的官方说明。

stock_code = "sz000001"        # 本例:深市代码,前缀和数字一起传入
start_date = "20250101"        # 起始日:YYYYMMDD
end_date = "20251231"          # 结束日:YYYYMMDD
adjust = "qfq"                 # 前复权;研究记录中必须写明

price_raw = ak.stock_zh_a_daily(
    symbol=stock_code,
    start_date=start_date,
    end_date=end_date,
    adjust=adjust,
)

print("原始表格的列名:", price_raw.columns.tolist())
print("原始表格的形状:", price_raw.shape)
display(price_raw.head(3))
display(price_raw.tail(3))
示例输出(数字和日期会随数据源更新) 原始表格的列名: ['date', 'open', 'high', 'low', 'close', 'volume', 'amount', ...] 原始表格的形状: (约 240 行, 若干列) date open high low close volume 0 2025-01-02 11.42 11.58 11.35 11.51 184532100

前复权是什么意思?发生分红、送配等公司行为后,历史价格会按规则调整,使价格序列更便于计算连续收益。它适合课程中的收益率练习;但不能把前复权价格直接当作某一天真实成交价。报告中必须写出 adjust="qfq"。

把“看到表格”变成“验证表格”

required_price_cols = ["date", "open", "high", "low", "close", "volume"]
missing_cols = [c for c in required_price_cols if c not in price_raw.columns]
if missing_cols:
    raise ValueError(f"行情接口缺少必要字段:{missing_cols}")

price = price_raw.copy()
price["date"] = pd.to_datetime(price["date"], errors="coerce")
for col in ["open", "high", "low", "close", "volume"]:
    price[col] = pd.to_numeric(price[col], errors="coerce")

print("日期缺失数:", price["date"].isna().sum())
print("收盘价缺失数:", price["close"].isna().sum())
print("重复日期数:", price["date"].duplicated().sum())
print("日期范围:", price["date"].min().date(), "至", price["date"].max().date())
print("收盘价是否全部大于 0:", (price["close"] > 0).all())
示例输出 日期缺失数: 0 收盘价缺失数: 0 重复日期数: 0 日期范围: 2025-01-02 至 2025-12-31 收盘价是否全部大于 0: True
检查项为什么需要若失败,先做什么
必要字段字段改名或接口变动时,后续代码不应悄悄算错。打印全部列名,对照官方接口文档修改代码。
日期可解析不能比较或排序的日期无法构造时间序列。先查看原始值,不要直接丢弃整列。
数值可转换价格含逗号、空字符或异常符号时,收益计算会出错。统计转为缺失的行,回看源数据。
重复日期同日重复会让统计量与收益率重复计算。先判断是接口重复还是不同市场 / 时点记录。
正价格收盘价为 0 或负数通常意味着错误、停牌口径或缺失编码。保留问题行,记录原因后再处理。

计算最小的行情摘要

模型不需要看到 240 行日线。我们首先计算一日收益率,再保留最后十个交易日。这十行的作用不是预测未来,而是让模型在报告中能够引用“数据截止到哪一天、近期价格怎样变化”。

price = (
    price.dropna(subset=["date", "close"])
         .sort_values("date")
         .drop_duplicates("date")
         .reset_index(drop=True)
)

price["return_1d"] = price["close"].pct_change()
price["return_1d_pct"] = (price["return_1d"] * 100).round(2)

latest = price.iloc[-1]
previous = price.iloc[-2]
price_window = price[["date", "close", "return_1d_pct", "volume"]].tail(10).copy()

print(f"最近交易日:{latest['date'].date()}")
print(f"最新收盘价:{latest['close']:.2f}")
print(f"相对上一交易日变动:{latest['return_1d_pct']:.2f}%")
display(price_window)
示例输出 最近交易日:2025-12-31 最新收盘价:11.44 相对上一交易日变动:-1.12%
copy()
创建独立副本,避免后续修改 price_window 时影响完整行情表。
pct_change()
计算 close_t / close_(t-1) - 1。第一行没有前一天可比较,所以结果是缺失值,这是正常现象。
iloc[-1]
按行位置取最后一行;-1 表示倒数第一行。排序必须在此之前完成。
tail(10)
只取最后十行,控制后续 Prompt 长度,并让输出更易阅读。

取得新闻:先记录“新闻是什么时候、来自哪里”

新闻在事件驱动分析中是候选事件线索,不是因果结论。先查看接口实际返回什么字段;不要预设一定叫 title、publish_time 或 source。

news_raw = ak.stock_info_global_news()

print("新闻表格形状:", news_raw.shape)
print("新闻表格列名:", news_raw.columns.tolist())
display(news_raw.head(5))
示例输出 新闻表格形状: (若干行, 若干列) 新闻表格列名: ['title', 'content', 'publish_time', 'source', 'url']

下面的代码把“接口可能有不同字段名”写成显式检查。若某个字段不存在,先显示提示,不会直接在后面悄悄报错。

candidate_news_cols = ["title", "publish_time", "source", "url"]
available_news_cols = [c for c in candidate_news_cols if c in news_raw.columns]

if "title" not in available_news_cols:
    raise ValueError("新闻接口没有 title 字段;请先查看列名和官方说明。")

news = news_raw[available_news_cols].copy()
if "publish_time" in news.columns:
    news["publish_time"] = pd.to_datetime(news["publish_time"], errors="coerce")

news = news.dropna(subset=["title"]).drop_duplicates(subset=["title"])
news_window = news.head(6).copy()

print("保留字段:", available_news_cols)
print("可用于摘要的新闻数:", len(news_window))
display(news_window)
示例输出 保留字段: ['title', 'publish_time', 'source', 'url'] 可用于摘要的新闻数: 6
重要限制:stock_info_global_news() 返回的是全球财经资讯流,不必然与某一只股票直接相关。课程练习可以用它学习管线;若要做公司层面的事件研究,应使用有明确公司匹配规则、可回到原文的资讯源,并记录筛选依据。

为什么要把 DataFrame 转成文本

大模型 API 通常接收 JSON,其中的文字放在 messages 字段中。Pandas DataFrame 是 Python 内存中的表格对象,不能直接让远端模型“看见”。因此要做两件事:第一,选择对问题真正有用的少量列和行;第二,把这些事实转换为可读的字符串,并在字符串里写清数据边界。

行情和新闻表格转成提示词上下文的过程
表格不会直接进入模型。先截取、排序、标记来源,再拼接为结构化文本;这样可控制长度,也让模型知道每段数据是什么。

为什么不能直接传完整表格

行数太多会浪费 token 与费用;噪声会掩盖关键信息;不同字段单位不清会增加误读风险;并且长文本更难检查模型到底读到了什么。

为什么不能只传一句总结

只给“最近跌了 1%”会丢掉日期、价格轨迹和新闻来源。模型没有足够事实,只能用常识补全,反而更容易出现看似合理但不可验证的文字。

第一步:将行情窗口格式化为文本

price_for_prompt = price_window.copy()
price_for_prompt["date"] = price_for_prompt["date"].dt.strftime("%Y-%m-%d")
price_for_prompt = price_for_prompt.rename(columns={
    "date": "日期",
    "close": "收盘价",
    "return_1d_pct": "单日收益率(%)",
    "volume": "成交量",
})

price_text = price_for_prompt.to_string(index=False)
print(price_text)
示例输出 日期 收盘价 单日收益率(%) 成交量 2025-12-19 11.61 0.34 153004200 ... 2025-12-31 11.44 -1.12 201884700
dt.strftime
把 Pandas 日期转换成统一格式。统一时间格式有助于后续核对“新闻发布在价格变化之前还是之后”。
rename
为给模型阅读的文本换成中文列名。原始数据表仍可保留英文列名,避免后续代码受显示名称影响。
to_string(index=False)
将表格转换为一段对齐的纯文本;index=False 不显示左侧行号,避免无意义字符占用文本长度。

第二步:将新闻窗口格式化为逐条文本

def format_one_news(row):
    title = str(row["title"]).strip()
    time_text = "时间未提供"
    source_text = "来源未提供"

    if "publish_time" in row.index and pd.notna(row["publish_time"]):
        time_text = row["publish_time"].strftime("%Y-%m-%d %H:%M")
    if "source" in row.index and pd.notna(row["source"]):
        source_text = str(row["source"]).strip()

    return f"- [{time_text}] [{source_text}] {title}"

news_text = "\n".join(
    format_one_news(row) for _, row in news_window.iterrows()
)
print(news_text)
示例输出 - [2025-12-31 09:12] [财联社] 某行业发布新的监管指引…… - [2025-12-31 08:47] [证券时报] 上市公司披露年度经营数据……

为什么要逐条写“时间 / 来源”?模型不会自动知道新闻是否可信、是否发生在价格变化之前。把这两个字段写进文本,至少能让输出中保留核对线索;但仍应回到 URL 或原始页面进行验证。

第三步:组装最终上下文,而不是直接让模型自由发挥

data_cutoff = str(price["date"].max().date())
context = f"""【数据范围】
股票代码:{stock_code}
价格区间:{price["date"].min().date()} 至 {data_cutoff}
价格口径:日线,adjust={adjust}
新闻条数:{len(news_window)}

【最近十个交易日行情】
{price_text}

【最新新闻线索】
{news_text}

【使用边界】
新闻为候选事件线索;未完成公司匹配、原文核验或因果识别。
所有文字仅用于课程研究练习,不构成投资建议。
"""
print(context)
示例输出 【数据范围】 股票代码:sz000001 价格区间:2025-01-02 至 2025-12-31 价格口径:日线,adjust=qfq 新闻条数:6 ...

把数据保存下来,给下一节和未来的自己

调用 API 得到的数据可能随着时间更新;模型也可能升级。为了让报告可以复查,应将本次使用的行情窗口、新闻窗口和上下文另存为文件。这里保存的是课程所需的最小数据,不是鼓励随意复制或公开受限数据。

from pathlib import Path
from datetime import datetime

Path("data").mkdir(exist_ok=True)
Path("outputs").mkdir(exist_ok=True)

run_id = datetime.now().strftime("%Y%m%d_%H%M%S")
price_window.to_csv(f"data/{run_id}_price_window.csv", index=False, encoding="utf-8-sig")
news_window.to_csv(f"data/{run_id}_news_window.csv", index=False, encoding="utf-8-sig")
Path(f"outputs/{run_id}_context.txt").write_text(context, encoding="utf-8")

print("已保存本次运行的三个文件,编号:", run_id)
示例输出 已保存本次运行的三个文件,编号: 20260811_153025

如果数据源的许可或课程要求不允许公开原始数据,请只提交代码、字段说明、脱敏后的样例和运行截图;不要把完整源数据上传到公开仓库。

独立完成前的自检与练习

环境终端与 Notebook 的 Python 内核一致,四个依赖均能导入。
行情已写清股票代码、日期范围、复权口径,并检查字段和缺失。
新闻已显示实际列名,新闻文本含时间与来源的占位或真实值。
上下文只保留必要行列,含数据范围与“不构成投资建议”边界。
  1. 换一个股票代码

    先从代码名称表确认代码和市场前缀,再查询 60 个交易日行情。不要只改数字,不看接口对格式的要求。

  2. 故意制造一个错误

    将列名 close 临时改成不存在的名称,观察必要字段检查如何报错;然后恢复代码。理解错误信息比“复制一次成功代码”更重要。

  3. 制作一份数据字典

    用 Markdown 写出 date、close、volume、title、publish_time 的含义、单位、来源、是否用于 Prompt。

  4. 提交最小成果

    提交 Notebook、requirements 文件、脱敏的三份输出样例和 300 字说明:数据从哪里来、哪些字段进入了 Prompt、为什么不能据此荐股。

下一节从哪里继续

下一节不会重新抓数据,而是读取本节的 context,将它作为大模型的用户消息。请保留本节运行生成的文件,并确认你能解释每一段上下文来自哪张表、哪个字段、哪个时间点。