Featured image of post Building an A-Share Sector Rotation Monitoring System with DuckDB

Building an A-Share Sector Rotation Monitoring System with DuckDB

Build a complete A-Share stock sector rotation monitoring system using DuckDB, akshare, and SQL window functions. From data ingestion to momentum calculation to automated report delivery — all in under 200 lines of code. Package it as a service product starting at

System Architecture Overview

The core data flow is simple:

Data Collection (akshare) → DuckDB Local Storage → SQL Indicator Calculation → Signal Generation → Auto Push

Everything runs on your Linux server (or any machine you have), orchestrated by cron — fully automated.


Step 1: Environment Setup & Data Collection

Install Dependencies

pip install duckdb akshare pandas

akshare is a free, open-source A-Share data API. No API key, no payment required.

Fetch Sector Data and Load into DuckDB

Create a Python script that connects to DuckDB and downloads sector data:

import akshare as ak
import duckdb
import pandas as pd
from datetime import datetime, timedelta

# Connect to DuckDB (creates a local file database)
con = duckdb.connect("sector_monitor.db")

# Get Shenwan tier-1 industry sector list
sector_df = ak.stock_board_industry_name_em()
print(f"Monitoring {len(sector_df)} industry sectors")

# Get data for the last 90 calendar days (~60 trading days)
end_date = datetime.now().strftime("%Y%m%d")
start_date = (datetime.now() - timedelta(days=90)).strftime("%Y%m%d")

# Create the main table
con.execute("""
 CREATE TABLE IF NOT EXISTS sector_daily (
 date DATE,
 sector VARCHAR,
 open DOUBLE,
 close DOUBLE,
 high DOUBLE,
 low DOUBLE,
 volume BIGINT,
 amount BIGINT,
 amplitude DOUBLE,
 change_pct DOUBLE,
 change_amount DOUBLE,
 turnover_rate DOUBLE
 )
""")

# Test with first 5 sectors
for _, row in sector_df.head(5).iterrows():
 sector_name = row["板块名称"]
 try:
 df = ak.stock_board_industry_hist_em(
 symbol=sector_name,
 start_date=start_date,
 end_date=end_date,
 period="daily",
 adjust="qfq"
 )
 if df.empty:
 continue
 df["sector"] = sector_name
 
 # Register DataFrame as temp table and write to DuckDB
 con.register("df_tmp", df)
 con.execute("""
 INSERT INTO sector_daily 
 SELECT date, sector, open, close, high, low, 
 volume, amount, amplitude, change_pct, 
 change_amount, turnover_rate
 FROM df_tmp
 """)
 print(f" ✓ {sector_name}: {len(df)} records")
 except Exception as e:
 print(f" ✗ {sector_name}: {e}")

💡 Note: Full collection of all 31 Shenwan tier-1 sectors takes about 2 minutes. In production, separate data collection from computation and schedule collection after market close.


Step 2: Calculate Core Momentum Indicators with SQL

Once the data is in DuckDB, the real analysis begins. We calculate three momentum indicators using window functions:

