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) %} -
-

{{ title }}

-
{{ s.range_label }}
-
+{% macro period_metrics_cells(s) %}
开单次数
{{ s.opens_count }}
平仓笔数
{{ s.closed_count }}
胜率
{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}
@@ -138,6 +168,20 @@
当前连续亏损笔数
{{ s.consecutive_losses }}
最长连续亏损(交易日)
{{ s.max_loss_streak_days }} 天
期内最大亏损日
{% if s.worst_day %}{{ s.worst_day }}({{ money_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}
+{% endmacro %} +{% macro period_stats_dual(title, pair) %} +
+

{{ title }}

+
{{ pair.range_label }}
+
+
+
机器人下单监控
+
{{ period_metrics_cells(pair.order) }}
+
+
+
趋势回调策略
+
{{ period_metrics_cells(pair.trend) }}
+
{% endmacro %} @@ -171,6 +215,8 @@
{% if page == 'trade' %} +
+

机器人下单监控(单仓)

@@ -232,24 +278,60 @@ -
+
+

实时持仓

+
{% for o in order %} -
-
{{ o.symbol }} | {{ '做多' if o.direction == 'long' else '做空' }}
-
- 风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ money_fmt(o.risk_amount) }}U - | {% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %} -
- 成交:{{ price_fmt(o.symbol, o.trigger_price) }} 止损:{{ price_fmt(o.symbol, o.stop_loss) }} 止盈:{{ price_fmt(o.symbol, o.take_profit) }} - | 盈亏比:{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %} - | 现价:- - | 浮盈亏:- - | 计划基数:{{ money_fmt(o.margin_capital) }}U | 所保证金:- - | 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}% + {% set osym = o.exchange_symbol or o.symbol %} +
+
+
+ {{ osym }} + {{ '做多' if o.direction == 'long' else '做空' }} +
+ 平仓 +
+
+ 来源: 下单监控 | 风格: {{ o.trade_style or 'trend' }} | 风险: {% if o.risk_percent is not none %}{{ o.risk_percent }}%{% else %}—{% endif %}≈{{ money_fmt(o.risk_amount) }}U + | {% if o.breakeven_enabled %}移动保本: 开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(osym, o.breakeven_price) }}{% else %}移动保本: 关{% endif %} +
+
+
+ 成交价 + {{ price_fmt(osym, o.trigger_price) }} +
+
+ 止损 + {{ price_fmt(osym, o.stop_loss) }} +
+
+ 止盈 + {{ price_fmt(osym, o.take_profit) }} +
+
+ 盈亏比 + {% if o.rr_ratio is not none %}{{ '%.2f'|format(o.rr_ratio) }}:1{% else %}—{% endif %} +
+
+ 标记价 + - +
+
+ 浮盈亏 + - +
+
+
+ 保证金: - + | 计划基数: {{ money_fmt(o.margin_capital) }}U + | 杠杆: {{ o.leverage }}x + | 仓位占比: {{ o.position_ratio }}%
- 平仓
+ {% else %} +
暂无机器人持仓
{% endfor %} +
@@ -326,23 +408,87 @@
该预览已过期(超过 {{ trend_pullback_preview_ttl }} 秒),请重新点击「生成预览」。
{% endif %} -

运行中的计划

-
+
+

运行中的计划

