refactor: 将共用代码迁入 lib/ 模块化目录

统一 strategy、key_monitor、trade、hub 等共用库到 lib/ 子包,并补充 lib-structure 文档,便于四所与中控维护。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-02 16:23:09 +08:00
parent 4742a0bb9d
commit 5797d49d8a
190 changed files with 27946 additions and 27499 deletions
@@ -0,0 +1,23 @@
<details class="tip-collapse gate-top-tips-collapse">
<summary class="tip-collapse-summary">
实时价格更新:<span id="price-last-updated">--</span>(北京时间 UTC+8
<span class="tip-collapse-hint">· 划转规则</span>
</summary>
<div class="tip-collapse-body rule-tip gate-transfer-tip">
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;将 {{ auto_transfer_to }} 调整至 {{ transfer_amount_fmt }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }}<strong>持仓中不划转</strong>并微信通知)
</div>
</details>
<form action="/manual_transfer" method="post" class="form-row gate-transfer-form">
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
<select name="from_account">
<option value="funding" {% if auto_transfer_from == 'funding' %}selected{% endif %}>from: funding</option>
<option value="swap" {% if auto_transfer_from == 'swap' %}selected{% endif %}>from: swap</option>
<option value="spot" {% if auto_transfer_from == 'spot' %}selected{% endif %}>from: spot</option>
</select>
<select name="to_account">
<option value="swap" {% if auto_transfer_to == 'swap' %}selected{% endif %}>to: swap</option>
<option value="funding" {% if auto_transfer_to == 'funding' %}selected{% endif %}>to: funding</option>
<option value="spot" {% if auto_transfer_to == 'spot' %}selected{% endif %}>to: spot</option>
</select>
<button type="submit">手动划转</button>
</form>
+175
View File
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=5"></script>
<title>{{ exchange_display }} | 关键位放大</title>
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
<link rel="stylesheet" href="/static/focus_chart_page.css?v=1">
</head>
<body class="focus-page">
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong class="focus-title">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span>
</div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
<div class="row" style="margin-top:10px">
<label>币种</label>
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
<label>关键位</label>
<select id="key-id">
<option value="">无(仅看K线)</option>
{% for k in key_list %}
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.direction == 'long' else '做空' }}</option>
{% endfor %}
</select>
<label>周期</label>
<select id="timeframe">
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label>K线数</label>
<select id="kline-limit">
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
</select>
<button id="manual-refresh" type="button">刷新</button>
<span id="load-status" class="status"></span>
</div>
</div>
<div class="card">
<div class="meta">
<div class="meta-item meta-item--emph"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
<div class="meta-item"><div class="k">监控类型</div><div class="v" id="m-type">-</div></div>
<div class="meta-item meta-item--emph"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
<div class="meta-item"><div class="k">上沿/阻力</div><div class="v" id="m-upper">-</div></div>
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</div></div>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
<div class="meta-item"><div class="k">距上沿</div><div class="v" id="m-updiff">-</div></div>
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
</div>
</div>
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
</div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/static/focus_chart_page.js?v=2"></script>
<script>
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
const keySelect = document.getElementById("key-id");
const symbolInput = document.getElementById("symbol-input");
const tfSelect = document.getElementById("timeframe");
const limitSelect = document.getElementById("kline-limit");
const statusEl = document.getElementById("load-status");
const updatedAtEl = document.getElementById("updated-at");
const chartHost = document.getElementById("chart");
const FCP = window.FocusChartPage;
const keyMap = {};
{% for k in key_list %}
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
{% endfor %}
let fc = null;
function ensureChart(){
if(fc && fc.ensureSeries()) return true;
if(!window.LightweightCharts){
statusEl.className = "status err";
statusEl.innerText = "图表库加载失败";
return false;
}
fc = FCP.createFocusChart(chartHost);
if(!fc || !fc.ensureSeries()){
statusEl.className = "status err";
statusEl.innerText = "K线序列初始化失败";
return false;
}
return true;
}
function syncSymbolByKey(){
const keyId = keySelect.value;
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
}
async function loadKeyKline(){
if(!ensureChart()) return;
const keyId = keySelect.value;
const symbol = (symbolInput.value || "").trim().toUpperCase();
const timeframe = tfSelect.value;
const limit = limitSelect.value;
if(!symbol && !keyId){
statusEl.className = "status err";
statusEl.innerText = "请先输入币种或选择关键位";
return;
}
statusEl.className = "status";
statusEl.innerText = "加载中...";
try{
const qs = new URLSearchParams();
if(keyId) qs.set("key_id", keyId);
if(symbol) qs.set("symbol", symbol);
qs.set("timeframe", timeframe);
qs.set("limit", limit);
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
const data = await resp.json();
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
if(fc && typeof fc.setPriceTick === "function") fc.setPriceTick(data.price_tick);
else FCP.setActivePriceTick(data.price_tick);
const candles = Array.isArray(data.candles) ? data.candles : [];
if(!candles.length){
statusEl.className = "status err";
statusEl.innerText = "暂无K线数据";
return;
}
fc.candleSeries.setData(candles);
fc.resetPriceLines();
fc.addLine(data.current_price, FCP.lineTitle("现价", data.current_price_display), "#42a5f5");
if(data.key_monitor){
const km = data.key_monitor;
fc.addLine(km.upper, FCP.lineTitle("上沿", km.upper_display), "#ffb84d");
fc.addLine(km.lower, FCP.lineTitle("下沿", km.lower_display), "#4cd97f");
}
fc.chart.timeScale().fitContent();
FCP.paintKeyMeta(data);
updatedAtEl.innerText = data.updated_at || "--";
statusEl.className = "status";
statusEl.innerText = `已加载 ${candles.length} 根K线`;
}catch(err){
statusEl.className = "status err";
statusEl.innerText = err && err.message ? err.message : "加载失败";
}
}
document.getElementById("manual-refresh").addEventListener("click", loadKeyKline);
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
symbolInput.addEventListener("change", ()=>{
if(symbolInput.value.trim()) keySelect.value = "";
loadKeyKline();
});
tfSelect.addEventListener("change", loadKeyKline);
limitSelect.addEventListener("change", loadKeyKline);
syncSymbolByKey();
loadKeyKline();
setInterval(loadKeyKline, refreshMs);
</script>
</body>
</html>
@@ -0,0 +1,327 @@
<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_monitor_type_label(k) -%}
{%- if k.monitor_type in ['关键阻力位','关键支撑位','关键支撑阻力'] -%}关键支撑阻力{%- else -%}{{ k.monitor_type }}{%- endif -%}
{%- endmacro %}
{% 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', 'trigger_entry_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 == 'trigger_entry_filled' -%}触价成交
{%- elif r == 'key_level_alert_done' -%}提醒完成
{%- elif r == 'alerts_complete' -%}提醒已满
{%- elif r == 'auto_opened' -%}自动开仓
{%- elif r == 'manual' -%}手动删除
{%- elif r == 'fib_invalidate' -%}斐波失效
{%- elif r == 'box_opposite_break' -%}反向突破失效
{%- elif r == 'trigger_tp_invalidate' -%}触价止盈失效
{%- elif r == 'trigger_sl_invalidate' -%}触价止损失效
{%- elif r == 'trigger_entry_expired' -%}触价过期
{%- elif r == 'trigger_exchange_failed' -%}触价下单失败
{%- 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" id="key-type-select" required>
{% if position_sizing_mode != 'full_margin' %}
<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>
{% endif %}
<option value="回调触价开仓">回调触价开仓</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="trigger_entry" id="key-trigger-entry" step="0.0001" placeholder="计划入场价" style="display:none">
<input name="trigger_sl" id="key-trigger-sl" step="0.0001" placeholder="止损价" style="display:none">
<input name="trigger_tp" id="key-trigger-tp" 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>
<span id="key-time-close-wrap" class="key-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="key-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="key-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</span>
<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.time_close_enabled and k.time_close_hours %}
<span class="pos-symbol-time-close pos-meta-on">时间平仓 {{ k.time_close_hours }}h</span>
{% endif %}
{% 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">{{ key_monitor_type_label(k) }}</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 and k.monitor_type in ['回调触价开仓','突破触价开仓','触价开仓'] %}<span class="pos-meta-item">E: {{ k.fib_entry_price }} / SL: {{ k.fib_stop_loss }} / TP: {{ k.fib_take_profit }}</span>{% elif 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>
</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">{{ key_monitor_type_label(h) }}</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">类型: {{ key_monitor_type_label(h) }}</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(/触价待触发/.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>
<script src="/static/key_monitor_form.js?v=2"></script>
@@ -0,0 +1,59 @@
{% set r = key_rule_ctx %}
<div class="key-rule-table-wrap">
<table class="key-rule-table">
<thead>
<tr>
<th>类型</th>
<th>填写</th>
<th>门控</th>
<th>止盈止损</th>
<th>执行</th>
</tr>
</thead>
<tbody>
<tr>
<td class="key-rule-type">箱体突破<br><span class="key-rule-sub">收敛突破</span></td>
<td class="key-rule-cell">方向必选;填 H/L<br>方案:标准 / 1R·1.5H / 趋势<br>可勾移动保本</td>
<td class="key-rule-cell">{{ r.tf }} 两根闭合 K{{ r.breakout_bar }}/{{ r.confirm_bar }}<br>突破 &gt;{{ r.amp_min_pct }}%;确认在箱外<br>&gt;前{{ r.vol_ma_bars }}均×{{ r.vol_ratio_min }}<br>成交 Top{{ r.vol_rank_max }}RR &gt;{{ r.min_rr }}<br>标记价先破反向边界→失效</td>
<td class="key-rule-cell">标准:SL 极值外{{ r.stop_outside_pct }}%TP=E±H<br>1RSL=E∓HTP=E∓1.5H<br>趋势:SL 极值外{{ r.trend_stop_outside_pct }}%TP 自填</td>
<td class="key-rule-cell">门控过→市价开仓→下单监控<br>满仓不可再加</td>
</tr>
<tr>
<td class="key-rule-type">斐波回调<br><span class="key-rule-sub">0.618 / 0.786</span></td>
<td class="key-rule-cell">方向 + H/L 波段<br>系统算 E/SL/TP</td>
<td class="key-rule-cell">多:E=HrΔ,SL=LTP=H<br>空:E=L+rΔ,SL=HTP=L<br>RR &gt;{{ r.min_rr }};先触 TP 侧失效</td>
<td class="key-rule-cell">公式固定 SL/TP<br>成交后挂所</td>
<td class="key-rule-cell">挂限价等成交<br>成交→下单监控</td>
</tr>
<tr>
<td class="key-rule-type">假突破<br><span class="key-rule-sub">BTC / ETH</span></td>
<td class="key-rule-cell">空填高点 / 多填低点<br>同币仅 1 条</td>
<td class="key-rule-cell">外侧 {{ r.fb_offset_pct }}% 限价<br>SL {{ r.fb_sl_pct }}%RR {{ r.fb_rr }}<br>有效 {{ r.fb_valid_hours }}h</td>
<td class="key-rule-cell">自动 E/SL/TP<br>可保本</td>
<td class="key-rule-cell">即挂限价<br>成交/过期→历史</td>
</tr>
<tr>
<td class="key-rule-type">回调触价开仓</td>
<td class="key-rule-cell">方向 + 入场 E / 止损 SL / 止盈 TP<br>可勾移动保本、时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }};做多 SL&lt;E&lt;TP<br>标记价回调触 E(多≤E / 空≥E)后下一轮询市价开<br>先触 TP 侧失效;有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价,无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td>
</tr>
<tr>
<td class="key-rule-type">突破触价开仓</td>
<td class="key-rule-cell">方向 + 突破价 E / 止损 SL / 止盈 TP<br>可勾移动保本、时间平仓</td>
<td class="key-rule-cell">RR &gt;{{ r.min_rr }};做多 SL&lt;E&lt;TP<br>标记价<strong>穿越</strong> E 立即市价开(多向上 / 空向下)<br>先触 TP 或 SL 侧失效;有效 {{ r.trigger_entry_validity_hours }}h</td>
<td class="key-rule-cell">程序盯价,无交易所挂单<br>成交后挂所 TP/SL → 下单监控</td>
<td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td>
</tr>
<tr>
<td class="key-rule-type">关键支撑阻力</td>
<td class="key-rule-cell">双向;填上/下沿</td>
<td class="key-rule-cell">{{ r.tf }} 收盘破上沿或下沿<br>上沿优先</td>
<td class="key-rule-cell">无(仅提醒)</td>
<td class="key-rule-cell">微信 ≤{{ r.alert_max }} 次<br>间隔 ≥{{ r.alert_interval_min }} 分</td>
</tr>
</tbody>
</table>
</div>
<p class="key-rule-foot">阈值来自 <code>.env</code>,修改后重启实例。</p>
+151
View File
@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=5"></script>
<title>{{ exchange_display }} | 实盘下单放大</title>
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
<link rel="stylesheet" href="/static/focus_chart_page.css?v=1">
</head>
<body class="focus-page">
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong class="focus-title">实盘下单放大(100根K线)</strong><span class="exchange-tag">{{ exchange_display }}</span>
</div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
{% if orders %}
<div class="row" style="margin-top:10px">
<label>订单</label>
<select id="order-id">
{% for o in orders %}
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
</option>
{% endfor %}
</select>
<label>周期</label>
<select id="timeframe">
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<button id="manual-refresh" type="button">刷新</button>
<span id="load-status" class="status"></span>
</div>
{% else %}
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
{% endif %}
</div>
{% if orders %}
<div class="card">
<div class="meta">
<div class="meta-item meta-item--emph"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
<div class="meta-item meta-item--emph"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
<div class="meta-item"><div class="k">移动保本</div><div class="v" id="m-breakeven">-</div></div>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
<div class="meta-item meta-item--emph meta-item--pnl"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
</div>
</div>
<div class="card">
<div id="chart-wrap"><div id="chart"></div></div>
</div>
{% endif %}
</div>
{% if orders %}
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/static/focus_chart_page.js?v=2"></script>
<script>
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
const orderSelect = document.getElementById("order-id");
const tfSelect = document.getElementById("timeframe");
const statusEl = document.getElementById("load-status");
const updatedAtEl = document.getElementById("updated-at");
const chartHost = document.getElementById("chart");
const FCP = window.FocusChartPage;
let fc = null;
function ensureChart(){
if(fc && fc.ensureSeries()) return true;
if(!window.LightweightCharts){
statusEl.className = "status err";
statusEl.innerText = "图表库加载失败";
return false;
}
fc = FCP.createFocusChart(chartHost);
if(!fc || !fc.ensureSeries()){
statusEl.className = "status err";
statusEl.innerText = "K线序列初始化失败";
return false;
}
return true;
}
async function loadOrderKline(){
if(!ensureChart()) return;
const orderId = orderSelect.value;
const timeframe = tfSelect.value;
if(!orderId) return;
statusEl.className = "status";
statusEl.innerText = "加载中...";
try{
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
const data = await resp.json();
if(!resp.ok || !data.ok) throw new Error(data.msg || "请求失败");
if(fc && typeof fc.setPriceTick === "function") fc.setPriceTick(data.price_tick);
else FCP.setActivePriceTick(data.price_tick);
const candles = Array.isArray(data.candles) ? data.candles : [];
if(!candles.length){
statusEl.className = "status err";
statusEl.innerText = "暂无K线数据";
return;
}
fc.candleSeries.setData(candles);
fc.resetPriceLines();
const o = data.order || {};
fc.addLine(o.trigger_price, FCP.lineTitle("成交价", o.trigger_price_display), "#42a5f5");
fc.addLine(o.stop_loss, FCP.lineTitle("止损", o.stop_loss_display), "#ff6666");
fc.addLine(o.take_profit, FCP.lineTitle("止盈", o.take_profit_display), "#4cd97f");
const markPx = o.current_price;
if(markPx) fc.addLine(markPx, FCP.lineTitle("现价", o.current_price_display), "#ffb74d");
fc.chart.timeScale().fitContent();
FCP.paintOrderMeta(o);
updatedAtEl.innerText = data.updated_at || "--";
statusEl.className = "status";
statusEl.innerText = `已加载 ${candles.length} 根K线`;
}catch(err){
statusEl.className = "status err";
statusEl.innerText = err && err.message ? err.message : "加载失败";
}
}
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
orderSelect.addEventListener("change", loadOrderKline);
tfSelect.addEventListener("change", loadOrderKline);
loadOrderKline();
setInterval(loadOrderKline, refreshMs);
</script>
{% endif %}
</body>
</html>
@@ -0,0 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ opens_today }}{% if daily_open_hard_limit > 0 %} / 硬上限 {{ daily_open_hard_limit }}{% endif %}AI 提醒 {{ daily_open_alert_threshold }});
{% if can_trade %}可开仓{% else %}不可开仓(持仓已满、单日开仓达上限,或未到北京时间 {{ reset_hour }}:00{% endif %}
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
</div>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓:合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x、其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -0,0 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ opens_today }}{% if daily_open_hard_limit > 0 %} / 硬上限 {{ daily_open_hard_limit }}{% endif %}AI 提醒 {{ daily_open_alert_threshold }});
{% if can_trade %}可开仓{% else %}不可开仓(持仓已满、单日开仓达上限,或未到北京时间 {{ reset_hour }}:00{% endif %}
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
</div>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓:合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x、其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -0,0 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则:最大同时持仓 {{ max_active_positions }}(当前 active {{ active_count }});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ opens_today }}{% if daily_open_hard_limit > 0 %} / 硬上限 {{ daily_open_hard_limit }}{% endif %}AI 提醒 {{ daily_open_alert_threshold }});
{% if can_trade %}可开仓{% else %}不可开仓(持仓达上限、单日开仓达上限、有趋势回调计划,或未到北京时间 {{ reset_hour }}:00{% endif %}
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
</div>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓:合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x、其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -0,0 +1,21 @@
<details class="tip-collapse order-rule-collapse">
<summary class="tip-collapse-summary">开仓规则说明</summary>
<div class="tip-collapse-body rule-tip" id="order-rule-tip">
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
本交易日开仓 {{ opens_today }}{% if daily_open_hard_limit > 0 %} / 硬上限 {{ daily_open_hard_limit }}{% endif %}AI 提醒 {{ daily_open_alert_threshold }});
{% if can_trade %}可开仓{% else %}不可开仓{% if active_count >= max_active_positions %}(持仓 {{ active_count }}/{{ max_active_positions }}{% endif %}{% if daily_open_hard_limit > 0 and opens_today >= daily_open_hard_limit %}(单日开仓达上限){% endif %}{% if open_guard_blocks_now %}(未到北京时间 {{ reset_hour }}:00{% endif %}{% endif %}
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
</div>
</details>
<details class="tip-collapse order-sizing-collapse">
<summary class="tip-collapse-summary">计仓与保本说明</summary>
<div class="tip-collapse-body rule-tip">
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓:合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x、其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>
</details>
@@ -0,0 +1,5 @@
<div id="order-plan-preview" class="order-plan-preview">
<span id="order-risk-preview" class="order-preview-risk">预估风险:<strong></strong></span>
<span id="order-profit-preview" class="order-preview-profit">预估盈利:<strong></strong></span>
<span id="order-rr-preview" class="order-preview-rr">预估盈亏比:<strong></strong></span>
</div>
@@ -0,0 +1,284 @@
{% set mf = money_fmt|default(funds_fmt) %}
<style>
.strategy-records-page{
padding:10px clamp(14px,2.2vw,22px) 22px;
box-sizing:border-box;
}
.strategy-records-page h2{margin:0 0 8px;color:#dbe4ff}
.strategy-records-tip{font-size:.76rem;color:#8892b0;line-height:1.55;margin-bottom:12px}
.sr-filters{display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center;padding:12px 14px;background:#141a2a;border:1px solid #2a3150;border-radius:10px;margin-bottom:16px}
.sr-filters label{font-size:.76rem;color:#8b95b8;display:flex;align-items:center;gap:6px}
.sr-filters select,.sr-filters input[type=datetime-local]{padding:5px 8px;background:#0f1424;border:1px solid #304164;border-radius:6px;color:#dbe4ff;font-size:.78rem}
.sr-chip-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
.sr-chip{padding:5px 12px;border:1px solid #304164;border-radius:16px;background:#151a2a;color:#9aa3c4;font-size:.74rem;cursor:pointer;user-select:none}
.sr-chip.active{background:#2a3f6c;color:#dbe4ff;border-color:#4a6a9a}
.sr-panels{display:grid;grid-template-columns:repeat(2,minmax(280px,1fr));gap:14px}
@media (max-width:960px){.sr-panels{grid-template-columns:1fr}}
.sr-panel{background:#141a2a;border:1px solid #2a3150;border-radius:12px;padding:12px 14px;min-height:120px;min-width:0}
.sr-panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:8px}
.sr-panel-title{font-size:.92rem;font-weight:700;color:#f0f2ff}
.sr-panel-title.trend{color:#6ab8ff}
.sr-panel-title.roll{color:#ffb020}
.sr-panel-count{font-size:.72rem;color:#8892b0}
.sr-list{display:flex;flex-direction:column;gap:8px;max-height:62vh;overflow:auto;min-width:0}
.sr-item{border:1px solid #243050;border-radius:10px;background:#0f1424;overflow:visible}
.sr-item.sr-hidden{display:none}
.sr-summary{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:10px 12px;cursor:pointer;font-size:.78rem;color:#cfd3ef !important;line-height:1.45;min-height:2.4rem;min-width:0}
.sr-summary > span{flex:0 1 auto;min-width:0;color:inherit}
.sr-summary .sr-sym{color:#f0f2ff !important;flex-shrink:0}
.sr-summary .sr-dca-tag{color:#8892b0 !important}
.sr-summary .sr-pnl.pos{color:#4cd97f !important}
.sr-summary .sr-pnl.neg{color:#ff6666 !important}
.sr-summary:hover{background:rgba(42,63,108,.2)}
.sr-summary::before{content:"▸";color:#6ab8ff;margin-right:2px;transition:transform .15s}
.sr-item.sr-open .sr-summary::before{transform:rotate(90deg)}
.sr-summary .sr-sym{font-weight:600;color:#f0f2ff}
.sr-summary .sr-pnl.pos{color:#4cd97f}
.sr-summary .sr-pnl.neg{color:#ff6666}
.sr-summary .sr-dca-tag{font-size:.7rem;color:#8892b0}
.sr-detail{display:none;padding:0 12px 12px;border-top:1px dashed #2a3558;font-size:.76rem;color:#cfd3ef}
.sr-item.sr-open .sr-detail{display:block}
.sr-detail-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 12px;margin:10px 0}
.sr-detail-grid .lbl{color:#8b95b8;font-size:.7rem}
.sr-detail-grid .val{color:#f0f2ff}
.sr-dca-table{width:100%;border-collapse:collapse;font-size:.72rem;margin-top:8px}
.sr-dca-table th,.sr-dca-table td{padding:5px 8px;border-bottom:1px solid #243050;text-align:left}
.sr-dca-table th{color:#6a7598}
.sr-dca-table .st-done{color:#4cd97f}
.sr-dca-table .st-pending{color:#9aa3c4}
.sr-empty{padding:20px;text-align:center;color:#8892b0;font-size:.8rem}
</style>
<div class="strategy-records-page card full">
<h2>策略交易记录</h2>
<p class="strategy-records-tip">
数据库保留最近 <strong>{{ strategy_records_limit|default(100) }}</strong> 条结束快照(按结束时间排序)。
趋势回调与顺势加仓分栏展示;点击行展开详情。结束计划、保本移交、止盈止损会自动写入。
</p>
<div class="sr-filters" id="sr-filters">
<label>币种
<select id="sr-filter-symbol">
<option value="">全部</option>
{% for sym in strategy_record_symbols %}
<option value="{{ sym }}">{{ sym }}</option>
{% endfor %}
</select>
</label>
<label>时间
<select id="sr-filter-time">
<option value="desc">最新优先</option>
<option value="asc">最早优先</option>
</select>
</label>
<div class="sr-chip-row">
<span style="font-size:.72rem;color:#8b95b8">筛选</span>
<button type="button" class="sr-chip" data-filter="profit">盈利</button>
<button type="button" class="sr-chip" data-filter="loss">亏损</button>
<button type="button" class="sr-chip" data-filter="no_dca">未补仓</button>
<button type="button" class="sr-chip" data-filter="dca">补仓</button>
<button type="button" class="sr-chip" id="sr-filter-reset" style="border-style:dashed">重置</button>
</div>
</div>
<div class="sr-panels">
<div class="sr-panel" data-panel="trend">
<div class="sr-panel-head">
<span class="sr-panel-title trend">趋势回调记录</span>
<span class="sr-panel-count" id="sr-trend-count">{{ strategy_trend_records|length }} 条</span>
</div>
<div class="sr-list" id="sr-trend-list">
{% for s in strategy_trend_records %}
{% set snap = s.snapshot or {} %}
{% set dca = snap.dca_levels if snap.dca_levels is defined else [] %}
{% set pnl = s.pnl_amount if s.pnl_amount is not none else snap.pnl_amount %}
{% set sym = s.symbol or s.exchange_symbol or snap.symbol or snap.exchange_symbol or '—' %}
<div class="sr-item" data-symbol="{{ s.filter_symbol or '' }}" data-pnl="{{ s.filter_pnl or '' }}" data-dca-tag="{{ s.dca_tag or '' }}" data-dca-done="{{ s.dca_done|default(0) }}" data-sort-ts="{{ s.sort_ts or '' }}">
<div class="sr-summary" role="button" tabindex="0">
<span class="sr-sym">#{{ s.id }} {{ sym }}</span>
<span class="badge {{ 'direction-long' if s.direction == 'long' else 'direction-short' }}">{{ '做多' if s.direction == 'long' else '做空' }}</span>
<span>{{ s.result_label or '—' }}</span>
<span class="sr-pnl {% if pnl is not none %}{% if pnl|float > 0 %}pos{% elif pnl|float < 0 %}neg{% endif %}{% endif %}">{% if pnl is not none %}{{ mf(pnl) }}U{% else %}—{% endif %}</span>
<span class="sr-dca-tag">补仓 {{ s.summary_dca or '—' }}</span>
<span style="color:#8892b0">{{ (s.closed_at or '')[:16] }}</span>
</div>
<div class="sr-detail">
<div class="sr-detail-grid">
<div><div class="lbl">计划 ID</div><div class="val">{{ s.source_id or '—' }}</div></div>
<div><div class="lbl">开仓</div><div class="val">{{ (s.opened_at or '')[:16] or '—' }}</div></div>
<div><div class="lbl">结束</div><div class="val">{{ (s.closed_at or '')[:16] or '—' }}</div></div>
<div><div class="lbl">均价</div><div class="val">{% if snap.avg_entry_price is not none %}{{ price_fmt(sym, snap.avg_entry_price) }}{% else %}—{% endif %}</div></div>
<div><div class="lbl">止损</div><div class="val">{% if snap.stop_loss is not none %}{{ price_fmt(sym, snap.stop_loss) }}{% else %}—{% endif %}</div></div>
<div><div class="lbl">止盈</div><div class="val">{% if snap.take_profit is not none %}{{ price_fmt(sym, snap.take_profit) }}{% else %}—{% endif %}</div></div>
<div><div class="lbl">风险%</div><div class="val">{{ snap.risk_percent if snap.risk_percent is defined else '—' }}</div></div>
<div><div class="lbl">杠杆</div><div class="val">{{ snap.leverage if snap.leverage is defined else '—' }}x</div></div>
<div><div class="lbl">计划保证金</div><div class="val">{% if snap.plan_margin_capital is not none %}{{ mf(snap.plan_margin_capital) }}U{% else %}—{% endif %}</div></div>
</div>
{% if dca and dca|length %}
<table class="sr-dca-table">
<tr><th>档位</th><th>触发价</th><th>张数</th><th>状态</th></tr>
{% for lv in dca %}
<tr>
<td>{{ lv.label or lv.leg_key }}</td>
<td>{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %}</td>
<td>{% if lv.contracts is not none %}{{ lv.contracts }}{% else %}—{% endif %}</td>
<td class="{% if lv.status == 'done' %}st-done{% else %}st-pending{% endif %}">{{ lv.status_label or '—' }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
{% else %}
<div class="sr-empty sr-empty-default">暂无趋势回调结束记录</div>
{% endfor %}
</div>
</div>
<div class="sr-panel" data-panel="roll">
<div class="sr-panel-head">
<span class="sr-panel-title roll">顺势加仓记录</span>
<span class="sr-panel-count" id="sr-roll-count">{{ strategy_roll_records|length }} 条</span>
</div>
<div class="sr-list" id="sr-roll-list">
{% for s in strategy_roll_records %}
{% set snap = s.snapshot or {} %}
{% set group = snap.group if snap.group is defined else {} %}
{% set legs = snap.legs if snap.legs is defined else [] %}
{% set pnl = s.pnl_amount if s.pnl_amount is not none else snap.pnl_amount %}
{% set sym = s.symbol or s.exchange_symbol or snap.symbol or snap.exchange_symbol or '—' %}
<div class="sr-item" data-symbol="{{ s.filter_symbol or '' }}" data-pnl="{{ s.filter_pnl or '' }}" data-dca-tag="{{ s.dca_tag or '' }}" data-dca-done="{{ s.dca_done|default(0) }}" data-sort-ts="{{ s.sort_ts or '' }}">
<div class="sr-summary" role="button" tabindex="0">
<span class="sr-sym">#{{ s.id }} {{ sym }}</span>
<span class="badge {{ 'direction-long' if s.direction == 'long' else 'direction-short' }}">{{ '做多' if s.direction == 'long' else '做空' }}</span>
<span>{{ s.result_label or '—' }}</span>
<span class="sr-pnl {% if pnl is not none %}{% if pnl|float > 0 %}pos{% elif pnl|float < 0 %}neg{% endif %}{% endif %}">{% if pnl is not none %}{{ mf(pnl) }}U{% else %}—{% endif %}</span>
<span class="sr-dca-tag">成交 {{ s.summary_dca or '—' }}</span>
<span style="color:#8892b0">{{ (s.closed_at or '')[:16] }}</span>
</div>
<div class="sr-detail">
<div class="sr-detail-grid">
<div><div class="lbl">组 ID</div><div class="val">{{ s.source_id or '—' }}</div></div>
<div><div class="lbl">创建</div><div class="val">{{ (s.opened_at or group.created_at or '')[:16] or '—' }}</div></div>
<div><div class="lbl">结束</div><div class="val">{{ (s.closed_at or '')[:16] or '—' }}</div></div>
<div><div class="lbl">状态</div><div class="val">{{ s.status_at_close or group.status or '—' }}</div></div>
<div><div class="lbl">杠杆</div><div class="val">{{ group.leverage if group.leverage is defined else '—' }}x</div></div>
<div><div class="lbl">备注</div><div class="val">{{ group.message if group.message is defined else '—' }}</div></div>
</div>
{% if legs and legs|length %}
<table class="sr-dca-table">
<tr><th>腿次</th><th>挂单价</th><th>张数</th><th>状态</th></tr>
{% for leg in legs %}
<tr>
<td>{{ leg.leg_index or loop.index }}</td>
<td>{% if leg.limit_price is not none %}{{ price_fmt(sym, leg.limit_price) }}{% else %}—{% endif %}</td>
<td>{% if leg.order_amount is not none %}{{ leg.order_amount }}{% else %}—{% endif %}</td>
<td>{{ leg.status_label or leg.status or '—' }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
{% else %}
<div class="sr-empty sr-empty-default">暂无顺势加仓结束记录</div>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
(function(){
const symbolSel = document.getElementById("sr-filter-symbol");
const timeSel = document.getElementById("sr-filter-time");
const chips = document.querySelectorAll(".sr-chip[data-filter]");
const resetBtn = document.getElementById("sr-filter-reset");
const active = new Set();
function itemMatches(el){
const sym = (symbolSel && symbolSel.value) || "";
if(sym && (el.getAttribute("data-symbol")||"").toUpperCase() !== sym.toUpperCase()) return false;
if(active.has("profit") && el.getAttribute("data-pnl") !== "profit") return false;
if(active.has("loss") && el.getAttribute("data-pnl") !== "loss") return false;
if(active.has("no_dca")){
const tag = el.getAttribute("data-dca-tag") || "";
const done = parseInt(el.getAttribute("data-dca-done")||"0",10);
if(tag !== "no_dca" && done > 0) return false;
}
if(active.has("dca")){
const done = parseInt(el.getAttribute("data-dca-done")||"0",10);
if(!(done > 0)) return false;
}
return true;
}
function sortList(listEl){
if(!listEl) return;
const asc = timeSel && timeSel.value === "asc";
const items = Array.from(listEl.querySelectorAll(".sr-item"));
items.sort((a,b)=>{
const ta = a.getAttribute("data-sort-ts") || "";
const tb = b.getAttribute("data-sort-ts") || "";
if(ta === tb) return 0;
return asc ? (ta < tb ? -1 : 1) : (ta > tb ? -1 : 1);
});
items.forEach(it => listEl.appendChild(it));
}
function applyFilters(){
["sr-trend-list","sr-roll-list"].forEach(id=>{
const list = document.getElementById(id);
if(!list) return;
sortList(list);
let visible = 0;
list.querySelectorAll(".sr-item").forEach(el=>{
const ok = itemMatches(el);
el.classList.toggle("sr-hidden", !ok);
if(ok) visible++;
});
const panel = list.closest(".sr-panel");
const cnt = panel && panel.querySelector(".sr-panel-count");
if(cnt) cnt.textContent = visible + " 条";
let empty = list.querySelector(".sr-empty-filter");
if(!visible && !list.querySelector(".sr-empty-default")){
if(!empty){
empty = document.createElement("div");
empty.className = "sr-empty sr-empty-filter";
empty.textContent = "无符合筛选的记录";
list.appendChild(empty);
}
} else if(empty) empty.remove();
});
}
chips.forEach(ch=>{
ch.addEventListener("click", ()=>{
const k = ch.getAttribute("data-filter");
if(active.has(k)) active.delete(k); else active.add(k);
ch.classList.toggle("active", active.has(k));
applyFilters();
});
});
if(symbolSel) symbolSel.addEventListener("change", applyFilters);
if(timeSel) timeSel.addEventListener("change", applyFilters);
if(resetBtn) resetBtn.addEventListener("click", ()=>{
active.clear();
chips.forEach(c=>c.classList.remove("active"));
if(symbolSel) symbolSel.value = "";
if(timeSel) timeSel.value = "desc";
applyFilters();
});
document.querySelectorAll(".sr-summary").forEach(sum=>{
const toggle = ()=>{
const item = sum.closest(".sr-item");
if(item) item.classList.toggle("sr-open");
};
sum.addEventListener("click", toggle);
sum.addEventListener("keydown", e=>{
if(e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); }
});
});
applyFilters();
})();
</script>
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>顺势加仓 · {{ exchange_display }}</title>
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
<meta name="theme-color" content="#0b0d14">
</head>
<body>
<div class="container" style="max-width:1100px;margin:0 auto;padding:16px">
{% with messages = get_flashed_messages() %}{% if messages %}<div class="flash">{{ messages[0] }}</div>{% endif %}{% endwith %}
{% include 'strategy_roll_panel.html' %}
<p class="rule-tip" style="margin-top:12px"><a href="/strategy/roll/docs" style="color:#8fc8ff">顺势加仓完整逻辑说明</a></p>
</div>
<script src="/static/strategy_roll.js?v=5"></script>
</body>
</html>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>顺势加仓 · 详细说明 · {{ exchange_display }}</title>
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
<meta name="theme-color" content="#0b0d14">
</head>
<body class="roll-doc-page">
<div class="container roll-doc-container">
<div class="doc-nav roll-doc-nav">
<a href="/strategy">← 返回策略交易</a>
&nbsp;·&nbsp;
<a href="/strategy/roll">顺势加仓</a>
</div>
<article class="doc-body roll-doc-body">
{{ doc_html|safe }}
</article>
</div>
</body>
</html>
@@ -0,0 +1,103 @@
<div class="strategy-panel-inner" id="strategy-roll-panel">
<h2 style="margin:0 0 8px">顺势加仓</h2>
<details class="tip-collapse strategy-roll-rule-collapse" open>
<summary class="tip-collapse-summary">顺势加仓规则说明{% if roll_trend_active %} · 当前有趋势回调计划{% endif %}</summary>
<div class="tip-collapse-body rule-tip">
<strong>仅人工提交</strong>;须先在「实盘下单」有同向持仓。仅<strong>以损定仓</strong>模式可用。<br>
做多/做空各最多滚仓 <strong>3</strong> 次(仅计已成交腿);止盈<strong>锁定首仓</strong>不变。<br>
风险比例读取所选监控单,<strong>不可手改</strong>;打到新止损时合并持仓亏损 ≈ 1 个风险单位(当前基数 × 监控 risk%)。<br>
斐波/突破为<strong>程序监控</strong>(mark 价穿越触发),触价后市价加仓;填写后直接点「执行滚仓」(无需预览)。同时仅允许 <strong>1</strong> 条监控中腿,提交后<strong>不可修改</strong>,可删除。<br>
手动平仓后滚仓监控自动结束;<strong>已成交腿历史保留</strong>供复盘。<br>
<a href="/strategy/roll/docs" target="_blank" rel="noopener" class="roll-doc-link">→ 顺势加仓完整逻辑说明</a><br>
{% if roll_trend_active %}<span style="color:#ff8f8f">当前有运行中的趋势回调计划,请先结束后再滚仓。</span>{% endif %}
</div>
</details>
<div id="roll-risk-banner" class="rule-tip roll-risk-banner">
当前风险:请选择持仓币种
</div>
<form id="roll-form" action="{{ url_for('strategy_roll_execute') }}" method="post" class="form-row" data-add-mode="market">
<select name="symbol" id="roll-symbol" required>
<option value="">选择持仓币种</option>
{% for o in roll_monitors %}
<option value="{{ o.symbol }}"
data-direction="{{ o.direction }}"
data-monitor-id="{{ o.id }}"
data-risk-percent="{{ o.risk_percent or default_risk_percent }}">
{{ o.symbol }} {{ '多' if o.direction=='long' else '空' }} #{{ o.id }} · 风险{{ o.risk_percent or default_risk_percent }}%
</option>
{% endfor %}
</select>
<input type="hidden" name="direction" id="roll-direction" value="long">
<select name="add_mode" id="roll-add-mode" onchange="var f=document.getElementById('roll-form');if(f){f.setAttribute('data-add-mode',this.value);if(window.syncRollFormMode)syncRollFormMode(f,this.value);}">
<option value="market">市价加仓</option>
<option value="fib_618">斐波 0.618</option>
<option value="fib_786">斐波 0.786</option>
<option value="breakout">突破加仓</option>
</select>
<span class="roll-field roll-field-fib">
<input name="fib_upper" id="roll-fib-upper" step="any" placeholder="上沿 H">
<input name="fib_lower" id="roll-fib-lower" step="any" placeholder="下沿 L">
</span>
<span class="roll-field roll-field-breakout">
<input name="breakthrough_price" id="roll-breakout" step="any" placeholder="突破价">
</span>
<input name="new_stop_loss" id="roll-stop-loss" type="number" min="0" step="any" placeholder="新止损价" required>
<button type="button" id="roll-preview-btn" class="roll-preview-btn" {% if roll_trend_active %}disabled{% endif %}>预览</button>
<button type="submit" id="roll-submit-btn" {% if roll_trend_active %}disabled style="opacity:.5" data-trend-locked="1"{% endif %} disabled>执行滚仓</button>
</form>
<div id="roll-preview-box" class="rule-tip roll-preview-box" style="display:none" role="status" aria-live="polite">
<div id="roll-preview-text"></div>
<div id="roll-countdown" class="roll-countdown" style="display:none"></div>
</div>
<h3 class="roll-section-title">活跃滚仓组</h3>
<div class="table-wrap">
<table>
<tr><th>ID</th><th>币种</th><th>方向</th><th>腿数</th><th>首仓TP</th><th>当前SL</th><th>当前均价</th><th>止盈盈利U</th></tr>
{% for g in roll_groups %}
<tr>
<td>{{ g.id }}</td>
<td>{{ g.symbol }}</td>
<td>{{ g.direction }}</td>
<td>{{ g.leg_count }}</td>
<td>{% if price_fmt %}{{ price_fmt(g.symbol, g.initial_take_profit) }}{% else %}{{ g.initial_take_profit }}{% endif %}</td>
<td>{% if price_fmt %}{{ price_fmt(g.symbol, g.current_stop_loss) }}{% else %}{{ g.current_stop_loss }}{% endif %}</td>
<td>{% if g.avg_entry_display %}{{ g.avg_entry_display }}{% elif g.avg_entry is not none %}{{ g.avg_entry }}{% else %}—{% endif %}</td>
<td>{% if g.reward_at_tp_usdt is not none %}{{ '%.2f'|format(g.reward_at_tp_usdt) }}{% else %}—{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="8" style="color:#8892b0">暂无</td></tr>
{% endfor %}
</table>
</div>
<h3 class="roll-section-title">最近滚仓腿</h3>
<div class="table-wrap">
<table>
<tr><th>#</th><th></th><th>方式</th><th>张数</th><th>触发/限价</th><th>新SL</th><th>状态</th><th>操作</th></tr>
{% for leg in roll_legs %}
<tr>
<td>{{ leg.leg_index }}</td>
<td>{{ leg.roll_group_id }}</td>
<td>{{ leg.add_mode }}</td>
<td>{% if leg.amount %}{{ leg.amount }}{% else %}—{% endif %}</td>
<td>{% if leg.limit_price %}{{ leg.limit_price }}{% elif leg.breakthrough_price %}{{ leg.breakthrough_price }}{% elif leg.fill_price %}{{ leg.fill_price }}{% else %}—{% endif %}</td>
<td>{{ leg.new_stop_loss }}</td>
<td>{{ leg.status_label or leg.status }}</td>
<td>
{% if leg.status == 'pending' %}
<form action="{{ url_for('strategy_roll_cancel_leg', leg_id=leg.id) }}" method="post" style="margin:0" onsubmit="return confirm('确认删除本条滚仓监控?')">
<button type="submit" style="padding:2px 8px;font-size:.75rem">删除</button>
</form>
{% else %}—{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="8" style="color:#8892b0">暂无</td></tr>
{% endfor %}
</table>
</div>
</div>
@@ -0,0 +1,4 @@
<div class="strategy-subnav top-nav" style="margin:0 0 12px;padding-bottom:8px;border-bottom:1px solid #2a3150">
<a href="/strategy/trend" class="{% if page == 'strategy_trend' %}active{% endif %}">趋势回调</a>
<a href="/strategy/roll" class="{% if page == 'strategy_roll' %}active{% endif %}">顺势加仓</a>
</div>
@@ -0,0 +1,47 @@
<style>
.trade-panels-row,.dual-panel-grid,.strategy-trading-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
.strategy-trading-grid .card{min-height:320px;display:flex;flex-direction:column}
.strategy-trading-grid .panel-scroll{flex:1;overflow:auto;max-height:78vh}
.strategy-panel-inner.trend-card{display:flex;flex-direction:column;gap:12px}
.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}
.plan-cell{display:flex;flex-direction:column;gap:3px}
.plan-cell .lbl{font-size:.72rem;color:#8b95b8}
.plan-cell .val{color:#f0f2ff;font-size:.88rem;font-weight:500}
.plan-cell .val.pnl-profit{color:#4cd97f}
.plan-cell .val.pnl-loss{color:#ff6666}
.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;display:inline-block}
.btn-close-plan:hover{filter:brightness(1.08)}
.plan-dca-block{margin-top:12px;padding-top:10px;border-top:1px dashed #2a3558}
.plan-dca-title{font-size:.74rem;color:#8b95b8;margin-bottom:8px;letter-spacing:.02em}
.plan-dca-table{width:100%;border-collapse:collapse;font-size:.76rem}
.plan-dca-table th,.plan-dca-table td{padding:6px 8px;border-bottom:1px solid #243050;text-align:left}
.plan-dca-table th{color:#6a7598;font-weight:600}
.plan-dca-table .st-done{color:#4cd97f}
.plan-dca-table .st-pending{color:#9aa3c4}
@media (max-width:720px){
.plan-card-grid{grid-template-columns:1fr}
}
@media (max-width:1200px){
.trade-panels-row,.dual-panel-grid,.strategy-trading-grid{grid-template-columns:1fr}
}
</style>
<div class="dual-panel-grid trade-panels-row strategy-trading-grid" style="grid-column:1/-1;align-items:stretch">
<div class="card strategy-panel-trend" style="display:flex;flex-direction:column;min-height:320px">
<div class="panel-scroll" style="flex:1;max-height:78vh;overflow:auto">
{% include 'strategy_trend_panel.html' %}
</div>
</div>
<div class="card strategy-panel-roll" style="display:flex;flex-direction:column;min-height:320px">
<div class="panel-scroll" style="flex:1;max-height:78vh;overflow:auto">
{% include 'strategy_roll_panel.html' %}
</div>
</div>
</div>
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>趋势回调 · {{ exchange_display }}</title>
<style>
body{font-family:system-ui,sans-serif;background:#0f1117;color:#e6e8ef;padding:24px}
a{color:#8fc8ff;margin-right:10px}
.box{max-width:640px;background:#151a2a;border:1px solid #2a3150;padding:20px;border-radius:10px}
</style>
</head>
<body>
<p><a href="/trade">← 实盘下单</a> <a href="/strategy/roll">顺势加仓</a></p>
<div class="box">
<h1>趋势回调</h1>
<p>{{ trend_note }}</p>
<p style="color:#8892b0;font-size:.9rem">趋势回调含自动补仓档位,仅在 Gate 趋势机器人(crypto_monitor_gate_bot)实例中运行。</p>
</div>
</body>
</html>
@@ -0,0 +1,16 @@
{% include 'strategy_subnav.html' %}
<div class="card trend-card" style="grid-column:1/-1">
<h2 style="margin-bottom:8px">趋势回调</h2>
<details class="tip-collapse strategy-trend-disabled-collapse">
<summary class="tip-collapse-summary">趋势回调说明(本实例未启用)</summary>
<div class="tip-collapse-body rule-tip">
{{ trend_disabled_note }}<br><br>
趋势回调含自动补仓档位与预览执行,仅在 <strong>Gate 趋势机器人</strong><code>crypto_monitor_gate_bot</code>)实例中运行。
请访问该实例同一菜单「策略交易 → 趋势回调」,或常用地址 <code>:5002/strategy/trend</code>
</div>
</details>
<p style="margin-top:12px;font-size:.85rem">
<a href="/trade" style="color:#8fc8ff">返回实盘下单</a>
<a href="/strategy/roll" style="color:#8fc8ff">顺势加仓(本实例可用)</a>
</p>
</div>
@@ -0,0 +1,208 @@
{% set mf = money_fmt|default(funds_fmt) %}
{% macro amt_disp(sym, val) %}{% if amt_fmt is defined %}{{ amt_fmt(sym, val) }}{% else %}{{ val }}{% endif %}{% endmacro %}
<div class="strategy-panel-inner trend-card">
<h2 style="margin-bottom:8px">趋势回调</h2>
<details class="tip-collapse strategy-trend-rule-collapse">
<summary class="tip-collapse-summary">趋势回调规则说明</summary>
<div class="tip-collapse-body rule-tip">
<strong>生成预览</strong>:读取合约 USDT <strong>可用余额快照</strong>并计算计划(不下单)。预览有效期 <strong>{{ trend_pullback_preview_ttl }} 秒</strong><br>
<strong>确认执行</strong>:市价首仓 50% + 挂交易所止损;首仓后可<strong>手动保本</strong>(默认均价+{{ trend_manual_breakeven_offset_pct }}%);剩余 50% 在止损与补仓区间之间共 {{ trend_pullback_dca_legs }} 档(做多为<strong>上沿</strong>、做空为<strong>下沿</strong>;程序可能因最小张数自动减档)市价补仓;<strong>止盈由程序监控</strong><br>
确认执行时若当前可用余额与预览快照相对偏差 &gt; <strong>{{ trend_preview_max_drift_pct }}%</strong> 会拒绝并要求重新预览。
</div>
</details>
{% if trend_dca_probes %}
{% for p in trend_dca_probes %}
{% if p.trigger_reached and p.block_reason %}
<div class="rule-tip" style="margin-bottom:10px;border-color:#a55;background:#2a1818;color:#ffb4b4">
<strong>计划 #{{ p.plan_id }}</strong> 标记价 {{ p.mark_price }} 已触达补仓触发价 {{ p.next_trigger }},但未自动补仓:
{{ p.block_reason }}。
{% if not live_trading_enabled %}
请在 <code>crypto_monitor_gate_bot/.env</code> 设置 <code>LIVE_TRADING_ENABLED=true</code> 后重启 PM2 进程 <strong>crypto_gate_bot</strong>(不是 manual-agent-gate-bot)。
{% endif %}
</div>
{% endif %}
{% endfor %}
{% endif %}
<form id="trend-pullback-form" action="{{ url_for('preview_trend_pullback') }}" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 ETH/USDT" required>
<select name="direction" id="trend-direction" required>
<option value="">方向</option>
<option value="long">做多</option>
<option value="short">做空</option>
</select>
<input name="leverage" type="number" min="1" step="1" placeholder="杠杆(必填)" required>
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" placeholder="风险%相对可用快照" title="默认5:最坏亏损约≤可用余额×5%">
<input name="sl" step="any" placeholder="止损价" required>
<input name="add_upper" id="trend-add-upper" step="any" placeholder="补仓上沿价" required>
<input name="take_profit" step="any" placeholder="止盈价(固定)" required>
<button type="submit" {% if can_trade_trend is defined %}{% if not can_trade_trend %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}{% elif not can_trade %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}>生成预览</button>
</form>
<script>
(function(){
const dirSel = document.getElementById("trend-direction");
const addInp = document.getElementById("trend-add-upper");
function syncAddUpperPlaceholder(){
if(!addInp || !dirSel) return;
const d = (dirSel.value || "long").toLowerCase();
addInp.placeholder = d === "short" ? "补仓下沿价" : "补仓上沿价";
}
if(dirSel){
dirSel.addEventListener("change", syncAddUpperPlaceholder);
syncAddUpperPlaceholder();
}
})();
</script>
{% if trend_preview %}
<div style="margin-top:14px;padding:12px;background:#141a2e;border:1px solid #2a3150;border-radius:8px">
<div style="display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px;margin-bottom:8px">
<strong style="color:#dbe4ff">当前预览(剩余 <span id="trend-preview-ttl">{{ trend_pullback_preview_ttl }}</span>s</strong>
<span style="font-size:.8rem;color:#9aa" data-expires-ms="{{ preview_expires_ms }}">倒计时加载中…</span>
</div>
<div style="font-size:.82rem;color:#cfd3ef;line-height:1.55;margin-bottom:10px">
{{ trend_preview.symbol }} {{ '做多' if trend_preview.direction == 'long' else '做空' }} {{ trend_preview.leverage }}x
预览可用快照 <strong>{{ mf(trend_preview.snapshot_available_usdt) }}</strong> U 参考价 {{ price_fmt(trend_preview.symbol, trend_preview.live_price_ref) }}
计划保证金≈{{ mf(trend_preview.plan_margin_capital) }} U 总张≈{{ amt_disp(trend_preview.symbol, trend_preview.target_order_amount) }}(首仓 {{ amt_disp(trend_preview.symbol, trend_preview.first_order_amount) }} + 补仓 {{ amt_disp(trend_preview.symbol, trend_preview.remainder_total) }}<br>
止损价 {{ price_fmt(trend_preview.symbol, trend_preview.preview_unified_stop_loss or trend_preview.stop_loss) }} 止损金额 {% if trend_preview.preview_risk_amount_u is not none %}{{ mf(trend_preview.preview_risk_amount_u) }}U{% else %}—{% endif %}(快照×风险{{ trend_preview.risk_percent }}%)| {{ trend_add_zone_label(trend_preview.direction) }} {{ price_fmt(trend_preview.symbol, trend_preview.add_upper) }} 止盈价 {{ price_fmt(trend_preview.symbol, trend_preview.take_profit) }} 首仓盈亏比 {% if trend_preview.preview_target_rr is not none %}{{ '%.2f'|format(trend_preview.preview_target_rr) }}{% else %}—{% endif %}
</div>
<div class="table-wrap" style="margin-bottom:10px">
<table>
<tr><th>档位</th><th>触发/参考价</th><th>张数</th><th>加仓后均价</th><th>止盈盈利(U)</th><th>止损(U)</th><th>盈亏比</th></tr>
{% for row in trend_preview_levels %}
<tr>
<td>{{ row.label or row.i }}</td>
<td>{{ price_fmt(trend_preview.symbol, row.price) }}</td>
<td>{{ amt_disp(trend_preview.symbol, row.contracts) }}</td>
<td>{% if row.avg_entry is not none %}{{ price_fmt(trend_preview.symbol, row.avg_entry) }}{% else %}—{% endif %}</td>
<td>{% if row.profit_u is not none %}{{ mf(row.profit_u) }}{% else %}—{% endif %}</td>
<td>{% if row.risk_u is not none %}{{ mf(row.risk_u) }}{% else %}—{% endif %}</td>
<td>{% if row.rr is not none %}{{ '%.2f'|format(row.rr) }}{% else %}—{% endif %}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="form-row" style="gap:10px;align-items:center">
<form action="{{ url_for('execute_trend_pullback') }}" method="post" style="display:inline">
<input type="hidden" name="preview_id" value="{{ trend_preview.id }}">
<button type="submit" onclick="return confirm('确认按预览参数实盘下单?')">确认执行(实盘)</button>
</form>
<form action="{{ url_for('cancel_trend_pullback_preview') }}" method="post" style="display:inline">
<input type="hidden" name="preview_id" value="{{ trend_preview.id }}">
<button type="submit" style="background:#2f2134;color:#ffb2b2">取消预览</button>
</form>
</div>
</div>
<script>
(function(){
const el = document.querySelector("[data-expires-ms]");
if(!el) return;
const exp = parseInt(el.getAttribute("data-expires-ms")||"0",10);
function tick(){
const left = Math.max(0, Math.floor((exp - Date.now()) / 1000));
el.innerText = left > 0 ? ("剩余 " + left + " 秒") : "已过期,请重新生成预览";
const span = document.getElementById("trend-preview-ttl");
if(span) span.innerText = String(left);
if(left <= 0) return;
setTimeout(tick, 1000);
}
tick();
})();
</script>
{% elif trend_preview_expired %}
<div class="rule-tip" style="margin-top:12px;color:#ff8f8f">该预览已过期(超过 {{ trend_pullback_preview_ttl }} 秒),请重新点击「生成预览」。</div>
{% endif %}
<div class="trend-running-plans">
<h3 style="margin:0 0 10px;font-size:.95rem;color:#b8c4ff">运行中的计划</h3>
<div class="running-plans-stack">
{% for t in trend_plans %}
{% set sym = t.exchange_symbol or t.symbol %}
{% set calc = namespace(pnlpct=None) %}
{% 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 %}
<div class="plan-position-card">
<div class="plan-card-head">
<div class="plan-card-title">
<span>#{{ t.id }} {{ sym }}</span>
<span class="badge {{ 'direction-long' if t.direction == 'long' else 'direction-short' }}">{{ '做多' if t.direction == 'long' else '做空' }}</span>
</div>
<a href="/stop_trend_pullback/{{ t.id }}" class="btn-close-plan" onclick="return confirm('结束计划:市价平仓并撤掉该合约全部挂单,确定?')">结束计划</a>
</div>
<div class="plan-card-meta">
来源: 趋势回调计划 风险: {% if t.risk_percent is not none %}{{ t.risk_percent }}%{% else %}—{% endif %}
<span class="accent">{{ trend_add_zone_label(t.direction) }} {{ price_fmt(sym, t.add_upper) }}</span>
| 已补仓 <strong>{{ t.legs_done }}/{{ t.dca_legs }}</strong>
</div>
<div class="plan-card-grid plan-card-grid--metrics">
<div class="plan-cell">
<span class="lbl">均价</span>
<span class="val">{% if t.avg_entry_price is not none %}{{ price_fmt(sym, t.avg_entry_price) }}{% else %}—{% endif %}</span>
</div>
<div class="plan-cell">
<span class="lbl">止损</span>
<span class="val">{{ price_fmt(sym, t.stop_loss) }}</span>
</div>
<div class="plan-cell">
<span class="lbl">止盈</span>
<span class="val">{{ price_fmt(sym, t.take_profit) }}</span>
</div>
<div class="plan-cell">
<span class="lbl">盈亏比</span>
<span class="val">{% if t.money_rr is not none %}{{ '%.2f'|format(t.money_rr) }}:1{% elif t.planned_rr is not none %}{{ '%.2f'|format(t.planned_rr) }}:1{% else %}—{% endif %}</span>
</div>
<div class="plan-cell">
<span class="lbl">标记价</span>
<span class="val">{% if t.floating_mark is not none %}{{ price_fmt(sym, t.floating_mark) }}{% else %}—{% endif %}</span>
</div>
<div class="plan-cell">
<span class="lbl">浮盈亏</span>
<span class="val {% if t.floating_pnl is not none %}{% if t.floating_pnl > 0 %}pnl-profit{% elif t.floating_pnl < 0 %}pnl-loss{% else %}pnl-neutral{% endif %}{% endif %}">
{% if t.floating_pnl is not none %}
{{ mf(t.floating_pnl) }}U{% if calc.pnlpct is not none %} ({{ '%+.2f'|format(calc.pnlpct) }}%){% endif %}
{% else %}—{% endif %}
</span>
</div>
</div>
{% if t.dca_levels %}
<div class="plan-dca-block">
<div class="plan-dca-title">补仓计划明细</div>
<table class="plan-dca-table">
<tr><th>档位</th><th>触发价</th><th>张数</th><th>加仓后均价</th><th>止盈盈利(U)</th><th>止损(U)</th><th>盈亏比</th><th>状态</th></tr>
{% for lv in t.dca_levels %}
<tr>
<td>{{ lv.label }}</td>
<td>{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %}</td>
<td>{% if lv.contracts is not none %}{{ amt_disp(sym, lv.contracts) }}{% else %}—{% endif %}</td>
<td>{% if lv.avg_entry is not none %}{{ price_fmt(sym, lv.avg_entry) }}{% else %}—{% endif %}</td>
<td>{% if lv.profit_u is not none %}{{ mf(lv.profit_u) }}{% else %}—{% endif %}</td>
<td>{% if lv.risk_u is not none %}{{ mf(lv.risk_u) }}{% else %}—{% endif %}</td>
<td>{% if lv.rr is not none %}{{ '%.2f'|format(lv.rr) }}{% else %}—{% endif %}</td>
<td class="{% if lv.status == 'done' %}st-done{% else %}st-pending{% endif %}">{{ lv.status_label }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
<div class="plan-card-meta" style="margin-top:8px">
<form action="{{ url_for('trend_pullback_breakeven', pid=t.id) }}" method="post" class="form-row" style="margin:0;align-items:center" onsubmit="return confirm('确认保本?将结束本趋势计划,持仓移交「下单监控」(备注趋势回调计划),并在交易所同时挂保本止损与计划止盈;后续平仓会写入交易记录。');">
<label style="font-size:.78rem;color:#cfd3ef;display:flex;align-items:center;gap:6px">
保本移交 偏移%
<input name="breakeven_offset_pct" type="number" min="0" step="0.01" value="{{ trend_manual_breakeven_offset_pct }}" style="width:72px;padding:4px 8px">
</label>
<button type="submit" style="padding:6px 12px;background:#1f4a3a;color:#8fc8ff">保本移交下单监控</button>
{% if t.breakeven_applied %}<span style="color:#6ab88a;font-size:.75rem">已保本 {{ (t.breakeven_applied_at or '')[:16] }}</span>{% endif %}
</form>
</div>
<div class="plan-card-meta" style="margin-bottom:0">
快照可用: {% if t.snapshot_available_usdt is not none %}{{ mf(t.snapshot_available_usdt) }}U{% else %}—{% endif %}
计划保证金≈{% if t.plan_margin_capital is not none %}{{ mf(t.plan_margin_capital) }}U{% else %}—{% endif %}
杠杆: {{ t.leverage }}x
</div>
</div>
{% else %}
<div class="plan-position-card" style="color:#8892b0;text-align:center;padding:16px">暂无运行中的趋势回调计划</div>
{% endfor %}
</div>
</div>
</div>