IndicatorMeaningCalculation
5-Day MomentumShort-term trendCumulative return over last 5 trading days
20-Day MomentumMedium-term trendCumulative return over last 20 trading days (~1 month)
60-Day MomentumLong-term trendCumulative return over last 60 trading days (~1 quarter)
-- Create momentum view
CREATE OR REPLACE VIEW sector_momentum AS
WITH daily_return AS (
 SELECT
 sector,
 date,
 (close - LAG(close) OVER (PARTITION BY sector ORDER BY date)) 
 / NULLIF(LAG(close) OVER (PARTITION BY sector ORDER BY date), 0) AS daily_ret
 FROM sector_daily
),
momentum AS (
 SELECT
 sector,
 MAX(date) AS latest_date,
 -- 5-day cumulative return (geometric compounding)
 EXP(SUM(LN(1 + COALESCE(daily_ret, 0))) 
 OVER (PARTITION BY sector ORDER BY date 
 ROWS BETWEEN 4 PRECEDING AND CURRENT ROW)) - 1 AS ret_5d,
 -- 20-day cumulative return
 EXP(SUM(LN(1 + COALESCE(daily_ret, 0))) 
 OVER (PARTITION BY sector ORDER BY date 
 ROWS BETWEEN 19 PRECEDING AND CURRENT ROW)) - 1 AS ret_20d,
 -- 60-day cumulative return
 EXP(SUM(LN(1 + COALESCE(daily_ret, 0))) 
 OVER (PARTITION BY sector ORDER BY date 
 ROWS BETWEEN 59 PRECEDING AND CURRENT ROW)) - 1 AS ret_60d,
 -- 20-day average trading amount (capital activity gauge)
 AVG(amount) OVER (PARTITION BY sector ORDER BY date 
 ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS avg_amount_20d
 FROM daily_return
)
SELECT DISTINCT
 sector,
 ret_5d,
 ret_20d,
 ret_60d,
 avg_amount_20d,
 -- Composite momentum score: short-term trend weighted highest
 ret_5d * 0.5 + ret_20d * 0.3 + ret_60d * 0.2 AS momentum_score
FROM momentum
WHERE date = (SELECT MAX(date) FROM daily_return)
ORDER BY momentum_score DESC;

Why Geometric Compounding?

If a sector goes up 10% one day and down 10% the next, simple addition gives 0%, but the actual return is (1+0.1)×(1-0.1)-1 = -1%. Using LN and EXP for geometric compounding accurately captures compounding effects — especially significant in high-volatility markets.

DuckDB’s window function performance shines here. Processing 31 sectors × 60 trading days ≈ 1,860 rows completes in under 0.1 seconds. MySQL 5.7 wouldn’t even support this OVER (PARTITION BY ... ROWS BETWEEN ...) syntax.


Step 3: Generate Trading Signals

With momentum scores calculated, we rank all sectors and generate buy/hold/sell signals:

-- Ranking and signal generation
CREATE OR REPLACE VIEW sector_signals AS
WITH ranked AS (
 SELECT *,
 ROW_NUMBER() OVER (ORDER BY momentum_score DESC) AS rank_asc,
 ROW_NUMBER() OVER (ORDER BY momentum_score ASC) AS rank_desc
 FROM sector_momentum
)
SELECT
 sector,
 ROUND(ret_5d * 100, 2) AS ret_5d_pct,
 ROUND(ret_20d * 100, 2) AS ret_20d_pct,
 ROUND(momentum_score * 100, 2) AS score,
 ROUND(avg_amount_20d / 1e8, 1) AS avg_amount_hundred_million,
 CASE 
 WHEN rank_asc <= 5 THEN '🔥 Strong Momentum'
 WHEN rank_asc <= 15 THEN '⚡ Moderate Momentum'
 WHEN rank_desc <= 5 THEN '❄️ Avoid / Weak'
 ELSE '➡️ Neutral'
 END AS signal,
 CASE
 WHEN rank_asc <= 5 AND avg_amount_20d > 1e10 THEN 'BUY'
 WHEN rank_desc <= 5 THEN 'SELL / AVOID'
 ELSE 'HOLD / WATCH'
 END AS action
FROM ranked
ORDER BY rank_asc;

Key logic:

  • BUY signal: Top 5 momentum + 20-day average amount > ¥10 billion (price-volume confirmation)
  • SELL signal: Bottom 5 momentum
  • HOLD signal: Everything else

Step 4: Auto-Generate Push Reports

This is where the system creates real value. Use DuckDB to build report text directly, then push via Telegram Bot / Feishu Webhook / Email:

# Generate report text directly from DuckDB
report = con.execute("""
SELECT 
 strftime(current_date, '%Y-%m-%d') || ' A-Share Sector Rotation Daily' AS title,
 '---' AS sep1,
 '🔥 Top 5 Strong Sectors:' AS section1,
 string_agg(
 ' ' || sector || ' | 5d: ' || ret_5d_pct || '% | Score: ' || score || ' | Signal: ' || action,
 chr(10)
 ) FILTER (WHERE rank_asc <= 5) AS top_sectors,
 '---' AS sep2,
 '❄️ Bottom 5 Weak Sectors:' AS section2,
 string_agg(
 ' ' || sector || ' | 20d: ' || ret_20d_pct || '% | Signal: ' || action,
 chr(10)
 ) FILTER (WHERE rank_desc <= 5) AS bottom_sectors,
 '---' AS sep3,
 '💡 Trading Advice:' AS section3,
 CASE
 WHEN count(*) FILTER (WHERE action = 'BUY') >= 3 
 THEN 'Market sentiment is optimistic. Watch for pullback entries in strong sectors.'
 WHEN count(*) FILTER (WHERE action = 'BUY') = 0 
 THEN 'No clear buy signals. Consider waiting or monitoring defensive sectors.'
 ELSE 'Structural market. Focus on sectors with sustained momentum.'
 END AS advice
FROM sector_signals
""").fetchone()

report_text = '\n'.join([str(r) for r in report if r])
print(report_text)

Sample Output

2026-05-31 A-Share Sector Rotation Daily
---
🔥 Top 5 Strong Sectors:
 Computer | 5d: 3.25% | Score: 2.18 | Signal: BUY
 Electronics | 5d: 2.87% | Score: 1.95 | Signal: BUY
 Telecommunications | 5d: 2.12% | Score: 1.56 | Signal: BUY
 Media | 5d: 1.89% | Score: 1.32 | Signal: HOLD
 Defense | 5d: 1.65% | Score: 1.08 | Signal: HOLD
---
❄️ Bottom 5 Weak Sectors:
 Real Estate | 20d: -4.23% | Signal: SELL
 Building Materials | 20d: -3.87% | Signal: SELL
 Beauty & Care | 20d: -3.12% | Signal: SELL
 Food & Beverage | 20d: -2.56% | Signal: SELL
 Agriculture | 20d: -2.01% | Signal: SELL
---
💡 Trading Advice:
Market sentiment is optimistic. Watch for pullback entries in strong sectors.

Telegram Push Code

def send_telegram(bot_token, chat_id, text):
 import requests
 url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
 requests.post(url, json={"chat_id": chat_id, "text": text})

Combine with cron to deliver to your paid subscribers every morning before market open.


Step 5: Deployment & Operations

Create the orchestration script run_sector_monitor.sh:

#!/bin/bash
cd /path/to/project
python3 collect_data.py # Data collection
python3 compute_signals.py # Indicator computation
python3 send_report.py # Push delivery

Configure cron:

# Collect data at 15:30 (after market close)
30 15 * * 1-5 /path/to/run_sector_monitor.sh >> /var/log/sector_monitor.log 2>&1
# Push report at 08:30 (before market open)
30 8 * * 1-5 /path/to/send_report.py >> /var/log/sector_push.log 2>&1

Why DuckDB is the Best Choice for Sector Rotation Analysis

Throughout this project, several DuckDB advantages stand out:

1. Zero Configuration From pip install duckdb to running your first SQL query: under 30 seconds. No database server setup, no connection strings, no configuration files. The database is a single file.

2. Full Window Function Support Sector rotation analysis lives and breathes window functions — LAG for daily returns, SUM OVER ROWS BETWEEN for cumulative returns, ROW_NUMBER for rankings. DuckDB’s SQL support rivals PostgreSQL and far exceeds MySQL 5.7 or SQLite.

3. Vectorized Execution Engine 1,860 rows × window functions completes in under 0.1 seconds. When you scale to 10× that volume with historical data, Pandas starts hitting memory limits. DuckDB’s columnar vectorized engine processes data in batches, using memory far more efficiently.

4. Seamless Python Integration con.register("df_tmp", df) — one line of code for zero-copy data transfer between Pandas DataFrames and DuckDB tables. Data fetched via akshare goes straight into the database.


Conclusion

This tutorial built a complete A-Share sector rotation monitoring system from scratch. The core code is under 200 lines, with only 50 lines of SQL powering the entire quantitative engine.

The project covers the full stack: data collection (Python + akshare), data storage (DuckDB file database), analytical computation (SQL window functions), automation (cron), and information delivery (Telegram/Feishu API).

Most importantly, it’s **inherently **. A ¥299/month SaaS tool can be replicated in a single afternoon with DuckDB. This is the minimal viable product for data analysts looking to their DuckDB skills.


🔍 Looking for the complete project code with push modules, multi-factor extensions, backtesting scripts, and Docker deployment? Find the full guide at duckdblab.org with production-ready setup instructions.


本文信息

项目内容
DuckDB 版本v1.5.x(部分功能基于 v2.0 Preview)
最后验证2026-09-12
测试环境Linux / x86_64 / 16GB RAM
官方文档DuckDB Documentation
GitHubpengzz9527/duckdb-blog

如发现错误,欢迎通过 GitHub Issue 或邮件 [email protected] 反馈。

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials