From f6ea0dc3992f4eb0608dd7f90893cc79f8503901 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 1 Aug 2026 09:49:47 +0800 Subject: [PATCH] Add all-time stats tab with monthly breakdown on instance analytics. Co-authored-by: Cursor --- crypto_monitor_binance/app.py | 52 +++++++++++++++-- crypto_monitor_gate/app.py | 58 +++++++++++++++---- crypto_monitor_okx/app.py | 52 +++++++++++++++-- lib/common/static/instance_page.css | 7 +++ lib/common/static/instance_stats.js | 2 +- lib/common/static/instance_theme.css | 7 +++ .../templates/embed_page_fragment.html | 40 ++++++++++++- lib/instance/templates/embed_shell.html | 6 +- lib/instance/templates/index.html | 44 +++++++++++++- 9 files changed, 239 insertions(+), 29 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 6344bf7..5d5f57b 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -1844,8 +1844,38 @@ def _compute_period_metrics(trades): } +def _bounds_for_month_key(ym): + """ym: YYYY-MM → 该自然月首末日(北京日历).""" + y, m = [int(x) for x in str(ym).split("-", 1)] + start = f"{y:04d}-{m:02d}-01" + if m == 12: + end = f"{y:04d}-12-31" + else: + end = (datetime(y, m + 1, 1) - timedelta(days=1)).date().strftime("%Y-%m-%d") + return start, end + + +def _build_monthly_stats_rows(conn, all_tr, seg_key): + """按北京交易日所在自然月聚合;新月在前.""" + by_month = {} + for p, t, td in all_tr: + if not td or len(str(td)) < 7: + continue + mk = str(td)[:7] + by_month.setdefault(mk, []).append((p, t, td)) + rows = [] + for mk in sorted(by_month.keys(), reverse=True): + metrics = _compute_period_metrics(by_month[mk]) + ms, me = _bounds_for_month_key(mk) + metrics["opens_count"] = _count_opens_for_segment(conn, ms, me, seg_key) + metrics["range_label"] = f"{ms} ~ {me}" + metrics["month_key"] = mk + rows.append(metrics) + return rows + + def compute_stats_bundle(conn, trading_day, now_dt=None): - """日 / 周 / 月 统计:平仓按北京时间交易日(默认 8:00 切日)计入.""" + """日 / 周 / 月 / 全部 统计:平仓按北京时间交易日(默认 8:00 切日)计入.""" now_dt = now_dt or app_now() pnls = _load_completed_trade_pnls(conn) total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0] @@ -1857,26 +1887,37 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): day_tr = [(p, t, td) for p, t, td, _r in seg_rows if td == trading_day] week_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and w_start <= td <= w_end] month_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and m_start <= td <= m_end] + all_tr = [(p, t, td) for p, t, td, _r in seg_rows if t] dm = _compute_period_metrics(day_tr) wm = _compute_period_metrics(week_tr) mm = _compute_period_metrics(month_tr) + am = _compute_period_metrics(all_tr) dm["opens_count"] = _count_opens_for_segment(conn, trading_day, trading_day, seg_key) wm["opens_count"] = _count_opens_for_segment(conn, w_start, w_end, seg_key) mm["opens_count"] = _count_opens_for_segment(conn, m_start, m_end, seg_key) + am["opens_count"] = _count_opens_for_segment(conn, "1970-01-01", "9999-12-31", seg_key) dm["range_label"] = f"北京时间交易日 {trading_day}({TRADING_DAY_RESET_HOUR}:00 切日)" wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天)" mm["range_label"] = f"{m_start} ~ {m_end}(北京自然月)" - return dm, wm, mm + tds = [td for _, _, td in all_tr if td] + if tds: + am["range_label"] = f"全部历史 {min(tds)} ~ {max(tds)}(北京交易日)" + else: + am["range_label"] = "全部历史(暂无平仓)" + am["monthly_rows"] = _build_monthly_stats_rows(conn, all_tr, seg_key) + return dm, wm, mm, am segments = [] seg_defs = effective_stats_segment_defs( STATS_SEGMENT_DEFS, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED ) for seg_key, seg_title, _meta in seg_defs: - dm, wm, mm = slice_metrics(seg_key) - segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm}) + dm, wm, mm, am = slice_metrics(seg_key) + segments.append( + {"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm, "all": am} + ) - dm, wm, mm = slice_metrics("all") + dm, wm, mm, am = slice_metrics("all") return { "trading_day": trading_day, @@ -1884,6 +1925,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): "day": dm, "week": wm, "month": mm, + "all": am, "segments": segments, "stats_reset_hour": TRADING_DAY_RESET_HOUR, } diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index ba38f56..a00dfd7 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -1842,45 +1842,80 @@ def _compute_period_metrics(trades): } +def _bounds_for_month_key(ym): + """ym: YYYY-MM → 该自然月首末日(北京日历).""" + y, m = [int(x) for x in str(ym).split("-", 1)] + start = f"{y:04d}-{m:02d}-01" + if m == 12: + end = f"{y:04d}-12-31" + else: + end = (datetime(y, m + 1, 1) - timedelta(days=1)).date().strftime("%Y-%m-%d") + return start, end + + +def _build_monthly_stats_rows(conn, all_tr, seg_key): + """按北京交易日所在自然月聚合;新月在前.""" + by_month = {} + for p, t, td in all_tr: + if not td or len(str(td)) < 7: + continue + mk = str(td)[:7] + by_month.setdefault(mk, []).append((p, t, td)) + rows = [] + for mk in sorted(by_month.keys(), reverse=True): + metrics = _compute_period_metrics(by_month[mk]) + ms, me = _bounds_for_month_key(mk) + metrics["opens_count"] = _count_opens_for_segment(conn, ms, me, seg_key) + metrics["range_label"] = f"{ms} ~ {me}" + metrics["month_key"] = mk + rows.append(metrics) + return rows + + def compute_stats_bundle(conn, trading_day, now_dt=None): - """日 / 周 / 月 统计:平仓按北京时间交易日(默认 8:00 切日)计入.""" + """日 / 周 / 月 / 全部 统计:平仓按北京时间交易日(默认 8:00 切日)计入.""" now_dt = now_dt or app_now() pnls = _load_completed_trade_pnls(conn) total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0] w_start, w_end = _session_week_bounds(trading_day) m_start, m_end = _calendar_month_bounds(now_dt) - def in_week(tr): - return tr[2] and w_start <= tr[2] <= w_end - - def in_month(tr): - return tr[2] and m_start <= tr[2] <= m_end - def slice_metrics(seg_key): seg_rows = [tr for tr in pnls if _pnl_row_matches_segment(tr[3], seg_key)] day_tr = [(p, t, td) for p, t, td, _r in seg_rows if td == trading_day] week_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and w_start <= td <= w_end] month_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and m_start <= td <= m_end] + all_tr = [(p, t, td) for p, t, td, _r in seg_rows if t] dm = _compute_period_metrics(day_tr) wm = _compute_period_metrics(week_tr) mm = _compute_period_metrics(month_tr) + am = _compute_period_metrics(all_tr) dm["opens_count"] = _count_opens_for_segment(conn, trading_day, trading_day, seg_key) wm["opens_count"] = _count_opens_for_segment(conn, w_start, w_end, seg_key) mm["opens_count"] = _count_opens_for_segment(conn, m_start, m_end, seg_key) + am["opens_count"] = _count_opens_for_segment(conn, "1970-01-01", "9999-12-31", seg_key) dm["range_label"] = f"北京时间交易日 {trading_day}({TRADING_DAY_RESET_HOUR}:00 切日)" wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天)" mm["range_label"] = f"{m_start} ~ {m_end}(北京自然月)" - return dm, wm, mm + tds = [td for _, _, td in all_tr if td] + if tds: + am["range_label"] = f"全部历史 {min(tds)} ~ {max(tds)}(北京交易日)" + else: + am["range_label"] = "全部历史(暂无平仓)" + am["monthly_rows"] = _build_monthly_stats_rows(conn, all_tr, seg_key) + return dm, wm, mm, am segments = [] seg_defs = effective_stats_segment_defs( STATS_SEGMENT_DEFS, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED ) for seg_key, seg_title, _meta in seg_defs: - dm, wm, mm = slice_metrics(seg_key) - segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm}) + dm, wm, mm, am = slice_metrics(seg_key) + segments.append( + {"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm, "all": am} + ) - dm, wm, mm = slice_metrics("all") + dm, wm, mm, am = slice_metrics("all") return { "trading_day": trading_day, @@ -1888,6 +1923,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): "day": dm, "week": wm, "month": mm, + "all": am, "segments": segments, "stats_reset_hour": TRADING_DAY_RESET_HOUR, } diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index d0a0cab..ec12361 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -1843,8 +1843,38 @@ def _compute_period_metrics(trades): } +def _bounds_for_month_key(ym): + """ym: YYYY-MM → 该自然月首末日(北京日历).""" + y, m = [int(x) for x in str(ym).split("-", 1)] + start = f"{y:04d}-{m:02d}-01" + if m == 12: + end = f"{y:04d}-12-31" + else: + end = (datetime(y, m + 1, 1) - timedelta(days=1)).date().strftime("%Y-%m-%d") + return start, end + + +def _build_monthly_stats_rows(conn, all_tr, seg_key): + """按北京交易日所在自然月聚合;新月在前.""" + by_month = {} + for p, t, td in all_tr: + if not td or len(str(td)) < 7: + continue + mk = str(td)[:7] + by_month.setdefault(mk, []).append((p, t, td)) + rows = [] + for mk in sorted(by_month.keys(), reverse=True): + metrics = _compute_period_metrics(by_month[mk]) + ms, me = _bounds_for_month_key(mk) + metrics["opens_count"] = _count_opens_for_segment(conn, ms, me, seg_key) + metrics["range_label"] = f"{ms} ~ {me}" + metrics["month_key"] = mk + rows.append(metrics) + return rows + + def compute_stats_bundle(conn, trading_day, now_dt=None): - """日 / 周 / 月 统计:平仓按北京时间交易日(默认 8:00 切日)计入.""" + """日 / 周 / 月 / 全部 统计:平仓按北京时间交易日(默认 8:00 切日)计入.""" now_dt = now_dt or app_now() pnls = _load_completed_trade_pnls(conn) total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0] @@ -1856,26 +1886,37 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): day_tr = [(p, t, td) for p, t, td, _r in seg_rows if td == trading_day] week_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and w_start <= td <= w_end] month_tr = [(p, t, td) for p, t, td, _r in seg_rows if t and m_start <= td <= m_end] + all_tr = [(p, t, td) for p, t, td, _r in seg_rows if t] dm = _compute_period_metrics(day_tr) wm = _compute_period_metrics(week_tr) mm = _compute_period_metrics(month_tr) + am = _compute_period_metrics(all_tr) dm["opens_count"] = _count_opens_for_segment(conn, trading_day, trading_day, seg_key) wm["opens_count"] = _count_opens_for_segment(conn, w_start, w_end, seg_key) mm["opens_count"] = _count_opens_for_segment(conn, m_start, m_end, seg_key) + am["opens_count"] = _count_opens_for_segment(conn, "1970-01-01", "9999-12-31", seg_key) dm["range_label"] = f"北京时间交易日 {trading_day}({TRADING_DAY_RESET_HOUR}:00 切日)" wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天)" mm["range_label"] = f"{m_start} ~ {m_end}(北京自然月)" - return dm, wm, mm + tds = [td for _, _, td in all_tr if td] + if tds: + am["range_label"] = f"全部历史 {min(tds)} ~ {max(tds)}(北京交易日)" + else: + am["range_label"] = "全部历史(暂无平仓)" + am["monthly_rows"] = _build_monthly_stats_rows(conn, all_tr, seg_key) + return dm, wm, mm, am segments = [] seg_defs = effective_stats_segment_defs( STATS_SEGMENT_DEFS, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED ) for seg_key, seg_title, _meta in seg_defs: - dm, wm, mm = slice_metrics(seg_key) - segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm}) + dm, wm, mm, am = slice_metrics(seg_key) + segments.append( + {"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm, "all": am} + ) - dm, wm, mm = slice_metrics("all") + dm, wm, mm, am = slice_metrics("all") return { "trading_day": trading_day, @@ -1883,6 +1924,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): "day": dm, "week": wm, "month": mm, + "all": am, "segments": segments, "stats_reset_hour": TRADING_DAY_RESET_HOUR, } diff --git a/lib/common/static/instance_page.css b/lib/common/static/instance_page.css index 8e7a0c2..ebeaea6 100644 --- a/lib/common/static/instance_page.css +++ b/lib/common/static/instance_page.css @@ -212,6 +212,13 @@ .inst-stats-details>summary{cursor:pointer;font-size:.84rem;color:#9aa3bf;padding:8px 0;user-select:none;list-style-position:inside} .inst-stats-details>summary::-webkit-details-marker{color:#6d7689} .inst-stats-details[open]>summary{margin-bottom:6px;color:#cfd3ef} + .inst-stats-month-table-wrap{overflow:auto;-webkit-overflow-scrolling:touch} + .inst-stats-month-table{width:100%;border-collapse:collapse;font-size:.8rem;font-variant-numeric:tabular-nums} + .inst-stats-month-table th,.inst-stats-month-table td{padding:8px 10px;text-align:right;border-bottom:1px solid #2a3348;white-space:nowrap} + .inst-stats-month-table th:first-child,.inst-stats-month-table td:first-child{text-align:left} + .inst-stats-month-table th{color:#8892b0;font-weight:600;font-size:.72rem} + .inst-stats-month-table td{color:#e8ecf4} + .inst-stats-month-table tbody tr:last-child td{border-bottom:none} @media (max-width:640px){.inst-stats-kpis{grid-template-columns:1fr}.inst-stats-risk-grid{grid-template-columns:1fr}} .key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150} .key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px} diff --git a/lib/common/static/instance_stats.js b/lib/common/static/instance_stats.js index 7b7045a..c0eecdf 100644 --- a/lib/common/static/instance_stats.js +++ b/lib/common/static/instance_stats.js @@ -1,7 +1,7 @@ (function (global) { "use strict"; - var PERIODS = ["day", "week", "month"]; + var PERIODS = ["day", "week", "month", "all"]; function statsSegmentSelect() { return document.getElementById("stats-segment-select"); diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 30181b4..1adefd7 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -859,6 +859,13 @@ html[data-theme="light"] .inst-stats-details > summary { html[data-theme="light"] .inst-stats-details[open] > summary { color: #0d4a7a !important; } +html[data-theme="light"] .inst-stats-month-table th { + color: #4a6078 !important; +} +html[data-theme="light"] .inst-stats-month-table td { + color: #142232 !important; + border-bottom-color: #d0dae4 !important; +} html[data-theme="light"] .key-history { border-top-color: #d0dae4 !important; diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html index e9a2d07..285148a 100644 --- a/lib/instance/templates/embed_page_fragment.html +++ b/lib/instance/templates/embed_page_fragment.html @@ -78,6 +78,42 @@
期内最大亏损日
{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}
+ {% if period_key == 'all' %} +
+
按月统计
+ {% if s.monthly_rows %} +
+ + + + + + + + + + + + + {% for m in s.monthly_rows %} + {% set m_net_cls = 'pos-pnl-profit' if m.net_pnl_u > 0 else ('pos-pnl-loss' if m.net_pnl_u < 0 else '') %} + + + + + + + + + {% endfor %} + +
月份开单平仓胜率净盈亏最大回撤
{{ m.month_key }}{{ m.opens_count }}{{ m.closed_count }}{% if m.win_rate_pct is not none %}{{ m.win_rate_pct }}%{% else %}—{% endif %}{% if m.net_pnl_u > 0 %}+{% endif %}{{ funds_fmt(m.net_pnl_u) }}{{ funds_fmt(m.max_drawdown_u) }}
+
+ {% else %} +

暂无按月平仓数据

+ {% endif %} +
+ {% endif %} {% endmacro %}
@@ -263,6 +299,7 @@ {% if page == 'records' %} {% include 'records_panel.html' %} {% endif %} +
{% if page == 'env_config' %} {% include 'env_config_panel.html' %} {% endif %} @@ -306,13 +343,14 @@ + {{ period_stats_pane("day", seg.day) }} {{ period_stats_pane("week", seg.week) }} {{ period_stats_pane("month", seg.month) }} + {{ period_stats_pane("all", seg.all) }} {% endfor %} {% endif %} - diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 7e38816..8d8ab11 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -7,8 +7,8 @@ - - + + {{ pwa_app_name }} @@ -158,7 +158,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj - + {% include 'embed_boot_scripts.html' %} diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 0456bab..09d5ed2 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -17,8 +17,8 @@ {{ pwa_app_name }} - - + +
期内最大亏损日
{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}
+ {% if period_key == 'all' %} +
+
按月统计
+ {% if s.monthly_rows %} +
+ + + + + + + + + + + + + {% for m in s.monthly_rows %} + {% set m_net_cls = 'pos-pnl-profit' if m.net_pnl_u > 0 else ('pos-pnl-loss' if m.net_pnl_u < 0 else '') %} + + + + + + + + + {% endfor %} + +
月份开单平仓胜率净盈亏最大回撤
{{ m.month_key }}{{ m.opens_count }}{{ m.closed_count }}{% if m.win_rate_pct is not none %}{{ m.win_rate_pct }}%{% else %}—{% endif %}{% if m.net_pnl_u > 0 %}+{% endif %}{{ funds_fmt(m.net_pnl_u) }}{{ funds_fmt(m.max_drawdown_u) }}
+
+ {% else %} +

暂无按月平仓数据

+ {% endif %} +
+ {% endif %} {% endmacro %}
@@ -384,10 +420,12 @@ +
{{ period_stats_pane("day", seg.day) }} {{ period_stats_pane("week", seg.week) }} {{ period_stats_pane("month", seg.month) }} + {{ period_stats_pane("all", seg.all) }} {% endfor %} @@ -429,7 +467,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj - +