Add auto break-even toggle and WeCom hub notifications.

Desktop can auto-pull stop to open+/-1 after a 3$ favorable move; hub pushes open/close events and a 23:00 BJT daily summary via webhook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-27 11:25:44 +08:00
parent 828038a5ae
commit 75b9bffe96
13 changed files with 582 additions and 5 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
## 功能
- 连接本机已登录 MT5,市价开平、ATR 止损止盈、保本、满仓灰显等(由原 `MT5黄金方案/Desktop` 迁入)
- 连接本机已登录 MT5,市价开平、ATR 止损止盈、保本(可勾选自动)、满仓灰显等(由原 `MT5黄金方案/Desktop` 迁入)
- **通用包 + 首次激活**:仅第一次填写中控地址与激活码;**不允许离线运行**
- **打开复盘**:用已保存的中控地址打开 `/login`
- **心跳**:约每 2 秒 ping 中控,配合总览 SSE 秒级在线状态
+11
View File
@@ -43,9 +43,20 @@ def load_device() -> dict[str, Any]:
data.setdefault("machine_name", platform.node() or "PC")
data.setdefault("server", "")
data.setdefault("token", "")
data.setdefault("auto_break_even", False)
return data
def get_auto_break_even() -> bool:
return bool(load_device().get("auto_break_even"))
def set_auto_break_even(enabled: bool) -> None:
d = load_device()
d["auto_break_even"] = bool(enabled)
save_device(d)
def save_device(data: dict[str, Any]) -> None:
ensure_dir()
DEVICE_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
+84 -2
View File
@@ -22,12 +22,19 @@ import MetaTrader5 as mt5
try:
import hub_client
from activation_dialog import ensure_activated
from device_store import is_activated, load_device
from device_store import (
get_auto_break_even,
is_activated,
load_device,
set_auto_break_even,
)
except ImportError: # 允许单独拷贝本文件调试
hub_client = None # type: ignore
ensure_activated = None # type: ignore
is_activated = lambda: False # type: ignore
load_device = lambda: {} # type: ignore
get_auto_break_even = lambda: False # type: ignore
set_auto_break_even = lambda _enabled: None # type: ignore
APP_TITLE = "黄金市价辅助"
@@ -1096,6 +1103,7 @@ class OrderApp(tk.Tk):
self._lot_auto_done = False
self._selected_tickets_set: set = set()
self._be_done_tickets: set = set() # 已拉过保本的单号,不再判断
self._auto_be_busy = False # 自动保本进行中,防并发改单
self._pos_sig = None
self._pos_profit_labels: dict = {}
self._metric_values: dict = {}
@@ -1320,6 +1328,13 @@ class OrderApp(tk.Tk):
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.var_auto_be = tk.BooleanVar(value=bool(get_auto_break_even()))
ttk.Checkbutton(
pos_bar,
text="自动保本",
variable=self.var_auto_be,
command=self._on_auto_be_toggle,
).pack(side=tk.RIGHT, padx=(4, 8))
self._mk_btn(pos_bar, "打开复盘", "#3a5a80", self.on_open_review, h=34, w=100).pack(
side=tk.RIGHT, padx=4
)
@@ -1384,7 +1399,8 @@ 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,
@@ -1743,6 +1759,72 @@ class OrderApp(tk.Tk):
except Exception:
ok = False
self._set_be_button_enabled(ok)
if ok and getattr(self, "var_auto_be", None) is not None and self.var_auto_be.get():
self._maybe_auto_break_even()
def _on_auto_be_toggle(self) -> None:
enabled = bool(self.var_auto_be.get())
try:
set_auto_break_even(enabled)
except Exception:
pass
self.set_status(
"已开启自动保本:有利≥3$ 时自动拉止损到开仓价±1$"
if enabled
else "已关闭自动保本(仍可手动点「保本」)",
ok=True,
)
if enabled:
try:
positions = self.bridge.positions(self._symbol())
except Exception:
positions = []
self._maybe_auto_break_even(positions)
def _maybe_auto_break_even(self, positions=None) -> None:
"""开启自动保本且存在候选单时,后台改止损(不抢手动操作的 _busy)。"""
if self._busy or self._auto_be_busy:
return
if not getattr(self, "var_auto_be", None) or not self.var_auto_be.get():
return
symbol = self._symbol()
def work():
self.bridge.connect()
pos = positions
if pos is None:
pos = self.bridge.positions(symbol)
tick = self.bridge.tick(symbol)
tickets = self._be_candidate_tickets(pos, tick)
if not tickets:
return None
msg, done = self.bridge.break_even_tickets(tickets, BE_OFFSET_USD)
self._be_done_tickets.update(done)
return f"自动保本:{msg}"
def after_ok(msg: str) -> None:
self.set_status(msg, ok=True)
self._pos_sig = None
try:
digits = int(self.bridge.symbol_info(symbol).digits)
except Exception:
digits = 3
self._refresh_positions(symbol, digits)
def runner():
try:
msg = work()
if msg:
self.after(0, lambda m=msg: after_ok(m))
except TradeError as exc:
self.after(0, lambda: self.set_status(f"自动保本:{exc}", ok=False))
except Exception as exc: # noqa: BLE001
self.after(0, lambda: self.set_status(f"自动保本异常:{exc}", ok=False))
finally:
self._auto_be_busy = False
self._auto_be_busy = True
threading.Thread(target=runner, daemon=True).start()
def _hub_upload_bars(self, symbol: str) -> None:
if hub_client is None or not is_activated():