From 3b687d17ebeed62fdca9ad1ef4ca14368dfc42e9 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 30 Jun 2026 09:13:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E8=AE=A1=E6=97=A5=E5=8E=86?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=86=85=E5=B5=8C=20bootstrap?= =?UTF-8?q?=EF=BC=8C=E9=A6=96=E5=B1=8F=E6=98=BE=E7=A4=BA=E7=9B=88=E4=BA=8F?= =?UTF-8?q?=E4=B8=8E=E7=AC=94=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与月统计同源 initial_calendar 写入页面,API 失败时仍渲染;四所日历路由独立注册并传入 get_db_fn。 Co-authored-by: Cursor --- crypto_monitor_binance/app.py | 20 ++++++- crypto_monitor_binance/templates/index.html | 13 +++-- crypto_monitor_gate/app.py | 20 ++++++- crypto_monitor_gate/templates/index.html | 13 +++-- crypto_monitor_gate_bot/app.py | 20 ++++++- crypto_monitor_gate_bot/templates/index.html | 13 +++-- crypto_monitor_okx/app.py | 20 ++++++- crypto_monitor_okx/templates/index.html | 13 +++-- embed_templates/embed_page_fragment.html | 3 + embed_templates/embed_shell.html | 10 ++-- hub_bridge.py | 3 +- instance_embed_context_lib.py | 6 +- static/trade_stats_calendar.js | 58 +++++++++++++++----- tests/test_trade_stats_calendar_lib.py | 17 +++++- trade_stats_calendar_lib.py | 19 +++++++ 15 files changed, 195 insertions(+), 53 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 4669a02..371a2f3 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -1888,6 +1888,12 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): dm, wm, mm = slice_metrics("all") + from trade_stats_calendar_lib import build_initial_stats_calendar + + initial_calendar = build_initial_stats_calendar( + pnls, now_dt, _pnl_row_matches_segment, reset_hour=TRADING_DAY_RESET_HOUR + ) + return { "trading_day": trading_day, "total_opens_all": total_opens_all, @@ -1896,6 +1902,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): "month": mm, "segments": segments, "stats_reset_hour": TRADING_DAY_RESET_HOUR, + "initial_calendar": initial_calendar, } @@ -9617,7 +9624,7 @@ try: _repo_root = Path(__file__).resolve().parent.parent if str(_repo_root) not in sys.path: sys.path.insert(0, str(_repo_root)) - from hub_bridge import install_on_app, register_trade_stats_calendar_route + from hub_bridge import install_on_app install_on_app( app, @@ -9637,15 +9644,22 @@ try: render_main_page_fn=render_main_page, login_required_fn=login_required, ) +except Exception as _hub_err: + print(f"[hub_bridge] binance: {_hub_err}") + +try: + from hub_bridge import register_trade_stats_calendar_route + register_trade_stats_calendar_route( app, login_required_fn=login_required, load_pnls_fn=_load_completed_trade_pnls, row_matches_segment_fn=_pnl_row_matches_segment, reset_hour=TRADING_DAY_RESET_HOUR, + get_db_fn=get_db, ) -except Exception as _hub_err: - print(f"[hub_bridge] binance: {_hub_err}") +except Exception as _cal_err: + print(f"[stats calendar] binance: {_cal_err}") @app.route("/strategy") diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 443c22f..db01ef8 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -3,7 +3,7 @@ - + @@ -243,8 +243,8 @@ .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} - - + + + {% if stats_bundle.initial_calendar %} + + {% endif %}
@@ -844,14 +847,14 @@
- + - + + @@ -243,8 +243,8 @@ .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} - - + + + {% if stats_bundle.initial_calendar %} + + {% endif %}
@@ -811,14 +814,14 @@
- + - + + @@ -243,8 +243,8 @@ .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} - - + + + {% if stats_bundle.initial_calendar %} + + {% endif %}
@@ -811,14 +814,14 @@
- + - + + @@ -243,8 +243,8 @@ .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} - - + + + {% if stats_bundle.initial_calendar %} + + {% endif %}
@@ -840,14 +843,14 @@
- + - + + {% endif %}
diff --git a/embed_templates/embed_shell.html b/embed_templates/embed_shell.html index f9235b4..1f06729 100644 --- a/embed_templates/embed_shell.html +++ b/embed_templates/embed_shell.html @@ -3,12 +3,12 @@ - + - - + + {{ exchange_display }} · 加密货币 | 交易监控复盘系统 @@ -113,7 +113,7 @@
- + @@ -121,7 +121,7 @@ - + {% include 'embed_boot_scripts.html' %} diff --git a/hub_bridge.py b/hub_bridge.py index f74c30c..49f1e57 100644 --- a/hub_bridge.py +++ b/hub_bridge.py @@ -92,6 +92,7 @@ def register_trade_stats_calendar_route( load_pnls_fn, row_matches_segment_fn, reset_hour: int, + get_db_fn=None, ): """四所统计分析页:按月返回各交易日盈亏/笔数。""" from flask import jsonify, request @@ -110,7 +111,7 @@ def register_trade_stats_calendar_route( now = datetime.now() year = year or now.year month = month or now.month - get_db = (app.config.get("HUB_CTX") or {}).get("get_db") + get_db = get_db_fn or (app.config.get("HUB_CTX") or {}).get("get_db") if not get_db: return jsonify({"ok": False, "msg": "未配置数据库"}), 500 conn = get_db() diff --git a/instance_embed_context_lib.py b/instance_embed_context_lib.py index dc682ca..9b774a2 100644 --- a/instance_embed_context_lib.py +++ b/instance_embed_context_lib.py @@ -81,4 +81,8 @@ def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[ def minimal_stats_bundle(reset_hour: int) -> dict[str, Any]: - return {"stats_reset_hour": reset_hour, "segments": []} + return { + "stats_reset_hour": reset_hour, + "segments": [], + "initial_calendar": None, + } diff --git a/static/trade_stats_calendar.js b/static/trade_stats_calendar.js index 95f641c..c899c0a 100644 --- a/static/trade_stats_calendar.js +++ b/static/trade_stats_calendar.js @@ -89,6 +89,38 @@ this.month = d.getMonth() + 1; }; + TradeStatsCalendar.prototype.applyPayload = function (data) { + if (!data) return; + var y = Number(data.year); + var m = Number(data.month); + if (Number.isFinite(y) && y > 0) this.year = y; + if (Number.isFinite(m) && m > 0) this.month = m; + this.days = this.parseResponse(data) || {}; + this.monthPnlTotal = Number(data.month_pnl_total) || 0; + this.monthOpenCount = Number(data.month_open_count) || 0; + if (!this.monthOpenCount) { + var self = this; + Object.keys(this.days).forEach(function (k) { + if (dayHasTrade(self.days[k])) { + self.monthOpenCount += dayOpenCount(self.days[k]); + self.monthPnlTotal += dayPnl(self.days[k]); + } + }); + this.monthPnlTotal = Math.round(this.monthPnlTotal * 10000) / 10000; + } + }; + + function readStatsCalendarBootstrap() { + var el = document.getElementById("stats-calendar-bootstrap"); + if (!el || !el.textContent) return null; + try { + return JSON.parse(el.textContent); + } catch (e) { + console.warn("[trade calendar] bootstrap parse", e); + return null; + } + } + TradeStatsCalendar.prototype.setSelectedDay = function (day) { this.selectedDay = day || ""; this.render(); @@ -192,23 +224,12 @@ }); if (!resp.ok) { console.warn("[trade calendar] api", resp.status); + this.render(); return; } data = await resp.json(); } - this.days = this.parseResponse(data) || {}; - this.monthPnlTotal = Number(data && data.month_pnl_total) || 0; - this.monthOpenCount = Number(data && data.month_open_count) || 0; - if (!this.monthOpenCount) { - var self = this; - Object.keys(this.days).forEach(function (k) { - if (dayHasTrade(self.days[k])) { - self.monthOpenCount += dayOpenCount(self.days[k]); - self.monthPnlTotal += dayPnl(self.days[k]); - } - }); - this.monthPnlTotal = Math.round(this.monthPnlTotal * 10000) / 10000; - } + this.applyPayload(data); this.render(); if (this.onMonthChange) this.onMonthChange(this.year, this.month, this.days); } catch (e) { @@ -253,6 +274,16 @@ global.initInstanceStatsCalendar = function () { var grid = document.getElementById("stats-calendar"); if (!grid || !global.TradeStatsCalendar) return null; + var bootstrap = readStatsCalendarBootstrap(); + if ( + global.statsCalendarWidget && + global.statsCalendarWidget.gridEl === grid + ) { + if (bootstrap) global.statsCalendarWidget.applyPayload(bootstrap); + global.statsCalendarWidget.render(); + void global.statsCalendarWidget.load(); + return global.statsCalendarWidget; + } global.statsCalendarWidget = new TradeStatsCalendar({ gridEl: grid, titleEl: document.getElementById("stats-cal-title"), @@ -273,6 +304,7 @@ return (data && data.days) || {}; }, }); + if (bootstrap) global.statsCalendarWidget.applyPayload(bootstrap); global.statsCalendarWidget.render(); void global.statsCalendarWidget.load(); return global.statsCalendarWidget; diff --git a/tests/test_trade_stats_calendar_lib.py b/tests/test_trade_stats_calendar_lib.py index 5ca608b..61375b0 100644 --- a/tests/test_trade_stats_calendar_lib.py +++ b/tests/test_trade_stats_calendar_lib.py @@ -1,7 +1,9 @@ import unittest from types import SimpleNamespace -from trade_stats_calendar_lib import build_trade_stats_calendar +from datetime import datetime + +from trade_stats_calendar_lib import build_initial_stats_calendar, build_trade_stats_calendar def _row(**kwargs): @@ -54,6 +56,19 @@ class TradeStatsCalendarLibTests(unittest.TestCase): with self.assertRaises(ValueError): build_trade_stats_calendar([], 2026, 13, "all", _matches_all) + def test_initial_calendar_uses_current_month(self): + pnls = [(2.5, None, "2026-06-20", _row())] + payload = build_initial_stats_calendar( + pnls, + datetime(2026, 6, 26, 12, 0), + _matches_all, + reset_hour=8, + ) + self.assertEqual(payload["year"], 2026) + self.assertEqual(payload["month"], 6) + self.assertEqual(payload["month_open_count"], 1) + self.assertIn("2026-06-20", payload["days"]) + if __name__ == "__main__": unittest.main() diff --git a/trade_stats_calendar_lib.py b/trade_stats_calendar_lib.py index be9862b..a2f5020 100644 --- a/trade_stats_calendar_lib.py +++ b/trade_stats_calendar_lib.py @@ -71,3 +71,22 @@ def build_trade_stats_calendar( "month_pnl_total": round(month_pnl, 4), "month_open_count": month_count, } + + +def build_initial_stats_calendar( + pnls: list[tuple], + now_dt: datetime, + row_matches_fn: Callable[[Any, str], bool], + *, + reset_hour: int = 8, + segment_key: str = "all", +) -> dict[str, Any]: + """统计页首屏内嵌日历(当前自然月、默认品类)。""" + return build_trade_stats_calendar( + pnls, + now_dt.year, + now_dt.month, + segment_key, + row_matches_fn, + reset_hour=reset_hour, + )