
Introduction: A Zero-Cost Money-Making Data Product
Have you ever thought that automatically sending a financial daily report could become a side business?
Small hedge funds, independent analysts, and financial content creators all need daily market data compilation. Manual work is time-consuming and error-prone. If you can provide an automated solution and charge a monthly subscription (¥500-2000/client), the marginal cost is practically zero.
The most attractive part of this project: zero infrastructure cost. DuckDB is a single-file analytical database, GitHub Actions provides free scheduled execution, and you only need a few lines of Python code to launch a complete automated data product.
Part 1: Architecture — Three-Layer Minimal Design
The entire system consists of three files, deployed in a single GitHub repository:
project/
├── data/
│ └── market.duckdb # DuckDB database file (persistence)
├── seed_data.py # Daily data ingestion (replaceable with API fetch)
├── generate_report.py # Query + Markdown report generation
└── .github/workflows/
└── daily-report.yml # GitHub Actions scheduled execution
Data flow: CSV/API → DuckDB → SQL aggregation → Markdown report → Telegram push
Part 2: Why DuckDB Instead of PostgreSQL?
This is a common question. Three core reasons:
| Dimension | DuckDB | PostgreSQL | pandas |
|---|---|---|---|
| Deployment complexity | Single .duckdb file, zero service | Requires database service installation/maintenance | No persistence, re-computes every run |
| Aggregation query speed | Columnar storage, 10-100x faster | Row-based storage, optimized for OLTP | In-memory computation, slow with large files |
| Scheduled execution | No dependencies, perfect for serverless | Needs cron + service running | Needs Python process startup |
| Parquet support | Native, extremely fast read/write | Requires extension | Needs pyarrow |
| Cost | Zero | Cloud server ¥50-200/month | Zero |
DuckDB is positioned as an analytical database (OLAP). Its columnar storage and vectorized execution engine make SQL aggregation queries far faster than traditional relational databases. For scenarios like “daily report generation” — read-heavy, write-light, aggregation-focused — DuckDB is the optimal choice.
Part 3: Core Code — Data Ingestion and Queries
1. Table Creation and Data Writing (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
)
""")
# Replace with Tushare/Yahoo Finance API in production
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("✅ Data written successfully")
2. Core SQL: Cross-Day Comparison and Movement Ranking
The core value of a daily report lies in cross-day comparison and movement ranking. Here are two key queries:
import duckdb, pandas as pd
con = duckdb.connect("data/market.duckdb")
# Query 1: Cross-day price change comparison (self-join)
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()
# Query 2: Today's details (with LAG window function)
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()
Key techniques:
LAG()window function is more efficient than self-join, especially with large datasetsnullif(..., 0)prevents division-by-zero errorsWITHCTEs make complex queries more readable
3. Markdown Report Generation
def generate_markdown(df_compare, df_today):
lines = []
lines.append(f"📊 **Financial Daily Report · {df_today['date'].iloc[0]}**")
lines.append("")
# Display grouped by market
for market, label in [("US", "🇺🇸 US Stocks"), ("HK", "🇭🇰 HK Stocks")]:
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)}")
# Top 3 movers
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"📈 Top 3 Movers: {movers_list}")
lines.append("")
lines.append("---")
lines.append("🤖 Auto-generated by DuckDB + GitHub Actions | [duckdblab.org](https://duckdblab.org)")
return "\n".join(lines)
Part 4: GitHub Actions Scheduled Execution
GitHub Actions provides 2000 free minutes per month, more than enough for a daily report running once per day.
name: Daily Market Report
on:
schedule:
# UTC 14:00 = Beijing time 22:00
- cron: '0 14 * * *'
workflow_dispatch: # Also supports manual trigger
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"
Configure TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in GitHub Repository Settings → Secrets for fully automated pushing.
Part 5: Monetization — Three Revenue Models
Model 1: Sell Report Subscriptions Directly
Promote your automated daily report service on V2EX, Jike, or Knowledge Stars.
- Basic Plan ¥299/month: Morning + evening quick brief (pushed to Telegram)
- Professional Plan ¥999/month: Includes post-market in-depth analysis PDF + movement alerts
- Target customers: Individual investors, small investment advisors, financial content creators
Model 2: Sell as a SaaS Backend
Package the DuckDB database + query logic into an API service:
from fastapi import FastAPI
import duckdb
app = FastAPI()
@app.get("/report/daily")
def get_daily_report():
con = duckdb.connect("data/market.duckdb")
# Execute queries...
return {"date": "...", "movers": [...]}
Clients call the API to get daily report data without building their own data pipeline. Monthly fee ¥500-2000/client.
Model 3: Productize as Data — Movement Monitoring Subscription
Accumulate daily report data into a historical database, then offer premium features:
- Movement Stock Monitor: Auto-push when a stock’s daily volatility exceeds a threshold
- Cross-Market Arbitrage Signals: Monitor price differences for the same stock across A-shares and HK stocks
- Quant Strategy Backtest Data: Sell Parquet-format historical data to quant teams
This is the key step from “daily report” to “data product”, priced separately at ¥500+/month.
Part 6: Production-Grade Optimization Tips
Why Parquet Instead of CSV for Archiving?
Daily reports aren’t just for human consumption — they’re data products for machines. Parquet has three major advantages:
- High compression: Columnar storage + encoding compression, 5-10x smaller than CSV
- Embedded schema: No type inference needed on each read, faster access
- Downstream friendly: natively supported by Pandas, Polars, ClickHouse — ideal for backtesting and ML feature engineering
# Native DuckDB Parquet read/write
con.execute("COPY daily_quotes TO 'data/market.parquet' (FORMAT PARQUET)")
con.execute("CREATE TABLE latest AS SELECT * FROM 'data/market.parquet'")
Idempotent Writes with Incremental Updates
In production, use conditional inserts to avoid duplicate data:
-- DuckDB alternative to 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;
Connecting Real Data Sources
Replace the sample data in seed_data.py with real APIs:
- A-shares/HK stocks: Tushare (free registration), AkShare (open source)
- US stocks: Yahoo Finance (yfinance library), Alpha Vantage
- Cryptocurrency: CoinGecko API (free)
# Example: Fetch US stock data with yfinance
import yfinance as yf
tickers = ["AAPL", "GOOGL", "MSFT", "TSLA"]
for t in tickers:
data = yf.download(t, period="1d", progress=False)
# Write to DuckDB...
Part 7: Real-World Performance
📊 Financial Daily Report · 2026-09-11
🇺🇸 US Stocks: AAPL +0.65%, GOOGL +0.73%, MSFT +0.52%
🇭🇰 HK Stocks: 9988.HK +0.71%, 0700.HK +0.64%
📈 Top 3 Movers: GOOGL (+0.73%) > 9988.HK (+0.71%) > AAPL (+0.65%)
✅ Parquet archived: output/report_2026-09-11.parquet (8 rows)
GitHub Actions logs show the complete pipeline finishes in 45 seconds: data fetch 15s + DuckDB write 3s + query aggregation 2s + Telegram push <1s.
Part 8: Next Steps for Upgrade
- Multi-market expansion: Add cryptocurrency and futures data, double your pricing space
- Visual enhancement: Generate HTML reports with candlestick charts for higher premium
- Email subscription: Support Mailchimp/SendGrid for wider reach
- Webhook integration: Connect to Notion, Slack, Feishu to expand distribution channels
- AI analysis boost: Use LLM to generate natural language summaries of report data, increasing professional plan value
The complete code repository is uploaded, including seed_data.py, generate_report.py, and GitHub Actions configuration — ready to use out of the box. The full tutorial (including Tushare data source integration and FastAPI service packaging) is published at duckdblab.org with more detailed steps and additional cases.