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,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>