前端面板修改

This commit is contained in:
dekun
2026-05-17 08:42:50 +08:00
parent eb32ec70b5
commit ff62666c4d
11 changed files with 1229 additions and 183 deletions
+178 -47
View File
@@ -149,7 +149,7 @@
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
</head>
<body>
<body data-page="{{ page }}">
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
@@ -200,10 +200,11 @@
<div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div>
<div class="grid">
{% if page == 'trade' %}
<div class="card monitor-card">
{% if page == 'key_monitor' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">关键位监控5m</h2>
<h2 style="margin-bottom:0">关键位监控</h2>
{% if focus_key_id %}
<a href="/key_focus?key_id={{ focus_key_id }}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(默认200根)</a>
{% else %}
@@ -225,44 +226,69 @@
<input name="lower" step="0.0001" placeholder="下沿/支撑" required>
<button type="submit">添加</button>
</form>
<div class="list">
<div class="rule-tip">{{ key_gate_rule_text }}</div>
<div class="panel-scroll pos-list">
{% for k in key %}
<div class="list-item" id="key-row-{{ k.id }}">
<div><strong>{{ k.symbol }}</strong> | {{ k.monitor_type }} | <span class="badge direction">{{ '做多' if k.direction == 'long' else '做空' }}</span></div>
<div>
上:{{ price_fmt(k.symbol, k.upper) }} 下:{{ price_fmt(k.symbol, k.lower) }}
| 已提醒:{{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
| 现价:<span id="key-price-{{ k.id }}">-</span>
| 距上沿:<span id="key-up-diff-{{ k.id }}">-</span>
| 距下沿:<span id="key-low-diff-{{ k.id }}">-</span>
| 门控:<span id="key-gate-{{ k.id }}" style="color:#9aa">-</span>
<span id="key-gate-metrics-{{ k.id }}" style="margin-left:8px;color:#8fc8ff;font-size:.78rem"></span>
<div class="pos-card" id="key-row-{{ k.id }}">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ k.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ '做多' if k.direction == 'long' else '做空' }}</span>
<span class="badge direction" style="margin-left:4px">{{ k.monitor_type }}</span>
</div>
<button type="button" class="pos-close-btn" style="border:none;cursor:pointer" onclick="deleteKeyMonitor({{ k.id }})"></button>
</div>
<button type="button" class="btn-del" style="border:none;cursor:pointer" onclick="deleteKeyMonitor({{ k.id }})"></button>
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
</div>
<div class="pos-grid">
<div class="pos-cell"><span class="pos-label">现价</span><span class="pos-value" id="key-price-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">距上沿</span><span class="pos-value" id="key-up-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">距下沿</span><span class="pos-value" id="key-low-diff-{{ k.id }}">-</span></div>
<div class="pos-cell"><span class="pos-label">门控</span><span class="pos-value" id="key-gate-{{ k.id }}" style="color:#9aa">-</span></div>
</div>
<div class="pos-meta" style="margin-top:8px"><span class="pos-meta-item" id="key-gate-metrics-{{ k.id }}" style="color:#8fc8ff"></span></div>
</div>
{% else %}
<div class="pos-empty">暂无监控中的关键位</div>
{% endfor %}
</div>
<div class="key-history">
<h3>关键位历史(满次提醒或手动删除)</h3>
<div class="sub">每种关键位触发后<strong>一次性结案</strong>并写入下方历史:箱体/收敛在计划 RR 达标时<strong>自动市价开仓</strong>;阻力/支撑仅<strong>单次企业微信提醒</strong>。详见项目根目录「关键位自动下单说明.md」。满多次提醒的旧逻辑已不适用。</div>
<div class="list">
{% for h in key_history %}
<div class="list-item">
<div>
<strong>{{ h.symbol }}</strong> | {{ h.monitor_type }} | {{ '做多' if h.direction == 'long' else '做空' }} | {{ h.close_reason }}
<button type="button" class="table-del" style="margin-left:8px" onclick="deleteKeyHistory({{ h.id }})">删除</button>
</div>
<div class="card">
<h2 style="margin-bottom:8px">关键位历史</h2>
<div class="sub" style="font-size:.72rem;color:#8892b0;margin-bottom:8px">失效或已结案的关键位</div>
<div class="panel-scroll pos-list">
{% for h in key_history %}
<div class="pos-card">
<div class="pos-card-head">
<div class="pos-card-symbol">
<strong>{{ h.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ '做多' if h.direction == 'long' else '做空' }}</span>
</div>
<div>上:{{ price_fmt(h.symbol, h.upper) }} 下:{{ price_fmt(h.symbol, h.lower) }} | 提醒次数:{{ h.notification_count }} | {{ (h.closed_at or '-')[:16] }}</div>
{% if h.last_alert_message %}<div style="font-size:.78rem;color:#aab;margin-top:4px;white-space:pre-wrap">{{ h.last_alert_message[:200] }}{% if h.last_alert_message|length > 200 %}…{% endif %}</div>{% endif %}
<button type="button" class="table-del" onclick="deleteKeyHistory({{ h.id }})">删除</button>
</div>
{% else %}
<div class="list-item" style="color:#8892b0">暂无历史</div>
{% endfor %}
<div class="pos-meta">
<span class="pos-meta-item">{{ h.monitor_type }}</span>
<span class="pos-meta-item">{{ h.close_reason }}</span>
<span class="pos-meta-item">{{ (h.closed_at or '-')[:16] }}</span>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上: {{ h.upper }} 下: {{ h.lower }}</span>
<span class="pos-meta-item">提醒: {{ h.notification_count }}</span>
</div>
{% if h.last_alert_message %}<div style="font-size:.75rem;color:#aab;margin-top:6px;white-space:pre-wrap">{{ h.last_alert_message[:180] }}{% if h.last_alert_message|length > 180 %}…{% endif %}</div>{% endif %}
</div>
{% else %}
<div class="pos-empty">暂无历史</div>
{% endfor %}
</div>
</div>
<div class="card order-card">
</div>
{% elif page == 'trade' %}
<div class="dual-panel-grid" style="grid-column:1/-1">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
<h2 style="margin-bottom:0">实盘下单监控</h2>
{% if focus_order_id %}
@@ -272,15 +298,15 @@
{% endif %}
</div>
<div class="rule-tip" id="order-rule-tip">
规则:仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
{% if can_trade %}可开仓{% else %}不可开仓(持仓或未到北京时间 {{ reset_hour }}:00{% endif %}
按风险比例自动计算仓位
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
{% if can_trade %}可开仓{% else %}不可开仓(持仓已满或未到北京时间 {{ reset_hour }}:00{% endif %}
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
</div>
<div class="rule-tip">
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
<div class="rule-tip">
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ usdt_fmt(auto_transfer_amount) }}U,来自 {{ auto_transfer_from }}
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ auto_transfer_amount }}U,来自 {{ auto_transfer_from }}
</div>
<form action="/manual_transfer" method="post" class="form-row">
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
@@ -296,7 +322,7 @@
</select>
<button type="submit">手动划转</button>
</form>
<form action="/add_order" method="post" class="form-row">
<form id="add-order-form" action="/add_order" method="post" class="form-row">
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select id="order-direction" name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
@@ -323,9 +349,10 @@
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
<button type="submit">开仓(以损定仓)</button>
</form>
<div class="pos-section">
<div class="pos-section-title">实时持仓</div>
<div class="pos-list">
</div>
<div class="card">
<h2 style="margin-bottom:8px">实时持仓</h2>
<div class="panel-scroll pos-list">
{% for o in order %}
<div class="pos-card" id="order-row-{{ o.id }}">
<div class="pos-card-head">
@@ -387,12 +414,13 @@
{% else %}
<div class="pos-empty">暂无持仓</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if page == 'records' %}
<div class="card full records-card">
<h2>交易记录 & 错过机会</h2>
@@ -1149,8 +1177,9 @@ if(keyForm){
alert((data && data.msg) || "日成交量排名读取失败");
return;
}
const rankMax = data.rank_max || 30;
if(!data.in_top30){
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前30,已拦截。`);
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
return;
}
keyForm.submit();
@@ -1164,6 +1193,19 @@ setTimeout(() => {
if(document.getElementById("review-list")) loadReviews();
}, 300);
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
function calcClientRr(direction, entry, sl, tp){
const e = Number(entry), s = Number(sl), t = Number(tp);
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
if(direction === 'short'){
if(s <= e || t >= e) return null;
return (e - t) / (s - e);
}
if(s >= e || t <= e) return null;
return (t - e) / (e - s);
}
let latestAvailableUsdt = null;
const lastPriceMap = {};
@@ -1327,11 +1369,11 @@ function refreshAccountSnapshot(){
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
latestAvailableUsdt = Number(data.available_trading_usdt);
}
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(持仓或未到北京时间 {{ reset_hour }}:00";
const canTradeText = data.can_trade ? "可开仓" : `不可开仓(持仓 ${data.active_count||0}/${data.max_active_positions||{{ max_active_positions }}} 或未到北京时间 {{ reset_hour }}:00`;
const tip = document.getElementById("order-rule-tip");
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约 ${formatUsdt2(latestAvailableUsdt)}U` : "";
if(tip){
tip.innerText = `规则:仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`;
tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`;
}
}).catch(()=>{});
}
@@ -1383,10 +1425,99 @@ if(_journalFormEl){
if(_jErSel) _jErSel.addEventListener("change", syncJournalEntryReasonOtherUi);
syncJournalEntryReasonOtherUi();
}
const addOrderForm = document.getElementById("add-order-form");
if(addOrderForm){
addOrderForm.addEventListener("submit", function(ev){
if(addOrderForm.dataset.rrOk === "1"){
addOrderForm.dataset.rrOk = "0";
return;
}
ev.preventDefault();
const direction = (document.getElementById("order-direction")||{}).value || "long";
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
let sl, tp, entry;
if(mode === "pct"){
alert("百分比模式请确认盈亏比后再提交;建议使用价格模式以便校验。");
return;
}
sl = Number((document.getElementById("order-sl")||{}).value);
tp = Number((document.getElementById("order-tp")||{}).value);
entry = sl;
fetch(`/api/order_defaults?symbol=${encodeURIComponent((document.getElementById("order-symbol")||{}).value||"")}&direction=${encodeURIComponent(direction)}`)
.then(r=>r.json())
.then(data=>{
const px = data.last_price || data.price;
if(px) entry = Number(px);
const rr = calcClientRr(direction, entry, sl, tp);
if(rr === null || rr < MANUAL_MIN_PLANNED_RR){
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单。`);
return;
}
addOrderForm.dataset.rrOk = "1";
addOrderForm.submit();
})
.catch(()=>{ alert("无法校验盈亏比,请稍后重试"); });
});
}
refreshOrderDefaults();
refreshPriceSnapshot();
refreshPriceSnapshotConditional();
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }});
function refreshPriceSnapshotConditional(){
const page = document.body.getAttribute("data-page") || "";
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
const updatedEl = document.getElementById("price-last-updated");
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
if(page === "key_monitor"){
(data.key_prices || []).forEach(k=>{
const pEl = document.getElementById(`key-price-${k.id}`);
if(pEl){ pEl.innerText = k.price_display || (Number.isFinite(Number(k.price)) ? Number(k.price).toFixed(6) : "-"); paintPriceTrend(pEl, `k-${k.id}`, Number(k.price)); }
const upEl = document.getElementById(`key-up-diff-${k.id}`);
if(upEl) upEl.innerText = `${formatSigned(k.upper_diff, 4)} (${formatSigned(k.upper_pct, 2)}%)`;
const lowEl = document.getElementById(`key-low-diff-${k.id}`);
if(lowEl) lowEl.innerText = `${formatSigned(k.lower_diff, 4)} (${formatSigned(k.lower_pct, 2)}%)`;
const gateEl = document.getElementById(`key-gate-${k.id}`);
if(gateEl){ gateEl.innerText = k.gate_summary || "-"; gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f"; }
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
});
}
if(page === "trade"){
(data.order_prices || []).forEach(o=>{
const pEl = document.getElementById(`order-price-${o.id}`);
if(pEl){
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
let disp = "";
if(hasMark && o.exchange_mark_price_display) disp = o.exchange_mark_price_display;
else if(o.price_display) disp = o.price_display;
else { const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price); disp = Number.isFinite(px) ? px.toFixed(6) : "-"; }
pEl.innerText = disp;
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
}
const exM = document.getElementById(`order-ex-margin-${o.id}`);
if(exM){
const mv = o.exchange_initial_margin;
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
if(!Number.isNaN(mn)) exM.innerText = `${mn.toFixed(2)}U`;
else { const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null; exM.innerText = (prc === 0) ? "无仓数据" : "-"; }
}
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");
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
});
}
}).catch(()=>{});
}
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
</script>
</body>
</html>