🚀 AI 一键生成 qmt 策略代码

QMT 策略开发教程:如何构建基于财务数据的行业轮动模型?

qmt | 发布时间: 2025/12/9
以下内容由 EasyQuant 生成。

问题描述

如何编写一个行业轮动策略,定期计算各行业的财务指标,并买入表现最好的行业的股票?

解决方案

这是一个非常经典的量化策略需求。行业轮动策略的核心在于:自上而下地选股,先选出基本面最好的行业,再从该行业中选出代表性股票。

策略设计思路

  1. 行业划分:使用申万一级行业(或证监会行业)作为轮动池。
  2. 评价指标:使用 ROE(净资产收益率) 作为行业景气度的评价标准。
    • 计算方法:行业ROE = $\frac{\sum(行业内成分股净利润)}{\sum(行业内成分股净资产)}$
    • 这种整体法计算可以避免小市值股票极端值对行业整体的影响。
  3. 选股逻辑
    • 每个调仓周期(如每月),计算所有行业的 ROE。
    • 选取 ROE 最高的 1 个行业。
    • 在该行业内,选取 净利润最大 的 5 只股票(代表行业龙头)进行买入。
  4. 交易执行
    • 卖出不在目标持仓列表中的股票。
    • 买入目标股票,等权重分配资金。

QMT 策略代码实现

以下是完整的 Python 策略代码。请将其复制到 QMT 的策略编辑器中。

# -*- coding: gbk -*-
import pandas as pd
import numpy as np

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置基本参数
    ContextInfo.holding_num = 5       # 每个行业买入的股票数量
    ContextInfo.rebalance_period = 20 # 调仓周期(交易日),约一个月
    ContextInfo.industry_top_n = 1    # 选取排名前几的行业
    
    # 2. 设置资金账号 (请替换为您自己的资金账号)
    # ContextInfo.set_account('您的资金账号') 
    
    # 3. 定义行业列表 (这里以申万一级行业为例,需确保客户端有板块数据)
    # 如果本地没有申万数据,可以使用 'CSRC金融业' 等证监会行业名称
    ContextInfo.industry_list = [
        '申万银行', '申万房地产', '申万医药生物', '申万食品饮料', 
        '申万电子', '申万计算机', '申万非银金融', '申万有色金属',
        '申万化工', '申万采掘', '申万钢铁', '申万公用事业'
    ]
    
    # 4. 设定回测相关参数 (仅回测有效)
    ContextInfo.set_commission(0, [0.0001, 0.001, 0.0003, 0.0003, 0.0003, 5]) # 设置费率
    ContextInfo.capital = 1000000 # 初始资金

def handlebar(ContextInfo):
    """
    K线周期运行函数
    """
    # 跳过历史K线,只在最后根K线或回测模式下运行
    # 如果是实盘,建议结合定时器使用,这里演示简单的bar驱动
    
    # 获取当前K线索引
    bar_index = ContextInfo.barpos
    
    # 检查是否达到调仓周期
    if bar_index % ContextInfo.rebalance_period != 0:
        return

    print(f"=== 开始进行行业轮动分析 (Bar: {bar_index}) ===")
    
    # 获取当前时间
    current_date = ContextInfo.get_bar_timetag(bar_index)
    current_date_str = timetag_to_datetime(current_date, '%Y%m%d')
    
    # 1. 计算各行业 ROE
    industry_scores = []
    
    for industry_name in ContextInfo.industry_list:
        # 获取行业成分股
        stock_list = ContextInfo.get_stock_list_in_sector(industry_name)
        if not stock_list:
            continue
            
        # 计算该行业的整体ROE
        roe = calculate_industry_roe(ContextInfo, stock_list, current_date_str)
        
        if roe is not None:
            industry_scores.append((industry_name, roe))
    
    # 2. 对行业按 ROE 从高到低排序
    industry_scores.sort(key=lambda x: x[1], reverse=True)
    
    if not industry_scores:
        print("未计算出有效行业数据")
        return

    # 选取排名靠前的行业
    target_industries = [x[0] for x in industry_scores[:ContextInfo.industry_top_n]]
    print(f"本期选中行业: {target_industries}, ROE: {industry_scores[0][1]:.4f}")
    
    # 3. 在选中行业中选股 (选净利润最大的龙头股)
    target_stocks = []
    for ind in target_industries:
        stocks = ContextInfo.get_stock_list_in_sector(ind)
        top_stocks = select_top_stocks(ContextInfo, stocks, current_date_str, ContextInfo.holding_num)
        target_stocks.extend(top_stocks)
        
    print(f"本期目标持仓: {target_stocks}")
    
    # 4. 执行交易
    rebalance_portfolio(ContextInfo, target_stocks)

