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)