为什么不能直接传完整表格
行数太多会浪费 token 与费用;噪声会掩盖关键信息;不同字段单位不清会增加误读风险;并且长文本更难检查模型到底读到了什么。
Python在金融中的应用 · 第四部分
这一节的任务是做出一条可以反复使用的数据准备管线:输入一个股票代码,程序取得行情和新闻,逐项检查数据,生成一段带有时间、来源和边界说明的文本。下一节的大模型只读取这段文本;所以,这一节决定了智能体“看到什么、漏掉什么、能否回溯”。
如果把一堆未经检查的网页文字直接交给模型,模型可能写出很流畅的解释,却没有可靠的时间、来源和价格依据。本课程把智能体拆成五个可检查的环节。任何一个环节有问题,都应先停下来修正,而不是让模型“猜一下”。
请在课程虚拟环境已经激活的前提下,新建一个文件夹,例如 finance-event-agent。在 Trae 的资源管理器中打开这个文件夹,再新建 section-11-api.ipynb。本节所有代码都写进这个 Notebook;数据、运行结果与密钥则放进不同位置。
.env 只保存本机密钥;data 与 outputs 保存可复查的文件。print("hello")。data 文件夹,用于保存原始和清洗后的数据。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__)"第一条:安装或更新课程依赖
pip,可以减少“包装进了 A 环境、Notebook 却在 B 环境运行”的问题。pip。可以理解成“请当前 Python 来执行 pip 这个工具”。site-packages 文件夹。DataFrame;清洗、筛选、收益计算都要用它。.env 文件中的环境变量。它让密钥不必写入 Python 源码。第二条:把当前环境“拍一张版本快照”
pandas==2.2.2。它不是安装命令,只是显示清单。python -m pip install -r requirements-agent.txt,尽量复现同一批依赖。requirements-agent.txt。双击它,应该能看到几十行类似 包名==版本号 的记录。这个文件可以提交到 Git;但 .env 绝对不可以提交。第三条:用一行代码验证安装位置与版本
ModuleNotFoundError,说明当前 Python 环境没有正确安装它。最常见的错误:终端检查成功,但 Notebook 仍提示 “No module named akshare”。原因通常是 Notebook 内核不是刚才安装包的 Python。解决办法是:在 Notebook 右上角重新选择同一个 Python / conda 内核,然后重启内核,再运行 import akshare as ak。
行情分析至少要回答三个问题:查的是谁、查哪个时间段、价格是否复权。下面用深市代码 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))前复权是什么意思?发生分红、送配等公司行为后,历史价格会按规则调整,使价格序列更便于计算连续收益。它适合课程中的收益率练习;但不能把前复权价格直接当作某一天真实成交价。报告中必须写出 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 或负数通常意味着错误、停牌口径或缺失编码。 | 保留问题行,记录原因后再处理。 |
模型不需要看到 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)price_window 时影响完整行情表。close_t / close_(t-1) - 1。第一行没有前一天可比较,所以结果是缺失值,这是正常现象。-1 表示倒数第一行。排序必须在此之前完成。新闻在事件驱动分析中是候选事件线索,不是因果结论。先查看接口实际返回什么字段;不要预设一定叫 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))下面的代码把“接口可能有不同字段名”写成显式检查。若某个字段不存在,先显示提示,不会直接在后面悄悄报错。
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)stock_info_global_news() 返回的是全球财经资讯流,不必然与某一只股票直接相关。课程练习可以用它学习管线;若要做公司层面的事件研究,应使用有明确公司匹配规则、可回到原文的资讯源,并记录筛选依据。大模型 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)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)为什么要逐条写“时间 / 来源”?模型不会自动知道新闻是否可信、是否发生在价格变化之前。把这两个字段写进文本,至少能让输出中保留核对线索;但仍应回到 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)调用 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)如果数据源的许可或课程要求不允许公开原始数据,请只提交代码、字段说明、脱敏后的样例和运行截图;不要把完整源数据上传到公开仓库。
先从代码名称表确认代码和市场前缀,再查询 60 个交易日行情。不要只改数字,不看接口对格式的要求。
将列名 close 临时改成不存在的名称,观察必要字段检查如何报错;然后恢复代码。理解错误信息比“复制一次成功代码”更重要。
用 Markdown 写出 date、close、volume、title、publish_time 的含义、单位、来源、是否用于 Prompt。
提交 Notebook、requirements 文件、脱敏的三份输出样例和 300 字说明:数据从哪里来、哪些字段进入了 Prompt、为什么不能据此荐股。
下一节不会重新抓数据,而是读取本节的 context,将它作为大模型的用户消息。请保留本节运行生成的文件,并确认你能解释每一段上下文来自哪张表、哪个字段、哪个时间点。