def calculate_industry_roe(ContextInfo, stock_list, date_str):
    """
    计算行业整体ROE = 行业总净利润 / 行业总净资产
    """
    # 财务字段: 净利润(归属母公司), 股东权益(归属母公司)
    fields = ['ASHAREINCOME.net_profit_incl_min_int_inc', 'ASHAREBALANCESHEET.total_shrhldr_eqy_excl_min_int']
    
    # 获取财务数据 (注意:get_financial_data 返回数据结构较为复杂,需处理)
    # 这里取最近的一个报告期数据
    df = ContextInfo.get_financial_data(fields, stock_list, date_str, date_str)
    
    if df is None or df.empty:
        return None
        
    # 数据清洗
    # get_financial_data 在多股单时间点下返回 DataFrame,index为股票代码,columns为字段
    try:
        # 确保数据类型为数值
        df = df.apply(pd.to_numeric, errors='coerce').fillna(0)
        
        total_net_profit = df['ASHAREINCOME.net_profit_incl_min_int_inc'].sum()
        total_equity = df['ASHAREBALANCESHEET.total_shrhldr_eqy_excl_min_int'].sum()
        
        if total_equity == 0:
            return 0
            
        return total_net_profit / total_equity
        
    except Exception as e:
        # print(f"计算行业ROE出错: {e}")
        return None

def select_top_stocks(ContextInfo, stock_list, date_str, top_n):
    """
    在指定股票列表中,选取净利润最大的前N只
    """
    field = ['ASHAREINCOME.net_profit_incl_min_int_inc']
    df = ContextInfo.get_financial_data(field, stock_list, date_str, date_str)
    
    if df is None or df.empty:
        return []
        
    try:
        df = df.apply(pd.to_numeric, errors='coerce').fillna(0)
        # 按净利润降序排列
        df_sorted = df.sort_values(by='ASHAREINCOME.net_profit_incl_min_int_inc', ascending=False)
        # 取前N只股票代码
        return df_sorted.head(top_n).index.tolist()
    except:
        return []

def rebalance_portfolio(ContextInfo, target_stocks):
    """
    调仓执行函数
    """
    # 获取当前持仓
    positions = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'position')
    current_holdings = [pos.m_strInstrumentID for pos in positions if pos.m_nVolume > 0]
    
    # 1. 卖出不在目标列表中的股票
    for stock in current_holdings:
        if stock not in target_stocks:
            order_target_value(stock, 0, ContextInfo, ContextInfo.accid)
            print(f"卖出: {stock}")
            
    # 2. 买入目标股票
    if not target_stocks:
        return
        
    # 获取账户总资产 (回测用 ContextInfo.capital 估算,实盘应用 get_trade_detail_data 获取 'account')
    # 这里简单演示,假设满仓操作,每只股票等权重
    
    # 获取账户净资产
    acc_info = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'account')
    total_asset = acc_info[0].m_dBalance if acc_info else ContextInfo.capital
    
    # 每只股票的目标市值
    target_value_per_stock = total_asset / len(target_stocks)
    
    for stock in target_stocks:
        order_target_value(stock, target_value_per_stock, ContextInfo, ContextInfo.accid)
        print(f"买入/调仓: {stock}, 目标市值: {target_value_per_stock}")

代码关键点解析

  1. 数据获取 (get_financial_data)

    • 我们使用了 ASHAREINCOME.net_profit_incl_min_int_inc (净利润) 和 ASHAREBALANCESHEET.total_shrhldr_eqy_excl_min_int (归属母公司股东权益) 这两个核心字段。
    • QMT 的财务数据接口会自动匹配离 date_str 最近的已公告财报数据,避免了未来函数(即不会在财报发布前就用到数据)。
  2. 行业整体法计算

    • 代码中 calculate_industry_roe 函数并没有简单地对每只股票的 ROE 求平均,而是将行业内所有公司的净利润加总,除以所有公司的净资产加总。这在量化中被称为“整体法”,能更真实地反映行业龙头的盈利能力,避免小市值亏损股扭曲行业数据。
  3. 板块成分股 (get_stock_list_in_sector)

    • 代码中使用了 '申万银行' 等名称。注意:在实际运行前,请务必在 QMT 客户端左侧的“板块”树中确认您已下载了对应的板块数据,且板块名称与代码中的字符串一致。如果您的客户端中叫 "SW银行",请相应修改代码。
  4. 交易执行 (order_target_value)

    • 使用 order_target_value 是最方便的调仓方式。它会自动计算需要买入或卖出的差额。例如,如果目标市值是 10 万,当前持仓市值 8 万,它会自动买入 2 万。

运行前准备

  1. 补充数据
    • 在 QMT 界面上方点击【数据管理】->【补充数据】。
    • 勾选【财务数据】和【板块数据】(特别是申万行业或证监会行业板块)。
    • 下载对应回测区间的历史日线数据。
  2. 设置账号
    • init 函数中,找到 ContextInfo.set_account('您的资金账号'),取消注释并填入真实的资金账号(回测模式下通常不需要这一步,但实盘必须设置)。

常见问题排查

  • 报错 NoneType object is not subscriptable:通常是因为 get_financial_data 没有取到数据。请检查数据管理中是否已下载了财务数据。
  • 行业列表为空:请检查 ContextInfo.industry_list 中的名称是否与 QMT 客户端左侧板块栏中的名称完全一致。