diff --git a/app.py b/app.py index cd1e5af..5a5106f 100644 --- a/app.py +++ b/app.py @@ -1667,14 +1667,18 @@ def api_dashboard_live(): from dashboard_lib import build_dashboard_payload _dashboard_sync_tick["n"] += 1 - sync_trades = _dashboard_sync_tick["n"] % 5 == 0 - payload = build_dashboard_payload( - get_db=get_db, - get_setting=get_setting, - fetch_price=fetch_price, - sync_ctp_trades=sync_trades, - ) - return jsonify(payload) + sync_trades = _dashboard_sync_tick["n"] % 15 == 0 + try: + payload = build_dashboard_payload( + get_db=get_db, + get_setting=get_setting, + fetch_price=fetch_price, + sync_ctp_trades=sync_trades, + ) + return jsonify(payload) + except Exception as exc: + app.logger.exception("dashboard live: %s", exc) + return jsonify({"ok": False, "error": "看板数据暂时不可用"}), 503 @app.route("/market") diff --git a/dashboard_lib.py b/dashboard_lib.py index 99e3071..2c2178d 100644 --- a/dashboard_lib.py +++ b/dashboard_lib.py @@ -10,6 +10,25 @@ 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: @@ -174,7 +193,7 @@ def build_dashboard_payload( sina = r["sina_code"] or "" upper = float(r["upper"] or 0) lower = float(r["lower"] or 0) - price = fetch_price(sym, market, sina) + 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) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index df6186a..4cff042 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -362,7 +362,8 @@ html:is([data-layout="phone"], .layout-phone) .dash-mobile-list { } html:is([data-layout="phone"], .layout-phone) .dash-pos-table-wrap, -html:is([data-layout="phone"], .layout-phone) .dash-keys-table-wrap { +html:is([data-layout="phone"], .layout-phone) .dash-keys-table-wrap, +html:is([data-layout="phone"], .layout-phone) .dash-closes-table-wrap { display: none; } @@ -416,6 +417,27 @@ html:is([data-layout="phone"], .layout-phone) .dash-keys-table-wrap { font-size: 0.78rem; } +.dash-mobile-item-summary { + flex: 1; + min-width: 0; + line-height: 1.45; + color: var(--text-muted); +} + +.dash-mobile-item-summary .pnl-pos { + color: var(--profit); + font-weight: 600; +} + +.dash-mobile-item-summary .pnl-neg { + color: var(--loss); + font-weight: 600; +} + +.dash-mobile-item-title .badge { + vertical-align: middle; +} + .dash-mobile-chevron { font-size: 0.72rem; color: var(--accent); diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 00a7420..469a755 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -20,17 +20,21 @@ var riskBodyEl = document.getElementById('dash-risk-body'); var posMobileList = document.getElementById('dash-pos-mobile-list'); var keysMobileList = document.getElementById('dash-keys-mobile-list'); + var closesMobileList = document.getElementById('dash-closes-mobile-list'); var detailModal = document.getElementById('dash-detail-modal'); var detailTitleEl = document.getElementById('dash-detail-title'); var detailGridEl = document.getElementById('dash-detail-grid'); var detailCloseBtn = document.getElementById('dash-detail-close'); var pollTimer = null; + var pollInFlight = false; + var pollFailStreak = 0; var positionSource = null; var posRowCache = {}; var posRenderSig = ''; var posMobileCache = {}; var keysMobileCache = {}; + var closesMobileCache = {}; var lastPosRows = []; var lastKeyRows = []; var lastKeyIds = ''; @@ -181,6 +185,77 @@ return '—'; } + function fmtMobileLots(v) { + var n = Number(v); + if (isNaN(n)) return '—'; + if (Math.abs(n - Math.round(n)) < 0.001) return Math.round(n) + '手'; + return fmtNum(n) + '手'; + } + + function fmtMobilePrice(v) { + if (v == null || v === '') return '—'; + var n = Number(v); + if (isNaN(n)) return '—'; + if (Math.abs(n - Math.round(n)) < 0.001) return String(Math.round(n)); + return fmtNum(n); + } + + function fmtMobilePnlNum(v) { + if (v == null || v === '') return '—'; + var n = Number(v); + if (isNaN(n)) return '—'; + var abs = Math.abs(n); + var s = Math.abs(abs - Math.round(abs)) < 0.01 ? String(Math.round(abs)) : fmtNum(abs); + if (n > 0) return s; + if (n < 0) return '-' + s; + return '0'; + } + + function mobilePnlSpanHtml(v) { + return '' + escHtml(fmtMobilePnlNum(v)) + ''; + } + + function mobileSummaryHtml(lots, priceLabel, price, pnl) { + return fmtMobileLots(lots) + ' ' + priceLabel + ' ' + fmtMobilePrice(price) + + ' 盈亏 ' + mobilePnlSpanHtml(pnl); + } + + function mobileSymbolTitleHtml(row) { + var name = row.symbol_name || row.symbol || ''; + var code = row.symbol_code || ''; + if (!code && row.symbol && String(row.symbol).toLowerCase() !== String(name).toLowerCase()) { + code = row.symbol; + } + var exchange = row.symbol_exchange || ''; + var mainBadge = row.symbol_is_main + ? ' 主力' : ''; + var titleInner = escHtml(name); + if (exchange) { + titleInner += ' ' + escHtml(exchange) + ''; + } + titleInner += mainBadge; + titleInner += breakevenBadgeHtml(row); + if (code && String(name).toLowerCase() !== String(code).toLowerCase()) { + titleInner += ' ' + escHtml(code) + ' ' + directionBadgeHtml(row); + } else if (!name && code) { + titleInner = (exchange + ? '' + escHtml(exchange) + ' ' + : '') + '' + escHtml(code) + ' ' + directionBadgeHtml(row); + } else { + titleInner += ' ' + directionBadgeHtml(row); + } + return titleInner; + } + + function mobileItemFootHtml(summaryHtml) { + return ( + '