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 %}
-
+
-
+
+
@@ -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 %}
-
+
-
+
+
@@ -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 %}
-
+
-
+
+
@@ -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 %}
-
+
-
+
+ {% 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,
+ )