From a507e7afbcafe2815fb58136e809d8d9b394b6d5 Mon Sep 17 00:00:00 2001 From: zlt Date: Tue, 1 Sep 2026 09:18:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E9=81=93=E5=BC=83=E6=B4=9E=E8=87=AA?= =?UTF-8?q?=E6=84=88+=E5=8F=82=E6=95=B0=E5=88=97=E6=94=BE=E5=AE=BDTEXT+?= =?UTF-8?q?=E8=BF=90=E7=BB=B4=E6=B8=85=E6=A0=87=E8=AE=B0=E6=8C=89=E9=92=AE?= =?UTF-8?q?+iPad=E6=B5=AE=E5=B1=82teleport=E4=B8=8E=E8=A7=A6=E5=B1=8F?= =?UTF-8?q?=E7=9B=B4=E8=BF=9E+=E4=BF=A1=E5=8F=B7=E8=AE=A1=E5=88=92?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- DEVLOG.md | 122 +++++++++++++++++++++++++++- README.md | 2 +- app/core/ws_codec.py | 17 ++++ app/web/static/index.html | 108 ++++++++++++++++++++++-- app/ws/runner.py | 58 ++++++++++++- ddl_pms_v1.sql | 5 +- scripts/migrate_param_value_text.py | 99 ++++++++++++++++++++++ scripts/run_tests.py | 4 +- scripts/test_batch19_units.py | 3 +- scripts/test_batch6_units.py | 20 +++++ scripts/ws_smoke.py | 13 ++- 公司原表-量化数据2026.8.3.xlsx | Bin 0 -> 43546 bytes 12 files changed, 433 insertions(+), 18 deletions(-) create mode 100644 scripts/migrate_param_value_text.py create mode 100644 公司原表-量化数据2026.8.3.xlsx diff --git a/DEVLOG.md b/DEVLOG.md index 54df35a..5eaf88e 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1139,7 +1139,127 @@ tlai4090 同法。无新 env、无 DDL。 存量合计/底行) 与图表单点标记都应出现。 **还欠着什么** -上节点的三个口径拍板照旧欠着; 图表逐像素对齐照旧按需。 +1. **样式仍有出入 (未决, 新会话第一件事)**: 隐藏行修复后使用者反馈「表格样式还是不对」, + 具体差异点没来得及描述。新会话需要: 让使用者指认哪里不对 (截图或把导出的 xlsx 发回), + 对照基准用仓库根目录的 公司原表-量化数据2026.8.3.xlsx (2026-08-28 已入库), 逐区 diff + (候选疑点: O/P 融资列折叠是否符合预期、黄条/粉底跨的列数、字体字号、列宽、边框、 + 图表配色与轴样式、标题行)。改样式动 app/assets/publish_template.xlsx 与 + publish_export.py, 改完跑 scripts/test_wiring.py + 全套 588 例。 +2. 部署状态待确认: 本节点三个文件已回写开发机工作区, commit/push 与两台服务器 + (真机 188 / 模拟机 tlai4090) 的 git pull + make deploy 是否已做, 新会话先问。 +3. 上节点的三个口径拍板照旧欠着; 图表逐像素对齐照旧按需。 + +--- + +## 2026-08-31 · 查透「通道要人工做一次全量对账」+ 参数列 1406 截断 + iPad 点不开侧栏 + 信号标计划 + +**做了什么** +使用者报三件事: 页面一直挂「通道要人工做一次全量对账」不知怎么操作; iPad Pro 上点 +左右窄条 (信号流 / 待办与操作) 展不开; 想在速览「没持仓的票」段标出选股计划里的票。 +只读排查模拟机 (155), 挖出三个真问题, 全部修掉: + +一、**水位卡死三天** (对账横幅的根源)。2026-08-28 17:21 pms-ws 重启首连时, 对端补发的 +99 条 pong 全部验签失败被丢 (SIG_INVALID, 一次性, 之后再无), 每条占一个 seq → 水位卡在 +365003 洞前; `_baselined` 一经置位终生不再对齐, 会话内的洞永远等不来 (ws 是有序流, +§6.1 补发只在重连握手时), 暂存涨到 5.3 万条、ack 停摆、重连时对端整段重发 4.9 万条。 +修法两层: `_session` 每连接重置 `_baselined` (重连即重对齐, 连续时是空操作); +新增**弃洞前进** watchdog (`_maybe_skip_gap` + 纯函数 `ws_codec.skip_gap`): 水位在同一 +位置卡满 PMS_QMT_GAP_SKIP_SEC (默认 300 秒, 参数中心可调, 0=关) 就按 §6.2 认定补不出 +来 —— 推水位到已到达最高序号、清暂存、置 resync 标记走全量对账。暂存序号全部来自验签 +通过的消息, 推进目标不受伪造数据影响。另加 sig_invalid / pending_gap / gap_skips 三个 +stat 计数, ws_smoke status 逐条给人话告警。 + +二、**pms_runtime_param.param_value VARCHAR(200) 装不下会长大的 JSON** (每分钟两处 +1406 截断错的共同病根)。清场名单 PMS_EXIT_CLEANUP_DONE (30 只闭仓票就超长; 存不上时 +「每次闭仓只清一次」防呆失守, 闭仓后新下的建仓单会被清场误撤) 与回放游标 +PMS_REPLAY_CURSOR (08-28 改 v2 带 seen 去重字典后必超; 游标推不动, 表回放每分钟报 +「游标写入失败」)。修法: DDL 放宽为 TEXT, 已有库跑新脚本 +scripts/migrate_param_value_text.py (幂等, 默认演练, --yes 执行, 显式过 map_tables, +batch19 [K2] 白名单同步加上)。 + +三、**页面**: ① 运维抽屉补上「清除通道对账标记」按钮 —— 后端接口 +(POST /api/ws-channel/clear-resync, 管理员) 早就有, 页面一直没入口, 横幅让人「经运维 +抽屉清除」却无处可点; 按钮带确认弹窗, 只在 resync_required 时出现。② iPad 点不开侧栏: +本地台架实测鼠标路径完好, 判为 iPad Safari 对非控件元素点按不合成 click 的老毛病; +修法是窄条与蒙层加 touchend 直连 (touchstart 记起点, 位移 >12px 判滑动误触不开, +preventDefault 挡合成 click 防双触发), CSS 加 touch-action:manipulation 与 +@media(pointer:coarse) 命中区加大 (窄条 40→48px)。鼠标端零改动 (无 touch 事件, 走原 +@click; coarse 媒询不命中)。③ 速览「没持仓的票」段: 行首加蓝色「计划」标 —— 票在 +今天上游选股计划主榜 (planRows, 登录即随 loadPlan 加载) 就标, 段标题写明含义; +「更早的信号」段同样标 (仅非持仓行)。 + +顺带查明账实差异现状: 账本与 ws 快照差 5 只 (002436 少 1600 / 002709 少 300 / +300308 多 900 / 301308 多 100 / 688676 多 3600), 正是缺口期丢成交所致; 爆炸半径闸 +判据是「>5 只且 >34%」, 5 只不触发, 15:10 日终结算的全量对账会自动照 ws 修正留痕。 +另发现 trading_position 表只有 2 只与 ws 快照 (7 只) 不同步, 事实源仲裁已按 ws 为准 +并告警 —— 「谁在写那张表」要与 QMT 侧核对。 + +**动了哪些文件** +app/ws/runner.py (每连接重置基线 + 弃洞 watchdog + sig_invalid 计数)、 +app/core/ws_codec.py (skip_gap)、app/web/static/index.html (清标记按钮 + 触屏直连 + +计划标)、ddl_pms_v1.sql (param_value → TEXT)、新增 scripts/migrate_param_value_text.py、 +scripts/ws_smoke.py (三条新告警)、scripts/test_batch6_units.py (弃洞 3 例, 65→68)、 +scripts/test_batch19_units.py ([K2] 白名单)、scripts/run_tests.py (共 591 例)、README.md。 + +**部署方式** +模拟机 155: git pull 后 make deploy (源码打镜像必须重建); 之后跑一次 +`docker compose run --rm --no-deps pms-web python scripts/migrate_param_value_text.py --yes` +(ALTER 秒级, 不用重启, 新库建表自带 TEXT 不用跑)。真机 188 同法, 时点等 QMT 侧联调 +恢复时一并 (它同样带着 VARCHAR(200) 和旧 runner)。 + +**真机判收** +开发机: 全部 20 套 591 例 ALL SUITES PASS; 本地台架 1024×1366 实测 —— 鼠标点窄条开浮层、 +触摸序列开浮层、滑动 380px 误触不开、触摸蒙层关闭、控制台无脚本错误。 +部署后待判收: ① pms-ws 起来后 5 分钟内日志出现「水位弃洞前进」且 make ws-status 水位 +追到当前、暂存归零; ② 页面「账本对账」差异清零后点「清除通道对账标记」, 横幅消失且 +不复发; ③ worker 日志不再刷 1406; ④ iPad 实机点窄条能展开 (模拟器只能模拟到触摸事件 +层, Safari 真机行为要使用者过手); ⑤ 速览出现蓝「计划」标。 + +**还欠着什么** +1. 155 部署与迁移待用户批准执行 (容器重建 + ALTER TABLE 各要一次授权)。 +2. 全量对账清差异 + 清标记按钮点掉横幅, 待部署后走一遍 (15:10 结算大概率已自动修正 + 差异, 部署后核对 make watch 与页面对账结果即可)。 +3. 与 QMT 模拟侧核对两件: 08-28 17:21 那批 pong 为何验签失败 (重放重签? 密钥轮换?); + trading_position 表还该不该有人写。 +4. iPad 实机判收 (第 ④ 条) 待使用者过手。 +5. 真机 188 的同款部署与迁移, 等 QMT 侧联调窗口。 + +--- + +## 2026-09-01 · iPad 浮层实锅重修: teleport 出栈 + 只用老写法的浮层几何 + +**做了什么** +使用者发来 iPad Pro 实机截图: 点右窄条浮层**能开但渲染烂了** —— 面板无背景、无遮罩, +内容与底下持仓表的浮动操作列穿插, 选股计划抽屉的深色提示块也粘在屏上。与 08-31 的判断 +修正一处: 实机上点击事件是通的, 坏的是浮层渲染。两个独立机制都补死: +一, **层叠上下文困局**: 浮层 aside 在 position:sticky 的 side 容器里, sticky 自成层叠 +上下文, 固定定位的浮层被困在里面, 与主区 el-table 浮动列 (自带 z-index) 的绘制次序在 +部分 Safari 上穿插。修法: 两个浮层与遮罩用 Vue 内建 teleport 挂到 body 下 +(:disabled 绑定「非浮层态」, 常驻停靠时原地不动, 行为与从前逐字节相同)。 +二, **新式 CSS 整条被丢**: 浮层宽度用 min(560px,88vw)、遮罩用 inset:0, 旧 Safari +(min 需 11.1+, inset 需 14.5+) 会把整条声明丢掉 —— 遮罩变 0×0 不可见。改成 +width+max-width 与 top/left/right/bottom 四边的等价老写法, 面板背景加一条 #fff 兜底 +再让变量覆盖。顺带: 触屏上 ? 号悬停气泡点按后粘住不消失 (截图那块黑浮块), +@media(hover:none) 下不出气泡; 新增 ?uidbg=1 远程排查小牌 (印浏览器 UA / 视口 / +停靠态 / 浮层态, 平时不出现), iPad 再有样式问题一张截图即可定位。 + +**动了哪些文件** +app/web/static/index.html (teleport ×3、浮层几何老写法、背景兜底、气泡抑制、uidbg)。 + +**部署方式** +随 08-31 那批一起: 155 git pull 后 make deploy, 另跑一次 migrate_param_value_text.py --yes。 + +**真机判收** +开发机台架两形态实测: 1024×1366 浮层 —— 面板已挂 body 下、position fixed、宽 560、 +白底、遮罩 display:block、z 2010; 1920×1080 常驻 —— 两栏 parent 仍是 side 容器 +(teleport 未触发)、窄条隐藏、三栏布局与改前一致; 控制台无脚本错误。 +部署后待判收: iPad 实机点窄条, 浮层应当白底带阴影、背后有半透明遮罩、点遮罩即关。 + +**还欠着什么** +1. 随 08-31 节点: 155 部署+迁移待批; iPad 实机判收待使用者过手 (地址后加 ?uidbg=1 + 截图可带上排查小牌)。 +2. 模拟仓重启方案已给使用者建议 (彻底清盘: 盘中紧急离场卖光 → 收盘后 purge-dead-orders + + reset-ledger → 次日从零自动建仓; 清账前先存档), 待拍板。 --- +
{{ dbgText }}
+
-
+
{{ sigBriefHeldCount }} 信号流
+ +
{{ hm(g.time) || '—' }} + 计划 {{ nm(g.ts_code) }} {{ g.cat }} {{ g.direction }} @@ -586,6 +617,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
{{ (g.ymd||'').slice(5) }} {{ hm(g.time) || '—' }} 持仓 + 计划 {{ nm(g.ts_code) }} {{ g.cat }} {{ g.direction }} @@ -752,6 +784,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
+
@@ -952,10 +985,11 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
-
+
待办与操作 {{ proposals.length }}
+ +
@@ -1748,6 +1783,11 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;} 生成日报 导出下游表结构
+
+ 清除通道对账标记 + 通道曾经补发不全(顶部横幅「通道要人工做一次 + 全量对账」就是它)。处理顺序:先点上面的「账本对账」,确认结果里差异为零,再点这里清除提醒。 +
当前下发通道: {{ dm.mode }} —— {{ dm.hint }} (通道在「参数设置 → PMS_DISPATCH_MODE」切换) @@ -2658,6 +2698,9 @@ createApp({ // 入场三路独立信号也并进来, 免得只看告警漏了真正会动手的那几条。 ── const code6 = s => (String(s || '').match(/\d{6}/) || [''])[0]; const heldCodeSet = computed(() => new Set(heldPositions.value.map(p => code6(p.ts_code)))); + // 今天选股计划里的票 (上游 /plan 主榜, 登录时随 loadPlan 加载): 速览里没持仓的 + // 信号若命中计划, 挂「计划」标 —— 交易员一眼分清哪些噪音其实是候选池的动静 (2026-08-31) + const planCodeSet = computed(() => new Set((planRows.value || []).map(r => code6(r.ts_code)))); const sigBrief = computed(() => { const held = heldCodeSet.value; const out = []; @@ -2679,6 +2722,8 @@ createApp({ for (const r of (s.timing || [])) out.push({ ts_code: r.ts_code, time: r.time || '', cat: '买入时机', direction: '买入', level: 'INFO', held: held.has(code6(r.ts_code)) }); + const inPlan = planCodeSet.value; + for (const g of out) g.plan = inPlan.has(code6(g.ts_code)); out.sort((a, b) => (b.held - a.held) || String(b.time).localeCompare(String(a.time))); return out; }); @@ -3004,6 +3049,21 @@ createApp({ opsResult.value = JSON.stringify(d, null, 2); opsLoading.value = false; await loadAll(); } + // 清通道对账标记 (2026-08-31): 后端接口早就有 (POST /api/ws-channel/clear-resync), + // 页面一直没有入口, 横幅让人"经运维抽屉清除"却无处可点。语义见后端注释: 标记只能 + // 人清, 清之前必须先做完全量对账 (上面「账本对账」按钮) 且差异为零。 + async function clearResync() { + try { + await ElementPlus.ElMessageBox.confirm( + '清除前请确认两件事:一,已点过「账本对账」;二,对账结果 diffs 为空(差异为零)。' + + '清除只是去掉「通道要人工做一次全量对账」的提醒,不改任何账目。', + '清除通道对账标记', { type: 'warning', confirmButtonText: '已对账,清除', cancelButtonText: '先不清' }); + } catch (e) { return; } + opsLoading.value = true; + const d = await call('post', '/api/ws-channel/clear-resync'); + opsResult.value = JSON.stringify(d, null, 2); opsLoading.value = false; + await Promise.all([loadWs(), loadAll()]); + } async function loadSchema() { opsLoading.value = true; const d = await call('get', '/api/ops/downstream-schema'); @@ -3202,10 +3262,22 @@ createApp({ document.body.classList.toggle('l-fold', lFold.value); document.body.classList.toggle('r-fold', rFold.value); } + // ?uidbg=1 远程排查 (2026-09-01): iPad 只能靠截图排样式问题, 这块小牌把浏览器 + // 版本与布局关键量直接印在屏幕上, 一张截图就够定位。平时不带参数, 完全不出现。 + const uidbg = new URLSearchParams(location.search).has('uidbg'); + const dbgText = ref(''); + function _dbgUpdate() { + if (!uidbg) return; + dbgText.value = navigator.userAgent + ' · 视口 ' + window.innerWidth + '×' + window.innerHeight + + ' · dock-l ' + document.body.classList.contains('dock-l') + + ' · dock-r ' + document.body.classList.contains('dock-r') + + ' · 浮层 ' + (railOpen.value || '关'); + } function applyDock() { const w = window.innerWidth; document.body.classList.toggle('dock-l', w >= W_DOCK_LEFT); document.body.classList.toggle('dock-r', w >= W_DOCK_BOTH); + _dbgUpdate(); // 这一侧已经能常驻了, 就不该还浮在上面 if (railOpen.value === 'l' && w >= W_DOCK_LEFT && !lFold.value) railOpen.value = null; if (railOpen.value === 'r' && w >= W_DOCK_BOTH && !rFold.value) railOpen.value = null; @@ -3214,7 +3286,7 @@ createApp({ if (side === 'l') lFold.value = true; else rFold.value = true; _railClasses(); _railSave(); } - function closeRail() { railOpen.value = null; } + function closeRail() { railOpen.value = null; _dbgUpdate(); } function openRailSide(side, forcePull) { const canDock = document.body.classList.contains(side === 'l' ? 'dock-l' : 'dock-r'); const folded = side === 'l' ? lFold.value : rFold.value; @@ -3225,7 +3297,24 @@ createApp({ } if (!forcePull && canDock && !folded) return; // 已经常驻着, 不用再浮一层 railOpen.value = side; + _dbgUpdate(); } + // 触屏直连 (2026-08-31): iPad 的 Safari 对非控件元素的点按并不总是合成 click 事件, + // 窄条在触屏上点不开就是这么来的。touchend 直接开侧栏, preventDefault 挡掉后续可能 + // 合成的 click 免得双触发; 记录起点加位移阈值, 挡住"滑动页面时扫过窄条"的误触。 + // 鼠标端没有 touch 事件, 走原来的 @click, 行为一个字都不变。 + const _tp = { x: 0, y: 0 }; + function railTS(e) { + const p = (e.touches && e.touches[0]) || {}; + _tp.x = p.clientX || 0; _tp.y = p.clientY || 0; + } + function railTE(e, side) { + const p = (e.changedTouches && e.changedTouches[0]) || {}; + if (Math.hypot((p.clientX || 0) - _tp.x, (p.clientY || 0) - _tp.y) > 12) return; + e.preventDefault(); + openRailSide(side); + } + function scrimTE(e) { e.preventDefault(); closeRail(); } function _onEsc(e) { if (e.key === 'Escape') closeRail(); } onMounted(() => { _railClasses(); applyDock(); @@ -3274,7 +3363,8 @@ createApp({ MSG_KINDS, kindLabel, hm, msgOpen, msgKind, msgFeed, msgCounts, msgShown, msgLatest, msgLevelClass, setMsgKind, msgGo, sigTab, srcOpen, toggleSrc, sigBriefHeld, sigBriefOther, - lFold, rFold, railOpen, foldRail, closeRail, openRailSide, + lFold, rFold, railOpen, foldRail, closeRail, openRailSide, railTS, railTE, scrimTE, + clearResync, uidbg, dbgText, authed, authReady, me, isAdmin, rolesText, loginForm, loginBusy, loginErr, doLogin, doLogout, doExport, sectClosed, toggleSect }; } diff --git a/app/ws/runner.py b/app/ws/runner.py index 9f1424e..433a5f8 100644 --- a/app/ws/runner.py +++ b/app/ws/runner.py @@ -79,7 +79,9 @@ class WsRunner: self._acked_seq = 0 # 已发出 ack_seq 的水位 self._pending_seq = set() # 乱序暂存 (正常恒空) self._unacked = 0 # 距上次 ack 又落了几条 - self._baselined = False # 是否已对齐对端序号起点 (见 _set_baseline) + self._baselined = False # 本连接是否已对齐对端序号起点 (见 _set_baseline; + # 每次连接在 _session 里重置, 2026-08-31) + self._gap_stuck = None # (卡住时的水位, 卡住起始时刻) —— 弃洞watchdog用 self._db_ready = False # 通道三表是否可用 (缺表时空转重试, 不写心跳) self._params = {} self._warn = "" # 握手期发现的非致命异常, 连上后仍要挂在 last_error @@ -107,6 +109,7 @@ class WsRunner: "max_attempts": param_store.get_int("PMS_QMT_SEND_MAX_ATTEMPTS", 3), "connect_timeout": param_store.get_int("PMS_QMT_CONNECT_TIMEOUT_SEC", 10), "query_interval_sec": param_store.get_int("PMS_QMT_QUERY_INTERVAL_SEC", 300), + "gap_skip_sec": param_store.get_int("PMS_QMT_GAP_SKIP_SEC", 300), } try: self._params = await _db(_load) @@ -117,7 +120,7 @@ class WsRunner: "idle_timeout_sec": 15, "ack_batch": 20, "ack_interval_sec": 2.0, "outbox_poll_sec": 0.5, "beat_sec": 2, "max_attempts": 3, "connect_timeout": 10, - "query_interval_sec": 300} + "query_interval_sec": 300, "gap_skip_sec": 300} logger.warning("参数刷新失败, 沿用上一份: %s", _brief_err(e)) @staticmethod @@ -292,6 +295,13 @@ class WsRunner: self._stat.pop("ack_seq_degraded", None) for k in ("peer_skew_min", "peer_skew_max"): self._stat.pop(k, None) # 时钟差/卡顿按会话统计, 跨连接混着看没意义 + # 序号基线每个连接都重新对齐一次 (2026-08-31): 原来 _baselined 一经置位终生 + # 不再对齐, 会话中途出的洞 (对端跳号/上行验签失败被本端丢弃) 补发也补不出来, + # 水位就永远卡在洞前 —— 真机卡了三天、暂存五万三千条就是这么来的。重连后首条 + # 补发消息与水位连续时 _set_baseline 是空操作, 行为与从前逐字节相同; 不连续 + # 说明那段真拿不到了, 推基线 + 置 resync 走全量对账, 与冷启动同一条 §6.2 路。 + self._baselined = False + self._gap_stuck = None await self._handshake(seed, peer) # last_error 用握手期攒下的告警覆盖: 连上了不等于没问题 (见 _handshake # 的序号倒挂检查), 一律清空会把唯一一条线索抹掉。 @@ -416,6 +426,9 @@ class WsRunner: except wsc.CodecError as e: # §2.1: 验签失败直接丢弃, **不执行任何业务动作**。不回 reject —— # 连不上信任的对端时, 多说一句话只是多给攻击者一个探测面。 + # 计数挂 stat (2026-08-31): 被丢的消息若带 seq, 水位上就多一个洞, + # 这个数是排「水位为什么卡住」时的第一线索, 光靠翻日志太难受。 + self._stat["sig_invalid"] = self._stat.get("sig_invalid", 0) + 1 logger.error("上行消息校验失败, 已丢弃 (%s): %s", e.code, e.message) continue self._track_skew(env) @@ -525,17 +538,58 @@ class WsRunner: # 明确收尾 —— 有始有终才看得出它到底有没有卡住。 self._stat["ooo"] = self._stat.get("ooo", 0) + 1 n, k = self._stat["ooo"], len(self._pending_seq) + self._stat["pending_gap"] = k if n == 1 or n % 200 == 0: logger.warning("水位未连续: 卡在 %s, 暂存 %s 条 (第 %s 次)。刚重连的话这是 " "§6.1 补发的正常形态, 补完自会收敛; 若暂存条数持续增长, " "才是真乱序", self._last_seq, k, n) + await self._maybe_skip_gap() elif had_pending: logger.info("水位缺口已补齐, 连续到 %s (期间未连续 %s 次)", self._last_seq, self._stat.get("ooo", 0)) self._stat["ooo"] = 0 + self._stat["pending_gap"] = 0 + self._gap_stuck = None if self._unacked >= self._p("ack_batch", 20): await self._flush_ack() + async def _maybe_skip_gap(self): + """水位在同一位置卡满 gap_skip_sec 秒 → 按 §6.2 弃洞前进 (2026-08-31)。 + + ws 是单连接有序流: 会话内出现的洞 (对端跳号 / 上行验签失败被本端丢弃) 在本会话里 + **永远**等不来, §6.1 补发只发生在重连握手时; 而对端日志滚动后连重连也补不出来。 + 原来的处置是干等 —— 暂存集合无限涨、ack 水位停摆、每次重连对端整段重发 (真机卡了 + 三天, 暂存五万三千条)。现在等满窗口就认定补不出来, 与 _set_baseline 处置真缺口 + 同一语义: 推水位到已到达的最高序号、清暂存、置 resync 标记走全量对账。 + 暂存里的序号全部来自**验签通过**的消息, 推进目标不受伪造数据影响。 + 窗口在参数中心调 (PMS_QMT_GAP_SKIP_SEC, 默认 300 秒; 0 = 关掉本机制)。 + """ + skip_after = self._p("gap_skip_sec", 300) + if skip_after <= 0 or not self._pending_seq: + return + now = time.monotonic() + if self._gap_stuck is None or self._gap_stuck[0] != self._last_seq: + self._gap_stuck = (self._last_seq, now) # 水位动过 → 重新计时 + return + if now - self._gap_stuck[1] < skip_after: + return + old, waited = self._last_seq, len(self._pending_seq) + self._last_seq, missing = wsc.skip_gap(self._last_seq, self._pending_seq) + self._pending_seq = set() + self._acked_seq = max(self._acked_seq, self._last_seq) + self._gap_stuck = None + self._stat["gap_skips"] = self._stat.get("gap_skips", 0) + 1 + self._stat["ooo"] = 0 + self._stat["pending_gap"] = 0 + with contextlib.suppress(Exception): + await _db(qmt_repo.save_watermark, self._last_seq, self._acked_seq) + with contextlib.suppress(Exception): + await _db(qmt_repo.set_conn, "ONLINE", resync=True) + logger.error("水位弃洞前进 (§6.2): 卡在 %s 已满 %s 秒, 暂存 %s 条早已到齐而中间 " + "%s 个序号等不来 —— 水位推到 %s。已置 resync 标记: 请做一次全量对账 " + "(运维「账本对账」, 差异清零), 然后在运维抽屉点「清除通道对账标记」", + old, skip_after, waited, missing, self._last_seq) + def _maybe_degrade_ack(self, pl: dict): """对端回「不认识 ack_seq」时停发 ack_seq。 diff --git a/ddl_pms_v1.sql b/ddl_pms_v1.sql index 20f1673..4b5062d 100644 --- a/ddl_pms_v1.sql +++ b/ddl_pms_v1.sql @@ -188,7 +188,10 @@ CREATE TABLE IF NOT EXISTS pms_industry_map ( -- 10. 运行参数持久层 (页面修改的参数落此, 优先于 settings 文件初值) CREATE TABLE IF NOT EXISTS pms_runtime_param ( param_key VARCHAR(64) PRIMARY KEY, - param_value VARCHAR(200) NOT NULL, + -- TEXT 而非 VARCHAR(200) (2026-08-31 放宽): 清场名单 PMS_EXIT_CLEANUP_DONE 与回放游标 + -- PMS_REPLAY_CURSOR 都是会随运行长大的 JSON, 30 只闭仓票的名单就超过 200 字符, + -- 写入每分钟报 1406 截断错。已有库用 scripts/migrate_param_value_text.py 迁移。 + param_value TEXT NOT NULL, updated_by VARCHAR(32) NOT NULL DEFAULT 'user', updated_at DATETIME NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='页面可调参数当前值'; diff --git a/scripts/migrate_param_value_text.py b/scripts/migrate_param_value_text.py new file mode 100644 index 0000000..b9579f1 --- /dev/null +++ b/scripts/migrate_param_value_text.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +""" +迁移: pms_runtime_param.param_value 由 VARCHAR(200) 放宽为 TEXT +================================================================ +运行: + docker compose run --rm --no-deps pms-web python scripts/migrate_param_value_text.py # 演练, 只打印 + docker compose run --rm --no-deps pms-web python scripts/migrate_param_value_text.py --yes # 实际执行 + +为什么要迁: + 这一列存的不全是短标量 —— 清场名单 (PMS_EXIT_CLEANUP_DONE, 闭仓票代码的 JSON 数组) 与 + 回放游标 (PMS_REPLAY_CURSOR, v2 起带 seen 去重字典) 都随运行越长越大。真机 2026-08-31 + 实测: 名单 30 只票就超 200 字符, 两处写入每分钟各报一次 1406 截断错, 游标推不动、 + 清场防呆标记存不上 (后者失守时, 闭仓后新下的建仓单会被清场误撤)。 + +为什么单独一个脚本, 不放进 init_db: + init_db.py 只认 `CREATE TABLE IF NOT EXISTS` 与幂等 INSERT, ALTER 语句会被它判成残句 + 整批中止; 而 CREATE IF NOT EXISTS 对已存在的表一个字都不改。建表语句 (ddl_pms_v1.sql) + 已同步改成 TEXT, **新库直接就有**, 不需要跑本脚本。 + +幂等: 列已是 TEXT 时自动跳过, 重复跑安全。绕过 db.session 的单表守卫 (直接用引擎), 与 + migrate_archived_at 同一套做法。若 ALTER 被 ShardingSphere-Proxy 拒绝, 脚本会把完整 + 语句打出来, 直接拿去物理库 (my_quant_db) 执行即可。 +""" +import argparse +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +TABLE = "pms_runtime_param" +COL = "param_value" +TARGET_TYPE = "text" + + +def _col_type(conn, table: str, col: str): + from sqlalchemy import text + rows = conn.execute(text(f"SHOW COLUMNS FROM {table}")).fetchall() + for r in rows: + if r[0] == col: + return str(r[1] or "").lower() + return None + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--yes", action="store_true", help="确认执行 (缺省只演练)") + args = ap.parse_args() + + from sqlalchemy import text + from app.db.session import TABLE_PREFIX, get_engine, map_tables + eng = get_engine("proxy") + if TABLE_PREFIX: + print(f"[双实例] 表名前缀 {TABLE_PREFIX!r} 生效: 实际迁移的是 {TABLE_PREFIX}{TABLE}") + + real_table = map_tables(TABLE) + try: + with eng.connect() as c: + cur = _col_type(c, real_table, COL) + except Exception as e: + print(f"FAIL: 查列失败 {type(e).__name__}: {e} (表不存在就先跑 init_db)") + sys.exit(1) + + if cur is None: + print(f"FAIL: {real_table} 没有 {COL} 列 —— 表结构与预期不符, 先人工核对") + sys.exit(1) + print(f"当前类型: {real_table}.{COL} = {cur}") + if cur.startswith(TARGET_TYPE): + print("无需迁移: 已是 TEXT。") + return + + stmt = map_tables( + f"ALTER TABLE {TABLE} MODIFY COLUMN {COL} TEXT NOT NULL " + f"COMMENT '参数值: 标量或 JSON; 2026-08-31 由 VARCHAR(200) 放宽 (清场名单/回放游标会长大)'") + + if not args.yes: + print("\n[演练模式] 未执行任何语句。确认无误后加 --yes 重跑。将执行:") + print(" " + stmt) + return + + try: + with eng.begin() as c: + c.execute(text(stmt)) + print("OK ALTER 执行成功") + except Exception as e: + print(f"FAIL {type(e).__name__}: {e}") + print(f"\n完整语句如下 —— 可直接拿到物理库 (my_quant_db) 执行:\n{stmt};") + sys.exit(1) + + with eng.connect() as c: + now = _col_type(c, real_table, COL) + if now and now.startswith(TARGET_TYPE): + print(f"ALL OK: {real_table}.{COL} 已是 {now}。清场名单与回放游标的写入即刻恢复, 无需重启。") + else: + print(f"FAIL: 迁移后类型仍为 {now}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_tests.py b/scripts/run_tests.py index dac967c..60c4a60 100644 --- a/scripts/run_tests.py +++ b/scripts/run_tests.py @@ -10,7 +10,7 @@ test_batch3_units.py 规则闸 / 择时执行器实现B 纯逻辑 (21 例) test_batch4_units.py 动作引擎 四类自主动作触发与数量口径 (11 例) test_batch5_units.py 决策系统信号流解析与消化口径 (8 例) - test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 (65 例) + test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/弃洞/DDL/逐笔入账 (68 例) test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 例) test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 例) test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (49 例) @@ -39,7 +39,7 @@ 缩放/日历按年降级/网格中枢与止盈闩锁/买入暂停 按来源分记/宏观失败路径保留留痕/双实例表名前缀 (19 例) test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (67 例) - 共 588 例 + 共 591 例 任一子集失败即整体失败 (退出码 1)。 """ import os diff --git a/scripts/test_batch19_units.py b/scripts/test_batch19_units.py index 899ec3f..a4bcb6b 100644 --- a/scripts/test_batch19_units.py +++ b/scripts/test_batch19_units.py @@ -441,7 +441,8 @@ def _(): # init_db / migrate_archived_at 拿 get_engine 直执行 DDL (有意绕过单表守卫), # 它们必须显式调 map_tables —— 否则正式实例 make deploy 会建出**无前缀**的表, # 与模拟实例的表撞在一起 - for f in ("scripts/init_db.py", "scripts/migrate_archived_at.py"): + for f in ("scripts/init_db.py", "scripts/migrate_archived_at.py", + "scripts/migrate_param_value_text.py"): src = open(os.path.join(root, f), encoding="utf-8").read() assert "map_tables" in src, f + " 直执 DDL 必须过 map_tables" diff --git a/scripts/test_batch6_units.py b/scripts/test_batch6_units.py index 044cc80..f255e31 100644 --- a/scripts/test_batch6_units.py +++ b/scripts/test_batch6_units.py @@ -340,6 +340,26 @@ def run(): last, pend = wsc.next_watermark(last, pend, s) eq((last, pend), (10003, set())) + # --- 弃洞前进。2026-08-31 真机缺陷的回归防线: 会话中途 99 条验签失败的 pong 被丢, + # 每条占一个 seq → 水位卡在洞前三天、暂存五万三千条。ws 是有序流, 会话内的洞 + # 等不来 (§6.1 补发只在重连握手时), 到点就按 §6.2 弃洞: 推水位、清暂存、置 resync。 + @case("弃洞前进: 推到已到达的最高序号, 洞数点得清") + def _(): + # 卡在 100, 暂存 {102,103,105}: 101 与 104 是洞 + eq(wsc.skip_gap(100, {102, 103, 105}), (105, 2)) + + @case("弃洞前进: 暂存为空原地不动; 全连续时洞数为 0") + def _(): + eq(wsc.skip_gap(100, set()), (100, 0)) + eq(wsc.skip_gap(100, {101, 102}), (102, 0)) + + @case("弃洞前进后水位能正常前进 (与 next_watermark 接得上)") + def _(): + last, _n = wsc.skip_gap(365003, {365100, 365101, 421943}) + eq(last, 421943) + last, pend = wsc.next_watermark(last, set(), 421944) + eq((last, pend), (421944, set())) + print("\n[F] 去重键与成交自洽") @case("trade 的第二层去重键取 trade_no") diff --git a/scripts/ws_smoke.py b/scripts/ws_smoke.py index 70ca82e..9a9f98f 100644 --- a/scripts/ws_smoke.py +++ b/scripts/ws_smoke.py @@ -146,7 +146,18 @@ def cmd_status(args): f"看清单: ws_smoke.py inbox --type trade; 确认真该入账再把那些行的 " f"processed 改回 0") if ch.get("resync_required"): - bad.append("resync_flag=1: 对端补发不全, 须走全量对账后经页面清除") + bad.append("resync_flag=1: 对端补发不全, 须走全量对账后经页面清除 " + "(运维抽屉「账本对账」→ 差异清零 →「清除通道对账标记」)") + if int(s.get("sig_invalid") or 0): + bad.append(f"验签失败被丢弃 {s['sig_invalid']} 条: 每条若带 seq 就在水位上留一个洞。" + f"持续增长要与 QMT 侧核对签名实现 (哪类消息在坏, 见 docker compose " + f"logs pms-ws | grep SIG_INVALID)") + if int(s.get("pending_gap") or 0) > 500: + bad.append(f"乱序暂存 {s['pending_gap']} 条: 水位卡在洞前。等满 PMS_QMT_GAP_SKIP_SEC " + f"(默认 300 秒) 会自动弃洞前进并置 resync 标记") + if int(s.get("gap_skips") or 0): + bad.append(f"本进程已弃洞前进 {s['gap_skips']} 次: 每次都置了 resync 标记, " + f"记得全量对账后清除") if bad: print() for b in bad: diff --git a/公司原表-量化数据2026.8.3.xlsx b/公司原表-量化数据2026.8.3.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..99fbf84e0849e4d89fb5768fe4926004876b1c71 GIT binary patch literal 43546 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 {kV(Q?nTtISBg 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�!6+s-xY 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 literal 0 HcmV?d00001