diff --git a/desktop/gold_order_app.py b/desktop/gold_order_app.py index 84264ad..ff994c1 100644 --- a/desktop/gold_order_app.py +++ b/desktop/gold_order_app.py @@ -73,9 +73,8 @@ COLOR_CARD2 = "#22222e" COLOR_MUTED = "#8a8a9c" COLOR_TEXT = "#f0f0f5" COLOR_ACCENT = "#4cc2ff" +BE_MIN_MOVE_USD = 3.0 # 价格向有利方向走出该美金距离才可保本(非账户浮盈) BE_OFFSET_USD = 1.0 # 保本止损 = 开仓价 ± 1$ -# 有利方向至少走出 offset + 缓冲,保证新止损相对现价合法(不再用固定 3$ 门槛) -BE_EXTRA_BUFFER_USD = 0.20 SPREAD_WARN_USD = 0.50 # 点差超过该美金时,买卖按钮区提示(不禁开) @@ -652,84 +651,146 @@ class Mt5Bridge: } return f"已平 #{ticket}({volume:.2f} 手)", report + def _sltp_retcode_ok(self, retcode: int) -> bool: + ok = { + mt5.TRADE_RETCODE_DONE, + getattr(mt5, "TRADE_RETCODE_DONE_PARTIAL", -1), + getattr(mt5, "TRADE_RETCODE_PLACED", -1), + } + return int(retcode) in ok + + def modify_position_sltp(self, ticket: int, sl: float, tp: Optional[float] = None) -> float: + """修改持仓止损/止盈;成功返回实际止损价,失败抛 TradeError。""" + p = self.get_position(int(ticket)) + symbol = str(p.symbol) + self.ensure_symbol(symbol) + term = mt5.terminal_info() + if term is None or not term.connected: + raise TradeError("终端未连接到交易服务器") + if not getattr(term, "trade_allowed", True): + raise TradeError("终端未开启自动交易(请勾选 Algo Trading)") + acc = mt5.account_info() + if acc is None or not getattr(acc, "trade_allowed", True): + raise TradeError("账户不允许交易") + + digits = int(self.symbol_info(symbol).digits) + sl = round(float(sl), digits) + if tp is None: + tp_val = float(p.tp) + else: + tp_val = float(tp) + tp_val = round(tp_val, digits) if tp_val > 1e-12 else 0.0 + old_sl = float(p.sl) + + # 多组请求:部分券商带 magic / 不带 magic 表现不同;TP=0 与保留原 TP 也分开试 + variants: list[dict] = [] + base = { + "action": mt5.TRADE_ACTION_SLTP, + "symbol": symbol, + "position": int(ticket), + "sl": sl, + } + variants.append({**base, "tp": tp_val}) + variants.append({**base, "tp": tp_val, "magic": MAGIC}) + if tp_val > 1e-12: + variants.append({**base, "tp": 0.0}) # 仅改 SL + # 去重 + seen = set() + uniq: list[dict] = [] + for req in variants: + key = (req.get("sl"), req.get("tp"), req.get("magic")) + if key in seen: + continue + seen.add(key) + uniq.append(req) + + last_detail = "" + for request in uniq: + check = mt5.order_check(request) + if check is not None and int(getattr(check, "retcode", 0)) not in ( + 0, + mt5.TRADE_RETCODE_DONE, + ): + last_detail = f"校验失败 {check.comment} ({check.retcode})" + # 校验失败仍尝试 send,部分终端 check 偏严 + result = mt5.order_send(request) + if result is None: + last_detail = str(mt5.last_error()) + continue + if not self._sltp_retcode_ok(result.retcode): + last_detail = f"{result.comment} ({result.retcode})" + continue + # 回读确认(券商可能延迟) + for _ in range(8): + time.sleep(0.2) + try: + p2 = self.get_position(int(ticket)) + except TradeError: + continue + if abs(float(p2.sl) - sl) <= max(10 ** (-digits), 0.01): + return float(p2.sl) + last_detail = ( + f"回报成功但止损未变(原 {old_sl:.{digits}f} → 目标 {sl:.{digits}f}," + f"当前仍为 {float(self.get_position(int(ticket)).sl):.{digits}f})" + ) + raise TradeError(f"#{ticket} {last_detail or '改止损失败'}") + def break_even_tickets(self, tickets: list, offset_usd: float = 1.0) -> Tuple[str, list]: """保本:多单 SL=开仓价+offset,空单 SL=开仓价-offset。返回 (消息, 成功单号列表)。""" if not tickets: raise TradeError("没有可保本的持仓") ok_tickets: list = [] errors: list = [] - done_ok = { - mt5.TRADE_RETCODE_DONE, - getattr(mt5, "TRADE_RETCODE_DONE_PARTIAL", -1), - getattr(mt5, "TRADE_RETCODE_PLACED", -1), - } + details: list = [] for ticket in tickets: try: p = self.get_position(int(ticket)) symbol = str(p.symbol) - term = mt5.terminal_info() - if term is None or not term.connected: - raise TradeError("终端未连接到交易服务器") - if not term.trade_allowed: - raise TradeError("终端未开启自动交易(请勾选 Algo Trading)") - acc = mt5.account_info() - if acc is None or not acc.trade_allowed: - raise TradeError("账户不允许交易") pos_type = int(p.type) open_price = float(p.price_open) - tp = float(p.tp) if float(p.tp) > 0 else 0.0 digits = int(self.symbol_info(symbol).digits) min_dist = self.min_stop_distance(symbol) t = self.tick(symbol) + old_sl = float(p.sl) if pos_type == mt5.POSITION_TYPE_BUY: sl = round(open_price + offset_usd, digits) ref = float(t.bid) - need = sl + max(min_dist, 0.0) - if ref + 1e-12 < need: + move = ref - open_price + if move + 1e-12 < BE_MIN_MOVE_USD: raise TradeError( - f"#{ticket} 多单现价 {ref:.{digits}f},需高于 {need:.{digits}f} " - f"才能挂保本止损 {sl:.{digits}f}" + f"#{ticket} 有利方向仅 {move:.2f}$,需 ≥ {BE_MIN_MOVE_USD:.0f}$" + ) + if sl + max(min_dist, 0.0) > ref + 1e-12: + raise TradeError( + f"#{ticket} 现价 {ref:.{digits}f} 距保本价 {sl:.{digits}f} 过近" ) else: sl = round(open_price - offset_usd, digits) ref = float(t.ask) - need = sl - max(min_dist, 0.0) - if ref - 1e-12 > need: + move = open_price - ref + if move + 1e-12 < BE_MIN_MOVE_USD: raise TradeError( - f"#{ticket} 空单现价 {ref:.{digits}f},需低于 {need:.{digits}f} " - f"才能挂保本止损 {sl:.{digits}f}" + f"#{ticket} 有利方向仅 {move:.2f}$,需 ≥ {BE_MIN_MOVE_USD:.0f}$" + ) + if ref > sl - max(min_dist, 0.0) + 1e-12: + raise TradeError( + f"#{ticket} 现价 {ref:.{digits}f} 距保本价 {sl:.{digits}f} 过近" ) - request = { - "action": mt5.TRADE_ACTION_SLTP, - "symbol": symbol, - "position": int(ticket), - "sl": sl, - "tp": tp, - "magic": MAGIC, - } - result = mt5.order_send(request) - if result is None: - raise TradeError(f"#{ticket} {mt5.last_error()}") - if int(result.retcode) not in done_ok: - raise TradeError(f"#{ticket} {result.comment} ({result.retcode})") - # 回读确认止损已改(部分券商异步) - time.sleep(0.15) - p2 = self.get_position(int(ticket)) - if abs(float(p2.sl) - sl) > 10 ** (-digits): - raise TradeError( - f"#{ticket} 服务器未接受保本止损(当前 SL={float(p2.sl):.{digits}f},目标 {sl:.{digits}f})" - ) + new_sl = self.modify_position_sltp(int(ticket), sl, float(p.tp)) ok_tickets.append(int(ticket)) + details.append( + f"#{ticket} SL {old_sl:.{digits}f}→{new_sl:.{digits}f}" + ) except TradeError as exc: errors.append(str(exc)) if not ok_tickets: raise TradeError("保本失败:" + (";".join(errors[:3]) if errors else "无可用持仓")) - msg = f"已保本 {len(ok_tickets)} 笔(开仓价 ± {offset_usd:.0f} 美金)" + msg = f"已保本 {len(ok_tickets)} 笔:" + ",".join(details) if errors: - msg += f";跳过 {len(errors)}:{errors[0]}" + msg += f";失败 {len(errors)}:{errors[0]}" return msg, ok_tickets def close_positions(self, symbol: str, pos_type: Optional[int] = None): @@ -1088,7 +1149,7 @@ class OrderApp(tk.Tk): text=( f"最多同时 {MAX_OPEN_POSITIONS} 笔 · 同向≤{MAX_SIDE_LOTS:.2f} 手 · " f"点差>{SPREAD_WARN_USD:.2f} 买卖提示 · " - "亏≥5%或盈≥10%禁止开仓 · 保本:有利方向足够后止损到开仓价±1$(看价格,非账户浮盈)" + "亏≥5%或盈≥10%禁止开仓 · 保本:价格有利≥3$ 后止损到开仓价±1$(看价格距离,非账户浮盈)" ), bg=COLOR_BG, fg=COLOR_MUTED, @@ -1337,26 +1398,16 @@ class OrderApp(tk.Tk): return float(tick.bid) - open_price return open_price - float(tick.ask) - def _be_needed_move_usd(self, symbol: str) -> float: - """保本所需的最小有利价格距离(美金)。""" - try: - min_dist = self.bridge.min_stop_distance(symbol) - except Exception: - min_dist = 0.0 - return BE_OFFSET_USD + max(float(min_dist), 0.0) + BE_EXTRA_BUFFER_USD - def _be_candidate_tickets(self, positions, tick=None) -> list: - """现价已能挂开仓价±1$ 止损、且尚未保本的单号。""" + """价格朝有利方向≥3$ 且尚未保本的单号。""" tickets = [] if not positions: return tickets - symbol = str(positions[0].symbol) if tick is None: try: - tick = self.bridge.tick(symbol) + tick = self.bridge.tick(str(positions[0].symbol)) except Exception: return tickets - need = self._be_needed_move_usd(symbol) for p in positions: ticket = int(p.ticket) if ticket in self._be_done_tickets: @@ -1364,7 +1415,7 @@ class OrderApp(tk.Tk): if self._is_sl_at_breakeven(p): self._be_done_tickets.add(ticket) continue - if self._favorable_move_usd(p, tick) + 1e-12 >= need: + if self._favorable_move_usd(p, tick) + 1e-12 >= BE_MIN_MOVE_USD: tickets.append(ticket) return tickets @@ -1378,7 +1429,6 @@ class OrderApp(tk.Tk): tick = self.bridge.tick(symbol) except Exception as exc: return f"无法读报价:{exc}" - need = self._be_needed_move_usd(symbol) best = -1e9 for p in positions: ticket = int(p.ticket) @@ -1387,11 +1437,11 @@ class OrderApp(tk.Tk): best = max(best, self._favorable_move_usd(p, tick)) if best < -1e8: return "持仓已保本或无需再拉" - lack = need - best + lack = BE_MIN_MOVE_USD - best if lack > 1e-8: return ( - f"有利方向仅 {best:.2f}$,保本需 ≥ {need:.2f}$" - f"(开仓价±{BE_OFFSET_USD:.0f}$),还差 {lack:.2f}$" + f"有利方向仅 {best:.2f}$,保本需 ≥ {BE_MIN_MOVE_USD:.0f}$" + f"(之后止损拉到开仓价±{BE_OFFSET_USD:.0f}$),还差 {lack:.2f}$" ) return "可保本" @@ -1920,7 +1970,32 @@ class OrderApp(tk.Tk): self._be_done_tickets.update(done) return msg - self._run_async(work) + def after_ok(msg: str) -> None: + self.set_status(msg, ok=True) + # 强制重绘持仓行,避免止损列仍显示旧值 + self._pos_sig = None + try: + digits = int(self.bridge.symbol_info(symbol).digits) + except Exception: + digits = 3 + self._refresh_positions(symbol, digits) + + def runner(): + try: + msg = work() + self.after(0, lambda m=msg: after_ok(m)) + except TradeError as exc: + self.after(0, lambda: self.set_status(str(exc), ok=False)) + except Exception as exc: # noqa: BLE001 + self.after(0, lambda: self.set_status(f"保本异常:{exc}", ok=False)) + finally: + self.after(0, self._done) + + if self._busy: + self.set_status("上一次操作还未完成", ok=False) + return + self._busy = True + threading.Thread(target=runner, daemon=True).start() def _hub_heartbeat(self) -> None: """约每 2 秒 ping 中控,供总览秒级在线状态。""" diff --git a/releases/黄金市价辅助.exe b/releases/黄金市价辅助.exe index 953e37f..d621006 100644 Binary files a/releases/黄金市价辅助.exe and b/releases/黄金市价辅助.exe differ