feat(config): QMT 部署身份单一权威源 config/qmt_identity.json——P2-18 账号硬编码根治(⚖️-2 用户拍板:确认永为模拟号+令根治硬编码)
根因三层(全案=audit/20261005_qmt_identity_env_plan.md): schtask 无 per-task env+机器级 env 未设→wrapper 被迫字面量;「部署身份」无单一权威源(env 名分裂三个);模拟号零安全压力致 32 文件 88 处 copy-paste 扩散。 - qmt_gate_common.load_identity 正典装载器(:14-59): env SANGUO_QMT_ACCOUNT/BIGQMT_ACCOUNT_ID(兼容别名)>json>RuntimeError fail-loud;SANGUO_IDENTITY_JSON 显式指路不存在=立即报错;查找=repo 相对>C:\sanguo_vnpy_v2\config\(生产副本回退位) - 灾备腿同源: relogin/qmt_bridge_probe/setclip 改 from qmt_gate_common import ACCOUNT - 桥腿镜像同链: xt_gateway._qmt_account/qmt_gateway_client._identity_default_account/is_price_source_vps/bridge_switch_ops(3 处 env 注入+1 处内嵌探针串)/check_xtquant——env 带默认的硬编码默认值全数退役 - xt_eod_wrapper.ps1 改 Get-Content identity json(保持 ASCII);码内 docstring 字面量→占位符(engine/runner/runner_live/live.yaml 注释/README) - .gitignore 白名单 !config/qmt_identity.json(全局 *.json 曾吞之,git check-ignore 实证) - tests/trader/test_qmt_identity.py 五钉: 装载链语义(env 双名覆盖/显式指路 fail)+单源一致性(identity≡live.yaml≡watch_accounts≡gate_common≡各腿镜像,休市表 CI 同步测先例)+码面字面量 grep-pin(生产码面零 66639661,只 config 三件套可含) - 监控 spec: §3.2 账号来源 bullet/§9.A-18 坑条/§12 行数+行号勘正(插入块致 bridge_ping/calendar_probe/relogin_running 引用行号推移);runbook: 非代码资产两行(副本回退路径+生产 wrapper 待改清单=10-08 窗口后 §五③) 生产 wrapper(C:\sanguo_bigqmt 库外)字面量收尾=10-08 复市验证窗口后非代码资产流程;VPS 班车同窗口(护 10-08 07:50 relogin 首班旧码纯净)。trader+api 620 绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -115,3 +115,6 @@ data_xtdata_stage/
|
|||||||
|
|
||||||
# 合成层权重档案(L1模型权重/L2人工覆盖唯一事实源,须入库)
|
# 合成层权重档案(L1模型权重/L2人工覆盖唯一事实源,须入库)
|
||||||
!sanguo_factor/weight_profiles/*.json
|
!sanguo_factor/weight_profiles/*.json
|
||||||
|
|
||||||
|
# QMT 部署身份单一权威源(P2-18 根治 10-05;须随 promote 走,无敏感值=模拟号)
|
||||||
|
!config/qmt_identity.json
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# QMT 部署身份单一权威源方案(P2-18 根治)——66639661 硬编码调研与实施
|
||||||
|
|
||||||
|
> 2026-10-05 傍晚用户拍板(⚖️-2):确认 66639661 **永为模拟柜台账号**;同时指出「硬编码本身是问题」,
|
||||||
|
> 令详调根因并给出硬编码以外的方案。infra 派单(025b4e6):本档落 audit/ + repo 侧实施归 stratergy,
|
||||||
|
> **VPS 班车 = 10-08 复市验证窗口之后**(护 10-08 07:50 relogin 首班纯净)。
|
||||||
|
|
||||||
|
## 一、全量枚举(git grep 实证)
|
||||||
|
|
||||||
|
库内 32 文件 88 处 + 生产侧库外若干。按形态分五类:
|
||||||
|
|
||||||
|
| 形态 | 位置 | 处置 |
|
||||||
|
|------|------|------|
|
||||||
|
| 纯常量 | `qmt_gate_common.py:14`、`qmt_relogin.py:41`、`qmt_bridge_probe.py:9`、`setclip_acc.py:8`、`check_xtquant.py:17` | → 读身份文件 |
|
||||||
|
| env 带硬编码默认 | `xt_gateway.py:22`、`qmt_gateway_client.py:76`、`is_price_source_vps.py:24` | → 删默认值,env > 身份文件 |
|
||||||
|
| wrapper 注入字面量 | `bridge_switch_ops.py:119/276/322/331`、`xt_eod_wrapper.ps1:10`、**生产 `run_shadow_bridge.ps1:7`(C:\sanguo_bigqmt\,不在 git)** | 库内 → 读身份文件;生产 wrapper 10-08 后按非代码资产流程 |
|
||||||
|
| 配置文件(正确形态,保留) | `config/live.yaml:9`、`config/data_platform.yaml:63` | 保留,CI 同步测钉一致 |
|
||||||
|
| 文档/注释/测试 | engine.py:41、runner.py:12、runner_live.py:12/294、live.yaml:4 注释、README、7 份 docs、9 个 test | 码内注释改占位符;docs 归档不动;测试保留 |
|
||||||
|
|
||||||
|
**换号真实成本(改前)**:库内 14 个生产文件 + 生产侧 ≥4 个 wrapper ps1;漏一处 = 静默盯错账号。
|
||||||
|
|
||||||
|
## 二、根因(三层)
|
||||||
|
|
||||||
|
1. **机制层**:Windows schtask 无 per-task env(sentinel_task.xml 实证无 env 节);VPS 机器级 env 未设
|
||||||
|
(`HKLM\...\Session Manager\Environment` reg query 实证空)→ 每个计划任务被迫 wrapper 注入,字面量顺手写进 wrapper。
|
||||||
|
2. **结构层**:「部署身份」(这台机器驱动哪个 QMT 账号)无单一权威源。三代各自发明:
|
||||||
|
07 月 live 腿 = env `SANGUO_QMT_ACCOUNT` > live.yaml > fail(**正规**,96b1924);
|
||||||
|
09 月大QMT 灾备腿 = 纯常量(stdlib 纯净 + 图省事,2b2ae3c/4444848);
|
||||||
|
wrapper 层 = 注入字面量。env 名分裂三个:`ACCOUNT_ID` / `SANGUO_QMT_ACCOUNT` / `BIGQMT_ACCOUNT_ID`。
|
||||||
|
3. **氛围层**:模拟号泄漏无害 → 零安全压力(铁证 `run_shadow_bridge.ps1` 同文件:redis 密码运行时读 conf
|
||||||
|
纪律在线,上一行账号就是字面量)。09-14/19 ops 批次 copy-paste 传播主力。
|
||||||
|
|
||||||
|
## 三、方案比选(已报用户+infra)
|
||||||
|
|
||||||
|
| 方案 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| A 全 env 化 | ❌ 字面量只是从 py 搬家到 ps1(生产 wrapper 已是此形态);灾备腿 fail-loud = 机器最坏时 gate 起不来 |
|
||||||
|
| B 机器级 env(setx /M) | ⭐ 优秀传输层非完整方案;机器事实≠版本管理事实,重建 VPS 丢(本批不采用,留作可选增强) |
|
||||||
|
| **C 单一配置文件权威** | ✅ **采纳**:`config/qmt_identity.json` + 各腿镜像读取器 + CI 同步测(休市表 7f9b413 先例) |
|
||||||
|
| D DB 注册表权威 | ❌ 鸡生蛋:relogin/gate 恰在「别的都坏」时要跑,不能加 DB 依赖(runtime 腿的 sticky 行机制 B1 已在用) |
|
||||||
|
|
||||||
|
**C 的关键约束**:灾备腿(gate/sentinel/relogin)stdlib 纯净 → 配置文件必须 **JSON**(`json` 是标准库,
|
||||||
|
pyyaml 不是)——这也是不能并进现有 `data_platform.yaml` 的原因。
|
||||||
|
|
||||||
|
**定位**:本方案买的是**可维护性**(换号一处生效、杜绝静默盯错账号),不是保密——git 历史已含该号;
|
||||||
|
真上实盘那天的正确动作 = 开新实盘号 + 模拟号退役,而非改配置。
|
||||||
|
|
||||||
|
## 四、实施设计
|
||||||
|
|
||||||
|
### 身份文件 `config/qmt_identity.json`(随 promote 走,git 管理)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"default_account": "66639661",
|
||||||
|
"kind": "simulated",
|
||||||
|
"mini_path": "C:\\国金QMT交易端模拟\\userdata_mini"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(`mini_path` 本批只存档不接线——账号才是 P2-18 的病灶;路径接线留后续按需。)
|
||||||
|
|
||||||
|
### 解析链(各腿一致)
|
||||||
|
|
||||||
|
```
|
||||||
|
env SANGUO_QMT_ACCOUNT > env BIGQMT_ACCOUNT_ID(兼容别名,存量 wrapper 设的就是它)
|
||||||
|
> config/qmt_identity.json > RuntimeError fail-loud(不猜、不默认)
|
||||||
|
```
|
||||||
|
|
||||||
|
文件查找顺序(灾备腿专用后两条):env `SANGUO_IDENTITY_JSON` 显式指路(不存在=立即报错,
|
||||||
|
显式覆盖不静默回退)> `<脚本>/../../config/`(repo 布局)> `C:\sanguo_vnpy_v2\config\`
|
||||||
|
(生产副本位——gate/relogin 以 C:\sanguo_bigqmt 副本运行时 repo 相对路径失效)。
|
||||||
|
|
||||||
|
### 改动清单(repo 侧本批)
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|------|------|
|
||||||
|
| `config/qmt_identity.json` | 新增 |
|
||||||
|
| `scripts/qmt_relogin/qmt_gate_common.py` | `load_identity()` 正典装载器 + `ACCOUNT` 改读它 |
|
||||||
|
| `scripts/qmt_relogin/qmt_relogin.py` / `qmt_bridge_probe.py` / `setclip_acc.py` | `from qmt_gate_common import ACCOUNT`(同目录,消重) |
|
||||||
|
| `sanguo_qmt_bridge/xt_gateway.py` / `check_xtquant.py` | 包内镜像 `_qmt_account()`(env ACCOUNT_ID/SANGUO_QMT_ACCOUNT/BIGQMT_ACCOUNT_ID > json) |
|
||||||
|
| `sanguo_trader/qmt_gateway_client.py` | `_setting()` 删默认字面量 → 镜像链 |
|
||||||
|
| `scripts/pipeline/is_price_source_vps.py` | `_ACCOUNT` 删 → `_identity_default_account()` |
|
||||||
|
| `scripts/bridge_switch_ops.py` | 3 处 env 注入 + 1 处内嵌代码串 → `_qmt_account()` |
|
||||||
|
| `scripts/data_platform/xt_eod_wrapper.ps1` | `Get-Content qmt_identity.json \| ConvertFrom-Json`(保持 ASCII) |
|
||||||
|
| `sanguo_live/engine.py` / `runner.py`、`runner_live.py`、`config/live.yaml:4` 注释 | 字面量 → `<账号>` 占位 |
|
||||||
|
| `sanguo_qmt_bridge/README.md` | 默认值表行 → 指向身份文件 |
|
||||||
|
| `tests/trader/test_qmt_identity.py` | 新增(见下) |
|
||||||
|
| `docs/deployment/vps-production-runbook.md` | 身份文件条目(CI 基建门:qmt_relogin/bridge_switch_ops 触发) |
|
||||||
|
| `docs/superpowers/specs/2026-10-03-monitoring-design.md` | gate 簇身份链一句话 + 坑台账 |
|
||||||
|
|
||||||
|
### 测试策略(tests/trader/test_qmt_identity.py 三组钉)
|
||||||
|
|
||||||
|
1. **装载链语义**:env 两名覆盖、`SANGUO_IDENTITY_JSON` 指空=RuntimeError、模块 `ACCOUNT` ≡ json。
|
||||||
|
2. **单源一致性(anti-drift,休市表先例)**:identity ≡ live.yaml.account ≡ ∈ watch_accounts ≡
|
||||||
|
gate_common.ACCOUNT ≡ xt_gateway/qmt_gateway_client 镜像解析值(env 清空)。
|
||||||
|
3. **字面量 grep-pin**:`66639661` 只允许出现在 config 三件套 + tests/ + docs/ + audit/;
|
||||||
|
生产码面(scripts/、sanguo_*/)零字面量——任何新增扩散直接被单测抓住。
|
||||||
|
|
||||||
|
## 五、分阶段上线
|
||||||
|
|
||||||
|
| 阶段 | 时点 | 内容 |
|
||||||
|
|------|------|------|
|
||||||
|
| ① repo | 本班(10-05) | 上表全部 + 测试 + 两档更新;push 后 CI test 绿即达(nas-deploy 楔死=找 infra 手动 promote,既定协议) |
|
||||||
|
| ② VPS | **10-08 复市验证窗口后**班车 | promote 带上 json + 新脚本;10-08 07:50 relogin 首班跑旧码=纯净验证 |
|
||||||
|
| ③ 生产 wrapper | ②后 | `run_shadow_bridge.ps1` 等库外资产改读 json(runbook 非代码资产流程报备 infra) |
|
||||||
|
| ④ 可选增强 | 按需 | setx /M 机器级兜底;mini_path 接线;watch_accounts 多账号扩展 |
|
||||||
|
|
||||||
|
## 六、回滚
|
||||||
|
|
||||||
|
单 commit revert 即回旧形态(各文件改动均为等价替换,行为差异仅「默认值来源」一项);
|
||||||
|
身份文件缺失时 fail-loud(RuntimeError/启动非零)而非静默错号——方向保守。
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# 实盘模拟交易配置
|
# 实盘模拟交易配置
|
||||||
# 用法:
|
# 用法:
|
||||||
# python -m sanguo_live
|
# python -m sanguo_live
|
||||||
# SANGUO_QMT_ACCOUNT=66639661 python -m sanguo_live
|
# SANGUO_QMT_ACCOUNT=<账号> python -m sanguo_live
|
||||||
#
|
#
|
||||||
# env SANGUO_QMT_ACCOUNT / SANGUO_QMT_PATH 优先于此文件。
|
# env SANGUO_QMT_ACCOUNT / SANGUO_QMT_PATH 优先于此文件。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"default_account": "66639661",
|
||||||
|
"kind": "simulated",
|
||||||
|
"mini_path": "C:\\国金QMT交易端模拟\\userdata_mini"
|
||||||
|
}
|
||||||
@@ -112,14 +112,14 @@
|
|||||||
|
|
||||||
| 资产 | 位置 | 说明 |
|
| 资产 | 位置 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 桥工具箱+防御脚本 | `C:\sanguo_bigqmt\`(源码=repo `scripts/qmt_relogin/`) | 哨兵/探针/relogin/gate/wintree/ui_act/dsp_reset/minall/setclip 等;**改 repo 后必须手动 scp 到 VPS 才生效** |
|
| 桥工具箱+防御脚本 | `C:\sanguo_bigqmt\`(源码=repo `scripts/qmt_relogin/`) | 哨兵/探针/relogin/gate/wintree/ui_act/dsp_reset/minall/setclip 等;**改 repo 后必须手动 scp 到 VPS 才生效**;10-05 起账号读部署身份单一权威源 `C:\sanguo_vnpy_v2\config\qmt_identity.json`(随 promote 走,P2-18 根治——C:\sanguo_bigqmt 副本经回退路径自动取到,无需在此目录另放;方案=audit/20261005_qmt_identity_env_plan.md) |
|
||||||
| glue shim | `C:\sanguo_bigqmt\xtquant_bridge\xtquant\` | 引擎+xt_eod 共用的接口兼容层;改动=双侧部署+relogin |
|
| glue shim | `C:\sanguo_bigqmt\xtquant_bridge\xtquant\` | 引擎+xt_eod 共用的接口兼容层;改动=双侧部署+relogin |
|
||||||
| **桥 server 部署位** | `C:\国金QMT交易端模拟\python\bigqmt_signal_trader\` | 含本地补丁(market_bigqmt.py 归一化等);**⚠️ 客户端升级会重装此树=补丁丢失须重打**;同名包在安装树内有多份拷贝,打补丁先确认运行时 sys.path 实际加载的那份 |
|
| **桥 server 部署位** | `C:\国金QMT交易端模拟\python\bigqmt_signal_trader\` | 含本地补丁(market_bigqmt.py 归一化等);**⚠️ 客户端升级会重装此树=补丁丢失须重打**;同名包在安装树内有多份拷贝,打补丁先确认运行时 sys.path 实际加载的那份 |
|
||||||
| 桥策略第三方包 | `...\bin.x64\Lib\site-packages\` | QMT 策略进程 sys.path 特殊,第三方包放这里(redis-py 用 3.5.3,4.x 不兼容 py3.6) |
|
| 桥策略第三方包 | `...\bin.x64\Lib\site-packages\` | QMT 策略进程 sys.path 特殊,第三方包放这里(redis-py 用 3.5.3,4.x 不兼容 py3.6) |
|
||||||
| redis | `C:\redis\`(Windows 服务) | requirepass;密码运行时读 redis.conf 零落盘 |
|
| redis | `C:\redis\`(Windows 服务) | requirepass;密码运行时读 redis.conf 零落盘 |
|
||||||
| **frps(FRP 服务端)** | `C:\frp\frps.exe -c C:\frp\frps.json`(nssm 服务 `sanguo-frps`,:7000) | 家里 Mac `frpc-beijing` 反连:13000=家Gitea 代码通道 / 13001=claude.mysanguo.online(北京 Caddy 反代)/ 18789=OpenClaw;token 看 frps.json(⚠️同目录 frps.toml 是 07-30 旧文件,勿混);**nssm 崩溃放弃后不自愈,须手动 `sc start sanguo-frps`**;验活=`sc query sanguo-frps`+`netstat -ano | findstr :7000`+首尔 `curl -sI https://claude.mysanguo.online`;**09-19 起哨兵第六探针自动巡检**(`[INFRA]` 行,每 30min 全天候:frps:7000+claude 200+commit%,gate 日志+`gate\qmt_gate_latest_infra.json`;RED 只记录不自愈,修复动作仍是手动 sc start) |
|
| **frps(FRP 服务端)** | `C:\frp\frps.exe -c C:\frp\frps.json`(nssm 服务 `sanguo-frps`,:7000) | 家里 Mac `frpc-beijing` 反连:13000=家Gitea 代码通道 / 13001=claude.mysanguo.online(北京 Caddy 反代)/ 18789=OpenClaw;token 看 frps.json(⚠️同目录 frps.toml 是 07-30 旧文件,勿混);**nssm 崩溃放弃后不自愈,须手动 `sc start sanguo-frps`**;验活=`sc query sanguo-frps`+`netstat -ano | findstr :7000`+首尔 `curl -sI https://claude.mysanguo.online`;**09-19 起哨兵第六探针自动巡检**(`[INFRA]` 行,每 30min 全天候:frps:7000+claude 200+commit%,gate 日志+`gate\qmt_gate_latest_infra.json`;RED 只记录不自愈,修复动作仍是手动 sc start) |
|
||||||
| 登录凭证 | `C:\sanguo_bigqmt\creds\qmt_login.enc` | DPAPI hex blob;换机/换用户即失效 |
|
| 登录凭证 | `C:\sanguo_bigqmt\creds\qmt_login.enc` | DPAPI hex blob;换机/换用户即失效 |
|
||||||
| wrapper ps1 ×5 | `C:\sanguo_bigqmt\run_live_bridge.ps1 / run_shadow_bridge.ps1 / qmt_relogin_run.ps1 / xt_eod wrapper / C:\sanguo_vnpy_v2\run_api.ps1` | **必须 ASCII-only**(PS5.1 无 BOM 按 GBK 解析中文注释可吃掉语句行);重定向已改追加式 `*>>`(保 stdout 证据) |
|
| wrapper ps1 ×5 | `C:\sanguo_bigqmt\run_live_bridge.ps1 / run_shadow_bridge.ps1 / qmt_relogin_run.ps1 / xt_eod wrapper / C:\sanguo_vnpy_v2\run_api.ps1` | **必须 ASCII-only**(PS5.1 无 BOM 按 GBK 解析中文注释可吃掉语句行);重定向已改追加式 `*>>`(保 stdout 证据);`run_shadow_bridge.ps1` 等**账号字面量待改读 identity json**(P2-18 §五③,10-08 复市验证窗口后、改前报备——repo 侧 xt_eod_wrapper.ps1 已改,生产生效位随 promote+班车) |
|
||||||
| 引擎运行时状态 | `C:\sanguo_vnpy_v2\runtime\<inst>\live_state.json` | 含锚(strategy_start_date),策略域所有,重启语义敏感 |
|
| 引擎运行时状态 | `C:\sanguo_vnpy_v2\runtime\<inst>\live_state.json` | 含锚(strategy_start_date),策略域所有,重启语义敏感 |
|
||||||
| gate 留痕 | `C:\sanguo_bigqmt\gate\` | 每日 log + latest.json |
|
| gate 留痕 | `C:\sanguo_bigqmt\gate\` | 每日 log + latest.json |
|
||||||
| schtask 注册本身 | 注册表 | XML 定义各任务;重建任务须原样(SYSTEM/HighestAvailable/频率) |
|
| schtask 注册本身 | 注册表 | XML 定义各任务;重建任务须原样(SYSTEM/HighestAvailable/频率) |
|
||||||
|
|||||||
@@ -92,13 +92,14 @@
|
|||||||
- **流程**(`scripts/qmt_relogin/qmt_relogin.py:171-239`):psutil 发现安装目录(活进程→磁盘缓存 `qmt_install_dir.txt` 冷启兜底)→ `flush_stale_queue`(清滞留只读请求;**队列里发现 ORDER 类=exit 7 拒动**,人工介入)→ taskkill XtItClient 先礼后兵(WM_CLOSE 8s→`/F`;**强制杀已知会跳过实例自启**,此后桥检失败 exit 5 fail loud 不装绿)→ open_qmt **exe 模式优先**(记住会话自动登录,每日 08:50 内建重启已证)→ 桥 RPC 150s 轮询(ping 失败回退 ORDER 类只读查询,登录后订单数据即刻可用)→ 都失败才 credential-typing **login 模式**兜底。
|
- **流程**(`scripts/qmt_relogin/qmt_relogin.py:171-239`):psutil 发现安装目录(活进程→磁盘缓存 `qmt_install_dir.txt` 冷启兜底)→ `flush_stale_queue`(清滞留只读请求;**队列里发现 ORDER 类=exit 7 拒动**,人工介入)→ taskkill XtItClient 先礼后兵(WM_CLOSE 8s→`/F`;**强制杀已知会跳过实例自启**,此后桥检失败 exit 5 fail loud 不装绿)→ open_qmt **exe 模式优先**(记住会话自动登录,每日 08:50 内建重启已证)→ 桥 RPC 150s 轮询(ping 失败回退 ORDER 类只读查询,登录后订单数据即刻可用)→ 都失败才 credential-typing **login 模式**兜底。
|
||||||
- **登录边界(09-09 夜测实证)**:登录框 4 位图形验证码 40s 刷新+默认不记密码——打字登录大概率死在验证码;**正解=一次性手工登录勾选「记住密码+自动登录」**,此后冷启走 exe 自动登录路。礼闭(WM_CLOSE)=登出会话(下次见登录框);强杀=保留会话(下次自动直进主窗)。XtMiniQmt/miniquote=xt_eod 数据腿,曾观察到随杀死亡→每次 kill 后留痕其状态(09-11 数据腿切桥 shim 后缺席=正常,`warn_data_leg` 已 no-op 化 :163-168)。
|
- **登录边界(09-09 夜测实证)**:登录框 4 位图形验证码 40s 刷新+默认不记密码——打字登录大概率死在验证码;**正解=一次性手工登录勾选「记住密码+自动登录」**,此后冷启走 exe 自动登录路。礼闭(WM_CLOSE)=登出会话(下次见登录框);强杀=保留会话(下次自动直进主窗)。XtMiniQmt/miniquote=xt_eod 数据腿,曾观察到随杀死亡→每次 kill 后留痕其状态(09-11 数据腿切桥 shim 后缺席=正常,`warn_data_leg` 已 no-op 化 :163-168)。
|
||||||
- **凭证纪律**:BIGQMT_LOGIN_USER/PASSWORD 由 PS wrapper 从 DPAPI blob 进程内解密注入 env——never in argv / never on disk。
|
- **凭证纪律**:BIGQMT_LOGIN_USER/PASSWORD 由 PS wrapper 从 DPAPI blob 进程内解密注入 env——never in argv / never on disk。
|
||||||
|
- **账号来源(P2-18 根治,10-05)**:`qmt_gate_common.ACCOUNT`(:59) 读部署身份单一权威源 `config/qmt_identity.json`(随 promote 走)——链=env `SANGUO_QMT_ACCOUNT`/`BIGQMT_ACCOUNT_ID`(兼容别名) > json > RuntimeError fail-loud;`SANGUO_IDENTITY_JSON` 显式指路不存在=立即报错;文件查找=repo 相对 > `C:\sanguo_vnpy_v2\config\`(C:\sanguo_bigqmt 副本运行的回退位)。relogin/probe/setclip/bridge_probe 同源 import;桥腿(xt_gateway/qmt_gateway_client/is_price_source_vps/bridge_switch_ops)各自镜像同链,CI 同步测+码面字面量 grep-pin 钉死(tests/trader/test_qmt_identity.py)。方案全档=audit/20261005_qmt_identity_env_plan.md(用户拍板 ⚖️-2:确认永为模拟号;VPS 班车 10-08 窗口后)。
|
||||||
- **退出码**:0 ok|3 凭证缺|4 console 会话锁(fallback 无法打字)|6 客户端关不掉/拉不起|7 滞留队列含订单类|5 登录后桥仍死。
|
- **退出码**:0 ok|3 凭证缺|4 console 会话锁(fallback 无法打字)|6 客户端关不掉/拉不起|7 滞留队列含订单类|5 登录后桥仍死。
|
||||||
- **防双触发**:调用方先查 `relogin_running()`(cmdline 含 qmt_relogin.py 的 python 进程,qmt_gate_common.py:143-158)+ relogin schtask 自身 IgnoreNew。
|
- **防双触发**:调用方先查 `relogin_running()`(cmdline 含 qmt_relogin.py 的 python 进程,qmt_gate_common.py:188-203)+ relogin schtask 自身 IgnoreNew。
|
||||||
|
|
||||||
### §3.3 哨兵(每 30min 全天候,第二层)
|
### §3.3 哨兵(每 30min 全天候,第二层)
|
||||||
- **判红仅两判据(用户核定)**:XtItClient.exe 进程没了 OR 桥 RPC ping 死(`qmt_sentinel.py:117-145`)。账户类探针(cash/mktval/queue_len)=telemetry 只记录不判红(QMT 各会话盘后视图互不一致,盘后假红 by design);数据腿进程(XtMiniQmt/miniquote)不判红。
|
- **判红仅两判据(用户核定)**:XtItClient.exe 进程没了 OR 桥 RPC ping 死(`qmt_sentinel.py:117-145`)。账户类探针(cash/mktval/queue_len)=telemetry 只记录不判红(QMT 各会话盘后视图互不一致,盘后假红 by design);数据腿进程(XtMiniQmt/miniquote)不判红。
|
||||||
- **探针执行序**:⑥INFRA 最先跑(`infra_probe()` :43-114,防交易栈早退跳过)→ ① tasklist 进程三件(`procs_alive`)→ ② `bridge_ping`(redis RPC ping;ok+pong+server_time+rev+allow_order,qmt_gate_common.py:62-77)→ ⑤ `calendar_probe` → ③ telemetry → GREEN 留痕。
|
- **探针执行序**:⑥INFRA 最先跑(`infra_probe()` :43-114,防交易栈早退跳过)→ ① tasklist 进程三件(`procs_alive`)→ ② `bridge_ping`(redis RPC ping;ok+pong+server_time+rev+allow_order,qmt_gate_common.py:107-122)→ ⑤ `calendar_probe` → ③ telemetry → GREEN 留痕。
|
||||||
- **⑤日历契约探针**(qmt_gate_common.py:87-140,09-11 夜加):fresh 子进程经桥调 `xtdata.get_trading_dates('SH','20260101','20261231')` 断言元素 numeric(epoch ms);**字符串元素=RED**(09-11 事故形态:消费方除以 1000,relogin 重启补丁 server 即归一);空列表/exec 异常=AMBER 只记录(探针形状问题不误红)。坑:market 参数必须 `'SH'/'SZ'`(ContextInfo 映射表),传 `'SSE'` 返空列表(§9.A-8)。
|
- **⑤日历契约探针**(qmt_gate_common.py:140-186,09-11 夜加):fresh 子进程经桥调 `xtdata.get_trading_dates('SH','20260101','20261231')` 断言元素 numeric(epoch ms);**字符串元素=RED**(09-11 事故形态:消费方除以 1000,relogin 重启补丁 server 即归一);空列表/exec 异常=AMBER 只记录(探针形状问题不误红)。坑:market 参数必须 `'SH'/'SZ'`(ContextInfo 映射表),传 `'SSE'` 返空列表(§9.A-8)。
|
||||||
- **⑥INFRA 探针**(09-19,runbook#35 波 1):frps:7000 TCP connect + claude.mysanguo.online HEAD 200 + commit 内存占(GlobalMemoryStatusEx,≥90% AMBER 预警在 OOM 级联之前);**纯观察,RED 永不触发 recover**(relogin 治不了 frps,修=手动 `sc start sanguo-frps`);独立状态灯 `qmt_gate_latest_infra.json`。
|
- **⑥INFRA 探针**(09-19,runbook#35 波 1):frps:7000 TCP connect + claude.mysanguo.online HEAD 200 + commit 内存占(GlobalMemoryStatusEx,≥90% AMBER 预警在 OOM 级联之前);**纯观察,RED 永不触发 recover**(relogin 治不了 frps,修=手动 `sc start sanguo-frps`);独立状态灯 `qmt_gate_latest_infra.json`。
|
||||||
- **RED→recover()**(:148-183):relogin 在飞→SKIP 只等桥;否则 `schtasks /run sanguo-qmt-relogin` → `wait_bridge` ≤600s(5s 间隔轮询)→ 复检 → RECOVERED/RED 双留痕。**无退避**(拍板④):每跳独立,失败堆 log 下跳自然重试。
|
- **RED→recover()**(:148-183):relogin 在飞→SKIP 只等桥;否则 `schtasks /run sanguo-qmt-relogin` → `wait_bridge` ≤600s(5s 间隔轮询)→ 复检 → RECOVERED/RED 双留痕。**无退避**(拍板④):每跳独立,失败堆 log 下跳自然重试。
|
||||||
- **gate 日志语义**:`C:\sanguo_bigqmt\gate\qmt_gate_YYYYMMDD.log` 每日 append,行=`YYYY-MM-DD HH:MM:SS [KIND] VERDICT msg`(KIND=SENTINEL/INFRA/PROBE0915…)+ `qmt_gate_latest.json` 覆盖式状态灯(+infra 独立灯)。值班判读=当日 log grep `RED|ACTION|RECOVERED`;哨兵每跳必写一行(GREEN 也写)——**当日 log 无行=哨兵自身死**(在岗面归 data-monitor SCHTASKS 自检+我方值班,§7.5)。
|
- **gate 日志语义**:`C:\sanguo_bigqmt\gate\qmt_gate_YYYYMMDD.log` 每日 append,行=`YYYY-MM-DD HH:MM:SS [KIND] VERDICT msg`(KIND=SENTINEL/INFRA/PROBE0915…)+ `qmt_gate_latest.json` 覆盖式状态灯(+infra 独立灯)。值班判读=当日 log grep `RED|ACTION|RECOVERED`;哨兵每跳必写一行(GREEN 也写)——**当日 log 无行=哨兵自身死**(在岗面归 data-monitor SCHTASKS 自检+我方值班,§7.5)。
|
||||||
@@ -399,7 +400,7 @@ kind 语义(检查分发依据,check_entry data_monitor.py:56-232):
|
|||||||
|
|
||||||
**A-7 探针撤单集合竞价段不确认(09-10 首日遗留单)**:现象=probe 撤单 3 重试后订单仍在柜台列表。根因=09:20-09:25 集合竞价不可撤单+盘后柜台不再处理;QMT 本地持有不上报(无 sysid 不可撤)。判别法=窗口时刻+0.01 限价买 100 股形状。修法坐标=窗口硬卡 09:15:00-09:19:30(qmt_probe_0915.py:31-52);遗留单 0.01 永不成交+日终清算自动消(:99-103 CRITICAL 留痕)——无害,改进方向(未做)=09:30 后补撤。
|
**A-7 探针撤单集合竞价段不确认(09-10 首日遗留单)**:现象=probe 撤单 3 重试后订单仍在柜台列表。根因=09:20-09:25 集合竞价不可撤单+盘后柜台不再处理;QMT 本地持有不上报(无 sysid 不可撤)。判别法=窗口时刻+0.01 限价买 100 股形状。修法坐标=窗口硬卡 09:15:00-09:19:30(qmt_probe_0915.py:31-52);遗留单 0.01 永不成交+日终清算自动消(:99-103 CRITICAL 留痕)——无害,改进方向(未做)=09:30 后补撤。
|
||||||
|
|
||||||
**A-8 calendar_probe market 参数 'SH' 非 'SSE'(09-11)**:现象=第五探针恒 AMBER empty。根因=xtdata.get_trading_dates 的 market 走 ContextInfo 映射表,只认 'SH'/'SZ',传交易所代号 'SSE' 返空列表。判别法=子进程直跑同参数返空。修法坐标=CALENDAR_PROBE_CODE 固定 'SH'(qmt_gate_common.py:87-92)。
|
**A-8 calendar_probe market 参数 'SH' 非 'SSE'(09-11)**:现象=第五探针恒 AMBER empty。根因=xtdata.get_trading_dates 的 market 走 ContextInfo 映射表,只认 'SH'/'SZ',传交易所代号 'SSE' 返空列表。判别法=子进程直跑同参数返空。修法坐标=CALENDAR_PROBE_CODE 固定 'SH'(qmt_gate_common.py:132-137)。
|
||||||
|
|
||||||
**A-9 被动查询测不出会话陈旧(26 废单级事故,防御链第一性)**:现象=隔夜后一切被动探针绿(进程在/ping pong/cash 基线不漂)但报单全废。根因=XtAsset 无交易日字段、桥 ping 绿≠柜台会话当日。判别法=唯一判别性症状=报单被拒/废。修法坐标=主动交易探针=唯一金标准(qmt_probe_0915.py 设计注);行业空白(xbc/cfquant grep 零命中)——勿信任何「被动心跳治日切」的方案。
|
**A-9 被动查询测不出会话陈旧(26 废单级事故,防御链第一性)**:现象=隔夜后一切被动探针绿(进程在/ping pong/cash 基线不漂)但报单全废。根因=XtAsset 无交易日字段、桥 ping 绿≠柜台会话当日。判别法=唯一判别性症状=报单被拒/废。修法坐标=主动交易探针=唯一金标准(qmt_probe_0915.py 设计注);行业空白(xbc/cfquant grep 零命中)——勿信任何「被动心跳治日切」的方案。
|
||||||
|
|
||||||
@@ -419,6 +420,8 @@ kind 语义(检查分发依据,check_entry data_monitor.py:56-232):
|
|||||||
|
|
||||||
**A-17 account_monitor 配置读取宽捕契约(10-05 审计 A-1+RB-C4)**:现象潜势=坏 YAML(yaml.YAMLError 未捕)或根节点非映射(AttributeError)曾令 poll 线程逐轮裸抛。修法坐标=sanguo_live/account_monitor.py `_extra_from_config` 宽捕 `(OSError, ValueError, yaml.YAMLError)`+根节点非映射显式 ValueError+`_cfg_fail_note` 节流(首败+每 30 败心跳)。**ImportError 不入捕=刻意设计**:yaml 上移模块级 import(runner.py:28 同款先例——account_monitor 由 runner 懒加载必先经其 import),缺依赖=进程启动即 fail loud;审计原方「懒加载+except 引 yaml.YAMLError」在 pyyaml 缺失时 except 表达式求值先于赋值→NameError 逃逸(RB-C4 复现留痕,audit/20261003_rebuttal.md)。
|
**A-17 account_monitor 配置读取宽捕契约(10-05 审计 A-1+RB-C4)**:现象潜势=坏 YAML(yaml.YAMLError 未捕)或根节点非映射(AttributeError)曾令 poll 线程逐轮裸抛。修法坐标=sanguo_live/account_monitor.py `_extra_from_config` 宽捕 `(OSError, ValueError, yaml.YAMLError)`+根节点非映射显式 ValueError+`_cfg_fail_note` 节流(首败+每 30 败心跳)。**ImportError 不入捕=刻意设计**:yaml 上移模块级 import(runner.py:28 同款先例——account_monitor 由 runner 懒加载必先经其 import),缺依赖=进程启动即 fail loud;审计原方「懒加载+except 引 yaml.YAMLError」在 pyyaml 缺失时 except 表达式求值先于赋值→NameError 逃逸(RB-C4 复现留痕,audit/20261003_rebuttal.md)。
|
||||||
|
|
||||||
|
**A-18 QMT 账号硬编码扩散(10-05 用户拍板 ⚖️-2 / P2-18 根治)**:现象=模拟柜台账号 66639661 曾硬编码 32 文件 88 处(灾备腿纯常量/桥腿 env 带默认/wrapper 注入字面量/env 名分裂三个),换号需动 14 生产文件+≥4 生产 wrapper。根因三层=schtask 无 per-task env+机器级 env 未设(wrapper 被迫字面量)/「部署身份」无单一权威源(三代各自发明)/模拟号零安全压力致 copy-paste 传播(run_shadow_bridge.ps1 同文件 redis 口令纪律在线、账号却是字面量=铁证)。修法=config/qmt_identity.json 单一权威源(JSON 非 yaml:灾备腿 stdlib 纯净不能依赖 pyyaml)+gate_common.load_identity 正典(:14-59)+各腿镜像+CI 同步测+码面字面量 grep-pin(tests/trader/test_qmt_identity.py,只 config 三件套可含字面量)。判别复用=换号只改 json+两 yaml(live.yaml/data_platform.yaml 由同步测钉一致)。全案=audit/20261005_qmt_identity_env_plan.md;生产 wrapper 字面量(库外 C:\sanguo_bigqmt)10-08 窗口后按非代码资产流程收尾。
|
||||||
|
|
||||||
|
|
||||||
### §9.B 数据面(data,20 条;格式=现象/根因/判别法/修法坐标)
|
### §9.B 数据面(data,20 条;格式=现象/根因/判别法/修法坐标)
|
||||||
1. **mirror 假黄连三日(0926~28)**:push.log DONE 行只带 [HH:MM:SS] 不带日期(仅 start 行带)→旧 `want_day in line` 过滤永剩 start 行。判别=DONE 行格式先实证。修=start 段归属法(nas_monitor_check.py:166-185)。
|
1. **mirror 假黄连三日(0926~28)**:push.log DONE 行只带 [HH:MM:SS] 不带日期(仅 start 行带)→旧 `want_day in line` 过滤永剩 start 行。判别=DONE 行格式先实证。修=start 段归属法(nas_monitor_check.py:166-185)。
|
||||||
@@ -500,7 +503,7 @@ kind 语义(检查分发依据,check_entry data_monitor.py:56-232):
|
|||||||
| data checks | scripts/data_platform/monitor_checks.py | 270 | expected_daily :37 / expected_weekly :49 / rolling_rows_band :106-138(常量 :101-103)/ check_disk 阈值参数化 :150 / `_error_lines` :190 / scan_signatures(仅测试) :196 / signatures_by_type :216 / parse_schtasks_list :248 |
|
| data checks | scripts/data_platform/monitor_checks.py | 270 | expected_daily :37 / expected_weekly :49 / rolling_rows_band :106-138(常量 :101-103)/ check_disk 阈值参数化 :150 / `_error_lines` :190 / scan_signatures(仅测试) :196 / signatures_by_type :216 / parse_schtasks_list :248 |
|
||||||
| alerts_store | sanguo_data/alerts_store.py | 168 | _SCHEMA :12-32 / upsert(severity 跟随最新) :59 / resolve_passed :88 / purge(open 黄 last_seen 28d) :101 / ack :143 / summary :158 |
|
| alerts_store | sanguo_data/alerts_store.py | 168 | _SCHEMA :12-32 / upsert(severity 跟随最新) :59 / resolve_passed :88 / purge(open 黄 last_seen 28d) :101 / ack :143 / summary :158 |
|
||||||
| NAS monitor v2 | scripts/nas_sync/nas_monitor_check.py + run_monitor_nas.sh | 266+47 | EXCHANGE_HOLIDAYS 镜像 :30-42 / last_weekday 交易日版 :43 / build_checks 六族 :141-235 / main+green_keys :238;宿主生效位=/volume1/stock/monitor_nas/(scp -O 同步纪律) |
|
| NAS monitor v2 | scripts/nas_sync/nas_monitor_check.py + run_monitor_nas.sh | 266+47 | EXCHANGE_HOLIDAYS 镜像 :30-42 / last_weekday 交易日版 :43 / build_checks 六族 :141-235 / main+green_keys :238;宿主生效位=/volume1/stock/monitor_nas/(scp -O 同步纪律) |
|
||||||
| QMT 哨兵链 | scripts/qmt_relogin/(回库 dbf57f2) | — | sentinel 188/probe 209/gate_common 179/relogin 243 |
|
| QMT 哨兵链 | scripts/qmt_relogin/(回库 dbf57f2;10-05 起账号读部署身份单一权威源 config/qmt_identity.json,P2-18) | — | sentinel 188/probe 209/gate_common 224/relogin 244 |
|
||||||
|
|
||||||
---
|
---
|
||||||
*建档 infra 2026-10-03;框架版首 commit 待各域认领填充(§0.2 表)。*
|
*建档 infra 2026-10-03;框架版首 commit 待各域认领填充(§0.2 表)。*
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class LiveTradingEngine:
|
|||||||
|
|
||||||
使用:
|
使用:
|
||||||
eng = LiveTradingEngine()
|
eng = LiveTradingEngine()
|
||||||
eng.connect({"交易账号": "66639661", "mini路径": "C:\\\\..."})
|
eng.connect({"交易账号": "<QMT账号>", "mini路径": "C:\\\\..."})
|
||||||
eng.add_strategy(AShareDoubleMaStrategy, "dm1", "600000.SSE", {...})
|
eng.add_strategy(AShareDoubleMaStrategy, "dm1", "600000.SSE", {...})
|
||||||
eng.subscribe(["600000.SSE"])
|
eng.subscribe(["600000.SSE"])
|
||||||
eng.init_all(); eng.start_all()
|
eng.init_all(); eng.start_all()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
python -m sanguo_live # 用 config/live.yaml
|
python -m sanguo_live # 用 config/live.yaml
|
||||||
python -m sanguo_live /path/to/cfg.yaml # 指定配置
|
python -m sanguo_live /path/to/cfg.yaml # 指定配置
|
||||||
python -m sanguo_live --supervisor [db] # DB 驱动 supervisor
|
python -m sanguo_live --supervisor [db] # DB 驱动 supervisor
|
||||||
SANGUO_QMT_ACCOUNT=66639661 python -m sanguo_live
|
SANGUO_QMT_ACCOUNT=<账号> python -m sanguo_live
|
||||||
|
|
||||||
注意:真实下单需要 miniQMT 同机运行且在交易时段。非交易时段运行只校验链路搭建。
|
注意:真实下单需要 miniQMT 同机运行且在交易时段。非交易时段运行只校验链路搭建。
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ live_accounts 行以**子进程**方式拉起本模块,env 传参:
|
|||||||
SANGUO_LIVE_DB / SANGUO_LIVE_ACCOUNT_ID 快照落库目标(缺省不落)
|
SANGUO_LIVE_DB / SANGUO_LIVE_ACCOUNT_ID 快照落库目标(缺省不落)
|
||||||
|
|
||||||
手动用法(交易日 + miniQMT 连接下):
|
手动用法(交易日 + miniQMT 连接下):
|
||||||
set SANGUO_QMT_ACCOUNT=66639661
|
set SANGUO_QMT_ACCOUNT=<账号>
|
||||||
python -m sanguo_portfolio.runner_live
|
python -m sanguo_portfolio.runner_live
|
||||||
|
|
||||||
不在 Mac 跑(Mac 无 xtquant/miniQMT 客户端)。
|
不在 Mac 跑(Mac 无 xtquant/miniQMT 客户端)。
|
||||||
@@ -291,7 +291,7 @@ def run_live(provider_config: Dict[str, Any] | None = None) -> None:
|
|||||||
if not cfg["account"]:
|
if not cfg["account"]:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"缺 SANGUO_QMT_ACCOUNT(miniQMT 交易账号),实盘无法启动。"
|
"缺 SANGUO_QMT_ACCOUNT(miniQMT 交易账号),实盘无法启动。"
|
||||||
"设 set SANGUO_QMT_ACCOUNT=66639661 后重试。"
|
"设 set SANGUO_QMT_ACCOUNT=<账号> 后重试。"
|
||||||
)
|
)
|
||||||
|
|
||||||
provider = build_provider(provider_config)
|
provider = build_provider(provider_config)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pip install -r requirements.txt
|
|||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| `BRIDGE_TOKEN` | **必填**。共享密钥,sanguo 端同值。不设则受保护接口返回 503 | — |
|
| `BRIDGE_TOKEN` | **必填**。共享密钥,sanguo 端同值。不设则受保护接口返回 503 | — |
|
||||||
| `MINIQMT_USERDATA` | miniQMT userdata_mini 路径 | `D:\国金QMT交易端模拟\userdata_mini` |
|
| `MINIQMT_USERDATA` | miniQMT userdata_mini 路径 | `D:\国金QMT交易端模拟\userdata_mini` |
|
||||||
| `ACCOUNT_ID` | miniQMT 账户 ID | `66639661` |
|
| `ACCOUNT_ID` | miniQMT 账户 ID(env 覆盖;不设则读部署身份单一权威源 `config/qmt_identity.json`,P2-18) | 身份文件值 |
|
||||||
| `BRIDGE_SESSION_ID` | xtquant session ID(int) | `20260710` |
|
| `BRIDGE_SESSION_ID` | xtquant session ID(int) | `20260710` |
|
||||||
|
|
||||||
**设置 BRIDGE_TOKEN**(PowerShell):
|
**设置 BRIDGE_TOKEN**(PowerShell):
|
||||||
|
|||||||
@@ -7,14 +7,25 @@ miniQMT 客户端必须先登录并保持运行(极简模式)。
|
|||||||
若某步 API 报错,参考 miniQMT 自带 xtquant 文档 dict.thinktrader.net
|
若某步 API 报错,参考 miniQMT 自带 xtquant 文档 dict.thinktrader.net
|
||||||
或 NAS github-repos/OSkhQuant 源码调整签名(xtquant 版本可能有差异)。
|
或 NAS github-repos/OSkhQuant 源码调整签名(xtquant 版本可能有差异)。
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# ===== 用户填写(开始)=====
|
# ===== 用户填写(开始)=====
|
||||||
# 1. miniQMT 安装目录下的 userdata_mini 文件夹绝对路径
|
# 1. miniQMT 安装目录下的 userdata_mini 文件夹绝对路径
|
||||||
MINIQMT_USERDATA = r"D:\国金QMT交易端模拟\userdata_mini"
|
MINIQMT_USERDATA = r"D:\国金QMT交易端模拟\userdata_mini"
|
||||||
|
|
||||||
# 2. miniQMT 模拟账户 ID(客户端登录后可见的数字账号)—— 这个要你填
|
# 2. miniQMT 模拟账户 ID(客户端登录后可见的数字账号)
|
||||||
ACCOUNT_ID = "66639661"
|
# 默认取部署身份单一权威源 config/qmt_identity.json(P2-18), env 可覆盖
|
||||||
|
_ACC_ENV = (os.environ.get("SANGUO_QMT_ACCOUNT")
|
||||||
|
or os.environ.get("BIGQMT_ACCOUNT_ID"))
|
||||||
|
if not _ACC_ENV:
|
||||||
|
_ident_path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "config",
|
||||||
|
"qmt_identity.json")
|
||||||
|
with open(_ident_path, encoding="utf-8") as _f:
|
||||||
|
_ACC_ENV = json.load(_f)["default_account"]
|
||||||
|
ACCOUNT_ID = _ACC_ENV
|
||||||
|
|
||||||
# 3. xtquant 所在的 site-packages 路径(miniQMT 安装目录\bin.x64\Lib\site-packages)
|
# 3. xtquant 所在的 site-packages 路径(miniQMT 安装目录\bin.x64\Lib\site-packages)
|
||||||
# 若 import 失败,在此目录找不到 xtquant,改 pip install xtquant 后填 None
|
# 若 import 失败,在此目录找不到 xtquant,改 pip install xtquant 后填 None
|
||||||
|
|||||||
@@ -19,7 +19,28 @@ MINIQMT_USERDATA = os.environ.get(
|
|||||||
"MINIQMT_USERDATA",
|
"MINIQMT_USERDATA",
|
||||||
r"D:\国金QMT交易端模拟\userdata_mini",
|
r"D:\国金QMT交易端模拟\userdata_mini",
|
||||||
)
|
)
|
||||||
ACCOUNT_ID = os.environ.get("ACCOUNT_ID", "66639661")
|
|
||||||
|
|
||||||
|
def _qmt_account() -> str:
|
||||||
|
"""部署身份链(env ACCOUNT_ID/SANGUO_QMT_ACCOUNT/BIGQMT_ACCOUNT_ID >
|
||||||
|
config/qmt_identity.json 单一权威源, P2-18 根治): 账号字面量退役。
|
||||||
|
|
||||||
|
镜像自 scripts/qmt_relogin/qmt_gate_common.load_identity(灾备腿正典),
|
||||||
|
CI 同步测钉一致; 文件缺失=FileNotFoundError fail-loud 不猜默认。
|
||||||
|
"""
|
||||||
|
for name in ("ACCOUNT_ID", "SANGUO_QMT_ACCOUNT", "BIGQMT_ACCOUNT_ID"):
|
||||||
|
v = os.environ.get(name)
|
||||||
|
if v:
|
||||||
|
return v.strip()
|
||||||
|
import json
|
||||||
|
p = os.path.normpath(os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "config",
|
||||||
|
"qmt_identity.json"))
|
||||||
|
with open(p, encoding="utf-8") as f:
|
||||||
|
return json.load(f)["default_account"]
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNT_ID = _qmt_account()
|
||||||
SESSION_ID = int(os.environ.get("BRIDGE_SESSION_ID", "20260710"))
|
SESSION_ID = int(os.environ.get("BRIDGE_SESSION_ID", "20260710"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ xtquant 进程内调用),不经 HTTP bridge。
|
|||||||
- ``QmtGatewayClient`` 包装单例 + 缓存,方法签名/返回与 BridgeClient 对齐,live_orchestrator
|
- ``QmtGatewayClient`` 包装单例 + 缓存,方法签名/返回与 BridgeClient 对齐,live_orchestrator
|
||||||
可零改调用方代码替换。
|
可零改调用方代码替换。
|
||||||
|
|
||||||
配置(env):SANGUO_QMT_ACCOUNT(交易账号,默认 66639661)、SANGUO_QMT_PATH(userdata_mini
|
配置(env):SANGUO_QMT_ACCOUNT(交易账号,不设则读部署身份单一权威源
|
||||||
|
config/qmt_identity.json,P2-18)、SANGUO_QMT_PATH(userdata_mini
|
||||||
路径,不设则扫 C:\\ 自动发现,避免中文路径字面量)。
|
路径,不设则扫 C:\\ 自动发现,避免中文路径字面量)。
|
||||||
|
|
||||||
依赖:vnpy 4.4.0 源码(sys.path)+ vnpy_qmt 0.3.3(pip)+ miniQMT 同机运行。
|
依赖:vnpy 4.4.0 源码(sys.path)+ vnpy_qmt 0.3.3(pip)+ miniQMT 同机运行。
|
||||||
@@ -19,6 +20,7 @@ xtquant 进程内调用),不经 HTTP bridge。
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -73,7 +75,9 @@ class _QmtExec:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _setting() -> dict[str, str]:
|
def _setting() -> dict[str, str]:
|
||||||
account = os.environ.get("SANGUO_QMT_ACCOUNT", "66639661")
|
account = (os.environ.get("SANGUO_QMT_ACCOUNT")
|
||||||
|
or os.environ.get("BIGQMT_ACCOUNT_ID")
|
||||||
|
or _identity_default_account())
|
||||||
path = os.environ.get("SANGUO_QMT_PATH") or _find_userdata_mini()
|
path = os.environ.get("SANGUO_QMT_PATH") or _find_userdata_mini()
|
||||||
return {"交易账号": account, "mini路径": path}
|
return {"交易账号": account, "mini路径": path}
|
||||||
|
|
||||||
@@ -89,6 +93,17 @@ class _QmtExec:
|
|||||||
self.orders[o.vt_orderid] = o
|
self.orders[o.vt_orderid] = o
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_default_account() -> str:
|
||||||
|
"""部署身份单一权威源 config/qmt_identity.json(P2-18 根治)——env 不设时
|
||||||
|
的账号兜底; 镜像自 scripts/qmt_relogin/qmt_gate_common(灾备腿正典),
|
||||||
|
CI 同步测钉一致; 文件缺失=FileNotFoundError fail-loud 不猜默认。"""
|
||||||
|
p = os.path.normpath(os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "config",
|
||||||
|
"qmt_identity.json"))
|
||||||
|
with open(p, encoding="utf-8") as f:
|
||||||
|
return json.load(f)["default_account"]
|
||||||
|
|
||||||
|
|
||||||
def _find_userdata_mini() -> str:
|
def _find_userdata_mini() -> str:
|
||||||
"""扫 C:\\ 找 userdata_mini 目录(避免中文路径字面量编码问题)。"""
|
"""扫 C:\\ 找 userdata_mini 目录(避免中文路径字面量编码问题)。"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -26,6 +27,21 @@ LIVE_IDS = list(range(17, 23)) # 17-22
|
|||||||
SHADOW_IDS = list(range(57, 63)) # 57-62 (shadow 双轨)
|
SHADOW_IDS = list(range(57, 63)) # 57-62 (shadow 双轨)
|
||||||
|
|
||||||
|
|
||||||
|
def _qmt_account() -> str:
|
||||||
|
"""部署身份链: env SANGUO_QMT_ACCOUNT/BIGQMT_ACCOUNT_ID >
|
||||||
|
config/qmt_identity.json 单一权威源(P2-18 根治)——账号字面量退役;
|
||||||
|
镜像自 scripts/qmt_relogin/qmt_gate_common(灾备腿正典), CI 同步测钉一致。"""
|
||||||
|
v = (os.environ.get("SANGUO_QMT_ACCOUNT")
|
||||||
|
or os.environ.get("BIGQMT_ACCOUNT_ID"))
|
||||||
|
if v:
|
||||||
|
return v.strip()
|
||||||
|
p = os.path.normpath(os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "config",
|
||||||
|
"qmt_identity.json"))
|
||||||
|
with open(p, encoding="utf-8") as f:
|
||||||
|
return json.load(f)["default_account"]
|
||||||
|
|
||||||
|
|
||||||
def _conn():
|
def _conn():
|
||||||
c = sqlite3.connect(DB, timeout=15)
|
c = sqlite3.connect(DB, timeout=15)
|
||||||
c.row_factory = sqlite3.Row
|
c.row_factory = sqlite3.Row
|
||||||
@@ -116,7 +132,7 @@ def cmd_redis_probe():
|
|||||||
r"C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;"
|
r"C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;"
|
||||||
r"C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3")
|
r"C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3")
|
||||||
env["DEFAULT_DATA_PROVIDER"] = "miniqmt"
|
env["DEFAULT_DATA_PROVIDER"] = "miniqmt"
|
||||||
env["BIGQMT_ACCOUNT_ID"] = "66639661"
|
env["BIGQMT_ACCOUNT_ID"] = _qmt_account()
|
||||||
env["BIGQMT_REDIS_PASSWORD"] = pw
|
env["BIGQMT_REDIS_PASSWORD"] = pw
|
||||||
code = ("import sys; from xtquant import xtdata, xtconstant; "
|
code = ("import sys; from xtquant import xtdata, xtconstant; "
|
||||||
"print('IMPORT_OK', xtconstant.STOCK_BUY); "
|
"print('IMPORT_OK', xtconstant.STOCK_BUY); "
|
||||||
@@ -273,7 +289,7 @@ def cmd_orders():
|
|||||||
r"C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;"
|
r"C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;"
|
||||||
r"C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3")
|
r"C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3")
|
||||||
env["DEFAULT_DATA_PROVIDER"] = "miniqmt"
|
env["DEFAULT_DATA_PROVIDER"] = "miniqmt"
|
||||||
env["BIGQMT_ACCOUNT_ID"] = "66639661"
|
env["BIGQMT_ACCOUNT_ID"] = _qmt_account()
|
||||||
env["BIGQMT_REDIS_PASSWORD"] = pw
|
env["BIGQMT_REDIS_PASSWORD"] = pw
|
||||||
code = (
|
code = (
|
||||||
"from xtquant import xttrader, xtconstant\n"
|
"from xtquant import xttrader, xtconstant\n"
|
||||||
@@ -319,8 +335,9 @@ def cmd_trade_probe():
|
|||||||
env["PYTHONPATH"] = (r"C:\sanguo_bigqmt\xtquant_bridge;"
|
env["PYTHONPATH"] = (r"C:\sanguo_bigqmt\xtquant_bridge;"
|
||||||
r"C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;"
|
r"C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;"
|
||||||
r"C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3")
|
r"C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3")
|
||||||
env["BIGQMT_ACCOUNT_ID"] = "66639661"
|
env["BIGQMT_ACCOUNT_ID"] = _qmt_account()
|
||||||
env["BIGQMT_REDIS_PASSWORD"] = pw
|
env["BIGQMT_REDIS_PASSWORD"] = pw
|
||||||
|
acc = _qmt_account() # 内嵌探针串注入账号(P2-18: 不字面量)
|
||||||
code = (
|
code = (
|
||||||
"from xtquant import xttrader\n"
|
"from xtquant import xttrader\n"
|
||||||
"import os\n"
|
"import os\n"
|
||||||
@@ -328,7 +345,7 @@ def cmd_trade_probe():
|
|||||||
"r'C:\\国金QMT交易端模拟\\userdata_mini', int(os.getpid()) % 100000)\n"
|
"r'C:\\国金QMT交易端模拟\\userdata_mini', int(os.getpid()) % 100000)\n"
|
||||||
"trader.start(); trader.connect()\n"
|
"trader.start(); trader.connect()\n"
|
||||||
"r = None\n"
|
"r = None\n"
|
||||||
"for args in ((), ('66639661',)):\n"
|
+ "for args in ((), (%r,)):\n" % acc +
|
||||||
" try:\n"
|
" try:\n"
|
||||||
" r = trader.query_stock_asset(*args)\n"
|
" r = trader.query_stock_asset(*args)\n"
|
||||||
" if r is not None:\n"
|
" if r is not None:\n"
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
# the 09-09 accidental kill; root cause unresolved).
|
# the 09-09 accidental kill; root cause unresolved).
|
||||||
# NOTE: keep this file ASCII-only (PS 5.1 reads no-BOM files as ANSI/GBK).
|
# NOTE: keep this file ASCII-only (PS 5.1 reads no-BOM files as ANSI/GBK).
|
||||||
$env:PYTHONPATH = 'C:\sanguo_bigqmt\xtquant_bridge;C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3'
|
$env:PYTHONPATH = 'C:\sanguo_bigqmt\xtquant_bridge;C:\sanguo_vnpy_v2;C:\sanguo_vnpy_v2\vnpy_v4.4.0;C:\sanguo_vnpy_v2\vnpy_qmt_v0.3.3'
|
||||||
$env:BIGQMT_ACCOUNT_ID = '66639661'
|
# QMT account from single-source identity file (P2-18 root fix; no literal here)
|
||||||
|
$ident = Get-Content 'C:\sanguo_vnpy_v2\config\qmt_identity.json' -Raw | ConvertFrom-Json
|
||||||
|
$env:BIGQMT_ACCOUNT_ID = $ident.default_account
|
||||||
$env:BIGQMT_REDIS_PASSWORD = (Select-String -Path 'C:\redis\redis.conf' -Pattern '^requirepass\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value })
|
$env:BIGQMT_REDIS_PASSWORD = (Select-String -Path 'C:\redis\redis.conf' -Pattern '^requirepass\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value })
|
||||||
Set-Location C:\sanguo_vnpy_v2
|
Set-Location C:\sanguo_vnpy_v2
|
||||||
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||||
|
|||||||
@@ -21,11 +21,22 @@ logger = logging.getLogger(__name__)
|
|||||||
_BRIDGE_PYROOT = os.environ.get(
|
_BRIDGE_PYROOT = os.environ.get(
|
||||||
"SANGUO_BRIDGE_PYROOT", r"C:\sanguo_bigqmt\xtquant_bridge")
|
"SANGUO_BRIDGE_PYROOT", r"C:\sanguo_bigqmt\xtquant_bridge")
|
||||||
_VNPY_ROOT = os.environ.get("SANGUO_VNPY_ROOT", r"C:\sanguo_vnpy_v2")
|
_VNPY_ROOT = os.environ.get("SANGUO_VNPY_ROOT", r"C:\sanguo_vnpy_v2")
|
||||||
_ACCOUNT = os.environ.get("BIGQMT_ACCOUNT_ID", "66639661")
|
|
||||||
_DAILY_DB = os.environ.get(
|
_DAILY_DB = os.environ.get(
|
||||||
"SANGUO_DAILY_DB", r"C:\sanguo_vnpy_v2\data\quant_trading.db")
|
"SANGUO_DAILY_DB", r"C:\sanguo_vnpy_v2\data\quant_trading.db")
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_default_account() -> str:
|
||||||
|
"""部署身份单一权威源 config/qmt_identity.json(P2-18 根治)——env 不设时
|
||||||
|
的账号兜底(wrapper 已设 BIGQMT_ACCOUNT_ID 时不触此); 镜像自
|
||||||
|
scripts/qmt_relogin/qmt_gate_common(灾备腿正典), CI 同步测钉一致。"""
|
||||||
|
import json
|
||||||
|
p = os.path.normpath(os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "..", "config",
|
||||||
|
"qmt_identity.json"))
|
||||||
|
with open(p, encoding="utf-8") as f:
|
||||||
|
return json.load(f)["default_account"]
|
||||||
|
|
||||||
|
|
||||||
def normalize_symbol(sym: str) -> str:
|
def normalize_symbol(sym: str) -> str:
|
||||||
"""600276.SH/600276/sh600276/600276.XSHG → 600276.SH(SZ 对称)。"""
|
"""600276.SH/600276/sh600276/600276.XSHG → 600276.SH(SZ 对称)。"""
|
||||||
s = sym.strip().upper()
|
s = sym.strip().upper()
|
||||||
@@ -60,7 +71,7 @@ def _ensure_bridge_env() -> None:
|
|||||||
parts.append(p)
|
parts.append(p)
|
||||||
os.environ["PYTHONPATH"] = os.pathsep.join(parts)
|
os.environ["PYTHONPATH"] = os.pathsep.join(parts)
|
||||||
os.environ.setdefault("DEFAULT_DATA_PROVIDER", "miniqmt")
|
os.environ.setdefault("DEFAULT_DATA_PROVIDER", "miniqmt")
|
||||||
os.environ.setdefault("BIGQMT_ACCOUNT_ID", _ACCOUNT)
|
os.environ.setdefault("BIGQMT_ACCOUNT_ID", _identity_default_account())
|
||||||
if "BIGQMT_REDIS_PASSWORD" not in os.environ:
|
if "BIGQMT_REDIS_PASSWORD" not in os.environ:
|
||||||
os.environ["BIGQMT_REDIS_PASSWORD"] = _read_redis_password()
|
os.environ["BIGQMT_REDIS_PASSWORD"] = _read_redis_password()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import re
|
|||||||
import redis
|
import redis
|
||||||
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
|
from bigqmt_signal_trader.redis_rpc import call_redis_rpc
|
||||||
|
|
||||||
ACCOUNT = "66639661"
|
from qmt_gate_common import ACCOUNT # 部署身份单一权威源(P2-18)
|
||||||
with open(r"C:\redis\redis.conf") as f:
|
with open(r"C:\redis\redis.conf") as f:
|
||||||
pw = re.search(r"^requirepass (\S+)", f.read(), re.M).group(1)
|
pw = re.search(r"^requirepass (\S+)", f.read(), re.M).group(1)
|
||||||
rc = redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
|
rc = redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
|
||||||
|
|||||||
@@ -8,10 +8,55 @@ qmt_gate_latest.json (overwrite status light).
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
ACCOUNT = "66639661"
|
# ---- 部署身份单一权威源(P2-18 根治, 2026-10-05): config/qmt_identity.json ----
|
||||||
|
# 链: env SANGUO_QMT_ACCOUNT / BIGQMT_ACCOUNT_ID(兼容别名) > 身份文件;
|
||||||
|
# env SANGUO_IDENTITY_JSON 显式指路时文件必须存在(显式覆盖不静默回退);
|
||||||
|
# 常规查找: <本文件>../../config/(repo 布局) > C:\sanguo_vnpy_v2\config\
|
||||||
|
# (生产副本位——gate/relogin 以 C:\sanguo_bigqmt 副本运行时 repo 相对路径失效)。
|
||||||
|
# 全部不可得=RuntimeError 启动即炸(schtask last_result!=0 立即可见, A-1
|
||||||
|
# fail-loud 先例)。设计档=audit/20261005_qmt_identity_env_plan.md。
|
||||||
|
_IDENTITY_FILENAME = "qmt_identity.json"
|
||||||
|
_VPS_REPO_CONFIG = r"C:\sanguo_vnpy_v2\config\qmt_identity.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_candidates():
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
return [
|
||||||
|
os.environ.get("SANGUO_IDENTITY_JSON") or "",
|
||||||
|
os.path.normpath(os.path.join(
|
||||||
|
here, "..", "..", "config", _IDENTITY_FILENAME)),
|
||||||
|
_VPS_REPO_CONFIG,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_identity():
|
||||||
|
"""部署身份 dict(env 账号覆盖 > json); 文件不可得=RuntimeError(fail-loud)."""
|
||||||
|
explicit = os.environ.get("SANGUO_IDENTITY_JSON")
|
||||||
|
if explicit and not os.path.isfile(explicit):
|
||||||
|
raise RuntimeError(
|
||||||
|
"SANGUO_IDENTITY_JSON 指向的身份文件不存在: %s" % explicit)
|
||||||
|
path = next((p for p in _identity_candidates() if p and os.path.isfile(p)),
|
||||||
|
None)
|
||||||
|
if path is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"部署身份文件 qmt_identity.json 未找到(查找=%s); 应随代码部署于"
|
||||||
|
" config/ 下" % ", ".join(p for p in _identity_candidates() if p))
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
ident = json.load(f)
|
||||||
|
acc = (os.environ.get("SANGUO_QMT_ACCOUNT")
|
||||||
|
or os.environ.get("BIGQMT_ACCOUNT_ID"))
|
||||||
|
if acc:
|
||||||
|
ident = dict(ident, default_account=acc.strip())
|
||||||
|
if not ident.get("default_account"):
|
||||||
|
raise RuntimeError("身份文件缺 default_account: %s" % path)
|
||||||
|
return ident
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNT = load_identity()["default_account"]
|
||||||
GATE_DIR = r"C:\sanguo_bigqmt\gate"
|
GATE_DIR = r"C:\sanguo_bigqmt\gate"
|
||||||
RELOGIN_TASK = "sanguo-qmt-relogin"
|
RELOGIN_TASK = "sanguo-qmt-relogin"
|
||||||
QMT_PROCS = ("XtItClient.exe", "XtMiniQmt.exe", "miniquote.exe")
|
QMT_PROCS = ("XtItClient.exe", "XtMiniQmt.exe", "miniquote.exe")
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
ACCOUNT = "66639661"
|
from qmt_gate_common import ACCOUNT # 部署身份单一权威源(P2-18)
|
||||||
|
|
||||||
CACHE_PATH = r"C:\sanguo_bigqmt\qmt_install_dir.txt"
|
CACHE_PATH = r"C:\sanguo_bigqmt\qmt_install_dir.txt"
|
||||||
QUEUE_KEY = "bigqmt:rpc:queue:%s" % ACCOUNT
|
QUEUE_KEY = "bigqmt:rpc:queue:%s" % ACCOUNT
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import wintypes
|
from ctypes import wintypes
|
||||||
|
|
||||||
|
from qmt_gate_common import ACCOUNT # 部署身份单一权威源(P2-18)
|
||||||
|
|
||||||
u = ctypes.windll.user32
|
u = ctypes.windll.user32
|
||||||
k = ctypes.windll.kernel32
|
k = ctypes.windll.kernel32
|
||||||
s = "66639661"
|
s = ACCOUNT
|
||||||
buf = ctypes.create_unicode_buffer(s)
|
buf = ctypes.create_unicode_buffer(s)
|
||||||
if not u.OpenClipboard(0):
|
if not u.OpenClipboard(0):
|
||||||
print("CLIP_OPEN_FAIL")
|
print("CLIP_OPEN_FAIL")
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""QMT 部署身份单一权威源契约(P2-18 根治, 2026-10-05)——config/qmt_identity.json。
|
||||||
|
|
||||||
|
三组钉(audit/20261005_qmt_identity_env_plan.md §四):
|
||||||
|
① 装载链语义: env SANGUO_QMT_ACCOUNT / BIGQMT_ACCOUNT_ID(兼容别名)覆盖 > json;
|
||||||
|
SANGUO_IDENTITY_JSON 显式指路不存在=RuntimeError(显式覆盖不静默回退)。
|
||||||
|
② 单源一致性(anti-drift, 休市表 CI 同步测先例): identity.default_account ≡
|
||||||
|
live.yaml.account ≡ ∈ data_platform.yaml watch_accounts ≡ gate_common.ACCOUNT
|
||||||
|
≡ 各腿镜像解析值(env 清空)。
|
||||||
|
③ 字面量 grep-pin: "66639661" 只允许出现在 config 三件套; 生产码面
|
||||||
|
(scripts/、sanguo_*)零字面量——任何新增扩散直接被本单测抓住。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
_REPO = Path(__file__).resolve().parents[2]
|
||||||
|
_CFG = _REPO / "config"
|
||||||
|
_GATE_DIR = _REPO / "scripts" / "qmt_relogin"
|
||||||
|
|
||||||
|
if str(_GATE_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_GATE_DIR))
|
||||||
|
|
||||||
|
import qmt_gate_common as gate # noqa: E402
|
||||||
|
|
||||||
|
_ENV_NAMES = ("SANGUO_QMT_ACCOUNT", "BIGQMT_ACCOUNT_ID", "ACCOUNT_ID")
|
||||||
|
|
||||||
|
|
||||||
|
def _ident() -> dict:
|
||||||
|
return json.loads(
|
||||||
|
(_CFG / "qmt_identity.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- ① 装载链语义 ----------------
|
||||||
|
|
||||||
|
def test_load_identity_env_overrides(monkeypatch):
|
||||||
|
for n in _ENV_NAMES + ("SANGUO_IDENTITY_JSON",):
|
||||||
|
monkeypatch.delenv(n, raising=False)
|
||||||
|
base = gate.load_identity()
|
||||||
|
assert base["default_account"] == _ident()["default_account"]
|
||||||
|
monkeypatch.setenv("SANGUO_QMT_ACCOUNT", "123")
|
||||||
|
assert gate.load_identity()["default_account"] == "123"
|
||||||
|
monkeypatch.delenv("SANGUO_QMT_ACCOUNT")
|
||||||
|
monkeypatch.setenv("BIGQMT_ACCOUNT_ID", "456") # 兼容别名
|
||||||
|
assert gate.load_identity()["default_account"] == "456"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_identity_explicit_path_missing_fails(monkeypatch):
|
||||||
|
for n in _ENV_NAMES:
|
||||||
|
monkeypatch.delenv(n, raising=False)
|
||||||
|
monkeypatch.setenv("SANGUO_IDENTITY_JSON", "/nonexistent/x.json")
|
||||||
|
with pytest.raises(RuntimeError, match="SANGUO_IDENTITY_JSON"):
|
||||||
|
gate.load_identity()
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_account_bound_to_identity():
|
||||||
|
assert gate.ACCOUNT == _ident()["default_account"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- ② 单源一致性 ----------------
|
||||||
|
|
||||||
|
def test_single_source_agreement(monkeypatch):
|
||||||
|
for n in _ENV_NAMES:
|
||||||
|
monkeypatch.delenv(n, raising=False)
|
||||||
|
monkeypatch.setenv("SANGUO_QMT_PATH", "/tmp") # 防扫 C:\
|
||||||
|
ident = _ident()
|
||||||
|
|
||||||
|
live = yaml.safe_load((_CFG / "live.yaml").read_text(encoding="utf-8"))
|
||||||
|
assert live["account"] == ident["default_account"]
|
||||||
|
|
||||||
|
dp = yaml.safe_load(
|
||||||
|
(_CFG / "data_platform.yaml").read_text(encoding="utf-8"))
|
||||||
|
assert ident["default_account"] in dp["live_trading"]["watch_accounts"]
|
||||||
|
|
||||||
|
assert gate.ACCOUNT == ident["default_account"]
|
||||||
|
|
||||||
|
from sanguo_qmt_bridge import xt_gateway
|
||||||
|
assert xt_gateway.ACCOUNT_ID == ident["default_account"]
|
||||||
|
|
||||||
|
from sanguo_trader import qmt_gateway_client as qgc
|
||||||
|
setting = qgc._QmtExec._setting()
|
||||||
|
assert setting["交易账号"] == ident["default_account"]
|
||||||
|
|
||||||
|
sys.path.insert(0, str(_REPO / "scripts"))
|
||||||
|
sys.path.insert(0, str(_REPO / "scripts" / "pipeline"))
|
||||||
|
try:
|
||||||
|
import bridge_switch_ops as bso # noqa: E402
|
||||||
|
assert bso._qmt_account() == ident["default_account"]
|
||||||
|
|
||||||
|
import is_price_source_vps as ips # noqa: E402
|
||||||
|
assert ips._identity_default_account() == ident["default_account"]
|
||||||
|
finally:
|
||||||
|
sys.path.remove(str(_REPO / "scripts"))
|
||||||
|
sys.path.remove(str(_REPO / "scripts" / "pipeline"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- ③ 字面量 grep-pin ----------------
|
||||||
|
|
||||||
|
_CODE_ROOTS = ("scripts", "sanguo_live", "sanguo_trader", "sanguo_qmt_bridge",
|
||||||
|
"sanguo_portfolio", "sanguo_data", "sanguo_factor")
|
||||||
|
_ALLOWED_EXACT = {
|
||||||
|
"config/qmt_identity.json", "config/live.yaml", "config/data_platform.yaml",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_account_literal_confined_to_config():
|
||||||
|
"""生产码面零账号字面量: 只 config 三件套可含(P2-18 前码面曾有 17 处)。"""
|
||||||
|
offenders = []
|
||||||
|
for root in _CODE_ROOTS:
|
||||||
|
base = _REPO / root
|
||||||
|
if not base.is_dir():
|
||||||
|
continue
|
||||||
|
for p in base.rglob("*"):
|
||||||
|
if (not p.is_file() or p.suffix not in
|
||||||
|
{".py", ".ps1", ".xml", ".md", ".yaml", ".json"}):
|
||||||
|
continue
|
||||||
|
rel = p.relative_to(_REPO).as_posix()
|
||||||
|
if rel in _ALLOWED_EXACT:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
text = p.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if "66639661" in text:
|
||||||
|
offenders.append(rel)
|
||||||
|
assert offenders == [], "账号字面量泄漏进码面: %s" % offenders
|
||||||
Reference in New Issue
Block a user