From 77a1521c38e9763fc427df34410d0dbfcc649c84 Mon Sep 17 00:00:00 2001 From: Kloping <3474006766@qq.com> Date: Tue, 28 Jul 2026 15:58:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=AF=86=E5=88=AB=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E4=B8=8E=E6=B1=82=E8=B4=AD/=E5=AF=84?= =?UTF-8?q?=E5=94=AE=E5=8F=8C=E5=90=91=E8=B7=9F=E4=BB=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 金币余额跳变校验:偏差超阈值时重读确认,防OCR误读被采信 - 挂单成功验证:OCR无输出/异常时重试3次,全部失败保守判定为未成功 - 撤单加固:相似度阈值0.4->0.6(可配置),确认前重新OCR复核目标行 - 对账保护:订单页识别为空且本地有订单时跳过,防账本误清空 - 价格轮询改双向跟价:求购跟涨/寄售跟跌才可成交,并防自我抬价/压价 - 撤单失败时保留账本不重挂,重挂数量以实际撤销数为准 --- Frame/config.py | 5 +++ Frame/controller.py | 78 +++++++++++++++++++++++++----------------- Frame/runner.py | 83 ++++++++++++++++++++++++++++++++------------- 3 files changed, 111 insertions(+), 55 deletions(-) diff --git a/Frame/config.py b/Frame/config.py index 42d2d5d..8e6a61b 100644 --- a/Frame/config.py +++ b/Frame/config.py @@ -32,6 +32,11 @@ class BotConfig: INVENTORY_SELL_ONLY_THRESHOLD = 9# 库存仅出售阈值 MAX_BUY_WITH_INVENTORY = 5# 最大用库存购买数 + # ========== 识别安全校验 ========== + COINS_JUMP_THRESHOLD = 200000# 金币余额跳变校验阈值,超过则重读确认 + VERIFY_OCR_RETRY = 3# 挂单成功验证OCR重试次数 + CANCEL_MATCH_THRESHOLD = 0.6# 撤单名称匹配最低相似度 + # ========== 操作延时 ========== SEARCH_LOAD_WAIT = (2.0, 3.0)# 搜索加载等待时间 INPUT_WAIT = (0.8, 1.5)# 输入等待时间 diff --git a/Frame/controller.py b/Frame/controller.py index ea75e45..3c6bc07 100644 --- a/Frame/controller.py +++ b/Frame/controller.py @@ -208,10 +208,18 @@ class ActiveOrderManager: self.ctrl.press("A") time.sleep(random.uniform(*self.config.CONFIRM_WAIT)) + @staticmethod + def _match_score(target_words: set, name: str) -> float: + """计算目标词集与订单名称的Jaccard相似度""" + name_words = set(name.lower().split()) + union = len(target_words | name_words) + return len(target_words & name_words) / union if union > 0 else 0 + def cancel_single_order(self, target_name: str, order_type: str = "buy") -> bool: target_clean = clean_item_name(target_name) target_type = order_type.lower() target_words = set(target_clean.lower().split()) + min_score = self.config.CANCEL_MATCH_THRESHOLD rows = self._read_first_page() best_idx = None @@ -220,15 +228,12 @@ class ActiveOrderManager: for i, (name, otype) in enumerate(rows): if not name or target_type not in otype: continue - name_words = set(name.lower().split()) - common = len(target_words & name_words) - union = len(target_words | name_words) - score = common / union if union > 0 else 0 + score = self._match_score(target_words, name) if score > best_score: best_score = score best_idx = i - if best_idx is None or best_score < 0.4: + if best_idx is None or best_score < min_score: best_idx = None best_score = 0 if len(rows) >= self.config.ORDER_READ_ROWS: @@ -242,22 +247,30 @@ class ActiveOrderManager: for i, (name, otype) in enumerate(rows_2): if not name or target_type not in otype: continue - name_words = set(name.lower().split()) - common = len(target_words & name_words) - union = len(target_words | name_words) - score = common / union if union > 0 else 0 + score = self._match_score(target_words, name) if score > best_score: best_score = score best_idx = i + self.config.ORDER_READ_ROWS self.ctrl.press("DOWN") time.sleep(0.3) - if best_idx is None: - print(f" [撤单] 未找到 {target_name} {order_type} 单,跳过撤单") + if best_idx is None or best_score < min_score: + print(f" [撤单] 未找到 {target_name} {order_type} 单(最高相似度{best_score:.0%} < {min_score:.0%}),跳过撤单") return False print(f" [撤单] 匹配到: {rows[best_idx][0] if best_idx < len(rows) else '(翻页)'} (相似度:{best_score:.0%})") self._navigate_to_row(best_idx) + + # 确认前二次校验:重新OCR目标行,防止误读导致撤错单(仅首页行可校验) + if best_idx < self.config.ORDER_READ_ROWS: + recheck_rows = self._read_first_page() + if best_idx < len(recheck_rows): + re_name, re_type = recheck_rows[best_idx] + re_score = self._match_score(target_words, re_name) if re_name else 0 + if not re_name or target_type not in re_type or re_score < min_score: + print(f" [撤单二次确认] 目标行复核失败(识别为'{re_name}' {re_type},相似度{re_score:.0%}),放弃撤单") + return False + self._confirm_cancel() print(f" [撤单] 已取消 {target_name} {order_type} 单") self._navigate_back_after_cancel(best_idx) @@ -483,23 +496,26 @@ class OrderOperator: if region == (0, 0, 0, 0): print(f" [验证] DETAIL_SUCCESS_REGION 未配置,跳过") return True - try: - img = OcrHelper.grab_region(region) - gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - cv2.imwrite("debug_verify_success.png", img) - output = self.price_checker.ocr.ocr(gray) - txts = OcrHelper.extract_txts(output) - text = " ".join(txts).strip() - if txts: - print(f" [验证] OCR原始结果: {txts}") - else: - print(f" [验证] OCR无输出") - expected = self.config.DETAIL_SUCCESS_TEXT.lower() - if expected in text.lower(): - print(f" [验证] ✗ 仍检测到'{self.config.DETAIL_SUCCESS_TEXT}',挂单未成功") - return False - print(f" [验证] ✓ 页面已跳转,挂单成功") - return True - except Exception as e: - print(f" [验证] 成功检测异常: {e},默认通过") - return True \ No newline at end of file + expected = self.config.DETAIL_SUCCESS_TEXT.lower() + # OCR无输出/异常时重试;全部失败则保守判定为未成功,宁可漏记不可幻挂 + for attempt in range(self.config.VERIFY_OCR_RETRY): + try: + img = OcrHelper.grab_region(region) + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + cv2.imwrite("debug_verify_success.png", img) + output = self.price_checker.ocr.ocr(gray) + txts = OcrHelper.extract_txts(output) + text = " ".join(txts).strip() + if txts: + print(f" [验证] OCR原始结果: {txts}") + if expected in text.lower(): + print(f" [验证] ✗ 仍检测到'{self.config.DETAIL_SUCCESS_TEXT}',挂单未成功") + return False + print(f" [验证] ✓ 页面已跳转,挂单成功") + return True + print(f" [验证] OCR无输出(第{attempt + 1}/{self.config.VERIFY_OCR_RETRY}次),重试...") + except Exception as e: + print(f" [验证] 成功检测异常(第{attempt + 1}/{self.config.VERIFY_OCR_RETRY}次): {e},重试...") + time.sleep(0.5) + print(f" [验证] {self.config.VERIFY_OCR_RETRY}次OCR均无有效结果,保守判定为未成功") + return False \ No newline at end of file diff --git a/Frame/runner.py b/Frame/runner.py index 59859e2..db3b467 100644 --- a/Frame/runner.py +++ b/Frame/runner.py @@ -78,6 +78,18 @@ class BotRunner: return False coins = self.ocr.read_coins(self.config.COINS_REGION) + + # 跳变校验:与上次余额偏差过大时重读确认,防止OCR误读被采信 + if self.current_coins > 0 and coins > 0: + if abs(coins - self.current_coins) > self.config.COINS_JUMP_THRESHOLD: + print(f"[金币校验] 读数 {coins} 与上次 {self.current_coins} 偏差过大,重读确认...") + recheck = self.ocr.read_coins(self.config.COINS_REGION) + if recheck > 0 and abs(recheck - coins) <= max(1, int(coins * 0.01)): + print(f"[金币校验] 重读一致({recheck}),采信新值") + else: + print(f"[金币校验] 重读不一致({recheck}),丢弃本次读数,沿用 {self.current_coins}") + return False + self.current_coins = coins print(f"[金币] 当前余额: {coins}") @@ -166,6 +178,13 @@ class BotRunner: order_rows = self.order_manager._read_all_rows() self.ctrl.back_to_list_from_orders() + # 对账保护:OCR整页失败时跳过,防止本地账本被误清空 + valid_rows = [(n, t) for n, t in order_rows if n] + local_total = self.active_buy.get_total_qty() + self.active_sell.get_total_qty() + if not valid_rows and local_total > 0: + print(f"[对账保护] 订单页未识别到任何行,但本地账本有 {local_total} 个订单,跳过本轮对账") + return + actual_counts = {"buy": {}, "sell": {}} for name, otype in order_rows: if not name: @@ -506,47 +525,57 @@ class BotRunner: # ========== 收集需要撤单重挂的商品 ========== to_cancel_and_reorder = [] - # 遍历求购单 + # 遍历求购单(双向跟价:跟涨才能保持价头可成交,跟跌可降低成本) for key, order in self.active_buy.get_all().items(): name = order["name"] if name not in price_map: + print(f" [求购跟进] {name}: 未获取到最新价格,跳过") continue latest = price_map[name] ok, new_my_bid, _ = self.filter.calc_prices(latest["market_bid"], latest["market_ask"]) if not ok: + print(f" [求购跟进] {name}: 新价差不满足利润条件(市场bid={latest['market_bid']} ask={latest['market_ask']}),暂不调整") continue - if new_my_bid == order["price"] or new_my_bid >= order["price"]: + # 市场买价等于自己挂单价时,大概率是自己的单在顶价,跳过防止自我抬价 + if latest["market_bid"] == order["price"]: continue - if abs(new_my_bid - order["price"]) >= self.config.PRICE_UPDATE_THRESHOLD: - print(f" [求购跟进] {name}: 挂单价{order['price']}->{new_my_bid}(市场{latest['market_bid']})") - updated = order.copy() - updated["my_bid"] = new_my_bid - updated["price"] = new_my_bid - updated["market_bid"] = latest["market_bid"] - updated["market_ask"] = latest["market_ask"] - qty = order.get("qty", 1) - to_cancel_and_reorder.append((name, updated, "buy", qty)) - - # 遍历寄售单 + if abs(new_my_bid - order["price"]) < self.config.PRICE_UPDATE_THRESHOLD: + continue + direction = "跟涨" if new_my_bid > order["price"] else "跟跌" + print(f" [求购跟进-{direction}] {name}: 挂单价{order['price']}->{new_my_bid}(市场{latest['market_bid']})") + updated = order.copy() + updated["my_bid"] = new_my_bid + updated["price"] = new_my_bid + updated["market_bid"] = latest["market_bid"] + updated["market_ask"] = latest["market_ask"] + qty = order.get("qty", 1) + to_cancel_and_reorder.append((name, updated, "buy", qty)) + + # 遍历寄售单(双向跟价:跟跌才能保持最低价可成交,跟涨可提升利润) for key, order in self.active_sell.get_all().items(): name = order["name"] if name not in price_map: + print(f" [卖出跟进] {name}: 未获取到最新价格,跳过") continue latest = price_map[name] ok, _, new_my_ask = self.filter.calc_prices(latest["market_bid"], latest["market_ask"]) if not ok: + print(f" [卖出跟进] {name}: 新价差不满足利润条件(市场bid={latest['market_bid']} ask={latest['market_ask']}),暂不调整") + continue + # 市场卖价等于自己挂单价时,大概率是自己的单在最低价,跳过防止自我压价 + if latest["market_ask"] == order["price"]: continue - if new_my_ask == order["price"] or new_my_ask <= order["price"]: + if abs(new_my_ask - order["price"]) < self.config.PRICE_UPDATE_THRESHOLD: continue - if abs(new_my_ask - order["price"]) >= self.config.PRICE_UPDATE_THRESHOLD: - print(f" [卖出跟进] {name}: 挂单价{order['price']}->{new_my_ask}(市场{latest['market_ask']})") - updated = order.copy() - updated["my_ask"] = new_my_ask - updated["price"] = new_my_ask - updated["market_bid"] = latest["market_bid"] - updated["market_ask"] = latest["market_ask"] - qty = order.get("qty", 1) - to_cancel_and_reorder.append((name, updated, "sell", qty)) + direction = "跟跌" if new_my_ask < order["price"] else "跟涨" + print(f" [卖出跟进-{direction}] {name}: 挂单价{order['price']}->{new_my_ask}(市场{latest['market_ask']})") + updated = order.copy() + updated["my_ask"] = new_my_ask + updated["price"] = new_my_ask + updated["market_bid"] = latest["market_bid"] + updated["market_ask"] = latest["market_ask"] + qty = order.get("qty", 1) + to_cancel_and_reorder.append((name, updated, "sell", qty)) # ========== 执行撤单重挂 ========== if to_cancel_and_reorder: @@ -554,16 +583,22 @@ class BotRunner: self.ctrl.open_active_orders() time.sleep(1.0) + # 只有实际撤单成功的才清账本并重挂,重挂数量以实际撤销数为准 + reorder_list = [] for name, updated, otype, qty in to_cancel_and_reorder: cancelled = self.order_manager.cancel_all_orders(name, otype) + if cancelled == 0: + print(f" [跟价] {name} 撤单失败(订单页未匹配到),保留账本,本轮不重挂") + continue if otype == "buy": self.active_buy.remove_all(name, "buy") else: self.active_sell.remove_all(name, "sell") print(f" 已取消 {name} 的 {cancelled} 个{otype}订单") + reorder_list.append((name, updated, otype, cancelled)) self.ctrl.back_to_list_from_orders() - for name, updated, otype, qty in to_cancel_and_reorder: + for name, updated, otype, qty in reorder_list: for i in range(qty): if self._get_total_orders() >= self.config.MAX_TOTAL_ORDERS: print(f" [订单上限] 已达上限,停止重挂") -- Gitee