+
{% for t in trend_plans %} -
-
#{{ t.id }} {{ t.symbol }} | {{ '做多' if t.direction == 'long' else '做空' }} | {{ t.leverage }}x
-
- 可用快照:{{ money_fmt(t.snapshot_available_usdt) }}U | 计划保证金≈{{ money_fmt(t.plan_margin_capital) }}U | 总张≈{{ amt_fmt(t.symbol, t.target_order_amount) }} 首仓{{ amt_fmt(t.symbol, t.first_order_amount) }} 补仓档{{ t.dca_legs }} -
止损:{{ price_fmt(t.symbol, t.stop_loss) }} 补仓上沿:{{ price_fmt(t.symbol, t.add_upper) }} 止盈:{{ price_fmt(t.symbol, t.take_profit) }} -
均价:{{ price_fmt(t.symbol, t.avg_entry_price) }} 已补仓:{{ t.legs_done }}/{{ t.dca_legs }} -
浮盈亏(交易所): {% if t.floating_pnl is not none %}{{ money_fmt(t.floating_pnl) }} U{% else %}—{% endif %}{% if t.floating_mark is not none %} | 标记价: {{ price_fmt(t.symbol, t.floating_mark) }}{% endif %} + {% set sym = t.exchange_symbol or t.symbol %} + {% set calc = namespace(rr=None, pnlpct=None) %} + {% if t.avg_entry_price is not none and t.stop_loss is not none and t.take_profit is not none %} + {% set e = t.avg_entry_price|float %} + {% set sl = t.stop_loss|float %} + {% set tp = t.take_profit|float %} + {% if t.direction == 'long' %} + {% set risk = e - sl %} + {% set reward = tp - e %} + {% else %} + {% set risk = sl - e %} + {% set reward = e - tp %} + {% endif %} + {% if risk > 0 %} + {% set calc.rr = reward / risk %} + {% endif %} + {% endif %} + {% if t.floating_pnl is not none and t.plan_margin_capital is not none and t.plan_margin_capital|float > 0 %} + {% set calc.pnlpct = (t.floating_pnl|float) / (t.plan_margin_capital|float) * 100 %} + {% endif %} +
+
+
+ #{{ t.id }} {{ sym }} + {{ '做多' if t.direction == 'long' else '做空' }} +
+ 结束计划 +
+
+ 来源: 趋势回调计划 | 风险: {% if t.risk_percent is not none %}{{ t.risk_percent }}%{% else %}—{% endif %} + | 补仓上沿 {{ price_fmt(sym, t.add_upper) }} + | 已补仓 {{ t.legs_done }}/{{ t.dca_legs }} +
+
+
+ 均价 + {% if t.avg_entry_price is not none %}{{ price_fmt(sym, t.avg_entry_price) }}{% else %}—{% endif %} +
+
+ 止损 + {{ price_fmt(sym, t.stop_loss) }} +
+
+ 止盈 + {{ price_fmt(sym, t.take_profit) }} +
+
+ 盈亏比 + {% if calc.rr is not none %}{{ '%.2f'|format(calc.rr) }}:1{% else %}—{% endif %} +
+
+ 标记价 + {% if t.floating_mark is not none %}{{ price_fmt(sym, t.floating_mark) }}{% else %}—{% endif %} +
+
+ 浮盈亏 + + {% if t.floating_pnl is not none %} + {{ money_fmt(t.floating_pnl) }}U{% if calc.pnlpct is not none %} ({{ '%+.2f'|format(calc.pnlpct) }}%){% endif %} + {% else %}—{% endif %} + +
+
+
+ 快照可用: {% if t.snapshot_available_usdt is not none %}{{ money_fmt(t.snapshot_available_usdt) }}U{% else %}—{% endif %} + | 计划保证金≈{% if t.plan_margin_capital is not none %}{{ money_fmt(t.plan_margin_capital) }}U{% else %}—{% endif %} + | 总张≈{{ amt_fmt(sym, t.target_order_amount) }}(首{{ amt_fmt(sym, t.first_order_amount) }} + 补{{ amt_fmt(sym, t.remainder_total) }}) + | 杠杆: {{ t.leverage }}x
- 结束计划
{% else %} -
暂无运行中的趋势回调计划
+
暂无运行中的趋势回调计划
{% endfor %}
+
+
+
{% endif %} @@ -477,12 +623,14 @@
持仓占用导致错过(累计)
{{ occupied_miss_total }}
- 已平仓「机器人下单 / 趋势回调」按平仓时间归入北京时间下的交易日;胜率按盈笔数/(盈+亏)。历史总开仓(累计): - {{ stats_bundle.total_opens_all }} 次 + 已平仓记录按平仓时间归入北京时间交易日;胜率按盈笔数/(盈+亏)。各策略分列统计。
+ 历史总开仓(累计):下单监控 {{ stats_bundle.total_opens_order }} 次 + | 趋势回调 {{ stats_bundle.total_opens_trend }} 次 + (合计 {{ stats_bundle.total_opens_all }} 次)
- {{ period_stats("日统计", stats_bundle.day) }} - {{ period_stats("周统计", stats_bundle.week) }} - {{ period_stats("月统计", stats_bundle.month) }} + {{ period_stats_dual("日统计", stats_bundle.day) }} + {{ period_stats_dual("周统计", stats_bundle.week) }} + {{ period_stats_dual("月统计", stats_bundle.month) }}
{% endif %} @@ -1163,14 +1311,15 @@ function refreshPriceSnapshot(){ const pnlEl = document.getElementById(`order-pnl-${o.id}`); if(pnlEl){ pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`; - pnlEl.classList.remove("price-up","price-down","price-flat"); - if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up"); - else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down"); - else pnlEl.classList.add("price-flat"); + pnlEl.classList.remove("pnl-profit","pnl-loss","pnl-neutral"); + const fp = Number(o.float_pnl); + if(fp > 0) pnlEl.classList.add("pnl-profit"); + else if(fp < 0) pnlEl.classList.add("pnl-loss"); + else pnlEl.classList.add("pnl-neutral"); } const rrEl = document.getElementById(`order-rr-${o.id}`); if(rrEl){ - rrEl.innerText = (typeof o.rr_ratio !== "undefined" && o.rr_ratio !== null) ? `1:${Number(o.rr_ratio).toFixed(2)}` : "-"; + rrEl.innerText = (typeof o.rr_ratio !== "undefined" && o.rr_ratio !== null) ? `${Number(o.rr_ratio).toFixed(2)}:1` : "-"; } }); }).catch(()=>{});