fix: 统计日历服务端内嵌 bootstrap,首屏显示盈亏与笔数

与月统计同源 initial_calendar 写入页面,API 失败时仍渲染;四所日历路由独立注册并传入 get_db_fn。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-30 09:13:58 +08:00
parent 4f784d09ac
commit 3b687d17eb
15 changed files with 195 additions and 53 deletions
+17 -3
View File
@@ -1888,6 +1888,12 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
dm, wm, mm = slice_metrics("all") 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 { return {
"trading_day": trading_day, "trading_day": trading_day,
"total_opens_all": total_opens_all, "total_opens_all": total_opens_all,
@@ -1896,6 +1902,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
"month": mm, "month": mm,
"segments": segments, "segments": segments,
"stats_reset_hour": TRADING_DAY_RESET_HOUR, "stats_reset_hour": TRADING_DAY_RESET_HOUR,
"initial_calendar": initial_calendar,
} }
@@ -9617,7 +9624,7 @@ try:
_repo_root = Path(__file__).resolve().parent.parent _repo_root = Path(__file__).resolve().parent.parent
if str(_repo_root) not in sys.path: if str(_repo_root) not in sys.path:
sys.path.insert(0, str(_repo_root)) 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( install_on_app(
app, app,
@@ -9637,15 +9644,22 @@ try:
render_main_page_fn=render_main_page, render_main_page_fn=render_main_page,
login_required_fn=login_required, 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( register_trade_stats_calendar_route(
app, app,
login_required_fn=login_required, login_required_fn=login_required,
load_pnls_fn=_load_completed_trade_pnls, load_pnls_fn=_load_completed_trade_pnls,
row_matches_segment_fn=_pnl_row_matches_segment, row_matches_segment_fn=_pnl_row_matches_segment,
reset_hour=TRADING_DAY_RESET_HOUR, reset_hour=TRADING_DAY_RESET_HOUR,
get_db_fn=get_db,
) )
except Exception as _hub_err: except Exception as _cal_err:
print(f"[hub_bridge] binance: {_hub_err}") print(f"[stats calendar] binance: {_cal_err}")
@app.route("/strategy") @app.route("/strategy")
+8 -5
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=36"></script> <script src="/static/instance_theme.js?v=46"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<script src="/static/account_risk_badge.js?v=4"></script> <script src="/static/account_risk_badge.js?v=4"></script>
@@ -243,8 +243,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .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} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=38"> <link rel="stylesheet" href="/static/instance_theme.css?v=48">
<link rel="stylesheet" href="/static/trade_stats_calendar.css?v=3"> <link rel="stylesheet" href="/static/trade_stats_calendar.css?v=4">
</head> </head>
<body <body
@@ -807,6 +807,9 @@
</select> </select>
</label> </label>
</div> </div>
{% if stats_bundle.initial_calendar %}
<script type="application/json" id="stats-calendar-bootstrap">{{ stats_bundle.initial_calendar | tojson }}</script>
{% endif %}
<div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap"> <div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap">
<div class="trade-cal-head"> <div class="trade-cal-head">
<button type="button" id="stats-cal-prev" class="btn" title="上一月"></button> <button type="button" id="stats-cal-prev" class="btn" title="上一月"></button>
@@ -844,14 +847,14 @@
</div> </div>
</div> </div>
<script src="/static/instance_ui.js?v=3"></script> <script src="/static/instance_ui.js?v=4"></script>
<script src="/static/instance_records_mobile.js?v=2"></script> <script src="/static/instance_records_mobile.js?v=2"></script>
<script src="/static/time_close_ui.js?v=2"></script> <script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script> <script src="/static/strategy_roll.js?v=5"></script>
<script src="/static/trade_stats_calendar.js?v=3"></script> <script src="/static/trade_stats_calendar.js?v=4"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }}; const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
+17 -3
View File
@@ -1883,6 +1883,12 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
dm, wm, mm = slice_metrics("all") 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 { return {
"trading_day": trading_day, "trading_day": trading_day,
"total_opens_all": total_opens_all, "total_opens_all": total_opens_all,
@@ -1891,6 +1897,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
"month": mm, "month": mm,
"segments": segments, "segments": segments,
"stats_reset_hour": TRADING_DAY_RESET_HOUR, "stats_reset_hour": TRADING_DAY_RESET_HOUR,
"initial_calendar": initial_calendar,
} }
@@ -9537,7 +9544,7 @@ try:
_repo_root = Path(__file__).resolve().parent.parent _repo_root = Path(__file__).resolve().parent.parent
if str(_repo_root) not in sys.path: if str(_repo_root) not in sys.path:
sys.path.insert(0, str(_repo_root)) 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( install_on_app(
app, app,
@@ -9558,15 +9565,22 @@ try:
render_main_page_fn=render_main_page, render_main_page_fn=render_main_page,
login_required_fn=login_required, login_required_fn=login_required,
) )
except Exception as _hub_err:
print(f"[hub_bridge] gate: {_hub_err}")
try:
from hub_bridge import register_trade_stats_calendar_route
register_trade_stats_calendar_route( register_trade_stats_calendar_route(
app, app,
login_required_fn=login_required, login_required_fn=login_required,
load_pnls_fn=_load_completed_trade_pnls, load_pnls_fn=_load_completed_trade_pnls,
row_matches_segment_fn=_pnl_row_matches_segment, row_matches_segment_fn=_pnl_row_matches_segment,
reset_hour=TRADING_DAY_RESET_HOUR, reset_hour=TRADING_DAY_RESET_HOUR,
get_db_fn=get_db,
) )
except Exception as _hub_err: except Exception as _cal_err:
print(f"[hub_bridge] gate: {_hub_err}") print(f"[stats calendar] gate: {_cal_err}")
@app.route("/strategy") @app.route("/strategy")
+8 -5
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=36"></script> <script src="/static/instance_theme.js?v=46"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<script src="/static/account_risk_badge.js?v=4"></script> <script src="/static/account_risk_badge.js?v=4"></script>
@@ -243,8 +243,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .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} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=38"> <link rel="stylesheet" href="/static/instance_theme.css?v=48">
<link rel="stylesheet" href="/static/trade_stats_calendar.css?v=3"> <link rel="stylesheet" href="/static/trade_stats_calendar.css?v=4">
</head> </head>
<body <body
@@ -774,6 +774,9 @@
</select> </select>
</label> </label>
</div> </div>
{% if stats_bundle.initial_calendar %}
<script type="application/json" id="stats-calendar-bootstrap">{{ stats_bundle.initial_calendar | tojson }}</script>
{% endif %}
<div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap"> <div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap">
<div class="trade-cal-head"> <div class="trade-cal-head">
<button type="button" id="stats-cal-prev" class="btn" title="上一月"></button> <button type="button" id="stats-cal-prev" class="btn" title="上一月"></button>
@@ -811,14 +814,14 @@
</div> </div>
</div> </div>
<script src="/static/instance_ui.js?v=3"></script> <script src="/static/instance_ui.js?v=4"></script>
<script src="/static/instance_records_mobile.js?v=2"></script> <script src="/static/instance_records_mobile.js?v=2"></script>
<script src="/static/time_close_ui.js?v=2"></script> <script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script> <script src="/static/strategy_roll.js?v=5"></script>
<script src="/static/trade_stats_calendar.js?v=3"></script> <script src="/static/trade_stats_calendar.js?v=4"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }}; const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
+17 -3
View File
@@ -1883,6 +1883,12 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
dm, wm, mm = slice_metrics("all") 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 { return {
"trading_day": trading_day, "trading_day": trading_day,
"total_opens_all": total_opens_all, "total_opens_all": total_opens_all,
@@ -1891,6 +1897,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
"month": mm, "month": mm,
"segments": segments, "segments": segments,
"stats_reset_hour": TRADING_DAY_RESET_HOUR, "stats_reset_hour": TRADING_DAY_RESET_HOUR,
"initial_calendar": initial_calendar,
} }
@@ -9533,7 +9540,7 @@ try:
_repo_root = Path(__file__).resolve().parent.parent _repo_root = Path(__file__).resolve().parent.parent
if str(_repo_root) not in sys.path: if str(_repo_root) not in sys.path:
sys.path.insert(0, str(_repo_root)) 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( install_on_app(
app, app,
@@ -9554,15 +9561,22 @@ try:
render_main_page_fn=render_main_page, render_main_page_fn=render_main_page,
login_required_fn=login_required, login_required_fn=login_required,
) )
except Exception as _hub_err:
print(f"[hub_bridge] gate_bot: {_hub_err}")
try:
from hub_bridge import register_trade_stats_calendar_route
register_trade_stats_calendar_route( register_trade_stats_calendar_route(
app, app,
login_required_fn=login_required, login_required_fn=login_required,
load_pnls_fn=_load_completed_trade_pnls, load_pnls_fn=_load_completed_trade_pnls,
row_matches_segment_fn=_pnl_row_matches_segment, row_matches_segment_fn=_pnl_row_matches_segment,
reset_hour=TRADING_DAY_RESET_HOUR, reset_hour=TRADING_DAY_RESET_HOUR,
get_db_fn=get_db,
) )
except Exception as _hub_err: except Exception as _cal_err:
print(f"[hub_bridge] gate_bot: {_hub_err}") print(f"[stats calendar] gate_bot: {_cal_err}")
@app.route("/strategy") @app.route("/strategy")
+8 -5
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=36"></script> <script src="/static/instance_theme.js?v=46"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<script src="/static/account_risk_badge.js?v=4"></script> <script src="/static/account_risk_badge.js?v=4"></script>
@@ -243,8 +243,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .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} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=38"> <link rel="stylesheet" href="/static/instance_theme.css?v=48">
<link rel="stylesheet" href="/static/trade_stats_calendar.css?v=3"> <link rel="stylesheet" href="/static/trade_stats_calendar.css?v=4">
</head> </head>
<body <body
@@ -774,6 +774,9 @@
</select> </select>
</label> </label>
</div> </div>
{% if stats_bundle.initial_calendar %}
<script type="application/json" id="stats-calendar-bootstrap">{{ stats_bundle.initial_calendar | tojson }}</script>
{% endif %}
<div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap"> <div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap">
<div class="trade-cal-head"> <div class="trade-cal-head">
<button type="button" id="stats-cal-prev" class="btn" title="上一月"></button> <button type="button" id="stats-cal-prev" class="btn" title="上一月"></button>
@@ -811,14 +814,14 @@
</div> </div>
</div> </div>
<script src="/static/instance_ui.js?v=3"></script> <script src="/static/instance_ui.js?v=4"></script>
<script src="/static/instance_records_mobile.js?v=2"></script> <script src="/static/instance_records_mobile.js?v=2"></script>
<script src="/static/time_close_ui.js?v=2"></script> <script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script> <script src="/static/strategy_roll.js?v=5"></script>
<script src="/static/trade_stats_calendar.js?v=3"></script> <script src="/static/trade_stats_calendar.js?v=4"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }}; const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
+17 -3
View File
@@ -1861,6 +1861,12 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
dm, wm, mm = slice_metrics("all") 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 { return {
"trading_day": trading_day, "trading_day": trading_day,
"total_opens_all": total_opens_all, "total_opens_all": total_opens_all,
@@ -1869,6 +1875,7 @@ def compute_stats_bundle(conn, trading_day, now_dt=None):
"month": mm, "month": mm,
"segments": segments, "segments": segments,
"stats_reset_hour": TRADING_DAY_RESET_HOUR, "stats_reset_hour": TRADING_DAY_RESET_HOUR,
"initial_calendar": initial_calendar,
} }
@@ -8998,7 +9005,7 @@ try:
_repo_root = Path(__file__).resolve().parent.parent _repo_root = Path(__file__).resolve().parent.parent
if str(_repo_root) not in sys.path: if str(_repo_root) not in sys.path:
sys.path.insert(0, str(_repo_root)) 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( install_on_app(
app, app,
@@ -9018,15 +9025,22 @@ try:
render_main_page_fn=render_main_page, render_main_page_fn=render_main_page,
login_required_fn=login_required, login_required_fn=login_required,
) )
except Exception as _hub_err:
print(f"[hub_bridge] okx: {_hub_err}")
try:
from hub_bridge import register_trade_stats_calendar_route
register_trade_stats_calendar_route( register_trade_stats_calendar_route(
app, app,
login_required_fn=login_required, login_required_fn=login_required,
load_pnls_fn=_load_completed_trade_pnls, load_pnls_fn=_load_completed_trade_pnls,
row_matches_segment_fn=_pnl_row_matches_segment, row_matches_segment_fn=_pnl_row_matches_segment,
reset_hour=TRADING_DAY_RESET_HOUR, reset_hour=TRADING_DAY_RESET_HOUR,
get_db_fn=get_db,
) )
except Exception as _hub_err: except Exception as _cal_err:
print(f"[hub_bridge] okx: {_hub_err}") print(f"[stats calendar] okx: {_cal_err}")
@app.route("/strategy") @app.route("/strategy")
+8 -5
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=36"></script> <script src="/static/instance_theme.js?v=46"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<script src="/static/account_risk_badge.js?v=4"></script> <script src="/static/account_risk_badge.js?v=4"></script>
@@ -243,8 +243,8 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .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} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=38"> <link rel="stylesheet" href="/static/instance_theme.css?v=48">
<link rel="stylesheet" href="/static/trade_stats_calendar.css?v=3"> <link rel="stylesheet" href="/static/trade_stats_calendar.css?v=4">
</head> </head>
<body <body
@@ -803,6 +803,9 @@
</select> </select>
</label> </label>
</div> </div>
{% if stats_bundle.initial_calendar %}
<script type="application/json" id="stats-calendar-bootstrap">{{ stats_bundle.initial_calendar | tojson }}</script>
{% endif %}
<div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap"> <div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap">
<div class="trade-cal-head"> <div class="trade-cal-head">
<button type="button" id="stats-cal-prev" class="btn" title="上一月"></button> <button type="button" id="stats-cal-prev" class="btn" title="上一月"></button>
@@ -840,14 +843,14 @@
</div> </div>
</div> </div>
<script src="/static/instance_ui.js?v=3"></script> <script src="/static/instance_ui.js?v=4"></script>
<script src="/static/instance_records_mobile.js?v=2"></script> <script src="/static/instance_records_mobile.js?v=2"></script>
<script src="/static/time_close_ui.js?v=2"></script> <script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script> <script src="/static/strategy_roll.js?v=5"></script>
<script src="/static/trade_stats_calendar.js?v=3"></script> <script src="/static/trade_stats_calendar.js?v=4"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }}; const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
+3
View File
@@ -452,6 +452,9 @@
</select> </select>
</label> </label>
</div> </div>
{% if stats_bundle.initial_calendar %}
<script type="application/json" id="stats-calendar-bootstrap">{{ stats_bundle.initial_calendar | tojson }}</script>
{% endif %}
<div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap"> <div id="stats-calendar-wrap" class="trade-cal-wrap stats-calendar-wrap">
<div class="trade-cal-head"> <div class="trade-cal-head">
<button type="button" id="stats-cal-prev" class="btn" title="上一月"></button> <button type="button" id="stats-cal-prev" class="btn" title="上一月"></button>
+5 -5
View File
@@ -3,12 +3,12 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=37"></script> <script src="/static/instance_theme.js?v=47"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<link rel="stylesheet" href="/static/instance_page.css?v=2"> <link rel="stylesheet" href="/static/instance_page.css?v=2">
<link rel="stylesheet" href="/static/instance_theme.css?v=38"> <link rel="stylesheet" href="/static/instance_theme.css?v=48">
<link rel="stylesheet" href="/static/trade_stats_calendar.css?v=3"> <link rel="stylesheet" href="/static/trade_stats_calendar.css?v=4">
<script src="/static/account_risk_badge.js?v=4"></script> <script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title> <title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
@@ -113,7 +113,7 @@
</div> </div>
</div> </div>
<script src="/static/instance_ui.js?v=3"></script> <script src="/static/instance_ui.js?v=4"></script>
<script src="/static/instance_records_mobile.js?v=2"></script> <script src="/static/instance_records_mobile.js?v=2"></script>
<script src="/static/time_close_ui.js?v=2"></script> <script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
@@ -121,7 +121,7 @@
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script> <script src="/static/strategy_roll.js?v=5"></script>
<script src="/static/key_monitor_form.js?v=2"></script> <script src="/static/key_monitor_form.js?v=2"></script>
<script src="/static/trade_stats_calendar.js?v=3"></script> <script src="/static/trade_stats_calendar.js?v=4"></script>
{% include 'embed_boot_scripts.html' %} {% include 'embed_boot_scripts.html' %}
<script src="/static/instance_embed.js?v=5"></script> <script src="/static/instance_embed.js?v=5"></script>
</body> </body>
+2 -1
View File
@@ -92,6 +92,7 @@ def register_trade_stats_calendar_route(
load_pnls_fn, load_pnls_fn,
row_matches_segment_fn, row_matches_segment_fn,
reset_hour: int, reset_hour: int,
get_db_fn=None,
): ):
"""四所统计分析页:按月返回各交易日盈亏/笔数。""" """四所统计分析页:按月返回各交易日盈亏/笔数。"""
from flask import jsonify, request from flask import jsonify, request
@@ -110,7 +111,7 @@ def register_trade_stats_calendar_route(
now = datetime.now() now = datetime.now()
year = year or now.year year = year or now.year
month = month or now.month 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: if not get_db:
return jsonify({"ok": False, "msg": "未配置数据库"}), 500 return jsonify({"ok": False, "msg": "未配置数据库"}), 500
conn = get_db() conn = get_db()
+5 -1
View File
@@ -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]: 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,
}
+45 -13
View File
@@ -89,6 +89,38 @@
this.month = d.getMonth() + 1; 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) { TradeStatsCalendar.prototype.setSelectedDay = function (day) {
this.selectedDay = day || ""; this.selectedDay = day || "";
this.render(); this.render();
@@ -192,23 +224,12 @@
}); });
if (!resp.ok) { if (!resp.ok) {
console.warn("[trade calendar] api", resp.status); console.warn("[trade calendar] api", resp.status);
this.render();
return; return;
} }
data = await resp.json(); data = await resp.json();
} }
this.days = this.parseResponse(data) || {}; this.applyPayload(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.render(); this.render();
if (this.onMonthChange) this.onMonthChange(this.year, this.month, this.days); if (this.onMonthChange) this.onMonthChange(this.year, this.month, this.days);
} catch (e) { } catch (e) {
@@ -253,6 +274,16 @@
global.initInstanceStatsCalendar = function () { global.initInstanceStatsCalendar = function () {
var grid = document.getElementById("stats-calendar"); var grid = document.getElementById("stats-calendar");
if (!grid || !global.TradeStatsCalendar) return null; 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({ global.statsCalendarWidget = new TradeStatsCalendar({
gridEl: grid, gridEl: grid,
titleEl: document.getElementById("stats-cal-title"), titleEl: document.getElementById("stats-cal-title"),
@@ -273,6 +304,7 @@
return (data && data.days) || {}; return (data && data.days) || {};
}, },
}); });
if (bootstrap) global.statsCalendarWidget.applyPayload(bootstrap);
global.statsCalendarWidget.render(); global.statsCalendarWidget.render();
void global.statsCalendarWidget.load(); void global.statsCalendarWidget.load();
return global.statsCalendarWidget; return global.statsCalendarWidget;
+16 -1
View File
@@ -1,7 +1,9 @@
import unittest import unittest
from types import SimpleNamespace 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): def _row(**kwargs):
@@ -54,6 +56,19 @@ class TradeStatsCalendarLibTests(unittest.TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
build_trade_stats_calendar([], 2026, 13, "all", _matches_all) 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__": if __name__ == "__main__":
unittest.main() unittest.main()
+19
View File
@@ -71,3 +71,22 @@ def build_trade_stats_calendar(
"month_pnl_total": round(month_pnl, 4), "month_pnl_total": round(month_pnl, 4),
"month_open_count": month_count, "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,
)