Files
crypto_monitor/strategy_templates/key_monitor_panel.html
T
dekun 959593cdab feat: add timed position close (1h/2h/4h) for key levels and live orders
Program monitors open positions and market-closes at deadline; UI shows label and countdown on instance and hub boards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 19:30:16 +08:00

308 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<style>
.key-monitor-dual-grid{align-items:stretch}
.key-monitor-dual-grid>.card{
height:100%;
min-height:0;
display:flex;
flex-direction:column;
}
.key-panel-scroll.panel-scroll.pos-list{
display:block;
flex:1 1 auto;
min-height:0;
overflow-x:hidden;
overflow-y:auto;
padding-bottom:6px;
-webkit-overflow-scrolling:touch;
scrollbar-gutter:stable;
}
.key-monitor-panel-scroll{min-height:200px}
.key-history-panel-scroll{
flex:0 0 auto;
max-height:calc(8 * 42px + 7 * 8px);
min-height:calc(3 * 42px + 2 * 8px);
}
.key-panel-scroll.panel-scroll.pos-list .key-row-collapse{flex-shrink:0}
.key-panel-scroll.panel-scroll.pos-list::-webkit-scrollbar{width:8px}
.key-panel-scroll.panel-scroll.pos-list::-webkit-scrollbar-thumb{background:#3a4660;border-radius:4px}
.key-panel-scroll.panel-scroll.pos-list::-webkit-scrollbar-track{background:transparent}
.key-row-collapse{border:1px solid #2a3348;border-radius:10px;background:#141923}
.key-row-collapse:not([open]){overflow:hidden}
.key-row-collapse[open]{overflow:visible}
.key-row-collapse+.key-row-collapse{margin-top:8px}
.key-row-collapse-summary{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;cursor:pointer;list-style:none;font-size:.8rem;color:#c5cde0;line-height:1.45}
.key-row-collapse-summary::-webkit-details-marker{display:none}
.key-row-collapse-summary::before{content:"▸";flex:0 0 auto;color:#6d7a99;transition:transform .15s ease}
.key-row-collapse[open]>.key-row-collapse-summary::before{transform:rotate(90deg)}
.key-row-summary-main{flex:1;min-width:0;display:flex;align-items:center;justify-content:space-between;gap:10px}
.key-row-summary-title{display:flex;align-items:center;gap:6px;flex:0 1 auto;flex-wrap:wrap;min-width:0}
.key-row-summary-title strong{font-size:.88rem;color:#fff}
.key-row-summary-line{color:#9aa8c4;font-size:.76rem;word-break:break-word}
.key-row-summary-live{
flex:1 1 auto;
min-width:0;
color:#8fc8ff;
font-size:.72rem;
text-align:right;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
.key-row-summary-live.key-row-summary-pending{color:#4cd97f;font-weight:600}
.key-history-panel-scroll .key-row-summary-main{justify-content:flex-start}
.key-row-summary-actions{flex:0 0 auto;display:flex;gap:6px;align-items:center}
.key-row-collapse-body{padding:0 12px 16px;border-top:1px solid #232b3d}
.key-row-collapse-body .pos-meta{margin-top:10px;margin-bottom:10px}
.key-row-collapse-body .pos-grid{margin-bottom:8px}
.key-history-alert{font-size:.75rem;color:#aab;margin-top:8px;margin-bottom:2px;padding-bottom:4px;white-space:pre-wrap;word-break:break-word;line-height:1.5}
.key-history-outcome-badge{font-size:.7rem;font-weight:600;padding:1px 7px;border-radius:4px;line-height:1.35}
.key-row-collapse.key-history-success{border-color:rgba(76,217,127,.42);background:rgba(18,32,26,.92)}
.key-row-collapse.key-history-success .key-row-collapse-summary{color:#c8f0d6}
.key-row-collapse.key-history-success .key-row-summary-title strong{color:#e8fff0}
.key-row-collapse.key-history-success .key-history-brief,.key-row-collapse.key-history-success .key-history-outcome-badge{color:#4cd97f;background:rgba(76,217,127,.12);border:1px solid rgba(76,217,127,.28)}
.key-row-collapse.key-history-manual{border-color:rgba(136,146,176,.45);background:rgba(22,24,32,.95)}
.key-row-collapse.key-history-manual .key-history-brief,.key-row-collapse.key-history-manual .key-history-outcome-badge{color:#9aa8c4;background:rgba(136,146,176,.12);border:1px solid rgba(136,146,176,.28)}
.key-row-collapse.key-history-failed{border-color:rgba(232,160,144,.4);background:rgba(36,22,24,.95)}
.key-row-collapse.key-history-failed .key-row-collapse-summary{color:#e8cfc8}
.key-row-collapse.key-history-failed .key-history-brief,.key-row-collapse.key-history-failed .key-history-outcome-badge{color:#e8a090;background:rgba(232,160,144,.1);border:1px solid rgba(232,160,144,.28)}
.key-rule-table-wrap{overflow-x:auto;margin:0 -2px}
.key-rule-table{width:100%;min-width:620px;border-collapse:collapse;font-size:.6rem;line-height:1.3}
.key-rule-table th,.key-rule-table td{border:1px solid #2a3348;padding:4px 6px;vertical-align:top;text-align:left}
.key-rule-table th{background:rgba(0,0,0,.28);color:#9ab;font-weight:600;white-space:nowrap;font-size:.58rem}
.key-rule-table td{color:#c5cde0}
.key-rule-table .key-rule-type{color:#fff;font-weight:600;line-height:1.25;white-space:nowrap}
.key-rule-table .key-rule-sub{color:#8fc8ff;font-size:.54rem;font-weight:500}
.key-rule-cell{word-break:break-word}
.key-rule-foot{margin:6px 0 0;font-size:.56rem;color:#8892b0;line-height:1.35}
.key-rule-foot code{font-size:.54rem;color:#8fc8ff}
</style>
{% macro key_direction_label(k) -%}
{% if k.direction == 'watch' %}双向{% elif k.direction == 'long' %}做多{% else %}做空{% endif %}
{%- endmacro %}
{% macro key_sl_tp_mode_label(k) -%}
{% if (k.sl_tp_mode or 'standard') == 'standard' %}标准突破{% elif k.sl_tp_mode == 'box_1p5' %}箱体1R·止盈1.5H{% else %}趋势单{% endif %}
{%- endmacro %}
{% macro key_monitor_brief(k) -%}
上{{ k.upper }} / 下{{ k.lower }} · 提醒 {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
{%- if k.monitor_type in ['箱体突破','收敛突破'] %} · {{ key_sl_tp_mode_label(k) }}{% endif %}
{%- if k.breakeven_enabled %} · 保本开{% else %} · 保本关{% endif %}
{%- endmacro %}
{% macro key_history_outcome_kind(h) -%}
{%- set r = (h.close_reason or '')|trim -%}
{%- if r in ['fib_filled', 'false_breakout_filled', 'key_level_alert_done', 'alerts_complete', 'auto_opened'] -%}success
{%- elif r == 'manual' -%}manual
{%- elif r -%}failed
{%- else -%}neutral
{%- endif -%}
{%- endmacro %}
{% macro key_history_outcome_label(h) -%}
{%- set r = (h.close_reason or '')|trim -%}
{%- if r == 'fib_filled' -%}斐波成交
{%- elif r == 'false_breakout_filled' -%}假突破成交
{%- elif r == 'key_level_alert_done' -%}提醒完成
{%- elif r == 'alerts_complete' -%}提醒已满
{%- elif r == 'auto_opened' -%}自动开仓
{%- elif r == 'manual' -%}手动删除
{%- elif r == 'fib_invalidate' -%}斐波失效
{%- elif r == 'false_breakout_expired' -%}假突破过期
{%- elif r == 'fib_plan_invalid' -%}计划无效
{%- elif r == 'rr_insufficient' -%}盈亏比不足
{%- elif r == 'exchange_failed' -%}下单失败
{%- else -%}{{ r or '—' }}
{%- endif -%}
{%- endmacro %}
{% macro key_history_brief(h) -%}
{{ key_history_outcome_label(h) }} · {{ (h.closed_at or '-')[:16] }} · 上{{ h.upper }} / 下{{ h.lower }} · 提醒 {{ h.notification_count or 0 }}
{%- endmacro %}
<div class="dual-panel-grid key-monitor-dual-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_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 %}
<a href="/key_focus" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">输入币种查看K线</a>
{% endif %}
</div>
<form id="key-form" action="/add_key" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select name="type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破(BTC/ETH</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
<select name="direction" id="key-direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none">
<input name="upper" id="key-upper" step="0.0001" placeholder="上沿/阻力" required>
<input name="lower" id="key-lower" step="0.0001" placeholder="下沿/支撑" required>
<select name="sl_tp_mode" id="key-sl-tp-mode" title="止盈止损方案">
<option value="standard">标准突破</option>
<option value="box_1p5">箱体1R·止盈1.5H</option>
<option value="trend_manual">趋势单·自填止盈</option>
</select>
<input name="manual_take_profit" id="key-manual-tp" step="0.0001" placeholder="趋势单止盈价" style="display:none">
<label id="key-breakeven-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="breakeven_enabled" value="1" id="key-breakeven-cb"> 移动保本
</label>
<label id="key-time-close-wrap" class="key-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="time_close_enabled" value="1" id="key-time-close-cb"> 时间平仓
<select name="time_close_hours" id="key-time-close-hours" disabled>
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</label>
<button type="submit">添加</button>
</form>
<details class="tip-collapse key-rule-collapse">
<summary class="tip-collapse-summary">关键位监控规则说明</summary>
<div class="tip-collapse-body rule-tip">
{% include 'key_monitor_rule_tips.html' %}
</div>
</details>
<div class="panel-scroll pos-list key-panel-scroll key-monitor-panel-scroll">
{% for k in key %}
<details class="key-row-collapse" id="key-row-{{ k.id }}">
<summary class="key-row-collapse-summary">
<span class="key-row-summary-main">
<span class="key-row-summary-title">
<strong>{{ k.symbol }}</strong>
{% if k.direction == 'watch' %}
<span class="pos-side-badge" style="background:#2a3152;color:#9ab">双向</span>
{% else %}
<span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(k) }}</span>
{% endif %}
<span class="badge direction">{{ k.monitor_type }}</span>
</span>
<span class="key-row-summary-live" id="key-summary-live-{{ k.id }}">现价 — · 门控 —</span>
</span>
<span class="key-row-summary-actions">
<button type="button" class="table-del" onclick="event.preventDefault(); event.stopPropagation(); deleteKeyMonitor({{ k.id }})"></button>
</span>
</summary>
<div class="key-row-collapse-body">
<div class="key-row-summary-line">{{ key_monitor_brief(k) }}</div>
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ k.upper }}</span>
<span class="pos-meta-item">下沿: {{ k.lower }}</span>
{% if k.fib_entry_price %}<span class="pos-meta-item">挂E: {{ k.fib_entry_price }}</span>{% endif %}
{% if k.monitor_type == '假突破' and k.fib_stop_loss %}<span class="pos-meta-item">SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% endif %}
<span class="pos-meta-item">已提醒: {{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}</span>
{% if k.monitor_type in ['箱体突破','收敛突破'] %}
<span class="pos-meta-item">方案: {{ key_sl_tp_mode_label(k) }}</span>
{% endif %}
<span class="pos-meta-item">保本: {{ '开' if k.breakeven_enabled else '关' }}</span>
{% if k.time_close_enabled and k.time_close_hours %}
<span class="pos-meta-item pos-meta-on">时间平仓: {{ k.time_close_hours }}h</span>
{% endif %}
</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"><span class="pos-meta-item" id="key-gate-metrics-{{ k.id }}" style="color:#8fc8ff"></span></div>
</div>
</details>
{% else %}
<div class="pos-empty">暂无监控中的关键位</div>
{% endfor %}
</div>
</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 key-panel-scroll key-history-panel-scroll">
{% for h in key_history %}
<details class="key-row-collapse key-history-{{ key_history_outcome_kind(h) }}">
<summary class="key-row-collapse-summary">
<span class="key-row-summary-main">
<span class="key-row-summary-title">
<strong>{{ h.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(h) }}</span>
<span class="badge direction">{{ h.monitor_type }}</span>
<span class="key-history-outcome-badge">{{ key_history_outcome_label(h) }}</span>
</span>
</span>
<span class="key-row-summary-actions">
<button type="button" class="table-del" onclick="event.preventDefault(); event.stopPropagation(); deleteKeyHistory({{ h.id }})">删除</button>
</span>
</summary>
<div class="key-row-collapse-body">
<div class="key-row-summary-line key-history-brief">{{ key_history_brief(h) }}</div>
<div class="pos-meta">
<span class="pos-meta-item">类型: {{ h.monitor_type }}</span>
<span class="pos-meta-item">结案: {{ key_history_outcome_label(h) }}{% if h.close_reason %} ({{ h.close_reason }}){% endif %}</span>
<span class="pos-meta-item">时间: {{ h.closed_at or '—' }}</span>
</div>
<div class="pos-meta">
<span class="pos-meta-item">上沿: {{ h.upper }}</span>
<span class="pos-meta-item">下沿: {{ h.lower }}</span>
<span class="pos-meta-item">提醒次数: {{ h.notification_count or 0 }}</span>
</div>
{% if h.last_alert_message %}
<div class="key-history-alert">{{ h.last_alert_message }}</div>
{% endif %}
</div>
</details>
{% else %}
<div class="pos-empty">暂无历史</div>
{% endfor %}
</div>
</div>
</div>
<script>
function keySummaryIsPending(snap){
if(!snap) return false;
const gs = String(snap.gate_summary || "");
if(gs.includes("标记价将失效")) return false;
const gm = String(snap.gate_metrics || "");
if(gm.includes("限价单") || gm.includes("挂单")) return true;
if(/等待成交/.test(gs)) return true;
if(/挂E=/.test(gs) && !gs.includes("将失效")) return true;
return false;
}
function paintKeyMonitorSummary(id, snap){
const el = document.getElementById(`key-summary-live-${id}`);
if(!el || !snap) return;
const px = snap.price_display || (Number.isFinite(Number(snap.price)) ? Number(snap.price).toFixed(6) : "—");
const gate = snap.gate_summary || "—";
el.innerText = `现价 ${px} · 门控 ${gate}`;
el.classList.toggle("key-row-summary-pending", keySummaryIsPending(snap));
}
document.querySelectorAll(".key-row-collapse").forEach((row)=>{
row.addEventListener("toggle", ()=>{
if(!row.open) return;
requestAnimationFrame(()=>{
const body = row.querySelector(".key-row-collapse-body");
const panel = row.closest(".key-panel-scroll");
if(body && panel){
const bodyRect = body.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
if(bodyRect.bottom > panelRect.bottom - 8){
panel.scrollTop += bodyRect.bottom - panelRect.bottom + 16;
} else if(bodyRect.top < panelRect.top + 8){
panel.scrollTop -= panelRect.top - bodyRect.top + 16;
}
} else {
row.scrollIntoView({block:"nearest", behavior:"smooth"});
}
});
});
});
</script>