diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 2f5f5fd..60179d0 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -1450,20 +1450,41 @@ def _count_opens_between(conn, start_td, end_td): ).fetchone()[0] -def _load_completed_live_pnls(conn): +def _count_trend_plan_opens_between(conn, start_td, end_td): + """趋势回调:按计划在库里的 session_date(开仓所属北京交易日)计数。""" + return conn.execute( + "SELECT COUNT(*) FROM trend_pullback_plans WHERE session_date IS NOT NULL AND TRIM(session_date) != '' " + "AND session_date >= ? AND session_date <= ?", + (start_td, end_td), + ).fetchone()[0] + + +def _load_completed_trade_pnls(conn, monitor_type: str): + """已平仓实盘记录:按 monitor_type 过滤;趋势回调优先用交易所同步盈亏。""" q = """SELECT pnl_amount, reviewed_pnl_amount, closed_at, reviewed_closed_at, created_at, - result, reviewed_result + result, reviewed_result, exchange_realized_pnl FROM trade_records - WHERE monitor_type = '下单监控' + WHERE monitor_type = ? ORDER BY COALESCE(closed_at, created_at, opened_at) ASC, id ASC""" - rows = conn.execute(q).fetchall() + rows = conn.execute(q, (monitor_type,)).fetchall() out = [] for r in rows: effective_result = (r["reviewed_result"] or r["result"] or "").strip() if effective_result not in TRADE_COMPLETED_RESULTS: continue try: - p = float(r["reviewed_pnl_amount"] if r["reviewed_pnl_amount"] is not None else (r["pnl_amount"] or 0)) + if monitor_type == MONITOR_TYPE_TREND: + ex = None + try: + ex = r["exchange_realized_pnl"] + except (KeyError, IndexError, TypeError): + ex = None + if ex is not None and str(ex).strip() != "": + p = float(ex) + else: + p = float(r["reviewed_pnl_amount"] if r["reviewed_pnl_amount"] is not None else (r["pnl_amount"] or 0)) + else: + p = float(r["reviewed_pnl_amount"] if r["reviewed_pnl_amount"] is not None else (r["pnl_amount"] or 0)) except (TypeError, ValueError): p = 0.0 t = parse_dt_for_trading_day(r["reviewed_closed_at"]) or parse_dt_for_trading_day(r["closed_at"]) or parse_dt_for_trading_day(r["created_at"]) @@ -1537,10 +1558,12 @@ def _compute_period_metrics(trades): def compute_stats_bundle(conn, trading_day, now_dt=None): - """日 / 周 / 月 统计:平仓按平仓时间所在交易日计入。""" + """日 / 周 / 月 统计:平仓按平仓时间所在交易日计入;下单监控与趋势回调分列。""" now_dt = now_dt or app_now() - pnls = _load_completed_live_pnls(conn) - total_opens_all = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0] + pnls_order = _load_completed_trade_pnls(conn, "下单监控") + pnls_trend = _load_completed_trade_pnls(conn, MONITOR_TYPE_TREND) + total_opens_order = conn.execute("SELECT COUNT(*) FROM order_monitors").fetchone()[0] + total_opens_trend = conn.execute("SELECT COUNT(*) FROM trend_pullback_plans").fetchone()[0] w_start, w_end = _session_week_bounds(trading_day) m_start, m_end = _calendar_month_bounds(now_dt) @@ -1552,26 +1575,39 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): _p, _t, td = tr return td and m_start <= td <= m_end - day_trades = [tr for tr in pnls if tr[2] == trading_day] - week_trades = [tr for tr in pnls if in_week(tr)] - month_trades = [tr for tr in pnls if in_month(tr)] + day_range = f"北京时间交易日 {trading_day}" + week_range = f"{w_start} ~ {w_end}(北京日期,近7天窗口)" + month_range = f"{m_start} ~ {m_end}(北京时间自然月)" - dm = _compute_period_metrics(day_trades) - wm = _compute_period_metrics(week_trades) - mm = _compute_period_metrics(month_trades) - dm["opens_count"] = _count_opens_between(conn, trading_day, trading_day) - wm["opens_count"] = _count_opens_between(conn, w_start, w_end) - mm["opens_count"] = _count_opens_between(conn, m_start, m_end) - dm["range_label"] = f"北京时间交易日 {trading_day}" - wm["range_label"] = f"{w_start} ~ {w_end}(北京日期,近7天窗口)" - mm["range_label"] = f"{m_start} ~ {m_end}(北京时间自然月)" + day_o = [tr for tr in pnls_order if tr[2] == trading_day] + day_t = [tr for tr in pnls_trend if tr[2] == trading_day] + dm_o = _compute_period_metrics(day_o) + dm_t = _compute_period_metrics(day_t) + dm_o["opens_count"] = _count_opens_between(conn, trading_day, trading_day) + dm_t["opens_count"] = _count_trend_plan_opens_between(conn, trading_day, trading_day) + + week_o = [tr for tr in pnls_order if in_week(tr)] + week_t = [tr for tr in pnls_trend if in_week(tr)] + wm_o = _compute_period_metrics(week_o) + wm_t = _compute_period_metrics(week_t) + wm_o["opens_count"] = _count_opens_between(conn, w_start, w_end) + wm_t["opens_count"] = _count_trend_plan_opens_between(conn, w_start, w_end) + + month_o = [tr for tr in pnls_order if in_month(tr)] + month_t = [tr for tr in pnls_trend if in_month(tr)] + mm_o = _compute_period_metrics(month_o) + mm_t = _compute_period_metrics(month_t) + mm_o["opens_count"] = _count_opens_between(conn, m_start, m_end) + mm_t["opens_count"] = _count_trend_plan_opens_between(conn, m_start, m_end) return { "trading_day": trading_day, - "total_opens_all": total_opens_all, - "day": dm, - "week": wm, - "month": mm, + "total_opens_order": total_opens_order, + "total_opens_trend": total_opens_trend, + "total_opens_all": int(total_opens_order) + int(total_opens_trend), + "day": {"range_label": day_range, "order": dm_o, "trend": dm_t}, + "week": {"range_label": week_range, "order": wm_o, "trend": wm_t}, + "month": {"range_label": month_range, "order": mm_o, "trend": mm_t}, } diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 8e0d526..580040d 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -58,6 +58,7 @@ .direction-short{background:#331e24;color:#ff6666} .pnl-profit{color:#4cd97f;font-weight:600} .pnl-loss{color:#ff6666;font-weight:600} + .pnl-neutral{color:#cfd3ef;font-weight:600} .flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164} .ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px} .price-up{color:#4cd97f} @@ -81,21 +82,48 @@ .detail-modal .panel-body{white-space:pre-wrap;line-height:1.5;font-size:.86rem;color:#e5e9ff} .detail-modal .panel-image{margin-top:10px;max-width:min(100%,680px);border-radius:8px;cursor:pointer;border:1px solid #2a3150} .table-wrap{overflow-x:auto} - .order-card{grid-column:1/-1} - .trend-card{grid-column:1/-1} + .trade-dashboard{grid-column:1/-1;display:flex;flex-direction:column;gap:14px} + .trade-panels-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch} + .trade-panels-row > .card{min-height:0;height:100%;display:flex;flex-direction:column;box-sizing:border-box} + .trade-panels-row > .trend-card{gap:12px} + .trade-panels-row > .order-card .order-live-positions{margin-top:auto;flex:0 1 auto;min-height:0} + .order-live-positions{display:flex;flex-direction:column;gap:10px;margin-top:12px;padding-top:14px;border-top:1px solid #2a3150;max-height:240px;overflow:auto} + .order-live-positions .running-plans-stack{margin-top:0} + .trade-panels-row > .trend-card .trend-running-plans{margin-top:auto} + .trend-running-plans{padding-top:14px;border-top:1px solid #2a3150} + .running-plans-stack{display:flex;flex-direction:column;gap:12px;margin-top:10px} + .plan-position-card{background:#141a2a;border:1px solid #2a3150;border-radius:12px;padding:12px 14px} + .plan-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:8px} + .plan-card-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:1rem;font-weight:700;color:#f0f2ff} + .plan-card-meta{font-size:.76rem;color:#8892b0;line-height:1.55;margin-bottom:10px} + .plan-card-meta .accent{color:#6ab8ff} + .plan-card-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px 14px;margin-bottom:10px} + @media (max-width:720px){ + .plan-card-grid{grid-template-columns:1fr} + } + .plan-cell{display:flex;flex-direction:column;gap:3px} + .plan-cell .lbl{font-size:.72rem;color:#8b95b8} + .plan-cell .val.pnl-profit,.plan-cell .val .pnl-profit{color:#4cd97f} + .plan-cell .val.pnl-loss,.plan-cell .val .pnl-loss{color:#ff6666} + .plan-cell .val.pnl-neutral,.plan-cell .val .pnl-neutral{color:#cfd3ef} + .btn-close-plan{padding:7px 14px;background:#5c1e2a;color:#ffb4b4;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap} + .btn-close-plan:hover{filter:brightness(1.08)} .records-card{grid-column:1/-1} .review-card{grid-column:1/-1} @media (min-width: 1900px){ .container{max-width:2100px} - .order-card .list{max-height:420px} + .order-card .order-live-positions{max-height:420px} .records-card .table-wrap{max-height:620px;overflow:auto} } @media (max-width: 1400px){ .container{width:min(99vw,1600px)} .grid{grid-template-columns:1fr} - .order-card,.records-card,.review-card{grid-column:auto} + .trade-dashboard,.records-card,.review-card{grid-column:auto} .panel-list{grid-template-columns:1fr} } + @media (max-width:1200px){ + .trade-panels-row{grid-template-columns:1fr} + } @media (max-width: 960px){ body{padding:10px} .form-grid{grid-template-columns:repeat(2,minmax(0,1fr))} @@ -119,14 +147,16 @@ .stats-period-block:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0} .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-split-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:start} + .stats-split-col{min-width:0;background:#101522;border:1px solid #252a45;border-radius:10px;padding:10px 12px} + .stats-split-head{font-size:.88rem;font-weight:600;color:#b8c4ff;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #2a3150} + @media (max-width:900px){ + .stats-split-row{grid-template-columns:1fr} + }
-{% macro period_stats(title, s) %} -