Add per-position manual stop-loss edit, separate from break-even.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+149
-11
@@ -831,6 +831,61 @@ class Mt5Bridge:
|
|||||||
)
|
)
|
||||||
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)。"""
|
||||||
|
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]:
|
def break_even_tickets(self, tickets: list, offset_usd: float = 1.0) -> Tuple[str, list]:
|
||||||
"""保本:多单 SL=开仓价+offset,空单 SL=开仓价-offset。返回 (消息, 成功单号列表)。"""
|
"""保本:多单 SL=开仓价+offset,空单 SL=开仓价-offset。返回 (消息, 成功单号列表)。"""
|
||||||
if not tickets:
|
if not tickets:
|
||||||
@@ -1204,7 +1259,7 @@ class OrderApp(tk.Tk):
|
|||||||
("止损", 12),
|
("止损", 12),
|
||||||
("止盈", 12),
|
("止盈", 12),
|
||||||
("浮盈", 9),
|
("浮盈", 9),
|
||||||
("操作", 10),
|
("操作", 16),
|
||||||
]
|
]
|
||||||
# 持仓区固定为 5 行可视高度,超出可滚动
|
# 持仓区固定为 5 行可视高度,超出可滚动
|
||||||
pos_wrap = tk.Frame(
|
pos_wrap = tk.Frame(
|
||||||
@@ -1253,7 +1308,7 @@ class OrderApp(tk.Tk):
|
|||||||
text=(
|
text=(
|
||||||
f"最多同时 {MAX_OPEN_POSITIONS} 笔 · 同向≤{MAX_SIDE_LOTS:.2f} 手 · "
|
f"最多同时 {MAX_OPEN_POSITIONS} 笔 · 同向≤{MAX_SIDE_LOTS:.2f} 手 · "
|
||||||
f"点差>{SPREAD_WARN_USD:.2f} 买卖提示 · "
|
f"点差>{SPREAD_WARN_USD:.2f} 买卖提示 · "
|
||||||
"亏≥5%或盈≥10%禁止开仓 · 保本:价格有利≥3$ 后止损到开仓价±1$(看价格距离,非账户浮盈)"
|
"亏≥5%或盈≥10%禁止开仓 · 持仓「改止损」可手动设价 · 保本:有利≥3$ 后止损到开仓价±1$"
|
||||||
),
|
),
|
||||||
bg=COLOR_BG,
|
bg=COLOR_BG,
|
||||||
fg=COLOR_MUTED,
|
fg=COLOR_MUTED,
|
||||||
@@ -1464,27 +1519,44 @@ class OrderApp(tk.Tk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _mk_close_btn(self, parent, ticket: int) -> tk.Frame:
|
def _mk_outline_btn(
|
||||||
"""红色边框的平仓按钮。"""
|
self, parent, text: str, color: str, command, padx: int = 8
|
||||||
border = tk.Frame(parent, bg=COLOR_DANGER, padx=1, pady=1)
|
) -> tk.Frame:
|
||||||
|
border = tk.Frame(parent, bg=color, padx=1, pady=1)
|
||||||
btn = tk.Button(
|
btn = tk.Button(
|
||||||
border,
|
border,
|
||||||
text="平仓",
|
text=text,
|
||||||
command=lambda t=ticket: self._close_ticket_now(t),
|
command=command,
|
||||||
fg=COLOR_DANGER,
|
fg=color,
|
||||||
bg="#2b2b2b",
|
bg="#2b2b2b",
|
||||||
activeforeground="#ffffff",
|
activeforeground="#ffffff",
|
||||||
activebackground="#a02828",
|
activebackground=color,
|
||||||
relief=tk.FLAT,
|
relief=tk.FLAT,
|
||||||
bd=0,
|
bd=0,
|
||||||
font=("Microsoft YaHei UI", 10, "bold"),
|
font=("Microsoft YaHei UI", 10, "bold"),
|
||||||
padx=10,
|
padx=padx,
|
||||||
pady=2,
|
pady=2,
|
||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
)
|
)
|
||||||
btn.pack()
|
btn.pack()
|
||||||
return border
|
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
|
@staticmethod
|
||||||
def _pos_profit(p) -> float:
|
def _pos_profit(p) -> float:
|
||||||
return float(p.profit) + float(p.swap)
|
return float(p.profit) + float(p.swap)
|
||||||
@@ -2106,7 +2178,8 @@ class OrderApp(tk.Tk):
|
|||||||
|
|
||||||
btn_wrap = tk.Frame(row, bg=bg)
|
btn_wrap = tk.Frame(row, bg=bg)
|
||||||
btn_wrap.grid(row=0, column=len(cells), sticky="nsew", padx=4, pady=3)
|
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))
|
row.bind("<Button-1>", lambda e, t=ticket: self._toggle_ticket_select(t))
|
||||||
|
|
||||||
if not live:
|
if not live:
|
||||||
@@ -2130,6 +2203,71 @@ class OrderApp(tk.Tk):
|
|||||||
|
|
||||||
self._run_async(work)
|
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:
|
def on_buy(self) -> None:
|
||||||
self._order(mt5.ORDER_TYPE_BUY, "买入")
|
self._order(mt5.ORDER_TYPE_BUY, "买入")
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user