04-行业景气查询

申万行业行情、轮动、估值查询。支撑中间段行业分析与景气度跟踪。

行业列表

SELECT industry_code, industry_name, industry_level, parent_code
FROM dim_industry
WHERE industry_level = 1
ORDER BY industry_code;

申万一级行业共 31 个,代码以 80 开头。

行业日线行情

from db import get_connection
from query_utils import query_to_df, quality_filter

conn = get_connection()
df = query_to_df(conn, f"""
    SELECT trade_date, open, close, high, low, volume, amount, pct_change
    FROM industry_daily
    WHERE industry_code = ?
      AND trade_date >= date('now', '-1 year')
      AND {quality_filter()}
    ORDER BY trade_date
""", ("801080",))  # 电子行业

行业均线与趋势

df["ma20"] = df["close"].rolling(20).mean()
df["ma60"] = df["close"].rolling(60).mean()
df["trend"] = df["close"] > df["ma60"]  # 简单趋势判断

行业涨跌幅排名

近期表现

-- 近 20 个交易日行业涨跌幅排名
WITH recent AS (
    SELECT industry_code, trade_date, close,
           ROW_NUMBER() OVER (PARTITION BY industry_code ORDER BY trade_date DESC) AS rn
    FROM industry_daily
    WHERE data_quality IN ('ok', 'manual_fixed')
      AND trade_date >= date('now', '-40 days')
),
ranked AS (
    SELECT
        r1.industry_code,
        d.industry_name,
        r1.close AS latest_close,
        r20.close AS close_20d_ago,
        (r1.close / r20.close - 1) * 100 AS pct_20d
    FROM recent r1
    JOIN recent r20 ON r1.industry_code = r20.industry_code AND r20.rn = 20
    JOIN dim_industry d ON r1.industry_code = d.industry_code
    WHERE r1.rn = 1
)
SELECT industry_name, pct_20d
FROM ranked
ORDER BY pct_20d DESC;

多周期对比

-- 近 5 日 / 20 日 / 60 日 / 250 日涨跌幅
WITH closes AS (
    SELECT
        d.industry_name,
        t.industry_code,
        MAX(CASE WHEN t.trade_date = date('now', '-5 days') THEN t.close END) AS c5,
        MAX(CASE WHEN t.trade_date = date('now', '-20 days') THEN t.close END) AS c20,
        MAX(CASE WHEN t.trade_date = date('now', '-60 days') THEN t.close END) AS c60,
        MAX(CASE WHEN t.trade_date = date('now', '-250 days') THEN t.close END) AS c250,
        MAX(CASE WHEN t.trade_date = (SELECT MAX(trade_date) FROM industry_daily) THEN t.close END) AS cnow
    FROM industry_daily t
    JOIN dim_industry d ON t.industry_code = d.industry_code
    WHERE t.data_quality IN ('ok', 'manual_fixed')
    GROUP BY d.industry_name, t.industry_code
)
SELECT
    industry_name,
    (cnow/c5 - 1) * 100 AS pct_5d,
    (cnow/c20 - 1) * 100 AS pct_20d,
    (cnow/c60 - 1) * 100 AS pct_60d,
    (cnow/c250 - 1) * 100 AS pct_250d
FROM closes
ORDER BY pct_20d DESC;

行业轮动分析

近期强势行业变化

-- 对比近 20 日与近 60 日的强势行业
WITH r20 AS (
    SELECT industry_code, industry_name,
           (latest/close_20d_ago - 1) * 100 AS pct_20d
    FROM (
        SELECT
            t.industry_code, d.industry_name,
            MAX(CASE WHEN t.trade_date = (SELECT MAX(trade_date) FROM industry_daily) THEN t.close END) AS latest,
            MAX(CASE WHEN t.trade_date = date('now', '-20 days') THEN t.close END) AS close_20d_ago
        FROM industry_daily t
        JOIN dim_industry d ON t.industry_code = d.industry_code
        WHERE t.data_quality IN ('ok', 'manual_fixed')
        GROUP BY t.industry_code, d.industry_name
    )
)
SELECT industry_name, pct_20d,
       RANK() OVER (ORDER BY pct_20d DESC) AS rank_20d
FROM r20
ORDER BY pct_20d DESC
LIMIT 10;

行业间相关性

import pandas as pd

# 获取所有行业日线
df = query_to_df(conn, """
    SELECT d.industry_name, t.trade_date, t.pct_change
    FROM industry_daily t
    JOIN dim_industry d ON t.industry_code = d.industry_code
    WHERE t.data_quality IN ('ok', 'manual_fixed')
      AND t.trade_date >= date('now', '-1 year')
""")

pivot = df.pivot(index="trade_date", columns="industry_name", values="pct_change")
corr = pivot.corr()
# 高相关行业对
print(corr.unstack().sort_values(ascending=False).drop_duplicates().head(20))

行业估值查询

行业指数本身没有直接估值数据,可通过成分股加权计算:

-- 行业成分股加权 PE(简化版,用算术平均)
SELECT
    d.industry_name,
    AVG(s.pe_ttm) AS avg_pe,
    AVG(s.pb) AS avg_pb,
    COUNT(*) AS stock_count
FROM dim_stock st
JOIN stock_daily s ON st.stock_code = s.stock_code
JOIN dim_industry d ON st.industry_code = d.industry_code
WHERE s.trade_date = (SELECT MAX(trade_date) FROM stock_daily)
  AND s.pe_ttm > 0
  AND s.pe_ttm < 200  -- 排除异常值
  AND s.data_quality IN ('ok', 'manual_fixed')
GROUP BY d.industry_name
ORDER BY avg_pe;

注意:算术平均有局限性,完整实现应按市值加权。

行业资金流向

-- 行业成交量变化(代理资金流向)
SELECT
    d.industry_name,
    AVG(CASE WHEN t.trade_date >= date('now', '-5 days') THEN t.amount END) AS avg_amount_5d,
    AVG(CASE WHEN t.trade_date >= date('now', '-20 days') AND t.trade_date < date('now', '-5 days') THEN t.amount END) AS avg_amount_20d,
    AVG(CASE WHEN t.trade_date >= date('now', '-5 days') THEN t.amount END) /
    AVG(CASE WHEN t.trade_date >= date('now', '-20 days') AND t.trade_date < date('now', '-5 days') THEN t.amount END) - 1 AS amount_change
FROM industry_daily t
JOIN dim_industry d ON t.industry_code = d.industry_code
WHERE t.data_quality IN ('ok', 'manual_fixed')
GROUP BY d.industry_name
HAVING amount_change IS NOT NULL
ORDER BY amount_change DESC;

成交量放大可能意味着资金关注度提升,结合估值位置判断是机会还是风险。

封装函数建议

def get_industry_list(conn, level: int = 1) -> pd.DataFrame:
    """获取行业列表。"""

def get_industry_daily(conn, industry_code: str, days: int = 250) -> pd.DataFrame:
    """获取行业日线。"""

def get_industry_ranking(conn, periods: list = [5, 20, 60, 250]) -> pd.DataFrame:
    """多周期行业涨跌幅排名。"""

def get_industry_valuation(conn, industry_code: str) -> pd.DataFrame:
    """行业成分股加权估值。"""