Add per-position manual stop-loss edit, separate from break-even.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-21 16:50:33 +08:00
parent 1714732b10
commit 229f0351e0
2 changed files with 149 additions and 11 deletions
+149 -11
View File
@@ -831,6 +831,61 @@ class Mt5Bridge:
)
raise TradeError(f"#{ticket} {last_detail or '改止损失败'}")
def set_stop_loss(self, ticket: int, new_sl: 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)
new_sl = round(float(new_sl), digits)
if new_sl <= 1e-12:
raise TradeError("止损价必须大于 0")
if pos_type == mt5.POSITION_TYPE_BUY:
ref = float(t.bid)
if new_sl >= ref - 1e-12:
raise TradeError(
f"多单止损须低于现价:现价 {ref:.{digits}f},止损 {new_sl:.{digits}f}"
)
if ref - new_sl + 1e-12 < min_dist:
raise TradeError(
f"止损过近:距现价 {ref - new_sl:.{digits}f},至少 {min_dist:.{digits}f}"
)
else:
ref = float(t.ask)
if new_sl <= ref + 1e-12:
raise TradeError(
f"空单止损须高于现价:现价 {ref:.{digits}f},止损 {new_sl:.{digits}f}"
)
if new_sl - ref + 1e-12 < min_dist:
raise TradeError(
f"止损过近:距现价 {new_sl - ref:.{digits}f},至少 {min_dist:.{digits}f}"
)
applied = self.modify_position_sltp(int(ticket), new_sl, float(p.tp))
msg = (
f"#{ticket} 止损已改 "
f"{(f'{old_sl:.{digits}f}' if old_sl > 0 else '无')}→{applied:.{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),
"tp": float(p.tp),
"snapshot": {"event": "modify_sl"},
}
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:
@@ -1204,7 +1259,7 @@ class OrderApp(tk.Tk):
("止损", 12),
("止盈", 12),
("浮盈", 9),
("操作", 10),
("操作", 16),
]
# 持仓区固定为 5 行可视高度,超出可滚动
pos_wrap = tk.Frame(
@@ -1253,7 +1308,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,
@@ -1464,27 +1519,44 @@ class OrderApp(tk.Tk):
except Exception:
pass
def _mk_close_btn(self, parent, ticket: int) -> tk.Frame:
"""红色边框的平仓按钮。"""
border = tk.Frame(parent, bg=COLOR_DANGER, padx=1, pady=1)
def _mk_outline_btn(
self, parent, text: str, color: str, command, padx: int = 8
) -> tk.Frame:
border = tk.Frame(parent, bg=color, padx=1, pady=1)
btn = tk.Button(
border,
text="平仓",
command=lambda t=ticket: self._close_ticket_now(t),
fg=COLOR_DANGER,
text=text,
command=command,
fg=color,
bg="#2b2b2b",
activeforeground="#ffffff",
activebackground="#a02828",
activebackground=color,
relief=tk.FLAT,
bd=0,
font=("Microsoft YaHei UI", 10, "bold"),
padx=10,
padx=padx,
pady=2,
cursor="hand2",
)
btn.pack()
return border
def _mk_close_btn(self, parent, ticket: int) -> tk.Frame:
"""红色边框的平仓按钮。"""
return self._mk_outline_btn(
parent, "平仓", COLOR_DANGER, lambda t=ticket: self._close_ticket_now(t)
)
def _mk_edit_sl_btn(self, parent, ticket: int) -> tk.Frame:
"""手动改止损(与保本独立)。"""
return self._mk_outline_btn(
parent,
"改止损",
COLOR_ACCENT,
lambda t=ticket: self._edit_sl_now(t),
padx=6,
)
@staticmethod
def _pos_profit(p) -> float:
return float(p.profit) + float(p.swap)
@@ -2106,7 +2178,8 @@ 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_close_btn(btn_wrap, ticket).pack(anchor="center")
self._mk_edit_sl_btn(btn_wrap, ticket).pack(side=tk.LEFT, padx=(0, 4))
self._mk_close_btn(btn_wrap, ticket).pack(side=tk.LEFT)
row.bind("<Button-1>", lambda e, t=ticket: self._toggle_ticket_select(t))
if not live:
@@ -2130,6 +2203,71 @@ class OrderApp(tk.Tk):
self._run_async(work)
def _edit_sl_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_sl = float(p.sl)
side = "多" if int(p.type) == mt5.POSITION_TYPE_BUY else "空"
cur_txt = f"{cur_sl:.{digits}f}" if cur_sl > 0 else "无"
prompt = (
f"单号 #{ticket}({side})\n"
f"开仓价 {open_price:.{digits}f}\n"
f"当前止损 {cur_txt}\n\n"
f"请输入新的止损价(与保本无关,按实际价格设置):"
)
initial = cur_sl if cur_sl > 0 else open_price
new_sl = simpledialog.askfloat(
"改止损",
prompt,
initialvalue=round(initial, digits),
minvalue=0.01,
parent=self,
)
if new_sl 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
def work():
self.bridge.connect()
return self.bridge.set_stop_loss(int(ticket), float(new_sl))
def after_ok(msg: str) -> None:
self.set_status(msg, ok=True)
self._pos_sig = None
try:
symbol = self._symbol()
d = int(self.bridge.symbol_info(symbol).digits)
self._refresh_positions(symbol, d)
except Exception:
pass
def runner():
try:
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))
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 on_buy(self) -> None:
self._order(mt5.ORDER_TYPE_BUY, "买入")
Binary file not shown.