Fix break-even: enable when SL placeable; clearer hints; verify SLTP.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-21 09:14:01 +08:00
parent 26a3c760db
commit 9d39632a48
2 changed files with 100 additions and 36 deletions
+100 -36
View File
@@ -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
Binary file not shown.