🚀 AI 一键生成 qmt 策略代码

如何在 QMT 中实现 Fama-French 三因子选股模型?(附源码)

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

问题描述

请帮我实现一个 Fama-French 三因子选股模型

解决方案

这是一个基于 Fama-French 三因子模型(Fama-French Three-Factor Model) 的 QMT 量化选股策略实现。

策略逻辑说明

Fama-French 三因子模型认为股票的超额收益可以由三个因子解释:

  1. 市场因子 (MKT):市场整体风险(在选股策略中通常作为基准,不直接用于排序)。
  2. 规模因子 (SMB - Small Minus Big):小市值股票倾向于跑赢大市值股票。
    • 选股指标:总市值 (Market Value),越小越好。
  3. 价值因子 (HML - High Minus Low):高账面市值比(Book-to-Market Ratio)的股票倾向于跑赢低账面市值比的股票。
    • 选股指标:市净率 (PB),越低越好(因为 PB = 1 / BM)。

策略执行流程:

  1. 股票池:选取中证500(000905.SH)成分股作为基础池。
  2. 过滤:剔除停牌股、ST股、涨跌停股票。
  3. 数据获取:获取“总市值”和“市净率(PB)”数据。
  4. 打分排序
    • 对市值进行从小到大排序(小市值优先)。
    • 对 PB 进行从小到大排序(低估值优先)。
    • 将两个排名的名次相加得到总分,总分越低越好。
  5. 调仓:每月第一个交易日进行调仓,买入排名靠前的 N 只股票,等权重持有。

QMT 策略代码

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

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置账号 (请替换为您自己的资金账号)
    ContextInfo.accid = 'YOUR_ACCOUNT_ID' 
    ContextInfo.set_account(ContextInfo.accid)
    
    # 2. 策略参数设置
    ContextInfo.target_num = 20       # 持仓股票数量
    ContextInfo.index_code = '000905.SH' # 股票池:中证500
    ContextInfo.rebalance_period = 'monthly' # 调仓周期
    
    # 3. 记录上一次调仓的月份,用于控制月度调仓
    ContextInfo.last_month = -1
    
    print("Fama-French 三因子策略初始化完成")

def handlebar(ContextInfo):
    """
    K线周期运行函数
    """
    # 获取当前K线的时间
    bar_time = ContextInfo.get_bar_timetag(ContextInfo.barpos)
    current_date_str = timetag_to_datetime(bar_time, '%Y%m%d')
    current_month = int(current_date_str[4:6])
    
    # --- 1. 判断是否触发调仓 (每月调仓一次) ---
    # 如果当前月份与上一次调仓月份相同,则跳过
    if current_month == ContextInfo.last_month:
        return
    
    # 更新调仓月份标记
    ContextInfo.last_month = current_month
    print(f"开始进行月度调仓,当前日期: {current_date_str}")

    # --- 2. 获取股票池与过滤 ---
    # 获取指数成分股
    stock_list = ContextInfo.get_sector(ContextInfo.index_code)
    
    # 过滤停牌、ST股
    valid_stocks = filter_stocks(ContextInfo, stock_list)
    if not valid_stocks:
        print("无有效股票,跳过本期调仓")
        return

    # --- 3. 获取因子数据 (市值 和 PB) ---
    # 使用 QMT 的 get_factor_data 接口获取多因子数据
    # 字段说明:
    # Valuation_and_Market_Cap.MktValue : 总市值
    # Valuation_and_Market_Cap.PB       : 市净率
    fields = ['Valuation_and_Market_Cap.MktValue', 'Valuation_and_Market_Cap.PB']
    
    # 获取数据 (注意:get_factor_data 返回的数据格式需处理)
    factor_data = ContextInfo.get_factor_data(fields, valid_stocks, current_date_str, current_date_str)
    
    # 将数据转换为 DataFrame 格式方便计算
    # 此时 factor_data 可能是一个 DataFrame (index=stock, columns=fields)
    if not isinstance(factor_data, pd.DataFrame) or factor_data.empty:
        print("因子数据获取失败")
        return
        
    df = factor_data.copy()
    
    # --- 4. 数据清洗与因子计算 ---
    # 去除空值
    df.dropna(inplace=True)
    # 剔除 PB <= 0 的股票 (通常为亏损或资不抵债)
    df = df[df['Valuation_and_Market_Cap.PB'] > 0]
    
    if df.empty:
        return

    # --- 5. 因子打分 (Rank) ---
    # 规模因子(SMB): 市值越小越好 -> 升序排名 (rank越小越好)
    df['rank_size'] = df['Valuation_and_Market_Cap.MktValue'].rank(ascending=True)
    
    # 价值因子(HML): PB越低越好 (即BM越高越好) -> 升序排名 (rank越小越好)
    df['rank_value'] = df['Valuation_and_Market_Cap.PB'].rank(ascending=True)
    
    # 综合打分: 等权重相加
    df['total_score'] = df['rank_size'] + df['rank_value']
    
    # 按总分从小到大排序,取前 N 只
    df.sort_values(by='total_score', ascending=True, inplace=True)
    target_list = df.index[:ContextInfo.target_num].tolist()
    
    print(f"本期选中股票 ({len(target_list)}只): {target_list}")

    # --- 6. 执行交易 ---
    rebalance_portfolio(ContextInfo, target_list)

