用 DuckDB 搭建自动财报分析器:从原始 CSV 到投资报告,全程一键生成
很多金融从业者和量化爱好者每天都在重复同一套动作:打开十几张 Excel,手动核对数字,写一堆 VLOOKUP 和公式,最后拼出一份财报分析。这套流程耗时 2-3 小时,且极易出错。
今天用 DuckDB 把整个流程自动化——从原始财报数据到可分享的分析报告,全程一条 SQL 搞定,耗时从 2 小时降到 30 秒。

一、场景定义:你需要分析什么
假设你持有以下原始数据文件(CSV 格式):
income_statement/*.csv:利润表(日期、股票代码、营收、净利润、毛利率等)balance_sheet/*.csv:资产负债表(日期、股票代码、总资产、总负债、净资产等)stock_quotes/*.csv:每日行情(日期、股票代码、收盘价、成交量等)
目标输出:每只股票的季度关键指标 + 同比/环比变化 + 简单的估值判断信号。
传统方案需要用 Excel 或 Python pandas 写十几行代码来处理,而 DuckDB 可以做到"数据在哪里,SQL 就在哪里跑"——不需要先把所有文件加载到内存,直接对磁盘上的文件执行分析。
二、核心代码:一条 SQL 搞定全链路
import duckdb
from datetime import datetime
# 连接 DuckDB(内存模式,也可换成持久化数据库)
con = duckdb.connect(":memory:")
# 1. 直接读取 CSV(支持 glob,一次读多文件)
con.execute("""
CREATE TABLE income AS
SELECT * FROM read_csv_auto('data/income_statement/*.csv');
CREATE TABLE balance AS
SELECT * FROM read_csv_auto('data/balance_sheet/*.csv');
CREATE TABLE quotes AS
SELECT * FROM read_csv_auto('data/stock_quotes/*.csv');
""")
# 2. 一行 SQL 输出季度财报分析
report_sql = """
WITH quarterly AS (
SELECT
i.code,
STRFTIME(i.date, '%Y-%q') AS quarter,
AVG(i.revenue) AS revenue,
AVG(i.net_profit) AS net_profit,
AVG(i.gross_margin) AS gross_margin,
b.total_assets,
b.total_liabilities,
b.equity
FROM income i
LEFT JOIN balance b
ON i.code = b.code
AND STRFTIME(i.date, '%Y-%m') = STRFTIME(b.date, '%Y-%m')
GROUP BY i.code, quarter, b.total_assets, b.total_liabilities, b.equity
),
with_growth AS (
SELECT
*,
LAG(revenue) OVER (PARTITION BY code ORDER BY quarter) AS rev_lag1,
LAG(net_profit) OVER (PARTITION BY code ORDER BY quarter) AS profit_lag1,
LAG(gross_margin) OVER (PARTITION BY code ORDER BY quarter) AS margin_lag1,
LAG(revenue) OVER (PARTITION BY code ORDER BY quarter) AS rev_lag4,
LAG(net_profit) OVER (PARTITION BY code ORDER BY quarter) AS profit_lag4
FROM quarterly
)
SELECT
code,
quarter,
ROUND(revenue / 1e8, 2) AS revenue_亿,
ROUND(net_profit / 1e8, 2) AS net_profit_亿,
ROUND(gross_margin * 100, 1) AS gross_margin_pct,
ROUND((revenue - rev_lag1) / NULLIF(ABS(rev_lag1), 0) * 100, 1) AS qoq_rev,
ROUND((net_profit - profit_lag1) / NULLIF(ABS(profit_lag1), 0) * 100, 1) AS qoq_profit,
ROUND((revenue - rev_lag4) / NULLIF(ABS(rev_lag4), 0) * 100, 1) AS yoy_rev,
ROUND((net_profit - profit_lag4) / NULLIF(ABS(profit_lag4), 0) * 100, 1) AS yoy_profit,
ROUND(equity / NULLIF(total_assets, 0) * 100, 1) AS equity_ratio,
ROUND(net_profit / NULLIF(equity, 0) * 100, 1) AS roe_pct
FROM with_growth
ORDER BY code, quarter DESC;
"""
result = con.execute(report_sql).fetchdf()
print(result.to_string(index=False))
# 3. 一键导出为 Excel 报告
result.to_excel("财报分析_自动生成.xlsx", index=False, engine='openpyxl')
print("✅ 报告已保存")
三、关键技术点深度解析
3.1 read_csv_auto —— 零配置读取,碾压 pandas
DuckDB 的 read_csv_auto 是真正的"开箱即用"。它自动推断每一列的数据类型(整数、浮点、日期、字符串),支持 glob 模式一次性读取目录下所有匹配的文件。
对比 pandas 的传统做法:
# pandas 需要手动循环、推断、合并
import pandas as pd
import glob
files = glob.glob('data/income_statement/*.csv')
dfs = [pd.read_csv(f) for f in files]
df = pd.concat(dfs, ignore_index=True)
DuckDB 只需要一行:
SELECT * FROM read_csv_auto('data/income_statement/*.csv');
当文件达到几百 MB 甚至几个 GB 时,DuckDB 的列式存储和向量化执行优势更加明显——pandas 很可能直接 OOM,而 DuckDB 依然流畅运行。
3.2 CTE + Window Function —— 替代所有 VLOOKUP
传统 Excel 方案中,计算环比需要:
- 用 VLOOKUP 找到上个季度的数据
- 手写
(本期 - 上期) / 上期公式 - 复制公式到每一行
DuckDB 用 CTE(公共表表达式)+ LAG() 窗口函数一步到位:
LAG(revenue) OVER (PARTITION BY code ORDER BY quarter) AS rev_lag1
LAG(revenue, 1) 表示取当前行之前第 1 行的 revenue 值。配合 PARTITION BY code ORDER BY quarter,就能按股票分组、按季度排序,精确拿到"上个季度"的数据。
同理,LAG(revenue, 4) 拿到的是去年同季度的数据(同比)。
NULLIF(ABS(...), 0) 用于防止除以零的错误——当基期为 0 时返回 NULL,避免整行计算崩掉。
3.3 链式管道,内存零拷贝
DuckDB 的所有中间 CTE 都是虚拟视图,不占额外内存。只有当你调用 fetchdf() 时,结果才会真正物化到 pandas DataFrame。这意味着你可以在一条 SQL 里写 10 层 CTE,中间数据不会在内存里反复拷贝。
四、进阶:接入行情数据,生成投资建议
在基础财报分析之上,加入实时行情可以做简单的估值和信号判断:
enrich_sql = """
WITH base AS (
-- 复用上面的 quarterly + with_growth 逻辑
SELECT
code, quarter, revenue_亿, net_profit_亿,
qoq_rev, qoq_profit, yoy_rev, yoy_profit,
roe_pct
FROM (/* 上面的完整 report_sql */ sub)
),
latest_quote AS (
SELECT code, close_price
FROM quotes
WHERE date = (SELECT MAX(date) FROM quotes)
)
SELECT
b.*,
q.close_price,
CASE
WHEN b.roe_pct > 15 AND b.yoy_profit > 10 THEN '🟢 推荐关注'
WHEN b.roe_pct > 10 AND b.yoy_profit > 0 THEN '🟡 观察'
ELSE '🔴 谨慎'
END AS signal
FROM base b
JOIN latest_quote q ON b.code = q.code
ORDER BY b.code, b.quarter DESC;
"""
这段 SQL 的逻辑是:取最新日期的收盘价,与财报数据 JOIN,然后根据 ROE 和净利润同比增长率给出简单的信号标签。
五、与传统工具的对比
Excel 方案:
- 每只股票手动填公式,10 只股票 = 10 个 sheet
- 数据更新需要重新打开文件、刷新公式
- 容易出错,版本管理混乱
pandas 方案:
- 代码量多 3-5 倍
- 大文件内存压力大
- 需要手动处理 glob、类型推断、缺失值
DuckDB 方案:
- 一条 SQL 搞定全链路
- 列式存储,GB 级数据轻松处理
- 结果直接
fetchdf()进入 pandas 生态 - 可无缝集成到 Streamlit / FastAPI 等应用
六、实际落地价值
这个脚本可以:
- 每周自动跑一次 —— 替换掉你手动整理报表的时间,把 2 小时的周报工作压缩到 30 秒
- 作为数据产品嵌入投研平台 —— 成为团队的差异化功能,提升你的不可替代性
- 副业起点 —— 把这套逻辑封装成标准财报分析 SaaS,向小机构或独立投资者收费(99-299 元/月)
- 量化策略数据源 —— 作为你的因子库,为量化回测提供清洗好的财务数据
建议先把 CSV 数据准备好,在本地 Jupyter 里跑通,再考虑部署到定时任务。
学习更多 DuckDB 实战经验 → duckdblab.org