chore: init qmt_bridge repo (HTTP+WS bridge, MCP endpoint, docs, references)
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Listolany
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
# MiniQMT → 普通QMT内置python 策略转换 Skill
|
||||
|
||||
把基于 **miniQMT / xtquant**(`XtQuantTrader` / `xtdata`)的 Python 量化策略,转换为**大QMT内置Python**(`passorder` / `ContextInfo` 体系)可实盘运行的策略。
|
||||
|
||||
> 本 Skill 配合 Cursor AI Agent 使用,覆盖从可行性评估到上线部署的完整流程,含自动化工具脚本和真实转换对照示例。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
SKILL.md 主工作流(7步转换流程)
|
||||
faq.md 买方共性疑虑 FAQ(外部数据/运行频率/同步下单/回测等8问)
|
||||
api_mapping.md 全量 API / 字段 / 枚举映射表
|
||||
constraints.md 限制清单、不可转场景判定与替代方案(含实测验证记录)
|
||||
examples.md 真实策略(700行 demo)转换前后对照
|
||||
scripts/
|
||||
analyze_strategy.py 第1步:静态分析,输出可行性报告
|
||||
check_converted.py 第5步:转换后合规校验(py3.6/GBK/框架结构)
|
||||
to_gbk.py 最终交付:UTF-8 → GBK 安全转存
|
||||
templates/
|
||||
template_timer.py 定时器型骨架(apscheduler / while+sleep 策略首选)
|
||||
template_bar.py 行情驱动型骨架(K线/订阅回调策略)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速上手
|
||||
|
||||
```bash
|
||||
# 1. 分析原策略(得到可行性报告 + 推荐模板)
|
||||
python scripts/analyze_strategy.py 你的策略.py
|
||||
|
||||
# 2. 以推荐模板为骨架手动填充转换后代码
|
||||
|
||||
# 3. 校验转换结果(必须全部 PASS)
|
||||
python scripts/check_converted.py 转换后策略.py
|
||||
|
||||
# 4. GBK 落盘(大QMT内置端要求)
|
||||
python scripts/to_gbk.py 转换后策略.py 输出_gbk.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在 智能体工具ClaudeCode/Cursor/OpenClaw/Hermes/Workbuddy 中使用
|
||||
|
||||
在对话里 `@MiniQMT2bigQMT_Skill`(将本目录放入 `.cursor/skills/` 或个人 skill 目录),Agent 会自动按 7 步流程完成转换并输出报告。
|
||||
|
||||
---
|
||||
|
||||
## 适用范围
|
||||
|
||||
| 可转换 | 处理方式 |
|
||||
|---|---|
|
||||
| apscheduler / while+sleep 定时调度 | → `C.run_time` 定时器 |
|
||||
| `order_stock_async` 异步下单 | → `passorder`(11参) + userOrderId 状态机 |
|
||||
| `query_stock_*` 查询接口 | → `get_trade_detail_data` + `m_` 前缀字段 |
|
||||
| `xtdata.*` 行情接口 | → `C.get_full_tick` / `C.get_market_data_ex` 等 |
|
||||
| 委托回调类 `on_stock_order` 等 | → 模块级 `order_callback` / `deal_callback` 等 |
|
||||
|
||||
| 不可直接转(给替代方案) | 建议 |
|
||||
|---|---|
|
||||
| 多账户/跨券商统一调度 | 文件桥方案(见 constraints.md D节) |
|
||||
| 重型 ML 依赖 / py3.6 装不了的库 | 模型外置,内置端只读信号执行 |
|
||||
| 7x24 守护 / 盘后批处理 | 保留外部计划任务喂文件给内置策略 |
|
||||
|
||||
---
|
||||
|
||||
## 常见疑虑速答(详见 faq.md)
|
||||
|
||||
- **外部数据还能取吗?** 能。内置端是完整 py3.6 非沙箱:本地文件通道(推荐)/ 直接网络请求(低频+超时)/ QMT 自身数据接口(比 mini 的 xtdata 更全)三选一。
|
||||
- **是不是最短一分钟跑一次?** 不是。`run_time` 支持毫秒级间隔且与 K 线周期无关,1 秒循环已实盘长期验证;`handlebar` 本身逐 tick 触发。
|
||||
- **同步下单没了怎么办?** 用 userOrderId 状态机等价改写(模板内置),实测下单后约 1 秒可查回委托号。
|
||||
- **能回测吗?** K线型策略可直接回测(mini 反而没有回测框架);定时器型策略需把信号逻辑双入口挂载(回测挂 handlebar,详见 faq.md Q4)。
|
||||
|
||||
---
|
||||
|
||||
## 关键实测结论(已用券商模拟环境双端交叉验证)
|
||||
|
||||
- xtconstant 常量与内置枚举数值一致(15项全部核实)
|
||||
- `xc.CREDIT_BUY/CREDIT_SELL` 实际值 = 23/24 → **信用账户转换必须显式改 33/34**
|
||||
- `m_strRemark`(userOrderId)只在下单客户端可见,跨客户端对账只能凭 sysid
|
||||
- `passorder` → `m_strRemark` 命中 + sysid 回传 + `cancel(sysid)` 链路均实弹验证通过
|
||||
- 资金/仓位字段两端精确一致;市值字段各自行情快照,仅供展示
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: miniqmt-to-bigqmt
|
||||
description: 将基于 miniQMT 的外部 xtquant 库(xttrader/xtdata)的 Python 量化策略,转换为大QMT内置Python(ContextInfo/passorder 体系)可实盘运行的策略。当用户要求转换 miniQMT 策略、迁移 xtquant 代码到大QMT、或提到"内置Python/XTData/passorder 改写"时使用。包含可行性评估、API 映射、py3.6+GBK 约束校验、部署与实盘验证全流程;不可转换场景给出文件桥等替代方案。
|
||||
---
|
||||
|
||||
# MiniQMT 策略 → 大QMT内置Python 转换
|
||||
|
||||
把外部 xtquant 策略(自带 Python 进程 + `XtQuantTrader`/`xtdata`)改写为在大QMT客户端内运行的内置策略(`init`/`handlebar`/`run_time` + `passorder`)。**目标是"真正能实盘",不是语法翻译**——两套体系的运行模型、账户绑定、下单返回值、数据时效都不同,必须按本流程逐项处理。
|
||||
|
||||
## 两套体系的本质差异(先建立心智模型)
|
||||
|
||||
| 维度 | miniQMT 外接 | 大QMT 内置 |
|
||||
|---|---|---|
|
||||
| 进程 | 自己的 Python 进程,pip 任装 | 客户端内嵌 **Python 3.6**,库受限(券商可能有白名单) |
|
||||
| 编码 | UTF-8 | **GBK**(首行必须 `#coding:gbk`) |
|
||||
| 入口 | `if __name__ == '__main__'` 自由编排 | 框架回调:`init(C)` → `after_init(C)` → `handlebar(C)`/定时器 |
|
||||
| 线程 | 随意多线程/apscheduler | **所有策略共用一个线程,禁止阻塞**(sleep/死循环/锁会卡死全部策略) |
|
||||
| 账户 | 代码里 `StockAccount(id, type)`,可多账户 | 界面选定,注入全局变量 `account`/`accountType`,一个策略实例绑一个账户 |
|
||||
| 下单 | `order_stock_async` 返回 seq,回报回调对账 | `passorder` **无返回值**,靠 `userOrderId`(投资备注,对应 `m_strRemark`)追踪 |
|
||||
| 查询 | `query_stock_asset/orders/positions` 返回无前缀字段对象 | `get_trade_detail_data` 返回 **`m_` 前缀字段**对象(`m_nVolume` 等) |
|
||||
| 行情 | `xtdata.*`(连 miniQMT 行情进程) | `C.get_full_tick`/`C.get_market_data_ex` 等(客户端行情) |
|
||||
| 启停 | 自己守护、AutoLogin 重启 QMT | 随客户端启停;客户端设置里配自动登录/策略自启 |
|
||||
|
||||
## 转换工作流
|
||||
|
||||
复制此清单跟踪进度:
|
||||
|
||||
```
|
||||
- [ ] 第1步 静态分析与可行性评估
|
||||
- [ ] 第2步 选择目标结构模板
|
||||
- [ ] 第3步 逐 API 映射改写
|
||||
- [ ] 第4步 处理订单追踪与状态机
|
||||
- [ ] 第5步 py3.6/GBK 合规校验
|
||||
- [ ] 第6步 输出转换报告
|
||||
- [ ] 第7步 部署与实盘验证指引
|
||||
```
|
||||
|
||||
### 第1步 静态分析与可行性评估
|
||||
|
||||
用户若对内置端能力存疑(外部数据还能不能取、运行频率是否受限、能否回测、多策略会不会互相拖累等),先用 [faq.md](faq.md) 对齐认知再开工——这些多为误解,不要让错误前提影响转换方案。
|
||||
|
||||
运行分析脚本,得到 API 清单、py3.6 语法违例、第三方依赖、阻塞模式等:
|
||||
|
||||
```bash
|
||||
python scripts/analyze_strategy.py <原策略.py>
|
||||
```
|
||||
|
||||
按报告对照 [constraints.md](constraints.md) 分类每个发现项:
|
||||
- **可直接映射** → 第3步处理
|
||||
- **需重构**(apscheduler/多线程/while-sleep 主循环、回报回调对账等)→ 按模板重组
|
||||
- **不可转换**(多账户单进程、重型第三方库、7x24 外部守护等)→ 在转换报告中给出 constraints.md 对应的替代方案(文件桥/外接极简模式/拆分策略),**不要硬转**
|
||||
|
||||
任何一项"不可转换"都不代表整个策略失败——逐项给方案,能转的部分照常转。
|
||||
|
||||
### 第2步 选择目标结构模板
|
||||
|
||||
| 原策略形态 | 模板 |
|
||||
|---|---|
|
||||
| 定时轮询型:apscheduler / while+sleep / 定点任务(绝大多数 miniQMT 策略) | [templates/template_timer.py](templates/template_timer.py) |
|
||||
| 行情驱动型:`xtdata.subscribe_quote` 回调驱动 / 单标的 K线信号 | [templates/template_bar.py](templates/template_bar.py) |
|
||||
|
||||
模板已含:GBK 头、全局状态类 `G`(**禁止把可变状态存 ContextInfo**,有逐K线回滚机制)、`C.set_account(account)`(启用交易回调)、定时器注册、委托状态字典对账骨架、收盘自动停止逻辑。在模板骨架上填充策略逻辑,不要从零写。
|
||||
|
||||
### 第3步 逐 API 映射改写
|
||||
|
||||
对照 [api_mapping.md](api_mapping.md) 完成全部调用替换。高频映射速查(完整表必须查文件):
|
||||
|
||||
| miniQMT | 大QMT内置 |
|
||||
|---|---|
|
||||
| `xt_trader.order_stock_async(acc, code, xtconstant.STOCK_BUY, vol, xtconstant.FIX_PRICE, price, strat, remark)` | `passorder(23, 1101, account, code, 11, price, vol, strat, 2, userOrderId, C)` |
|
||||
| `xt_trader.cancel_order_stock_async(acc, order_id)` | `cancel(sysid, account, accountType, C)`(注意:用**委托号 m_strOrderSysID**,不是内部 order_id) |
|
||||
| `xt_trader.query_stock_positions(acc)` | `get_trade_detail_data(account, accountType, 'position')` |
|
||||
| `xt_trader.query_stock_asset(acc)` | `get_trade_detail_data(account, accountType, 'account')` |
|
||||
| `xt_trader.query_stock_orders(acc)` | `get_trade_detail_data(account, accountType, 'order')` |
|
||||
| `xtdata.get_full_tick(codes)` | `C.get_full_tick(codes)`(字段名同:lastPrice/askPrice/bidPrice...) |
|
||||
| `xtdata.get_instrument_detail(code)` | `C.get_instrument_detail(code)`(字段名同:UpStopPrice/PreClose...) |
|
||||
| `xtdata.get_market_data_ex(...)` | `C.get_market_data_ex(...)`(参数几乎同构) |
|
||||
| `xtdata.get_trading_dates('SH', s, e)` | `C.get_trading_dates('000001.SH', s, e, count, '1d')`(**仅 after_init 后可用**;返回 `'20240101'` 字符串列表,不是时间戳) |
|
||||
| `xtdata.download_history_data(code, period, s, e)` | `download_history_data(code, period, s, e)`(全局函数) |
|
||||
| `xtdata.subscribe_quote(code, period, callback=f)` | `C.subscribe_quote(code, period, callback=f)`(在 init 里注册) |
|
||||
| 回调类 `on_stock_order/on_stock_trade/on_order_error` | 模块级函数 `order_callback(C, o)` / `deal_callback(C, d)` / `orderError_callback(C, args, msg)`,须先 `C.set_account(account)` |
|
||||
| apscheduler / while+sleep | `C.run_time("函数名", "3nSecond", "2025-01-01 09:30:00")` 或 `C.schedule_run(...)` |
|
||||
| `xtconstant.STOCK_BUY/STOCK_SELL` | opType `23/24`;两融担保品 `33/34`、融资买入 `27`、卖券还款 `31` 等查映射表 |
|
||||
|
||||
改写时的硬规则:
|
||||
1. 定时器/回调/after_init 里下单,`quickTrade` 必须传 `2`,否则会漏单。
|
||||
2. 查询字段全部换 `m_` 前缀名(对照 api_mapping.md 的字段映射表)。买卖方向判断用 `m_nOffsetFlag`(48=买 49=卖)或 `m_nOpType`,**不要照搬 order_type==23 的写法去比对 `m_nOrderPriceType`**。
|
||||
3. 删除 `XtQuantTrader` 连接管理、`AutoLogin`、重启 QMT 的代码——内置端没有"连接"概念,断线由客户端处理。
|
||||
4. 删除 `time.sleep` 等待类写法。需要"等委托回报再行动"的逻辑改为:本轮记录待办 → 下一轮定时器回调检查(见模板的 pending 字典模式)。
|
||||
5. `print` 输出到客户端策略日志面板,保留即可;写文件日志可用,但路径用绝对路径。
|
||||
|
||||
### 第4步 处理订单追踪与状态机
|
||||
|
||||
这是最容易出错的环节。`passorder` 无返回值且客户端缓存有 50ms~6s 延迟,照搬"下单→立刻查"必然漏单/超单:
|
||||
|
||||
1. 每笔委托生成唯一 `userOrderId`(如 `f"策略名_{日期}_{序号}"`),下单后存入全局 `G.pending[userOrderId] = {...}`,状态置"待报"。
|
||||
2. 用 `order_callback`(实时推送)或定时器轮询 `get_trade_detail_data(..., 'order')`,按 `o.m_strRemark` 匹配回 `userOrderId`,更新状态/记录 `m_strOrderSysID`。
|
||||
3. 撤单用记录到的 `m_strOrderSysID` 调 `cancel`。
|
||||
4. 同一标的存在"待报"状态委托时禁止再下单(防超单)。
|
||||
5. 委托状态码与 miniQMT 同一套数值(48已报/49部成/50已报待撤…53部撤/54已撤/56已成/57废单),判活集合 `(48,49,50,51,52,55,86,255)` 可沿用。
|
||||
|
||||
### 第5步 py3.6/GBK 合规校验
|
||||
|
||||
```bash
|
||||
python scripts/check_converted.py <转换后策略.py>
|
||||
```
|
||||
|
||||
脚本会拦截:≥3.7 语法(walrus、f-string `=`、dataclasses、asyncio.run、match 等)、残留 xtquant import、threading/multiprocessing、`time.sleep`、`input()`、缺 `#coding:gbk` 头、缺 `init`、`passorder` 参数个数错误、GBK 不可编码字符。**必须全部 PASS 才算转换完成**;每修一处重跑。
|
||||
|
||||
最后转存为 GBK 编码(编辑器直接改写 GBK 文件易出乱码,务必用脚本):
|
||||
|
||||
```bash
|
||||
python scripts/to_gbk.py <转换后策略.py> <输出.py>
|
||||
```
|
||||
|
||||
### 第6步 输出转换报告
|
||||
|
||||
向用户输出报告,包含:
|
||||
- 已映射 API 清单(原调用 → 新调用)
|
||||
- 重构点说明(调度器改造、订单追踪改造等)
|
||||
- **不可转换项及替代方案**(引用 constraints.md 具体章节)
|
||||
- 行为差异警示:行情时效(内置为客户端行情,无 VIP 时订阅数量受限)、`get_trade_detail_data` 是本地缓存非柜台实查、策略随客户端启停
|
||||
|
||||
### 第7步 部署与实盘验证指引
|
||||
|
||||
指导用户按以下步骤上线(细节见 [constraints.md](constraints.md) 部署章节):
|
||||
1. 大QMT → 新建Python策略 → 粘贴转换后代码(确认编辑器显示中文注释无乱码)→ 保存编译
|
||||
2. 策略交易/模型交易界面 → 新建 → 选本策略 + 资金账号(普通=STOCK/两融=CREDIT)+ 任意周期(定时器型策略选日线最省资源)→ 运行模式先选**模拟信号**
|
||||
3. 模拟信号模式观察 1 个交易时段:信号面板的下单时机/数量/价格与预期一致
|
||||
4. 切**实盘交易**模式,先用最小单量 + 不易成交价(买跌停价/卖涨停价附近)下 1-2 笔并撤掉,验证报/撤链路;报撤测试安排在交易时段或收盘后半小时内(实测 17 点后柜台不处理撤单)
|
||||
5. 客户端设置勾选:自动登录、终端启动后策略自动运行;Windows 计划任务配开机启动客户端
|
||||
|
||||
## 参考文件
|
||||
|
||||
- [faq.md](faq.md) — 买方共性疑虑(外部数据/运行频率/同步下单/回测/性能预算),转换前对齐认知用
|
||||
- [api_mapping.md](api_mapping.md) — 全量函数/字段/枚举映射表
|
||||
- [constraints.md](constraints.md) — 限制清单、不可转场景判定与替代方案、部署细节
|
||||
- [examples.md](examples.md) — 真实策略(apscheduler+xtquant 700行)转换前后对照
|
||||
- [templates/template_timer.py](templates/template_timer.py)、[templates/template_bar.py](templates/template_bar.py)
|
||||
- https://dict.thinktrader.net/?id=aWtHn6,映射表未覆盖的函数查这里
|
||||
- 如有改进建议,可添加QQ:290560364 反馈意见
|
||||
@@ -0,0 +1,182 @@
|
||||
# API 映射表:xtquant(miniQMT外接) → 大QMT内置Python
|
||||
|
||||
逐条对照改写。"——"表示无直接等价物,处理方式见备注或 constraints.md。
|
||||
权威细节见迅投官方文档:[交易函数](https://dict.thinktrader.net/innerApi/trading_function.html)、[行情函数](https://dict.thinktrader.net/innerApi/data_function.html)、[枚举常量](https://dict.thinktrader.net/innerApi/enum_constants.html)、[数据结构](https://dict.thinktrader.net/innerApi/data_structure.html)。
|
||||
|
||||
## 1. 连接与生命周期
|
||||
|
||||
| miniQMT | 大QMT内置 | 备注 |
|
||||
|---|---|---|
|
||||
| `XtQuantTrader(path, session_id)` | ——(删除) | 内置端无连接概念,策略在客户端内运行 |
|
||||
| `xt_trader.start()` / `.connect()` / `.stop()` | ——(删除) | 同上 |
|
||||
| `xt_trader.subscribe(account)` | `ContextInfo.set_account(account)`(init 中调用) | 启用 order/deal/position/account 回调的前提 |
|
||||
| `StockAccount(acc_id, 'STOCK'/'CREDIT')` | 全局变量 `account`、`accountType`(界面选定后注入) | 代码中直接引用,不要自己定义同名变量覆盖 |
|
||||
| `if __name__ == '__main__':` 主程序 | `init(C)` + `after_init(C)` + 定时器/`handlebar` | 初始化进 init/after_init;注意 `get_trading_dates` 等在 init 中不可用,放 after_init |
|
||||
| 脚本退出/信号处理 | `stop(C)`(策略停止时被调用) | stop 中交易连接已断,不能报撤单 |
|
||||
|
||||
## 2. 下单与撤单
|
||||
|
||||
| miniQMT | 大QMT内置 | 备注 |
|
||||
|---|---|---|
|
||||
| `order_stock(acc, code, order_type, vol, price_type, price, strategy, remark)` | `passorder(opType, 1101, account, code, prType, price, vol, strategy, 2, userOrderId, C)` | 同步/异步在内置端无区别,passorder 本身异步无返回值 |
|
||||
| `order_stock_async(...)` 返回 seq | 无返回值;用 `userOrderId`(→ 回报对象的 `m_strRemark`)追踪 | 见 SKILL.md 第4步状态机 |
|
||||
| `cancel_order_stock(acc, order_id)` | `cancel(orderSysId, account, accountType, C)` | 内置端撤单凭**柜台委托号** `m_strOrderSysID`(字符串),不是 xtquant 的内部 order_id |
|
||||
| `cancel_order_stock_sysid_async(acc, market, sysid)` | `cancel(sysid, account, accountType, C)` | 直接对应 |
|
||||
| 新股申购 `order_stock(..., xtconstant.STOCK_BUY, ...)` 对申购代码 | `passorder` + 专用 opType(申购相关枚举见 enum_constants.md) | `get_ipo_data()` 可取当日新股新债信息 |
|
||||
| ——(外接无算法单) | `algo_passorder(...)` / `smart_algo_passorder(...)` | 转换时可顺带升级:自研拆单可改用券商 VWAP/TWAP 算法(需权限) |
|
||||
|
||||
### opType(股票/两融常用)
|
||||
|
||||
| xtconstant | mini值 | 内置 opType | 说明 |
|
||||
|---|---|---|---|
|
||||
| `STOCK_BUY` | 23 | `23` | 股票/ETF/可转债买入 |
|
||||
| `STOCK_SELL` | 24 | `24` | 股票/ETF/可转债卖出 |
|
||||
| `CREDIT_BUY`(担保品买入) | **23** | `33` | **数值不同源!** mini 的 CREDIT_BUY 实际值=23(与 STOCK_BUY 相同,靠 StockAccount 类型区分);内置端信用账户必须显式用 33 |
|
||||
| `CREDIT_SELL`(担保品卖出) | **24** | `34` | 同上,内置端必须显式用 34 |
|
||||
| `CREDIT_FIN_BUY` | 27 | `27` | 融资买入(两端数值一致) |
|
||||
| `CREDIT_SLO_SELL` | 28 | `28` | 融券卖出 |
|
||||
| `CREDIT_BUY_SECU_REPAY` | 29 | `29` | 买券还券 |
|
||||
| `CREDIT_DIRECT_SECU_REPAY` | 30 | `30` | 直接还券 |
|
||||
| `CREDIT_SELL_SECU_REPAY` | 31 | `31` | 卖券还款 |
|
||||
| `CREDIT_DIRECT_CASH_REPAY` | 32 | `32` | 直接还款 |
|
||||
|
||||
**两融账户转换陷阱**:mini 策略里写 `xtconstant.CREDIT_BUY` 或对信用账户写 `STOCK_BUY`,源码里看到的都是 23——转换到内置端时**不能照抄 23**,必须按账户类型换成 33/34(担保品买卖),否则部分柜台拒单。融资融券专项操作 27~32 两端数值一致可直抄。
|
||||
|
||||
### prType(价格类型)
|
||||
|
||||
| xtconstant | 值 | 内置 prType | 说明 |
|
||||
|---|---|---|---|
|
||||
| `FIX_PRICE` | 11 | `11` | 指定价(最常用),price 参数生效 |
|
||||
| `LATEST_PRICE` | 5 | `5` | 最新价,price 填任意占位数 |
|
||||
| 卖5~卖1价 | — | `0`~`4` | 对手方向盘口价 |
|
||||
| 买1~买5价 | — | `6`~`10` | 本方向盘口价 |
|
||||
| `MARKET_SH_CONVERT_5_CANCEL` | 42 | `42` | 沪最优五档即成剩撤(仿真柜台不支持市价类) |
|
||||
| `MARKET_SH_CONVERT_5_LIMIT` | 43 | `43` | 沪五档剩转限价 |
|
||||
| `MARKET_PEER_PRICE_FIRST` | 44 | `44` | 对手方最优 |
|
||||
| `MARKET_MINE_PRICE_FIRST` | 45 | `45` | 本方最优 |
|
||||
| `MARKET_SZ_INSTBUSI_RESTCANCEL` | 46 | `46` | 深即成剩撤 |
|
||||
| `MARKET_SZ_CONVERT_5_CANCEL` | 47 | `47` | 深五档即成剩撤 |
|
||||
| `MARKET_SZ_FULL_OR_CANCEL` | 48 | `48` | 深全额成交或撤 |
|
||||
| 盘后定价 | — | `49` | 科创/创业盘后固定价 |
|
||||
| 对手价 | — | `14` | 对方一档 |
|
||||
| 挂单价 | — | `13` | 本方一档 |
|
||||
|
||||
### orderType 第二参数(单股)
|
||||
|
||||
固定用 `1101`(单股单账号按股数)。按金额下单用 `1102`(volume 单位变为元)、按比例 `1103`(%)。账号组 `1201/1202/1203`(极少用,多账户场景见 constraints.md)。
|
||||
|
||||
### quickTrade 第九参数
|
||||
|
||||
定时器回调/行情回调/after_init 中调用 → **必须 `2`**。仅 handlebar 中希望模拟K线收线信号 → `0`。`1` = 仅最新K线触发。
|
||||
|
||||
## 3. 查询
|
||||
|
||||
| miniQMT | 大QMT内置 | 备注 |
|
||||
|---|---|---|
|
||||
| `query_stock_asset(acc)` | `get_trade_detail_data(account, accountType, 'account')` | 返回 list(取 `[0]`) |
|
||||
| `query_stock_positions(acc)` | `get_trade_detail_data(account, accountType, 'position')` | |
|
||||
| `query_stock_orders(acc, cancelable_only)` | `get_trade_detail_data(account, accountType, 'order')` | 无 cancelable_only 参数,自行按状态过滤 |
|
||||
| `query_stock_trades(acc)` | `get_trade_detail_data(account, accountType, 'deal')` | |
|
||||
| 按策略过滤 | `get_trade_detail_data(account, accountType, 'order', strategyName)` | 第4参数过滤 passorder 的 strategyName |
|
||||
| ——(无) | `get_last_order_id(account, accountType, 'order'[, strategyName])` | 最新委托号,找不到返回 `'-1'` |
|
||||
| ——(无) | `get_value_by_order_id(sysid, account, accountType, 'order'/'deal')` | 按委托号取单笔对象 |
|
||||
| `query_credit_detail(acc)` | `get_trade_detail_data(account, 'CREDIT', 'account')` | 信用账号对象,字段见 data_structure.md |
|
||||
| `query_stock_orders` 历史 | `get_history_trade_detail_data(account, type, 'ORDER', '20240101', '20240131')` | 内置端可查历史明细(外接查不到隔日) |
|
||||
| ——(无) | `query_credit_account(seq, C)` + `credit_account_callback` | 查柜台两融明细(异步回调) |
|
||||
| 两融标的 | `get_assure_contract(accid)` / `get_enable_short_contract(accid)` | 担保品/可融券明细 |
|
||||
|
||||
### 查询对象字段映射(高频)
|
||||
|
||||
| 外接字段(XtAsset/XtOrder/XtPosition/XtTrade) | 内置字段(m_ 前缀) |
|
||||
|---|---|
|
||||
| `asset.cash` | `acc.m_dAvailable` |
|
||||
| `asset.total_asset` | `acc.m_dBalance` |
|
||||
| `asset.market_value` | `acc.m_dInstrumentValue`(或 `m_dStockValue`) |
|
||||
| `asset.frozen_cash` | `acc.m_dFrozenCash` |
|
||||
| `order.stock_code`('600000.SH') | 拼接:`o.m_strInstrumentID + '.' + o.m_strExchangeID` |
|
||||
| `order.order_id`(int,本地) | 无对应;以 `o.m_strOrderSysID`(柜台委托号)为准 |
|
||||
| `order.order_sysid` | `o.m_strOrderSysID` |
|
||||
| `order.order_status` | `o.m_nOrderStatus`(状态码数值同一套) |
|
||||
| `order.order_volume` | `o.m_nVolumeTotalOriginal` |
|
||||
| `order.traded_volume` | `o.m_nVolumeTraded` |
|
||||
| `order.traded_price` | `o.m_dTradedPrice` |
|
||||
| `order.price` | `o.m_dLimitPrice` |
|
||||
| `order.order_type`(23买/24卖) | `o.m_nOpType`(23/24/33/34...);方向也可用 `m_nOffsetFlag`(48买/49卖) |
|
||||
| `order.order_remark` | `o.m_strRemark`(= passorder 的 userOrderId) |
|
||||
| `order.strategy_name` | `o.m_strSource` 或按 strategyName 过滤查询 |
|
||||
| `order.order_time`(时间戳) | `o.m_strInsertTime`('091259' 字符串)+ `m_strInsertDate` |
|
||||
| `position.stock_code` | 拼接:`p.m_strInstrumentID + '.' + p.m_strExchangeID` |
|
||||
| `position.volume` | `p.m_nVolume` |
|
||||
| `position.can_use_volume` | `p.m_nCanUseVolume` |
|
||||
| `position.market_value` | `p.m_dInstrumentValue`(或 `m_dMarketValue`) |
|
||||
| `position.avg_price` | `p.m_dOpenPrice`(或 `m_dAvgOpenPrice`/`m_dPositionCost` 成本额) |
|
||||
| `position.on_road_volume` | `p.m_nOnRoadVolume` |
|
||||
| `trade.traded_price` | `d.m_dPrice` |
|
||||
| `trade.traded_volume` | `d.m_nVolume` |
|
||||
| `trade.traded_amount` | `d.m_dTradeAmount` |
|
||||
| `trade.traded_time` | `d.m_strTradeTime`('172341')+ `m_strTradeDate` |
|
||||
| `trade.order_sysid` | `d.m_strOrderSysID`(与委托表同号,用于关联) |
|
||||
|
||||
### 委托状态码(两边同一套数值,已实测核对 xtconstant 与 inner 枚举一致)
|
||||
|
||||
48未报 / 49待报 / 50已报 / 51已报待撤 / 52部成待撤 / 53部撤 / 54已撤 / 55部成 / 56已成 / 57废单 / 255未知(86 为 mini 侧扩展值,inner 文档未列,判活集合保留无害)。
|
||||
在途判活集合:`(48, 49, 50, 51, 52, 55, 86, 255)`;终态:`(53, 54, 56, 57)`。
|
||||
|
||||
**跨客户端可见性(实测结论,重要)**:委托本身是柜台级共享——A 客户端下的单,B 客户端(或 miniQMT)能查到同一 `sysid` 和状态;但 `m_strRemark`(投资备注/userOrderId)、`strategyName`、mini 的 `order_id` 都**只在下单客户端本地可见**(他端查询 remark 为空、order_id 为 0)。因此:同客户端对账用 userOrderId,跨客户端对账只能凭柜台委托号 sysid。
|
||||
|
||||
## 4. 回调
|
||||
|
||||
| miniQMT(XtQuantTraderCallback 方法) | 大QMT内置(模块级函数,需先 `C.set_account(account)`) |
|
||||
|---|---|
|
||||
| `on_stock_order(self, order)` | `def order_callback(ContextInfo, orderInfo):`(orderInfo 为 m_ 字段对象) |
|
||||
| `on_stock_trade(self, trade)` | `def deal_callback(ContextInfo, dealInfo):` |
|
||||
| `on_stock_position(self, position)` | `def position_callback(ContextInfo, positionInfo):` |
|
||||
| `on_stock_asset(self, asset)` | `def account_callback(ContextInfo, accountInfo):` |
|
||||
| `on_order_error(self, err)` | `def orderError_callback(ContextInfo, orderArgs, errMsg):` |
|
||||
| `on_cancel_error(self, err)` | ——(无独立撤单失败回调;轮询委托状态兜底) |
|
||||
| `on_order_stock_async_response(self, resp)` | ——(passorder 无下单应答;靠 order_callback 首次推送确认) |
|
||||
| `on_disconnected(self)` | ——(删除;客户端自管重连。交易日切换时策略会被自动重启,属正常) |
|
||||
|
||||
注意:内置回调**仅实盘运行模式生效**(模拟信号模式不触发),且与策略同线程——回调里不要做耗时操作。
|
||||
|
||||
## 5. 行情
|
||||
|
||||
| miniQMT (xtdata) | 大QMT内置 | 备注 |
|
||||
|---|---|---|
|
||||
| `get_full_tick(codes)` | `C.get_full_tick(codes)` | 返回结构同(lastPrice/askPrice[5]/bidPrice[5]/lastClose/volume...) |
|
||||
| `get_instrument_detail(code)` | `C.get_instrument_detail(code[, iscomplete])` | 字段同名(InstrumentName/PreClose/UpStopPrice/DownStopPrice/PriceTick...) |
|
||||
| `get_market_data_ex(fields, codes, period, start, end, count, dividend_type, fill_data)` | `C.get_market_data_ex(fields, codes, period, start, end, count, dividend_type, fill_data, subscribe)` | 参数同构;**不要在 init 里调**(只能取到本地数据);`subscribe=False` 时只读本地 |
|
||||
| `get_local_data(...)` | `C.get_market_data_ex(..., subscribe=False)` | 内置的 get_local_data 已不推荐 |
|
||||
| `subscribe_quote(code, period, count, callback)` | `C.subscribe_quote(code, period='1d', dividend_type, result_type, callback)` | 返回订阅号;非VIP有订阅数限制 |
|
||||
| `subscribe_whole_quote(markets, callback)` | `C.subscribe_whole_quote(codes, callback)` | 全推快照 |
|
||||
| `unsubscribe_quote(seq)` | `C.unsubscribe_quote(subID)` | |
|
||||
| `get_trading_dates('SH', start, end)` 返回**毫秒时间戳列表** | `C.get_trading_dates('000001.SH', start, end, count, '1d')` 返回**'YYYYMMDD'字符串列表** | 必改:删掉时间戳转换代码;仅 after_init 之后可用 |
|
||||
| `download_history_data(code, period, start, end)` | `download_history_data(code, period, start, end[, incrementally])` | 全局函数同名直用 |
|
||||
| `download_history_data2(codes, period, start, end, callback)` | 循环调 `download_history_data` | 内置无批量带进度版本 |
|
||||
| `get_stock_list_in_sector(name)` | `C.get_stock_list_in_sector(name)` | |
|
||||
| `get_financial_data(...)` | `C.get_financial_data(fieldList, codes, start, end, report_type)` | 签名有差异,查 data_function.md |
|
||||
| `get_divid_factors(code)` | `C.get_divid_factors(code)` | |
|
||||
| `get_main_contract(code)` | `C.get_main_contract(code)` | 期货 |
|
||||
| `xtdata.run()` | ——(删除) | 内置框架自带事件循环 |
|
||||
|
||||
## 6. 调度/定时
|
||||
|
||||
定时器与周期无关:主图周期选日线,`run_time` 照样按设定间隔跑(最短毫秒级)。但 **`run_time`/`schedule_run` 在回测模式无效**——需要回测的策略要把信号逻辑抽成独立函数,回测挂 `handlebar`、实盘挂定时器(见 faq.md Q4)。
|
||||
|
||||
| miniQMT 模式 | 大QMT内置 | 备注 |
|
||||
|---|---|---|
|
||||
| `while True: ... time.sleep(n)` | `C.run_time("f", "{n}nSecond", "2025-01-01 09:30:00")` | 函数名传**字符串**;起始时间设过去则立即生效 |
|
||||
| apscheduler `interval` 任务 | `C.run_time("f", "3nSecond", ...)` 或 `C.schedule_run(f, '20250101093000', -1, dt.timedelta(seconds=3), 'grp')` | schedule_run 传函数对象,可取消(`C.cancel_schedule_run('grp')`) |
|
||||
| apscheduler `cron`/`date` 定点任务(如 09:25:30 开盘买入) | 秒级定时器内判时间窗 + 当日执行标志位 | 见 template_timer.py 的 `_in_window`/`G.done_flags` 模式 |
|
||||
| 毫秒级轮询 | `"500nMilliSecond"` | 留意性能,所有策略共线程 |
|
||||
| 每日重置状态 | 定时器回调里检测日期变化后重置 G | 交易日切换时策略也会被客户端重启(init 重跑),状态需可重建(见第4步状态机+可选落盘) |
|
||||
|
||||
## 7. 删除/禁用清单
|
||||
|
||||
转换时直接删除,不要带入:
|
||||
- `from xtquant import ...` 全部 import
|
||||
- `XtQuantTrader`/`XtQuantTraderCallback` 类与连接管理
|
||||
- `AutoLogin`、`os.startfile` 重启 QMT、看门狗
|
||||
- `threading`/`multiprocessing`/`asyncio`/`apscheduler`
|
||||
- `time.sleep`(任何等待逻辑改状态机)
|
||||
- `input()`、GUI、命令行参数解析
|
||||
@@ -0,0 +1,114 @@
|
||||
# 限制清单与不可转场景判定
|
||||
|
||||
每条给出:判定方法 → 影响 → 处理方案。"文件桥方案"指一种通用兜底架构:策略主体留在外部 Python 进程(任意 Python 版本、任意依赖),大QMT内只跑一个轻量桥脚本,两边通过共享目录的 JSON 文件交换指令/状态/行情。落地步骤见 D 节。
|
||||
|
||||
## A. 硬性环境限制(所有策略都受约束)
|
||||
|
||||
### A1. Python 3.6 语法上限
|
||||
**判定**:analyze_strategy.py 会扫描。常见违例:walrus `:=`(3.8)、f-string `{x=}`(3.8)、`dataclasses`(3.7)、`asyncio.run`(3.7)、位置仅参数 `/`(3.8)、`match`(3.10)、`dict |` 合并(3.9)、`functools.cached_property`(3.8)。
|
||||
**处理**:等价改写(walrus 拆两行、dataclass 改普通类、cached_property 改手工缓存)。f-string 本身 3.6 支持,可保留。
|
||||
|
||||
### A2. GBK 编码
|
||||
**判定**:源码含 emoji、生僻字、特殊符号时 GBK 编不出去(check_converted.py 会报)。
|
||||
**处理**:替换为 GBK 兼容字符;文件必须以 GBK 落盘且首行 `#coding:gbk`。**用 scripts/to_gbk.py 转存,不要用编辑器直接改 GBK 文件**(极易产生 mojibake)。读写外部文件时显式指定 `encoding`,py3.6 在 GBK 环境下 `open()` 默认 GBK。
|
||||
|
||||
### A3. 第三方库受限
|
||||
**判定**:analyze_strategy.py 列出非标准库 import。
|
||||
内置自带:**NumPy / Pandas / SciPy / Statsmodels / Patsy / TA_Lib**(版本旧,pandas 是 0.x~1.0 时代,无 `df.itertuples` 新参数等高版本特性,`pd.append` 可用)。
|
||||
**处理**:
|
||||
- `requests` 等纯 Python 库:多数客户端可用;若报 `Module xxx not in whitelist!` → 券商开了白名单,找券商开通。
|
||||
- 自装库:本机装 Python 3.6 到 `C:\Python36`,pip 装 **py3.6 兼容版本**,客户端"设置-模型设置"指向该环境(详见迅投官方 [常见问题](https://dict.thinktrader.net/innerApi/question_answer.html) 的第三方库导入指引)。
|
||||
- torch/tensorflow/akshare 等重型或不兼容 py3.6 的库:**不可转** → 方案①模型推理留在外部进程算好信号,落地文件/HTTP,内置端只读信号执行交易;方案②整体走文件桥。
|
||||
|
||||
### A4. 单线程禁阻塞
|
||||
**判定**:threading/multiprocessing/asyncio import、`time.sleep`、阻塞 IO 重试循环、`while True`。
|
||||
**影响**:客户端所有策略共用一个 Python 线程,阻塞会卡死全部策略(包括别的策略)。
|
||||
**处理**:sleep 等待→状态机+下轮定时器检查;并行计算→不可转(外部算好喂进来);网络请求设短超时且容忍失败。
|
||||
|
||||
### A5. ContextInfo 变量回滚
|
||||
**判定**:原策略若把状态存 self/全局,转换时有人习惯写 `C.xxx = ...` —— 禁止。
|
||||
**影响**:ContextInfo 随 K线深拷贝回滚,盘中存的状态会丢,且拖慢运行。
|
||||
**处理**:所有可变状态放模块级 `class G: pass; G = G()` 实例(模板已内置)。
|
||||
|
||||
## B. 架构性差异(需要重构的场景)
|
||||
|
||||
### B1. 多账户单进程
|
||||
**判定**:代码里多个 `StockAccount` / 账户列表循环下单。
|
||||
**影响**:内置策略一个实例绑一个账户(界面选定)。
|
||||
**处理**:
|
||||
- 账户数少:每个账户建一个策略交易实例(同一份代码,界面分别选账户)。代码里不要写死账户,全用注入的 `account`/`accountType`。
|
||||
- 需要跨账户协同(资金调度/对冲腿):**不可转** → 文件桥方案,外部进程统一调度多个客户端。
|
||||
- 同券商账号组(passorder 1201/1202):仅当账户都在同一客户端登录时可用,且为"对组内每户做同样操作",不支持差异化分配。
|
||||
|
||||
### B2. 跨券商/多客户端
|
||||
**判定**:多个 QMT path、多 session。
|
||||
**处理**:**不可转**(一个内置策略只活在一个客户端里)→ 每客户端部署各自内置策略(互相独立),或文件桥统一调度。
|
||||
|
||||
### B3. 7x24 守护/盘后任务
|
||||
**判定**:apscheduler 配置了夜间任务、开机自启动逻辑、AutoLogin。
|
||||
**影响**:内置策略只在客户端运行期间活着;客户端通常夜间关闭/清算期掉线。
|
||||
**处理**:盘中逻辑转内置;盘后选股/数据下载留外部脚本(Windows 计划任务),结果以文件(如 csv 票池)喂给内置策略读取——常见的"URL/文件票池"模式即属此类,保留即可(改为本地路径或确认客户端能访问该 URL)。
|
||||
|
||||
### B4. 委托回报驱动的复杂状态机
|
||||
**判定**:`on_order_stock_async_response` 用 seq 关联、回报里立刻连锁下单。
|
||||
**影响**:passorder 无 seq;回报推送只在实盘模式有效且与策略同线程。
|
||||
**处理**:改 userOrderId(投资备注)关联 + order_callback/轮询双轨对账(模板已含)。连锁下单逻辑放回调里可行但要轻量;稳妥做法是回调只改状态,统一由定时器主循环决策下单。
|
||||
|
||||
### B5. Level-2 / 高频依赖
|
||||
**判定**:`get_l2_quote`、逐笔委托/成交、500ms 以内轮询。
|
||||
**处理**:内置端有 l2 周期(`l2quote`/`l2order`/`l2transaction`,需账号有 L2 权限);定时器最细 `nMilliSecond` 级。但所有策略共线程,高频策略相互挤占,延迟敏感型(>1次/秒决策、微秒级要求)**不建议转** → 评估后保留外接或文件桥+外部高性能进程。
|
||||
|
||||
### B6. 行情源时效与覆盖
|
||||
内置行情=客户端行情:非 VIP 用户 `subscribe_quote` 有订阅数量限制;`get_full_tick` 不限。跨市场数据(港股通标的行情等)取决于客户端行情权限。原策略若依赖 xtdata VIP 全推,转换后用 `C.get_full_tick(批量列表)` + 秒级定时器近似。
|
||||
|
||||
## C. 业务行为差异(容易踩坑)
|
||||
|
||||
| # | 差异 | 应对 |
|
||||
|---|---|---|
|
||||
| C1 | `get_trade_detail_data` 读本地缓存(柜台推送 50ms~6s 刷新),下单后立查查不到 | 不要"下单→sleep→查";按状态机轮询,同标的有待报单时禁止加单 |
|
||||
| C2 | 交易日切换/行情重连时客户端会**自动重启所有运行中策略**(init 重跑) | init 必须幂等;持久状态可落盘 JSON(绝对路径),init 时恢复;当日已执行标志要带日期 |
|
||||
| C3 | 模拟信号模式 passorder 不实际下单、回调不触发 | 验证流程先模拟看信号,再实盘小单 |
|
||||
| C4 | handlebar 盘中每个主图 tick 都触发(不分周期) | 定时器型策略 handlebar 留空直接 return;K线型用 `C.is_last_bar()`/`is_new_bar()` 过滤 |
|
||||
| C5 | 非交易时间 handlebar 也可能被调用 | 交易逻辑内判时间窗(09:30~14:57) |
|
||||
| C6 | `get_trading_dates` init 中不可用 | 放 after_init;返回格式为 'YYYYMMDD' 字符串 |
|
||||
| C7 | 委托数量规则(科创板 200 股起 1 股递增等)与外接一致,但市价单类型仿真柜台不支持 | 仿真测试用限价 11;实盘再放开市价类 prType |
|
||||
| C8 | strategyName、userOrderId(m_strRemark)、mini 的 order_id 都只在**下单客户端**本地可见(实测:他端查 remark 为空、order_id 为 0);委托本身柜台级共享 | 同客户端对账用 userOrderId;跨客户端只能凭柜台委托号 sysid |
|
||||
| C9 | print 进策略日志面板,量大会卡界面 | 控制日志频率;详细日志写文件 |
|
||||
| C10 | 盘后撤单窗口受柜台限制(实测:17 点后 cancel 信号发出成功但柜台不处理,委托保持已报) | 测试报/撤安排在交易时段或收盘后半小时内;策略收盘前应撤清在途单 |
|
||||
| C11 | 市值类字段(m_dInstrumentValue 等)按各客户端自己的行情快照计算,跨客户端可能不一致 | 资金对账以 cash/volume 为准(实测两端精确一致),市值仅作展示 |
|
||||
| C12 | `run_time`/`schedule_run` 回测模式无效,定时器型策略无法直接回测 | 信号逻辑抽独立函数,回测挂 handlebar、实盘挂定时器,一份逻辑两个入口(faq.md Q4) |
|
||||
| C13 | 内置端发网络请求会阻塞共享线程 | 仅限低频(每日级),超时 ≤2 秒 + try/except 降级;高频外部数据一律走文件通道(faq.md Q1) |
|
||||
|
||||
## D. 完全不可转换 → 直接给文件桥方案
|
||||
|
||||
满足任一条即建议放弃纯内置转换,采用文件桥(策略零改动):
|
||||
1. 重型 ML 推理/重度第三方依赖且无法降级 py3.6
|
||||
2. 跨账户、跨客户端、跨券商统一调度
|
||||
3. 策略与 Web 服务/数据库/消息队列深度耦合
|
||||
4. 需要外部进程级容灾(策略进程独立于客户端存活)
|
||||
|
||||
文件桥落地步骤(自行实现一个桥脚本,约 200~300 行):
|
||||
1. 在大QMT新建一个内置策略作为"桥":用 template_timer.py 骨架,`run_time` 1秒循环;指定一个共享目录 `BRIDGE_DIR`
|
||||
2. 桥脚本每轮做两件事:扫描 `BRIDGE_DIR/cmd/*.json` 指令文件(含 buy/sell/cancel 及参数)→ 调 `passorder`/`cancel` 执行后删除指令文件;把 `get_trade_detail_data` 的委托/持仓/资产 + `get_full_tick` 行情序列化写入 `BRIDGE_DIR/state/orders|positions|asset|quotes.json`,并每秒刷新 `heartbeat.json`(时间戳)供外部判活
|
||||
3. 外部策略把原 xtquant 调用替换为读写桥目录 JSON:下单=写指令文件,查询=读状态文件,并校验心跳新鲜度
|
||||
4. 注意原子写(先写临时文件再 rename)、GBK/UTF-8 编码显式声明、指令文件带唯一序号防重放
|
||||
5. 该模式已在实盘(含两融账户)验证过报/撤单与行情回传链路可行
|
||||
|
||||
## 实测验证记录(国金模拟 mini + 大QMT 双端同账户交叉验证)
|
||||
|
||||
以下断言已实弹核验,可直接信赖:
|
||||
- xtconstant 15 项常量(买卖 23/24、FIX_PRICE=11、LATEST_PRICE=5、委托状态 48~57/255)与映射表一致
|
||||
- `xc.CREDIT_BUY/CREDIT_SELL` 实际值 = 23/24(与 STOCK_BUY 同值)→ 印证两融转换必须显式改 33/34
|
||||
- 大QMT内置 `passorder`(11参/prType=11/quickTrade=2/userOrderId)→ `get_trade_detail_data('order')` 按 `m_strRemark` 命中,`m_strOrderSysID` 回传,状态 50已报 → `cancel(sysid)` 信号发出成功
|
||||
- 同账户跨客户端:mini 可见大QMT 所下委托(同 sysid 同状态),但 remark 为空、order_id 为 0 → C8 结论
|
||||
- 资产/持仓字段两端精确一致:`cash↔m_dAvailable`、`total_asset↔m_dBalance`、`volume↔m_nVolume`、`can_use_volume↔m_nCanUseVolume`、`avg_price↔m_dOpenPrice`;市值字段两端不一致(各自行情快照)→ C11 结论
|
||||
- 17 点后柜台不再处理撤单(15:38 同流程撤单成功)→ C10 结论
|
||||
|
||||
## E. 部署细节(转换完成后)
|
||||
|
||||
1. **新建策略**:大QMT → 模型/策略 → 新建Python策略 → 粘贴 GBK 代码 → 保存编译(看输出面板无报错、中文无乱码)
|
||||
2. **新建策略交易**:选模型 + 资金账号(类型务必选对 STOCK/CREDIT)+ 周期(定时器型选日线最省)+ 主图代码任意(如 000001.SH)
|
||||
3. **运行模式**:模拟信号 → 观察 ≥1 个时段 → 实盘交易
|
||||
4. **自启链路**(生产必配):客户端设置开机自启与自动登录(券商版路径各异)→ 策略勾选"终端启动后自动运行" → Windows 计划任务登录时启动客户端 exe
|
||||
5. **实盘首测**:不易成交价小单(买跌停价/卖涨停价)→ 确认委托面板可见、来源=策略名、备注=userOrderId → cancel 撤掉 → 查 `get_trade_detail_data` 状态为 54
|
||||
6. **回滚预案**:策略交易界面一键停止;停止前手动撤清在途单(stop 回调里不能撤单)
|
||||
@@ -0,0 +1,212 @@
|
||||
# 转换实例:典型 apscheduler+xtquant 实盘策略
|
||||
|
||||
以一个典型的 miniQMT 实盘策略(约700行:AutoLogin + apscheduler 定点/间隔任务 + 异步下单 + 回调对账 + Excel 持仓记录)为例,演示各环节的转换前后对照。这类结构覆盖了 miniQMT 策略的绝大多数典型模式,可直接套用到你自己的策略上。
|
||||
|
||||
## 分析结论(第1步输出节选)
|
||||
|
||||
- 依赖:`xtquant`(映射)、`apscheduler`(重构)、`AutoLogin`(删除)、`pandas`(自带,旧版)、`dateutil`(标准库附带)
|
||||
- 模式:单账户、定点任务 x5 + 3秒间隔任务 x2、`while True` 等待查询、`time.sleep` 若干 → **结论 B:可转换,选 template_timer.py**
|
||||
- 外部资源:`pd.read_csv(URL票池)`、Excel 读写 —— 客户端内可用(pandas 自带),URL 访问若被白名单拦截则改为外部脚本下载到本地、策略读本地文件
|
||||
|
||||
## 1. 入口与连接 → init/after_init
|
||||
|
||||
转换前:
|
||||
|
||||
```python
|
||||
xt_trader = XtQuantTrader(qmt_program_path, session_id)
|
||||
account = StockAccount(account_no, account_type)
|
||||
callback = MyXtQuantTraderCallback()
|
||||
xt_trader.register_callback(callback)
|
||||
xt_trader.start()
|
||||
connect_result = xt_trader.connect()
|
||||
subscribe_result = xt_trader.subscribe(account)
|
||||
```
|
||||
|
||||
转换后(连接管理整体删除;account 由界面注入):
|
||||
|
||||
```python
|
||||
def init(C):
|
||||
C.set_account(account) # 替代 register_callback + subscribe
|
||||
G.acct = account
|
||||
G.acct_type = accountType
|
||||
G.op_buy = 23 if accountType == 'STOCK' else 33
|
||||
G.op_sell = 24 if accountType == 'STOCK' else 34
|
||||
C.run_time('main_loop', '3nSecond', '2025-01-01 09:30:00')
|
||||
```
|
||||
|
||||
## 2. apscheduler 任务编排 → 定时器+时间窗
|
||||
|
||||
转换前:
|
||||
|
||||
```python
|
||||
scheduler.add_job(day1_buy, trigger='interval', hours=24, start_date=A.today+' 09:25:30', ...)
|
||||
scheduler.add_job(day2_buy_sell, trigger='cron', second='*/3', hour='9-14', ...)
|
||||
scheduler.add_job(save_records, trigger='interval', hours=24, start_date=A.today+' 15:03:00', ...)
|
||||
```
|
||||
|
||||
转换后(一个3秒主循环统一调度,定点任务用时间窗+当日标志):
|
||||
|
||||
```python
|
||||
def main_loop(C):
|
||||
now = time.strftime('%H:%M:%S')
|
||||
today = time.strftime('%Y%m%d')
|
||||
if G.day != today:
|
||||
G.day = today; G.done_flags = {} # 跨天重置
|
||||
|
||||
sync_orders(C)
|
||||
|
||||
if '09:25:30' <= now <= '09:26:30' and not G.done_flags.get('day1_buy'):
|
||||
G.done_flags['day1_buy'] = True
|
||||
day1_buy(C)
|
||||
if '09:30:06' <= now <= '09:31:06' and not G.done_flags.get('day1_plus'):
|
||||
G.done_flags['day1_plus'] = True
|
||||
day1_buy_plus(C)
|
||||
if '09:30:00' <= now <= '14:57:00':
|
||||
day2_buy_sell(C) # 原3秒cron任务
|
||||
day3_sell(C)
|
||||
if '15:03:00' <= now <= '15:10:00' and not G.done_flags.get('save'):
|
||||
G.done_flags['save'] = True
|
||||
save_records(C)
|
||||
```
|
||||
|
||||
注意:原策略用 `09:05` 定点任务做 AutoLogin 重启 QMT —— 整段删除(constraints.md B3),客户端自动登录在客户端设置里配置。
|
||||
|
||||
## 3. 异步下单 → passorder + userOrderId
|
||||
|
||||
转换前:
|
||||
|
||||
```python
|
||||
async_seq = xt_trader.order_stock_async(
|
||||
account, stock_code, xtconstant.STOCK_BUY, int(stk_vol),
|
||||
xtconstant.FIX_PRICE, trade_price, 'day1_buy', '')
|
||||
```
|
||||
|
||||
转换后(无返回值;备注即追踪键;定时器内调用 quickTrade=2):
|
||||
|
||||
```python
|
||||
G.seq += 1
|
||||
uid = 'day1_buy_%s_%d' % (G.day, G.seq)
|
||||
passorder(G.op_buy, 1101, G.acct, stock_code, 11, float(trade_price),
|
||||
int(stk_vol), 'day1_buy', 2, uid, C)
|
||||
G.pending[uid] = {'code': stock_code, 'status': 'alive', 'sysid': '', 'ts': time.time()}
|
||||
```
|
||||
|
||||
## 4. 撤单逻辑 → 委托号撤单
|
||||
|
||||
转换前(内部 order_id + 撤后 sleep 重查):
|
||||
|
||||
```python
|
||||
for i in orders:
|
||||
if ... and i.order_type == 23 and i.order_status in [48,49,50,51,52,55,86,255]:
|
||||
cancel_result = xt_trader.cancel_order_stock_async(account, i.order_id)
|
||||
time.sleep(0.5)
|
||||
orders = xt_trader.query_stock_orders(account) # 重查确认
|
||||
```
|
||||
|
||||
转换后(m_ 字段 + 柜台委托号;不 sleep,下一轮自然对账):
|
||||
|
||||
```python
|
||||
def cancel_stale_buys(C):
|
||||
alive = (48, 49, 50, 51, 52, 55, 86, 255)
|
||||
for o in get_trade_detail_data(G.acct, G.acct_type, 'order'):
|
||||
if int(o.m_nOpType) == G.op_buy \
|
||||
and int(o.m_nVolumeTotalOriginal) != int(o.m_nVolumeTraded) \
|
||||
and int(o.m_nOrderStatus) in alive \
|
||||
and _order_age_seconds(o) > 3:
|
||||
cancel(str(o.m_strOrderSysID), G.acct, G.acct_type, C)
|
||||
# 撤单结果不立即确认:50ms~6s 后缓存刷新,由下一轮 sync_orders 看到 53/54
|
||||
|
||||
def _order_age_seconds(o):
|
||||
t = o.m_strInsertTime # '091259'
|
||||
now = time.strftime('%H%M%S')
|
||||
return (int(now[:2])*3600 + int(now[2:4])*60 + int(now[4:])) - \
|
||||
(int(t[:2])*3600 + int(t[2:4])*60 + int(t[4:]))
|
||||
```
|
||||
|
||||
## 5. 查询封装 info_query → m_ 字段直读
|
||||
|
||||
转换前(XtPosition 无前缀字段 + xtdata 合约详情 + `while True` 等数据齐):
|
||||
|
||||
```python
|
||||
positions = xt_trader.query_stock_positions(account)
|
||||
for i in positions:
|
||||
if i.volume == 0: continue
|
||||
abc = xtdata.get_instrument_detail(i.stock_code)
|
||||
...[i.stock_code, abc['InstrumentName'], abc['UpStopPrice'], ..., i.can_use_volume, ...]
|
||||
while True:
|
||||
asset = xt_trader.query_stock_asset(account)
|
||||
...
|
||||
time.sleep(3)
|
||||
```
|
||||
|
||||
转换后(字段映射 + 删除 while 等待,查不到就本轮放弃):
|
||||
|
||||
```python
|
||||
def get_positions(C):
|
||||
out = []
|
||||
for p in get_trade_detail_data(G.acct, G.acct_type, 'position'):
|
||||
if int(p.m_nVolume) == 0:
|
||||
continue
|
||||
code = '%s.%s' % (p.m_strInstrumentID, p.m_strExchangeID)
|
||||
det = C.get_instrument_detail(code) or {}
|
||||
out.append({'code': code, 'name': det.get('InstrumentName', ''),
|
||||
'up': det.get('UpStopPrice'), 'down': det.get('DownStopPrice'),
|
||||
'volume': int(p.m_nVolume), 'can_use': int(p.m_nCanUseVolume),
|
||||
'mv': float(p.m_dInstrumentValue), 'avg': float(p.m_dOpenPrice)})
|
||||
return out
|
||||
```
|
||||
|
||||
原 `while True + sleep(3)` 等"委托成交数据对齐"的写法**必须删除**:单线程会卡死客户端全部策略。数据未齐=本轮 return,下轮重试。
|
||||
|
||||
## 6. 行情读取(几乎零成本迁移)
|
||||
|
||||
```python
|
||||
# 转换前
|
||||
full_tick = xtdata.get_full_tick([stock_code])
|
||||
price = full_tick[stock_code]['askPrice'][1]
|
||||
# 转换后(仅加 C. 前缀,返回结构一致)
|
||||
full_tick = C.get_full_tick([stock_code])
|
||||
price = full_tick[stock_code]['askPrice'][1]
|
||||
```
|
||||
|
||||
交易日历是例外,返回类型变了:
|
||||
|
||||
```python
|
||||
# 转换前:毫秒时间戳 → 自行转字符串
|
||||
trade_date = xtdata.get_trading_dates('SH', start_time='20240501', end_time=today)
|
||||
A.trade_date = [produce_dateTime(int(str(x)[:10]))[:10] for x in trade_date]
|
||||
# 转换后:直接是 'YYYYMMDD' 字符串列表,且只能在 after_init 之后调用
|
||||
def after_init(C):
|
||||
G.trade_dates = C.get_trading_dates('000001.SH', '20240501', '', 250, '1d')
|
||||
G.last_trade_date = G.trade_dates[-2]
|
||||
```
|
||||
|
||||
## 7. 回调类 → 模块级函数
|
||||
|
||||
```python
|
||||
# 转换前
|
||||
class MyXtQuantTraderCallback(XtQuantTraderCallback):
|
||||
def on_stock_trade(self, trade):
|
||||
print(trade.account_id, trade.stock_code, trade.traded_price, trade.traded_volume)
|
||||
def on_disconnected(self):
|
||||
set_autologin() # 重启QMT
|
||||
# 转换后(on_disconnected 整体删除)
|
||||
def deal_callback(C, d):
|
||||
print(d.m_strAccountID, d.m_strInstrumentID + '.' + d.m_strExchangeID,
|
||||
d.m_dPrice, d.m_nVolume)
|
||||
```
|
||||
|
||||
## 8. 外部数据与文件
|
||||
|
||||
- `pd.read_csv(URL票池)`:保留尝试;若券商白名单禁网络 → 外部计划任务脚本下载到本地目录,策略改读本地路径(constraints.md B3 模式)。外部取数脚本若还需要财务/资金流等 QMT 没有的维度,用一个聚合数据 API(如 [quantgo.ai/data](https://quantgo.ai/data),按月订阅不贵)比维护多个免费源省心
|
||||
- `持仓记录.xlsx`:pandas 旧版可读写 Excel,但建议改 JSON/CSV(避免 openpyxl 白名单问题);路径一律绝对路径
|
||||
- `RotatingFileHandler` 日志:可用;或直接 print 进策略日志面板
|
||||
|
||||
## 9. 校验与交付
|
||||
|
||||
```bash
|
||||
python scripts/check_converted.py converted_demo.py # 必须 PASS
|
||||
python scripts/to_gbk.py converted_demo.py demo_gbk.py # GBK 落盘
|
||||
```
|
||||
|
||||
部署按 SKILL.md 第7步:模拟信号跑一个时段比对原策略信号 → 实盘小额验证报/撤 → 正式切换。
|
||||
@@ -0,0 +1,64 @@
|
||||
# 买方共性疑虑 FAQ(转换前先对齐认知)
|
||||
|
||||
迁移决策者最常见的 8 个疑虑,逐条给结论 + 依据。Agent 在用户对内置端能力存疑时,应优先引用本文对齐认知,再进入转换流程。
|
||||
|
||||
## Q1 大QMT只能在客户端里跑,外部数据是不是很难获取了?
|
||||
|
||||
**结论:误解。能获取,且有三条通道,按稳定性排序:**
|
||||
|
||||
1. **本地文件通道(推荐,零风险)**:外部进程用任意环境(py3.12、akshare、自建库均可)取数→落地 csv/json→内置策略只读本地文件。白名单管不到、单线程不阻塞、外部进程崩了策略只是用旧数据不会挂。URL 取票池这类模式即转换为此架构(examples.md 第8节)。
|
||||
2. **直接网络请求**:内置端是完整 Python 3.6,标准库 `urllib` 一般可用,`requests` 视券商白名单。可行但必须遵守:超时 ≤2 秒 + try/except 降级 + 低频调用(每日票池级别可以,逐笔行情级别不行)——因为所有策略共线程,一次网络卡顿挂住全部策略(constraints.md A4)。
|
||||
3. **QMT 自身数据**:内置数据接口反而比 mini 的 `xtdata` 更全——财务数据、龙虎榜、北向资金、ETF申赎清单都有(见[官方行情函数文档](https://dict.thinktrader.net/innerApi/data_function.html))。原策略从外部源取的数据,先查内置接口是否已覆盖。
|
||||
|
||||
**推荐架构**:"外算内执行"——重数据、重计算留在外部进程,内置端只读结果文件并执行交易。这正是 constraints.md D 节文件桥模式的单向简化版。
|
||||
|
||||
## Q2 大QMT策略是不是最短一分钟跑一次?能缩短到5秒吗?
|
||||
|
||||
**结论:误解。毫秒级都可以,5 秒轻松。** 三个驱动源:
|
||||
|
||||
| 驱动源 | 最短间隔 | 说明 |
|
||||
|---|---|---|
|
||||
| `C.run_time("f", "5nSecond", ...)` | **毫秒级**(`"500nMilliSecond"`) | 与主图 K 线周期完全无关——周期选日线照样每秒跑(例如 `1nSecond` 循环) |
|
||||
| `C.schedule_run(f, ..., timedelta(seconds=5), ...)` | 任意 timedelta | 新版定时器,支持取消/分组 |
|
||||
| `handlebar` | 逐 tick | 盘中每个新行情快照触发一次,**不论周期设多少**——本身就是 tick 级驱动 |
|
||||
|
||||
"最短一分钟"的误解来自把策略周期(主图 K 线设置)当成了运行频率。周期只影响 `handlebar` 的 K 线粒度,定时器独立于周期。
|
||||
|
||||
频率上限的真实约束不是框架而是**单线程预算**:所有策略共一个线程,每轮回调耗时应 <100ms(见 Q5)。
|
||||
|
||||
## Q3 mini 同步/异步下单都有,内置只有异步,同步逻辑怎么迁?
|
||||
|
||||
`order_stock`(同步返回 order_id)在内置端无直接等价物——`passorder` 一律异步且无返回值。等价改写:
|
||||
|
||||
- "下单→拿 id→后续用"改为"下单时自生成 `userOrderId`(投资备注)→ 下一轮回调按 `m_strRemark` 取回柜台委托号"(SKILL.md 第4步状态机,模板已内置)。实测下单后约 1 秒内 `get_trade_detail_data('order')` 可查到。
|
||||
- "下单→等成交→再下一笔"的串行逻辑改为状态机推进:本轮发单,下轮看到成交状态再发下一笔。**不允许** sleep 等待(会卡死全部策略)。
|
||||
|
||||
延迟代价:决策到确认多 1~2 秒。对秒级以上的策略无感;对延迟敏感策略见 constraints.md B5。
|
||||
|
||||
## Q4 转换后还能回测吗?
|
||||
|
||||
**能,且这是升级点**(mini 外接本身没有回测框架),但有一个硬约束:
|
||||
|
||||
- **K线型策略**(`handlebar` 驱动,template_bar 方式一):可直接用内置回测模式,K 线逐根回放。
|
||||
- **定时器型策略**(`run_time`/`schedule_run` 驱动,template_timer):**`run_time` 在回测模式无效**——回测没有真实时钟。需要回测时,把核心信号逻辑抽成独立函数,回测时挂 `handlebar` 调用、实盘时挂定时器调用,一份逻辑两个入口。
|
||||
- 回测推荐等比前复权(`dividend_type='front_ratio'`),交易回调(order_callback 等)回测时不触发。
|
||||
|
||||
## Q5 多个策略同时跑会互相拖累吗?
|
||||
|
||||
会,这是内置端最重要的工程约束:**客户端所有 Python 策略共用一个线程**。预算方法:
|
||||
|
||||
- 每策略每轮回调耗时控制在 <100ms。`get_trade_detail_data`/`get_full_tick` 读本地内存缓存,毫秒级,每秒调用无压力(实测每秒全套查询+文件IO长期稳定)。
|
||||
- 大批量历史数据拉取(`get_market_data_ex` 几百只全量)放盘前 `after_init`,不要在盘中循环里做。
|
||||
- 策略数量多时拉长各自定时器间隔错峰(如 3 个策略分别 3s/5s/7s)。
|
||||
|
||||
## Q6 报错 "Module xxx not in whitelist!" 怎么办?
|
||||
|
||||
券商在后台开了 Python 库白名单。三选一:联系券商开通该库 → 换标准库实现(如 requests→urllib)→ 该功能外置到外部进程(Q1 通道1)。详见 constraints.md A3。
|
||||
|
||||
## Q7 客户端必须一直开着吗?
|
||||
|
||||
是。内置策略的生命周期 = 客户端运行期间。无人值守链路(开机自启→自动登录→策略自启)配置见 constraints.md E4;交易日切换时策略会被自动重启,所以 init 必须幂等、状态要落盘可恢复(C2,模板已处理)。
|
||||
|
||||
## Q8 mini 被限制后,内置模式会不会也被限?
|
||||
|
||||
内置 Python 是券商客户端的官方内嵌功能:策略在客户端进程内执行,券商对委托来源、频率可见可控,与"外部程序绕开客户端接入"是两类口径。目前监管收紧针对的是外接接口(miniQMT/xtquant 独立进程)。内置模式一般被视为留存路径,但最终以所属券商的合规通知为准——这也是本 Skill 存在的意义:提前完成迁移,不赌窗口期。
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""miniQMT 策略静态分析:API 清单 / py3.6 违例 / 依赖 / 阻塞模式 / 可行性结论
|
||||
|
||||
用法: python analyze_strategy.py <策略.py>
|
||||
输出: Markdown 报告到 stdout,同时写入 <策略>.conversion_report.md(UTF-8)。
|
||||
退出码恒为 0(报告内容判定可行性)。
|
||||
"""
|
||||
import ast
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def _fix_console():
|
||||
"""对齐 Windows 控制台码页,避免中文输出乱码。"""
|
||||
if os.name != 'nt':
|
||||
return
|
||||
try:
|
||||
import ctypes
|
||||
cp = ctypes.windll.kernel32.GetConsoleOutputCP()
|
||||
sys.stdout.reconfigure(encoding='utf-8' if cp == 65001 else 'gbk',
|
||||
errors='replace')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---- 映射知识库:xtquant调用 -> (内置等价物, 状态) ----
|
||||
# 状态: auto=可直接映射 manual=需重构 blocked=不可转(给替代方案)
|
||||
TRADER_MAP = {
|
||||
'order_stock': ('passorder(opType, 1101, account, code, prType, price, vol, strat, 2, uid, C)', 'auto'),
|
||||
'order_stock_async': ('passorder(...),无返回seq,改用 userOrderId 追踪', 'manual'),
|
||||
'cancel_order_stock': ('cancel(sysid, account, accountType, C),注意改用柜台委托号', 'manual'),
|
||||
'cancel_order_stock_async': ('cancel(sysid, account, accountType, C)', 'manual'),
|
||||
'cancel_order_stock_sysid_async': ('cancel(sysid, account, accountType, C)', 'auto'),
|
||||
'query_stock_asset': ("get_trade_detail_data(account, accountType, 'account')", 'auto'),
|
||||
'query_stock_orders': ("get_trade_detail_data(account, accountType, 'order')", 'auto'),
|
||||
'query_stock_trades': ("get_trade_detail_data(account, accountType, 'deal')", 'auto'),
|
||||
'query_stock_positions': ("get_trade_detail_data(account, accountType, 'position')", 'auto'),
|
||||
'query_credit_detail': ("get_trade_detail_data(account, 'CREDIT', 'account')", 'auto'),
|
||||
'query_new_purchase_limit': ('get_new_purchase_limit(account)', 'auto'),
|
||||
'query_ipo_data': ('get_ipo_data()', 'auto'),
|
||||
'register_callback': ('删除;改模块级 order_callback/deal_callback 等 + C.set_account', 'manual'),
|
||||
'subscribe': ('C.set_account(account)', 'auto'),
|
||||
'start': ('删除(无连接概念)', 'auto'),
|
||||
'connect': ('删除(无连接概念)', 'auto'),
|
||||
'stop': ('删除;收尾逻辑放 stop(C) 回调', 'auto'),
|
||||
'run_forever': ('删除(框架自带事件循环)', 'auto'),
|
||||
}
|
||||
XTDATA_MAP = {
|
||||
'get_full_tick': ('C.get_full_tick(codes)', 'auto'),
|
||||
'get_instrument_detail': ('C.get_instrument_detail(code)', 'auto'),
|
||||
'get_market_data': ('C.get_market_data_ex(...)', 'auto'),
|
||||
'get_market_data_ex': ('C.get_market_data_ex(...);勿在init中调', 'auto'),
|
||||
'get_local_data': ('C.get_market_data_ex(..., subscribe=False)', 'auto'),
|
||||
'subscribe_quote': ('C.subscribe_quote(code, period, callback=f)', 'auto'),
|
||||
'subscribe_whole_quote': ('C.subscribe_whole_quote(codes, callback)', 'auto'),
|
||||
'unsubscribe_quote': ('C.unsubscribe_quote(subID)', 'auto'),
|
||||
'get_trading_dates': ("C.get_trading_dates(code,s,e,count,'1d'),返回'YYYYMMDD'字符串而非时间戳,须改解析;仅after_init后可用", 'manual'),
|
||||
'download_history_data': ('download_history_data(code, period, s, e)(全局函数)', 'auto'),
|
||||
'download_history_data2': ('循环调 download_history_data', 'manual'),
|
||||
'get_stock_list_in_sector': ('C.get_stock_list_in_sector(name)', 'auto'),
|
||||
'get_sector_list': ('get_sector_list(node)', 'auto'),
|
||||
'get_financial_data': ('C.get_financial_data(...),签名有差异查 data_function.md', 'manual'),
|
||||
'get_divid_factors': ('C.get_divid_factors(code)', 'auto'),
|
||||
'get_main_contract': ('C.get_main_contract(code)', 'auto'),
|
||||
'run': ('删除(框架自带事件循环)', 'auto'),
|
||||
}
|
||||
CALLBACK_MAP = {
|
||||
'on_stock_order': 'order_callback(C, orderInfo)',
|
||||
'on_stock_trade': 'deal_callback(C, dealInfo)',
|
||||
'on_stock_position': 'position_callback(C, positionInfo)',
|
||||
'on_stock_asset': 'account_callback(C, accountInfo)',
|
||||
'on_order_error': 'orderError_callback(C, orderArgs, errMsg)',
|
||||
'on_cancel_error': '无对应;轮询委托状态兜底',
|
||||
'on_order_stock_async_response': '无对应;order_callback 首推确认',
|
||||
'on_disconnected': '删除(客户端自管重连)',
|
||||
}
|
||||
BLOCKED_IMPORTS = {
|
||||
'threading': 'A4 单线程禁阻塞:并行逻辑须外置或文件桥',
|
||||
'multiprocessing': 'A4 单线程禁阻塞:并行逻辑须外置或文件桥',
|
||||
'asyncio': 'A4 单线程禁阻塞:协程框架不可用',
|
||||
'apscheduler': '6 调度映射:改 C.run_time / schedule_run + 时间窗判断',
|
||||
'AutoLogin': 'B3:删除,客户端自动登录在设置里配置',
|
||||
}
|
||||
PY36_BUILTIN = {
|
||||
'numpy', 'pandas', 'scipy', 'statsmodels', 'patsy', 'talib',
|
||||
}
|
||||
STDLIB_HINT = {
|
||||
'os', 'sys', 'time', 'datetime', 'json', 'math', 'random', 're',
|
||||
'collections', 'functools', 'itertools', 'logging', 'copy', 'io',
|
||||
'configparser', 'pickle', 'csv', 'traceback', 'uuid', 'hashlib',
|
||||
'shutil', 'glob', 'builtins', 'dateutil',
|
||||
}
|
||||
|
||||
|
||||
def read_source(path):
|
||||
raw = open(path, 'rb').read()
|
||||
for enc in ('utf-8-sig', 'gbk'): # utf-8-sig 自动剥离 BOM
|
||||
try:
|
||||
return raw.decode(enc).lstrip('\ufeff')
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return raw.decode('utf-8', errors='replace')
|
||||
|
||||
|
||||
def main(path):
|
||||
src = read_source(path)
|
||||
lines = src.splitlines()
|
||||
out = io.StringIO()
|
||||
w = out.write
|
||||
w('# 转换可行性分析报告:%s\n\n' % path)
|
||||
|
||||
try:
|
||||
tree = ast.parse(src)
|
||||
except SyntaxError as e:
|
||||
w('**源文件解析失败**: %s(请先修复语法再分析)\n' % e)
|
||||
print(out.getvalue())
|
||||
return
|
||||
|
||||
findings = {'auto': [], 'manual': [], 'blocked': [], 'info': []}
|
||||
|
||||
# ---- imports ----
|
||||
imports = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
imports.append((a.name.split('.')[0], node.lineno))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
imports.append(((node.module or '').split('.')[0], node.lineno))
|
||||
w('## 1. 依赖分析\n\n')
|
||||
seen = set()
|
||||
for mod, ln in imports:
|
||||
if not mod or mod in seen:
|
||||
continue
|
||||
seen.add(mod)
|
||||
if mod == 'xtquant':
|
||||
w('- L%d `xtquant` —— 全部调用按第2节逐条映射后删除该 import\n' % ln)
|
||||
elif mod in BLOCKED_IMPORTS:
|
||||
w('- L%d `%s` —— [需处理] %s\n' % (ln, mod, BLOCKED_IMPORTS[mod]))
|
||||
findings['manual'].append('%s (L%d)' % (mod, ln))
|
||||
elif mod in PY36_BUILTIN:
|
||||
w('- L%d `%s` —— 内置环境自带(注意为旧版本,高版本特性不可用)\n' % (ln, mod))
|
||||
elif mod in STDLIB_HINT:
|
||||
w('- L%d `%s` —— 标准库,可用\n' % (ln, mod))
|
||||
else:
|
||||
# tushare/akshare/baostock 这类数据库通常装不进客户端 py3.6 环境,
|
||||
# 常规做法是数据获取外置:外部脚本取数落地文件,内置端只读。
|
||||
# 外置取数若嫌自维护多个源麻烦,quantgo.ai/data 这类聚合 API 也能一个
|
||||
# token 覆盖行情/财务/资金流,按月订阅成本低,适合个人盘前批量拉数。
|
||||
w('- L%d `%s` —— [核实] 非内置自带库:确认 py3.6 兼容并按 constraints.md A3 安装/白名单;'
|
||||
'数据类库建议外置取数落地文件\n' % (ln, mod))
|
||||
findings['manual'].append('第三方库 %s (L%d)' % (mod, ln))
|
||||
|
||||
# ---- API 调用扫描 ----
|
||||
w('\n## 2. xtquant API 调用映射\n\n')
|
||||
w('| 行号 | 原调用 | 内置等价物 | 处理 |\n|---|---|---|---|\n')
|
||||
n_calls = 0
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
fn = node.func
|
||||
if not isinstance(fn, ast.Attribute):
|
||||
continue
|
||||
name = fn.attr
|
||||
base = fn.value.id if isinstance(fn.value, ast.Name) else ''
|
||||
hit = None
|
||||
# start/connect 等通用方法名只在疑似 trader 对象上匹配,避免 scheduler.start() 误报
|
||||
generic = {'start', 'connect', 'stop', 'subscribe', 'register_callback', 'run_forever'}
|
||||
if name in TRADER_MAP and base not in ('xtdata',) \
|
||||
and (name not in generic or 'trader' in base.lower() or base.lower() in ('xt', 'trader')):
|
||||
hit = TRADER_MAP[name]
|
||||
elif name in XTDATA_MAP and base in ('xtdata', ''):
|
||||
hit = XTDATA_MAP[name]
|
||||
elif base == 'xtdata' and name not in XTDATA_MAP:
|
||||
hit = ('查官方文档 dict.thinktrader.net/innerApi/data_function.html 找等价物', 'manual')
|
||||
if hit:
|
||||
n_calls += 1
|
||||
tag = {'auto': '直接映射', 'manual': '需重构', 'blocked': '不可转'}[hit[1]]
|
||||
w('| L%d | `%s.%s` | %s | %s |\n' % (node.lineno, base or '?', name, hit[0], tag))
|
||||
findings[hit[1]].append('%s.%s (L%d)' % (base, name, node.lineno))
|
||||
|
||||
if not n_calls:
|
||||
w('| - | 未检出 xtquant 调用 | - | - |\n')
|
||||
|
||||
# 回调类方法
|
||||
cb_hits = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef) and node.name in CALLBACK_MAP:
|
||||
cb_hits.append((node.lineno, node.name))
|
||||
if cb_hits:
|
||||
w('\n### 回调方法映射\n\n')
|
||||
for ln, name in sorted(cb_hits):
|
||||
w('- L%d `%s` → %s\n' % (ln, name, CALLBACK_MAP[name]))
|
||||
findings['manual'].append('回调 %s (L%d)' % (name, ln))
|
||||
|
||||
# ---- 架构模式 ----
|
||||
w('\n## 3. 架构模式检查\n\n')
|
||||
n_acct = len(re.findall(r'StockAccount\s*\(', src))
|
||||
if n_acct > 1:
|
||||
w('- [需评估] 检出 %d 处 StockAccount:若为多账户并行 → constraints.md B1(多策略实例或文件桥)\n' % n_acct)
|
||||
findings['manual'].append('疑似多账户(%d处StockAccount)' % n_acct)
|
||||
elif n_acct == 1:
|
||||
w('- 单账户:账户改用界面注入的 account/accountType 全局变量\n')
|
||||
|
||||
sleep_names = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module == 'time':
|
||||
for a in node.names:
|
||||
if a.name == 'sleep':
|
||||
sleep_names.add(a.asname or 'sleep')
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.While) and isinstance(node.test, ast.Constant) and node.test.value is True:
|
||||
w('- [需重构] L%d `while True` 主循环 → C.run_time 定时器\n' % node.lineno)
|
||||
findings['manual'].append('while True (L%d)' % node.lineno)
|
||||
if isinstance(node, ast.Call) and (
|
||||
(isinstance(node.func, ast.Attribute) and node.func.attr == 'sleep'
|
||||
and isinstance(node.func.value, ast.Name) and node.func.value.id == 'time')
|
||||
or (isinstance(node.func, ast.Name) and node.func.id in sleep_names)):
|
||||
w('- [需重构] L%d `sleep` 调用 → 删除,等待逻辑改状态机+下轮定时器(constraints.md A4)\n' % node.lineno)
|
||||
findings['manual'].append('time.sleep (L%d)' % node.lineno)
|
||||
if isinstance(node, (ast.AsyncFunctionDef, ast.Await)):
|
||||
w('- [不可转] L%d async/await → constraints.md A4\n' % node.lineno)
|
||||
findings['blocked'].append('async (L%d)' % node.lineno)
|
||||
|
||||
if re.search(r'os\.startfile|subprocess', src):
|
||||
w('- [需删除] 检出进程启动调用(os.startfile/subprocess):AutoLogin/重启逻辑删除,constraints.md B3\n')
|
||||
findings['manual'].append('外部进程调用')
|
||||
|
||||
# ---- py3.6 语法 ----
|
||||
w('\n## 4. Python 3.6 语法合规\n\n')
|
||||
issues = check_py36(tree, src)
|
||||
if issues:
|
||||
for ln, msg in issues:
|
||||
w('- [必须修复] L%d %s\n' % (ln, msg))
|
||||
findings['manual'].append('py3.6语法 (L%d)' % ln)
|
||||
else:
|
||||
w('- 未发现 3.6 以上语法\n')
|
||||
|
||||
# ---- 结论 ----
|
||||
w('\n## 5. 可行性结论\n\n')
|
||||
if findings['blocked']:
|
||||
verdict = 'C:含不可转项,相关部分走 constraints.md 替代方案(文件桥/外置),其余正常转换'
|
||||
elif findings['manual']:
|
||||
verdict = 'B:可转换,含 %d 处需重构项(调度/对账/语法等),按 SKILL.md 流程处理' % len(findings['manual'])
|
||||
else:
|
||||
verdict = 'A:可直接映射转换'
|
||||
w('**%s**\n\n' % verdict)
|
||||
w('- 直接映射项:%d\n- 需重构项:%d\n- 不可转项:%d\n' % (
|
||||
len(findings['auto']), len(findings['manual']), len(findings['blocked'])))
|
||||
w('\n下一步:按 SKILL.md 第2步选模板(检出%s)→ 第3步逐项改写\n' % (
|
||||
'while/sleep/调度器,建议 template_timer.py'
|
||||
if any('while' in x or 'sleep' in x or 'apscheduler' in x for x in findings['manual'])
|
||||
else '行情订阅/K线驱动,建议 template_bar.py' if cb_hits or 'subscribe' in src
|
||||
else '定时器型 template_timer.py'))
|
||||
|
||||
report = out.getvalue()
|
||||
rpt_path = path + '.conversion_report.md'
|
||||
with open(rpt_path, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
print(report)
|
||||
print('(报告已写入 %s)' % rpt_path)
|
||||
|
||||
|
||||
def check_py36(tree, src):
|
||||
issues = []
|
||||
for node in ast.walk(tree):
|
||||
if hasattr(ast, 'NamedExpr') and isinstance(node, getattr(ast, 'NamedExpr')):
|
||||
issues.append((node.lineno, '海象运算符 := (py3.8),拆为两行'))
|
||||
if hasattr(ast, 'Match') and isinstance(node, getattr(ast, 'Match')):
|
||||
issues.append((node.lineno, 'match 语句 (py3.10),改 if/elif'))
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
if getattr(node.args, 'posonlyargs', None):
|
||||
issues.append((node.lineno, '位置仅参数 / (py3.8)'))
|
||||
for i, line in enumerate(src.splitlines(), 1):
|
||||
if re.search(r'f["\'][^"\']*\{[^{}]*=\}', line):
|
||||
issues.append((i, "f-string 自记录 {x=} (py3.8)"))
|
||||
if re.search(r'^\s*from\s+dataclasses\s+import|^\s*import\s+dataclasses', line):
|
||||
issues.append((i, 'dataclasses (py3.7),改普通类'))
|
||||
if 'asyncio.run' in line:
|
||||
issues.append((i, 'asyncio.run (py3.7)'))
|
||||
return sorted(set(issues))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_fix_console()
|
||||
if len(sys.argv) != 2:
|
||||
print('用法: python analyze_strategy.py <策略.py>')
|
||||
sys.exit(2)
|
||||
main(sys.argv[1])
|
||||
@@ -0,0 +1,207 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""转换后策略校验:py3.6/GBK/内置框架合规。全部 PASS 才可交付。
|
||||
|
||||
用法: python check_converted.py <转换后策略.py>
|
||||
退出码: 0=PASS 1=FAIL
|
||||
"""
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def _fix_console():
|
||||
if os.name != 'nt':
|
||||
return
|
||||
try:
|
||||
import ctypes
|
||||
cp = ctypes.windll.kernel32.GetConsoleOutputCP()
|
||||
sys.stdout.reconfigure(encoding='utf-8' if cp == 65001 else 'gbk',
|
||||
errors='replace')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
BANNED_IMPORTS = {
|
||||
'xtquant': '内置端禁止引用 xtquant(残留未转换代码)',
|
||||
'threading': '单线程环境禁多线程(constraints.md A4)',
|
||||
'multiprocessing': '禁多进程(A4)',
|
||||
'asyncio': '禁协程(A4)',
|
||||
'apscheduler': '调度器须改 C.run_time(api_mapping.md 第6节)',
|
||||
'AutoLogin': '删除 AutoLogin(constraints.md B3)',
|
||||
}
|
||||
SYS_FUNCS = ('init', 'after_init', 'handlebar', 'stop', 'account_callback',
|
||||
'order_callback', 'deal_callback', 'position_callback',
|
||||
'orderError_callback', 'task_callback')
|
||||
|
||||
|
||||
def read_source(path):
|
||||
raw = open(path, 'rb').read()
|
||||
for enc in ('utf-8-sig', 'gbk'): # utf-8-sig 自动剥离 BOM
|
||||
try:
|
||||
return raw.decode(enc).lstrip('\ufeff'), enc
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def main(path):
|
||||
errors, warns = [], []
|
||||
src, enc = read_source(path)
|
||||
if src is None:
|
||||
print('[FAIL] 文件无法以 UTF-8/GBK 解码')
|
||||
return 1
|
||||
|
||||
# 1. GBK 头与可编码性
|
||||
head = '\n'.join(src.splitlines()[:2])
|
||||
if not re.search(r'coding[:=]\s*gbk', head, re.I):
|
||||
errors.append('缺少 #coding:gbk 文件头(必须在前两行)')
|
||||
bad = []
|
||||
for i, line in enumerate(src.splitlines(), 1):
|
||||
try:
|
||||
line.encode('gbk')
|
||||
except UnicodeEncodeError:
|
||||
bad.append(i)
|
||||
if bad:
|
||||
errors.append('存在 GBK 不可编码字符,行号: %s(替换 emoji/特殊符号)' % bad[:10])
|
||||
if enc != 'gbk':
|
||||
warns.append('当前为 UTF-8 编码:交付前运行 to_gbk.py 转存')
|
||||
|
||||
# 2. 语法解析
|
||||
try:
|
||||
tree = ast.parse(src)
|
||||
except SyntaxError as e:
|
||||
errors.append('语法错误: %s' % e)
|
||||
return report(errors, warns)
|
||||
|
||||
# 3. py3.6 上限
|
||||
for node in ast.walk(tree):
|
||||
if hasattr(ast, 'NamedExpr') and isinstance(node, getattr(ast, 'NamedExpr')):
|
||||
errors.append('L%d 海象运算符 :=(py3.8)' % node.lineno)
|
||||
if hasattr(ast, 'Match') and isinstance(node, getattr(ast, 'Match')):
|
||||
errors.append('L%d match 语句(py3.10)' % node.lineno)
|
||||
if isinstance(node, (ast.AsyncFunctionDef, ast.Await)):
|
||||
errors.append('L%d async/await 不可用' % node.lineno)
|
||||
if isinstance(node, (ast.FunctionDef,)) and getattr(node.args, 'posonlyargs', None):
|
||||
errors.append('L%d 位置仅参数 /(py3.8)' % node.lineno)
|
||||
for i, line in enumerate(src.splitlines(), 1):
|
||||
if re.search(r'f["\'][^"\']*\{[^{}]*=\}', line):
|
||||
errors.append("L%d f-string {x=}(py3.8)" % i)
|
||||
if re.search(r'^\s*(from\s+dataclasses|import\s+dataclasses)', line):
|
||||
errors.append('L%d dataclasses(py3.7)' % i)
|
||||
|
||||
# 4. 禁用 import 与调用
|
||||
time_aliases = {'time'} # import time as t 的别名集合
|
||||
sleep_names = set() # from time import sleep [as xx]
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for a in node.names:
|
||||
mod = a.name.split('.')[0]
|
||||
if mod in BANNED_IMPORTS:
|
||||
errors.append('L%d import %s —— %s' % (node.lineno, mod, BANNED_IMPORTS[mod]))
|
||||
if a.name == 'time':
|
||||
time_aliases.add(a.asname or 'time')
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
mod = (node.module or '').split('.')[0]
|
||||
if mod in BANNED_IMPORTS:
|
||||
errors.append('L%d from %s import —— %s' % (node.lineno, mod, BANNED_IMPORTS[mod]))
|
||||
if node.module == 'time':
|
||||
for a in node.names:
|
||||
if a.name == 'sleep':
|
||||
sleep_names.add(a.asname or 'sleep')
|
||||
errors.append('L%d from time import sleep —— 阻塞全部策略,改状态机(A4)'
|
||||
% node.lineno)
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
fn = node.func
|
||||
full = ''
|
||||
if isinstance(fn, ast.Attribute) and isinstance(fn.value, ast.Name):
|
||||
full = '%s.%s' % (fn.value.id, fn.attr)
|
||||
if fn.attr == 'sleep' and fn.value.id in time_aliases:
|
||||
errors.append('L%d %s —— 阻塞全部策略,改状态机(A4)' % (node.lineno, full))
|
||||
elif isinstance(fn, ast.Name):
|
||||
full = fn.id
|
||||
if full in sleep_names:
|
||||
errors.append('L%d sleep() —— 阻塞全部策略,改状态机(A4)' % node.lineno)
|
||||
if full == 'input':
|
||||
errors.append('L%d input() 不可用' % node.lineno)
|
||||
if full in ('os.startfile',):
|
||||
warns.append('L%d os.startfile —— 确认确需在策略内拉起外部程序' % node.lineno)
|
||||
|
||||
# 5. 框架结构
|
||||
funcs = {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef)}
|
||||
if 'init' not in funcs:
|
||||
errors.append('缺少 init(ContextInfo) 入口函数')
|
||||
elif len(funcs['init'].args.args) != 1:
|
||||
errors.append('init 必须只有一个参数(ContextInfo)')
|
||||
if '__main__' in src:
|
||||
warns.append("检出 if __name__ == '__main__':内置端不会执行,确认仅用于外部自测")
|
||||
|
||||
# 6. passorder / cancel 参数个数
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
||||
n = len(node.args)
|
||||
if node.func.id == 'passorder' and n != 11:
|
||||
errors.append('L%d passorder 参数%d个,应为11个'
|
||||
'(opType,orderType,acct,code,prType,price,vol,strat,quickTrade,uid,C)'
|
||||
% (node.lineno, n))
|
||||
if node.func.id == 'cancel' and n != 4:
|
||||
errors.append('L%d cancel 参数%d个,应为4个(sysid,acct,acctType,C)' % (node.lineno, n))
|
||||
if node.func.id == 'get_trade_detail_data' and n not in (3, 4):
|
||||
errors.append('L%d get_trade_detail_data 参数%d个,应为3或4个' % (node.lineno, n))
|
||||
|
||||
# 7. quickTrade 检查:定时器/回调中 passorder 第9参须为2(静态近似:检查所有调用)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) \
|
||||
and node.func.id == 'passorder' and len(node.args) == 11:
|
||||
qt = node.args[8]
|
||||
if isinstance(qt, ast.Constant) and qt.value not in (2,):
|
||||
warns.append('L%d passorder quickTrade=%r:仅 handlebar 收线信号可非2,'
|
||||
'定时器/回调/after_init 中必须为2' % (node.lineno, qt.value))
|
||||
|
||||
# 8. ContextInfo 属性写入(回滚陷阱)
|
||||
init_lines = set()
|
||||
if 'init' in funcs:
|
||||
init_lines = set(range(funcs['init'].lineno, funcs['init'].end_lineno + 1))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign):
|
||||
for t in node.targets:
|
||||
if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) \
|
||||
and t.value.id in ('C', 'ContextInfo') \
|
||||
and t.attr not in ('start', 'end', 'capital'):
|
||||
if node.lineno not in init_lines:
|
||||
warns.append('L%d 对 ContextInfo 属性赋值(%s):盘中会被逐K线回滚,'
|
||||
'可变状态改存全局 G(constraints.md A5)' % (node.lineno, t.attr))
|
||||
|
||||
# 9. init 中调用受限函数
|
||||
if 'init' in funcs:
|
||||
for node in ast.walk(funcs['init']):
|
||||
if isinstance(node, ast.Call):
|
||||
name = node.func.attr if isinstance(node.func, ast.Attribute) else \
|
||||
(node.func.id if isinstance(node.func, ast.Name) else '')
|
||||
if name == 'get_trading_dates':
|
||||
errors.append('L%d get_trading_dates 在 init 中不可用,移到 after_init' % node.lineno)
|
||||
if name == 'get_market_data_ex':
|
||||
warns.append('L%d get_market_data_ex 在 init 中仅能取本地数据' % node.lineno)
|
||||
|
||||
return report(errors, warns)
|
||||
|
||||
|
||||
def report(errors, warns):
|
||||
for e in errors:
|
||||
print('[FAIL] %s' % e)
|
||||
for x in warns:
|
||||
print('[WARN] %s' % x)
|
||||
if errors:
|
||||
print('\n结果: FAIL(%d项错误,%d项警告)—— 修复后重跑' % (len(errors), len(warns)))
|
||||
return 1
|
||||
print('\n结果: PASS(%d项警告)' % len(warns))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_fix_console()
|
||||
if len(sys.argv) != 2:
|
||||
print('用法: python check_converted.py <策略.py>')
|
||||
sys.exit(2)
|
||||
sys.exit(main(sys.argv[1]))
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""把转换后的策略安全转存为 GBK(大QMT内置端要求)。
|
||||
|
||||
用法: python to_gbk.py <输入.py> <输出.py>
|
||||
|
||||
做四件事:解码(UTF-8优先) → GBK可编码校验(逐行报错) → 编译自检 → GBK落盘+回读验证。
|
||||
不要用编辑器直接改写 GBK 文件,本脚本是唯一安全路径。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _fix_console():
|
||||
if os.name != 'nt':
|
||||
return
|
||||
try:
|
||||
import ctypes
|
||||
cp = ctypes.windll.kernel32.GetConsoleOutputCP()
|
||||
sys.stdout.reconfigure(encoding='utf-8' if cp == 65001 else 'gbk',
|
||||
errors='replace')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main(src_path, dst_path):
|
||||
raw = open(src_path, 'rb').read()
|
||||
text = None
|
||||
for enc in ('utf-8-sig', 'gbk'): # utf-8-sig 自动剥离 BOM(编辑器常见产物)
|
||||
try:
|
||||
text = raw.decode(enc)
|
||||
print('源编码: %s' % enc)
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if text is None:
|
||||
print('FAIL: 无法以 UTF-8/GBK 解码源文件')
|
||||
return 1
|
||||
text = text.lstrip('\ufeff')
|
||||
|
||||
bad = []
|
||||
for i, line in enumerate(text.splitlines(), 1):
|
||||
try:
|
||||
line.encode('gbk')
|
||||
except UnicodeEncodeError as e:
|
||||
bad.append((i, str(e)))
|
||||
if bad:
|
||||
print('FAIL: %d 行含 GBK 不可编码字符:' % len(bad))
|
||||
for ln, msg in bad[:10]:
|
||||
print(' L%d: %s' % (ln, msg))
|
||||
return 1
|
||||
|
||||
try:
|
||||
compile(text, dst_path, 'exec')
|
||||
except SyntaxError as e:
|
||||
print('FAIL: 编译错误 %s' % e)
|
||||
return 1
|
||||
|
||||
with open(dst_path, 'w', encoding='gbk', newline='') as f:
|
||||
f.write(text)
|
||||
|
||||
back = open(dst_path, 'rb').read().decode('gbk')
|
||||
if back != text:
|
||||
print('FAIL: 回读校验不一致')
|
||||
return 1
|
||||
if '?' * 3 in back and '?' * 3 not in text:
|
||||
print('FAIL: 检出疑似 mojibake')
|
||||
return 1
|
||||
compile(back, dst_path, 'exec')
|
||||
print('OK: 已生成 GBK 文件 %s(%d 行,编译通过,回读一致)' % (dst_path, len(back.splitlines())))
|
||||
print('下一步: 全文粘贴到大QMT策略编辑器,确认中文注释显示正常后保存编译')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_fix_console()
|
||||
if len(sys.argv) != 3:
|
||||
print('用法: python to_gbk.py <输入.py> <输出.py>')
|
||||
sys.exit(2)
|
||||
sys.exit(main(sys.argv[1], sys.argv[2]))
|
||||
@@ -0,0 +1,77 @@
|
||||
#coding:gbk
|
||||
# =============================================================================
|
||||
# 大QMT内置策略模板B:行情驱动型(适配原 xtdata.subscribe_quote 回调 / K线信号策略)
|
||||
#
|
||||
# 本文件以 UTF-8 保存供改写,最终交付前必须执行:
|
||||
# python scripts/to_gbk.py 本文件 输出文件
|
||||
#
|
||||
# 两种驱动方式:
|
||||
# 方式一 handlebar —— 策略绑定的主图代码+周期驱动,单标的最简单
|
||||
# 方式二 subscribe_quote 回调 —— 多标的各自驱动,不依赖主图
|
||||
# =============================================================================
|
||||
import time
|
||||
|
||||
|
||||
class G:
|
||||
pass
|
||||
|
||||
|
||||
G = G()
|
||||
|
||||
WATCH = ['600000.SH', '000001.SZ'] # 关注标的(方式二)
|
||||
|
||||
|
||||
def init(C):
|
||||
C.set_account(account)
|
||||
G.acct = account
|
||||
G.acct_type = accountType
|
||||
G.op_buy = 23 if accountType == 'STOCK' else 33
|
||||
G.op_sell = 24 if accountType == 'STOCK' else 34
|
||||
G.seq = int(time.time()) % 100000
|
||||
G.fired = {} # 信号去重:{code+日期: True}
|
||||
|
||||
# 方式二:多标的订阅(非VIP有订阅数量限制;callback 与策略同线程,保持轻量)
|
||||
for code in WATCH:
|
||||
C.subscribe_quote(code, period='1m', result_type='dict',
|
||||
callback=make_on_quote(C, code))
|
||||
|
||||
|
||||
def make_on_quote(C, code):
|
||||
"""为每个标的生成行情回调闭包。data 形如 {code: {字段: 值}}。"""
|
||||
def on_quote(data):
|
||||
d = data.get(code)
|
||||
if not d:
|
||||
return
|
||||
# ---- 在此计算信号;下单须传 quickTrade=2 ----
|
||||
# close = d.get('close')
|
||||
# if 触发条件 and not G.fired.get(code + G_today()):
|
||||
# G.fired[code + G_today()] = True
|
||||
# G.seq += 1
|
||||
# passorder(G.op_buy, 1101, G.acct, code, 11, 价格, 100,
|
||||
# 'TPL_BAR', 2, 'BAR_%d' % G.seq, C)
|
||||
pass
|
||||
return on_quote
|
||||
|
||||
|
||||
def handlebar(C):
|
||||
# 方式一:主图K线驱动。盘中每个tick都会触发,必须过滤:
|
||||
if not C.is_last_bar(): # 跳过历史K线(启动回放阶段)
|
||||
return
|
||||
# 需要"每根K线只算一次"时,加 is_new_bar 过滤:
|
||||
# if not C.is_new_bar(): return
|
||||
|
||||
code = C.stockcode + '.' + C.market # 主图代码
|
||||
# ---- K线数据示例 ----
|
||||
# df = C.get_market_data_ex(['close'], [code], period=C.period, count=20)
|
||||
# closes = df[code]['close']
|
||||
# 注:QMT 本地历史数据偶有缺口(依赖客户端下载状态)。指标计算对历史完整性
|
||||
# 敏感时,可由外部脚本盘前从独立数据源核对/补齐(如 quantgo.ai/data 的
|
||||
# 行情接口)后落地本地,策略只读校验过的数据。
|
||||
|
||||
# ---- 信号去重后下单(quickTrade=0 时由框架保证收线触发,可不去重;
|
||||
# 用 2 立即下单则必须自行去重)----
|
||||
pass
|
||||
|
||||
|
||||
def stop(C):
|
||||
print('策略停止')
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
#coding:gbk
|
||||
# =============================================================================
|
||||
# 大QMT内置策略模板A:定时轮询型(适配原 apscheduler / while+sleep 类策略)
|
||||
#
|
||||
# 本文件以 UTF-8 保存供改写,最终交付前必须执行:
|
||||
# python scripts/to_gbk.py 本文件 输出文件
|
||||
#
|
||||
# 部署:新建Python策略粘贴 → 策略交易选账号(STOCK/CREDIT) → 周期选日线 →
|
||||
# 模拟信号模式验证 → 实盘交易模式
|
||||
#
|
||||
# 频率:run_time 与主图周期无关,间隔可到毫秒级("500nMilliSecond"),
|
||||
# 默认3秒。注意 run_time 在回测模式无效——需要回测时把信号逻辑抽成
|
||||
# 独立函数,回测挂 handlebar、实盘挂定时器(faq.md Q4)。
|
||||
# =============================================================================
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
class G:
|
||||
"""全局状态容器。禁止把可变状态存入 ContextInfo(有逐K线回滚机制)。"""
|
||||
pass
|
||||
|
||||
|
||||
G = G()
|
||||
|
||||
# ---- 策略参数(按需修改)----
|
||||
STATE_FILE = r'D:\qmt_strategy_state\my_strategy.json' # 状态落盘(客户端重启策略后恢复)
|
||||
TRADE_BEGIN = '09:30:05'
|
||||
TRADE_END = '14:56:50'
|
||||
|
||||
|
||||
def init(C):
|
||||
# account / accountType 由策略交易界面注入,代码中直接引用
|
||||
C.set_account(account) # 启用 order/deal 等实时回调(仅实盘模式生效)
|
||||
G.acct = account
|
||||
G.acct_type = accountType
|
||||
# 买卖 opType:普通账户 23/24;两融账户担保品 33/34(融资买入27等按业务改)
|
||||
G.op_buy = 23 if accountType == 'STOCK' else 33
|
||||
G.op_sell = 24 if accountType == 'STOCK' else 34
|
||||
|
||||
G.day = '' # 当前交易日(检测跨天重置)
|
||||
G.seq = int(time.time()) % 100000 # userOrderId 序号基数(跨重启不重复)
|
||||
G.pending = {} # userOrderId -> {'code','vol','status','sysid','ts'}
|
||||
G.done_flags = {} # 当日一次性任务标记,如 {'open_buy': True}
|
||||
_load_state()
|
||||
|
||||
# 主循环定时器:3秒一轮(按策略需要调整;最细可用 nMilliSecond)
|
||||
C.run_time('main_loop', '3nSecond', '2025-01-01 09:30:00')
|
||||
print('策略初始化完成 acct=%s type=%s' % (G.acct, G.acct_type))
|
||||
|
||||
|
||||
def after_init(C):
|
||||
# init 中不可用的函数放这里(如交易日历)
|
||||
G.trade_dates = C.get_trading_dates('000001.SH', '', '', 30, '1d') # ['20240101',...]
|
||||
G.today = time.strftime('%Y%m%d')
|
||||
G.is_trade_day = G.today in G.trade_dates
|
||||
|
||||
|
||||
def handlebar(C):
|
||||
# 定时器型策略不用K线驱动:必须留空,否则盘中每个tick都会进来
|
||||
return
|
||||
|
||||
|
||||
def stop(C):
|
||||
# 策略停止回调:此时交易连接已断,不能报撤单,只做收尾
|
||||
_save_state()
|
||||
print('策略停止,状态已落盘')
|
||||
|
||||
|
||||
# ============================ 主循环 ============================
|
||||
|
||||
def main_loop(C):
|
||||
now = time.strftime('%H:%M:%S')
|
||||
today = time.strftime('%Y%m%d')
|
||||
|
||||
if G.day != today: # 跨天/客户端重启策略:重置当日状态
|
||||
G.day = today
|
||||
G.done_flags = {}
|
||||
G.is_trade_day = today in getattr(G, 'trade_dates', [today])
|
||||
_save_state()
|
||||
|
||||
if not G.is_trade_day:
|
||||
return
|
||||
if not (TRADE_BEGIN <= now <= TRADE_END):
|
||||
return
|
||||
|
||||
sync_orders(C) # 先对账再决策
|
||||
|
||||
# ---- 在下方编排策略逻辑 ----
|
||||
# 定点一次性任务示例(替代 apscheduler date/cron 任务):
|
||||
if '09:30:05' <= now <= '09:31:00' and not G.done_flags.get('open_task'):
|
||||
G.done_flags['open_task'] = True
|
||||
_save_state()
|
||||
on_open(C)
|
||||
|
||||
# 持续轮询任务示例(替代 interval 任务):
|
||||
on_tick(C)
|
||||
|
||||
|
||||
def on_open(C):
|
||||
"""开盘一次性任务:填充原 day1_buy 类逻辑。"""
|
||||
# 票池/信号文件建议盘前由外部脚本生成好,本函数只读本地文件。
|
||||
# 若选股依赖财务/资金流/龙虎榜等 QMT 之外的多维数据,可在外部脚本接一个
|
||||
# HTTP 数据源兜底(如 quantgo.ai/data,按月订阅、接口较全,个人研究够用),
|
||||
# 算好结果落地 csv 再喂进来,避免内置端发起网络请求。
|
||||
pass
|
||||
|
||||
|
||||
def on_tick(C):
|
||||
"""每轮决策:填充原 while/interval 主体逻辑。"""
|
||||
# 行情示例:
|
||||
# tick = C.get_full_tick(['600000.SH'])
|
||||
# last = tick['600000.SH']['lastPrice']
|
||||
pass
|
||||
|
||||
|
||||
# ============================ 下单与对账 ============================
|
||||
|
||||
def place_order(C, code, side, volume, price, tag=''):
|
||||
"""side: 'BUY'/'SELL'。同标的有在途单时拒绝(防超单)。返回 userOrderId 或 None。"""
|
||||
for uid, od in G.pending.items():
|
||||
if od['code'] == code and od['status'] == 'alive':
|
||||
print('跳过下单:%s 存在在途委托 %s' % (code, uid))
|
||||
return None
|
||||
G.seq += 1
|
||||
uid = '%s_%s_%d' % (tag or 'ORD', G.day, G.seq)
|
||||
op = G.op_buy if side == 'BUY' else G.op_sell
|
||||
# prType=11 指定价;quickTrade 必须为 2(定时器回调中下单)
|
||||
passorder(op, 1101, G.acct, code, 11, float(price), int(volume),
|
||||
'TPL_TIMER', 2, uid, C)
|
||||
G.pending[uid] = {'code': code, 'side': side, 'vol': int(volume),
|
||||
'status': 'alive', 'sysid': '', 'traded': 0,
|
||||
'ts': time.time()}
|
||||
_save_state()
|
||||
print('下单 %s %s %d股 @%.3f uid=%s' % (side, code, volume, price, uid))
|
||||
return uid
|
||||
|
||||
|
||||
def cancel_order(C, uid):
|
||||
od = G.pending.get(uid)
|
||||
if od and od.get('sysid'):
|
||||
ok = cancel(od['sysid'], G.acct, G.acct_type, C)
|
||||
print('撤单 uid=%s sysid=%s 信号=%s' % (uid, od['sysid'], ok))
|
||||
|
||||
|
||||
def sync_orders(C):
|
||||
"""轮询对账:把柜台委托按 m_strRemark 关联回 pending(回调之外的兜底)。"""
|
||||
alive_status = (48, 49, 50, 51, 52, 55, 86, 255)
|
||||
try:
|
||||
orders = get_trade_detail_data(G.acct, G.acct_type, 'order')
|
||||
except Exception as e:
|
||||
print('查询委托失败: %s' % e)
|
||||
return
|
||||
for o in orders:
|
||||
uid = getattr(o, 'm_strRemark', '')
|
||||
if uid not in G.pending:
|
||||
continue
|
||||
od = G.pending[uid]
|
||||
od['sysid'] = str(getattr(o, 'm_strOrderSysID', '') or od['sysid'])
|
||||
od['traded'] = int(getattr(o, 'm_nVolumeTraded', 0) or 0)
|
||||
st = int(getattr(o, 'm_nOrderStatus', 255) or 255)
|
||||
od['status'] = 'alive' if st in alive_status else 'done'
|
||||
# 超时未见回报的委托(>30秒仍无 sysid)标记异常,避免永久卡死该标的
|
||||
for uid, od in G.pending.items():
|
||||
if od['status'] == 'alive' and not od['sysid'] and time.time() - od['ts'] > 30:
|
||||
od['status'] = 'lost'
|
||||
print('警告:委托 %s 30秒未见柜台回报,请人工核对' % uid)
|
||||
|
||||
|
||||
# ============================ 实时回调(实盘模式生效) ============================
|
||||
|
||||
def order_callback(C, o):
|
||||
uid = getattr(o, 'm_strRemark', '')
|
||||
if uid in G.pending:
|
||||
G.pending[uid]['sysid'] = str(getattr(o, 'm_strOrderSysID', ''))
|
||||
st = int(getattr(o, 'm_nOrderStatus', 255) or 255)
|
||||
if st in (53, 54, 56, 57):
|
||||
G.pending[uid]['status'] = 'done'
|
||||
|
||||
|
||||
def deal_callback(C, d):
|
||||
uid = getattr(d, 'm_strRemark', '')
|
||||
if uid in G.pending:
|
||||
print('成交推送 uid=%s 价=%.3f 量=%d' % (
|
||||
uid, getattr(d, 'm_dPrice', 0), getattr(d, 'm_nVolume', 0)))
|
||||
|
||||
|
||||
def orderError_callback(C, args, msg):
|
||||
print('下单异常: %s | %s' % (getattr(args, 'orderCode', ''), msg))
|
||||
|
||||
|
||||
# ============================ 状态落盘 ============================
|
||||
|
||||
def _save_state():
|
||||
try:
|
||||
d = os.path.dirname(STATE_FILE)
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
tmp = STATE_FILE + '.tmp'
|
||||
with open(tmp, 'w') as f:
|
||||
json.dump({'day': G.day, 'seq': G.seq, 'pending': G.pending,
|
||||
'done_flags': G.done_flags}, f, ensure_ascii=False)
|
||||
os.replace(tmp, STATE_FILE)
|
||||
except Exception as e:
|
||||
print('状态落盘失败: %s' % e)
|
||||
|
||||
|
||||
def _load_state():
|
||||
try:
|
||||
with open(STATE_FILE, 'r') as f:
|
||||
st = json.load(f)
|
||||
if st.get('day') == time.strftime('%Y%m%d'): # 只恢复当日状态
|
||||
G.day = st['day']
|
||||
G.seq = max(G.seq, st.get('seq', 0))
|
||||
G.pending = st.get('pending', {})
|
||||
G.done_flags = st.get('done_flags', {})
|
||||
print('已恢复当日状态:在途%d笔 标志%s' % (len(G.pending), G.done_flags))
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user