PTrade/QMT 策略适配器¶
本篇导览
| 项目 | 说明 |
|---|---|
| 目标 | 理解本地回测与 QMT 实盘之间的差异,以及导入与 API 映射关系 |
| 前置 | 已能使用 eqlib 跑通回测;参见 用户手册 — §12 模拟盘 |
将 EasyQuant (eqlib) 编写的策略转换为 PTrade / QMT 平台可运行格式,在尽量保持策略逻辑不变的前提下替换数据源与下单接口。
目录¶
1. 概述¶
1.1 什么是 PTrade/QMT 适配器¶
EasyQuant 的 PTrade 适配器是一个兼容层(compatibility layer),让你用同一套策略代码:
- 本地:使用
from eqlib import *+run_backtest()进行回测(数据源:akshare) - QMT 平台:使用
from eqlib.ptrade_adapter import *+start()/on_bar()进行实盘/模拟(数据源:QMT 实时行情)
策略逻辑完全一致,只需修改导入语句和入口函数。
1.2 支持的 QMT 版本¶
适配器基于 QMT Python API 接口文档(QMT_Python_API_Doc)编写,兼容 QMT 策略编辑器中的 Python 运行环境。
1.3 核心能力¶
| 能力 | 说明 |
|---|---|
| 生命周期桥接 | EasyQuant 的 initialize → QMT 的 init,handle_data → QMT 的 handlebar |
| 代码转换 | 601390 → 601390.SH,000300.XSHG → 000300.SH |
| 数据适配 | attribute_history → ContextInfo.get_market_data |
| 交易适配 | order_value → QMT order_value(),order_target → order_shares |
| 组合模拟 | 内置 Portfolio / Position 模拟,可同步 QMT 真实账户状态 |
2. 快速开始¶
2.1 最简单的方式:使用导出工具¶
生成 examples/ptrade_strategy_generated.py(默认由 examples/03_run_backtest.py 生成样例),复制到 QMT 策略编辑器即可运行。
2.2 手动方式:三步迁移¶
Step 1: 编写 EasyQuant 策略
# my_strategy.py — 本地回测用
from eqlib import *
def initialize(context):
g.security = '601390'
set_benchmark('000300.XSHG')
run_daily(market_open, time='every_bar')
def market_open(context):
hist = attribute_history(g.security, 20, '1d', ['close'])
if hist.empty:
return
ma = hist['close'].mean()
price = hist['close'].iloc[-1]
if price > ma:
order_value(g.security, context.portfolio.available_cash)
else:
order_target(g.security, 0)
Step 2: 在 QMT 策略编辑器中创建策略
# QMT 策略编辑器 — 实盘/模拟用
from eqlib.ptrade_adapter import *
# === 粘贴上面的策略代码(去掉 from eqlib import *) ===
g.security = '601390'
def initialize(context):
set_benchmark('000300.XSHG')
set_account('YOUR_ACCOUNT_ID') # 你的 QMT 账号
run_daily(market_open, time='every_bar')
def market_open(context):
hist = attribute_history(g.security, 20, '1d', ['close'])
if hist.empty:
return
ma = hist['close'].mean()
price = hist['close'].iloc[-1]
if price > ma:
order_value(g.security, context.portfolio.available_cash)
else:
order_target(g.security, 0)
# === QMT 入口函数 — 固定写法 ===
def init(ContextInfo):
set_account('YOUR_ACCOUNT_ID')
start(ContextInfo)
def handlebar(ContextInfo):
on_bar(ContextInfo)
Step 3: 在 QMT 中运行
设置账号 → 回测/实盘。
3. 工作原理¶
3.1 生命周期映射¶
| EasyQuant | QMT | 说明 |
|---|---|---|
initialize(context) |
init(ContextInfo) → start() |
策略初始化,只执行一次 |
handle_data(context, data) |
handlebar(ContextInfo) → on_bar() |
每根 K 线执行 |
run_daily(func) |
handlebar → 每日回调 |
每日定时回调 |
run_weekly(func) |
handlebar → 每周回调 |
每周定时回调 |
run_monthly(func) |
handlebar → 每月回调 |
每月定时回调 |
before_trading_start(context) |
新交易日检测 | 开盘前回调 |
after_trading_end(context) |
is_last_bar() 检测 |
收盘后回调 |
3.2 数据流¶
EasyQuant API QMT Native API
───────────── ──────────────
attribute_history() ──► ContextInfo.get_market_data()
get_price() ──► ContextInfo.get_market_data_ex()
get_current_data() ──► ContextInfo.get_full_tick()
get_universe() ──► ContextInfo.get_universe()
set_universe() ──► ContextInfo.set_universe()
3.4 交易流¶
EasyQuant API QMT Native API
───────────── ──────────────
order() ──► order_shares()
order_value() ──► order_value()
order_target() ──► order_shares() (with position calc)
order_target_value() ──► order_target_value()
4. API 对照¶
4.1 完全兼容的 API¶
以下 API 在 QMT 中与本地回测行为一致:
| API | 参数 | 说明 |
|---|---|---|
set_benchmark(security) |
security: 股票代码 |
设置基准 |
set_option(name, value) |
name, value |
设置选项 |
run_daily(func, time) |
func, time |
每日调度 |
run_weekly(func, day, time) |
func, day_of_week, time |
每周调度 |
run_monthly(func, day, time) |
func, day_of_month, time |
每月调度 |
log(msg) |
msg: 字符串 |
日志输出 |
record(**kwargs) |
键值对 | 记录数值 |
g.* |
任意属性 | 全局变量 |
4.2 适配后兼容的 API¶
以下 API 在底层映射到 QMT 原生函数:
| EasyQuant API | QMT 原生 | 差异说明 |
|---|---|---|
attribute_history(sec, count, unit, fields) |
ContextInfo.get_market_data() |
返回格式统一为 DataFrame |
history(end_date, count, unit, fields) |
ContextInfo.get_market_data() |
同上 |
get_price(sec, start_date, end_date) |
ContextInfo.get_market_data_ex() |
支持日期范围查询 |
order(sec, amount) |
order_shares() |
自动转换代码格式 |
order_value(sec, value) |
order_value() |
正买负卖,自动转换代码 |
order_target(sec, target_amount) |
order_shares() |
计算持仓差值后下单 |
order_target_value(sec, target_value) |
order_target_value() |
直接映射 |
get_current_data() |
ContextInfo.get_full_tick() |
返回字典,键为 EQ 格式代码 |
get_index_stocks(index) |
ContextInfo.get_sector() |
返回 EQ 格式代码列表 |
set_universe(securities) |
ContextInfo.set_universe() |
自动转换代码格式 |
get_universe() |
ContextInfo.get_universe() |
返回 EQ 格式代码列表 |
4.3 新增 API(仅 QMT 模式)¶
以下 API 仅在 QMT 适配器中提供:
| API | 说明 |
|---|---|
set_account(account_id) |
设置 QMT 交易账号 |
start(ContextInfo) |
在 QMT 的 init() 中调用,启动 EasyQuant 策略 |
on_bar(ContextInfo) |
在 QMT 的 handlebar() 中调用,驱动策略逻辑 |
export_ptrade_script(file, output) |
导出工具:生成 QMT 策略文件 |
QMT_TEMPLATE |
QMT 策略模板字符串 |
@initialize |
装饰器:注册初始化函数 |
@handle_data |
装饰器:注册 handle_data 函数 |
4.4 不支持的 API¶
以下 EasyQuant API 在 QMT 中不支持,需要使用 QMT 原生函数替代:
| EasyQuant API | 替代方案 |
|---|---|
fetch_stock_data() |
QMT 自带历史数据 |
download_stock_data() |
不需要,QMT 已有本地数据 |
load_csv() |
使用 pandas 直接读取 |
scan_market() |
使用 QMT 的 get_stock_list_in_sector() |
run_backtest() |
在 QMT 界面操作回测 |
run_portfolio_backtest() |
在 QMT 界面操作回测 |
generate_chart() |
QMT 自带图表 |
get_financial_abstract() |
QMT 的 get_financial_data() |
get_fundamentals() |
QMT 的 get_factor_data() |
5. 使用方式¶
5.1 方式一:直接粘贴到 QMT 编辑器¶
适用于简单策略。将整个策略代码(含 init / handlebar)粘贴到 QMT 策略编辑器。
from eqlib.ptrade_adapter import *
g.security = '601390'
def initialize(context):
set_benchmark('000300.XSHG')
set_account('6000000248')
run_daily(market_open, time='every_bar')
def market_open(context):
hist = attribute_history(g.security, 20, '1d', ['close'])
if hist.empty:
return
ma20 = hist['close'].mean()
price = hist['close'].iloc[-1]
if price > ma20 * 1.02:
order_value(g.security, context.portfolio.available_cash)
elif price < ma20 * 0.98:
order_target(g.security, 0)
def init(ContextInfo):
set_account('6000000248')
start(ContextInfo)
def handlebar(ContextInfo):
on_bar(ContextInfo)
5.2 方式二:使用装饰器风格¶
from eqlib.ptrade_adapter import *
g = {}
@initialize
def my_init(context):
g['security'] = '601390'
set_benchmark('000300.XSHG')
set_account('6000000248')
run_daily(market_open)
@handle_data
def my_handler(context, data):
sec = g['security']
hist = attribute_history(sec, 20, '1d', ['close'])
# ... strategy logic ...
# QMT 入口
def init(ContextInfo):
set_account('6000000248')
start(ContextInfo)
def handlebar(ContextInfo):
on_bar(ContextInfo)
5.3 方式三:使用导出工具¶
适用于已有完整的 EasyQuant 策略文件。
# 可在 13_ptrade_export.py 中修改 strategy_file / output_file
python examples/13_ptrade_export.py
# 输出 examples/ptrade_strategy_generated.py → 复制到 QMT
6. 代码格式转换¶
6.1 股票代码转换规则¶
适配器自动处理以下转换:
| EasyQuant 格式 | QMT 格式 | 规则 |
|---|---|---|
601390 |
601390.SH |
6 开头 → 上海 |
000001 |
000001.SZ |
0 开头 → 深圳 |
300001 |
300001.SZ |
3 开头 → 深圳(创业板) |
688001 |
688001.SH |
68 开头 → 上海(科创板) |
000300.XSHG |
000300.SH |
.XSHG → .SH |
000001.XSHE |
000001.SZ |
.XSHE → .SZ |
601390.SH |
601390.SH |
已为 QMT 格式 → 不变 |
6.2 账户设置¶
在 QMT 中交易需要设置账号:
或者在 QMT 入口函数中设置:
6.3 订单类型¶
| EasyQuant | QMT 等效 | 说明 |
|---|---|---|
order_value(sec, 10000) |
order_value(sec.SH, 10000) |
买入 10000 元 |
order_value(sec, -10000) |
order_value(sec.SH, -10000) |
卖出 10000 元 |
order_target(sec, 0) |
order_shares(sec.SH, -持仓量) |
清仓 |
order(sec, 100) |
order_shares(sec.SH, 100) |
买入 100 股 |
7. 注意事项¶
7.1 回测模式差异¶
- EasyQuant 本地回测:使用 akshare 数据,完整的报告/图表生成
- QMT 回测模式:使用 QMT 本地数据,交易函数通过虚拟账号执行,在 K 线上记录买卖点
- QMT 实盘模式:使用实时行情,交易函数产生实际委托
7.2 数据类型差异¶
- EasyQuant 的
attribute_history返回pandas.DataFrame,索引为日期 - QMT 的
get_market_data返回格式取决于参数,适配器会统一转换为 DataFrame
7.3 交易函数限制¶
- QMT 回测模式中,
cancel、can_cancel_order、do_order等函数无实际意义 - QMT 模拟运行模式下,交易函数无效(需实盘或回测模式)
- EasyQuant 本地回测为“当日信号、次日开盘成交”;QMT 以平台成交规则为准(通常为当下委托撮合)
7.4 股票池¶
- EasyQuant 使用
context.universe或set_universe()管理股票池 - QMT 使用
ContextInfo.set_universe()设定,ContextInfo.get_universe()获取 - 适配器自动同步两者
7.5 全局变量¶
- EasyQuant 使用
g.security等属性访问 - QMT 适配器中的
g是一个字典,也支持属性访问:g.security或g['security']
7.6 滑点和手续费¶
- EasyQuant 本地回测通过
set_order_cost()配置 - QMT 回测通过
ContextInfo.set_commission()/ContextInfo.set_slippage()配置 - QMT 实盘由券商实际费率决定
7.7 ContextInfo 直接访问¶
适配器暴露了底层的 QMT ContextInfo 对象,可通过 context._qmt 访问,以便在需要时使用 QMT 独有 API:
def market_open(context):
# EasyQuant API
hist = attribute_history('601390', 20, '1d', ['close'])
# QMT native API (when needed)
ci = context._qmt
barpos = ci.barpos # current bar index
period = ci.period # current period, e.g. '1d'
附录:完整迁移示例¶
以下是一个完整的均线策略从 EasyQuant 到 QMT 的迁移。
EasyQuant 原始代码(本地回测)¶
from eqlib import *
g.security = '601390'
g.fast_period = 5
g.slow_period = 20
def initialize(context):
set_benchmark('000300.XSHG')
run_daily(market_open, time='every_bar')
def market_open(context):
close_data = attribute_history(g.security, 25, '1d', ['close'])
if close_data.empty or len(close_data) < g.slow_period:
return
ma_fast = close_data['close'].tail(g.fast_period).mean()
ma_slow = close_data['close'].mean()
current_price = close_data['close'].iloc[-1]
if current_price > ma_fast > ma_slow:
if g.security not in context.portfolio.positions or \
context.portfolio.positions[g.security].amount == 0:
order_value(g.security, context.portfolio.available_cash)
log.info('BUY %s' % g.security)
elif current_price < ma_fast < ma_slow:
if g.security in context.portfolio.positions and \
context.portfolio.positions[g.security].amount > 0:
order_target(g.security, 0)
log.info('SELL %s' % g.security)
# 运行回测
result = run_backtest(initialize, '2024-01-01', '2024-12-31',
starting_cash=100000, benchmark='000300.XSHG')
QMT 版本(实盘/模拟)¶
from eqlib.ptrade_adapter import *
g.security = '601390'
g.fast_period = 5
g.slow_period = 20
def initialize(context):
set_benchmark('000300.XSHG')
set_account('YOUR_ACCOUNT_ID')
run_daily(market_open, time='every_bar')
def market_open(context):
close_data = attribute_history(g.security, 25, '1d', ['close'])
if close_data.empty or len(close_data) < g.slow_period:
return
ma_fast = close_data['close'].tail(g.fast_period).mean()
ma_slow = close_data['close'].mean()
current_price = close_data['close'].iloc[-1]
if current_price > ma_fast > ma_slow:
if g.security not in context.portfolio.positions or \
context.portfolio.positions[g.security].amount == 0:
order_value(g.security, context.portfolio.available_cash)
log.info('BUY %s' % g.security)
elif current_price < ma_fast < ma_slow:
if g.security in context.portfolio.positions and \
context.portfolio.positions[g.security].amount > 0:
order_target(g.security, 0)
log.info('SELL %s' % g.security)
# QMT 入口
def init(ContextInfo):
set_account('YOUR_ACCOUNT_ID')
start(ContextInfo)
def handlebar(ContextInfo):
on_bar(ContextInfo)
仅三处差异:
1. from eqlib import * → from eqlib.ptrade_adapter import *
2. 增加 set_account('YOUR_ACCOUNT_ID')
3. 增加 init() / handlebar() 入口函数
策略逻辑完全不变。