diff --git a/src/trader/ths/win.py b/src/trader/ths/win.py index bf08e9c..a3c0e6b 100644 --- a/src/trader/ths/win.py +++ b/src/trader/ths/win.py @@ -663,17 +663,29 @@ def cancel_last(self): return self._bulk_cancel("last") def get_balance(self): - self.switch_to_normal() - hot_key(["F4"]) - self.refresh() - hwnd = self.get_right_hwnd() - data = {} - for key, cid in BALANCE_CONTROL_ID_GROUP.items(): - ctrl = self._find_ctrl_by_id(hwnd, cid) - if ctrl > 0: - data[key] = get_text(ctrl) - self.state.update("balance", data) - return {"code": 0, "status": "succeed", "data": data} + # 多账户登录时每个账户各挂一套同 ID 资金控件,只有当前账户的可见; + # 不按可见性过滤会读到其他账户隐藏面板的数字(2026-07-14 双账户 + # 切换演练:Alt+2 已切到账户二,balance 仍返回账户一全套数字)。 + # 只认可见控件、读不到重试后明确报错——不做未过滤兜底:兜底在面板 + # 加载间隙同样可能抓到其他账户的隐藏副本,真钱 sizing 宁可失败不可读错。 + for retry in range(retry_time): + self.switch_to_normal() + hot_key(["F4"]) + self.refresh() + hwnd = self.get_right_hwnd() + data = {} + for key, cid in BALANCE_CONTROL_ID_GROUP.items(): + ctrl = self._find_ctrl_by_id(hwnd, cid, visible=True) + if ctrl > 0: + data[key] = get_text(ctrl) + if data: + self.state.update("balance", data) + return {"code": 0, "status": "succeed", "data": data} + time.sleep(sleep_time) + return {"code": 1, "status": "failed", + "msg": "未找到可见的资金面板控件(面板未加载完或客户端异常)," + "已放弃读取——不回退读隐藏面板(多账户下可能是其他账户的数字)," + "请稍后重试"} def get_position(self): for retry in range(retry_time): diff --git a/tests/test_balance_visible.py b/tests/test_balance_visible.py new file mode 100644 index 0000000..8b30010 --- /dev/null +++ b/tests/test_balance_visible.py @@ -0,0 +1,52 @@ +"""get_balance 可见性过滤回归:多账户下必须读当前账户的可见控件。 + +2026-07-14 双账户切换演练事故:xiadan 多账户登录时每个账户各挂一套同 ID +资金控件(0x3F4..),只有当前账户的可见。get_balance 原先不过滤可见性, +Alt+2 已切到账户二后仍返回账户一的全套数字——真钱 sizing 的输入被污染。 + +这里锁定两条纪律:① 只认可见控件;② 读不到就明确失败,绝不回退到未过滤 +匹配(兜底在面板加载间隙同样可能抓到其他账户的隐藏副本——宁可失败不可读错)。 +Win32 层全部打桩,可跨平台跑。 +""" +from trader.ths import win as w +from trader.ths.win import WinThsBackend + +HIDDEN, VISIBLE = 111, 222 + + +def _stubbed_backend(monkeypatch, find_ctrl): + b = WinThsBackend() + monkeypatch.setattr(b, "switch_to_normal", lambda: None) + monkeypatch.setattr(b, "refresh", lambda: None) + monkeypatch.setattr(b, "get_right_hwnd", lambda: 999) + monkeypatch.setattr(b, "_find_ctrl_by_id", find_ctrl) + monkeypatch.setattr(w, "hot_key", lambda keys: None) + monkeypatch.setattr(w, "sleep_time", 0) + monkeypatch.setattr( + w, "get_text", lambda h: "1.23" if h == VISIBLE else "34915.47" + ) + return b + + +def test_reads_only_visible_ctrl(monkeypatch): + """可见副本存在时必须用它——绝不能读到其他账户隐藏面板的数字。""" + + def find_ctrl(root, cid, cls=None, visible=False): + return VISIBLE if visible else HIDDEN + + result = _stubbed_backend(monkeypatch, find_ctrl).get_balance() + assert result["code"] == 0 + assert set(result["data"].values()) == {"1.23"} + + +def test_fails_loudly_when_no_visible_ctrl(monkeypatch): + """无可见控件 → 明确报失败,绝不回退未过滤匹配去读隐藏面板。""" + + def find_ctrl(root, cid, cls=None, visible=False): + return 0 if visible else HIDDEN + + result = _stubbed_backend(monkeypatch, find_ctrl).get_balance() + assert result["code"] == 1 + assert "不回退" in result["msg"] + # 隐藏副本的数字绝不能出现在任何返回里 + assert "34915.47" not in str(result)