问题描述
解决方案
这是一个非常经典的“基本面选股 + 技术面择时”的双重策略。
策略逻辑分析
-
基本面筛选(选股):
- 低市盈率 (Low PE):寻找估值较低的股票,具有安全边际。
- 高净资产收益率 (High ROE):寻找盈利能力强的优质公司。
- 我们将在策略中设定阈值,例如:PE < 30 且 ROE > 15%。
-
技术面择时(买卖点):
- 均线金叉:当短期均线(如5日线)上穿长期均线(如20日线)时,视为上涨趋势确立,执行买入。
- 卖出逻辑:为了策略完整性,我增加了一个简单的卖出逻辑(死叉卖出),即短期均线下穿长期均线时平仓。
QMT 策略代码实现
以下是完整的 Python 策略代码。代码中包含了详细的注释,直接复制到 QMT 的 Python 编辑器中即可使用。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 1. 设置账号(请替换为您自己的资金账号)
# ContextInfo.set_account('您的资金账号')
# 2. 设置策略参数
ContextInfo.pe_threshold = 30 # 市盈率阈值(小于此值)
ContextInfo.roe_threshold = 15 # ROE阈值(大于此值)
ContextInfo.ma_short_period = 5 # 短期均线周期
ContextInfo.ma_long_period = 20 # 长期均线周期
ContextInfo.target_pos = 0.1 # 每只股票买入仓位占比(例如10%)
# 3. 设置股票池(这里以沪深300成分股为例)
# 注意:实际运行时建议在“数据管理”中补充好相关品种的日线和财务数据
ContextInfo.trade_code_list = ContextInfo.get_stock_list_in_sector('沪深300')
ContextInfo.set_universe(ContextInfo.trade_code_list)
# 4. 设置回测费率等(仅回测有效)
ContextInfo.set_commission(0, [0.001, 0.001, 0.0003, 0.0003, 0.0003, 5])
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前K线位置
index = ContextInfo.barpos
# 获取当前时间戳并转换为日期格式 YYYYMMDD
timetag = ContextInfo.get_bar_timetag(index)
current_date = timetag_to_datetime(timetag, '%Y%m%d')
# 跳过历史数据不足以计算均线的时期
if index < ContextInfo.ma_long_period:
return
# --- 第一步:基本面筛选 ---
# 定义需要获取的因子字段
# PE: Valuation_and_Market_Cap.PE (市盈率)
# ROE: Profitability_and_Earning Quality.ROE (权益回报率)
factor_fields = [
'Valuation_and_Market_Cap.PE',
'Profitability_and_Earning Quality.ROE'
]
# 获取当天的因子数据
# 注意:get_factor_data 在回测中能取到历史对应时刻的数据
factor_data = ContextInfo.get_factor_data(
factor_fields,
ContextInfo.trade_code_list,
current_date,
current_date
)
# 筛选出的优质股票列表
fundamental_select_list = []
if not factor_data.empty:
# 遍历股票池进行筛选
for stock in ContextInfo.trade_code_list:
if stock in factor_data.index:
try:
pe_val = factor_data.loc[stock, 'Valuation_and_Market_Cap.PE']
roe_val = factor_data.loc[stock, 'Profitability_and_Earning Quality.ROE']
# 过滤无效数据
if np.isnan(pe_val) or np.isnan(roe_val):
continue
# 判断条件:低PE 且 高ROE
if 0 < pe_val < ContextInfo.pe_threshold and roe_val > ContextInfo.roe_threshold:
fundamental_select_list.append(stock)
except KeyError:
continue
# --- 第二步:技术面择时与交易 ---
# 获取持仓信息
positions = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'position')
holding_stocks = [pos.m_strInstrumentID + '.' + pos.m_strExchangeID for pos in positions]
# 遍历筛选出的股票,检查均线金叉
for stock in fundamental_select_list:
# 获取历史行情数据 (多取一点数据以确保计算均线足够)
# 注意:get_market_data_ex 返回的是 dict {code: dataframe}
market_data = ContextInfo.get_market_data_ex(
['close'],
[stock],
period='1d',
end_time=current_date,
count=ContextInfo.ma_long_period + 2,
dividend_type='front' # 前复权
)
if stock not in market_data or market_data[stock].empty:
continue
df_price = market_data[stock]
# 如果数据长度不足,跳过
if len(df_price) < ContextInfo.ma_long_period + 1:
continue
# 计算均线
close_prices = df_price['close'].values
ma_short = pd.Series(close_prices).rolling(window=ContextInfo.ma_short_period).mean().values
ma_long = pd.Series(close_prices).rolling(window=ContextInfo.ma_long_period).mean().values
# 获取最近两个点的均线值(用于判断交叉)
# -1 是当前bar,-2 是上一根bar
curr_short = ma_short[-1]
curr_long = ma_long[-1]
prev_short = ma_short[-2]
prev_long = ma_long[-2]
# 检查是否有效
if np.isnan(curr_short) or np.isnan(curr_long):
continue
# 信号判断:金叉 (短期均线上穿长期均线)
# 逻辑:当前短线 > 长线 且 上一根短线 <= 长线
golden_cross = (curr_short > curr_long) and (prev_short <= prev_long)
# 执行买入
if golden_cross and stock not in holding_stocks:
# 使用目标比例下单,控制仓位
# 注意:实盘中需要确保已设置账号
print(f"日期: {current_date}, 股票: {stock} 触发金叉买入 (PE:{pe_val:.2f}, ROE:{roe_val:.2f})")
order_target_percent(stock, ContextInfo.target_pos, ContextInfo, ContextInfo.accid)
# --- 第三步:卖出逻辑 (死叉平仓) ---
# 遍历当前持仓,如果发生死叉则卖出
for stock in holding_stocks:
market_data = ContextInfo.get_market_data_ex(
['close'],
[stock],
period='1d',
end_time=current_date,
count=ContextInfo.ma_long_period + 2,
dividend_type='front'
)
if stock not in market_data or market_data[stock].empty:
continue
df_price = market_data[stock]
if len(df_price) < ContextInfo.ma_long_period + 1:
continue
close_prices = df_price['close'].values
ma_short = pd.Series(close_prices).rolling(window=ContextInfo.ma_short_period).mean().values
ma_long = pd.Series(close_prices).rolling(window=ContextInfo.ma_long_period).mean().values
curr_short = ma_short[-1]
curr_long = ma_long[-1]
prev_short = ma_short[-2]
prev_long = ma_long[-2]
# 信号判断:死叉 (短期均线下穿长期均线)
dead_cross = (curr_short < curr_long) and (prev_short >= prev_long)
if dead_cross:
print(f"日期: {current_date}, 股票: {stock} 触发死叉卖出")
order_target_percent(stock, 0, ContextInfo, ContextInfo.accid)
代码关键点解析
-
数据获取 (
get_factor_data):- 使用了 QMT 内置的多因子库。
Valuation_and_Market_Cap.PE代表市盈率,Profitability_and_Earning Quality.ROE代表净资产收益率。 - 这个接口非常强大,在回测时会自动获取历史时刻的财务数据,避免了“未来函数”的问题。
- 使用了 QMT 内置的多因子库。
-
行情获取 (
get_market_data_ex):- 我们使用
get_market_data_ex获取历史收盘价来计算均线。 - 设置了
dividend_type='front'(前复权),这对于技术指标计算(如均线)非常重要,可以防止分红除权导致的价格跳空影响信号。
- 我们使用
-
交易执行 (
order_target_percent):- 使用
order_target_percent可以方便地管理仓位。例如设置为0.1表示将该股票买入至总资产的 10%。 - 卖出时设置为
0即可清仓。
- 使用
-
运行频率:
- 该策略设计为日线级别 (
period='1d')。在 QMT 界面运行或回测时,请确保主图周期选择为“日线”。
- 该策略设计为日线级别 (
使用前准备
- 补充数据:在 QMT 客户端顶部菜单,点击【数据管理】->【补充数据】,下载“沪深300”成分股的日线数据和财务数据/多因子数据。
- 设置账号:在代码
init函数中,取消注释ContextInfo.set_account并填入您的真实或模拟资金账号,或者在策略编辑器的右侧参数栏设置交易账号。
Q&A
Q: 如何修改选股范围,比如改成中证500?
A: 在 init 函数中,将 ContextInfo.get_stock_list_in_sector('沪深300') 修改为 ContextInfo.get_stock_list_in_sector('中证500') 即可。
Q: 为什么回测时没有交易?
A: 常见原因有:1. 没有下载多因子数据或日线数据;2. 筛选条件过严(例如PE设置得太低);3. 回测时间段内没有触发金叉信号。建议先放宽 PE/ROE 阈值测试一下。
Q: 这个策略可以实盘自动交易吗?
A: 可以。在实盘模式下,QMT 会在每日收盘前或盘中(取决于您的设置)运行 handlebar,当数据满足条件时会自动发出委托。请务必先在模拟盘充分测试。