Fix break-even: enable when SL placeable; clearer hints; verify SLTP.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+99
-35
@@ -73,8 +73,9 @@ COLOR_CARD2 = "#22222e"
|
|||||||
COLOR_MUTED = "#8a8a9c"
|
COLOR_MUTED = "#8a8a9c"
|
||||||
COLOR_TEXT = "#f0f0f5"
|
COLOR_TEXT = "#f0f0f5"
|
||||||
COLOR_ACCENT = "#4cc2ff"
|
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 # 点差超过该美金时,买卖按钮区提示(不禁开)
|
SPREAD_WARN_USD = 0.50 # 点差超过该美金时,买卖按钮区提示(不禁开)
|
||||||
|
|
||||||
|
|
||||||
@@ -657,13 +658,26 @@ class Mt5Bridge:
|
|||||||
raise TradeError("没有可保本的持仓")
|
raise TradeError("没有可保本的持仓")
|
||||||
ok_tickets: list = []
|
ok_tickets: list = []
|
||||||
errors: 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:
|
for ticket in tickets:
|
||||||
try:
|
try:
|
||||||
p = self.get_position(int(ticket))
|
p = self.get_position(int(ticket))
|
||||||
symbol = str(p.symbol)
|
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)
|
pos_type = int(p.type)
|
||||||
open_price = float(p.price_open)
|
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)
|
digits = int(self.symbol_info(symbol).digits)
|
||||||
min_dist = self.min_stop_distance(symbol)
|
min_dist = self.min_stop_distance(symbol)
|
||||||
t = self.tick(symbol)
|
t = self.tick(symbol)
|
||||||
@@ -671,21 +685,21 @@ class Mt5Bridge:
|
|||||||
if pos_type == mt5.POSITION_TYPE_BUY:
|
if pos_type == mt5.POSITION_TYPE_BUY:
|
||||||
sl = round(open_price + offset_usd, digits)
|
sl = round(open_price + offset_usd, digits)
|
||||||
ref = float(t.bid)
|
ref = float(t.bid)
|
||||||
if sl >= ref - 1e-12:
|
need = sl + max(min_dist, 0.0)
|
||||||
|
if ref + 1e-12 < need:
|
||||||
raise TradeError(
|
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:
|
else:
|
||||||
sl = round(open_price - offset_usd, digits)
|
sl = round(open_price - offset_usd, digits)
|
||||||
ref = float(t.ask)
|
ref = float(t.ask)
|
||||||
if sl <= ref + 1e-12:
|
need = sl - max(min_dist, 0.0)
|
||||||
|
if ref - 1e-12 > need:
|
||||||
raise TradeError(
|
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 = {
|
request = {
|
||||||
"action": mt5.TRADE_ACTION_SLTP,
|
"action": mt5.TRADE_ACTION_SLTP,
|
||||||
@@ -693,11 +707,20 @@ class Mt5Bridge:
|
|||||||
"position": int(ticket),
|
"position": int(ticket),
|
||||||
"sl": sl,
|
"sl": sl,
|
||||||
"tp": tp,
|
"tp": tp,
|
||||||
|
"magic": MAGIC,
|
||||||
}
|
}
|
||||||
result = mt5.order_send(request)
|
result = mt5.order_send(request)
|
||||||
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
|
if result is None:
|
||||||
detail = mt5.last_error() if result is None else f"{result.comment} ({result.retcode})"
|
raise TradeError(f"#{ticket} {mt5.last_error()}")
|
||||||
raise TradeError(f"#{ticket} {detail}")
|
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))
|
ok_tickets.append(int(ticket))
|
||||||
except TradeError as exc:
|
except TradeError as exc:
|
||||||
errors.append(str(exc))
|
errors.append(str(exc))
|
||||||
@@ -998,6 +1021,7 @@ class OrderApp(tk.Tk):
|
|||||||
pos_bar, text="持仓记录", bg=COLOR_BG, fg=COLOR_TEXT,
|
pos_bar, text="持仓记录", bg=COLOR_BG, fg=COLOR_TEXT,
|
||||||
font=("Microsoft YaHei UI", 13, "bold"),
|
font=("Microsoft YaHei UI", 13, "bold"),
|
||||||
).pack(side=tk.LEFT)
|
).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 = 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.btn_break_even.pack(side=tk.RIGHT, padx=4)
|
||||||
self._mk_btn(pos_bar, "打开复盘", "#3a5a80", self.on_open_review, h=34, w=100).pack(
|
self._mk_btn(pos_bar, "打开复盘", "#3a5a80", self.on_open_review, h=34, w=100).pack(
|
||||||
@@ -1064,7 +1088,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%禁止开仓 · 保本:有利方向足够后止损到开仓价±1$(看价格,非账户浮盈)"
|
||||||
),
|
),
|
||||||
bg=COLOR_BG,
|
bg=COLOR_BG,
|
||||||
fg=COLOR_MUTED,
|
fg=COLOR_MUTED,
|
||||||
@@ -1313,13 +1337,26 @@ class OrderApp(tk.Tk):
|
|||||||
return float(tick.bid) - open_price
|
return float(tick.bid) - open_price
|
||||||
return open_price - float(tick.ask)
|
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:
|
def _be_candidate_tickets(self, positions, tick=None) -> list:
|
||||||
"""价格朝有利方向>3$ 且尚未保本的单号;已拉过保本的不再进入。"""
|
"""现价已能挂开仓价±1$ 止损、且尚未保本的单号。"""
|
||||||
tickets = []
|
tickets = []
|
||||||
if not positions:
|
if not positions:
|
||||||
return tickets
|
return tickets
|
||||||
|
symbol = str(positions[0].symbol)
|
||||||
if tick is None:
|
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:
|
for p in positions:
|
||||||
ticket = int(p.ticket)
|
ticket = int(p.ticket)
|
||||||
if ticket in self._be_done_tickets:
|
if ticket in self._be_done_tickets:
|
||||||
@@ -1327,13 +1364,42 @@ class OrderApp(tk.Tk):
|
|||||||
if self._is_sl_at_breakeven(p):
|
if self._is_sl_at_breakeven(p):
|
||||||
self._be_done_tickets.add(ticket)
|
self._be_done_tickets.add(ticket)
|
||||||
continue
|
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)
|
tickets.append(ticket)
|
||||||
return tickets
|
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:
|
def _set_be_button_enabled(self, enabled: bool) -> None:
|
||||||
|
"""外观区分可点/未就绪;始终 NORMAL,避免灰态点不到。"""
|
||||||
if not hasattr(self, "btn_break_even"):
|
if not hasattr(self, "btn_break_even"):
|
||||||
return
|
return
|
||||||
|
self._be_ready = bool(enabled)
|
||||||
if enabled:
|
if enabled:
|
||||||
self.btn_break_even.configure(
|
self.btn_break_even.configure(
|
||||||
state=tk.NORMAL,
|
state=tk.NORMAL,
|
||||||
@@ -1341,22 +1407,24 @@ class OrderApp(tk.Tk):
|
|||||||
fg="white",
|
fg="white",
|
||||||
activebackground=COLOR_BE,
|
activebackground=COLOR_BE,
|
||||||
activeforeground="white",
|
activeforeground="white",
|
||||||
disabledforeground="white",
|
|
||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.btn_break_even.configure(
|
self.btn_break_even.configure(
|
||||||
state=tk.DISABLED,
|
state=tk.NORMAL,
|
||||||
bg=COLOR_BE_OFF,
|
bg=COLOR_BE_OFF,
|
||||||
fg="#888888",
|
fg="#aaaaaa",
|
||||||
activebackground=COLOR_BE_OFF,
|
activebackground="#666666",
|
||||||
activeforeground="#888888",
|
activeforeground="#dddddd",
|
||||||
disabledforeground="#888888",
|
cursor="hand2",
|
||||||
cursor="arrow",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _sync_be_button(self, positions) -> None:
|
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:
|
def _hub_upload_bars(self, symbol: str) -> None:
|
||||||
if hub_client is None or not is_activated():
|
if hub_client is None or not is_activated():
|
||||||
@@ -1834,13 +1902,7 @@ class OrderApp(tk.Tk):
|
|||||||
self._run_async(work)
|
self._run_async(work)
|
||||||
|
|
||||||
def on_break_even(self) -> None:
|
def on_break_even(self) -> None:
|
||||||
"""点击一次:寻找价格朝有利方向走出>3$ 且未保本的单,改止损到开仓价±1$。"""
|
"""点击一次:对可保本持仓改止损到开仓价±1$。"""
|
||||||
if str(self.btn_break_even["state"]) == str(tk.DISABLED):
|
|
||||||
self.set_status(
|
|
||||||
f"没有价格朝有利方向走出超过 {BE_MIN_MOVE_USD:.0f} 美金的可保本持仓",
|
|
||||||
ok=False,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
symbol = self._symbol()
|
symbol = self._symbol()
|
||||||
|
|
||||||
def work():
|
def work():
|
||||||
@@ -1848,10 +1910,12 @@ class OrderApp(tk.Tk):
|
|||||||
positions = self.bridge.positions(symbol)
|
positions = self.bridge.positions(symbol)
|
||||||
tick = self.bridge.tick(symbol)
|
tick = self.bridge.tick(symbol)
|
||||||
tickets = self._be_candidate_tickets(positions, tick)
|
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:
|
if not tickets:
|
||||||
raise TradeError(
|
raise TradeError(self._be_status_hint(positions, tick))
|
||||||
f"没有价格朝有利方向走出超过 {BE_MIN_MOVE_USD:.0f} 美金的可保本持仓"
|
|
||||||
)
|
|
||||||
msg, done = self.bridge.break_even_tickets(tickets, BE_OFFSET_USD)
|
msg, done = self.bridge.break_even_tickets(tickets, BE_OFFSET_USD)
|
||||||
self._be_done_tickets.update(done)
|
self._be_done_tickets.update(done)
|
||||||
return msg
|
return msg
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user