diff --git a/desktop/gold_order_app.py b/desktop/gold_order_app.py index 4115a64..af3da3b 100644 --- a/desktop/gold_order_app.py +++ b/desktop/gold_order_app.py @@ -44,7 +44,12 @@ MAGIC = 20261016 COMMENT = "GoldPanel" DEVIATION = 100 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_LOW = 1000.0 @@ -509,8 +514,8 @@ class Mt5Bridge: pos_ticket = int(result.order) open_px = float(result.price) open_ts = time.time() - for _ in range(10): - time.sleep(0.05) + for _ in range(6): + time.sleep(0.02) for p in self.positions(symbol): if int(p.ticket) == int(result.order) or abs(float(p.price_open) - open_px) < 0.05: pos_ticket = int(p.ticket) @@ -965,6 +970,14 @@ class OrderApp(tk.Tk): self._pos_profit_labels: dict = {} self._metric_values: dict = {} 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.after(200, self._try_connect_and_refresh) self.after(3000, self._hub_heartbeat) @@ -1619,28 +1632,25 @@ class OrderApp(tk.Tk): threading.Thread(target=run, daemon=True).start() 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 now = time.time() - last = float(getattr(self, "_last_float_sync", 0) or 0) - if now - last < 2.0: + if now - float(self._last_float_sync or 0) < HUB_FLOAT_MS / 1000.0: return 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: poss = self.bridge.positions(symbol) except Exception: return open_tickets = [str(p.ticket) for p in poss] - reports: list = [] - # 1) 用成交历史补平仓(修复旧数据 / 漏报) - try: - reports.extend(self.bridge.recent_close_reports(symbol, hours=72)) - except Exception: - pass - # 2) 当前持仓浮盈 + float_reports = [] for p in poss: - reports.append({ + float_reports.append({ "ticket": str(p.ticket), "symbol": str(p.symbol), "side": "buy" if int(p.type) == mt5.POSITION_TYPE_BUY else "sell", @@ -1654,13 +1664,32 @@ class OrderApp(tk.Tk): "tp": float(p.tp), "snapshot": {"event": "float"}, }) - if reports: - self._hub_report(reports, with_bars=False) - # 3) 告诉服务器:不在 open_tickets 里的未平仓记录应结束「持仓中」 - try: - hub_client.reconcile_trades(open_tickets, symbol) - except Exception: - pass + self._hub_sync_busy = True + + def run() -> None: + try: + reports = list(float_reports) + if do_reconcile: + 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: if self._busy: @@ -1682,7 +1711,10 @@ class OrderApp(tk.Tk): msg = result self.after(0, lambda m=msg: self.set_status(m, ok=True)) 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: self.after(0, lambda: self.set_status(str(exc), ok=False)) except Exception as exc: # noqa: BLE001 @@ -1692,8 +1724,33 @@ class OrderApp(tk.Tk): 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: self._busy = False + self._kick_after_trade() self.refresh_quotes() def on_auto_lot(self) -> None: @@ -1774,7 +1831,7 @@ class OrderApp(tk.Tk): atr_txt = sl_txt = tp_txt = "--" if self.var_use_atr.get(): try: - atr = self.bridge.atr(symbol) + atr = self._cached_atr(symbol) base = self._atr_for_stops(atr) 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") @@ -1817,7 +1874,7 @@ class OrderApp(tk.Tk): fg=COLOR_WARN if spread_wide else COLOR_MUTED, ) 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 self._set_metric("elapsed", day["elapsed_text"], elapsed_fg) self._set_metric("open", f"{day['open']:.{digits}f}") @@ -1849,7 +1906,7 @@ class OrderApp(tk.Tk): self._set_metric("range", "--") self.lbl_day_ohlc.configure(text="日线暂不可用(休市也会尽量显示最近交易日)") try: - self._refresh_today_stats(symbol) + self._refresh_today_stats(symbol, floating=profit) except Exception: self.lbl_today_base.configure(text="今日开单 -- 次") self.lbl_today_closed.configure(text=" 已平 --", fg=COLOR_NEUTRAL) @@ -1900,15 +1957,64 @@ class OrderApp(tk.Tk): return COLOR_LOSS return COLOR_NEUTRAL - def _refresh_today_stats(self, symbol: str) -> None: - st = self.bridge.today_stats(symbol) + def _cached_atr(self, symbol: str) -> float: + 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"] - floating = st["floating"] - total = st["total"] - rate = st["profit_rate"] * 100.0 + floating_v = st["floating"] if floating is None else float(floating) + total = float(st.get("closed_pnl") or 0) + floating_v + 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_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_ratio.configure( text=( diff --git a/releases/黄金市价辅助.exe b/releases/黄金市价辅助.exe index d003067..0ce32ab 100644 Binary files a/releases/黄金市价辅助.exe and b/releases/黄金市价辅助.exe differ