Speed up UI: throttle history/stats, async hub sync, faster post-trade refresh.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+138
-32
@@ -44,7 +44,12 @@ MAGIC = 20261016
|
|||||||
COMMENT = "GoldPanel"
|
COMMENT = "GoldPanel"
|
||||||
DEVIATION = 100
|
DEVIATION = 100
|
||||||
MUST_CONFIRM = False # 开仓/平仓默认不二次确认
|
MUST_CONFIRM = False # 开仓/平仓默认不二次确认
|
||||||
REFRESH_MS = 500
|
REFRESH_MS = 400 # 报价/持仓轻量刷新
|
||||||
|
TODAY_STATS_MS = 2500 # 今日战绩(含 history)节流
|
||||||
|
ATR_CACHE_MS = 2000
|
||||||
|
DAY_OHLC_CACHE_MS = 1500
|
||||||
|
HUB_FLOAT_MS = 3000 # 浮盈上报间隔
|
||||||
|
HUB_RECONCILE_MS = 45000 # 历史回填/对账间隔(重)
|
||||||
|
|
||||||
BALANCE_HIGH = 10000.0
|
BALANCE_HIGH = 10000.0
|
||||||
BALANCE_LOW = 1000.0
|
BALANCE_LOW = 1000.0
|
||||||
@@ -509,8 +514,8 @@ class Mt5Bridge:
|
|||||||
pos_ticket = int(result.order)
|
pos_ticket = int(result.order)
|
||||||
open_px = float(result.price)
|
open_px = float(result.price)
|
||||||
open_ts = time.time()
|
open_ts = time.time()
|
||||||
for _ in range(10):
|
for _ in range(6):
|
||||||
time.sleep(0.05)
|
time.sleep(0.02)
|
||||||
for p in self.positions(symbol):
|
for p in self.positions(symbol):
|
||||||
if int(p.ticket) == int(result.order) or abs(float(p.price_open) - open_px) < 0.05:
|
if int(p.ticket) == int(result.order) or abs(float(p.price_open) - open_px) < 0.05:
|
||||||
pos_ticket = int(p.ticket)
|
pos_ticket = int(p.ticket)
|
||||||
@@ -965,6 +970,14 @@ class OrderApp(tk.Tk):
|
|||||||
self._pos_profit_labels: dict = {}
|
self._pos_profit_labels: dict = {}
|
||||||
self._metric_values: dict = {}
|
self._metric_values: dict = {}
|
||||||
self._last_spread: float = 0.0
|
self._last_spread: float = 0.0
|
||||||
|
self._force_today_stats = True
|
||||||
|
self._last_today_stats_ts = 0.0
|
||||||
|
self._atr_cache: Tuple[float, float] = (0.0, 0.0) # (ts, value)
|
||||||
|
self._day_ohlc_cache: Tuple[float, Any] = (0.0, None) # (ts, dict)
|
||||||
|
self._last_float_sync = 0.0
|
||||||
|
self._last_reconcile_sync = 0.0
|
||||||
|
self._hub_sync_busy = False
|
||||||
|
self._cached_today_stats: Optional[dict] = None
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self.after(200, self._try_connect_and_refresh)
|
self.after(200, self._try_connect_and_refresh)
|
||||||
self.after(3000, self._hub_heartbeat)
|
self.after(3000, self._hub_heartbeat)
|
||||||
@@ -1619,28 +1632,25 @@ class OrderApp(tk.Tk):
|
|||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
def _hub_sync_floating(self, symbol: str) -> None:
|
def _hub_sync_floating(self, symbol: str) -> None:
|
||||||
"""浮盈上报 + 历史平仓回填 + 对账清掉已不在仓的「假持仓中」。"""
|
"""浮盈上报 +(低频)历史回填/对账。网络在后台,不堵界面。"""
|
||||||
if hub_client is None or not is_activated():
|
if hub_client is None or not is_activated() or self._hub_sync_busy:
|
||||||
return
|
return
|
||||||
now = time.time()
|
now = time.time()
|
||||||
last = float(getattr(self, "_last_float_sync", 0) or 0)
|
if now - float(self._last_float_sync or 0) < HUB_FLOAT_MS / 1000.0:
|
||||||
if now - last < 2.0:
|
|
||||||
return
|
return
|
||||||
self._last_float_sync = now
|
self._last_float_sync = now
|
||||||
|
do_reconcile = (now - float(self._last_reconcile_sync or 0)) >= HUB_RECONCILE_MS / 1000.0
|
||||||
|
if do_reconcile:
|
||||||
|
self._last_reconcile_sync = now
|
||||||
|
# 持仓快照在 UI 线程取,避免与刷新抢 MT5
|
||||||
try:
|
try:
|
||||||
poss = self.bridge.positions(symbol)
|
poss = self.bridge.positions(symbol)
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
open_tickets = [str(p.ticket) for p in poss]
|
open_tickets = [str(p.ticket) for p in poss]
|
||||||
reports: list = []
|
float_reports = []
|
||||||
# 1) 用成交历史补平仓(修复旧数据 / 漏报)
|
|
||||||
try:
|
|
||||||
reports.extend(self.bridge.recent_close_reports(symbol, hours=72))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# 2) 当前持仓浮盈
|
|
||||||
for p in poss:
|
for p in poss:
|
||||||
reports.append({
|
float_reports.append({
|
||||||
"ticket": str(p.ticket),
|
"ticket": str(p.ticket),
|
||||||
"symbol": str(p.symbol),
|
"symbol": str(p.symbol),
|
||||||
"side": "buy" if int(p.type) == mt5.POSITION_TYPE_BUY else "sell",
|
"side": "buy" if int(p.type) == mt5.POSITION_TYPE_BUY else "sell",
|
||||||
@@ -1654,13 +1664,32 @@ class OrderApp(tk.Tk):
|
|||||||
"tp": float(p.tp),
|
"tp": float(p.tp),
|
||||||
"snapshot": {"event": "float"},
|
"snapshot": {"event": "float"},
|
||||||
})
|
})
|
||||||
if reports:
|
self._hub_sync_busy = True
|
||||||
self._hub_report(reports, with_bars=False)
|
|
||||||
# 3) 告诉服务器:不在 open_tickets 里的未平仓记录应结束「持仓中」
|
def run() -> None:
|
||||||
try:
|
try:
|
||||||
hub_client.reconcile_trades(open_tickets, symbol)
|
reports = list(float_reports)
|
||||||
except Exception:
|
if do_reconcile:
|
||||||
pass
|
try:
|
||||||
|
reports[0:0] = self.bridge.recent_close_reports(symbol, hours=48)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for item in reports:
|
||||||
|
try:
|
||||||
|
hub_client.report_trade(item)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if do_reconcile:
|
||||||
|
try:
|
||||||
|
hub_client.reconcile_trades(open_tickets, symbol)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.after(0, lambda: setattr(self, "_hub_sync_busy", False))
|
||||||
|
|
||||||
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
def _run_async(self, work) -> None:
|
def _run_async(self, work) -> None:
|
||||||
if self._busy:
|
if self._busy:
|
||||||
@@ -1682,7 +1711,10 @@ class OrderApp(tk.Tk):
|
|||||||
msg = result
|
msg = result
|
||||||
self.after(0, lambda m=msg: self.set_status(m, ok=True))
|
self.after(0, lambda m=msg: self.set_status(m, ok=True))
|
||||||
if report is not None:
|
if report is not None:
|
||||||
self.after(0, lambda r=report: self._hub_report(r))
|
# 成交后立刻刷新持仓;K 线上报延后,避免和 MT5 抢线程
|
||||||
|
self.after(0, self._kick_after_trade)
|
||||||
|
self.after(0, lambda r=report: self._hub_report(r, with_bars=False))
|
||||||
|
self.after(800, lambda r=report: self._hub_upload_bars_for_report(r))
|
||||||
except TradeError as exc:
|
except TradeError as exc:
|
||||||
self.after(0, lambda: self.set_status(str(exc), ok=False))
|
self.after(0, lambda: self.set_status(str(exc), ok=False))
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
@@ -1692,8 +1724,33 @@ class OrderApp(tk.Tk):
|
|||||||
|
|
||||||
threading.Thread(target=runner, daemon=True).start()
|
threading.Thread(target=runner, daemon=True).start()
|
||||||
|
|
||||||
|
def _kick_after_trade(self) -> None:
|
||||||
|
"""开平仓成功后立刻重绘持仓与今日战绩。"""
|
||||||
|
self._force_today_stats = True
|
||||||
|
self._pos_sig = None
|
||||||
|
self._last_float_sync = 0.0
|
||||||
|
|
||||||
|
def _hub_upload_bars_for_report(self, payload: Union[dict, list, None]) -> None:
|
||||||
|
if hub_client is None or not is_activated() or not payload:
|
||||||
|
return
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
try:
|
||||||
|
items = payload if isinstance(payload, list) else [payload]
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
sym = str(item.get("symbol") or self._symbol())
|
||||||
|
self._hub_upload_bars(sym)
|
||||||
|
break # 一次成交只传一套 K 线即可
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
def _done(self) -> None:
|
def _done(self) -> None:
|
||||||
self._busy = False
|
self._busy = False
|
||||||
|
self._kick_after_trade()
|
||||||
self.refresh_quotes()
|
self.refresh_quotes()
|
||||||
|
|
||||||
def on_auto_lot(self) -> None:
|
def on_auto_lot(self) -> None:
|
||||||
@@ -1774,7 +1831,7 @@ class OrderApp(tk.Tk):
|
|||||||
atr_txt = sl_txt = tp_txt = "--"
|
atr_txt = sl_txt = tp_txt = "--"
|
||||||
if self.var_use_atr.get():
|
if self.var_use_atr.get():
|
||||||
try:
|
try:
|
||||||
atr = self.bridge.atr(symbol)
|
atr = self._cached_atr(symbol)
|
||||||
base = self._atr_for_stops(atr)
|
base = self._atr_for_stops(atr)
|
||||||
atr_txt = f"{atr:.2f}" + (f"→{base:.0f}" if base > atr + 1e-9 else "")
|
atr_txt = f"{atr:.2f}" + (f"→{base:.0f}" if base > atr + 1e-9 else "")
|
||||||
sl_m = float(self.var_atr_mult.get().replace(",", ".") or "0")
|
sl_m = float(self.var_atr_mult.get().replace(",", ".") or "0")
|
||||||
@@ -1817,7 +1874,7 @@ class OrderApp(tk.Tk):
|
|||||||
fg=COLOR_WARN if spread_wide else COLOR_MUTED,
|
fg=COLOR_WARN if spread_wide else COLOR_MUTED,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
day = self.bridge.day_ohlc(symbol)
|
day = self._cached_day_ohlc(symbol, bid=bid, ask=ask, market_open=market_open)
|
||||||
elapsed_fg = COLOR_WARN if not day["market_open"] else COLOR_ACCENT
|
elapsed_fg = COLOR_WARN if not day["market_open"] else COLOR_ACCENT
|
||||||
self._set_metric("elapsed", day["elapsed_text"], elapsed_fg)
|
self._set_metric("elapsed", day["elapsed_text"], elapsed_fg)
|
||||||
self._set_metric("open", f"{day['open']:.{digits}f}")
|
self._set_metric("open", f"{day['open']:.{digits}f}")
|
||||||
@@ -1849,7 +1906,7 @@ class OrderApp(tk.Tk):
|
|||||||
self._set_metric("range", "--")
|
self._set_metric("range", "--")
|
||||||
self.lbl_day_ohlc.configure(text="日线暂不可用(休市也会尽量显示最近交易日)")
|
self.lbl_day_ohlc.configure(text="日线暂不可用(休市也会尽量显示最近交易日)")
|
||||||
try:
|
try:
|
||||||
self._refresh_today_stats(symbol)
|
self._refresh_today_stats(symbol, floating=profit)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.lbl_today_base.configure(text="今日开单 -- 次")
|
self.lbl_today_base.configure(text="今日开单 -- 次")
|
||||||
self.lbl_today_closed.configure(text=" 已平 --", fg=COLOR_NEUTRAL)
|
self.lbl_today_closed.configure(text=" 已平 --", fg=COLOR_NEUTRAL)
|
||||||
@@ -1900,15 +1957,64 @@ class OrderApp(tk.Tk):
|
|||||||
return COLOR_LOSS
|
return COLOR_LOSS
|
||||||
return COLOR_NEUTRAL
|
return COLOR_NEUTRAL
|
||||||
|
|
||||||
def _refresh_today_stats(self, symbol: str) -> None:
|
def _cached_atr(self, symbol: str) -> float:
|
||||||
st = self.bridge.today_stats(symbol)
|
now = time.time()
|
||||||
|
ts, val = self._atr_cache
|
||||||
|
if val > 0 and (now - ts) * 1000.0 < ATR_CACHE_MS:
|
||||||
|
return val
|
||||||
|
atr = float(self.bridge.atr(symbol))
|
||||||
|
self._atr_cache = (now, atr)
|
||||||
|
return atr
|
||||||
|
|
||||||
|
def _cached_day_ohlc(
|
||||||
|
self, symbol: str, bid: float = 0.0, ask: float = 0.0, market_open: bool = True
|
||||||
|
) -> dict:
|
||||||
|
now = time.time()
|
||||||
|
ts, cached = self._day_ohlc_cache
|
||||||
|
if cached is not None and (now - ts) * 1000.0 < DAY_OHLC_CACHE_MS:
|
||||||
|
day = dict(cached)
|
||||||
|
# 缓存期内仍用最新报价刷新高低价与开盘时长
|
||||||
|
if market_open and bid > 0:
|
||||||
|
day["high"] = max(float(day["high"]), bid, ask or bid)
|
||||||
|
day["low"] = min(float(day["low"]), bid, ask or bid)
|
||||||
|
day["market_open"] = market_open
|
||||||
|
day["elapsed_text"] = format_open_elapsed(int(day.get("open_ts") or 0), market_open)
|
||||||
|
return day
|
||||||
|
day = self.bridge.day_ohlc(symbol)
|
||||||
|
self._day_ohlc_cache = (now, dict(day))
|
||||||
|
return day
|
||||||
|
|
||||||
|
def _refresh_today_stats(self, symbol: str, floating: Optional[float] = None) -> None:
|
||||||
|
now = time.time()
|
||||||
|
need = self._force_today_stats or (
|
||||||
|
(now - self._last_today_stats_ts) * 1000.0 >= TODAY_STATS_MS
|
||||||
|
)
|
||||||
|
if need:
|
||||||
|
st = self.bridge.today_stats(symbol)
|
||||||
|
self._cached_today_stats = st
|
||||||
|
self._last_today_stats_ts = now
|
||||||
|
self._force_today_stats = False
|
||||||
|
else:
|
||||||
|
st = self._cached_today_stats
|
||||||
|
if not st:
|
||||||
|
st = self.bridge.today_stats(symbol)
|
||||||
|
self._cached_today_stats = st
|
||||||
|
self._last_today_stats_ts = now
|
||||||
|
else:
|
||||||
|
# 节流期内只刷新浮盈/合计,避免每拍扫成交历史
|
||||||
|
st = dict(st)
|
||||||
|
if floating is not None:
|
||||||
|
st["floating"] = float(floating)
|
||||||
|
st["total"] = float(st.get("closed_pnl") or 0) + float(floating)
|
||||||
|
bal = float(st.get("balance") or 0)
|
||||||
|
st["profit_rate"] = (st["total"] / bal) if bal > 1e-8 else 0.0
|
||||||
closed = st["closed_pnl"]
|
closed = st["closed_pnl"]
|
||||||
floating = st["floating"]
|
floating_v = st["floating"] if floating is None else float(floating)
|
||||||
total = st["total"]
|
total = float(st.get("closed_pnl") or 0) + floating_v
|
||||||
rate = st["profit_rate"] * 100.0
|
rate = (total / float(st.get("balance") or 1)) * 100.0 if float(st.get("balance") or 0) > 1e-8 else 0.0
|
||||||
self.lbl_today_base.configure(text=f"今日开单 {st['opens']} 次")
|
self.lbl_today_base.configure(text=f"今日开单 {st['opens']} 次")
|
||||||
self.lbl_today_closed.configure(text=f" 已平 {closed:+.2f}", fg=self._pnl_color(closed))
|
self.lbl_today_closed.configure(text=f" 已平 {closed:+.2f}", fg=self._pnl_color(closed))
|
||||||
self.lbl_today_mid.configure(text=f" 浮盈 {floating:+.2f} 合计 ")
|
self.lbl_today_mid.configure(text=f" 浮盈 {floating_v:+.2f} 合计 ")
|
||||||
self.lbl_today_total.configure(text=f"{total:+.2f}", fg=self._pnl_color(total))
|
self.lbl_today_total.configure(text=f"{total:+.2f}", fg=self._pnl_color(total))
|
||||||
self.lbl_today_ratio.configure(
|
self.lbl_today_ratio.configure(
|
||||||
text=(
|
text=(
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user