问题描述
解决方案
在 QMT 中组合多个策略并进行资金分配,最佳的实践方式是采用 面向对象编程 (OOP) 的思想。
我们可以将每个独立的策略封装成一个 Python class(类)。主程序的 init 函数负责实例化这些策略并分配资金比例,主程序的 handlebar 函数负责循环调用这些策略的执行逻辑。
核心设计思路
- 策略封装:每个策略是一个独立的类,拥有自己的参数(如均线周期)、标的股票和状态变量。
- 资金管理:在主程序中获取账户总资产,根据预设的比例(例如 策略A 40%,策略B 60%)计算每个策略可用的资金上限。
- 统一调度:主图的 K 线驱动
handlebar,handlebar再驱动所有子策略运行。
代码实现
以下是一个完整的示例代码。它包含两个不同的策略:
- TrendStrategy (趋势策略):双均线金叉买入,死叉卖出。
- ReversalStrategy (反转策略):RSI 超卖买入,超买卖出。
这两个策略将同时运行,分别交易不同的股票,并按比例分配资金。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
# ==============================================================================
# 1. 定义策略基类与子策略
# ==============================================================================
class BaseStrategy:
"""
策略基类,定义通用接口
"""
def __init__(self, name, stock_code, capital_ratio):
self.name = name # 策略名称
self.stock_code = stock_code # 策略操作的标的
self.capital_ratio = capital_ratio # 资金分配比例 (0.0 - 1.0)
def on_bar(self, context, total_asset):
"""
每根K线执行的逻辑,由主函数调用
total_asset: 当前账户总资产
"""
pass
class TrendStrategy(BaseStrategy):
"""
子策略1:双均线趋势策略
逻辑:短期均线上穿长期均线买入,下穿卖出
"""
def __init__(self, name, stock_code, capital_ratio, short_period=5, long_period=20):
super().__init__(name, stock_code, capital_ratio)
self.short_period = short_period
self.long_period = long_period
def on_bar(self, context, total_asset):
# 1. 获取数据
# 获取足够的历史数据以计算均线
data = context.get_market_data_ex(
['close'],
[self.stock_code],
period='1d',
count=self.long_period + 5,
dividend_type='front'
)
if self.stock_code not in data or data[self.stock_code].empty:
return
close_prices = data[self.stock_code]['close']
if len(close_prices) < self.long_period:
return
# 2. 计算指标
ma_short = close_prices.rolling(self.short_period).mean()
ma_long = close_prices.rolling(self.long_period).mean()
curr_short = ma_short.iloc[-1]
curr_long = ma_long.iloc[-1]
prev_short = ma_short.iloc[-2]
prev_long = ma_long.iloc[-2]
# 3. 计算本策略可用资金 (目标持仓市值)
# 策略的目标市值 = 总资产 * 分配比例
target_value = total_asset * self.capital_ratio
# 4. 交易逻辑
# 金叉:短线上穿长线 -> 买入(满仓该策略分配的额度)
if prev_short < prev_long and curr_short > curr_long:
print(f"[{self.name}] {self.stock_code} 金叉买入,目标市值: {target_value:.2f}")
order_target_value(self.stock_code, target_value, context, context.account_id)
# 死叉:短线下穿长线 -> 卖出(清仓)
elif prev_short > prev_long and curr_short < curr_long:
print(f"[{self.name}] {self.stock_code} 死叉卖出,目标市值: 0")
order_target_value(self.stock_code, 0, context, context.account_id)
class ReversalStrategy(BaseStrategy):
"""
子策略2:RSI反转策略
逻辑:RSI < 30 买入,RSI > 70 卖出
"""
def __init__(self, name, stock_code, capital_ratio, rsi_period=14):
super().__init__(name, stock_code, capital_ratio)
self.rsi_period = rsi_period
def calculate_rsi(self, series, period):
delta = series.diff()
up = delta.clip(lower=0)
down = -1 * delta.clip(upper=0)
ma_up = up.rolling(window=period).mean()
ma_down = down.rolling(window=period).mean()
rsi = ma_up / (ma_up + ma_down) * 100
return rsi
def on_bar(self, context, total_asset):
# 1. 获取数据
data = context.get_market_data_ex(
['close'],
[self.stock_code],
period='1d',
count=self.rsi_period + 20,
dividend_type='front'
)
if self.stock_code not in data or data[self.stock_code].empty:
return
close_prices = data[self.stock_code]['close']
if len(close_prices) < self.rsi_period:
return
# 2. 计算指标
rsi_series = self.calculate_rsi(close_prices, self.rsi_period)
current_rsi = rsi_series.iloc[-1]
# 3. 计算本策略可用资金
target_value = total_asset * self.capital_ratio
# 4. 交易逻辑
# 超卖:RSI < 30 -> 抄底买入
if current_rsi < 30:
print(f"[{self.name}] {self.stock_code} RSI超卖({current_rsi:.2f}),买入")
order_target_value(self.stock_code, target_value, context, context.account_id)
# 超买:RSI > 70 -> 止盈卖出
elif current_rsi > 70:
print(f"[{self.name}] {self.stock_code} RSI超买({current_rsi:.2f}),卖出")
order_target_value(self.stock_code, 0, context, context.account_id)
# ==============================================================================
# 2. QMT 主程序入口
# ==============================================================================
def init(ContextInfo):
# 设置账号 (请替换为您的真实账号)
ContextInfo.account_id = '600000248'
ContextInfo.set_account(ContextInfo.account_id)
# 初始化策略列表
ContextInfo.strategies = []
# --- 配置策略组合 ---
# 策略A:趋势策略,操作 平安银行,分配 40% 资金
s1 = TrendStrategy(
name="趋势策略A",
stock_code="000001.SZ",
capital_ratio=0.4,
short_period=5,
long_period=10
)
ContextInfo.strategies.append(s1)
# 策略B:反转策略,操作 贵州茅台,分配 60% 资金
s2 = ReversalStrategy(
name="反转策略B",
stock_code="600519.SH",
capital_ratio=0.6,
rsi_period=14
)
ContextInfo.strategies.append(s2)
# 设置股票池(将所有策略用到的股票加入,以便回测或行情订阅)
all_stocks = [s.stock_code for s in ContextInfo.strategies]
ContextInfo.set_universe(all_stocks)
# 回测参数设置
ContextInfo.start = '20220101'
ContextInfo.end = '20230101'
ContextInfo.benchmark = '000300.SH'
ContextInfo.capital = 1000000 # 回测初始资金
def handlebar(ContextInfo):
# 跳过未完成的K线(如果是日线策略)
if not ContextInfo.is_last_bar():
return
# 1. 获取当前账户总资产
# 回测模式下使用 ContextInfo.capital + 盈亏 (简化处理直接取净值 * 初始资金,或者模拟计算)
# 实盘模式下使用 get_trade_detail_data 获取真实资产
total_asset = 0
if ContextInfo.do_back_test:
# 回测中获取当前净值 * 初始资金作为总资产估算
# 注意:QMT回测中直接获取动态总资产比较复杂,这里用 get_net_value 近似
# 或者简单地假设资金固定,仅做演示。
# 更严谨的回测写法是根据持仓市值+现金计算。
net_value = ContextInfo.get_net_value(ContextInfo.barpos)
total_asset = ContextInfo.capital * net_value
else:
# 实盘模式:获取账户资金
acct_info = get_trade_detail_data(ContextInfo.account_id, 'stock', 'account')
if len(acct_info) > 0:
total_asset = acct_info[0].m_dBalance # 总资产
else:
print("未获取到账户信息")
return
# 2. 遍历并运行所有策略
for strategy in ContextInfo.strategies:
try:
strategy.on_bar(ContextInfo, total_asset)
except Exception as e:
print(f"策略 {strategy.name} 运行出错: {e}")
代码详解
-
BaseStrategy类:- 这是一个父类,定义了所有策略共有的属性:
name(策略名)、stock_code(操作标的)、capital_ratio(资金占比)。 - 它规定了所有子类必须实现
on_bar方法。
- 这是一个父类,定义了所有策略共有的属性:
-
TrendStrategy和ReversalStrategy类:- 这是具体的策略逻辑实现。
- 它们在
__init__中接收各自特有的参数(如均线周期、RSI周期)。 - 在
on_bar中,它们独立获取数据、计算指标。 - 关键点:使用
order_target_value下单。目标价值计算公式为total_asset * self.capital_ratio。这意味着如果总资产是 100 万,分配比例 0.4,策略会努力将持仓市值调整到 40 万。如果当前持仓 0,它会买入 40 万;如果当前持仓 40 万且触发卖出信号,它会卖出至 0。
-
init函数:- 创建了一个列表
ContextInfo.strategies。 - 实例化不同的策略对象,并添加到列表中。在这里你可以随意组合不同的策略、不同的股票和不同的资金比例。
- 注意:所有策略的资金比例之和建议不要超过 1.0(100%),否则可能导致资金不足下单失败。
- 创建了一个列表
-
handlebar函数:- 这是 QMT 的驱动核心。
- 它首先获取账户的总资产(区分了回测和实盘的获取方式)。
- 然后通过一个
for循环,依次调用每个策略对象的on_bar方法,将控制权暂时移交给子策略。
这种架构的优点
- 隔离性:修改“趋势策略”的代码不会影响“反转策略”。
- 可扩展性:如果想加第三个策略,只需写一个新的类,并在
init中append进去即可,无需修改handlebar的逻辑。 - 资金风控:通过
capital_ratio严格控制每个策略能使用的最大资金量,防止某个策略亏损过大拖累整体,或者某个策略占用过多资金导致其他策略无法开仓。
注意事项
- 标的冲突:本示例假设不同策略操作不同的股票。如果两个策略同时操作同一只股票(例如策略A想买,策略B想卖),上述代码会产生冲突(后执行的策略会覆盖先执行的策略的仓位目标)。如果需要多策略操作同一只股票,需要引入更复杂的“虚拟持仓”管理系统。
- 数据获取:使用
get_market_data_ex是最高效的方式,它返回 Pandas DataFrame,方便计算指标。 - 实盘账号:请务必在
init中将ContextInfo.account_id修改为您真实的资金账号。
Q&A
Q: 如果我想让策略A分配固定金额(如10万元),而不是比例,怎么改?
A: 可以在 BaseStrategy 中增加一个标记,比如 is_fixed_capital=True。在 on_bar 计算 target_value 时,判断如果是固定金额模式,直接使用该金额;如果是比例模式,则用 total_asset * ratio。
Q: 这种写法支持期货吗?
A: 框架支持,但下单函数需要修改。期货通常使用 buy_open, sell_close 等函数,且需要处理多空方向。order_target_value 主要用于股票。你需要在子策略中重写下单逻辑。
Q: 如何查看每个策略的盈亏?
A: QMT 的原生界面只显示账户总盈亏。如果需要分策略统计,你需要在 BaseStrategy 中自己记录每次交易的盈亏,或者将交易记录输出到 CSV 文件中进行盘后分析。