diff --git a/desktop/gold_order_app.py b/desktop/gold_order_app.py index 1792b14..de6ff03 100644 --- a/desktop/gold_order_app.py +++ b/desktop/gold_order_app.py @@ -756,8 +756,10 @@ class Mt5Bridge: } return int(retcode) in ok - def modify_position_sltp(self, ticket: int, sl: float, tp: Optional[float] = None) -> float: - """修改持仓止损/止盈;成功返回实际止损价,失败抛 TradeError。""" + def modify_position_sltp( + self, ticket: int, sl: float, tp: Optional[float] = None + ) -> Tuple[float, float]: + """修改持仓止损/止盈;成功返回 (实际止损, 实际止盈),失败抛 TradeError。""" p = self.get_position(int(ticket)) symbol = str(p.symbol) self.ensure_symbol(symbol) @@ -771,15 +773,19 @@ class Mt5Bridge: raise TradeError("账户不允许交易") digits = int(self.symbol_info(symbol).digits) + tol = max(10 ** (-digits), 0.01) sl = round(float(sl), digits) + old_sl = float(p.sl) + old_tp = float(p.tp) if tp is None: - tp_val = float(p.tp) + tp_val = old_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 也分开试 + # 目标止盈与当前相同 → 视为「只改止损」,可尝试 TP=0 兜底(部分券商) + keep_tp = abs(tp_val - old_tp) <= tol or (tp_val <= 1e-12 and old_tp <= 1e-12) + variants: list[dict] = [] base = { "action": mt5.TRADE_ACTION_SLTP, @@ -789,9 +795,8 @@ class Mt5Bridge: } 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 - # 去重 + if keep_tp and tp_val > 1e-12: + variants.append({**base, "tp": 0.0}) # 仅改 SL 时的兜底,勿用于改 TP seen = set() uniq: list[dict] = [] for req in variants: @@ -801,6 +806,11 @@ class Mt5Bridge: seen.add(key) uniq.append(req) + def _match(a: float, b: float) -> bool: + if b <= 1e-12: + return a <= 1e-12 + return abs(a - b) <= tol + last_detail = "" for request in uniq: check = mt5.order_check(request) @@ -809,7 +819,6 @@ class Mt5Bridge: 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()) @@ -817,20 +826,28 @@ class Mt5Bridge: 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) + cur_sl = float(p2.sl) + cur_tp = float(p2.tp) + # 兜底请求把 TP 清零时,只核验止损 + req_tp = float(request.get("tp") or 0) + if keep_tp and req_tp <= 1e-12: + if _match(cur_sl, sl): + return cur_sl, cur_tp + elif _match(cur_sl, sl) and _match(cur_tp, tp_val): + return cur_sl, cur_tp + p3 = self.get_position(int(ticket)) last_detail = ( - f"回报成功但止损未变(原 {old_sl:.{digits}f} → 目标 {sl:.{digits}f}," - f"当前仍为 {float(self.get_position(int(ticket)).sl):.{digits}f})" + f"回报成功但未生效(目标 SL {sl:.{digits}f} / TP {tp_val:.{digits}f}," + f"当前 SL {float(p3.sl):.{digits}f} / TP {float(p3.tp):.{digits}f};" + f"原 SL {old_sl:.{digits}f} / TP {old_tp:.{digits}f})" ) - raise TradeError(f"#{ticket} {last_detail or '改止损失败'}") + raise TradeError(f"#{ticket} {last_detail or '改止损/止盈失败'}") def set_stop_loss(self, ticket: int, new_sl: float) -> Tuple[str, dict]: """手动改止损价(与保本无关);保留原止盈。返回 (消息, 上报用 dict)。""" @@ -841,6 +858,7 @@ class Mt5Bridge: min_dist = self.min_stop_distance(symbol) t = self.tick(symbol) old_sl = float(p.sl) + old_tp = float(p.tp) new_sl = round(float(new_sl), digits) if new_sl <= 1e-12: raise TradeError("止损价必须大于 0") @@ -866,10 +884,10 @@ class Mt5Bridge: f"止损过近:距现价 {new_sl - ref:.{digits}f},至少 {min_dist:.{digits}f}" ) - applied = self.modify_position_sltp(int(ticket), new_sl, float(p.tp)) + applied_sl, applied_tp = self.modify_position_sltp(int(ticket), new_sl, old_tp) msg = ( f"#{ticket} 止损已改 " - f"{(f'{old_sl:.{digits}f}' if old_sl > 0 else '无')}→{applied:.{digits}f}" + f"{(f'{old_sl:.{digits}f}' if old_sl > 0 else '无')}→{applied_sl:.{digits}f}" ) report = { "ticket": str(ticket), @@ -881,12 +899,68 @@ class Mt5Bridge: "open_time": float(p.time), "close_time": None, "profit": float(p.profit) + float(p.swap), - "sl": float(applied), - "tp": float(p.tp), + "sl": float(applied_sl), + "tp": float(applied_tp), "snapshot": {"event": "modify_sl"}, } return msg, report + def set_take_profit(self, ticket: int, new_tp: float) -> Tuple[str, dict]: + """手动改止盈价;保留原止损。返回 (消息, 上报用 dict)。""" + p = self.get_position(int(ticket)) + symbol = str(p.symbol) + pos_type = int(p.type) + digits = int(self.symbol_info(symbol).digits) + min_dist = self.min_stop_distance(symbol) + t = self.tick(symbol) + old_sl = float(p.sl) + old_tp = float(p.tp) + new_tp = round(float(new_tp), digits) + if new_tp <= 1e-12: + raise TradeError("止盈价必须大于 0") + + if pos_type == mt5.POSITION_TYPE_BUY: + ref = float(t.bid) + if new_tp <= ref + 1e-12: + raise TradeError( + f"多单止盈须高于现价:现价 {ref:.{digits}f},止盈 {new_tp:.{digits}f}" + ) + if new_tp - ref + 1e-12 < min_dist: + raise TradeError( + f"止盈过近:距现价 {new_tp - ref:.{digits}f},至少 {min_dist:.{digits}f}" + ) + else: + ref = float(t.ask) + if new_tp >= ref - 1e-12: + raise TradeError( + f"空单止盈须低于现价:现价 {ref:.{digits}f},止盈 {new_tp:.{digits}f}" + ) + if ref - new_tp + 1e-12 < min_dist: + raise TradeError( + f"止盈过近:距现价 {ref - new_tp:.{digits}f},至少 {min_dist:.{digits}f}" + ) + + applied_sl, applied_tp = self.modify_position_sltp(int(ticket), old_sl, new_tp) + msg = ( + f"#{ticket} 止盈已改 " + f"{(f'{old_tp:.{digits}f}' if old_tp > 0 else '无')}→{applied_tp:.{digits}f}" + ) + report = { + "ticket": str(ticket), + "symbol": symbol, + "side": "buy" if pos_type == mt5.POSITION_TYPE_BUY else "sell", + "volume": float(p.volume), + "open_price": float(p.price_open), + "close_price": None, + "open_time": float(p.time), + "close_time": None, + "profit": float(p.profit) + float(p.swap), + "sl": float(applied_sl), + "tp": float(applied_tp), + "snapshot": {"event": "modify_tp"}, + } + return msg, report + def break_even_tickets(self, tickets: list, offset_usd: float = 1.0) -> Tuple[str, list]: """保本:多单 SL=开仓价+offset,空单 SL=开仓价-offset。返回 (消息, 成功单号列表)。""" if not tickets: @@ -930,7 +1004,7 @@ class Mt5Bridge: f"#{ticket} 现价 {ref:.{digits}f} 距保本价 {sl:.{digits}f} 过近" ) - new_sl = self.modify_position_sltp(int(ticket), sl, float(p.tp)) + new_sl, _new_tp = 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}" @@ -1310,7 +1384,7 @@ class OrderApp(tk.Tk): text=( f"最多同时 {MAX_OPEN_POSITIONS} 笔 · 同向≤{MAX_SIDE_LOTS:.2f} 手 · " f"点差>{SPREAD_WARN_USD:.2f} 买卖提示 · " - "亏≥5%或盈≥10%禁止开仓 · 持仓「改止损」可手动设价 · 保本:有利≥3$ 后止损到开仓价±1$" + "亏≥5%或盈≥10%禁止开仓 · 持仓可单独「改止损/改止盈」 · 保本:有利≥3$ 后止损到开仓价±1$" ), bg=COLOR_BG, fg=COLOR_MUTED, @@ -1559,6 +1633,16 @@ class OrderApp(tk.Tk): padx=6, ) + def _mk_edit_tp_btn(self, parent, ticket: int) -> tk.Frame: + """手动改止盈(保留原止损)。""" + return self._mk_outline_btn( + parent, + "改止盈", + COLOR_ACCENT, + lambda t=ticket: self._edit_tp_now(t), + padx=6, + ) + @staticmethod def _pos_profit(p) -> float: return float(p.profit) + float(p.swap) @@ -2185,6 +2269,7 @@ class OrderApp(tk.Tk): btn_wrap = tk.Frame(row, bg=bg) btn_wrap.grid(row=0, column=len(cells), sticky="nsew", padx=4, pady=3) self._mk_edit_sl_btn(btn_wrap, ticket).pack(side=tk.LEFT, padx=(0, 4)) + self._mk_edit_tp_btn(btn_wrap, ticket).pack(side=tk.LEFT, padx=(0, 4)) self._mk_close_btn(btn_wrap, ticket).pack(side=tk.LEFT) row.bind("", lambda e, t=ticket: self._toggle_ticket_select(t)) @@ -2242,10 +2327,52 @@ class OrderApp(tk.Tk): self.set_status(f"改止损异常:{exc}", ok=False) return - def work(): - self.bridge.connect() - return self.bridge.set_stop_loss(int(ticket), float(new_sl)) + self._run_modify_sltp( + ticket, + lambda: self.bridge.set_stop_loss(int(ticket), float(new_sl)), + err_prefix="改止损", + ) + def _edit_tp_now(self, ticket: int) -> None: + """弹窗输入实际止盈价;保留原止损。""" + try: + self.bridge.connect() + p = self.bridge.get_position(int(ticket)) + digits = int(self.bridge.symbol_info(str(p.symbol)).digits) + open_price = float(p.price_open) + cur_tp = float(p.tp) + side = "多" if int(p.type) == mt5.POSITION_TYPE_BUY else "空" + cur_txt = f"{cur_tp:.{digits}f}" if cur_tp > 0 else "无" + prompt = ( + f"单号 #{ticket}({side})\n" + f"开仓价 {open_price:.{digits}f}\n" + f"当前止盈 {cur_txt}\n\n" + f"请输入新的止盈价(保留原止损,按实际价格设置):" + ) + initial = cur_tp if cur_tp > 0 else open_price + new_tp = simpledialog.askfloat( + "改止盈", + prompt, + initialvalue=round(initial, digits), + minvalue=0.01, + parent=self, + ) + if new_tp is None: + return + except TradeError as exc: + self.set_status(str(exc), ok=False) + return + except Exception as exc: # noqa: BLE001 + self.set_status(f"改止盈异常:{exc}", ok=False) + return + + self._run_modify_sltp( + ticket, + lambda: self.bridge.set_take_profit(int(ticket), float(new_tp)), + err_prefix="改止盈", + ) + + def _run_modify_sltp(self, ticket: int, work, err_prefix: str) -> None: def after_ok(msg: str) -> None: self.set_status(msg, ok=True) self._pos_sig = None @@ -2258,13 +2385,16 @@ class OrderApp(tk.Tk): def runner(): try: + self.bridge.connect() msg, report = work() self.after(0, lambda m=msg: after_ok(m)) self.after(0, lambda r=report: self._hub_report(r, with_bars=False)) 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)) + self.after( + 0, lambda: self.set_status(f"{err_prefix}异常:{exc}", ok=False) + ) finally: self.after(0, self._done) diff --git a/releases/黄金市价辅助.exe b/releases/黄金市价辅助.exe index e9589fa..7da7695 100644 Binary files a/releases/黄金市价辅助.exe and b/releases/黄金市价辅助.exe differ