Featured image of post 用 DuckDB + GitHub Actions 搭建自动财经日报——零成本月入几千的数据产品

用 DuckDB + GitHub Actions 搭建自动财经日报——零成本月入几千的数据产品

实战拆解如何用 DuckDB + GitHub Actions 免费搭建自动财经日报系统:从数据拉取、SQL 聚合分析到自动推送 Telegram。零服务器成本,可卖给中小投资机构,月收入 3000-8000 元。

架构概览

引言:一个零成本的赚钱数据产品

你是否想过,每天自动发送一份财经日报,可以成为一门小生意?

中小私募、独立分析师、财经自媒体,每天都有整理市场数据的需求。人工操作耗时且易错,而如果你能提供一个自动化的解决方案,按月订阅收费(¥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?

这是很多初学者会问的问题。三个核心原因:

维度DuckDBPostgreSQLpandas
部署复杂度单个 .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) 防止除零报错
  • WITH CTE 让复杂查询可读性更好

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_TOKENTELEGRAM_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 格式有三大优势:

  1. 压缩率高:列式存储 + 编码压缩,体积比 CSV 小 5-10 倍
  2. schema 内嵌:每次读取无需推断类型,速度更快
  3. 下游友好: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。

八、下一步升级方向

  1. 多市场扩展:加入加密货币、期货数据,报价空间翻倍
  2. 可视化增强:生成带 K 线图的 HTML 报告,溢价更高
  3. 邮件订阅:支持 Mailchimp/SendGrid 推送,触达更广泛用户
  4. Webhook 集成:接入 Notion、Slack、飞书等工具,扩大分发渠道
  5. AI 分析增强:用 LLM 对日报数据做自然语言总结,提升专业版价值

本项目完整代码仓库已上传,包含 seed_data.py、generate_report.py 和 GitHub Actions 配置,开箱即用。本文的完整版教程(含 Tushare 数据源接入、API 封装为 FastAPI 服务的步骤)已发布在 duckdblab.org,包含更详细的步骤和更多案例。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

⚠️ 本站为独立社区项目,与 DuckDB 基金会及 DuckDB 官方项目无任何从属、背书或赞助关系。

"DuckDB" 是 DuckDB 基金会的注册商标,本站仅以事实描述方式使用该名称。

本站内容仅供教育与社区推广用途,不构成任何商业服务。