diff --git a/.claude/skills/superpowers/SKILL.md.backup b/.claude/skills/superpowers/SKILL.md.backup
deleted file mode 100644
index 41285fea..00000000
--- a/.claude/skills/superpowers/SKILL.md.backup
+++ /dev/null
@@ -1,78 +0,0 @@
----
-name: superpowers
-description: "Complete Superpowers 5-step workflow: brainstorming → planning → execution → review → verification. Use /superpowers to start the full workflow for any task."
----
-
-# /superpowers - Superpowers 完整工作流
-
-自动化执行 Superpowers 五步法,确保任务从需求到完成的完整质量保障。
-
-## 使用方法
-
-```
-/superpowers # 对当前任务执行完整工作流
-/superpowers "完成用户登录功能" # 对指定任务执行工作流
-/superpowers --quick "修复登录 bug" # 快速模式(简化步骤)
-/superpowers --debug "支付失败问题" # 调试模式(强化 systematic-debugging)
-```
-
-## 工作流步骤
-
-### Step 1: Brainstorming (需求探索)
-- 使用 `superpowers:brainstorming` 技能
-- 探索用户意图、需求边界、设计考虑
-- 输出:需求文档草案
-
-### Step 2: Writing Plans (编写计划)
-- 使用 `superpowers:writing-plans` 技能
-- 编写详细的实现计划
-- 输出:IMPLEMENTATION_PLAN.md
-
-### Step 3: Executing Plans (执行计划)
-- 使用 `superpowers:executing-plans` 或 `superpowers:subagent-driven-development` 技能
-- 按计划执行实现
-- 输出:代码变更
-
-### Step 4: Code Review (代码审查)
-- 使用 `superpowers:requesting-code-review` 技能
-- 验证实现符合需求
-- 输出:审查报告
-
-### Step 5: Verification & Finishing (验证完成)
-- 使用 `superpowers:verification-before-completion` 技能
-- 使用 `superpowers:finishing-a-development-branch` 技能
-- 确认完成,决定合并方式
-- 输出:完成报告
-
-## 模式说明
-
-| 模式 | 说明 |
-|------|------|
-| 默认模式 | 完整 5 步工作流 |
-| --quick | 简化版:合并 brainstorming + planning,快速审查 |
-| --debug | 强化 systematic-debugging,专注于问题定位和修复 |
-| --review-only | 仅执行代码审查步骤 |
-
-## 工作产物
-
-所有工作产物保存在 `.claude/workdir/` 目录:
-```
-.claude/workdir/
-├── BRAINSTORM.md # 需求探索结果
-├── IMPLEMENTATION_PLAN.md # 实现计划
-├── EXECUTION_LOG.md # 执行日志
-├── REVIEW_REPORT.md # 代码审查报告
-└── COMPLETION_SUMMARY.md # 完成总结
-```
-
-## When Invoked (调用时必须执行)
-
-1. **确认任务**:如果用户没有指定任务,询问要完成什么
-2. **选择模式**:根据用户指定的 flag 选择对应模式
-3. **按步骤执行**:严格按照 5 步顺序执行,不可跳过
-4. **记录进度**:每步完成后更新工作产物
-5. **汇报结果**:最终向用户汇报完整工作流的结果
-
-## 与 CLAUDE.md 的关系
-
-此技能遵循项目 `.claude/CLAUDE.md` 中定义的 Superpowers 五技能体系和工作流程。
diff --git a/.claude/workdir/BRAINSTORM.md b/.claude/workdir/BRAINSTORM.md
deleted file mode 100644
index e81c629d..00000000
--- a/.claude/workdir/BRAINSTORM.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Phase 2 需求探索结果
-
-**日期**: 2026-07-03
-**任务**: 继续完成 Phase 2 的任务
-
----
-
-## 当前状态分析
-
-### 已完成 (Phase 1 + 部分 Phase 2)
-- ✅ 成交监控 API (`sanguo_web/api/routes/trades.py`)
-- ✅ 资金监控 API (`sanguo_web/api/routes/accounts.py`)
-- ✅ 全局配置 API (`sanguo_web/api/routes/settings.py`)
-- ✅ 前端页面扩展 (`sanguo_web/static/js/app.js`, `templates/index.html`)
-- ✅ 样式文件 (`sanguo_web/static/css/main.css`)
-
-### 待完成 (Phase 2 剩余)
-根据 `requirements/implementation-plan.md` 和当前代码状态:
-
-| 功能 | 后端 | 前端 | 状态 |
-|------|------|------|------|
-| 成交监控页面 | ✅ | ✅ | 需验证 |
-| 资金监控页面 | ✅ | ✅ | 需验证 |
-| 活动委托视图 | ✅ | ✅ | 需验证 |
-| 市场深度盘口 | ✅ | ✅ | 需验证 |
-| 合约管理 | ✅ | ✅ | 需验证 |
-| 表格排序 | - | ✅ | 需验证 |
-| 全局配置编辑器 | ✅ | 🟡 | **需完成** |
-
-### 需要明确的问题
-
-1. **全局配置编辑器**:
- - 后端 API 已完成 (`settings.py`)
- - 前端表单部分完成
- - 需要确认:哪些配置项需要编辑?是否有安全限制?
-
-2. **集成测试**:
- - 测试文件已创建 (`test_phase2_enhancements.py`)
- - 需要运行并验证
-
-3. **代码审查**:
- - 新增代码需要审查
- - 需要确认审查标准
-
----
-
-## Phase 2 完成定义
-
-Phase 2 被认为完成当:
-- [ ] 所有 Phase 2 功能的后端 API 已实现并可用
-- [ ] 所有 Phase 2 功能的前端页面已实现并可用
-- [ ] 集成测试通过
-- [ ] 代码审查完成
-- [ ] 文档更新
-
----
-
-## 下一步
-
-进入 Step 2: 编写实现计划
diff --git a/.claude/workdir/COMPLETION_SUMMARY.md b/.claude/workdir/COMPLETION_SUMMARY.md
deleted file mode 100644
index 6cc46b76..00000000
--- a/.claude/workdir/COMPLETION_SUMMARY.md
+++ /dev/null
@@ -1,97 +0,0 @@
-# Phase 2 完成总结
-
-**日期**: 2026-07-03
-**状态**: ✅ **已完成**
-
----
-
-## 工作流执行结果
-
-### Step 1: Brainstorming ✅
-- 需求探索完成
-- 确定待完成任务:全局配置编辑器
-
-### Step 2: Writing Plans ✅
-- 实现计划编写完成
-- 4 个任务分解完成
-
-### Step 3: Executing Plans ✅
-- Task 1: 全局配置编辑器前端 - **已完成**
-- Task 2: 验证 Phase 2 功能 - **部分完成** (API 测试需服务器运行)
-- Task 3: 代码审查 - **通过**
-- Task 4: 文档更新 - **已完成**
-
-### Step 4: Code Review ✅
-- 审查 5 个文件
-- 审查结论:**通过**
-- 发现 3 个优化建议(非阻塞)
-
-### Step 5: Verification & Finishing ✅
-- Phase 2 状态更新为完成
-- 文档已更新
-
----
-
-## Phase 2 完成状态
-
-| 功能模块 | 状态 |
-|----------|------|
-| 成交监控页面 | ✅ 完成 |
-| 资金监控页面 | ✅ 完成 |
-| 网关连接管理 | ✅ 完成 |
-| 活动委托视图 | ✅ 完成 |
-| 市场深度盘口 | ✅ 完成 |
-| 合约管理 | ✅ 完成 |
-| 表格排序 | ✅ 完成 |
-| 全局配置编辑器 | ✅ 完成 |
-
----
-
-## 代码统计
-
-| 类型 | 新增 |
-|------|------|
-| 后端 API 路由 | 3 个文件 |
-| 前端页面 | 多个页面组件 |
-| 测试文件 | 2 个 |
-| 总代码行数 | +1652 行 |
-
----
-
-## 待办事项
-
-1. **启动服务器后运行完整测试**:
- ```bash
- python run_web.py
- python sanguo_web/test_phase2_enhancements.py
- ```
-
-2. **优化建议(可选)**:
- - settings.py: 使用 `dict(SETTINGS)` 优化性能
- - trades.py: 确保时间字段类型一致性
- - accounts.py: 使用 `Decimal` 进行金融计算
-
-3. **后续阶段**:
- - Phase 3: 双击交互、CSV 导出、微信通知设置
-
----
-
-## 下一步
-
-Phase 2 已完成。可以:
-1. 启动服务器验证功能
-2. 开始 Phase 3 规划
-3. 或进行其他功能开发
-
----
-
-## 工作产物目录
-
-```
-.claude/workdir/
-├── BRAINSTORM.md # 需求探索结果
-├── IMPLEMENTATION_PLAN.md # 实现计划
-├── EXECUTION_LOG.md # 执行日志
-├── REVIEW_REPORT.md # 代码审查报告
-└── COMPLETION_SUMMARY.md # 本文件
-```
diff --git a/.claude/workdir/EXECUTION_LOG.md b/.claude/workdir/EXECUTION_LOG.md
deleted file mode 100644
index 9c478343..00000000
--- a/.claude/workdir/EXECUTION_LOG.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Phase 2 执行日志
-
-**日期**: 2026-07-03
-**执行人**: Claude (Main Agent)
-
----
-
-## Task 1: 全局配置编辑器前端 ✅
-
-### 状态: 完成
-
-检查结果:
-- ✅ 配置表单 UI 已实现
-- ✅ 保存/刷新功能已实现
-- ✅ 加载状态和错误处理已实现
-- ✅ 动态类型渲染已完成 (string/number/boolean/array)
-
-相关文件:
-- `sanguo_web/templates/index.html` (line 931-1003)
-- `sanguo_web/static/js/app.js` (line 100-107, 619-647, 836-838)
-- `sanguo_web/static/js/api.js` (line 428-447)
-
----
-
-## Task 2: 验证 Phase 2 功能 ⚠️
-
-### 状态: 部分完成
-
-测试结果:
-- ✗ 活动委托 API - 服务器未运行
-- ✗ 合约管理 API - 服务器未运行
-- ✗ 行情数据深度 - 服务器未运行
-- ✗ 成交监控 API - 服务器未运行
-- ✗ 资金监控 API - 服务器未运行
-- ✓ 前端文件验证 - 通过
-
-**备注**: API 测试失败是因为服务器未运行在 localhost:8000。需要启动服务器后重新测试。
-
-前端验证通过项:
-- ✓ active_orders page
-- ✓ contracts page
-- ✓ order book
-- ✓ table sort
-- ✓ market depth display
-
----
-
-## 待完成
-
-1. 启动 Web 服务器
-2. 重新运行 API 测试
-3. 代码审查
-4. 文档更新
-
----
-
-## 建议下一步
-
-1. 启动服务器: `python run_web.py`
-2. 重新测试: `python sanguo_web/test_phase2_enhancements.py`
-3. 如测试通过,进入代码审查阶段
diff --git a/.claude/workdir/IMPLEMENTATION_PLAN.md b/.claude/workdir/IMPLEMENTATION_PLAN.md
deleted file mode 100644
index a74cdea6..00000000
--- a/.claude/workdir/IMPLEMENTATION_PLAN.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# Phase 2 完成计划
-
-**日期**: 2026-07-03
-**目标**: 完成剩余 Phase 2 功能并验证
-
----
-
-## 任务分解
-
-### Task 1: 完成全局配置编辑器前端
-- **状态**: 🟡 部分完成
-- **文件**:
- - 后端: `sanguo_web/api/routes/settings.py` ✅
- - 前端: `sanguo_web/static/js/app.js` 🟡
- - 模板: `sanguo_web/templates/index.html` 🟡
-- **剩余工作**:
- - [ ] 完善配置表单 UI
- - [ ] 添加配置验证
- - [ ] 实现保存/重置功能
- - [ ] 添加重启提示
-
-### Task 2: 验证所有 Phase 2 功能
-- **文件**: `sanguo_web/test_phase2_enhancements.py`
-- **测试项**:
- - [ ] 活动委托 API
- - [ ] 合约管理 API
- - [ ] 行情数据深度(五档)
- - [ ] 成交监控 API
- - [ ] 资金监控 API
- - [ ] 前端页面验证
-
-### Task 3: 代码审查
-- **审查文件**:
- - `sanguo_web/api/routes/*.py`
- - `sanguo_web/static/js/*.js`
- - `sanguo_web/templates/*.html`
-- **审查标准**:
- - 代码质量
- - 安全性
- - 性能
- - 一致性
-
-### Task 4: 文档更新
-- [ ] 更新 `README.md`
-- [ ] 更新 API 文档
-- [ ] 记录已知问题
-
----
-
-## 执行顺序
-
-```
-Task 1 (全局配置编辑器)
- ↓
-Task 2 (验证测试)
- ↓
-Task 3 (代码审查)
- ↓
-Task 4 (文档更新)
-```
-
----
-
-## 验收标准
-
-- [ ] 全局配置编辑器可以编辑并保存配置
-- [ ] 所有 Phase 2 功能测试通过
-- [ ] 代码审查完成,无明显问题
-- [ ] 文档更新完成
-- [ ] 可以标记 Phase 2 为完成状态
-
----
-
-## 预计时间
-
-| Task | 预计时间 |
-|------|----------|
-| Task 1 | 1-2 小时 |
-| Task 2 | 1 小时 |
-| Task 3 | 1 小时 |
-| Task 4 | 0.5 小时 |
-| **总计** | **3.5-4.5 小时** |
diff --git a/.claude/workdir/REVIEW_REPORT.md b/.claude/workdir/REVIEW_REPORT.md
deleted file mode 100644
index 2d56bf3c..00000000
--- a/.claude/workdir/REVIEW_REPORT.md
+++ /dev/null
@@ -1,90 +0,0 @@
-# Phase 2 代码审查报告
-
-**日期**: 2026-07-03
-**审查人**: Claude (Main Agent)
-**审查范围**: Phase 2 新增代码
-
----
-
-## 审查文件
-
-| 文件 | 行数 | 状态 |
-|------|------|------|
-| `sanguo_web/api/routes/settings.py` | 118 | ✅ 通过 |
-| `sanguo_web/api/routes/accounts.py` | 98 | ✅ 通过 |
-| `sanguo_web/api/routes/trades.py` | 164 | ✅ 通过 |
-| `sanguo_web/static/js/app.js` | 1132 | ✅ 通过 |
-| `sanguo_web/templates/index.html` | 1085 | ✅ 通过 |
-
----
-
-## 审查结果
-
-### ✅ 通过项
-
-#### 1. 代码质量
-- ✓ 命名规范清晰
-- ✓ 代码结构合理
-- ✓ 注释充分
-- ✓ 类型提示完整
-
-#### 2. 安全性
-- ✓ 依赖注入 (`Depends(get_current_user)`) 确保认证
-- ✓ 输入验证 (`validate_settings`)
-- ✓ 错误处理完善 (try/except, HTTPException)
-- ✓ 敏感信息保护(不返回明文密码)
-
-#### 3. 性能
-- ✓ 查询效率合理(使用 `get()` 避免 KeyError)
-- ✓ 列表推导式使用得当
-- ✓ 数据分页支持 (`/latest?limit=50`)
-
-#### 4. 一致性
-- ✓ 与项目现有代码风格一致
-- ✓ API 响应格式统一
-- ✓ 错误处理模式一致
-
----
-
-## 观察到的小问题(非阻塞)
-
-### 1. settings.py
-```python
-# Line 39-42: 可能的性能问题
-for key, value in SETTINGS.items():
- settings_dict[key] = value
-```
-**建议**: 如果配置项很多,可以考虑使用 `dict(SETTINGS)` 直接复制
-
-### 2. trades.py
-```python
-# Line 63: 潜在的类型问题
-key=lambda x: x.get("time", datetime.min),
-```
-**建议**: 确保 `time` 字段类型一致性
-
-### 3. accounts.py
-```python
-# Line 85-87: 可能的精度问题
-total_balance = sum(acc.get("balance", 0.0) for acc in accounts)
-```
-**建议**: 金融计算建议使用 `decimal.Decimal`
-
----
-
-## 审查结论
-
-**总体评价**: ✅ **通过审查**
-
-代码质量良好,无明显缺陷。观察到的问题都是优化建议,不影响当前功能。
-
-**建议**: 可以合并到主分支。
-
----
-
-## 下一步
-
-1. 修复建议的小问题(可选)
-2. 运行完整的集成测试
-3. 更新文档
-4. 标记 Phase 2 为完成
diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local
deleted file mode 100644
index 6b970765..00000000
--- a/docker/Dockerfile.local
+++ /dev/null
@@ -1,43 +0,0 @@
-# Sanguo VeighNa Docker 镜像 - 本地构建版
-# 使用国内 PyPI 镜像源,跳过 apt-get 和 TA-Lib 编译
-
-FROM python:3.10-slim
-
-# 设置环境变量
-ENV TZ=Asia/Shanghai \
- DEBIAN_FRONTEND=noninteractive \
- PYTHONUNBUFFERED=1 \
- PYTHONDONTWRITEBYTECODE=1 \
- VNPY_LOG_LEVEL=INFO \
- TA_LIBRARY_PATH=/usr/local/lib \
- TA_INCLUDE_PATH=/usr/local/include \
- PIP_DEFAULT_TIMEOUT=300 \
- # 使用清华 PyPI 镜像
- PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
- PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
-
-# 设置工作目录
-WORKDIR /app
-
-# 创建数据目录
-RUN mkdir -p /app/data /app/logs /app/config /app/temp
-
-# 复制 requirements 并安装依赖
-COPY requirements-docker.txt .
-RUN pip install --no-cache-dir --upgrade pip && \
- pip install --no-cache-dir -r requirements-docker.txt
-
-# 复制应用代码
-COPY sanguo_web/ /app/sanguo_web/
-COPY vnpy_v4.4.0/vnpy/ /app/vnpy/
-COPY run_web.py /app/
-
-# 复制启动脚本
-COPY entrypoint.sh /app/
-RUN chmod +x /app/entrypoint.sh
-
-# 暴露端口
-EXPOSE 8000 8080
-
-# 启动命令
-ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test
deleted file mode 100644
index 5448ef09..00000000
--- a/docker/Dockerfile.test
+++ /dev/null
@@ -1,11 +0,0 @@
-# NAS 测试镜像:复用现成 with-backtester 环境,补 pytest
-# with-backtester 已含 py3.10/pandas2.3.3/numpy2.2.6/TA-Lib0.6.8/pyarrow/polars + vnpy源码
-# 只缺 pytest,此处薄层补齐。代码通过 docker run -v 挂载(不 bake 进镜像,便于代码更新)
-FROM sanguo_vnpy_v2:with-backtester
-
-RUN pip install --no-cache-dir -q -i https://pypi.tuna.tsinghua.edu.cn/simple \
- pytest==9.1.1 pytest-asyncio==1.4.0 pytest-mock==3.15.1 \
- akshare==1.18.64 baostock==0.9.3 scipy==1.15.3 \
- bullet-trade==0.9.2
-
-WORKDIR /code
diff --git a/examples/.gitkeep b/examples/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/scripts/bridge_err_full.py b/scripts/bridge_err_full.py
deleted file mode 100644
index 5b150c31..00000000
--- a/scripts/bridge_err_full.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-"""order_error_events 完整 payload(拒因定位)。"""
-import json
-import re
-
-pw = None
-for line in open(r"C:\redis\redis.conf"):
- m = re.match(r"^requirepass\s+(\S+)", line)
- if m:
- pw = m.group(1).strip()
-import redis
-
-r = redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
- decode_responses=True)
-seen_msgs = {}
-for ts, fields in r.xrevrange("bigqmt:order_error_events:66639661", count=26):
- d = json.loads(fields["payload"])
- msg = d.get("error_msg") or d.get("msg") or d.get("order_msg") or str(d)
- code = d.get("stock_code")
- err_id = d.get("error_id") or d.get("errorid") or ""
- seen_msgs.setdefault((str(err_id), str(msg)[:200]), []).append(code)
-
-for (err_id, msg), codes in seen_msgs.items():
- print("err_id=%s n=%d codes=%s" % (err_id, len(codes), ",".join(codes[:8])))
- print(" msg:", msg)
diff --git a/scripts/bridge_streams_tail.py b/scripts/bridge_streams_tail.py
deleted file mode 100644
index 07604345..00000000
--- a/scripts/bridge_streams_tail.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# -*- coding: utf-8 -*-
-"""拉桥事件流尾部(拒因/成交/状态机),09-09 上午废单定位用。"""
-import re
-import sys
-
-pw = None
-for line in open(r"C:\redis\redis.conf"):
- m = re.match(r"^requirepass\s+(\S+)", line)
- if m:
- pw = m.group(1).strip()
-import redis
-
-r = redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
- decode_responses=True)
-
-for key in sys.argv[1:] or ["bigqmt:order_error_events:66639661",
- "bigqmt:trade_events:66639661"]:
- print("=" * 20, key, "len=", r.xlen(key))
- for ts, d in r.xrevrange(key, count=12):
- parts = []
- for k in sorted(d):
- v = str(d[k])[:90]
- parts.append("%s=%s" % (k, v))
- print(ts, " | ".join(parts)[:600])
diff --git a/scripts/diag_cta.py b/scripts/diag_cta.py
deleted file mode 100644
index 039a0035..00000000
--- a/scripts/diag_cta.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""Diagnostic: real CTA backtest of DoubleMaStrategy on 600000 (A-share daily).
-Guarded entry for spawn. Throwaway."""
-import sys
-import os
-import traceback
-
-_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "vnpy_v4.4.0"))
-_REPO = os.path.dirname(_VNPY_SRC)
-for _p in (_REPO, _VNPY_SRC):
- if _p not in sys.path:
- sys.path.insert(0, _p)
-
-
-def main():
- # Configure vnpy DB to the real quant_trading.db BEFORE engine.load_data()
- from vnpy.trader.setting import SETTINGS
- from sanguo_data.config import load_config
- cfg = load_config("/app/config/data_platform.yaml")
- SETTINGS["database.name"] = "sqlite"
- SETTINGS["database.database"] = cfg.data_paths["vnpy_db"]
- print("DB:", SETTINGS["database.database"])
-
- from vnpy_ctastrategy.strategies.double_ma_strategy import DoubleMaStrategy
- print("DoubleMaStrategy.parameters:", getattr(DoubleMaStrategy, "parameters", "?"))
-
- from sanguo_backtest.cta_engine import run_cta_backtest
- # Classic double-MA params (vnpy example defaults); fixed_size=1
- params = {"fast_window": 10, "slow_window": 20, "fixed_size": 1}
-
- print(f"run_cta_backtest DoubleMaStrategy on 600000, 2024-01-01..2024-06-30, params={params}")
- try:
- result = run_cta_backtest(
- DoubleMaStrategy, "600000", params,
- "2024-01-01", "2024-06-30", cfg, "/tmp/cta_results.db",
- )
- print("=== status:", result.status)
- print("=== statistics ===")
- if result.statistics:
- for k, v in result.statistics.items():
- print(f" {k}: {v}")
- else:
- print(" (empty)")
- if result.status == "failed":
- print("=== error ===")
- print(result.error_msg)
- except Exception:
- traceback.print_exc()
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/harvest_zombies.py b/scripts/harvest_zombies.py
deleted file mode 100644
index 380cf02c..00000000
--- a/scripts/harvest_zombies.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# -*- coding: utf-8 -*-
-"""周一晨僵尸收割(2026-09-14 07:2x): 杀 12 个 wedged 引擎子进程, supervisor 自动重拉。
-不动 supervisor 本体。用法: python -X utf8 C:\\sanguo_bigqmt\\harvest_zombies.py [status]
-"""
-from __future__ import annotations
-
-import sys
-import time
-
-
-def _procs():
- import psutil
- live_sup, shadow_sup, children = [], [], []
- for p in psutil.process_iter(["pid", "name", "cmdline", "create_time"]):
- if "python" not in (p.info["name"] or "").lower():
- continue
- cmd = p.info["cmdline"] or []
- j = " ".join(cmd)
- if "sanguo_live" in j and "--supervisor" in j:
- live_sup.append((p.info["pid"], _ts(p)))
- elif "runner_live" in j:
- children.append(p)
- elif "sanguo_trader.shadow" in j and "--auto" in j:
- shadow_sup.append((p.info["pid"], _ts(p)))
- elif "sanguo_trader.shadow" in j:
- children.append(p)
- return live_sup, shadow_sup, children
-
-
-def _ts(p):
- return time.strftime("%m-%d %H:%M", time.localtime(p.info["create_time"]))
-
-
-def main():
- status_only = len(sys.argv) > 1 and sys.argv[1] == "status"
- live_sup, shadow_sup, children = _procs()
- print("SUP live=%s shadow=%s" % (live_sup, shadow_sup))
- print("CHILDREN n=%d (zombies, expect start=09-11/09-12):" % len(children))
- for p in children:
- print(" pid=%s start=%s" % (p.info["pid"], _ts(p)))
- if status_only:
- return
- if not children:
- print("NO_CHILDREN_TO_HARVEST")
- for p in children:
- try:
- p.terminate()
- print("TERM", p.info["pid"])
- except Exception as e:
- print("TERM_FAIL", p.info["pid"], e)
- t0 = time.time()
- while time.time() - t0 < 60:
- if not _procs()[2]:
- print("HARVEST_DONE %.0fs" % (time.time() - t0))
- break
- time.sleep(5)
- else:
- print("HARVEST_TIMEOUT residual=%d" % len(_procs()[2]))
- raise SystemExit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/qmt_relogin/childtree.py b/scripts/qmt_relogin/childtree.py
deleted file mode 100644
index d7a74290..00000000
--- a/scripts/qmt_relogin/childtree.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Enumerate child windows of the QMT editor window (pid filter).
-Qt usually collapses to one hwnd, but dialogs may expose buttons."""
-import ctypes
-import ctypes.wintypes as wt
-
-user32 = ctypes.windll.user32
-
-target = None
-Ws = ctypes.WINFUNCTYPE(ctypes.c_bool, wt.HWND, wt.LPARAM)
-
-
-@Ws
-def find_cb(h, _):
- global target
- buf = ctypes.create_unicode_buffer(256)
- user32.GetClassNameW(h, buf, 256)
- return True
-
-
-def cls(h):
- buf = ctypes.create_unicode_buffer(256)
- user32.GetClassNameW(h, buf, 256)
- return buf.value
-
-
-def txt(h):
- buf = ctypes.create_unicode_buffer(512)
- user32.GetWindowTextW(h, buf, 512)
- return buf.value
-
-
-def rect(h):
- r = wt.RECT()
- user32.GetWindowRect(h, ctypes.byref(r))
- return (r.left, r.top, r.right, r.bottom)
-
-
-def esc(s):
- return str(s).encode("unicode_escape").decode("ascii")[:70]
-
-
-# find all top-level Qt windows, then enumerate their children
-qt_windows = []
-
-
-@Ws
-def enum_top(h, _):
- if user32.IsWindowVisible(h):
- if cls(h).startswith("Qt5"):
- r = rect(h)
- if r[0] > -30000:
- qt_windows.append(h)
- return True
-
-
-user32.EnumWindows(enum_top, 0)
-print("QT_TOP=%d" % len(qt_windows))
-for q in qt_windows:
- print("TOP hwnd=%s class=%s title=%s rect=%s" % (
- q, esc(cls(q)), esc(txt(q)), rect(q)))
- kids = []
-
- @Ws
- def enum_child(h, _):
- kids.append(h)
- return True
- user32.EnumChildWindows(q, enum_child, 0)
- print(" CHILDREN=%d" % len(kids))
- for k in kids[:25]:
- print(" kid hwnd=%s class=%s title=%s rect=%s" % (
- k, esc(cls(k)), esc(txt(k)), rect(k)))
diff --git a/scripts/qmt_relogin/esc_wrapper.ps1 b/scripts/qmt_relogin/esc_wrapper.ps1
deleted file mode 100644
index 97060b1c..00000000
--- a/scripts/qmt_relogin/esc_wrapper.ps1
+++ /dev/null
@@ -1 +0,0 @@
-C:\Python310\python.exe C:\sanguo_bigqmt\qmt_dlg_esc.py > C:\sanguo_bigqmt\esc.log 2>&1
diff --git a/scripts/qmt_relogin/ingest333.ps1 b/scripts/qmt_relogin/ingest333.ps1
deleted file mode 100644
index 69d3e831..00000000
--- a/scripts/qmt_relogin/ingest333.ps1
+++ /dev/null
@@ -1,34 +0,0 @@
-$ErrorActionPreference = 'Stop'
-# Ingest the plaintext password from Desktop 333.txt into a DPAPI-encrypted
-# blob (current-user + machine scope). Password never leaves this machine and
-# is never printed. On success the plaintext file is deleted.
-$src = 'C:\Users\Administrator\Desktop\333.txt'
-$credDir = 'C:\sanguo_bigqmt\creds'
-$encPath = Join-Path $credDir 'qmt_login.enc'
-
-New-Item -ItemType Directory -Force -Path $credDir | Out-Null
-$pw = (Get-Content -Raw -LiteralPath $src).Trim()
-if (-not $pw) { throw '333.txt is empty' }
-
-$cred = @{ user = '66639661'; password = $pw } | ConvertTo-Json -Compress
-$sec = ConvertTo-SecureString -String $cred -AsPlainText -Force
-$enc = ConvertFrom-SecureString -SecureString $sec
-Set-Content -LiteralPath $encPath -Value $enc -Encoding ASCII
-
-# Lock the directory down on top of DPAPI (belt and suspenders).
-icacls $credDir /inheritance:r | Out-Null
-icacls $credDir /grant 'Administrator:F' | Out-Null
-icacls $encPath /grant 'Administrator:F' | Out-Null
-
-# Round-trip verify before destroying the plaintext source.
-$enc2 = (Get-Content -Raw -LiteralPath $encPath).Trim()
-$sec2 = ConvertTo-SecureString -String $enc2
-$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec2)
-$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
-[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
-$j = $plain | ConvertFrom-Json
-if ([string]$j.user -ne '66639661' -or [string]$j.password -ne $pw) { throw 'round-trip mismatch' }
-Write-Output ('INGEST_OK encbytes=' + (Get-Item -LiteralPath $encPath).Length + ' pwdlen=' + $pw.Length)
-
-Remove-Item -LiteralPath $src -Force
-Write-Output '333_DELETED'
diff --git a/scripts/qmt_relogin/inspect333.ps1 b/scripts/qmt_relogin/inspect333.ps1
deleted file mode 100644
index 1b3aa9f2..00000000
--- a/scripts/qmt_relogin/inspect333.ps1
+++ /dev/null
@@ -1,13 +0,0 @@
-$ErrorActionPreference = 'Stop'
-$p = 'C:\Users\Administrator\Desktop\333.txt'
-$raw = Get-Content -Raw -LiteralPath $p
-$t = $raw.Trim()
-$lines = @($t -split "`r?`n")
-Write-Output ("TOTALLEN=" + $raw.Length)
-Write-Output ("TRIMLEN=" + $t.Length)
-Write-Output ("NLINES=" + $lines.Count)
-for ($i = 0; $i -lt $lines.Count; $i++) {
- $L = $lines[$i]
- Write-Output ("LINE" + $i + "_LEN=" + $L.Length + "_ALLDIG=" + ([bool]($L -match '^[0-9]+$')) + "_HASWS=" + ([bool]($L -match '[ \t]')))
-}
-Write-Output ("HASCHARTAB=" + ([bool]($raw -match '[ \t]')))
diff --git a/scripts/qmt_relogin/nav_wrapper.ps1 b/scripts/qmt_relogin/nav_wrapper.ps1
deleted file mode 100644
index 402d5ab7..00000000
--- a/scripts/qmt_relogin/nav_wrapper.ps1
+++ /dev/null
@@ -1 +0,0 @@
-C:\Python310\python.exe C:\sanguo_bigqmt\qmt_nav_pystg.py > C:\sanguo_bigqmt\nav.log 2>&1
diff --git a/scripts/qmt_relogin/patch_market_bigqmt_20260911.py b/scripts/qmt_relogin/patch_market_bigqmt_20260911.py
deleted file mode 100644
index 6f1664de..00000000
--- a/scripts/qmt_relogin/patch_market_bigqmt_20260911.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Contract fix 2026-09-11: server-side get_trading_dates output normalization.
-
-The native xtdata SDK path returns 'YYYYMMDD' strings whenever any local
-quote source has been reachable; the ContextInfo path returns epoch-ms ints.
-Consumers (bullet_trade miniqmt provider ~line 1824) do ts/1000, so both
-shapes must leave the server as Beijing-midnight epoch-ms ints.
-Idempotent: re-running on a patched file exits 0.
-
-Run ON THE VPS: C:\\Python310\\python.exe -X utf8 patch_market_bigqmt_20260911.py
-"""
-import io
-import shutil
-import sys
-
-P = r"C:\sanguo_bigqmt\bigqmt_signal_trader\adapters\market_bigqmt.py"
-BAK = P + ".bak_20260911"
-
-OLD = (
- " return self._native_or_context(\n"
- ' "get_trading_dates", _via_context, market, start_time, end_time, count\n'
- " )"
-)
-NEW = (
- " return _norm_trading_dates(self._native_or_context(\n"
- ' "get_trading_dates", _via_context, market, start_time, end_time, count))'
-)
-
-HELPER = '''
-
-def _norm_trading_dates(result):
- """Contract fix 2026-09-11: native returns 'YYYYMMDD' strings whenever a
- local quote source has been reachable; ContextInfo returns epoch-ms ints.
- Normalize to Beijing-midnight epoch ms (official xtquant contract;
- consumers do ts/1000 with local-timezone fromtimestamp, server runs
- Asia/Shanghai so datetime().timestamp() is the exact inverse).
- All-numeric lists pass through untouched; mixed/odd payloads keep
- original elements that are not 8-digit strings."""
- if not isinstance(result, list) or not result:
- return result
- if all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in result):
- return result
- out = []
- for x in result:
- if isinstance(x, str) and len(x) == 8 and x.isdigit():
- import datetime as _dt
- out.append(int(_dt.datetime(int(x[:4]), int(x[4:6]), int(x[6:8])).timestamp() * 1000))
- else:
- out.append(x)
- return out
-'''
-
-src = io.open(P, encoding="utf-8").read()
-
-if "_norm_trading_dates(self._native_or_context" in src:
- print("ALREADY_PATCHED")
- sys.exit(0)
-
-if src.count(OLD) != 1:
- print("ANCHOR_NOT_UNIQUE: %d" % src.count(OLD))
- sys.exit(1)
-
-shutil.copyfile(P, BAK)
-src = src.replace(OLD, NEW)
-src = src.rstrip("\n") + "\n" + HELPER
-io.open(P, "w", encoding="utf-8", newline="\n").write(src)
-print("PATCHED ok, backup=%s" % BAK)
diff --git a/scripts/qmt_relogin/patch_market_bigqmt_deploy_20260911.py b/scripts/qmt_relogin/patch_market_bigqmt_deploy_20260911.py
deleted file mode 100644
index f9e3c1e5..00000000
--- a/scripts/qmt_relogin/patch_market_bigqmt_deploy_20260911.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Contract fix 2026-09-11: server-side get_trading_dates output normalization.
-
-The native xtdata SDK path returns 'YYYYMMDD' strings whenever any local
-quote source has been reachable; the ContextInfo path returns epoch-ms ints.
-Consumers (bullet_trade miniqmt provider ~line 1824) do ts/1000, so both
-shapes must leave the server as Beijing-midnight epoch-ms ints.
-Idempotent: re-running on a patched file exits 0.
-
-Run ON THE VPS: C:\\Python310\\python.exe -X utf8 patch_market_bigqmt_20260911.py
-"""
-import io
-import shutil
-import sys
-
-P = r"C:\国金QMT交易端模拟\python\bigqmt_signal_trader\adapters\market_bigqmt.py"
-BAK = P + ".bak_20260911"
-
-OLD = (
- " return self._native_or_context(\n"
- ' "get_trading_dates", _via_context, market, start_time, end_time, count\n'
- " )"
-)
-NEW = (
- " return _norm_trading_dates(self._native_or_context(\n"
- ' "get_trading_dates", _via_context, market, start_time, end_time, count))'
-)
-
-HELPER = '''
-
-def _norm_trading_dates(result):
- """Contract fix 2026-09-11: native returns 'YYYYMMDD' strings whenever a
- local quote source has been reachable; ContextInfo returns epoch-ms ints.
- Normalize to Beijing-midnight epoch ms (official xtquant contract;
- consumers do ts/1000 with local-timezone fromtimestamp, server runs
- Asia/Shanghai so datetime().timestamp() is the exact inverse).
- All-numeric lists pass through untouched; mixed/odd payloads keep
- original elements that are not 8-digit strings."""
- if not isinstance(result, list) or not result:
- return result
- if all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in result):
- return result
- out = []
- for x in result:
- if isinstance(x, str) and len(x) == 8 and x.isdigit():
- import datetime as _dt
- out.append(int(_dt.datetime(int(x[:4]), int(x[4:6]), int(x[6:8])).timestamp() * 1000))
- else:
- out.append(x)
- return out
-'''
-
-src = io.open(P, encoding="utf-8").read()
-
-if "_norm_trading_dates(self._native_or_context" in src:
- print("ALREADY_PATCHED")
- sys.exit(0)
-
-if src.count(OLD) != 1:
- print("ANCHOR_NOT_UNIQUE: %d" % src.count(OLD))
- sys.exit(1)
-
-shutil.copyfile(P, BAK)
-src = src.replace(OLD, NEW)
-src = src.rstrip("\n") + "\n" + HELPER
-io.open(P, "w", encoding="utf-8", newline="\n").write(src)
-print("PATCHED ok, backup=%s" % BAK)
diff --git a/scripts/qmt_relogin/patch_shim_xtdata_20260911.py b/scripts/qmt_relogin/patch_shim_xtdata_20260911.py
deleted file mode 100644
index da5fa3dd..00000000
--- a/scripts/qmt_relogin/patch_shim_xtdata_20260911.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# -*- coding: utf-8 -*-
-"""B-pack client-side fix 2026-09-11: shim get_trading_dates output normalization.
-
-The server-side fix (market_bigqmt.py patch) normalizes the RPC answer, but
-fresh client processes (xt_eod wrapper, future engines) load THIS shim first.
-Defense in depth: normalize here too, so the contract holds even if the server
-side regresses (e.g. server file restored from a backup).
-Idempotent: re-running on a patched file exits 0.
-
-Run ON THE VPS: C:\\Python310\\python.exe -X utf8 patch_shim_xtdata_20260911.py
-"""
-import io
-import shutil
-import sys
-
-P = r"C:\sanguo_bigqmt\xtquant_bridge\xtquant\xtdata.py"
-BAK = P + ".bak_20260911"
-
-OLD = '''def get_trading_dates(market, start_time="", end_time="", count=-1):
- return _compat.xtdata.get_trading_dates(market, start_time, end_time, count)'''
-
-NEW = '''def _norm_calendar_ms(result):
- """Contract fix 2026-09-11: 'YYYYMMDD' strings -> Beijing-midnight epoch ms.
-
- The bridge server's native xtdata path returns string dates whenever a
- local quote source has been reachable (2026-09-11 incident: all 12 engine
- schedulers starved on ts/1000 TypeErrors). Consumers divide by 1000 with
- local-timezone fromtimestamp, and this host runs Asia/Shanghai, so
- datetime().timestamp() is the exact inverse. All-numeric lists pass
- through untouched."""
- if not isinstance(result, list) or not result:
- return result
- if all(isinstance(x, (int, float)) and not isinstance(x, bool) for x in result):
- return result
- out = []
- for x in result:
- if isinstance(x, str) and len(x) == 8 and x.isdigit():
- import datetime as _dt
- out.append(int(_dt.datetime(int(x[:4]), int(x[4:6]), int(x[6:8])).timestamp() * 1000))
- else:
- out.append(x)
- return out
-
-
-def get_trading_dates(market, start_time="", end_time="", count=-1):
- return _norm_calendar_ms(_compat.xtdata.get_trading_dates(market, start_time, end_time, count))'''
-
-src = io.open(P, encoding="utf-8").read()
-
-if "_norm_calendar_ms(_compat.xtdata.get_trading_dates" in src:
- print("ALREADY_PATCHED")
- sys.exit(0)
-
-if src.count(OLD) != 1:
- print("ANCHOR_NOT_UNIQUE: %d" % src.count(OLD))
- sys.exit(1)
-
-shutil.copyfile(P, BAK)
-src = src.replace(OLD, NEW)
-io.open(P, "w", encoding="utf-8", newline="\n").write(src)
-print("PATCHED ok, backup=%s" % BAK)
diff --git a/scripts/qmt_relogin/qmt_attrdiff.py b/scripts/qmt_relogin/qmt_attrdiff.py
deleted file mode 100644
index f20cddae..00000000
--- a/scripts/qmt_relogin/qmt_attrdiff.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Read-only: instance element state attrs (suspend/record/runMode/...) across files."""
-import io
-import os
-import re
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-
-WATCH = ("startupAutorun", "suspend", "record", "runMode", "id", "period",
- "autoCancel", "end_restore", "is_main", "specialType", "runname")
-
-files = [
- ("d0907", os.path.join(root, "config", "Temp", "indexUserConfig.xml_20260907")),
- ("d0908", os.path.join(root, "config", "Temp", "indexUserConfig.xml_20260908")),
- ("d0909", os.path.join(root, "config", "Temp", "indexUserConfig.xml_20260909")),
- ("now", os.path.join(root, "config", "indexUserConfig.xml")),
-]
-for tag, p in files:
- t = io.open(p, encoding="utf-8", errors="replace").read()
- i = t.find("startupAutorun")
- if i < 0:
- print(tag, "NO startupAutorun")
- continue
- start = t.rfind("<", 0, i)
- end = t.find(">", i)
- elem = t[start:end + 1]
- pairs = []
- for k, v in re.findall(r'(\w+)="([^"]*)"', elem):
- if k in WATCH and k != "startupAutorun":
- pairs.append("%s=%s" % (k, v.encode("unicode_escape").decode("ascii")[:40]))
- sa = re.search(r'startupAutorun="([^"]*)"', elem)
- print("%-6s startupAutorun=%s %s" % (tag, sa.group(1), " ".join(pairs)))
-print("DONE7")
diff --git a/scripts/qmt_relogin/qmt_cfgpeek.py b/scripts/qmt_relogin/qmt_cfgpeek.py
deleted file mode 100644
index f8392ec3..00000000
--- a/scripts/qmt_relogin/qmt_cfgpeek.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Grep strategy/run-state entries in QMT users Config.xml (349KB, likely GBK)."""
-import io
-import os
-import re
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-cfg = os.path.join(os.path.dirname(d), "userdata", "users", "66639661", "Config.xml")
-
-raw = open(cfg, "rb").read()
-for enc in ("utf-8", "gbk"):
- try:
- text = raw.decode(enc)
- print("DECODED_AS", enc, "len", len(text))
- break
- except UnicodeDecodeError:
- continue
-else:
- text = raw.decode("gbk", errors="replace")
- print("DECODED_AS gbk/replace")
-
-# show the XML declaration / first line
-print("HEAD:", text[:120].encode("unicode_escape").decode("ascii"))
-
-# any element mentioning formula / strategy / python / run
-pat = re.compile(r"<[^>]*>", re.I)
-lines = text.splitlines()
-print("TOTAL_LINES", len(lines))
-hits = 0
-for i, ln in enumerate(lines):
- low = ln.lower()
- if any(k in low for k in ("formula", "strategy", "python", "model")):
- print("%6d %s" % (i + 1, ln.strip()[:400].encode("unicode_escape").decode("ascii")))
- hits += 1
- if hits > 60:
- print("...truncated")
- break
-if not hits:
- print("NO formula/strategy/python/model tags; sampling run-ish attributes")
- for i, ln in enumerate(lines):
- low = ln.lower()
- if re.search(r'(auto.?run|run.?flag|isrun|running|"run")', low):
- print("%6d %s" % (i + 1, ln.strip()[:300].encode("unicode_escape").decode("ascii")))
- hits += 1
- if hits > 40:
- break
diff --git a/scripts/qmt_relogin/qmt_cfgpeek2.py b/scripts/qmt_relogin/qmt_cfgpeek2.py
deleted file mode 100644
index cfdf593b..00000000
--- a/scripts/qmt_relogin/qmt_cfgpeek2.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Grep Chinese strategy-name / instance entries in QMT Config.xml."""
-import os
-import re
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-cfg = os.path.join(root, "userdata", "users", "66639661", "Config.xml")
-text = open(cfg, encoding="utf-8").read()
-
-kws = ["新建策略文件", "实例", "BIGQMT_REDIS_DRYRUN", "modbootstart",
- "autorun", "AutoRun", "bootstart", "自运行", "自动"]
-hits = 0
-for i, ln in enumerate(text.splitlines(), 1):
- if any(k in ln for k in kws):
- # print trimmed around first match
- print("%5d %s" % (i, ln.strip()[:600].encode("unicode_escape").decode("ascii")))
- hits += 1
- if hits > 40:
- print("...TRUNC")
- break
-print("HITS", hits)
diff --git a/scripts/qmt_relogin/qmt_cfgsnap.py b/scripts/qmt_relogin/qmt_cfgsnap.py
deleted file mode 100644
index 2b907b7b..00000000
--- a/scripts/qmt_relogin/qmt_cfgsnap.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Snapshot config\indexUserConfig.xml as pre-submit baseline."""
-import os
-import shutil
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-src = os.path.join(os.path.dirname(d), "config", "indexUserConfig.xml")
-dst = r"C:\sanguo_bigqmt\indexUserConfig_before.xml"
-shutil.copy2(src, dst)
-print("SNAP", os.path.getsize(src), "->", os.path.getsize(dst))
diff --git a/scripts/qmt_relogin/qmt_click_pystg.py b/scripts/qmt_relogin/qmt_click_pystg.py
deleted file mode 100644
index 1a157467..00000000
--- a/scripts/qmt_relogin/qmt_click_pystg.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# -*- coding: utf-8 -*-
-"""UI step8 (atomic): ensure model-trade page -> right-click row -> 修改 -> shot.
-Only one goal per run; dialog must be visible in the shot."""
-import time
-
-import pyautogui
-import pygetwindow as gw
-
-pyautogui.FAILSAFE = False
-
-
-def log_front(tag):
- try:
- w = gw.getActiveWindow()
- print("FRONT", tag, w.title.encode("unicode_escape").decode("ascii")[:80] if w else None)
- except Exception as e:
- print("FRONT", tag, "ERR", e)
-
-
-time.sleep(1.5)
-log_front("start")
-pyautogui.press("esc")
-time.sleep(1.0)
-log_front("after-esc")
-
-# navigate to model-trade page via tree: click known point -> UP -> Enter
-pyautogui.click(75, 432) # 数据导出 (verified)
-time.sleep(0.6)
-pyautogui.press("up") # python策略
-time.sleep(0.6)
-pyautogui.press("enter")
-time.sleep(2.5)
-log_front("after-nav")
-
-pyautogui.click(500, 178) # select instance row 1
-time.sleep(0.8)
-pyautogui.click(500, 178, button="right")
-time.sleep(1.2)
-pyautogui.click(520, 192) # 修改
-time.sleep(4)
-log_front("after-xiugai")
-pyautogui.screenshot(r"C:\sanguo_bigqmt\shot.png")
-print("DONE atomic reopen")
diff --git a/scripts/qmt_relogin/qmt_click_run.py b/scripts/qmt_relogin/qmt_click_run.py
deleted file mode 100644
index 70895431..00000000
--- a/scripts/qmt_relogin/qmt_click_run.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# -*- coding: utf-8 -*-
-"""UI step9: python策略(y=385, current boot) -> row1 -> right-click -> 运行.
-Then screenshot + objective formula-log runonce check."""
-import datetime
-import io
-import os
-import time
-
-import pyautogui
-
-pyautogui.FAILSAFE = False
-
-time.sleep(1.5)
-pyautogui.click(75, 385) # python策略 (verified on THIS boot's screenshot)
-time.sleep(0.8)
-pyautogui.press("enter")
-time.sleep(2.5)
-pyautogui.click(500, 178) # select instance row 1
-time.sleep(0.8)
-pyautogui.click(500, 178, button="right")
-time.sleep(1.2)
-pyautogui.click(520, 213) # 运行
-time.sleep(4)
-pyautogui.screenshot(r"C:\sanguo_bigqmt\shot.png")
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-logp = os.path.join(os.path.dirname(d), "userdata", "log",
- "XtClient_Formula_20260909.log")
-n = 0
-try:
- now = datetime.datetime.now()
- for ln in io.open(logp, "r", encoding="gbk", errors="replace"):
- try:
- ts = datetime.datetime.strptime(ln[:19], "%Y-%m-%d %H:%M:%S")
- except ValueError:
- continue
- if (now - ts).total_seconds() < 120 and (
- "runonce" in ln or "PythonFormula" in ln):
- n += 1
-except OSError:
- pass
-print("RUNONCE_LINES_120S=", n)
-print("DONE click run")
diff --git a/scripts/qmt_relogin/qmt_datadirs.py b/scripts/qmt_relogin/qmt_datadirs.py
deleted file mode 100644
index b9b59bbb..00000000
--- a/scripts/qmt_relogin/qmt_datadirs.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Look at userdata/data + formulaserver.ini for strategy registry / run state."""
-import os
-import time
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-
-ini = os.path.join(root, "config", "formulaserver", "formulaserver.ini")
-print("=== formulaserver.ini ===")
-try:
- raw = open(ini, "rb").read()
- for enc in ("utf-8", "gbk"):
- try:
- txt = raw.decode(enc)
- break
- except UnicodeDecodeError:
- continue
- else:
- txt = raw.decode("gbk", "replace")
- print(txt.encode("unicode_escape").decode("ascii")[:3000])
-except OSError as e:
- print("ERR", e)
-
-dd = os.path.join(root, "userdata", "data")
-print("=== userdata/data tree (top 2 levels) ===")
-for dirpath, dirnames, files in os.walk(dd):
- rel = os.path.relpath(dirpath, dd)
- if rel.count(os.sep) >= 2:
- dirnames[:] = []
- continue
- print("D " + rel.encode("unicode_escape").decode("ascii"))
- for f in sorted(files)[:25]:
- p = os.path.join(dirpath, f)
- st = os.stat(p)
- print(" %-52s %8d %s" % (
- f.encode("unicode_escape").decode("ascii")[:52], st.st_size,
- time.strftime("%m-%d %H:%M", time.localtime(st.st_mtime))))
diff --git a/scripts/qmt_relogin/qmt_dlg_esc.py b/scripts/qmt_relogin/qmt_dlg_esc.py
deleted file mode 100644
index 450f9e46..00000000
--- a/scripts/qmt_relogin/qmt_dlg_esc.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-"""E1 step1c: dismiss the quit-confirm modal (确定/取消 at ~(525/609, 355))
-that is hijacking ALL clicks. ESC on a modal == cancel; no coordinates needed."""
-import ctypes
-import time
-
-import pyautogui
-
-pyautogui.FAILSAFE = False
-
-hwnd = ctypes.windll.kernel32.GetConsoleWindow()
-if hwnd:
- ctypes.windll.user32.ShowWindow(hwnd, 6) # SW_MINIMIZE
-time.sleep(0.8)
-
-pyautogui.press("esc")
-time.sleep(1.5)
-pyautogui.screenshot(r"C:\sanguo_bigqmt\shot_esc.png")
-print("DONE esc")
diff --git a/scripts/qmt_relogin/qmt_find_stgwin.py b/scripts/qmt_relogin/qmt_find_stgwin.py
deleted file mode 100644
index defef705..00000000
--- a/scripts/qmt_relogin/qmt_find_stgwin.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- coding: utf-8 -*-
-"""E1 step1b: locate the 策略交易/模型交易 child window (python策略 page is a
-separate window, not a main-frame page). Enumerate windows, log geometry,
-activate the strategy window, screenshot it."""
-import ctypes
-import time
-
-import pyautogui
-
-pyautogui.FAILSAFE = False
-
-hwnd = ctypes.windll.kernel32.GetConsoleWindow()
-if hwnd:
- ctypes.windll.user32.ShowWindow(hwnd, 6) # SW_MINIMIZE
-time.sleep(0.8)
-
-import pygetwindow as gw
-
-for w in gw.getAllWindows():
- t = (w.title or "").strip()
- if not t:
- continue
- print("WIN: %r box=%s" % (t[:60], (w.left, w.top, w.width, w.height)))
-
-target = None
-for w in gw.getAllWindows():
- t = w.title or ""
- if ("策略交易" in t) or ("模型" in t) or ("增强" in t):
- target = w
- break
-if target is None:
- print("NO strategy window found")
-else:
- print("TARGET:", target.title, (target.left, target.top,
- target.width, target.height))
- try:
- target.activate()
- except Exception as e:
- print("activate ERR", repr(e))
- time.sleep(1.0)
- try:
- if target.isMinimized:
- target.restore()
- print("restored")
- except Exception as e:
- print("restore ERR", repr(e))
-time.sleep(1.0)
-pyautogui.screenshot(r"C:\sanguo_bigqmt\shot_stgwin.png")
-print("DONE find_stgwin")
diff --git a/scripts/qmt_relogin/qmt_find_strategy.py b/scripts/qmt_relogin/qmt_find_strategy.py
deleted file mode 100644
index 99d91e65..00000000
--- a/scripts/qmt_relogin/qmt_find_strategy.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Locate QMT strategy (formula) storage + run-state flags under userdata."""
-import os
-import time
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-base = os.path.join(root, "userdata", "users")
-print("BASE", base.encode("unicode_escape").decode("ascii"))
-
-for dirpath, dirnames, files in os.walk(base):
- dirnames[:] = [x for x in dirnames if x.lower() != "__pycache__"]
- rel = os.path.relpath(dirpath, base)
- print("D " + rel.encode("unicode_escape").decode("ascii"))
- for f in sorted(files)[:80]:
- p = os.path.join(dirpath, f)
- try:
- st = os.stat(p)
- except OSError:
- continue
- print(" f %-44s %9d %s" % (
- f, st.st_size,
- time.strftime("%m-%d %H:%M", time.localtime(st.st_mtime))))
diff --git a/scripts/qmt_relogin/qmt_gate_common.py b/scripts/qmt_relogin/qmt_gate_common.py
deleted file mode 100644
index d5c52869..00000000
--- a/scripts/qmt_relogin/qmt_gate_common.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Shared helpers for the QMT gate scripts (qmt_sentinel.py / qmt_probe_0915.py).
-
-Design: scripts judge AVAILABILITY (process + bridge RPC + counter behaviour),
-agents/humans judge CORRECTNESS (cash baseline drift is recorded, never red).
-Audit trail: C:\\sanguo_bigqmt\\gate\\qmt_gate_YYYYMMDD.log (append) +
-qmt_gate_latest.json (overwrite status light).
-"""
-import datetime
-import json
-import re
-import subprocess
-
-ACCOUNT = "66639661"
-GATE_DIR = r"C:\sanguo_bigqmt\gate"
-RELOGIN_TASK = "sanguo-qmt-relogin"
-QMT_PROCS = ("XtItClient.exe", "XtMiniQmt.exe", "miniquote.exe")
-
-
-def now_str():
- return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
-
-def gate_log(kind, verdict, msg):
- line = "%s [%s] %s %s" % (now_str(), kind, verdict, msg)
- try:
- import os
- os.makedirs(GATE_DIR, exist_ok=True)
- path = r"%s\qmt_gate_%s.log" % (
- GATE_DIR, datetime.datetime.now().strftime("%Y%m%d"))
- with open(path, "a", encoding="utf-8") as f:
- f.write(line + "\n")
- except Exception as exc:
- line += " (log-write-failed %r)" % exc
- print(line, flush=True)
- return line
-
-
-def gate_json(kind, verdict, fname=None, **detail):
- import os
- payload = {"ts": now_str(), "kind": kind, "verdict": verdict}
- payload.update(detail)
- try:
- os.makedirs(GATE_DIR, exist_ok=True)
- name = fname or "qmt_gate_latest.json"
- with open(r"%s\%s" % (GATE_DIR, name), "w",
- encoding="utf-8") as f:
- json.dump(payload, f, ensure_ascii=False, default=str, indent=1)
- except Exception as exc:
- print("GATE_JSON_WRITE_FAIL %r" % exc, flush=True)
- return payload
-
-
-def redis_client():
- import redis
- with open(r"C:\redis\redis.conf") as f:
- pw = re.search(r"^requirepass (\S+)", f.read(), re.M).group(1)
- return redis.Redis(host="127.0.0.1", port=6379, db=5, password=pw,
- socket_connect_timeout=5, decode_responses=True)
-
-
-def bridge_ping(rc, timeout=10):
- """(up, detail) — up means the RPC server answered ping with ok+pong."""
- from bigqmt_signal_trader.redis_rpc import call_redis_rpc
- try:
- r = call_redis_rpc(rc, ACCOUNT, "ping", {}, timeout_seconds=timeout)
- except Exception as exc:
- return False, "RPC_DOWN %s %s" % (type(exc).__name__, str(exc)[:120])
- if not isinstance(r, dict):
- return False, "RPC_BAD %r" % (r,)
- if not r.get("ok"):
- return False, "RPC_ERR %s" % str(r.get("error") or r)[:150]
- d = r.get("data") or {}
- if not d.get("pong"):
- return False, "RPC_NO_PONG %s" % str(d)[:150]
- return True, "server_time=%s rev=%s allow_order=%s" % (
- d.get("server_time"), d.get("rpc_revision"), d.get("allow_order_methods"))
-
-
-def procs_alive():
- """QMT family process presence via tasklist (no psutil dependency)."""
- out = subprocess.run(["tasklist", "/FO", "CSV", "/NH"],
- capture_output=True, text=True, timeout=30).stdout or ""
- return {name: ('"%s"' % name) in out for name in QMT_PROCS}
-
-
-CALENDAR_PROBE_CODE = (
- "from xtquant import xtdata; "
- "r = xtdata.get_trading_dates('SH', '20260101', '20261231'); "
- "e = r[-1] if isinstance(r, list) and r else None; "
- "print('CAL', type(e).__name__, repr(e))"
-)
-
-
-def calendar_probe(timeout=25):
- """Fifth gate (added 2026-09-11): fresh child process fetches the trading
- calendar through the bridge and checks the element type.
-
- On 2026-09-11 all 12 engine schedulers starved because the bridge returned
- 'YYYYMMDD' STRINGS from its native xtdata path while every other probe
- (process / ping / asset / probe order) stayed green. Consumers divide by
- 1000, so a string element is a hard contract break.
-
- Returns (state, detail): state in ok / bad / empty / exec.
- ok - non-empty numeric elements (epoch ms), contract healthy
- bad - string elements -> RED (relogin restarts the patched server,
- which normalizes the output again)
- empty - probe shape got no data (not proof the bridge is bad) -> AMBER
- exec - child process failed -> AMBER
- """
- import os
- env = dict(os.environ)
- env["PYTHONPATH"] = r"C:\sanguo_bigqmt\xtquant_bridge"
- env["BIGQMT_ACCOUNT_ID"] = ACCOUNT
- with open(r"C:\redis\redis.conf") as f:
- conf = f.read()
- env["BIGQMT_REDIS_PASSWORD"] = re.search(
- r"^requirepass (\S+)", conf, re.M).group(1)
- try:
- r = subprocess.run(
- ["C:\\Python310\\python.exe", "-X", "utf8", "-c",
- CALENDAR_PROBE_CODE],
- capture_output=True, text=True, timeout=timeout, env=env)
- except Exception as exc:
- return "exec", "CAL_PROBE_EXEC %s %s" % (type(exc).__name__,
- str(exc)[:100])
- cal = next((l for l in (r.stdout or "").splitlines()
- if l.startswith("CAL ")), "")
- if not cal:
- return "exec", "CAL_PROBE_NO_OUT rc=%s %s" % (
- r.returncode, (r.stderr or "").strip()[:100])
- parts = cal.split(None, 2)
- if len(parts) < 3:
- return "exec", "CAL_PROBE_BAD '%s'" % cal[:80]
- etype, eval_ = parts[1], parts[2][:40]
- if etype == "NoneType":
- return "empty", "calendar probe returned no data (%s)" % eval_
- if etype in ("int", "float"):
- return "ok", "calendar elem=%s %s" % (etype, eval_)
- return "bad", "CALENDAR_STR_CONTRACT elem=%s %s" % (etype, eval_)
-
-
-def relogin_running():
- """True if the v2 relogin python is mid-flight (avoid double trigger)."""
- try:
- import psutil
- except ImportError:
- return False
- for p in psutil.process_iter(["pid", "name", "cmdline"]):
- try:
- if "python" not in (p.info["name"] or "").lower():
- continue
- cmd = " ".join(p.info["cmdline"] or [])
- except Exception:
- continue
- if "qmt_relogin.py" in cmd:
- return True
- return False
-
-
-def trigger_relogin():
- r = subprocess.run(["schtasks", "/run", "/tn", RELOGIN_TASK],
- capture_output=True, text=True, timeout=30)
- ok = r.returncode == 0
- return ok, ((r.stdout or "") + (r.stderr or "")).strip()[:120]
-
-
-def wait_bridge(rc, timeout_sec, kind):
- """Poll bridge ping until it answers; every probe logged once at the end."""
- import time
- t0 = time.time()
- last = ""
- while time.time() - t0 < timeout_sec:
- up, detail = bridge_ping(rc)
- if up:
- return True, int(time.time() - t0), detail
- last = detail
- time.sleep(5)
- return False, int(time.time() - t0), last
diff --git a/scripts/qmt_relogin/qmt_grepall.py b/scripts/qmt_relogin/qmt_grepall.py
deleted file mode 100644
index 7960a939..00000000
--- a/scripts/qmt_relogin/qmt_grepall.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Binary-grep the whole QMT install for the strategy instance name.
-Searches UTF-8, GBK, UTF-16LE encodings of the anchor string."""
-import os
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-
-name = "\u65b0\u5efa\u7b56\u7565\u6587\u4ef63" # 新建策略文件3
-needles = {
- "utf8": name.encode("utf-8"),
- "gbk": name.encode("gbk"),
- "u16": name.encode("utf-16-le"),
-}
-
-skip_dirs = {"log", "__pycache__", "malldown"}
-found = 0
-for dirpath, dirnames, files in os.walk(root):
- dirnames[:] = [x for x in dirnames if x not in skip_dirs]
- for f in files:
- p = os.path.join(dirpath, f)
- try:
- if os.path.getsize(p) > 60 * 1024 * 1024:
- continue
- raw = open(p, "rb").read()
- except OSError:
- continue
- hit = [enc for enc, nd in needles.items() if nd in raw]
- if hit:
- rel = os.path.relpath(p, root)
- print("HIT %-60s %s size=%d" % (
- rel.encode("unicode_escape").decode("ascii")[:60],
- ",".join(hit), len(raw)))
- found += 1
- if found > 30:
- print("...TRUNC")
- break
- if found > 30:
- break
-print("FOUND", found)
diff --git a/scripts/qmt_relogin/qmt_logpeek.py b/scripts/qmt_relogin/qmt_logpeek.py
deleted file mode 100644
index cf56b84b..00000000
--- a/scripts/qmt_relogin/qmt_logpeek.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Peek at QMT client logs (GBK-encoded) around the restart window.
-Prints ASCII-escaped so nothing dies on the GBK console."""
-import datetime
-import io
-import os
-
-
-def esc(s):
- return str(s).encode("unicode_escape").decode("ascii")[:200]
-
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-logdir = os.path.join(root, "userdata", "log")
-
-
-def tail(path, n=40):
- try:
- with io.open(path, "r", encoding="gbk", errors="replace") as f:
- lines = f.readlines()
- return lines[-n:]
- except OSError as e:
- return ["<%s>" % e]
-
-
-f1 = os.path.join(logdir, "XtClient_Formula_20260909.log")
-print("=== tail XtClient_Formula (%s) ===" % esc(f1))
-for ln in tail(f1, 35):
- print(" ", esc(ln.rstrip()))
-
-f2 = os.path.join(logdir, "XtClient_20260909.log")
-print("=== grep XtClient (model/strategy/auto/formula/python, after 19:2x) ===")
-kws = ("模型", "策略", "自动", "formula", "Formula", "Model", "python", "Python",
- "auto", "Auto", "恢复", "异常")
-try:
- with io.open(f2, "r", encoding="gbk", errors="replace") as f:
- for ln in f:
- if any(k in ln for k in kws) and ("19:2" in ln or "19:3" in ln):
- print(" ", esc(ln.rstrip()))
-except OSError as e:
- print(" ", e)
-
-f3 = os.path.join(logdir, "XtClient_FormulaOutput_20260909.log")
-print("=== tail FormulaOutput ===")
-for ln in tail(f3, 15):
- print(" ", esc(ln.rstrip()))
diff --git a/scripts/qmt_relogin/qmt_nav_pystg.py b/scripts/qmt_relogin/qmt_nav_pystg.py
deleted file mode 100644
index 5f3e4b1d..00000000
--- a/scripts/qmt_relogin/qmt_nav_pystg.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-"""E1 step1 v2: the schtask PowerShell console window COVERS the QMT client
-(analyzed screenshot proved clicks landed on the console). So:
-1) minimize our own console, 2) foreground QMT, 3) click python策略 tree item
-(THIS boot, 3x-crop measured y=413, not the stale 385), 4) screenshot."""
-import ctypes
-import time
-
-import pyautogui
-
-pyautogui.FAILSAFE = False
-
-# 1) minimize the console window hosting this very process
-hwnd = ctypes.windll.kernel32.GetConsoleWindow()
-if hwnd:
- ctypes.windll.user32.ShowWindow(hwnd, 6) # SW_MINIMIZE
-time.sleep(1.0)
-
-# 2) bring the QMT client window to front (log all titles for diagnosis)
-try:
- import pygetwindow as gw
- titles = [w.title for w in gw.getAllWindows() if (w.title or "").strip()]
- print("TITLES:", "|".join(titles)[:600])
- for w in gw.getAllWindows():
- t = w.title or ""
- if ("XtItClient" in t or "QMT" in t or "quant" in t.lower()
- or "迅投" in t):
- try:
- w.activate()
- print("ACTIVATED:", t)
- except Exception as e:
- print("activate ERR", repr(e))
- break
-except Exception as e:
- print("focus ERR", repr(e))
-time.sleep(1.0)
-
-# 3) click python策略 (3x-crop verified on THIS boot: crop y=398 -> (398/3)+280 = 413)
-pyautogui.click(75, 413)
-time.sleep(3)
-pyautogui.screenshot(r"C:\sanguo_bigqmt\shot_nav.png")
-print("DONE nav2")
diff --git a/scripts/qmt_relogin/qmt_postmortem.py b/scripts/qmt_relogin/qmt_postmortem.py
deleted file mode 100644
index 02071425..00000000
--- a/scripts/qmt_relogin/qmt_postmortem.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Postmortem after a QMT client restart: redis bridge keys, rpc queue depth,
-and files touched in the last 30 min under the install root and tool dir.
-All output ASCII-escaped (console codepage is GBK)."""
-import datetime
-import os
-import pathlib
-import re
-import time
-
-
-def esc(s):
- return str(s).encode("unicode_escape").decode("ascii")[:170]
-
-
-print("=== postmortem %s ===" % datetime.datetime.now().strftime("%H:%M:%S"))
-
-try:
- import redis
- 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,
- decode_responses=True)
- keys = sorted(rc.keys("*"))
- print("REDIS_DB5_KEYS=%d" % len(keys))
- for k in keys[:50]:
- t = rc.type(k)
- extra = ""
- if t == "list":
- extra = " llen=%d" % rc.llen(k)
- print(" KEY %s type=%s%s" % (esc(k), t, extra))
-except Exception as e:
- print("REDIS_ERR", repr(e))
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-print("ROOT", esc(root))
-now = time.time()
-for label, base in (("INSTALL", root), (r"TOOLS", r"C:\sanguo_bigqmt")):
- hits = []
- for p in pathlib.Path(base).rglob("*"):
- try:
- if p.is_file() and (now - p.stat().st_mtime) < 1800:
- hits.append((p.stat().st_mtime, p.stat().st_size, p))
- except OSError:
- pass
- hits.sort(reverse=True)
- print("[%s] RECENT_30MIN=%d" % (label, len(hits)))
- for mt, sz, p in hits[:35]:
- try:
- rel = esc(p.relative_to(base))
- except Exception:
- rel = esc(p)
- print(" %s %8d %s" % (
- datetime.datetime.fromtimestamp(mt).strftime("%H:%M:%S"), sz, rel))
diff --git a/scripts/qmt_relogin/qmt_probe0915_task.xml b/scripts/qmt_relogin/qmt_probe0915_task.xml
deleted file mode 100644
index 53e2c95b..00000000
--- a/scripts/qmt_relogin/qmt_probe0915_task.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
- Big QMT pre-open ACTIVE trade probe 09:15 Mon-Fri: 510050 x100 @0.01 never-fill order + instant cancel. Junked order = stale session -> triggers sanguo-qmt-relogin before the open. Audit trail C:\sanguo_bigqmt\gate\qmt_gate_*.log
-
-
-
- 2026-09-10T09:15:00
- true
-
-
-
-
-
-
-
-
- 1
-
-
-
-
- false
- false
- PT12M
- IgnoreNew
- true
- true
-
-
-
- S-1-5-18
- HighestAvailable
-
-
-
-
- C:\Python310\python.exe
- -X utf8 C:\sanguo_bigqmt\qmt_probe_0915.py
-
-
-
diff --git a/scripts/qmt_relogin/qmt_probe_0915.py b/scripts/qmt_relogin/qmt_probe_0915.py
deleted file mode 100644
index 36105bd6..00000000
--- a/scripts/qmt_relogin/qmt_probe_0915.py
+++ /dev/null
@@ -1,209 +0,0 @@
-# -*- coding: utf-8 -*-
-"""sanguo-qmt-probe0915 — pre-open ACTIVE trade probe (the only gold standard
-for session-day freshness: passive queries cannot see a stale session).
-
-Order shape (proven 2026-09-09 09:40 on this exact account/counter):
- 510050.SH BUY 100 @ 0.01 FIX_PRICE — a price that can NEVER fill, so the
- probe is risk-free; what matters is how the counter treats it.
- accepted (status 48..56) -> session is day-current -> GREEN -> cancel it
- junked (status 57 / reject msg) -> stale session -> RED -> trigger
- sanguo-qmt-relogin (~4min, back before the 09:30 open)
-
-Window is hard-coded 09:15:00-09:19:30: 09:20-09:25 is the no-cancel auction
-phase — an order placed there could not be pulled before the open.
-
-Exit codes: 0 green | 1 red | 2 amber/skip.
-"""
-import datetime
-import sys
-import time
-
-from qmt_gate_common import (ACCOUNT, bridge_ping, gate_json, gate_log,
- procs_alive, redis_client, relogin_running,
- trigger_relogin, wait_bridge)
-
-STOCK = "510050.SH"
-WINDOW_START = (9, 15, 0)
-WINDOW_END = (9, 19, 30)
-LIVE_STATUSES = {48, 49, 50, 55, 56} # unreported..reported, part/full fill
-CANCEL_FAMILY = {51, 52, 53, 54} # accepted then (part) cancelled
-JUNK_STATUS = 57 # ORDER_JUNK = counter rejection
-HOLIDAY_WORDS = ("非交易", "休市", "闭市", "非交易日", "节假日")
-RECOVER_TIMEOUT = 360 # 6 min: back before the 09:30 open
-
-
-def _in_window(now):
- hms = (now.hour, now.minute, now.second)
- if hms < WINDOW_START:
- wait = (WINDOW_START[0] * 3600 + WINDOW_START[1] * 60 + WINDOW_START[2]
- - (hms[0] * 3600 + hms[1] * 60 + hms[2]))
- return "early", wait
- if hms > WINDOW_END:
- return "late", 0
- return "in", 0
-
-
-def rpc(rc, method, params, timeout=30):
- from bigqmt_signal_trader.redis_rpc import call_redis_rpc
- return call_redis_rpc(rc, ACCOUNT, method, params, timeout_seconds=timeout)
-
-
-def find_order(rc, remark):
- """The probe order of today by exact remark, from the counter's order list."""
- for _ in range(5):
- try:
- r = rpc(rc, "query_stock_orders", {"account_id": ACCOUNT})
- orders = r.get("data") if isinstance(r.get("data"), list) else []
- for o in orders:
- if str(o.get("remark") or o.get("user_order_id") or "") == remark:
- return o
- except Exception as exc:
- gate_log("PROBE0915", "NOTE", "orders query err %r" % exc)
- time.sleep(2)
- return None
-
-
-def place(rc, remark):
- res = rpc(rc, "order_stock", {
- "account_id": ACCOUNT, "stock_code": STOCK, "order_type": 23,
- "order_volume": 100, "price_type": 11, "price": 0.01,
- "strategy_name": "probe", "order_remark": remark})
- gate_log("PROBE0915", "NOTE",
- "order placed remark=%s res=%s" % (remark, str(res)[:160]))
-
-
-def cancel(rc, order):
- sysid = str(order.get("order_sys_id") or "")
- if not sysid:
- gate_log("PROBE0915", "WARN", "no order_sys_id to cancel: %s"
- % str(order)[:160])
- return False
- for attempt in range(3):
- try:
- r = rpc(rc, "cancel_order_stock_sysid",
- {"account_id": ACCOUNT, "market": "", "order_sysid": sysid})
- ok = "ok" in r and r.get("ok") is not False
- gate_log("PROBE0915", "NOTE", "cancel sysid=%s try%d -> %s"
- % (sysid, attempt + 1, str(r)[:120]))
- except Exception as exc:
- gate_log("PROBE0915", "WARN", "cancel exc %r" % exc)
- ok = False
- time.sleep(2)
- cur = find_order(rc, str(order.get("remark") or ""))
- if cur and int(cur.get("status") or 0) in (CANCEL_FAMILY | LIVE_STATUSES):
- st = int(cur.get("status") or 0)
- if st in CANCEL_FAMILY:
- return True
- elif cur is None:
- return True # gone from the list: fully pulled
- gate_log("PROBE0915", "CRITICAL",
- "PROBE_ORDER_LEFT_LIVE sysid=%s status=%s — cannot fill @0.01, "
- "day-end settlement clears it; review manually"
- % (sysid, (cur or {}).get("status")))
- return False
-
-
-def main():
- now = datetime.datetime.now()
- remark = "probe:gate:%s" % now.strftime("%Y%m%d")
- state, wait = _in_window(now)
- if state == "late":
- gate_log("PROBE0915", "SKIP", "past 09:19:30 window, no probe today")
- gate_json("probe0915", "SKIP", reason="late")
- return 2
- if state == "early":
- gate_log("PROBE0915", "NOTE", "early by %ds, waiting for window" % wait)
- time.sleep(min(wait + 1, 300))
- now = datetime.datetime.now()
- if _in_window(now)[0] != "in":
- gate_log("PROBE0915", "SKIP", "window lost while waiting")
- gate_json("probe0915", "SKIP", reason="window-lost")
- return 2
-
- try:
- rc = redis_client()
- except Exception as exc:
- gate_log("PROBE0915", "AMBER", "redis connect failed %r" % exc)
- return 2
- up, detail = bridge_ping(rc)
- if not up:
- gate_log("PROBE0915", "AMBER",
- "bridge already down pre-order (%s); relogin is sentinel's "
- "job, skipping probe" % detail)
- return 2
-
- existing = find_order(rc, remark)
- if existing is None:
- place(rc, remark)
- # counter reporting can lag at the 09:15:00 boundary second
- # (night test 09-09: after-hours submissions stayed invisible);
- # poll patiently — the cancel window still has minutes of margin.
- for _ in range(3):
- time.sleep(3)
- existing = find_order(rc, remark)
- if existing is not None:
- break
- if existing is None:
- gate_log("PROBE0915", "AMBER",
- "order not visible after 5 queries; cannot judge, no trigger")
- gate_json("probe0915", "AMBER", reason="order-not-visible")
- return 2
-
- status = int(existing.get("status") or 0)
- msg = str(existing.get("status_msg") or "")[:100]
- gate_log("PROBE0915", "NOTE",
- "order status=%d msg=%r sysid=%s" % (
- status, msg, existing.get("order_sys_id")))
-
- if status == JUNK_STATUS or (msg and any(w in msg for w in ("废", "拒"))):
- if any(w in msg for w in HOLIDAY_WORDS):
- gate_log("PROBE0915", "AMBER",
- "junked with holiday wording (%r) — not a staleness "
- "signal, no trigger" % msg)
- gate_json("probe0915", "AMBER", status=status, msg=msg)
- return 2
- gate_log("PROBE0915", "RED",
- "counter junked the probe order -> session stale; "
- "triggering relogin")
- return recover(rc, "junked:%s" % msg)
-
- # accepted by the counter in any form -> session is day-current
- ok_cancel = True
- if status in LIVE_STATUSES:
- ok_cancel = cancel(rc, existing)
- gate_log("PROBE0915", "GREEN",
- "order accepted (status=%d) -> session day-current; cancelled=%s"
- % (status, ok_cancel))
- gate_json("probe0915", "GREEN", status=status, cancelled=ok_cancel,
- sysid=str(existing.get("order_sys_id")))
- return 0
-
-
-def recover(rc, reason):
- if relogin_running():
- gate_log("PROBE0915", "SKIP", "relogin already in flight (%s)" % reason)
- else:
- ok, out = trigger_relogin()
- if not ok:
- gate_log("PROBE0915", "AMBER", "schtasks /run failed: %s" % out)
- return 2
- gate_log("PROBE0915", "ACTION",
- "relogin triggered (%s); polling bridge <=%ds" % (reason,
- RECOVER_TIMEOUT))
- ok, took, detail = wait_bridge(rc, RECOVER_TIMEOUT, "PROBE0915")
- procs = procs_alive()
- if ok:
- gate_log("PROBE0915", "RECOVERED",
- "bridge back in %ds before open (%s) procs=%s" % (
- took, detail, procs))
- gate_json("probe0915", "RED_RECOVERED", reason=reason, took_sec=took)
- return 1 # the session WAS stale this morning; red stays on record
- gate_log("PROBE0915", "RED",
- "NOT recovered in %ds (%s) — manual attention before open" % (
- took, detail))
- gate_json("probe0915", "RED", reason=reason, took_sec=took, procs=procs)
- return 1
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/scripts/qmt_relogin/qmt_pypeek.py b/scripts/qmt_relogin/qmt_pypeek.py
deleted file mode 100644
index 67aed91e..00000000
--- a/scripts/qmt_relogin/qmt_pypeek.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Inspect /python (strategy scripts) + hunt strategy registry files."""
-import os
-import time
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-
-pydir = os.path.join(root, "python")
-print("=== %s ===" % pydir.encode("unicode_escape").decode("ascii"))
-if os.path.isdir(pydir):
- for f in sorted(os.listdir(pydir)):
- p = os.path.join(pydir, f)
- st = os.stat(p)
- print(" %-50s %9d %s" % (
- f.encode("unicode_escape").decode("ascii")[:50], st.st_size,
- time.strftime("%m-%d %H:%M", time.localtime(st.st_mtime))))
-else:
- print(" MISSING")
-
-# strategy registry candidates anywhere under install root (small config-ish files
-# mentioning formula), excluding logs/data dirs we already know
-print("=== hunting registry candidates ===")
-skip = {"log", "dumps"}
-for dirpath, dirnames, files in os.walk(root):
- dirnames[:] = [x for x in dirnames if x not in skip and not x.startswith("bin")]
- rel = os.path.relpath(dirpath, root)
- depth = rel.count(os.sep)
- if depth > 3:
- dirnames[:] = []
- continue
- for f in files:
- low = f.lower()
- if any(k in low for k in ("formula", "strategy", "model")) and \
- low.endswith((".xml", ".json", ".ini", ".dat", ".cfg", ".db")):
- p = os.path.join(dirpath, f)
- st = os.stat(p)
- print(" %-70s %8d %s" % (
- os.path.join(rel, f).encode("unicode_escape").decode("ascii")[:70],
- st.st_size,
- time.strftime("%m-%d %H:%M", time.localtime(st.st_mtime))))
diff --git a/scripts/qmt_relogin/qmt_queue_peek.py b/scripts/qmt_relogin/qmt_queue_peek.py
deleted file mode 100644
index 8f6e8431..00000000
--- a/scripts/qmt_relogin/qmt_queue_peek.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Read-only: inspect stale bridge RPC queue composition before any revival."""
-import json
-import re
-import time
-from collections import Counter
-
-import redis
-
-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,
- decode_responses=True)
-Q = "bigqmt:rpc:queue:66639661"
-
-n1 = rc.llen(Q)
-time.sleep(5)
-n2 = rc.llen(Q)
-print("llen:", n1, "->", n2, "(+5s delta %d, producers %s)" % (
- n2 - n1, "ACTIVE" if n2 != n1 else "idle"))
-
-items = rc.lrange(Q, 0, -1)
-print("items:", len(items))
-methods = Counter()
-order_like = 0
-for it in items:
- try:
- obj = json.loads(it)
- m = obj.get("method", "?")
- except Exception:
- m = ""
- methods[m] += 1
- if any(k in m for k in ("order", "cancel", "trade", "buy", "sell")):
- order_like += 1
-print("method histogram:")
-for m, c in methods.most_common():
- print(" %-30s %d" % (m, c))
-print("ORDER-LIKE total:", order_like)
-
-print("=== head 3 raw ===")
-for it in items[:3]:
- print(" ", it[:260].encode("unicode_escape").decode("ascii"))
-print("=== tail 3 raw ===")
-for it in items[-3:]:
- print(" ", it[:260].encode("unicode_escape").decode("ascii"))
-print("DONE5")
diff --git a/scripts/qmt_relogin/qmt_queue_scan.py b/scripts/qmt_relogin/qmt_queue_scan.py
deleted file mode 100644
index c051eee3..00000000
--- a/scripts/qmt_relogin/qmt_queue_scan.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Read-only: decode ALL stale queue items with the library's own codec."""
-import json
-from collections import Counter
-
-import redis
-
-from bigqmt_signal_trader.redis_rpc import (
- ORDER_METHODS, decode_rpc_request_payload)
-
-with open(r"C:\redis\redis.conf") as f:
- import re
- 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,
- decode_responses=True)
-Q = "bigqmt:rpc:queue:66639661"
-
-items = rc.lrange(Q, 0, -1)
-methods = Counter()
-bad = 0
-for it in items:
- try:
- obj = json.loads(decode_rpc_request_payload(it))
- methods[obj.get("method", "?")] += 1
- except Exception:
- bad += 1
-print("total:", len(items), "undecodable:", bad)
-for m, c in methods.most_common():
- tag = "ORDER!" if m in ORDER_METHODS else "read"
- print(" %-30s %5d %s" % (m, c, tag))
-print("ORDER total:", sum(c for m, c in methods.items() if m in ORDER_METHODS))
-print("DONE6")
diff --git a/scripts/qmt_relogin/qmt_relogin.py b/scripts/qmt_relogin/qmt_relogin.py
deleted file mode 100644
index 5658ed02..00000000
--- a/scripts/qmt_relogin/qmt_relogin.py
+++ /dev/null
@@ -1,243 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Morning auto-relogin for the Big QMT client (surgical: XtItClient.exe only).
-
-Flow: discover install dir (psutil, cached to disk) -> check session state
--> flush stale bridge queue (read-only guard: abort if any ORDER method found)
--> taskkill XtItClient.exe (polite first; mini/miniquote untouched, xt_eod
-depends on them) -> settle -> open_qmt: exe mode first (remembered-session
-auto-login, proven by the daily 08:50 built-in restart), login mode with
-typed credentials as fallback -> poll bridge RPC until it answers.
-
-Force-kill note: a forced shutdown is known to SKIP instance auto-run on next
-boot; kill_client still forces as last resort but the bridge check will then
-fail loudly (exit 5) instead of lying green.
-
-Known boundaries (measured 09-09 night):
-- The login dialog has a 4-char graphic captcha (refreshes every 40s) and
- ships with remember-password / auto-login unchecked. Typed-credential login
- WILL most likely die on the captcha; the fix is a one-time manual login with
- both checkboxes ticked, after which cold boots take the exe auto-login path.
-- Polite close (WM_CLOSE) logs the session OUT -> next boot shows the login
- dialog; force-kill KEEPS the session -> next boot auto-logins straight to
- the main window (proven 19:27 vs 20:53 same night).
-- XtMiniQmt.exe / miniquote.exe are the xt_eod data leg. They were observed
- dying alongside our surgical XtItClient kills even though taskkill /IM
- matches only one name -- so we log their state after every kill for early
- detection, and warn (not fail) if they are gone at success time.
-
-Credentials come from BIGQMT_LOGIN_USER / BIGQMT_LOGIN_PASSWORD env, set by the
-PowerShell wrapper from a DPAPI-encrypted blob: never in argv, never on disk.
-
-Exit codes: 0 ok | 3 creds missing | 4 session locked (fallback blocked)
- 6 client would not close/open | 7 order-class items in stale queue
- 5 bridge rpc still down after login
-"""
-import datetime
-import os
-import subprocess
-import sys
-import time
-
-ACCOUNT = "66639661"
-CACHE_PATH = r"C:\sanguo_bigqmt\qmt_install_dir.txt"
-QUEUE_KEY = "bigqmt:rpc:queue:%s" % ACCOUNT
-
-
-def log(msg):
- print("[%s] %s" % (datetime.datetime.now().strftime("%m-%d %H:%M:%S"), msg), flush=True)
-
-
-def discover_install_dir():
- """Find bin.x64 from the running XtItClient.exe; cache for cold starts."""
- try:
- import psutil
- for p in psutil.process_iter(["name"]):
- try:
- if (p.info["name"] or "").lower() == "xtitclient.exe":
- exe = p.exe()
- bin_dir = os.path.dirname(exe)
- with open(CACHE_PATH, "w", encoding="utf-8") as f:
- f.write(bin_dir)
- log("install_dir discovered live: %r" % bin_dir)
- return bin_dir
- except Exception:
- continue
- except Exception as exc:
- log("psutil discovery failed: %r" % exc)
- if os.path.isfile(CACHE_PATH):
- with open(CACHE_PATH, encoding="utf-8") as f:
- cached = f.read().strip()
- if cached:
- log("install_dir from cache: %r" % cached)
- return cached
- return None
-
-
-def _procs_alive():
- """Name -> bool for the QMT family processes we care about."""
- out = subprocess.run(["tasklist", "/FO", "CSV", "/NH"],
- capture_output=True, text=True).stdout or ""
- return {name: ('"%s"' % name) in out
- for name in ("XtItClient.exe", "XtMiniQmt.exe", "miniquote.exe")}
-
-
-def kill_client(grace_seconds=8):
- """Kill only XtItClient.exe. Polite first (WM_CLOSE lets it flush), force after."""
- subprocess.run(["taskkill", "/IM", "XtItClient.exe"],
- capture_output=True)
- deadline = time.time() + grace_seconds
- while time.time() < deadline:
- out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq XtItClient.exe"],
- capture_output=True, text=True).stdout or ""
- if "XtItClient.exe" not in out:
- log("XtItClient closed cleanly; QMT family now: %s"
- % _procs_alive())
- return True
- time.sleep(1)
- log("still alive after %ds; forcing (auto-run may be skipped!)" % grace_seconds)
- subprocess.run(["taskkill", "/IM", "XtItClient.exe", "/F"], capture_output=True)
- time.sleep(3)
- out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq XtItClient.exe"],
- capture_output=True, text=True).stdout or ""
- ok = "XtItClient.exe" not in out
- log("force kill done, gone=%s" % ok)
- return ok
-
-
-def flush_stale_queue():
- """Delete stale read-only requests; REFUSE if any order-class item waits."""
- import json
- import re
- import redis
- from bigqmt_signal_trader.redis_rpc import (ORDER_METHODS,
- decode_rpc_request_payload)
- 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,
- decode_responses=True)
- n = rc.llen(QUEUE_KEY)
- orders = 0
- for it in rc.lrange(QUEUE_KEY, 0, -1):
- try:
- obj = json.loads(decode_rpc_request_payload(it))
- if obj.get("method") in ORDER_METHODS:
- orders += 1
- except Exception:
- continue
- if orders:
- log("FATAL: %d order-class items in stale queue; manual review" % orders)
- return False
- rc.delete(QUEUE_KEY)
- log("queue flushed: %d stale read-only items" % n)
- return True
-
-
-def bridge_alive(timeout_seconds=150, rc=None):
- """Poll the bridge RPC server until it answers. Any dict envelope counts --
- 'ping' may not be a registered method, so fall back to an ORDER-class query
- (read-only, and ORDER data is available immediately after login)."""
- from bigqmt_signal_trader.redis_rpc import call_redis_rpc
- if rc is None:
- import re
- import redis
- 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,
- decode_responses=True)
- deadline = time.time() + timeout_seconds
- last = None
- while time.time() < deadline:
- for method, params in (("ping", {}), ("query_stock_orders", {"account_id": ACCOUNT})):
- try:
- r = call_redis_rpc(rc, ACCOUNT, method, params, timeout_seconds=10)
- if isinstance(r, dict):
- return method, r
- last = "%s -> %r" % (method, r)
- except Exception as exc:
- last = "%s -> %s" % (method, exc)
- time.sleep(5)
- log("bridge rpc last: %s" % last)
- return None
-
-
-def warn_data_leg():
- """2026-09-11: xt_eod switched to the big-QMT bridge (xtquant_bridge
- shim), so XtMiniQmt.exe absence is NORMAL and no longer a data gap.
- Kept as a no-op placeholder because the relogin flow used to log a
- misleading warning here; see xt-eod-bridge-leg-switch-20260911."""
- return None
-
-
-def main():
- user = os.environ.get("BIGQMT_LOGIN_USER", "")
- pwd = os.environ.get("BIGQMT_LOGIN_PASSWORD", "")
- log("=== qmt_relogin start (user=%s pwdlen=%d) ===" % (user, len(pwd)))
-
- from bigqmt_signal_trader import qmt_launcher as L
-
- install_dir = discover_install_dir()
- if not install_dir or not os.path.isdir(install_dir):
- log("FATAL: cannot resolve install dir: %r" % install_dir)
- return 6
-
- if not flush_stale_queue():
- return 7
-
- if not kill_client():
- log("FATAL: XtItClient.exe would not die")
- return 6
-
- time.sleep(5) # socket linger settle (upstream restart_qmt uses 5s)
-
- creds = {"user": user, "password": pwd} if user and pwd else None
- locked = L.session_is_locked()
- if locked:
- log("warn: console session locked; exe auto-login still allowed, "
- "credential-typing fallback will not be possible")
-
- mode_used = None
- try:
- waited = L.open_qmt(install_dir, mode="exe", ready_timeout_seconds=240)
- mode_used = "exe"
- log("open_qmt(exe) ready in %.1fs" % waited)
- except Exception as exc:
- log("exe-mode failed: %r" % exc)
- mode_used = None
-
- got = bridge_alive(150) if mode_used else None
- if got:
- log("bridge up after exe-mode (remembered session) via %r ok=%s"
- % (got[0], got[1].get("ok")))
- log("=== RELOGIN_DONE ===")
- return 0
-
- # fallback: credential-typing login mode
- if not creds:
- log("FATAL: bridge down and no credentials for login fallback")
- return 3
- if locked:
- log("FATAL: bridge down, fallback needs typing but session locked")
- return 4
- if mode_used: # client is up but bridge dead: close before login retry
- if not kill_client():
- return 6
- time.sleep(5)
- try:
- waited = L.open_qmt(install_dir, mode="login", credentials=creds,
- window_title_prefix="QMT", ready_timeout_seconds=240)
- log("open_qmt(login) ready in %.1fs" % waited)
- except Exception as exc:
- log("FATAL: open_qmt(login) failed: %r" % exc)
- return 6
-
- got = bridge_alive(150)
- if not got:
- log("FATAL: bridge rpc did not answer after login")
- return 5
- log("bridge rpc answering via %r: ok=%s" % (got[0], got[1].get("ok")))
- log("=== RELOGIN_DONE ===")
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/scripts/qmt_relogin/qmt_relogin_run.ps1 b/scripts/qmt_relogin/qmt_relogin_run.ps1
deleted file mode 100644
index 60b66ae7..00000000
--- a/scripts/qmt_relogin/qmt_relogin_run.ps1
+++ /dev/null
@@ -1,21 +0,0 @@
-# Decrypt the DPAPI credential blob into process env and run the relogin.
-# Plaintext exists only in this process's memory and the child's environment.
-# Keep this file ASCII-only (the console codepage is GBK).
-$ErrorActionPreference = 'Stop'
-$encPath = 'C:\sanguo_bigqmt\creds\qmt_login.enc'
-$logPath = 'C:\sanguo_bigqmt\qmt_relogin.log'
-
-$enc = (Get-Content -Raw -LiteralPath $encPath).Trim()
-$sec = ConvertTo-SecureString -String $enc
-$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)
-$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
-[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
-$j = $plain | ConvertFrom-Json
-$plain = $null
-
-$env:BIGQMT_LOGIN_USER = [string]$j.user
-$env:BIGQMT_LOGIN_PASSWORD = [string]$j.password
-
-& C:\Python310\python.exe C:\sanguo_bigqmt\qmt_relogin.py 2>&1 |
- Out-File -Append -Encoding utf8 -LiteralPath $logPath
-Add-Content -Encoding utf8 -LiteralPath $logPath -Value ("EXIT=" + $LASTEXITCODE)
diff --git a/scripts/qmt_relogin/qmt_relogin_task.xml b/scripts/qmt_relogin/qmt_relogin_task.xml
deleted file mode 100644
index 7bd2bd43..00000000
--- a/scripts/qmt_relogin/qmt_relogin_task.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- Big QMT morning auto-relogin (surgical XtItClient.exe only; mini/miniquote untouched). Decryption+UI login+bridge verify, log at C:\sanguo_bigqmt\qmt_relogin.log
-
-
-
- 2026-09-10T07:50:00
- true
-
- 1
-
-
-
-
- false
- false
- PT15M
- IgnoreNew
- true
-
-
-
- Administrator
- InteractiveToken
- LeastPrivilege
-
-
-
-
- powershell.exe
- -NoProfile -ExecutionPolicy Bypass -File C:\sanguo_bigqmt\qmt_relogin_run.ps1
-
-
-
diff --git a/scripts/qmt_relogin/qmt_relogin_test.py b/scripts/qmt_relogin/qmt_relogin_test.py
deleted file mode 100644
index 5d6cc45d..00000000
--- a/scripts/qmt_relogin/qmt_relogin_test.py
+++ /dev/null
@@ -1,143 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Experiment: graceful close -> exe relaunch -> auto-login/auto-run/bridge.
-Read-only w.r.t. trading: no order methods sent anywhere. Exit codes:
-0 all green | 10 polite close refused | 11 launch/ready failed
-12 instance not auto-run | 13 bridge rpc dead."""
-import datetime
-import io
-import os
-import re
-import subprocess
-import sys
-import time
-
-ACCOUNT = "66639661"
-
-
-def log(m):
- print("[%s] %s" % (datetime.datetime.now().strftime("%H:%M:%S"), m), flush=True)
-
-
-def shot(name):
- try:
- import pyautogui
- pyautogui.FAILSAFE = False
- pyautogui.screenshot(r"C:\sanguo_bigqmt\%s" % name)
- log("shot saved: %s" % name)
- except Exception as e:
- log("shot err %r" % e)
-
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-formula_log = os.path.join(root, "userdata", "log", "XtClient_Formula_20260909.log")
-
-from bigqmt_signal_trader import qmt_launcher as L # noqa: E402
-from bigqmt_signal_trader.redis_rpc import call_redis_rpc # noqa: E402
-import redis # noqa: E402
-
-try:
- log("session locked: %s" % L.session_is_locked())
-except Exception as e:
- log("session check err %r" % e)
-
-# 1) flush stale queue (composition pre-verified: 100% read-only, 0 orders)
-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,
- decode_responses=True)
-Q = "bigqmt:rpc:queue:%s" % ACCOUNT
-n = rc.llen(Q)
-rc.delete(Q)
-log("queue flushed: %d stale read-only items deleted" % n)
-
-# 2) graceful close, polite ONLY
-subprocess.run(["taskkill", "/IM", "XtItClient.exe"], capture_output=True)
-deadline = time.time() + 20
-alive = True
-while time.time() < deadline:
- out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq XtItClient.exe"],
- capture_output=True, text=True).stdout or ""
- if "XtItClient.exe" not in out:
- alive = False
- break
- time.sleep(1)
-if alive:
- log("ABORT: polite close refused (dialog up?); NOT forcing")
- shot("relogin_abort.png")
- sys.exit(10)
-log("polite close ok")
-time.sleep(5)
-t_launch = datetime.datetime.now()
-log("relaunching via open_qmt(mode=exe) at %s" % t_launch.strftime("%H:%M:%S"))
-
-# 3) exe relaunch: remembered-session auto-login path (no typing)
-try:
- waited = L.open_qmt(d, mode="exe", ready_timeout_seconds=240)
- log("open_qmt(exe) ready in %.1fs" % waited)
-except Exception as e:
- log("FATAL open_qmt(exe): %r" % e)
- shot("relogin_launchfail.png")
- sys.exit(11)
-
-
-def formula_events(since):
- """Return (banner_seen, runonce_seen) for log lines timestamped >= since."""
- banner = runonce = False
- try:
- with io.open(formula_log, "r", encoding="gbk", errors="replace") as f:
- for ln in f:
- try:
- ts = datetime.datetime.strptime(ln[:19], "%Y-%m-%d %H:%M:%S")
- except ValueError:
- continue
- if ts < since:
- continue
- if "XtItClient/tags" in ln:
- banner = True
- if "runonce" in ln or "PythonFormula::" in ln:
- runonce = True
- except OSError:
- pass
- return banner, runonce
-
-
-# 4) instance auto-run detection
-t0 = time.time()
-banner = runonce = False
-while time.time() - t0 < 150:
- time.sleep(5)
- banner, runonce = formula_events(t_launch)
- if runonce:
- break
-log("formula after relaunch: banner=%s runonce=%s (waited %.0fs)"
- % (banner, runonce, time.time() - t0))
-if not runonce:
- log("instance did NOT auto-run after graceful relaunch")
- shot("relogin_norun.png")
- sys.exit(12)
-
-# 5) bridge probe (ORDER-class query, safe immediately after login)
-t0 = time.time()
-got = None
-while time.time() - t0 < 150:
- for method, params in (("ping", {}), ("query_stock_orders",
- {"account_id": ACCOUNT})):
- try:
- r = call_redis_rpc(rc, ACCOUNT, method, params, timeout_seconds=10)
- if isinstance(r, dict):
- got = (method, r)
- break
- except Exception as e:
- last = "%s -> %s" % (method, e)
- if got:
- break
- time.sleep(5)
-if not got:
- log("FATAL bridge rpc no answer; last=%r" % last)
- shot("relogin_norpc.png")
- sys.exit(13)
-log("bridge answering via %r ok=%s" % (got[0], got[1].get("ok")))
-shot("relogin_green.png")
-log("=== RELOGIN_TEST_ALL_GREEN ===")
-sys.exit(0)
diff --git a/scripts/qmt_relogin/qmt_sentinel.py b/scripts/qmt_relogin/qmt_sentinel.py
deleted file mode 100644
index 5a6daa02..00000000
--- a/scripts/qmt_relogin/qmt_sentinel.py
+++ /dev/null
@@ -1,188 +0,0 @@
-# -*- coding: utf-8 -*-
-"""sanguo-qmt-sentinel — every-30min availability patrol for the Big QMT stack.
-
-Verdict rules (2026-09-09 design, user-approved):
- judge RED only on: XtItClient.exe gone OR bridge RPC ping dead.
- account-class probes (asset/positions) and the data leg (XtMiniQmt/miniquote)
- are recorded as telemetry only — post-close false-reds by design.
- RED -> schtasks /run sanguo-qmt-relogin (reuse v2, never copy login code)
- -> poll bridge <=10min -> re-check -> full audit trail in gate log.
- No backoff on consecutive failures (user decision #4): every tick independent,
- failures just stack in the log; the next tick retries naturally.
-
-Exit codes: 0 green/recovered | 1 red-remains | 2 amber (cannot judge).
-"""
-import json
-
-from qmt_gate_common import (ACCOUNT, bridge_ping, calendar_probe, gate_json,
- gate_log, procs_alive, redis_client,
- relogin_running, trigger_relogin, wait_bridge)
-
-RECOVER_TIMEOUT = 600 # 10 min budget for relogin round trip
-
-
-def telemetry(rc):
- """Recorded, never judged: allow_order bit, asset cash, queue length."""
- from bigqmt_signal_trader.redis_rpc import call_redis_rpc
- t = {}
- try:
- r = call_redis_rpc(rc, ACCOUNT, "query_stock_asset",
- {"account_id": ACCOUNT}, timeout_seconds=15)
- d = r.get("data") or {}
- t["cash"] = d.get("cash")
- t["mktval"] = d.get("market_value")
- except Exception as exc:
- t["asset_err"] = "%s %s" % (type(exc).__name__, str(exc)[:80])
- try:
- t["queue_len"] = rc.llen("bigqmt:rpc:queue:%s" % ACCOUNT)
- except Exception as exc:
- t["queue_err"] = str(exc)[:60]
- return t
-
-
-def infra_probe():
- """Sixth probe (2026-09-19, runbook#35 wave 1, user-approved).
-
- Trading stack is NOT the only thing on this box: the 09-13 OOM cascade
- starved frps for 5 days and nobody noticed -- the sentinel never looked.
- Pure observation: RED here NEVER triggers recover() (relogin cannot help
- frps; the fix for a dead frps is documented `sc start sanguo-frps`).
- Runs first in main() so trading-stack early-returns cannot skip it.
- """
- import ctypes
- import socket
- import urllib.error
- import urllib.request
-
- commit_pct = None
- try:
- class _MS(ctypes.Structure):
- _fields_ = [("dwLength", ctypes.c_ulong),
- ("dwMemoryLoad", ctypes.c_ulong),
- ("ullTotalPhys", ctypes.c_ulonglong),
- ("ullAvailPhys", ctypes.c_ulonglong),
- ("ullTotalPageFile", ctypes.c_ulonglong),
- ("ullAvailPageFile", ctypes.c_ulonglong),
- ("ullTotalVirtual", ctypes.c_ulonglong),
- ("ullAvailVirtual", ctypes.c_ulonglong),
- ("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
- ms = _MS()
- ms.dwLength = ctypes.sizeof(_MS)
- if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(ms)) \
- and ms.ullTotalPageFile:
- commit_pct = round(
- 100.0 * (ms.ullTotalPageFile - ms.ullAvailPageFile)
- / ms.ullTotalPageFile, 1)
- except Exception:
- pass # non-Windows / API failure: telemetry gap, not a verdict
-
- frps_up = False
- try:
- s = socket.create_connection(("127.0.0.1", 7000), timeout=3)
- s.close()
- frps_up = True
- except OSError:
- pass
-
- claude = "?"
- try:
- req = urllib.request.Request(
- "https://claude.mysanguo.online", method="HEAD")
- with urllib.request.urlopen(req, timeout=10) as resp:
- claude = resp.status
- except urllib.error.HTTPError as exc:
- claude = exc.code
- except Exception as exc:
- claude = type(exc).__name__
-
- verdict, notes = "GREEN", []
- if not frps_up:
- verdict = "RED"
- notes.append("frps:7000 dead -> sc start sanguo-frps (runbook#35)")
- if claude != 200:
- verdict = "RED"
- notes.append("claude=%s" % claude)
- if commit_pct is not None and commit_pct >= 90:
- if verdict == "GREEN":
- verdict = "AMBER" # predictive: warn BEFORE the OOM cascade
- notes.append("commit=%s%% >=90" % commit_pct)
- gate_log("INFRA", verdict, "frps=%s claude=%s commit=%s%% %s" % (
- "up" if frps_up else "DEAD", claude, commit_pct,
- "; ".join(notes) if notes else "ok"))
- gate_json("infra_probe", verdict, fname="qmt_gate_latest_infra.json",
- frps_up=frps_up, claude=claude, commit_pct=commit_pct)
- return verdict
-
-
-def main():
- infra_probe()
- procs = procs_alive()
- if not procs.get("XtItClient.exe"):
- msg = "XtItClient.exe DEAD family=%s" % procs
- gate_log("SENTINEL", "RED", msg)
- return recover("process-dead")
- try:
- rc = redis_client()
- except Exception as exc:
- gate_log("SENTINEL", "AMBER", "redis connect failed %r" % exc)
- return 2
- up, detail = bridge_ping(rc)
- if not up:
- gate_log("SENTINEL", "RED", "bridge ping dead (%s)" % detail)
- return recover("bridge-dead")
- cal_state, cal_detail = calendar_probe()
- if cal_state == "bad":
- gate_log("SENTINEL", "RED", cal_detail)
- return recover("calendar-str-contract")
- if cal_state in ("empty", "exec"):
- gate_log("SENTINEL", "AMBER", "fifth-probe inconclusive: %s" % cal_detail)
- t = telemetry(rc)
- msg = "proc=1 bridge=up %s mini=%d miniquote=%d cash=%s mktval=%s q=%s" % (
- detail, procs.get("XtMiniQmt.exe", 0), procs.get("miniquote.exe", 0),
- t.get("cash"), t.get("mktval"), t.get("queue_len"))
- gate_log("SENTINEL", "GREEN", msg)
- gate_json("sentinel", "GREEN", bridge=detail, procs=procs, **t)
- return 0
-
-
-def recover(reason):
- """Trigger v2 relogin, wait for the bridge, re-check, log the outcome."""
- try:
- rc = redis_client()
- except Exception as exc:
- gate_log("SENTINEL", "AMBER", "redis failed pre-recovery %r" % exc)
- return 2
- if relogin_running():
- gate_log("SENTINEL", "SKIP",
- "relogin already in flight (%s); let it finish" % reason)
- ok, took, detail = wait_bridge(rc, RECOVER_TIMEOUT, "SENTINEL")
- return 0 if ok else 1
- ok, out = trigger_relogin()
- if not ok:
- gate_log("SENTINEL", "AMBER",
- "schtasks /run %s failed: %s" % (reason, out))
- return 2
- gate_log("SENTINEL", "ACTION",
- "relogin triggered (%s); polling bridge <=%ds" % (reason,
- RECOVER_TIMEOUT))
- ok, took, detail = wait_bridge(rc, RECOVER_TIMEOUT, "SENTINEL")
- procs = procs_alive()
- if ok and procs.get("XtItClient.exe"):
- t = telemetry(rc)
- gate_log("SENTINEL", "RECOVERED",
- "bridge back in %ds (%s) procs=%s cash=%s" % (
- took, detail, procs, t.get("cash")))
- gate_json("sentinel", "RECOVERED", reason=reason, took_sec=took,
- bridge=detail, procs=procs, **t)
- return 0
- gate_log("SENTINEL", "RED",
- "NOT recovered in %ds (bridge=%s procs=%s) — no backoff by "
- "design, next tick retries" % (took, detail, procs))
- gate_json("sentinel", "RED", reason=reason, took_sec=took, bridge=detail,
- procs=procs)
- return 1
-
-
-if __name__ == "__main__":
- import sys
- sys.exit(main())
diff --git a/scripts/qmt_relogin/qmt_sentinel_task.xml b/scripts/qmt_relogin/qmt_sentinel_task.xml
deleted file mode 100644
index 1364d404..00000000
--- a/scripts/qmt_relogin/qmt_sentinel_task.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
- Big QMT 30min availability sentinel: XtItClient process + bridge RPC ping; RED triggers sanguo-qmt-relogin then polls recovery. Audit trail C:\sanguo_bigqmt\gate\qmt_gate_*.log
-
-
-
- 2026-09-10T00:10:00
- true
-
- 1
-
-
- PT30M
- P1D
- false
-
-
-
-
- false
- false
- PT12M
- IgnoreNew
- true
- true
-
-
-
- S-1-5-18
- HighestAvailable
-
-
-
-
- C:\Python310\python.exe
- -X utf8 C:\sanguo_bigqmt\qmt_sentinel.py
-
-
-
diff --git a/scripts/qmt_relogin/qmt_today.py b/scripts/qmt_relogin/qmt_today.py
deleted file mode 100644
index ff657f65..00000000
--- a/scripts/qmt_relogin/qmt_today.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Read-only: today's QMT panorama — log timeline, processes, bridge liveness."""
-import datetime
-import io
-import os
-import re
-import subprocess
-
-ACCOUNT = "66639661"
-
-
-def esc(s):
- return str(s).encode("unicode_escape").decode("ascii")
-
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-logdir = os.path.join(root, "userdata", "log")
-
-print("=== today's logs (size/mtime) ===")
-for f in sorted(os.listdir(logdir)):
- if "20260909" in f:
- p = os.path.join(logdir, f)
- st = os.stat(p)
- print(" %-46s %8d mtime=%s" % (
- f, st.st_size,
- datetime.datetime.fromtimestamp(st.st_mtime).strftime("%H:%M:%S")))
-
-kws = ("登录", "登陆", "login", "Login", "自动", "运行", "模型", "策略",
- "启动", "恢复", "auto", "Auto", "Formula", "自启")
-f2 = os.path.join(logdir, "XtClient_20260909.log")
-print("=== XtClient key windows (08:5x / 11:xx / 12:0x) ===")
-try:
- with io.open(f2, "r", encoding="gbk", errors="replace") as f:
- for ln in f:
- hhmm = ln[:5]
- if any(k in ln for k in kws) and (
- hhmm.startswith("08:5") or hhmm.startswith("11:")
- or hhmm.startswith("12:0")):
- print(" ", esc(ln.rstrip())[:230])
-except OSError as e:
- print(" ", e)
-
-f1 = os.path.join(logdir, "XtClient_Formula_20260909.log")
-print("=== Formula head 12 ===")
-try:
- lines = io.open(f1, "r", encoding="gbk", errors="replace").readlines()
- for ln in lines[:12]:
- print(" ", esc(ln.rstrip())[:200])
- print("=== Formula tail 25 (of %d) ===" % len(lines))
- for ln in lines[-25:]:
- print(" ", esc(ln.rstrip())[:200])
-except OSError as e:
- print(" ", e)
-
-print("=== processes ===")
-raw = subprocess.run(["tasklist"], capture_output=True).stdout
-for ln in raw.decode("gbk", errors="replace").splitlines():
- if any(x in ln for x in ("Xt", "ython", "redis-server", "Redis")):
- print(" ", esc(ln)[:130])
-
-print("=== bridge liveness (read-only RPC) ===")
-try:
- import redis
- 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,
- decode_responses=True)
- q = "bigqmt:rpc:queue:%s" % ACCOUNT
- print(" queue llen:", rc.llen(q))
- from bigqmt_signal_trader.redis_rpc import call_redis_rpc
- r = call_redis_rpc(rc, ACCOUNT, "query_stock_orders",
- {"account_id": ACCOUNT}, timeout_seconds=10)
- print(" RPC:", esc(str(r))[:400])
-except Exception as e:
- print(" RPC ERR:", esc(repr(e))[:300])
-print("DONE4")
diff --git a/scripts/qmt_relogin/qmt_xmldiff.py b/scripts/qmt_relogin/qmt_xmldiff.py
deleted file mode 100644
index 448545e4..00000000
--- a/scripts/qmt_relogin/qmt_xmldiff.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Read-only round 3: startupAutorun / modbootstart values across daily baselines."""
-import os
-import re
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-
-
-def esc(s):
- return s.encode("unicode_escape").decode("ascii")
-
-
-files = {
- "now": os.path.join(root, "config", "indexUserConfig.xml"),
- "snap_before": r"C:\sanguo_bigqmt\indexUserConfig_before.xml",
- "d0907": os.path.join(root, "config", "Temp", "indexUserConfig.xml_20260907"),
- "d0908": os.path.join(root, "config", "Temp", "indexUserConfig.xml_20260908"),
- "d0909": os.path.join(root, "config", "Temp", "indexUserConfig.xml_20260909"),
-}
-for tag, p in files.items():
- t = open(p, encoding="utf-8", errors="replace").read()
- sa = re.findall(r'startupAutorun="([^"]*)"', t)
- # instance element presence: count id attrs near FromulaExpandData
- fe = t.count("FromulaExpandData")
- # find modbootstart-ish global in same file too
- mb = re.findall(r'modbootstart="([^"]*)"', t)
- # restarttimelist
- rt = re.findall(r'restarttimelist="([^"]*)"', t)
- print("%-11s size=%d startupAutorun=%s FromulaExpandData=%d modbootstart=%s restarttimelist=%s"
- % (tag, len(t.encode('utf-8')), sa, fe, mb, [esc(x) for x in rt]))
-
-# Config.xml (user) modbootstart + restart time attrs
-p2 = os.path.join(root, "userdata", "users", "66639661", "Config.xml")
-t2 = open(p2, encoding="utf-8", errors="replace").read()
-for kw in ["modbootstart", "restarttime", "restarttimelist", "TradeSetting"]:
- for m in re.finditer(re.escape(kw), t2):
- a = max(0, m.start() - 80)
- b = min(len(t2), m.end() + 250)
- print("CFG", esc(t2[a:b]).replace("\\n", " ")[:400])
- print("---")
-print("DONE3")
diff --git a/scripts/qmt_relogin/qmt_xmldump.py b/scripts/qmt_relogin/qmt_xmldump.py
deleted file mode 100644
index 23d7f2f6..00000000
--- a/scripts/qmt_relogin/qmt_xmldump.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Read-only: dump instance entry from indexUserConfig.xml + autorun-ish attrs
-from userdata Config.xml. All output unicode-escaped (console is GBK)."""
-import os
-import re
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-
-
-def esc(s):
- return s.encode("unicode_escape").decode("ascii")
-
-
-# --- 1) indexUserConfig.xml: element(s) containing the instance name ---
-p1 = os.path.join(root, "config", "indexUserConfig.xml")
-t1 = open(p1, encoding="utf-8", errors="replace").read()
-name = "新建策略文件3"
-for m in re.finditer(re.escape(name), t1):
- a = max(0, m.start() - 1500)
- b = min(len(t1), m.end() + 1500)
- print("=== indexUserConfig ctx ===")
- print(esc(t1[a:b]))
- print("=== end ===")
- break # first occurrence with wide context
-
-# also list all attribute names seen in file for flag hunting
-attrs = sorted(set(re.findall(r'([A-Za-z][A-Za-z0-9_]{2,30})\s*=', t1)))
-print("ATTRS", esc(",".join(attrs)))
-
-# --- 2) userdata Config.xml: every line with boot/autorun/mod flags ---
-p2 = os.path.join(root, "userdata", "users", "66639661", "Config.xml")
-t2 = open(p2, encoding="utf-8", errors="replace").read()
-print("=== Config.xml flag lines ===")
-pat = re.compile(r"(modbootstart|autorun|auto_run|bootstart|selfstart|autostart)", re.I)
-for m in pat.finditer(t2):
- a = max(0, m.start() - 300)
- b = min(len(t2), m.end() + 300)
- print(esc(t2[a:b]))
- print("---")
-print("DONE")
diff --git a/scripts/qmt_relogin/qmt_xmldump2.py b/scripts/qmt_relogin/qmt_xmldump2.py
deleted file mode 100644
index 460f35a1..00000000
--- a/scripts/qmt_relogin/qmt_xmldump2.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Read-only round 2: locate the INSTANCE element (startupAutorun) precisely."""
-import os
-import re
-
-d = open(r"C:\sanguo_bigqmt\qmt_install_dir.txt", encoding="utf-8").read().strip()
-root = os.path.dirname(d)
-
-
-def esc(s):
- return s.encode("unicode_escape").decode("ascii")
-
-
-p1 = os.path.join(root, "config", "indexUserConfig.xml")
-t1 = open(p1, encoding="utf-8", errors="replace").read()
-
-print("### startupAutorun occurrences:", t1.count("startupAutorun"))
-for m in re.finditer("startupAutorun", t1):
- a = max(0, m.start() - 900)
- b = min(len(t1), m.end() + 400)
- print("=== SA ctx ===")
- print(esc(t1[a:b]))
- print("=== end ===")
-
-print("### name occurrences:", t1.count("新建策略文件3"))
-for i, m in enumerate(re.finditer("新建策略文件3", t1)):
- a = max(0, m.start() - 200)
- b = min(len(t1), m.end() + 200)
- print("--- occ%d pos%d ---" % (i, m.start()))
- print(esc(t1[a:b]))
-
-# Config.xml modbootstart compact
-p2 = os.path.join(root, "userdata", "users", "66639661", "Config.xml")
-t2 = open(p2, encoding="utf-8", errors="replace").read()
-print("### modbootstart occurrences:", t2.lower().count("modbootstart"))
-for m in re.finditer("modbootstart", t2, re.I):
- a = max(0, m.start() - 150)
- b = min(len(t2), m.end() + 150)
- print(esc(t2[a:b]))
-print("DONE2")
diff --git a/scripts/qmt_relogin/relogin_test.ps1 b/scripts/qmt_relogin/relogin_test.ps1
deleted file mode 100644
index b7f52a13..00000000
--- a/scripts/qmt_relogin/relogin_test.ps1
+++ /dev/null
@@ -1 +0,0 @@
-C:\Python310\python.exe C:\sanguo_bigqmt\qmt_relogin_test.py > C:\sanguo_bigqmt\relogin_test.log 2>&1
diff --git a/scripts/qmt_relogin/shot.ps1 b/scripts/qmt_relogin/shot.ps1
deleted file mode 100644
index b258554a..00000000
--- a/scripts/qmt_relogin/shot.ps1
+++ /dev/null
@@ -1 +0,0 @@
-& C:\Python310\python.exe -c "import pyautogui; im = pyautogui.screenshot(); im.save(r'C:\sanguo_bigqmt\shot.png'); print('SHOT_OK')"
diff --git a/scripts/qmt_relogin/shot_click.ps1 b/scripts/qmt_relogin/shot_click.ps1
deleted file mode 100644
index 8a1cf357..00000000
--- a/scripts/qmt_relogin/shot_click.ps1
+++ /dev/null
@@ -1 +0,0 @@
-C:\Python310\python.exe C:\sanguo_bigqmt\qmt_click_pystg.py
diff --git a/scripts/qmt_relogin/shot_pure.ps1 b/scripts/qmt_relogin/shot_pure.ps1
deleted file mode 100644
index 77188ead..00000000
--- a/scripts/qmt_relogin/shot_pure.ps1
+++ /dev/null
@@ -1 +0,0 @@
-C:\Python310\python.exe -c "import pyautogui; pyautogui.FAILSAFE=False; pyautogui.screenshot(r'C:\sanguo_bigqmt\shot.png')"
diff --git a/scripts/qmt_relogin/shot_task.xml b/scripts/qmt_relogin/shot_task.xml
deleted file mode 100644
index 4132d23a..00000000
--- a/scripts/qmt_relogin/shot_task.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
- One-shot console session screenshot for ops diagnosis
-
-
-
- 2026-09-09T23:59:00
- true
-
-
-
- false
- false
- PT5M
- IgnoreNew
- true
-
-
-
- Administrator
- InteractiveToken
- LeastPrivilege
-
-
-
-
- powershell.exe
- -NoProfile -ExecutionPolicy Bypass -File C:\sanguo_bigqmt\shot.ps1
-
-
-
diff --git a/scripts/qmt_relogin/ui_act.py b/scripts/qmt_relogin/ui_act.py
deleted file mode 100644
index dd94c3cb..00000000
--- a/scripts/qmt_relogin/ui_act.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Execute UI actions from ui_actions.json (list of ops), screenshot after each
-marked step. Runs in console session via schtask. ASCII output only.
-
-Ops: click(x,y,clicks,button) move(x,y) key(k) hotkey(keys[]) scroll(dy,x,y)
- wait(sec) shot(tag)
-"""
-import json
-import time
-
-import pyautogui
-
-pyautogui.FAILSAFE = True
-PLAN = r"C:\sanguo_bigqmt\ui_actions.json"
-SHOT = r"C:\sanguo_bigqmt\shot_%02d.png"
-
-plan = json.load(open(PLAN, encoding="utf-8")) # utf-8 regardless of GBK console
-n = 0
-
-
-def shot(tag=""):
- global n
- n += 1
- pyautogui.screenshot().save(SHOT % n)
- print("SHOT_%02d %s" % (n, tag))
-
-
-for a in plan:
- op = a["op"]
- if op == "closewin":
- import ctypes
- user32 = ctypes.windll.user32
- WM_CLOSE = 0x0010
- if a.get("hwnd"):
- user32.PostMessageW(a["hwnd"], WM_CLOSE, 0, 0)
- print("CLOSEWIN hwnd=%s" % a["hwnd"])
- else:
- Ws2 = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
- hits = []
-
- @Ws2
- def cb2(h2, _):
- buf = ctypes.create_unicode_buffer(256)
- user32.GetWindowTextW(h2, buf, 256)
- if a["contains"] in buf.value:
- hits.append(h2)
- return True
- user32.EnumWindows(cb2, 0)
- for h2 in hits:
- user32.PostMessageW(h2, WM_CLOSE, 0, 0)
- print("CLOSEWIN %s hwnd=%s" % (a["contains"], h2))
- if op == "exec":
- import subprocess
- r = subprocess.run([r"C:\Python310\python.exe", a["script"]],
- capture_output=True, text=True, timeout=60)
- for ln in (r.stdout or "").splitlines():
- print("X| " + ln)
- for ln in (r.stderr or "").splitlines()[:10]:
- print("E| " + ln)
- if op == "activate":
- import ctypes
- user32 = ctypes.windll.user32
- title = a.get("title")
- h = user32.FindWindowW(None, title) if title else 0
- if not h and a.get("contains"):
- Ws = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
- found = []
-
- @Ws
- def cb(h2, _):
- buf = ctypes.create_unicode_buffer(256)
- user32.GetWindowTextW(h2, buf, 256)
- if a["contains"] in buf.value and user32.IsWindowVisible(h2):
- found.append(h2)
- return True
- user32.EnumWindows(cb, 0)
- h = found[0] if found else 0
- if h:
- user32.ShowWindow(h, 9) # SW_RESTORE
- user32.SetForegroundWindow(h)
- time.sleep(0.8)
- print("ACTIVATE ok hwnd=%s title=%s" % (h, a.get("title") or a.get("contains")))
- else:
- print("ACTIVATE_FAIL not_found")
- if op == "launch":
- import subprocess
- cwd = a.get("cwd") or None
- subprocess.Popen([a["exe"]] + list(a.get("args", [])), cwd=cwd)
- print("LAUNCH %s %s" % (a["exe"].encode("unicode_escape").decode("ascii"),
- " ".join(a.get("args", []))))
- if op == "minimize":
- import ctypes
- user32 = ctypes.windll.user32
- if a.get("hwnd"):
- user32.ShowWindow(a["hwnd"], 6) # SW_MINIMIZE
- print("MINIMIZE hwnd=%s" % a["hwnd"])
- elif a.get("contains"):
- Ws3 = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
- hits = []
-
- @Ws3
- def cb3(h2, _):
- buf = ctypes.create_unicode_buffer(256)
- user32.GetWindowTextW(h2, buf, 256)
- if a["contains"] in buf.value and user32.IsWindowVisible(h2):
- hits.append(h2)
- return True
- user32.EnumWindows(cb3, 0)
- for h2 in hits[:1]:
- user32.ShowWindow(h2, 6)
- print("MINIMIZE %s hwnd=%s" % (a["contains"], h2))
- if op == "credtype":
- # decrypt DPAPI secret in-process and type it into field (x,y).
- # never printed, never written to disk.
- import base64
- import ctypes
- class DATA_BLOB(ctypes.Structure):
- _fields_ = [("cbData", ctypes.c_ulong),
- ("pbData", ctypes.POINTER(ctypes.c_char))]
- enc_hex = open(r"C:\sanguo_bigqmt\creds\qmt_login.enc",
- encoding="ascii").read().strip()
- raw = bytes.fromhex(enc_hex)
- bin_ = DATA_BLOB(len(raw), ctypes.cast(
- ctypes.create_string_buffer(raw, len(raw)),
- ctypes.POINTER(ctypes.c_char)))
- bout = DATA_BLOB()
- ok = ctypes.windll.crypt32.CryptUnprotectData(
- ctypes.byref(bin_), None, None, None, None, 0, ctypes.byref(bout))
- if not ok:
- print("CREDTYPE decrypt_failed")
- else:
- plain = ctypes.string_at(bout.pbData, bout.cbData).decode(
- "utf-16-le", errors="replace").rstrip("\x00")
- # cred json: {"user": "...", "password": "..."}
- import json as _j
- pw = _j.loads(plain)["password"]
- pyautogui.click(a["x"], a["y"])
- time.sleep(0.4)
- pyautogui.typewrite(pw, interval=0.06)
- print("CREDTYPE typed len=%d" % len(pw))
- ctypes.windll.kernel32.LocalFree(bout.pbData)
- if op == "text":
- # type arbitrary (ASCII) text at x,y — used for captcha fields
- pyautogui.click(a["x"], a["y"])
- time.sleep(0.3)
- pyautogui.typewrite(a["s"], interval=0.08)
- print("TEXT typed len=%d" % len(a["s"]))
- if op == "info":
- im = pyautogui.screenshot()
- print("SIZE_cursor=%s SIZE_bitmap=%s" % (pyautogui.size(), im.size))
- if op == "shot":
- shot(a.get("tag", ""))
- elif op == "region":
- n += 1
- im = pyautogui.screenshot()
- im.crop((a["x"], a["y"], a["x"] + a["w"], a["y"] + a["h"])).save(
- SHOT % n)
- print("REGION_%02d %s" % (n, a.get("tag", "")))
- elif op == "zoom":
- n += 1
- s = a.get("scale", 3)
- im = pyautogui.screenshot().crop(
- (a["x"], a["y"], a["x"] + a["w"], a["y"] + a["h"]))
- im = im.resize((im.width * s, im.height * s))
- im.save(SHOT % n)
- print("ZOOM_%02d %s" % (n, a.get("tag", "")))
- elif op == "click":
- pyautogui.click(a["x"], a["y"], clicks=a.get("clicks", 1),
- interval=0.35, button=a.get("button", "left"))
- print("CLICK %s %s x%s" % (a["x"], a["y"], a.get("clicks", 1)))
- elif op == "move":
- pyautogui.moveTo(a["x"], a["y"], duration=0.25)
- print("MOVE %s %s" % (a["x"], a["y"]))
- elif op == "key":
- pyautogui.press(a["key"])
- print("KEY %s" % a["key"])
- elif op == "hotkey":
- pyautogui.hotkey(*a["keys"])
- print("HOTKEY %s" % "+".join(a["keys"]))
- elif op == "scroll":
- pyautogui.scroll(a.get("dy", -300), x=a.get("x"), y=a.get("y"))
- print("SCROLL %s" % a.get("dy", -300))
- elif op == "wait":
- time.sleep(a["sec"])
- print("WAIT %s" % a["sec"])
- time.sleep(a.get("after", 0.6))
-
-shot("end")
-print("UI_ACT_DONE nshots=%d" % n)
diff --git a/scripts/qmt_relogin/ui_task.xml b/scripts/qmt_relogin/ui_task.xml
deleted file mode 100644
index 571dccee..00000000
--- a/scripts/qmt_relogin/ui_task.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
- Console-session UI action runner for QMT ops
-
-
-
- 2026-09-09T23:59:00
- true
-
-
-
- false
- false
- PT5M
- IgnoreNew
- true
-
-
-
- Administrator
- InteractiveToken
- LeastPrivilege
-
-
-
-
- powershell.exe
- -NoProfile -ExecutionPolicy Bypass -File C:\sanguo_bigqmt\ui_act.ps1
-
-
-
diff --git a/scripts/reanchor_value.py b/scripts/reanchor_value.py
deleted file mode 100644
index e41fccb0..00000000
--- a/scripts/reanchor_value.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# -*- coding: utf-8 -*-
-"""value 双轨重锚工序(2026-09-11 夜,用户拍板A)。
-stop(live_21+shadow_62) -> 等停死 -> 备份+删 live_state.json 的 strategy_start_date -> start。
-安全闸: 引擎未停死拒绝改档。用法: python -X utf8 C:\\sanguo_bigqmt\\reanchor_value.py [edit-only]
-"""
-from __future__ import annotations
-
-import datetime
-import json
-import shutil
-import sqlite3
-import sys
-import time
-
-DB = r"C:\sanguo_vnpy_v2\data\backtest_results.db"
-RUNTIMES = [r"C:\sanguo_vnpy_v2\runtime\live_21\live_state.json",
- r"C:\sanguo_vnpy_v2\runtime\shadow_62\live_state.json"]
-
-
-def _children_alive():
- """返回 (live_21存活, shadow_62存活)。"""
- import psutil
- live21 = shadow62 = False
- for p in psutil.process_iter(["pid", "name", "cmdline"]):
- if "python" not in (p.info["name"] or "").lower():
- continue
- cmd = p.info["cmdline"] or []
- joined = " ".join(cmd)
- if "runner_live" in joined and "21" in cmd:
- live21 = True
- if "sanguo_trader.shadow" in joined and "62" in cmd and "--auto" not in joined:
- shadow62 = True
- return live21, shadow62
-
-
-def _set_status(value):
- with sqlite3.connect(DB, timeout=15) as c:
- c.execute("UPDATE live_accounts SET status=? WHERE id=21", (value,))
- c.execute("UPDATE paper_accounts SET status=? WHERE id=62 "
- "AND mode='shadow' AND strategy_type='portfolio'", (value,))
- c.commit()
- print("STATUS ->", value)
-
-
-def main():
- edit_only = len(sys.argv) > 1 and sys.argv[1] == "edit-only"
- if not edit_only:
- # 1. stop
- _set_status("stopped")
- t0 = time.time()
- while time.time() - t0 < 150:
- l, s = _children_alive()
- if not l and not s:
- print("HARVEST_DONE %.0fs" % (time.time() - t0))
- break
- time.sleep(10)
- else:
- print("HARVEST_TIMEOUT live21=%s shadow62=%s" % _children_alive())
- raise SystemExit(1)
- # 2. 安全闸 + 备份 + 删字段
- l, s = _children_alive()
- if l or s:
- print("SAFETY_ABORT engines still alive", l, s)
- raise SystemExit(1)
- stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
- for path in RUNTIMES:
- bak = path + ".bak_reanchor_" + stamp
- shutil.copy2(path, bak)
- st = json.load(open(path, encoding="utf-8"))
- strat = st.get("strategy") or {}
- old = strat.pop("strategy_start_date", None)
- st["strategy"] = strat
- json.dump(st, open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
- print("EDIT", path, "old_start=", old, "bak=", bak.split("\\")[-1])
- if edit_only:
- return
- # 3. start
- _set_status("running")
- print("STARTED — 等 boot 后读新锚(post 锚=首个交易日事件日)")
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/smoke_e2e.py b/scripts/smoke_e2e.py
deleted file mode 100644
index dc5f598c..00000000
--- a/scripts/smoke_e2e.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""端到端冒烟:验证 read_db_daily 能读 NAS quant_trading.db(真实数据)。
-
-在临时容器内跑(docker run --rm,挂载 /volume1/stock + /app)。
-探测 vnpy 4.4.0 database 配置 + 验证数据读取。
-"""
-import sys
-sys.path.insert(0, "/app")
-
-print("=== Step 1: 探测 vnpy 4.4.0 database 配置 ===")
-from vnpy.trader.setting import SETTINGS
-db_keys = {k: v for k, v in SETTINGS.items() if "database" in k.lower() or "sql" in k.lower()}
-print("database 相关 SETTINGS:", db_keys)
-
-SETTINGS["database.driver"] = "sqlite"
-SETTINGS["database.database"] = "/volume1/stock/sanguo_vnpy/data/quant_trading.db"
-print(f"已设置: driver=sqlite, database={SETTINGS['database.database']}")
-
-print("\n=== Step 2: 加载 sanguo_data 配置 ===")
-from sanguo_data.config import load_config
-cfg = load_config("config/data_platform.yaml")
-print(f"cfg vnpy_db: {cfg.data_paths.get('vnpy_db')}")
-
-print("\n=== Step 3: read_db_daily 读 600000 ===")
-try:
- from sanguo_data.datareader import read_db_daily
- bars = read_db_daily("600000", "2026-01-01", "2026-06-30", cfg)
- print(f"RESULT: 读取 {len(bars)} 条 BarData")
- if bars:
- b = bars[0]
- print(f"FIRST: symbol={b.symbol} dt={b.datetime} close={b.close_price} vol={b.volume}")
- else:
- print("WARN: 0 条——可能 vnpy database 路径配置不对,或数据不在该 symbol/区间")
-except Exception as e:
- import traceback
- traceback.print_exc()
- print(f"FAIL: {type(e).__name__}: {e}")
-
-print("\n=== Step 4: 直接探测 NAS db 文件 ===")
-import os
-db_path = "/volume1/stock/sanguo_vnpy/data/quant_trading.db"
-print(f"{db_path} exists: {os.path.exists(db_path)}, size: {os.path.getsize(db_path) if os.path.exists(db_path) else 'N/A'}")
diff --git a/scripts/smoke_phase2.py b/scripts/smoke_phase2.py
deleted file mode 100644
index c05573a9..00000000
--- a/scripts/smoke_phase2.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""Phase 2 端到端冒烟:result_store + 因子 registry + API + data_adapter(真实数据)。"""
-import sys
-sys.path.insert(0, "/app")
-sys.path.insert(0, "/app/vnpy_v4.4.0")
-
-
-def main():
- print("=== Phase 2 E2E Smoke ===")
- import pandas as pd
-
- # 1. result_store 增删查
- from sanguo_backtest.result_store import save_result, load_result, BacktestResult
- r = BacktestResult("smoke", "cta", "done", "Test", "600000", {"n": 5},
- "2024-01-01", "2024-06-30", {"return": 0.1},
- pd.DataFrame({"e": [1.0]}), pd.DataFrame(), None)
- rid = save_result(r, db_path="/tmp/smoke.db", file_dir="/tmp")
- print(f" result_store: id={rid}, strategy={load_result(rid, '/tmp/smoke.db').strategy}")
-
- # 2. 因子 registry
- import sanguo_factor.library
- from sanguo_factor.registry import list_factors
- print(f" factors: {len(list_factors())} total, {len(list_factors('builtin'))} builtin")
-
- # 3. API 启动
- from sanguo_api.app import create_app
- app = create_app(db_path="/tmp/smoke.db", file_dir="/tmp")
- routes = [r.path for r in app.routes if hasattr(r, "path")]
- print(f" API routes: {len(routes)} loaded")
-
- # 4. data_adapter(真实 A 股数据)
- from sanguo_data.datareader import read_db_daily
- from sanguo_data.config import load_config
- from sanguo_factor.data_adapter import convert_bars_to_alpha_df
- cfg = load_config("/app/config/data_platform.yaml")
- bars = read_db_daily("600000", "2024-01-01", "2024-06-30", cfg)
- df = convert_bars_to_alpha_df(bars)
- print(f" data_adapter: {len(bars)} bars -> df height={df.height}, cols={df.columns[:5]}...")
-
- print("=== Phase 2 Smoke DONE ===")
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/smoke_phase3a.py b/scripts/smoke_phase3a.py
deleted file mode 100644
index e312d720..00000000
--- a/scripts/smoke_phase3a.py
+++ /dev/null
@@ -1,344 +0,0 @@
-"""Phase 3a 端到端冒烟:异步 submit + WS 阶段 + JWT + tears(容器)。"""
-import sys
-import os
-import asyncio
-import tempfile
-import shutil
-
-# Set up paths for both local and container environments
-if os.path.exists("/app"):
- sys.path.insert(0, "/app")
- sys.path.insert(0, "/app/vnpy_v4.4.0")
- print("=== Using container paths ===")
-else:
- # Local development environment
- repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- sys.path.insert(0, repo_root)
- vnpy_path = os.path.join(repo_root, "vnpy_v4.4.0")
- if vnpy_path not in sys.path:
- sys.path.insert(0, vnpy_path)
- print(f"=== Using local paths: {repo_root} ===")
-
-
-def test_sys_path():
- """Test sys.path configuration"""
- print("=== SYS.PATH CONFIG ===")
- for i, p in enumerate(sys.path[:3]):
- print(f" path[{i}]: {p}")
- print("=== PASS: sys.path configured ===")
- print()
-
-
-def test_jwt_login():
- """Test JWT login endpoint"""
- print("=== JWT LOGIN TEST ===")
- try:
- from fastapi.testclient import TestClient
- from sanguo_api.app import create_app
- from sanguo_api.auth import hash_password
-
- # Create temporary directory for test
- tmpdir = tempfile.mkdtemp()
- db_path = os.path.join(tmpdir, "test.db")
- file_dir = tmpdir
-
- try:
- # Generate real password hash using bcrypt directly
- test_password = "password123"
- password_hash = hash_password(test_password)
-
- # Create app with auth config
- app = create_app(
- db_path=db_path,
- file_dir=file_dir,
- auth_config={
- "username": "admin",
- "password_hash": password_hash,
- "jwt_secret": "test-secret",
- "expire_minutes": 60
- },
- max_workers=1
- )
-
- # Test login with TestClient - REAL password verification
- client = TestClient(app)
- response = client.post("/api/v1/auth/login", json={
- "username": "admin",
- "password": test_password
- })
-
- assert response.status_code == 200, f"Expected 200, got {response.status_code}"
- data = response.json()
- assert "token" in data, "Token not in response"
- print(f" Login successful, token: {data['token'][:20]}...")
- print("=== PASS: JWT login ===")
- return data['token']
- finally:
- shutil.rmtree(tmpdir, ignore_errors=True)
- except Exception as e:
- print(f" === FAIL: JWT login test failed: {type(e).__name__}: {e} ===")
- import traceback
- traceback.print_exc()
- print("=== FAIL: JWT login ===")
- return None
-
-
-def test_protected_route_auth():
- """Test protected route authentication"""
- print("=== PROTECTED ROUTE AUTH TEST ===")
- try:
- from fastapi.testclient import TestClient
- from sanguo_api.app import create_app
- from sanguo_api.auth import hash_password
-
- tmpdir = tempfile.mkdtemp()
- db_path = os.path.join(tmpdir, "test.db")
- file_dir = tmpdir
-
- try:
- # Generate real password hash using bcrypt directly
- test_password = "password123"
- password_hash = hash_password(test_password)
-
- app = create_app(
- db_path=db_path,
- file_dir=file_dir,
- auth_config={
- "username": "admin",
- "password_hash": password_hash,
- "jwt_secret": "test-secret",
- "expire_minutes": 60
- },
- max_workers=1
- )
-
- client = TestClient(app)
-
- # Test without token - should get 401
- response = client.get("/api/v1/task/foo")
- assert response.status_code == 401, f"Expected 401 without token, got {response.status_code}"
- print(" No token: 401 Unauthorized ✓")
-
- # Get valid token using REAL password verification
- login_response = client.post("/api/v1/auth/login", json={
- "username": "admin",
- "password": test_password
- })
- token = login_response.json()["token"]
-
- # Test with token but non-existent task - should get 404
- response = client.get(
- "/api/v1/task/foo",
- headers={"Authorization": f"Bearer {token}"}
- )
- assert response.status_code == 404, f"Expected 404 with valid token, got {response.status_code}"
- print(" Valid token, non-existent task: 404 Not Found ✓")
- print("=== PASS: protected route auth ===")
- finally:
- shutil.rmtree(tmpdir, ignore_errors=True)
- except Exception as e:
- print(f" === FAIL: Protected route auth test failed: {type(e).__name__}: {e} ===")
- import traceback
- traceback.print_exc()
- print("=== FAIL: protected route auth ===")
-
-
-async def test_orchestrator_async():
- """Test orchestrator async submission"""
- print("=== ORCHESTRATOR ASYNC TEST ===")
- from sanguo_orchestrator.runner import Orchestrator
-
- tmpdir = tempfile.mkdtemp()
- db_path = os.path.join(tmpdir, "test.db")
-
- try:
- orch = Orchestrator(db_path=db_path, file_dir=tmpdir, max_workers=1)
-
- # Check executor type
- from concurrent.futures import ProcessPoolExecutor
- executor_type = type(orch.pool.executor).__name__
- print(f" Orchestrator pool executor: {executor_type}")
- assert executor_type == "ProcessPoolExecutor", f"Expected ProcessPoolExecutor, got {executor_type}"
-
- # Submit a tiny factor task
- task_id = await orch.submit_factor(
- symbols=["600000.SSE"],
- factor_names=["ma5"],
- start="2024-01-01",
- end="2024-06-30",
- cfg=None, # Use default config for smoke test
- output_dir=tmpdir
- )
-
- print(f" Task submitted: {task_id}")
- assert task_id.startswith("factor_"), f"Expected task_id to start with 'factor_', got {task_id}"
-
- # Check task status
- status = orch.get_status(task_id)
- print(f" Task status: {status}")
-
- print("=== PASS: orchestrator async ===")
- return task_id
- finally:
- shutil.rmtree(tmpdir, ignore_errors=True)
-
-
-async def test_ws_stage_wiring():
- """Test WebSocket stage callback wiring"""
- print("=== WS STAGE WIRING TEST ===")
- from sanguo_api.app import create_app
- from sanguo_api.ws import manager
- from sanguo_api.routes import get_orchestrator
- from sanguo_api.auth import hash_password
-
- tmpdir = tempfile.mkdtemp()
- db_path = os.path.join(tmpdir, "test.db")
-
- try:
- # Generate real password hash using bcrypt directly
- test_password = "password123"
- password_hash = hash_password(test_password)
-
- # Create app to trigger wiring
- app = create_app(
- db_path=db_path,
- file_dir=tmpdir,
- auth_config={
- "username": "admin",
- "password_hash": password_hash,
- "jwt_secret": "test-secret",
- "expire_minutes": 60
- },
- max_workers=1
- )
-
- # Check orchestrator has on_stage callback set
- orch = get_orchestrator()
- assert orch._on_stage is not None, "on_stage callback not set"
-
- print(" WS manager exists ✓")
- print(" on_stage callback wired ✓")
- print("=== PASS: WS stage wiring ===")
- finally:
- shutil.rmtree(tmpdir, ignore_errors=True)
-
-
-async def test_real_factor_tears_pipeline():
- """Test real factor tears pipeline with multiprocessing guard"""
- print("=== REAL FACTOR TEARS PIPELINE TEST ===")
-
- # Check if we're in container with full dependencies
- try:
- import polars
- import alphalens
- print(" Container environment detected (polars + alphalens available)")
- except ImportError as e:
- print(f" === SKIP: polars/alphalens not available ({e}) ===")
- return
-
- tmpdir = tempfile.mkdtemp()
- output_dir = os.path.join(tmpdir, "factor_output")
-
- try:
- from sanguo_factor.analyzer import run_factor_analysis
- from sanguo_data.config import load_config
-
- # Load config for database access
- if os.path.exists("/app/config/data_platform.yaml"):
- cfg = load_config("/app/config/data_platform.yaml")
- print(" Loaded database config")
- else:
- print(" === SKIP: database config not found ===")
- return
-
- # ≥2 symbols: alphalens IC is cross-sectional (1 symbol → empty bins → fails)
- symbols = ["600000", "000001", "300750"] # BARE symbols (no .EXCHANGE suffix) for DB lookup
- factor_names = ["ma5"] # Simple factor
- start = "2024-01-01"
- end = "2024-06-30" # Use longer range for reliable IC (Phase 1 confirmed 541 bars)
-
- print(f" Running factor analysis: {symbols}, {factor_names}, {start} to {end}")
- print(" This will test the multiprocessing pipeline...")
-
- # Run the analysis
- result = run_factor_analysis(
- symbols=symbols,
- factor_names=factor_names,
- start=start,
- end=end,
- cfg=cfg,
- output_dir=output_dir
- )
-
- print(f" Analysis complete: {len(result.factor_names)} factors processed")
- print(f" IC summary: {result.ic_summary}")
- print(f" Report paths: {result.report_paths}")
-
- # Verify we got a result
- assert result is not None, "Result is None"
- assert len(result.factor_names) > 0, "No factors processed"
-
- # CRITICAL: Verify IC is non-empty (Root cause C fix)
- assert result.ic_summary, "ic_summary empty - no IC computed"
- assert "ma5" in result.ic_summary, "ma5 not in ic_summary"
- assert result.ic_summary["ma5"].get("ic"), "no IC data for ma5"
- print(f" ✓ IC computed: {result.ic_summary['ma5']['ic']}")
-
- # Check if any reports were generated
- if result.report_paths:
- print(f" Tears report generated: {result.report_paths}")
- for factor_name, path in result.report_paths.items():
- if os.path.exists(path.replace('.html', '.png')): # Check for actual PNG file
- print(f" ✓ {factor_name}: {path}")
- else:
- print(f" ⚠ {factor_name}: {path} (file not found)")
-
- print("=== PASS: real factor tears pipeline ===")
- except Exception as e:
- print(f" === FAIL: real tears pipeline failed with {type(e).__name__}: {e} ===")
- import traceback
- traceback.print_exc()
- raise
- finally:
- shutil.rmtree(tmpdir, ignore_errors=True)
-
-
-async def main():
- """Run all Phase 3a smoke tests"""
- print("\n")
- print("=" * 60)
- print("PHASE 3A SMOKE TEST SUITE")
- print("=" * 60)
- print()
-
- # Test 1: sys.path configuration
- test_sys_path()
-
- # Test 2: JWT login
- test_jwt_login()
-
- # Test 3: Protected route authentication
- test_protected_route_auth()
-
- # Test 4: Orchestrator async submission
- await test_orchestrator_async()
-
- # Test 5: WebSocket stage wiring
- await test_ws_stage_wiring()
-
- # Test 6: Real factor tears pipeline (container only)
- await test_real_factor_tears_pipeline()
-
- print()
- print("=" * 60)
- print("PHASE 3A SMOKE TEST SUITE: ALL TESTS PASSED")
- print("=" * 60)
- print()
-
-
-if __name__ == "__main__":
- # CRITICAL: multiprocessing guard for vnpy.alpha
- # This is required because vnpy.alpha's AlphaDataset.prepare_data() spawns
- # a multiprocessing.Pool, which fails without this guard on spawn platforms
- print("=== Running with multiprocessing guard ===")
- asyncio.run(main())
diff --git a/scripts/smoke_phase3b.py b/scripts/smoke_phase3b.py
deleted file mode 100644
index ab9d95d6..00000000
--- a/scripts/smoke_phase3b.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env python3
-"""Phase 3b S1 end-to-end smoke.
-
-Login -> submit CTA backtest (DoubleMaStrategy on 600000) -> poll status ->
-verify the result-page endpoints (equity-curve / daily-pnl / trades / kline)
-return non-empty data.
-
-Runs from the Mac against the NAS container (http://192.168.2.154:8000).
-No third-party deps (urllib only).
-"""
-import json
-import sys
-import time
-import urllib.request
-
-BASE = "http://192.168.2.154:8000"
-
-
-def _request(method: str, path: str, token: str | None = None, body: dict | None = None) -> dict:
- headers = {"Content-Type": "application/json"}
- if token:
- headers["Authorization"] = f"Bearer {token}"
- data = json.dumps(body).encode() if body is not None else None
- req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
- with urllib.request.urlopen(req, timeout=30) as resp:
- return json.loads(resp.read())
-
-
-def main() -> int:
- tok = _request("POST", "/api/v1/auth/login", body={"username": "admin", "password": "admin"})["token"]
- print("[1] login OK")
-
- sub = _request("POST", "/api/v1/backtest/cta", token=tok, body={
- "symbol": "600000",
- "strategy": "DoubleMaStrategy",
- "params": {"fast_window": 10, "slow_window": 20, "fixed_size": 1},
- "start": "2024-01-01",
- "end": "2024-06-30",
- })
- tid = sub["task_id"]
- print(f"[2] submitted: {tid}")
-
- status = "pending"
- for i in range(60):
- s = _request("GET", f"/api/v1/task/{tid}", token=tok)
- status = s["status"]
- print(f" [{i:02d}] status={status} stage={s.get('stage', '')}")
- if status in ("done", "failed"):
- break
- time.sleep(3)
-
- if status != "done":
- print(f"[!] backtest did not complete: {status}")
- return 1
-
- eq = _request("GET", f"/api/v1/task/{tid}/equity-curve", token=tok)
- pnl = _request("GET", f"/api/v1/task/{tid}/daily-pnl", token=tok)
- tr = _request("GET", f"/api/v1/task/{tid}/trades", token=tok)
- kl = _request("GET", "/api/v1/kline?symbol=600000&start=2024-01-01&end=2024-06-30", token=tok)
-
- n_eq = len(eq.get("equity_curve", []))
- n_pnl = len(pnl.get("daily_pnl", []))
- n_tr = len(tr.get("trades", []))
- n_kl = len(kl.get("kline", []))
- print(f"[3] equity={n_eq} pnl={n_pnl} trades={n_tr} kline={n_kl}")
-
- assert n_eq > 0, "equity_curve empty"
- assert n_kl > 0, "kline empty"
- print("[4] SMOKE PASSED")
- return 0
-
-
-if __name__ == "__main__":
- try:
- sys.exit(main())
- except AssertionError as e:
- print(f"[SMOKE FAILED] {e}")
- sys.exit(2)
- except Exception as e:
- print(f"[SMOKE ERROR] {type(e).__name__}: {e}")
- sys.exit(3)
diff --git a/scripts/smoke_phase3b_factor.py b/scripts/smoke_phase3b_factor.py
deleted file mode 100644
index 2470c930..00000000
--- a/scripts/smoke_phase3b_factor.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python3
-"""Phase 3b S2 end-to-end smoke: login -> submit factor analysis (ma5,
-multi-symbol) -> poll -> verify ic-summary non-empty.
-"""
-import json
-import sys
-import time
-import urllib.request
-
-BASE = "http://192.168.2.154:8000"
-
-
-def _req(method, path, token=None, body=None):
- headers = {"Content-Type": "application/json"}
- if token:
- headers["Authorization"] = f"Bearer {token}"
- data = json.dumps(body).encode() if body is not None else None
- req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
- with urllib.request.urlopen(req, timeout=60) as resp:
- return json.loads(resp.read())
-
-
-def main() -> int:
- tok = _req("POST", "/api/v1/auth/login", body={"username": "admin", "password": "admin"})["token"]
- print("[1] login OK")
-
- fl = _req("GET", "/api/v1/factor/list", token=tok)
- print(f"[2] factors: {[f['name'] for f in fl['factors']]}")
-
- sub = _req("POST", "/api/v1/factor/analyze", token=tok, body={
- "symbols": ["600000", "000001", "300750"],
- "factor_names": ["ma5"],
- "start": "2024-01-01",
- "end": "2024-06-30",
- })
- tid = sub["task_id"]
- print(f"[3] submitted: {tid}")
-
- status = "pending"
- for i in range(60):
- s = _req("GET", f"/api/v1/task/{tid}", token=tok)
- status = s["status"]
- print(f" [{i:02d}] status={status} stage={s.get('stage', '')}")
- if status in ("done", "failed"):
- break
- time.sleep(3)
-
- if status != "done":
- print(f"[!] factor analysis did not complete: {status}")
- return 1
-
- ic = _req("GET", f"/api/v1/task/{tid}/ic-summary", token=tok)["ic_summary"]
- print(f"[4] ic_summary keys: {list(ic.keys())}")
- assert "ma5" in ic, "ma5 missing from ic_summary"
- ma5 = ic["ma5"]
- print(f" ma5 status: {ma5.get('status')}")
- print(f" ma5 ic: {json.dumps(ma5.get('ic', {}), ensure_ascii=False)[:300]}")
- assert ma5.get("ic"), "ma5 ic empty"
- print("[5] SMOKE PASSED")
- return 0
-
-
-if __name__ == "__main__":
- try:
- sys.exit(main())
- except AssertionError as e:
- print(f"[SMOKE FAILED] {e}")
- sys.exit(2)
- except Exception as e:
- print(f"[SMOKE ERROR] {type(e).__name__}: {e}")
- sys.exit(3)
diff --git a/scripts/smoke_phase3b_optimize.py b/scripts/smoke_phase3b_optimize.py
deleted file mode 100644
index 7a2be665..00000000
--- a/scripts/smoke_phase3b_optimize.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-"""Phase 3b S3 smoke: history list + parameter optimization end-to-end."""
-import json
-import sys
-import time
-import urllib.request
-
-BASE = "http://192.168.2.154:8000"
-
-
-def _req(method, path, token=None, body=None):
- headers = {"Content-Type": "application/json"}
- if token:
- headers["Authorization"] = f"Bearer {token}"
- data = json.dumps(body).encode() if body is not None else None
- req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
- with urllib.request.urlopen(req, timeout=60) as resp:
- return json.loads(resp.read())
-
-
-def main() -> int:
- tok = _req("POST", "/api/v1/auth/login", body={"username": "admin", "password": "admin"})["token"]
-
- tasks = _req("GET", "/api/v1/task", token=tok)["tasks"]
- print(f"[history] {len(tasks)} tasks; latest: {tasks[0]['task_id'] if tasks else 'none'}")
- assert isinstance(tasks, list)
-
- sub = _req("POST", "/api/v1/backtest/optimize", token=tok, body={
- "symbol": "600000",
- "strategy": "DoubleMaStrategy",
- "grid": {"fast_window": [5, 15, 5], "slow_window": [15, 25, 5]},
- "start": "2024-01-01",
- "end": "2024-06-30",
- "max_workers": 2,
- })
- tid = sub["task_id"]
- print(f"[optimize] submitted: {tid}")
-
- status = "pending"
- for i in range(60):
- s = _req("GET", f"/api/v1/task/{tid}", token=tok)
- status = s["status"]
- print(f" [{i:02d}] {status} {s.get('stage', '')}")
- if status in ("done", "failed"):
- break
- time.sleep(3)
-
- if status != "done":
- print(f"[!] optimize failed: {status}")
- return 1
-
- res = _req("GET", f"/api/v1/task/{tid}/optimization-results", token=tok)["results"]
- print(f"[results] {len(res)} param combos")
- assert len(res) > 0, "no optimization results"
- for r in res[:3]:
- print(f" params={r['params']} sharpe={r['statistics'].get('sharpe_ratio')}")
- print("SMOKE PASSED")
- return 0
-
-
-if __name__ == "__main__":
- try:
- sys.exit(main())
- except AssertionError as e:
- print(f"[FAILED] {e}"); sys.exit(2)
- except Exception as e:
- print(f"[ERROR] {type(e).__name__}: {e}"); sys.exit(3)
diff --git a/scripts/smoke_phase3c.py b/scripts/smoke_phase3c.py
deleted file mode 100644
index d663c217..00000000
--- a/scripts/smoke_phase3c.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""Phase 3c 模拟盘端到端冒烟(部署后跑)。
-
-流程:login → POST /paper/create → GET /paper/{id} → /equity → /trades → /strategies
-用法:python scripts/smoke_phase3c.py https://vnpy.mysanguo.top admin
-"""
-import json
-import sys
-import urllib.request
-
-
-def _req(method: str, base: str, path: str, token: str | None, body=None):
- url = f"{base}/api/v1{path}"
- headers = {"Content-Type": "application/json"}
- if token:
- headers["Authorization"] = f"Bearer {token}"
- data = json.dumps(body).encode() if body is not None else None
- req = urllib.request.Request(url, data=data, headers=headers, method=method)
- with urllib.request.urlopen(req, timeout=30) as resp:
- return json.loads(resp.read())
-
-
-def main() -> int:
- base = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8000"
- user = sys.argv[2] if len(sys.argv) > 2 else "admin"
- pwd = sys.argv[3] if len(sys.argv) > 3 else "admin"
-
- tok = _req("POST", base, "/auth/login", None, {"username": user, "password": pwd})["token"]
- print("✓ login")
-
- aid = _req("POST", base, "/paper/create", tok, {
- "mode": "replay", "interval": "d", "symbols": ["600000"],
- "strategies": [{"name": "DoubleMa", "symbol": "600000",
- "match_session": "next_open"}],
- "start": "2024-01-01", "end": "2024-06-30", "initial_capital": 1_000_000,
- })["account_id"]
- print(f"✓ create paper #{aid}")
-
- assert _req("GET", base, f"/paper/{aid}", tok)["id"] == aid
- print("✓ get paper")
- assert isinstance(_req("GET", base, f"/paper/{aid}/equity", tok), list)
- assert isinstance(_req("GET", base, f"/paper/{aid}/trades", tok), list)
- assert isinstance(_req("GET", base, f"/paper/{aid}/strategies", tok), list)
- print("✓ equity / trades / strategies")
- print(f"\n✅ Phase 3c 冒烟通过(account #{aid})")
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/scripts/spike_s1_alpha_ashare.py b/scripts/spike_s1_alpha_ashare.py
deleted file mode 100644
index 09c9d6a1..00000000
--- a/scripts/spike_s1_alpha_ashare.py
+++ /dev/null
@@ -1,99 +0,0 @@
-"""S1: 验证 vnpy.alpha 能否处理 A 股数据(Phase 1 真实数据 → AlphaLab → 因子计算)。
-
-验证链:
-1. AlphaLab init
-2. read_db_daily 读 A 股 bars(Phase 1)
-3. save_bar_data + load_bar_data(存取兼容性)
-4. 构造 panel df(datetime, vt_symbol, OHLCV)
-5. AlphaDataset + add_feature(ma5) + prepare_data(spawn pool 因子计算)
-6. fetch_raw(验证因子列生成)
-
-A 股特性支撑度(T+1/涨跌停/停牌)在因子层层面:alpha 模块本身不强制这些约束,
-由策略/回测层处理;因子计算只需 OHLCV,A 股数据格式无差异。
-"""
-import sys
-import os
-
-sys.path.insert(0, "/app/vnpy_v4.4.0")
-sys.path.insert(0, "/app")
-
-
-def main():
- from vnpy.alpha.lab import AlphaLab
- from vnpy.alpha.dataset import AlphaDataset, Segment
- from vnpy.trader.constant import Interval
- import polars as pl
-
- results = {}
-
- # 1. AlphaLab init
- try:
- lab = AlphaLab("/tmp/alpha_lab_spike")
- results["alphalab_init"] = "OK"
- except Exception as e:
- results["alphalab_init"] = f"FAIL: {e}"
- return _print(results)
-
- # 2. read A 股
- try:
- from sanguo_data.datareader import read_db_daily
- from sanguo_data.config import load_config
- cfg = load_config("/app/config/data_platform.yaml")
- bars = read_db_daily("600000", "2024-01-01", "2024-06-30", cfg)
- results["read_bars"] = f"OK ({len(bars)} bars)" if bars else "FAIL: 0 bars"
- if not bars:
- return _print(results)
- except Exception as e:
- import traceback; traceback.print_exc()
- results["read_bars"] = f"FAIL: {e}"
- return _print(results)
-
- # 3. save + load
- try:
- lab.save_bar_data(bars)
- vt = bars[0].vt_symbol
- loaded = lab.load_bar_data(vt, Interval.DAILY, "2024-01-01", "2024-06-30")
- results["save_load"] = f"OK (vt={vt}, loaded={len(loaded)})"
- except Exception as e:
- import traceback; traceback.print_exc()
- results["save_load"] = f"FAIL: {e}"
- return _print(results)
-
- # 4. panel df + AlphaDataset + add_feature + prepare + fetch
- try:
- df = pl.DataFrame({
- "datetime": [b.datetime.replace(tzinfo=None) for b in bars],
- "vt_symbol": [b.vt_symbol for b in bars],
- "open": [b.open_price for b in bars],
- "high": [b.high_price for b in bars],
- "low": [b.low_price for b in bars],
- "close": [b.close_price for b in bars],
- "volume": [b.volume for b in bars],
- "turnover": [b.turnover for b in bars],
- "open_interest": [b.open_interest for b in bars],
- })
- ds = AlphaDataset(df, ("2024-01-01", "2024-04-30"),
- ("2024-05-01", "2024-05-15"),
- ("2024-05-16", "2024-06-30"))
- ds.add_feature("ma5", "ts_mean(close, 5)")
- ds.prepare_data(max_workers=1)
- raw = ds.fetch_raw(Segment.TEST)
- has_ma5 = "ma5" in raw.columns
- results["factor_calc"] = f"OK (cols={raw.columns}, rows={raw.height}, ma5={has_ma5})"
- except Exception as e:
- import traceback; traceback.print_exc()
- results["factor_calc"] = f"FAIL: {e}"
-
- _print(results)
-
-
-def _print(results):
- print("=== S1 Spike Result ===")
- for k, v in results.items():
- print(f" {k}: {v}")
- ok = all("OK" in v for v in results.values())
- print(f"\nS1 VERDICT: {'PASS' if ok else 'PARTIAL/FAIL'}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/spike_s2_optimization.py b/scripts/spike_s2_optimization.py
deleted file mode 100644
index a81681a4..00000000
--- a/scripts/spike_s2_optimization.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""S2: 验证 run_optimization 容器内多进程 API + spawn context。
-
-vnpy 4.4.0 重构:run_optimization 是 BacktestingEngine 方法(非顶层函数)。
-multiprocessing 用 get_context("spawn"),与 AlphaDataset.prepare_data 同机制
-(S1 已证容器内 spawn pool 工作)。
-"""
-import sys
-import os
-sys.path.insert(0, "/app/vnpy_v4.4.0")
-
-
-def main():
- import inspect
- results = {}
-
- try:
- from vnpy_ctastrategy.backtesting import (
- BacktestingEngine, run_bf_optimization, run_ga_optimization, OptimizationSetting
- )
- results["import"] = "OK (vnpy_ctastrategy 1.4.1)"
- results["engine.run_optimization"] = str(inspect.signature(BacktestingEngine.run_optimization))
- results["run_bf_optimization"] = "OK (顶层 brute-force)"
- results["OptimizationSetting"] = "OK (可构造)"
- except Exception as e:
- import traceback; traceback.print_exc()
- results["import"] = f"FAIL: {e}"
-
- # spawn context 确认
- try:
- import vnpy.trader.optimize as opt
- src = inspect.getsource(opt)
- results["multiprocessing"] = "spawn (get_context('spawn') + ProcessPoolExecutor/Pool)"
- except Exception as e:
- results["multiprocessing"] = f"FAIL: {e}"
-
- print("=== S2 Spike Result ===")
- for k, v in results.items():
- print(f" {k}: {v}")
- ok = "OK" in results.get("import", "")
- print(f"\nS2 VERDICT: {'PASS' if ok else 'FAIL'}")
- print("max_workers 推荐: 2 (NAS braswell 2 核, spawn 开销大, S1 单因子 11s)")
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/spike_s3_peewee.py b/scripts/spike_s3_peewee.py
deleted file mode 100644
index 672f8ac1..00000000
--- a/scripts/spike_s3_peewee.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""S3: 验证 alphalens 与 vnpy_sqlite(peewee) 能否共存。
-
-vnpy.alpha dataset/template.py:13 已 `from alphalens.utils import ...`,
-若 alphalens(empyrical-reloaded 要 peewee<3.17.4) 与 vnpy_sqlite(peewee 4.1.1)
-冲突,vnpy.alpha 将无法 import —— 这是因子层方案 A 的前置阻塞项。
-"""
-import sys
-import os
-
-_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "vnpy_v4.4.0"))
-sys.path.insert(0, _VNPY_SRC)
-
-
-def main():
- results = {}
-
- # 1. alphalens 能否 import
- try:
- from alphalens.utils import get_clean_factor_and_forward_returns
- from alphalens.tears import create_full_tear_sheet
- results["alphalens_import"] = "OK"
- except Exception as e:
- results["alphalens_import"] = f"FAIL: {type(e).__name__}: {e}"
-
- # 2. vnpy_sqlite 能否 import(peewee)
- try:
- import peewee
- results["peewee_version"] = peewee.__version__
- from vnpy_sqlite.sqlite_database import SqliteDatabase
- results["vnpy_sqlite_import"] = "OK"
- except Exception as e:
- results["vnpy_sqlite_import"] = f"FAIL: {type(e).__name__}: {e}"
-
- # 3. alpha dataset 能否 import(同时拉 alphalens + vnpy)
- try:
- from vnpy.alpha.dataset import AlphaDataset
- results["alpha_dataset_import"] = "OK"
- except Exception as e:
- results["alpha_dataset_import"] = f"FAIL: {type(e).__name__}: {e}"
-
- print("=== S3 Spike Result ===")
- for k, v in results.items():
- print(f" {k}: {v}")
-
- ok = all(v == "OK" or v.startswith("4") for v in results.values())
- print(f"\nS3 VERDICT: {'PASS' if ok else 'FAIL/CONFLICT'}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/verify_dual_source.py b/scripts/verify_dual_source.py
deleted file mode 100644
index d87fd81f..00000000
--- a/scripts/verify_dual_source.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python3
-"""双源(raw/qfq)部署验证:容器内读 NAS data_platform.yaml 配置的双源,
-确认 fetch_day(C-S3 实走接口)+ iter_bars 在 raw_dir/qfq_dir 正确路由,
-且除权日 raw 有缺口 / qfq 平滑(分红除权准确)。
-
-用法(容器):
- docker run --rm -v /volume1/stock:/volume1/stock \
- -v /volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2:/app \
- --entrypoint sh sanguo_vnpy_v2:with-sqlite \
- -c "cd /app && python scripts/verify_dual_source.py"
-"""
-import sys
-sys.path.insert(0, "/app")
-from sanguo_data.config import load_config, find_config_path
-from sanguo_trader.data_source import fetch_day, iter_bars
-
-cfg = load_config(find_config_path())
-print(f"config: {find_config_path()}")
-print(f"data_paths: raw_dir={cfg.data_paths.get('raw_dir')} qfq_dir={cfg.data_paths.get('qfq_dir')}")
-
-OK = True
-
-# 1) fetch_day(C-S3 实走接口)除权日双源
-print("\n=== fetch_day 除权日双源(浦发 2022-07-21 除权)===")
-res = {}
-for adj in ["qfq", "raw"]:
- b = fetch_day("600000", "2022-07-21", "d", adj, cfg)
- bp = fetch_day("600000", "2022-07-20", "d", adj, cfg)
- if b is None or bp is None:
- print(f" ❌ {adj}: fetch_day 返回 None(数据缺失或路由失败)")
- OK = False
- continue
- drop = ((b.close_price / bp.close_price) - 1) * 100
- res[adj] = drop
- print(f" {adj}: 07-20={bp.close_price:.4f} → 07-21={b.close_price:.4f} ({drop:+.1f}%)")
-
-# 除权日 raw 应有明显缺口(< -3%),qfq 平滑(> -3%)
-if "raw" in res and "qfq" in res:
- if res["raw"] < -3 and res["qfq"] > -3:
- print(" ✅ 除权处理正确:raw 含缺口(撮合真实价)/ qfq 平滑(信号准)")
- else:
- print(f" ⚠️ 除权差异不明显 raw={res['raw']:.1f}% qfq={res['qfq']:.1f}%")
-
-# 2) iter_bars cross-section(symbols=list)
-print("\n=== iter_bars cross-section(4 天)===")
-for adj in ["qfq", "raw"]:
- days = list(iter_bars(["600000"], "2022-07-19", "2022-07-22", "d", adj, cfg))
- if not days:
- print(f" ❌ {adj}: iter_bars 0 days")
- OK = False
- else:
- last = days[-1]
- print(f" {adj}: {len(days)} days, 末日={last[0]} close={last[1]['600000'].close_price:.4f}")
-
-# 3) 全市场可读性抽检(5 只)
-print("\n=== 全市场抽检(5 只 fetch_day)===")
-for sym in ["600000", "000001", "000858", "300750", "688981"]:
- b = fetch_day(sym, "2026-07-07", "d", "qfq", cfg)
- print(f" {sym}: {'✅ close='+format(b.close_price,'.4f') if b else '❌ None'}")
- if b is None:
- OK = False
-
-print("\n" + ("✅ 双源部署验证通过" if OK else "❌ 双源验证有失败项"))
-sys.exit(0 if OK else 1)
diff --git a/test_real_tears.py b/test_real_tears.py
deleted file mode 100644
index e36daaf3..00000000
--- a/test_real_tears.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env python3
-"""Quick test to verify the real tears pipeline runs in container."""
-import sys
-import os
-_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "vnpy_v4.4.0"))
-sys.path.insert(0, _VNPY_SRC)
-
-from unittest.mock import Mock, MagicMock
-from sanguo_factor.analyzer import run_factor_analysis
-from sanguo_factor.registry import register_factor
-
-# Register a simple test factor
-register_factor("test_ma5", "ts_mean(close, 5)")
-
-# Create minimal cfg mock
-cfg = Mock()
-cfg.data_paths = {"vnpy_db": "/tmp/test.db"}
-
-try:
- # Run with minimal data
- result = run_factor_analysis(
- symbols=["600000.SH"], # Single symbol
- factor_names=["test_ma5"],
- start="2024-01-01",
- end="2024-01-31", # Small date range
- cfg=cfg,
- output_dir="/tmp/test_tears"
- )
-
- print(f"Test completed successfully!")
- print(f"Result: {result}")
- print(f"IC Summary: {result.ic_summary}")
- print(f"Report Path: {result.report_path}")
-
-except Exception as e:
- print(f"Test failed with error: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)