diff --git a/.gitignore b/.gitignore index 0df2a769..925b625e 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,6 @@ data_xtdata_stage/ # 合成层权重档案(L1模型权重/L2人工覆盖唯一事实源,须入库) !sanguo_factor/weight_profiles/*.json + +# QMT 部署身份单一权威源(P2-18 根治 10-05;须随 promote 走,无敏感值=模拟号) +!config/qmt_identity.json diff --git a/audit/20261005_qmt_identity_env_plan.md b/audit/20261005_qmt_identity_env_plan.md new file mode 100644 index 00000000..c12000d0 --- /dev/null +++ b/audit/20261005_qmt_identity_env_plan.md @@ -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/启动非零)而非静默错号——方向保守。 diff --git a/config/live.yaml b/config/live.yaml index f7dee452..49db1242 100644 --- a/config/live.yaml +++ b/config/live.yaml @@ -1,7 +1,7 @@ # 实盘模拟交易配置 # 用法: # 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 优先于此文件。 diff --git a/config/qmt_identity.json b/config/qmt_identity.json new file mode 100644 index 00000000..a6c87beb --- /dev/null +++ b/config/qmt_identity.json @@ -0,0 +1,5 @@ +{ + "default_account": "66639661", + "kind": "simulated", + "mini_path": "C:\\国金QMT交易端模拟\\userdata_mini" +} diff --git a/docs/deployment/vps-production-runbook.md b/docs/deployment/vps-production-runbook.md index e09c9f02..78975f8e 100644 --- a/docs/deployment/vps-production-runbook.md +++ b/docs/deployment/vps-production-runbook.md @@ -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 | | **桥 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) | | 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) | | 登录凭证 | `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\\live_state.json` | 含锚(strategy_start_date),策略域所有,重启语义敏感 | | gate 留痕 | `C:\sanguo_bigqmt\gate\` | 每日 log + latest.json | | schtask 注册本身 | 注册表 | XML 定义各任务;重建任务须原样(SYSTEM/HighestAvailable/频率) | diff --git a/docs/superpowers/specs/2026-10-03-monitoring-design.md b/docs/superpowers/specs/2026-10-03-monitoring-design.md index 149f4edf..d65736fb 100644 --- a/docs/superpowers/specs/2026-10-03-monitoring-design.md +++ b/docs/superpowers/specs/2026-10-03-monitoring-design.md @@ -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 模式**兜底。 - **登录边界(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。 +- **账号来源(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 登录后桥仍死。 -- **防双触发**:调用方先查 `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 全天候,第二层) - **判红仅两判据(用户核定)**: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 留痕。 -- **⑤日历契约探针**(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)。 +- **探针执行序**:⑥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: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`。 - **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)。 @@ -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-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 零命中)——勿信任何「被动心跳治日切」的方案。 @@ -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-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 条;格式=现象/根因/判别法/修法坐标) 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 | | 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 同步纪律) | -| 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 表)。* diff --git a/sanguo_live/engine.py b/sanguo_live/engine.py index 1ac99727..3398e805 100644 --- a/sanguo_live/engine.py +++ b/sanguo_live/engine.py @@ -38,7 +38,7 @@ class LiveTradingEngine: 使用: eng = LiveTradingEngine() - eng.connect({"交易账号": "66639661", "mini路径": "C:\\\\..."}) + eng.connect({"交易账号": "", "mini路径": "C:\\\\..."}) eng.add_strategy(AShareDoubleMaStrategy, "dm1", "600000.SSE", {...}) eng.subscribe(["600000.SSE"]) eng.init_all(); eng.start_all() diff --git a/sanguo_live/runner.py b/sanguo_live/runner.py index a190664b..8ef4450f 100644 --- a/sanguo_live/runner.py +++ b/sanguo_live/runner.py @@ -9,7 +9,7 @@ python -m sanguo_live # 用 config/live.yaml python -m sanguo_live /path/to/cfg.yaml # 指定配置 python -m sanguo_live --supervisor [db] # DB 驱动 supervisor - SANGUO_QMT_ACCOUNT=66639661 python -m sanguo_live + SANGUO_QMT_ACCOUNT=<账号> python -m sanguo_live 注意:真实下单需要 miniQMT 同机运行且在交易时段。非交易时段运行只校验链路搭建。 """ diff --git a/sanguo_portfolio/runner_live.py b/sanguo_portfolio/runner_live.py index 83549043..a7963710 100644 --- a/sanguo_portfolio/runner_live.py +++ b/sanguo_portfolio/runner_live.py @@ -9,7 +9,7 @@ live_accounts 行以**子进程**方式拉起本模块,env 传参: SANGUO_LIVE_DB / SANGUO_LIVE_ACCOUNT_ID 快照落库目标(缺省不落) 手动用法(交易日 + miniQMT 连接下): - set SANGUO_QMT_ACCOUNT=66639661 + set SANGUO_QMT_ACCOUNT=<账号> python -m sanguo_portfolio.runner_live 不在 Mac 跑(Mac 无 xtquant/miniQMT 客户端)。 @@ -291,7 +291,7 @@ def run_live(provider_config: Dict[str, Any] | None = None) -> None: if not cfg["account"]: raise RuntimeError( "缺 SANGUO_QMT_ACCOUNT(miniQMT 交易账号),实盘无法启动。" - "设 set SANGUO_QMT_ACCOUNT=66639661 后重试。" + "设 set SANGUO_QMT_ACCOUNT=<账号> 后重试。" ) provider = build_provider(provider_config) diff --git a/sanguo_qmt_bridge/README.md b/sanguo_qmt_bridge/README.md index be0ed3f8..9bcd766d 100644 --- a/sanguo_qmt_bridge/README.md +++ b/sanguo_qmt_bridge/README.md @@ -30,7 +30,7 @@ pip install -r requirements.txt |------|------|--------| | `BRIDGE_TOKEN` | **必填**。共享密钥,sanguo 端同值。不设则受保护接口返回 503 | — | | `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_TOKEN**(PowerShell): diff --git a/sanguo_qmt_bridge/check_xtquant.py b/sanguo_qmt_bridge/check_xtquant.py index ccaad70c..a9562767 100644 --- a/sanguo_qmt_bridge/check_xtquant.py +++ b/sanguo_qmt_bridge/check_xtquant.py @@ -7,14 +7,25 @@ miniQMT 客户端必须先登录并保持运行(极简模式)。 若某步 API 报错,参考 miniQMT 自带 xtquant 文档 dict.thinktrader.net 或 NAS github-repos/OSkhQuant 源码调整签名(xtquant 版本可能有差异)。 """ +import json +import os import sys # ===== 用户填写(开始)===== # 1. miniQMT 安装目录下的 userdata_mini 文件夹绝对路径 MINIQMT_USERDATA = r"D:\国金QMT交易端模拟\userdata_mini" -# 2. miniQMT 模拟账户 ID(客户端登录后可见的数字账号)—— 这个要你填 -ACCOUNT_ID = "66639661" +# 2. miniQMT 模拟账户 ID(客户端登录后可见的数字账号) +# 默认取部署身份单一权威源 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) # 若 import 失败,在此目录找不到 xtquant,改 pip install xtquant 后填 None diff --git a/sanguo_qmt_bridge/xt_gateway.py b/sanguo_qmt_bridge/xt_gateway.py index 5a3bae72..d1086694 100644 --- a/sanguo_qmt_bridge/xt_gateway.py +++ b/sanguo_qmt_bridge/xt_gateway.py @@ -19,7 +19,28 @@ MINIQMT_USERDATA = os.environ.get( "MINIQMT_USERDATA", 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")) diff --git a/sanguo_trader/qmt_gateway_client.py b/sanguo_trader/qmt_gateway_client.py index 850592c3..76140c10 100644 --- a/sanguo_trader/qmt_gateway_client.py +++ b/sanguo_trader/qmt_gateway_client.py @@ -11,7 +11,8 @@ xtquant 进程内调用),不经 HTTP bridge。 - ``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:\\ 自动发现,避免中文路径字面量)。 依赖:vnpy 4.4.0 源码(sys.path)+ vnpy_qmt 0.3.3(pip)+ miniQMT 同机运行。 @@ -19,6 +20,7 @@ xtquant 进程内调用),不经 HTTP bridge。 from __future__ import annotations import logging +import json import os import threading from typing import Any @@ -73,7 +75,9 @@ class _QmtExec: @staticmethod 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() return {"交易账号": account, "mini路径": path} @@ -89,6 +93,17 @@ class _QmtExec: 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: """扫 C:\\ 找 userdata_mini 目录(避免中文路径字面量编码问题)。""" try: diff --git a/scripts/bridge_switch_ops.py b/scripts/bridge_switch_ops.py index 90a19a04..90dac056 100644 --- a/scripts/bridge_switch_ops.py +++ b/scripts/bridge_switch_ops.py @@ -14,6 +14,7 @@ """ from __future__ import annotations +import json import os import re import sqlite3 @@ -26,6 +27,21 @@ LIVE_IDS = list(range(17, 23)) # 17-22 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(): c = sqlite3.connect(DB, timeout=15) 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\vnpy_qmt_v0.3.3") env["DEFAULT_DATA_PROVIDER"] = "miniqmt" - env["BIGQMT_ACCOUNT_ID"] = "66639661" + env["BIGQMT_ACCOUNT_ID"] = _qmt_account() env["BIGQMT_REDIS_PASSWORD"] = pw code = ("import sys; from xtquant import xtdata, xtconstant; " "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\vnpy_qmt_v0.3.3") env["DEFAULT_DATA_PROVIDER"] = "miniqmt" - env["BIGQMT_ACCOUNT_ID"] = "66639661" + env["BIGQMT_ACCOUNT_ID"] = _qmt_account() env["BIGQMT_REDIS_PASSWORD"] = pw code = ( "from xtquant import xttrader, xtconstant\n" @@ -319,8 +335,9 @@ def cmd_trade_probe(): 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\vnpy_qmt_v0.3.3") - env["BIGQMT_ACCOUNT_ID"] = "66639661" + env["BIGQMT_ACCOUNT_ID"] = _qmt_account() env["BIGQMT_REDIS_PASSWORD"] = pw + acc = _qmt_account() # 内嵌探针串注入账号(P2-18: 不字面量) code = ( "from xtquant import xttrader\n" "import os\n" @@ -328,7 +345,7 @@ def cmd_trade_probe(): "r'C:\\国金QMT交易端模拟\\userdata_mini', int(os.getpid()) % 100000)\n" "trader.start(); trader.connect()\n" "r = None\n" - "for args in ((), ('66639661',)):\n" + + "for args in ((), (%r,)):\n" % acc + " try:\n" " r = trader.query_stock_asset(*args)\n" " if r is not None:\n" diff --git a/scripts/data_platform/xt_eod_wrapper.ps1 b/scripts/data_platform/xt_eod_wrapper.ps1 index bb3a518f..e6929043 100644 --- a/scripts/data_platform/xt_eod_wrapper.ps1 +++ b/scripts/data_platform/xt_eod_wrapper.ps1 @@ -7,7 +7,9 @@ # the 09-09 accidental kill; root cause unresolved). # 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: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 }) Set-Location C:\sanguo_vnpy_v2 $ts = Get-Date -Format 'yyyyMMdd_HHmmss' diff --git a/scripts/pipeline/is_price_source_vps.py b/scripts/pipeline/is_price_source_vps.py index 52a58598..9a2f1656 100644 --- a/scripts/pipeline/is_price_source_vps.py +++ b/scripts/pipeline/is_price_source_vps.py @@ -21,11 +21,22 @@ logger = logging.getLogger(__name__) _BRIDGE_PYROOT = os.environ.get( "SANGUO_BRIDGE_PYROOT", r"C:\sanguo_bigqmt\xtquant_bridge") _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( "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: """600276.SH/600276/sh600276/600276.XSHG → 600276.SH(SZ 对称)。""" s = sym.strip().upper() @@ -60,7 +71,7 @@ def _ensure_bridge_env() -> None: parts.append(p) os.environ["PYTHONPATH"] = os.pathsep.join(parts) 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: os.environ["BIGQMT_REDIS_PASSWORD"] = _read_redis_password() diff --git a/scripts/qmt_relogin/qmt_bridge_probe.py b/scripts/qmt_relogin/qmt_bridge_probe.py index 01564eee..ca38bbc2 100644 --- a/scripts/qmt_relogin/qmt_bridge_probe.py +++ b/scripts/qmt_relogin/qmt_bridge_probe.py @@ -6,7 +6,7 @@ import re import redis 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: 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, diff --git a/scripts/qmt_relogin/qmt_gate_common.py b/scripts/qmt_relogin/qmt_gate_common.py index d5c52869..1fb98c0b 100644 --- a/scripts/qmt_relogin/qmt_gate_common.py +++ b/scripts/qmt_relogin/qmt_gate_common.py @@ -8,10 +8,55 @@ qmt_gate_latest.json (overwrite status light). """ import datetime import json +import os import re 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" RELOGIN_TASK = "sanguo-qmt-relogin" QMT_PROCS = ("XtItClient.exe", "XtMiniQmt.exe", "miniquote.exe") diff --git a/scripts/qmt_relogin/qmt_relogin.py b/scripts/qmt_relogin/qmt_relogin.py index 5658ed02..2235b760 100644 --- a/scripts/qmt_relogin/qmt_relogin.py +++ b/scripts/qmt_relogin/qmt_relogin.py @@ -38,7 +38,8 @@ import subprocess import sys import time -ACCOUNT = "66639661" +from qmt_gate_common import ACCOUNT # 部署身份单一权威源(P2-18) + CACHE_PATH = r"C:\sanguo_bigqmt\qmt_install_dir.txt" QUEUE_KEY = "bigqmt:rpc:queue:%s" % ACCOUNT diff --git a/scripts/qmt_relogin/setclip_acc.py b/scripts/qmt_relogin/setclip_acc.py index 53408ae1..6377b57c 100644 --- a/scripts/qmt_relogin/setclip_acc.py +++ b/scripts/qmt_relogin/setclip_acc.py @@ -3,9 +3,11 @@ import ctypes from ctypes import wintypes +from qmt_gate_common import ACCOUNT # 部署身份单一权威源(P2-18) + u = ctypes.windll.user32 k = ctypes.windll.kernel32 -s = "66639661" +s = ACCOUNT buf = ctypes.create_unicode_buffer(s) if not u.OpenClipboard(0): print("CLIP_OPEN_FAIL") diff --git a/tests/trader/test_qmt_identity.py b/tests/trader/test_qmt_identity.py new file mode 100644 index 00000000..39ad2927 --- /dev/null +++ b/tests/trader/test_qmt_identity.py @@ -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