diff --git a/desktop/gold_order_app.py b/desktop/gold_order_app.py index b47ada9..84264ad 100644 --- a/desktop/gold_order_app.py +++ b/desktop/gold_order_app.py @@ -73,8 +73,9 @@ COLOR_CARD2 = "#22222e" COLOR_MUTED = "#8a8a9c" COLOR_TEXT = "#f0f0f5" COLOR_ACCENT = "#4cc2ff" -BE_MIN_MOVE_USD = 3.0 # 价格向有利方向走出该美金距离才可保本(非账户浮盈) -BE_OFFSET_USD = 1.0 # 保本止损 = 开仓价 ± 1$ +BE_OFFSET_USD = 1.0 # 保本止损 = 开仓价 ± 1$ +# 有利方向至少走出 offset + 缓冲,保证新止损相对现价合法(不再用固定 3$ 门槛) +BE_EXTRA_BUFFER_USD = 0.20 SPREAD_WARN_USD = 0.50 # 点差超过该美金时,买卖按钮区提示(不禁开) @@ -657,13 +658,26 @@ class Mt5Bridge: 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), + } 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) + 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) @@ -671,21 +685,21 @@ class Mt5Bridge: if pos_type == mt5.POSITION_TYPE_BUY: sl = round(open_price + offset_usd, digits) ref = float(t.bid) - if sl >= ref - 1e-12: + need = sl + max(min_dist, 0.0) + if ref + 1e-12 < need: raise TradeError( - f"#{ticket} 多单现价 {ref:.{digits}f} 尚未高于保本价 {sl:.{digits}f}" + f"#{ticket} 多单现价 {ref:.{digits}f},需高于 {need:.{digits}f} " + f"才能挂保本止损 {sl:.{digits}f}" ) - if abs(ref - sl) + 1e-12 < min_dist: - raise TradeError(f"#{ticket} 保本止损过近,至少 {min_dist:.2f} 美元") else: sl = round(open_price - offset_usd, digits) ref = float(t.ask) - if sl <= ref + 1e-12: + need = sl - max(min_dist, 0.0) + if ref - 1e-12 > need: raise TradeError( - f"#{ticket} 空单现价 {ref:.{digits}f} 尚未低于保本价 {sl:.{digits}f}" + f"#{ticket} 空单现价 {ref:.{digits}f},需低于 {need:.{digits}f} " + f"才能挂保本止损 {sl:.{digits}f}" ) - if abs(sl - ref) + 1e-12 < min_dist: - raise TradeError(f"#{ticket} 保本止损过近,至少 {min_dist:.2f} 美元") request = { "action": mt5.TRADE_ACTION_SLTP, @@ -693,11 +707,20 @@ class Mt5Bridge: "position": int(ticket), "sl": sl, "tp": tp, + "magic": MAGIC, } result = mt5.order_send(request) - if result is None or result.retcode != mt5.TRADE_RETCODE_DONE: - detail = mt5.last_error() if result is None else f"{result.comment} ({result.retcode})" - raise TradeError(f"#{ticket} {detail}") + 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})" + ) ok_tickets.append(int(ticket)) except TradeError as exc: errors.append(str(exc)) @@ -998,6 +1021,7 @@ class OrderApp(tk.Tk): pos_bar, text="持仓记录", bg=COLOR_BG, fg=COLOR_TEXT, font=("Microsoft YaHei UI", 13, "bold"), ).pack(side=tk.LEFT) + self._be_ready = False self.btn_break_even = self._mk_btn(pos_bar, "保本", COLOR_BE, self.on_break_even, h=34, w=90) self.btn_break_even.pack(side=tk.RIGHT, padx=4) self._mk_btn(pos_bar, "打开复盘", "#3a5a80", self.on_open_review, h=34, w=100).pack( @@ -1064,7 +1088,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%禁止开仓 · 保本:有利方向足够后止损到开仓价±1$(看价格,非账户浮盈)" ), bg=COLOR_BG, fg=COLOR_MUTED, @@ -1313,13 +1337,26 @@ 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: - """价格朝有利方向>3$ 且尚未保本的单号;已拉过保本的不再进入。""" + """现价已能挂开仓价±1$ 止损、且尚未保本的单号。""" tickets = [] if not positions: return tickets + symbol = str(positions[0].symbol) if tick is None: - tick = self.bridge.tick(str(positions[0].symbol)) + try: + tick = self.bridge.tick(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: @@ -1327,13 +1364,42 @@ 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) > BE_MIN_MOVE_USD + 1e-8: + if self._favorable_move_usd(p, tick) + 1e-12 >= need: tickets.append(ticket) return tickets + def _be_status_hint(self, positions, tick=None) -> str: + """保本按钮不可用时的说明(含还差多少美金)。""" + if not positions: + return "当前无持仓,无法保本" + symbol = str(positions[0].symbol) + try: + if tick is None: + 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) + if ticket in self._be_done_tickets or self._is_sl_at_breakeven(p): + continue + best = max(best, self._favorable_move_usd(p, tick)) + if best < -1e8: + return "持仓已保本或无需再拉" + lack = need - best + if lack > 1e-8: + return ( + f"有利方向仅 {best:.2f}$,保本需 ≥ {need:.2f}$" + f"(开仓价±{BE_OFFSET_USD:.0f}$),还差 {lack:.2f}$" + ) + return "可保本" + def _set_be_button_enabled(self, enabled: bool) -> None: + """外观区分可点/未就绪;始终 NORMAL,避免灰态点不到。""" if not hasattr(self, "btn_break_even"): return + self._be_ready = bool(enabled) if enabled: self.btn_break_even.configure( state=tk.NORMAL, @@ -1341,22 +1407,24 @@ class OrderApp(tk.Tk): fg="white", activebackground=COLOR_BE, activeforeground="white", - disabledforeground="white", cursor="hand2", ) else: self.btn_break_even.configure( - state=tk.DISABLED, + state=tk.NORMAL, bg=COLOR_BE_OFF, - fg="#888888", - activebackground=COLOR_BE_OFF, - activeforeground="#888888", - disabledforeground="#888888", - cursor="arrow", + fg="#aaaaaa", + activebackground="#666666", + activeforeground="#dddddd", + cursor="hand2", ) def _sync_be_button(self, positions) -> None: - self._set_be_button_enabled(bool(self._be_candidate_tickets(positions))) + try: + ok = bool(self._be_candidate_tickets(positions)) + except Exception: + ok = False + self._set_be_button_enabled(ok) def _hub_upload_bars(self, symbol: str) -> None: if hub_client is None or not is_activated(): @@ -1834,13 +1902,7 @@ class OrderApp(tk.Tk): self._run_async(work) def on_break_even(self) -> None: - """点击一次:寻找价格朝有利方向走出>3$ 且未保本的单,改止损到开仓价±1$。""" - if str(self.btn_break_even["state"]) == str(tk.DISABLED): - self.set_status( - f"没有价格朝有利方向走出超过 {BE_MIN_MOVE_USD:.0f} 美金的可保本持仓", - ok=False, - ) - return + """点击一次:对可保本持仓改止损到开仓价±1$。""" symbol = self._symbol() def work(): @@ -1848,10 +1910,12 @@ class OrderApp(tk.Tk): positions = self.bridge.positions(symbol) tick = self.bridge.tick(symbol) tickets = self._be_candidate_tickets(positions, tick) + selected = self._selected_tickets() + if selected: + picked = [t for t in tickets if t in selected] + tickets = picked if picked else list(selected) if not tickets: - raise TradeError( - f"没有价格朝有利方向走出超过 {BE_MIN_MOVE_USD:.0f} 美金的可保本持仓" - ) + raise TradeError(self._be_status_hint(positions, tick)) msg, done = self.bridge.break_even_tickets(tickets, BE_OFFSET_USD) self._be_done_tickets.update(done) return msg diff --git a/releases/黄金市价辅助.exe b/releases/黄金市价辅助.exe index 211b82b..953e37f 100644 Binary files a/releases/黄金市价辅助.exe and b/releases/黄金市价辅助.exe differ