问题描述
解决方案
这是一个基于 QMT 平台、使用 Python 和 sklearn 库实现的机器学习交易策略示例。
策略思路
- 数据获取:在
handlebar中获取目标股票的历史 K 线数据。 - 特征工程:计算简单的技术指标作为机器学习模型的输入特征(Feature),例如:
- 5日、10日、20日均线与收盘价的比率。
- RSI(相对强弱指标)。
- 成交量变化率。
- 标签生成:将“第二天收盘价是否上涨”作为预测目标(Label)。如果涨了标记为 1,否则为 0。
- 模型训练:使用
RandomForestClassifier(随机森林分类器)进行滚动训练。即每天使用过去一段时间的数据训练模型。 - 预测与交易:
- 使用当天的技术指标预测明天的涨跌。
- 如果预测上涨(1)且当前无持仓,则买入。
- 如果预测下跌(0)且当前有持仓,则卖出。
注意事项
- 环境依赖:请确保你的 QMT Python 环境中已安装
scikit-learn和pandas。QMT 自带环境通常包含这些库,如果没有,需要手动安装。 - 回测速度:由于该策略在每根 K 线(每天)都会重新训练模型,回测速度可能会比普通策略慢。
- 账号设置:代码中的
ContextInfo.account_id需要替换为你实际的资金账号。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
def init(ContextInfo):
"""
初始化函数
"""
# 设置操作标的,这里以浦发银行为例
ContextInfo.stock = '600000.SH'
# 设置资金账号 (请修改为您自己的账号)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
ContextInfo.set_account(ContextInfo.account_id)
# 设置股票池
ContextInfo.set_universe([ContextInfo.stock])
# 策略参数
ContextInfo.train_window = 150 # 使用过去150天的数据进行训练
ContextInfo.holding = False # 记录持仓状态
# 初始化模型:随机森林分类器
# n_estimators=10 为了演示速度设置较小,实盘可适当增加
ContextInfo.model = RandomForestClassifier(n_estimators=20, max_depth=5, random_state=42)
def calculate_features(df):
"""
特征工程函数:计算技术指标
"""
data = df.copy()
# 1. 计算移动平均线 (MA)
data['MA5'] = data['close'].rolling(window=5).mean()
data['MA10'] = data['close'].rolling(window=10).mean()
data['MA20'] = data['close'].rolling(window=20).mean()
# 2. 构造特征:价格相对于均线的偏离度
data['feat_ma5_ratio'] = data['close'] / data['MA5'] - 1
data['feat_ma10_ratio'] = data['close'] / data['MA10'] - 1
data['feat_ma20_ratio'] = data['close'] / data['MA20'] - 1
# 3. 构造特征:日收益率
data['feat_return'] = data['close'].pct_change()
# 4. 构造特征:成交量变化
data['feat_vol_change'] = data['volume'].pct_change()
# 5. 简单的动量特征 (RSI的简化版逻辑,N日涨跌幅)
data['feat_momentum_5'] = data['close'] / data['close'].shift(5) - 1
# 清洗数据,去除因计算指标产生的NaN
data.dropna(inplace=True)
return data
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前K线位置
index = ContextInfo.barpos
# 获取当前时间
realtime = ContextInfo.get_bar_timetag(index)
# 确保有足够的数据进行训练 (训练窗口 + 指标计算预留窗口)
if index < ContextInfo.train_window + 30:
return
# 1. 获取历史行情数据
# 获取足够长的历史数据以计算指标和训练
# count = 训练窗口 + 预测当天(1) + 指标计算缓冲(30)
fetch_count = ContextInfo.train_window + 31
# 使用 get_market_data_ex 获取数据
# 注意:get_market_data_ex 返回的是字典 {code: dataframe}
market_data = ContextInfo.get_market_data_ex(
fields=['open', 'high', 'low', 'close', 'volume'],
stock_code=[ContextInfo.stock],
period='1d',
count=fetch_count,
dividend_type='front' # 前复权
)
if ContextInfo.stock not in market_data:
return
df = market_data[ContextInfo.stock]
# 2. 特征工程
df_features = calculate_features(df)
# 如果数据处理后长度不足,跳过
if len(df_features) < 20:
return
# 3. 构造标签 (Label)
# 目标:预测"明天"的涨跌。
# 逻辑:如果 T+1 日收盘价 > T 日收盘价,则 T 日的标签为 1,否则为 0
# shift(-1) 将未来的数据向上平移,使得当前行包含了未来的信息作为 Label
df_features['target'] = (df_features['close'].shift(-1) > df_features['close']).astype(int)
# 4. 划分训练集和预测集
# 预测集 (X_pred):取最后一行数据(即当前Bar的数据),它的 target 是 NaN(因为不知道明天价格),用于预测
current_bar_features = df_features.iloc[-1:].copy()
# 删除非特征列
feature_cols = [c for c in df_features.columns if c.startswith('feat_')]
X_pred = current_bar_features[feature_cols]
# 训练集 (X_train, y_train):取除最后一行以外的数据
# 并且要去掉最后一行包含 NaN target 的数据(实际上 shift(-1) 后最后一行 target 本来就是 NaN,倒数第二行才是最近的有效 Label)
train_data = df_features.iloc[:-1].copy()
X_train = train_data[feature_cols]
y_train = train_data['target']
# 5. 模型训练
try:
ContextInfo.model.fit(X_train, y_train)
except Exception as e:
print(f"Training error: {e}")
return
# 6. 预测
# predict 返回 [0] 或 [1]
prediction = ContextInfo.model.predict(X_pred)[0]
# 获取当前持仓
positions = ContextInfo.get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
current_holding = 0
for pos in positions:
if pos.m_strInstrumentID == ContextInfo.stock:
current_holding = pos.m_nVolume
break
# 7. 交易逻辑
# 预测上涨 (1) 且空仓 -> 买入
if prediction == 1:
if current_holding == 0:
print(f"{realtime}: 预测上涨,执行买入 {ContextInfo.stock}")
# 全仓买入 (目标价值设为总资产的 95% 以预留手续费)
# 注意:回测模式下 order_target_percent 比较方便
order_target_percent(ContextInfo.stock, 0.95, ContextInfo, ContextInfo.account_id)
ContextInfo.holding = True
# 预测下跌 (0) 且持仓 -> 卖出
elif prediction == 0:
if current_holding > 0:
print(f"{realtime}: 预测下跌,执行卖出 {ContextInfo.stock}")
# 清仓
order_target_percent(ContextInfo.stock, 0.0, ContextInfo, ContextInfo.account_id)
ContextInfo.holding = False
代码关键点解析
-
calculate_features函数:- 这是机器学习策略的核心。我们没有直接使用原始价格(Open/Close),而是使用了比率(如
close/MA5 - 1)和变化率(pct_change)。 - 原因:机器学习模型对数据的尺度敏感。原始股价(如 10 元和 100 元)差异巨大,且不具备平稳性。使用比率和收益率可以将数据标准化,使模型更容易学习规律。
- 这是机器学习策略的核心。我们没有直接使用原始价格(Open/Close),而是使用了比率(如
-
标签(Target)的构建:
df_features['target'] = (df_features['close'].shift(-1) > df_features['close']).astype(int)- 这行代码非常关键。我们在训练时,是用今天的指标(MA, RSI等)去对应明天的涨跌结果。因此,需要把明天的涨跌情况
shift(-1)移动到今天的行上作为 Label。
-
防未来函数(Look-ahead Bias):
- 在划分训练集时,我们使用了
df_features.iloc[:-1]。 - 这意味着我们只使用当前时刻之前已经发生并确定的数据来训练模型,绝不使用当前时刻还未发生的未来数据(除了作为 Label 的历史数据)。
- 预测时,我们使用
df_features.iloc[-1:](即当前的 K 线数据)输入模型,得到对“下一根 K 线”的预测。
- 在划分训练集时,我们使用了
-
get_market_data_ex:- 这是 QMT 推荐的高效数据接口。它返回的是一个字典,我们需要通过
market_data[ContextInfo.stock]提取出 DataFrame。
- 这是 QMT 推荐的高效数据接口。它返回的是一个字典,我们需要通过
常见问题 (Q&A)
Q: 为什么回测时速度很慢?
A: 因为代码在 handlebar 中,每过一天(每根 K 线)都会重新训练一次随机森林模型。这是为了模拟最真实的“滚动学习”过程。如果想加快速度,可以设置一个计数器,每隔 10 天或 30 天训练一次模型。
Q: 如何更换其他机器学习模型?
A: 可以在 init 函数中修改 ContextInfo.model。例如,如果你想用逻辑回归:
from sklearn.linear_model import LogisticRegression
ContextInfo.model = LogisticRegression()
Q: 这个策略能直接实盘盈利吗?
A: 不能。 这是一个基础的教学示例。真实的金融市场非常嘈杂,仅靠简单的几个技术指标和默认参数的随机森林很难获得稳定的超额收益。实盘策略通常需要更复杂的因子挖掘、风险控制、参数调优以及防止过拟合的处理。