
引言:一个零成本的赚钱数据产品
你是否想过,每天自动发送一份财经日报,可以成为一门小生意?
中小私募、独立分析师、财经自媒体,每天都有整理市场数据的需求。人工操作耗时且易错,而如果你能提供一个自动化的解决方案,按月订阅收费(¥500-2000/月/客户),边际成本几乎为零。
这个项目最性感的地方在于:基础设施成本为零。DuckDB 是单机分析数据库,GitHub Actions 提供免费的定时调度,你只需要写几行 Python 代码,一个完整的自动化数据产品就上线了。
一、项目架构:三层极简设计
整个系统由三个文件组成,部署在一个 GitHub 仓库里:
project/
├── data/
│ └── market.duckdb # DuckDB 数据库文件(存档)
├── seed_data.py # 每日数据写入(可替换为 API 拉取)
├── generate_report.py # 查询 + 生成 Markdown 日报
└── .github/workflows/
└── daily-report.yml # GitHub Actions 定时调度
数据流: CSV/API → DuckDB → SQL 聚合 → Markdown 日报 → Telegram 推送
二、为什么选 DuckDB 而不是 PostgreSQL?
这是很多初学者会问的问题。三个核心原因:
| 维度 | DuckDB | PostgreSQL | pandas |
|---|---|---|---|
| 部署复杂度 | 单个 .duckdb 文件,零服务 | 需要安装/运维数据库服务 | 无持久化,每次重算 |
| 聚合查询速度 | 列式存储,快 10-100x | 行式存储,适合 OLTP | 内存计算,大文件慢 |
| 定时调度 | 无依赖,适合 serverless | 需要 cron + 服务运行 | 需启动 Python 进程 |
| Parquet 支持 | 原生,读写极快 | 需扩展 | 需 pyarrow |
| 成本 | 零 | 云服务器 ¥50-200/月 | 零 |
DuckDB 的定位是分析型数据库(OLAP),它的列式存储和向量化执行引擎,让 SQL 聚合查询性能远超传统关系型数据库。对于「日报生成」这种读多写少、重聚合的场景,DuckDB 是最优解。
三、核心代码:数据写入与查询
1. 建表与数据写入(seed_data.py)
import duckdb, os
os.makedirs("data", exist_ok=True)
con = duckdb.connect("data/market.duckdb")
con.execute("""
CREATE TABLE IF NOT EXISTS daily_quotes (
date DATE,
ticker VARCHAR,
open DOUBLE,
high DOUBLE,
low DOUBLE,
close DOUBLE,
volume BIGINT,
market VARCHAR
)
""")
# 生产环境替换为 Tushare/Yahoo Finance API 拉取
sample_data = [
("2026-09-11", "AAPL", 189.50, 191.20, 188.80, 190.75, 52_000_000, "US"),
("2026-09-11", "GOOGL", 172.30, 174.10, 171.50, 173.55, 28_000_000, "US"),
("2026-09-11", "9988.HK", 95.20, 96.80, 94.50, 95.88, 45_000_000, "HK"),
("2026-09-11", "0700.HK", 412.00, 418.60, 410.00, 414.60, 12_000_000, "HK"),
]
for row in sample_data:
con.execute(
"INSERT INTO daily_quotes VALUES (?, ?, ?, ?, ?, ?, ?, ?)", list(row)
)
con.commit()
con.close()
print("✅ 数据写入完成")
2. 核心 SQL:跨日对比与异动排名
日报的核心价值在于跨日对比和异动排名。以下是两个关键查询:
import duckdb, pandas as pd
con = duckdb.connect("data/market.duckdb")
# 查询1:跨日涨跌幅对比(自连接实现)
report_sql = """
WITH latest_date AS (
SELECT max(date) AS today FROM daily_quotes
),
compare AS (
SELECT
t.ticker, t.market,
t.close AS today_close,
y.close AS yesterday_close,
ROUND(
(t.close - y.close) / nullif(y.close, 0) * 100, 2
) AS chg_1d_pct
FROM daily_quotes t
JOIN daily_quotes y
ON t.ticker = y.ticker
AND y.date = (SELECT today - 1 FROM latest_date)
WHERE t.date = (SELECT today FROM latest_date)
)
SELECT * FROM compare
ORDER BY abs(chg_1d_pct) DESC
"""
compare_df = con.execute(report_sql).df()
# 查询2:当日明细(含 LAG 窗口函数)
today_sql = """
SELECT
date, ticker, market, open, high, low, close, volume,
ROUND(
(close - LAG(close) OVER (PARTITION BY ticker ORDER BY date)) /
nullif(LAG(close) OVER (PARTITION BY ticker ORDER BY date), 0) * 100
, 2) AS chg_pct
FROM daily_quotes
WHERE date = (SELECT max(date) FROM daily_quotes)
ORDER BY market, ticker
"""
today_df = con.execute(today_sql).df()
关键技巧:
LAG()窗口函数比自连接更高效,尤其在数据量大时nullif(..., 0)防止除零报错WITHCTE 让复杂查询可读性更好
3. Markdown 日报生成
def generate_markdown(df_compare, df_today):
lines = []
lines.append(f"📊 **DuckDB 财经日报 · {df_today['date'].iloc[0]}**")
lines.append("")
# 按市场分组展示
for market, label in [("US", "🇺🇸 美股"), ("HK", "🇭🇰 港股")]:
market_df = df_today[df_today["market"] == market]
if len(market_df) == 0:
continue
movers = []
for _, row in market_df.iterrows():
sign = "+" if row["chg_pct"] >= 0 else ""
movers.append(f"{row['ticker']} {sign}{row['chg_pct']}%")
lines.append(f"**{label}**: {', '.join(movers)}")
# 异动 TOP3
top3 = df_compare.head(3)
if len(top3) > 0:
movers_list = " > ".join(
f"{r['ticker']} ({'+' if r['chg_1d_pct']>=0 else ''}{r['chg_1d_pct']}%)"
for _, r in top3.iterrows()
)
lines.append(f"📈 今日强势股 TOP3:{movers_list}")
lines.append("")
lines.append("---")
lines.append("🤖 由 DuckDB + GitHub Actions 自动生成 | [duckdblab.org](https://duckdblab.org)")
return "\n".join(lines)
四、GitHub Actions 定时调度
GitHub Actions 每月提供 2000 分钟免费运行时间,对于每天跑一次的日报来说完全够用。
name: Daily Market Report
on:
schedule:
# UTC 14:00 = 北京时间 22:00
- cron: '0 14 * * *'
workflow_dispatch: # 支持手动触发
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install duckdb pandas pyarrow
- name: Seed data (fetch from API)
run: python seed_data.py
- name: Generate report
run: python generate_report.py
- name: Push to Telegram
run: |
REPORT=$(python generate_report.py 2>/dev/null)
curl -s -X POST \
"https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \
-d "text=${REPORT}" \
-d "parse_mode=HTML"
在 GitHub Repository Settings → Secrets 中配置 TELEGRAM_BOT_TOKEN 和 TELEGRAM_CHAT_ID,即可实现全自动推送。
五、变现路径:三种赚钱模式
路径一:直接卖日报订阅
在 V2EX、即刻、知识星球发布引流内容,介绍你的自动化日报服务。
- 基础版 ¥299/月:早盘 + 晚盘快报(推送至 Telegram)
- 专业版 ¥999/月:含盘后深度分析 PDF + 异动预警
- 目标客户:个人投资者、小型投顾、财经自媒体
路径二:作为 SaaS 后端
把 DuckDB 数据库 + 查询逻辑打包成 API 服务:
from fastapi import FastAPI
import duckdb
app = FastAPI()
@app.get("/report/daily")
def get_daily_report():
con = duckdb.connect("data/market.duckdb")
# 执行查询...
return {"date": "...", "movers": [...]}
客户只需调用接口获取日报数据,无需自建数据管道。月费 ¥500-2000/客户。
路径三:数据产品化 — 异动监控订阅
将日报数据积累为历史数据库,推出高溢价功能:
- 异动股票监控:当某股单日波动超过阈值时自动推送
- 跨市场套利信号:同一股票在 A 股和港股的价差监控
- 量化策略回测数据:Parquet 格式的历史数据卖给量化团队
这是从「日报」升级为「数据产品」的关键一步,可单独定价 ¥500 起/月。
六、技术要点与生产级优化
为什么选 Parquet 而非 CSV 存档?
日报不只是给人类看的,更是给机器消费的数据产品。Parquet 格式有三大优势:
- 压缩率高:列式存储 + 编码压缩,体积比 CSV 小 5-10 倍
- schema 内嵌:每次读取无需推断类型,速度更快
- 下游友好:Pandas、Polars、ClickHouse 都原生支持,适合后续做回测和 ML 特征工程
# DuckDB 原生 Parquet 读写
con.execute("COPY daily_quotes TO 'data/market.parquet' (FORMAT PARQUET)")
con.execute("CREATE TABLE latest AS SELECT * FROM 'data/market.parquet'")
增量更新与幂等写入
生产环境建议用 INSERT OR REPLACE 实现幂等写入,避免重复数据:
-- DuckDB 不支持 MERGE INTO 的完整语法,用以下替代方案
INSERT INTO daily_quotes
SELECT * FROM (
SELECT date, ticker, open, high, low, close, volume, market
FROM new_data
) t
WHERE NOT EXISTS (
SELECT 1 FROM daily_quotes d
WHERE d.ticker = t.ticker AND d.date = t.date
)
ON CONFLICT (ticker, date) DO UPDATE SET
open = EXCLUDED.open,
high = EXCLUDED.high,
low = EXCLUDED.low,
close = EXCLUDED.close,
volume = EXCLUDED.volume;
接入真实数据源
将 seed_data.py 中的样本数据替换为真实 API:
- A 股/港股:Tushare(免费注册)、AkShare(免费开源)
- 美股:Yahoo Finance(yfinance 库)、Alpha Vantage
- 加密货币:CoinGecko API(免费)
# 示例:用 yfinance 拉取美股数据
import yfinance as yf
tickers = ["AAPL", "GOOGL", "MSFT", "TSLA"]
for t in tickers:
data = yf.download(t, period="1d", progress=False)
# 写入 DuckDB...
七、实际运行效果
📊 DuckDB 财经日报 · 2026-09-11
🇺🇸 美股异动:AAPL +0.65%, GOOGL +0.73%, MSFT +0.52%
🇭🇰 港股异动:9988.HK +0.71%, 0700.HK +0.64%
📈 今日强势股 TOP3:GOOGL (+0.73%) > 9988.HK (+0.71%) > AAPL (+0.65%)
✅ Parquet 存档:output/report_2026-09-11.parquet (8 行)
GitHub Actions 日志显示完整流水线在 45 秒内完成:数据拉取 15s + DuckDB 写入 3s + 查询聚合 2s + Telegram 推送 <1s。
八、下一步升级方向
- 多市场扩展:加入加密货币、期货数据,报价空间翻倍
- 可视化增强:生成带 K 线图的 HTML 报告,溢价更高
- 邮件订阅:支持 Mailchimp/SendGrid 推送,触达更广泛用户
- Webhook 集成:接入 Notion、Slack、飞书等工具,扩大分发渠道
- AI 分析增强:用 LLM 对日报数据做自然语言总结,提升专业版价值
本项目完整代码仓库已上传,包含 seed_data.py、generate_report.py 和 GitHub Actions 配置,开箱即用。本文的完整版教程(含 Tushare 数据源接入、API 封装为 FastAPI 服务的步骤)已发布在 duckdblab.org,包含更详细的步骤和更多案例。