# Copyright (c) 2025-2026 马建军. All rights reserved. # 专有软件 — 未经授权禁止复制、传播、转售。 # 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md """数据看板:账户、关键位、平仓记录聚合。""" from __future__ import annotations from datetime import datetime from typing import Any, Callable, Optional from zoneinfo import ZoneInfo _TZ = ZoneInfo("Asia/Shanghai") _PRICE_CACHE: dict[str, tuple[float, float]] = {} _PRICE_CACHE_TTL = 2.0 def _cached_fetch_price( fetch_price: Callable[[str, str, str], Optional[float]], sym: str, market: str, sina: str, ) -> Optional[float]: key = sym or "" now = datetime.now().timestamp() hit = _PRICE_CACHE.get(key) if hit and (now - hit[1]) < _PRICE_CACHE_TTL: return hit[0] price = fetch_price(sym, market, sina) if price is not None: _PRICE_CACHE[key] = (float(price), now) return price def _direction_label(direction: str) -> str: return "做多" if (direction or "").strip().lower() == "long" else "做空" def _symbol_fields(ths_code: str) -> dict[str, Any]: from symbols import position_symbol_meta sym = (ths_code or "").strip() meta = position_symbol_meta(sym) return { "symbol_code": sym, "symbol_name": meta.get("name") or sym, "symbol_exchange": meta.get("exchange") or "", "symbol_is_main": bool(meta.get("is_main")), } def build_risk_overview( conn, get_setting: Callable[[str, str], str], *, equity: Optional[float] = None, margin_used: Optional[float] = None, ) -> dict[str, Any]: from risk.account_risk_lib import ( cooling_hours_manual, cooling_hours_manual_journal, count_daily_opens, daily_position_limit, daily_trading_risk_pct_limit, daily_trading_risk_used_pct, ensure_account_risk_schema, get_risk_status, manual_close_daily_limit, max_active_positions, risk_control_enabled, trading_day_label, trading_day_reset_hour, ) from trading_context import ( get_fixed_amount, get_fixed_lots, get_max_margin_pct, get_roll_max_margin_pct, get_sizing_mode, ) ensure_account_risk_schema(conn) risk = dict(get_risk_status(conn, equity=equity) or {}) row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() td = trading_day_label() stored_td = str(row["trading_day"] or "") if row else "" manual_count = int(row["manual_close_count"] or 0) if row and stored_td == td else 0 margin_pct_used: Optional[float] = None if equity and equity > 0 and margin_used is not None and margin_used >= 0: margin_pct_used = round(float(margin_used) / float(equity) * 100, 2) max_margin = get_max_margin_pct(get_setting) sizing = get_sizing_mode(get_setting) sizing_label = "固定金额" if sizing == "amount" else "固定手数" daily_opens = int(risk.get("daily_open_count") or count_daily_opens(conn)) daily_risk_used = risk.get("daily_risk_used_pct") if daily_risk_used is None and equity and equity > 0: daily_risk_used = daily_trading_risk_used_pct(conn, float(equity)) return { "enabled": risk_control_enabled(), "status": risk, "manual_close_count_today": manual_count, "margin_pct_used": margin_pct_used, "daily_open_count": daily_opens, "daily_risk_used_pct": daily_risk_used, "limits": { "max_active_positions": max_active_positions(), "position_mode": "single" if max_active_positions() <= 1 else "multi", "position_mode_label": "单仓模式" if max_active_positions() <= 1 else "多仓模式", "daily_position_limit": daily_position_limit(), "daily_trading_risk_pct_limit": daily_trading_risk_pct_limit(), "manual_close_daily_limit": manual_close_daily_limit(), "cooling_hours_manual": cooling_hours_manual(), "cooling_hours_manual_journal": cooling_hours_manual_journal(), "trading_day_reset_hour": trading_day_reset_hour(), "max_margin_pct": max_margin, "roll_max_margin_pct": get_roll_max_margin_pct(get_setting), "sizing_mode": sizing, "sizing_label": sizing_label, "fixed_lots": get_fixed_lots(get_setting), "fixed_amount": get_fixed_amount(get_setting), }, } def build_dashboard_payload( *, get_db: Callable, get_setting: Callable[[str, str], str], fetch_price: Callable[[str, str, str], Optional[float]], closes_limit: int = 40, sync_ctp_trades: bool = False, ) -> dict[str, Any]: from trading_context import get_account_capital, get_trading_mode, trading_mode_label from vnpy_bridge import ctp_account_margin_used, ctp_status, get_bridge mode = get_trading_mode(get_setting) ctp_st = dict(ctp_status(mode) or {}) conn = get_db() try: capital = float(get_account_capital(conn, get_setting) or 0) equity = capital available: Optional[float] = None margin_used: Optional[float] = None if ctp_st.get("connected"): if sync_ctp_trades: try: from ctp_trade_sync import sync_trade_logs_from_ctp sync_trade_logs_from_ctp( conn, mode, capital=capital, trading_mode=mode, ) conn.commit() except Exception: pass try: b = get_bridge() if b.connected_mode == mode and b.ping(): acc = b.get_account() or {} else: acc = {} balance = float(acc.get("balance") or 0) if balance > 0: equity = balance avail = acc.get("available") if avail is not None: available = round(float(avail), 2) mu = ctp_account_margin_used(mode) if mu is not None and mu > 0: margin_used = round(float(mu), 2) elif available is not None and equity > 0: margin_used = round(max(0.0, equity - available), 2) except Exception: pass key_rows = conn.execute( """ SELECT id, symbol, symbol_name, market_code, sina_code, monitor_type, direction, upper, lower, trade_mode, bar_period, trailing_be FROM key_monitors WHERE status='active' OR status IS NULL ORDER BY id DESC """ ).fetchall() keys: list[dict[str, Any]] = [] for r in key_rows: sym = r["symbol"] market = r["market_code"] or "" sina = r["sina_code"] or "" upper = float(r["upper"] or 0) lower = float(r["lower"] or 0) price = _cached_fetch_price(fetch_price, sym, market, sina) dist_upper = dist_lower = None if price is not None: dist_upper = round(upper - float(price), 2) dist_lower = round(float(price) - lower, 2) mtype = r["monitor_type"] or "" sf = _symbol_fields(sym) keys.append({ "id": r["id"], "symbol": sym, **sf, "symbol_name": r["symbol_name"] or sf.get("symbol_name") or sym, "monitor_type": mtype, "direction": r["direction"] or "", "direction_label": _direction_label(r["direction"] or "long") if r["direction"] else "", "upper": upper, "lower": lower, "trade_mode": r["trade_mode"] or "", "bar_period": r["bar_period"] or "5m", "trailing_be": bool(r["trailing_be"]), "price": price, "dist_upper": dist_upper, "dist_lower": dist_lower, }) close_rows = conn.execute( """ SELECT id, symbol, symbol_name, direction, lots, entry_price, close_price, pnl, pnl_net, fee, close_time, result, source FROM trade_logs ORDER BY id DESC LIMIT ? """, (max(1, min(200, closes_limit)),), ).fetchall() closes: list[dict[str, Any]] = [] for r in close_rows: sym_code = r["symbol"] or "" sf = _symbol_fields(sym_code) closes.append({ "id": r["id"], "symbol": r["symbol_name"] or sf.get("symbol_name") or sym_code, "symbol_code": sym_code, **sf, "symbol_name": r["symbol_name"] or sf.get("symbol_name") or sym_code, "direction": r["direction"] or "long", "direction_label": _direction_label(r["direction"] or "long"), "lots": float(r["lots"] or 0), "entry_price": float(r["entry_price"] or 0), "close_price": float(r["close_price"] or 0), "pnl": float(r["pnl"] or 0) if r["pnl"] is not None else None, "pnl_net": float(r["pnl_net"] or 0) if r["pnl_net"] is not None else None, "fee": float(r["fee"] or 0) if r["fee"] is not None else None, "close_time": (r["close_time"] or "")[:16].replace("T", " "), "result": r["result"] or "", "source": r["source"] or "", }) now_iso = datetime.now(_TZ).strftime("%Y-%m-%d %H:%M:%S") risk = build_risk_overview( conn, get_setting, equity=equity, margin_used=margin_used, ) return { "ok": True, "updated_at": now_iso, "trading_mode_label": trading_mode_label(get_setting), "ctp_status": ctp_st, "account": { "equity": round(equity, 2), "margin_used": margin_used, "available": available, "capital_fallback": round(capital, 2), }, "risk": risk, "keys": keys, "closes": closes, } finally: conn.close()