def filter_stocks(ContextInfo, stock_list):
    """
    过滤停牌和ST股票
    """
    valid_list = []
    for code in stock_list:
        # 1. 剔除停牌
        if ContextInfo.is_suspended_stock(code):
            continue
            
        # 2. 剔除 ST 股 (通过名称判断)
        name = ContextInfo.get_stock_name(code)
        if 'ST' in name or '退' in name:
            continue
            
        valid_list.append(code)
    return valid_list

def rebalance_portfolio(ContextInfo, target_list):
    """
    调仓执行函数:卖出不在目标池的,买入目标池的
    """
    # 获取当前持仓
    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 code in current_holdings:
        if code not in target_list:
            # 目标价值设为 0 即为清仓
            order_target_value(code, 0, ContextInfo, ContextInfo.accid)
            print(f"卖出清仓: {code}")
    
    # 2. 买入逻辑:对目标列表股票进行等权重买入
    if len(target_list) > 0:
        # 计算每只股票的目标仓位比例 (1.0 / 股票数量)
        # 注意:实盘中可能需要预留少量现金防止手续费不够,这里设为 0.98 总仓位
        target_weight = 0.98 / len(target_list)
        
        for code in target_list:
            # 使用 order_target_percent 自动计算股数
            order_target_percent(code, target_weight, ContextInfo, ContextInfo.accid)
            print(f"调仓买入: {code}, 目标权重: {target_weight:.2%}")

代码关键点解析

  1. 数据获取 (get_factor_data)

    • 我们使用了 QMT 内置的因子库 Valuation_and_Market_Cap
    • MktValue 代表总市值,用于衡量规模因子。
    • PB 代表市净率,用于衡量价值因子(低 PB 对应高价值)。
    • 注意:使用此接口前,请确保在 QMT 客户端的【数据管理】中下载了对应的“多因子数据”。
  2. 排序逻辑 (rank)

    • Fama-French 模型的核心是相对排名。我们没有使用绝对阈值(例如 PB < 1),而是使用 rank() 函数对全市场(或股票池)进行排名。
    • total_score = rank_size + rank_value 是一种简单有效的多因子合成方法。
  3. 调仓控制

    • 代码通过 current_monthContextInfo.last_month 的对比,实现了月度调仓。只有在月份发生变化(即每个月的第一根 K 线)时才会触发交易逻辑。
  4. 交易执行 (order_target_percent)

    • 使用 order_target_percent 是最简便的调仓方式,它会自动计算当前持仓与目标权重的差额,并自动下单买入或卖出。

如何在 QMT 中运行

  1. 新建策略:在 QMT 策略编辑器中新建一个 Python 策略。
  2. 粘贴代码:将上述代码完整复制进去。
  3. 修改账号:将 init 函数中的 'YOUR_ACCOUNT_ID' 替换为您真实的资金账号。
  4. 补充数据
    • 点击菜单栏的【数据管理】->【补充数据】。
    • 勾选【多因子数据】,下载对应时间段的数据。
  5. 回测/运行
    • 回测:设置好回测时间段(例如过去3年),周期选择“日线”,点击回测。
    • 实盘:将运行模式切换为“实盘”或“模拟”,点击运行。

常见问题 (Q&A)

Q: 为什么使用 PB 而不是 BM (账面市值比)?
A: 在 QMT 的因子库中,直接提供了 PB (市净率)。PB 是 BM 的倒数。Fama-French 原文中是买入 High BM 的股票,这等同于买入 Low PB 的股票。逻辑是一致的。

Q: 为什么回测时没有交易?
A: 请检查以下几点:

  1. 是否下载了“多因子数据”?(这是最常见的原因)。
  2. ContextInfo.accid 是否设置正确(回测模式下通常不需要真实账号,但代码中调用了 set_account,建议保留)。
  3. 回测的基准指数(如中证500)是否有成分股数据。

Q: 如何加入市场因子 (MKT)?
A: 在纯选股策略中,市场因子通常体现为“全额投资”或“持有高 Beta 股票”。上述代码默认满仓操作(0.98仓位),实际上已经暴露在市场风险下,符合三因子模型在多头端的应用。如果您想做多空对冲(Alpha 策略),则需要做空对应的股指期货。