问题描述
解决方案
这是一个基于 Fama-French 三因子模型(Fama-French Three-Factor Model) 的 QMT 量化选股策略实现。
策略逻辑说明
Fama-French 三因子模型认为股票的超额收益可以由三个因子解释:
- 市场因子 (MKT):市场整体风险(在选股策略中通常作为基准,不直接用于排序)。
- 规模因子 (SMB - Small Minus Big):小市值股票倾向于跑赢大市值股票。
- 选股指标:总市值 (Market Value),越小越好。
- 价值因子 (HML - High Minus Low):高账面市值比(Book-to-Market Ratio)的股票倾向于跑赢低账面市值比的股票。
- 选股指标:市净率 (PB),越低越好(因为 PB = 1 / BM)。
策略执行流程:
- 股票池:选取中证500(000905.SH)成分股作为基础池。
- 过滤:剔除停牌股、ST股、涨跌停股票。
- 数据获取:获取“总市值”和“市净率(PB)”数据。
- 打分排序:
- 对市值进行从小到大排序(小市值优先)。
- 对 PB 进行从小到大排序(低估值优先)。
- 将两个排名的名次相加得到总分,总分越低越好。
- 调仓:每月第一个交易日进行调仓,买入排名靠前的 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%}")
代码关键点解析
-
数据获取 (
get_factor_data):- 我们使用了 QMT 内置的因子库
Valuation_and_Market_Cap。 MktValue代表总市值,用于衡量规模因子。PB代表市净率,用于衡量价值因子(低 PB 对应高价值)。- 注意:使用此接口前,请确保在 QMT 客户端的【数据管理】中下载了对应的“多因子数据”。
- 我们使用了 QMT 内置的因子库
-
排序逻辑 (
rank):- Fama-French 模型的核心是相对排名。我们没有使用绝对阈值(例如 PB < 1),而是使用
rank()函数对全市场(或股票池)进行排名。 total_score = rank_size + rank_value是一种简单有效的多因子合成方法。
- Fama-French 模型的核心是相对排名。我们没有使用绝对阈值(例如 PB < 1),而是使用
-
调仓控制:
- 代码通过
current_month和ContextInfo.last_month的对比,实现了月度调仓。只有在月份发生变化(即每个月的第一根 K 线)时才会触发交易逻辑。
- 代码通过
-
交易执行 (
order_target_percent):- 使用
order_target_percent是最简便的调仓方式,它会自动计算当前持仓与目标权重的差额,并自动下单买入或卖出。
- 使用
如何在 QMT 中运行
- 新建策略:在 QMT 策略编辑器中新建一个 Python 策略。
- 粘贴代码:将上述代码完整复制进去。
- 修改账号:将
init函数中的'YOUR_ACCOUNT_ID'替换为您真实的资金账号。 - 补充数据:
- 点击菜单栏的【数据管理】->【补充数据】。
- 勾选【多因子数据】,下载对应时间段的数据。
- 回测/运行:
- 回测:设置好回测时间段(例如过去3年),周期选择“日线”,点击回测。
- 实盘:将运行模式切换为“实盘”或“模拟”,点击运行。
常见问题 (Q&A)
Q: 为什么使用 PB 而不是 BM (账面市值比)?
A: 在 QMT 的因子库中,直接提供了 PB (市净率)。PB 是 BM 的倒数。Fama-French 原文中是买入 High BM 的股票,这等同于买入 Low PB 的股票。逻辑是一致的。
Q: 为什么回测时没有交易?
A: 请检查以下几点:
- 是否下载了“多因子数据”?(这是最常见的原因)。
ContextInfo.accid是否设置正确(回测模式下通常不需要真实账号,但代码中调用了set_account,建议保留)。- 回测的基准指数(如中证500)是否有成分股数据。
Q: 如何加入市场因子 (MKT)?
A: 在纯选股策略中,市场因子通常体现为“全额投资”或“持有高 Beta 股票”。上述代码默认满仓操作(0.98仓位),实际上已经暴露在市场风险下,符合三因子模型在多头端的应用。如果您想做多空对冲(Alpha 策略),则需要做空对应的股指期货。