Fix break-even: enable when SL placeable; clearer hints; verify SLTP.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+100
-36
@@ -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.
Reference in New Issue
Block a user