Why Sector Rotation Monitoring is a Monetizable Project
The A-Share market has a fundamental truth: no sector goes up forever, but rotation creates constant opportunities. Data from 2024-2026 shows that quarterly return spreads between Shenwan tier-1 industry sectors regularly exceed 40%. Picking the right sector matters more than picking the right stock.
Commercial sector rotation SaaS tools on the market charge ¥299-999/month ($42-$140). Using DuckDB + free data sources (akshare), you can build a more powerful, customized private version in under an hour. Use it for your own quantitative trading signals or package it as a data product for clients.
This tutorial will walk you through building a complete automated sector rotation monitoring system from scratch.
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:
| Indicator | Meaning | Calculation |
|---|---|---|
| 5-Day Momentum | Short-term trend | Cumulative return over last 5 trading days |
| 20-Day Momentum | Medium-term trend | Cumulative return over last 20 trading days (~1 month) |
| 60-Day Momentum | Long-term trend | Cumulative 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.
Monetization Roadmap
The basic sector rotation monitor is already a shippable product. To command higher prices, layer on these features:
1. Multi-Factor Scoring
Beyond pure momentum, incorporate additional factors:
CREATE OR REPLACE VIEW multi_factor_score AS
SELECT
m.sector,
m.momentum_score * 0.3 + -- Momentum factor
v.volume_change * 0.2 + -- Volume change factor
p.price_stability * 0.2 + -- Price stability factor
r.relative_strength * 0.3 -- Relative strength factor
AS composite_score
FROM sector_momentum m
JOIN sector_volume v USING (sector)
JOIN sector_stability p USING (sector)
JOIN sector_rel_strength r USING (sector);
2. Historical Backtesting Engine
Validate your strategy with DuckDB’s blazing-fast historical analysis:
-- Backtest: weekly rebalance, buy top 3 momentum sectors
WITH weekly_rank AS (
SELECT
date,
sector,
momentum_score,
ROW_NUMBER() OVER (PARTITION BY date ORDER BY momentum_score DESC) AS rnk
FROM sector_daily_momentum
WHERE dayofweek(date) = 5 -- Every Friday rebalance
)
SELECT
sector,
COUNT(*) AS hold_weeks,
AVG(ret_20d) AS avg_return,
STDDEV(ret_20d) AS volatility,
AVG(ret_20d) / NULLIF(STDDEV(ret_20d), 0) AS sharpe_ratio
FROM weekly_rank
WHERE rnk <= 3
GROUP BY sector
ORDER BY sharpe_ratio DESC;
Run 3 years of backtesting in under 30 seconds — something that would take minutes on traditional databases.
3. SaaS Pricing Tiers
| Tier | Price | Features |
|---|---|---|
| Basic | ¥99/mo ($14) | Daily sector rotation push + Top/Bottom 5 |
| Pro | ¥299/mo ($42) | Basic + Multi-factor scoring + Stock picks |
| Enterprise | ¥999/mo ($140) | Pro + Backtest reports + Custom factors |
A single low-end cloud server (2 vCPU, 4GB RAM, ~$7/mo) can easily support 100+ subscribers. The margin is exceptional.
4. Additional Data Source Integration
| Source | Purpose | Access |
|---|---|---|
| Northbound Capital | Foreign capital flows | akshare.stock_hsgt_north_net_flow_in_em |
| Top Traders (LHB) | Hot money tracking | akshare.stock_lhb_yy_em |
| Margin Trading | Leverage sentiment | akshare.stock_margin_detail_szse |
| Index Futures Basis | Market sentiment | akshare.futures_main_sina |
Cross-table JOIN in DuckDB fuses all these signals into a single composite score.
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 monetizable. 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 monetize 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.
