feat(gate-bot): align order monitor with Gate main site
Dual-panel trade UI, exchange TP/SL entrust modal, and place/cancel_tpsl APIs so bot manual trading matches Gate.
This commit is contained in:
@@ -113,6 +113,8 @@
|
||||
.table-wrap{overflow-x:auto}
|
||||
.trade-dashboard{grid-column:1/-1;display:flex;flex-direction:column;gap:14px}
|
||||
.trade-panels-row,.dual-panel-grid,.strategy-trading-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
|
||||
.dual-panel-grid .card{height:100%;display:flex;flex-direction:column}
|
||||
.panel-scroll{flex:1;min-height:280px;max-height:420px;overflow:auto}
|
||||
.strategy-trading-grid .card{min-height:320px;display:flex;flex-direction:column}
|
||||
.strategy-trading-grid .panel-scroll{flex:1;overflow:auto;max-height:78vh}
|
||||
.trade-panels-row > .card{min-height:0;height:100%;display:flex;flex-direction:column;box-sizing:border-box}
|
||||
@@ -157,6 +159,7 @@
|
||||
.review-card.is-fullscreen .panel-item{max-height:none;height:auto;min-height:280px}
|
||||
.review-card.is-fullscreen .ai-result{max-height:min(36vh, 320px)}
|
||||
@media (min-width: 1440px){
|
||||
.panel-scroll,.pos-list{max-height:420px}
|
||||
.order-card .order-live-positions{max-height:420px}
|
||||
.records-card .table-wrap{max-height:620px;overflow:auto}
|
||||
}
|
||||
@@ -174,6 +177,7 @@
|
||||
@media (max-width: 1100px){
|
||||
.grid{grid-template-columns:1fr}
|
||||
.trade-dashboard,.records-card,.review-card{grid-column:auto}
|
||||
.dual-panel-grid{grid-template-columns:1fr}
|
||||
.panel-list{grid-template-columns:1fr}
|
||||
}
|
||||
@media (max-width:1200px){
|
||||
@@ -197,6 +201,52 @@
|
||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||
.key-history .list{max-height:200px}
|
||||
.pos-list{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto}
|
||||
.dual-panel-grid .pos-list-live{max-height:none;overflow:visible;flex:1 1 auto}
|
||||
.dual-panel-grid .panel-scroll.pos-list-live{max-height:none;overflow:visible}
|
||||
.pos-card{background:#141923;border:1px solid #2a3348;border-radius:10px;padding:12px 14px}
|
||||
.pos-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
||||
.pos-meta{font-size:.74rem;color:#8b95a8;line-height:1.45;margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:4px 0}
|
||||
.pos-meta-item{display:inline-flex;align-items:center}
|
||||
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
|
||||
.pos-meta-on{color:#6eb5ff}
|
||||
.pos-meta-off{color:#7d8799}
|
||||
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
|
||||
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
|
||||
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
|
||||
.pos-side-long{background:#253a6e;color:#6eb5ff}
|
||||
.pos-side-short{background:#4a2230;color:#ff8a8a}
|
||||
.pos-head-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
||||
.pos-entrust-btn{padding:6px 12px;background:#2a4a7a;color:#8fc8ff;border:none;border-radius:8px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap}
|
||||
.pos-entrust-btn:hover{background:#355d96}
|
||||
.pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap;border:none;cursor:pointer;display:inline-block}
|
||||
.pos-close-btn:hover{background:#d66565;color:#fff}
|
||||
.pos-ex-orders{margin-top:10px;padding-top:10px;border-top:1px dashed #2a3348}
|
||||
.pos-ex-orders-title{font-size:.74rem;color:#7d8799;margin-bottom:6px}
|
||||
.pos-ex-order-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.78rem;color:#c5cce0;margin-top:5px}
|
||||
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
||||
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
||||
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
||||
.tpsl-modal-backdrop.open{display:flex}
|
||||
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
||||
.tpsl-modal h3{margin:0 0 12px;font-size:1rem;color:#fff}
|
||||
.tpsl-modal .form-row{margin-bottom:10px}
|
||||
.tpsl-modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
|
||||
.tpsl-modal-actions button{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem}
|
||||
.tpsl-modal-submit{background:#2d6a4f;color:#fff}
|
||||
.tpsl-modal-cancel{background:#3a3f52;color:#ddd}
|
||||
.pos-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;margin-bottom:12px}
|
||||
.pos-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||||
.pos-label{font-size:.72rem;color:#7d8799}
|
||||
.pos-value{font-size:.88rem;color:#e8ecf4;font-weight:500;line-height:1.25}
|
||||
.pos-val-dash{opacity:.75;color:#8b95a8}
|
||||
.pos-value.price-up{color:#4cd97f}
|
||||
.pos-value.price-down{color:#ff6666}
|
||||
.pos-value.price-flat{color:#e8ecf4}
|
||||
.pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689}
|
||||
.pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
|
||||
@media (max-width:520px){.pos-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.stats-card{grid-column:1/-1;margin-top:14px}
|
||||
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
|
||||
.stats-card.collapsed .stats-content{display:none}
|
||||
@@ -214,7 +264,7 @@
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<body data-page="{{ page }}">
|
||||
{% macro period_metrics_cells(s) %}
|
||||
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
|
||||
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
|
||||
@@ -310,9 +360,8 @@
|
||||
|
||||
<div class="grid">
|
||||
{% if page == 'trade' %}
|
||||
<div class="trade-dashboard">
|
||||
<div class="trade-panels-row">
|
||||
<div class="card order-card">
|
||||
<div class="dual-panel-grid" style="grid-column:1/-1">
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
||||
<h2 style="margin-bottom:0">机器人下单监控(单仓)</h2>
|
||||
{% if focus_order_id %}
|
||||
@@ -322,9 +371,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="rule-tip" id="order-rule-tip">
|
||||
规则:最大同时持仓 {{ max_active_positions }}(与 Gate 主站 MAX_ACTIVE_POSITIONS 一致,当前 active {{ active_count }});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;
|
||||
规则:最大同时持仓 {{ max_active_positions }}(当前 active {{ active_count }});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;
|
||||
{% if can_trade %}可开仓{% else %}不可开仓(持仓达上限、有趋势回调计划,或未到北京时间 {{ reset_hour }}:00){% endif %};
|
||||
按风险比例自动计算仓位
|
||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
||||
</div>
|
||||
<div class="rule-tip">
|
||||
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
|
||||
@@ -365,7 +414,9 @@
|
||||
<option value="trend">趋势单</option>
|
||||
<option value="swing">波段单</option>
|
||||
</select>
|
||||
{% if position_sizing_mode != 'full_margin' %}
|
||||
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
||||
{% endif %}
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||
</label>
|
||||
@@ -379,64 +430,120 @@
|
||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||
<button type="submit">{{ open_position_button_label }}</button>
|
||||
</form>
|
||||
<div class="order-live-positions">
|
||||
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff">实时持仓</h3>
|
||||
<div class="running-plans-stack">
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||
<div class="panel-scroll pos-list pos-list-live">
|
||||
{% for o in order %}
|
||||
{% set osym = o.exchange_symbol or o.symbol %}
|
||||
<div class="plan-position-card">
|
||||
<div class="plan-card-head">
|
||||
<div class="plan-card-title">
|
||||
<span>{{ osym }}</span>
|
||||
<span class="badge {{ 'direction-long' if o.direction == 'long' else 'direction-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
|
||||
<div class="pos-card" id="order-row-{{ o.id }}"
|
||||
data-monitor-id="{{ o.id }}"
|
||||
data-symbol="{{ o.symbol }}"
|
||||
data-direction="{{ o.direction }}"
|
||||
data-plan-sl="{% if o.stop_loss %}{{ price_fmt(o.symbol, o.stop_loss) }}{% endif %}"
|
||||
data-plan-tp="{% if o.take_profit %}{{ price_fmt(o.symbol, o.take_profit) }}{% endif %}"
|
||||
data-entry="{% if o.trigger_price %}{{ price_fmt(o.symbol, o.trigger_price) }}{% endif %}">
|
||||
<div class="pos-card-head">
|
||||
<div class="pos-card-symbol">
|
||||
<strong>{{ o.exchange_symbol or o.symbol }}</strong>
|
||||
<span class="pos-side-badge {{ 'pos-side-long' if o.direction == 'long' else 'pos-side-short' }}">{{ '做多' if o.direction == 'long' else '做空' }}</span>
|
||||
</div>
|
||||
<a href="/del_order/{{ o.id }}" class="btn-close-plan" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
</div>
|
||||
<div class="plan-card-meta">
|
||||
来源: 下单监控 | 风格: {{ o.trade_style or 'trend' }} | 风险: {% if o.risk_percent is not none %}{{ o.risk_percent }}%{% else %}—{% endif %}≈{{ money_fmt(o.risk_amount) }}U
|
||||
| {% if o.breakeven_enabled %}<span class="accent">移动保本: 开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(osym, o.breakeven_price) }}</span>{% else %}移动保本: 关{% endif %}
|
||||
<span id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
|
||||
</div>
|
||||
<div class="plan-card-grid">
|
||||
<div class="plan-cell">
|
||||
<span class="lbl">成交价</span>
|
||||
<span class="val">{{ price_fmt(osym, o.trigger_price) }}</span>
|
||||
</div>
|
||||
<div class="plan-cell">
|
||||
<span class="lbl">止损</span>
|
||||
<span class="val">{{ price_fmt(osym, o.stop_loss) }}</span>
|
||||
</div>
|
||||
<div class="plan-cell">
|
||||
<span class="lbl">止盈</span>
|
||||
<span class="val">{{ price_fmt(osym, o.take_profit) }}</span>
|
||||
</div>
|
||||
<div class="plan-cell">
|
||||
<span class="lbl">盈亏比</span>
|
||||
<span class="val"><span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%.2f'|format(o.rr_ratio) }}:1{% else %}—{% endif %}</span></span>
|
||||
</div>
|
||||
<div class="plan-cell">
|
||||
<span class="lbl">标记价</span>
|
||||
<span class="val"><span id="order-price-{{ o.id }}">-</span></span>
|
||||
</div>
|
||||
<div class="plan-cell">
|
||||
<span class="lbl">浮盈亏</span>
|
||||
<span class="val"><span id="order-pnl-{{ o.id }}">-</span></span>
|
||||
<div class="pos-head-actions">
|
||||
<button type="button" class="pos-entrust-btn" onclick="openTpslEntrustModal({{ o.id }})">委托</button>
|
||||
<a href="/del_order/{{ o.id }}" class="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-card-meta" style="margin-bottom:0">
|
||||
保证金: <span id="order-ex-margin-{{ o.id }}">-</span>
|
||||
| 计划基数: {{ money_fmt(o.margin_capital) }}U
|
||||
| 杠杆: {{ o.leverage }}x
|
||||
| 仓位占比: {{ o.position_ratio }}%
|
||||
<div class="pos-meta">
|
||||
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
|
||||
<span class="pos-meta-item">风格: {{ o.trade_style or 'trend' }}</span>
|
||||
<span class="pos-meta-item">风险: {{ o.risk_percent or '-' }}%≈{{ money_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U</span>
|
||||
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
|
||||
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||
</span>
|
||||
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
|
||||
</div>
|
||||
<div class="pos-grid">
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">成交价</span>
|
||||
<span class="pos-value">{{ price_fmt(o.symbol, o.trigger_price) }}</span>
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">止损</span>
|
||||
{% if o.stop_loss %}
|
||||
<span class="pos-value">{{ price_fmt(o.symbol, o.stop_loss) }}</span>
|
||||
{% else %}
|
||||
<span class="pos-value pos-val-dash">—</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">止盈</span>
|
||||
{% if o.take_profit %}
|
||||
<span class="pos-value">{{ price_fmt(o.symbol, o.take_profit) }}</span>
|
||||
{% else %}
|
||||
<span class="pos-value pos-val-dash">—</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">盈亏比</span>
|
||||
<span class="pos-value" id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}{{ '%g'|format(o.rr_ratio) }}:1{% else %}-:1{% endif %}</span>
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">标记价</span>
|
||||
<span class="pos-value" id="order-price-{{ o.id }}">-</span>
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">浮盈亏</span>
|
||||
<span class="pos-value" id="order-pnl-{{ o.id }}">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pos-footer">
|
||||
<span>保证金: <span id="order-ex-margin-{{ o.id }}">-</span></span>
|
||||
<span>计划基数: {{ money_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
||||
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
||||
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
||||
</div>
|
||||
<div class="pos-ex-orders">
|
||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||
<div class="pos-ex-order-row">
|
||||
<span class="pos-ex-order-main" id="ex-sl-text-{{ o.id }}">止损:加载中…</span>
|
||||
<button type="button" class="pos-ex-cancel-btn" id="ex-sl-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'sl')">撤单</button>
|
||||
</div>
|
||||
<div class="pos-ex-order-row">
|
||||
<span class="pos-ex-order-main" id="ex-tp-text-{{ o.id }}">止盈:加载中…</span>
|
||||
<button type="button" class="pos-ex-cancel-btn" id="ex-tp-cancel-{{ o.id }}" disabled onclick="cancelExchangeTpsl({{ o.id }}, 'tp')">撤单</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="plan-position-card" style="color:#8892b0;text-align:center;padding:16px">暂无机器人持仓</div>
|
||||
<div class="pos-empty">暂无持仓</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tpsl-modal" class="tpsl-modal-backdrop" onclick="if(event.target===this)closeTpslEntrustModal()">
|
||||
<div class="tpsl-modal" onclick="event.stopPropagation()">
|
||||
<h3 id="tpsl-modal-title">挂止盈止损</h3>
|
||||
<p style="font-size:.78rem;color:#8892b0;margin:0 0 10px">将先撤销该合约已有 TP/SL,再按下列价格重挂。</p>
|
||||
<div class="form-row">
|
||||
<select id="tpsl-modal-mode" onchange="toggleTpslModalMode()">
|
||||
<option value="price">价格模式</option>
|
||||
<option value="pct">百分比模式</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="tpsl-modal-sl" step="any" placeholder="止损价格">
|
||||
<input id="tpsl-modal-tp" step="any" placeholder="止盈价格">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="tpsl-modal-sl-pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
||||
<input id="tpsl-modal-tp-pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||
</div>
|
||||
<div class="tpsl-modal-actions">
|
||||
<button type="button" class="tpsl-modal-cancel" onclick="closeTpslEntrustModal()">取消</button>
|
||||
<button type="button" class="tpsl-modal-submit" onclick="submitTpslEntrust()">先撤后挂</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
|
||||
{% set can_trade_trend = can_trade %}
|
||||
@@ -1462,20 +1569,6 @@ if(keyForm){
|
||||
});
|
||||
});
|
||||
}
|
||||
const addOrderForm = document.getElementById("add-order-form");
|
||||
if(addOrderForm){
|
||||
addOrderForm.addEventListener("submit", function(ev){
|
||||
if(addOrderForm.dataset.submitOnce === "1"){
|
||||
addOrderForm.dataset.submitOnce = "0";
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||
addOrderForm.dataset.submitOnce = "1";
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(addOrderForm, "开仓提交中…");
|
||||
else addOrderForm.submit();
|
||||
});
|
||||
}
|
||||
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
|
||||
setTimeout(() => {
|
||||
if(document.getElementById("journal-list")) loadJournals();
|
||||
@@ -1484,6 +1577,132 @@ setTimeout(() => {
|
||||
|
||||
let latestAvailableUsdt = null;
|
||||
const lastPriceMap = {};
|
||||
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
|
||||
function calcClientRr(direction, entry, sl, tp){
|
||||
const e = Number(entry), s = Number(sl), t = Number(tp);
|
||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
||||
if(direction === 'short'){
|
||||
if(s <= e || t >= e) return null;
|
||||
return (e - t) / (s - e);
|
||||
}
|
||||
if(s >= e || t <= e) return null;
|
||||
return (t - e) / (e - s);
|
||||
}
|
||||
function calcClientRrFromPct(slPct, tpPct){
|
||||
const sl = Number(slPct), tp = Number(tpPct);
|
||||
if(!Number.isFinite(sl) || !Number.isFinite(tp) || sl <= 0 || tp <= 0) return null;
|
||||
return tp / sl;
|
||||
}
|
||||
function rejectManualOrderRr(rr){
|
||||
if(rr !== null && rr >= MANUAL_MIN_PLANNED_RR) return false;
|
||||
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单。`);
|
||||
return true;
|
||||
}
|
||||
let tpslEntrustMonitorId = null;
|
||||
function formatExTpslLine(role, slot){
|
||||
const label = role === 'sl' ? '止损' : '止盈';
|
||||
if(!slot || !slot.order_id) return label + ':未挂单';
|
||||
const px = slot.trigger_display || slot.trigger_price || '-';
|
||||
const amt = slot.amount != null && !Number.isNaN(Number(slot.amount)) ? ` 数量 ${Number(slot.amount)}` : '';
|
||||
return `${label}:触发 ${px}${amt}`;
|
||||
}
|
||||
function paintExchangeTpslRow(orderId, tpsl){
|
||||
const data = tpsl || {};
|
||||
const slText = document.getElementById(`ex-sl-text-${orderId}`);
|
||||
const tpText = document.getElementById(`ex-tp-text-${orderId}`);
|
||||
const slBtn = document.getElementById(`ex-sl-cancel-${orderId}`);
|
||||
const tpBtn = document.getElementById(`ex-tp-cancel-${orderId}`);
|
||||
if(slText) slText.innerText = formatExTpslLine('sl', data.sl);
|
||||
if(tpText) tpText.innerText = formatExTpslLine('tp', data.tp);
|
||||
if(slBtn) slBtn.disabled = !(data.sl && data.sl.order_id);
|
||||
if(tpBtn) tpBtn.disabled = !(data.tp && data.tp.order_id);
|
||||
}
|
||||
function toggleTpslModalMode(){
|
||||
const mode = (document.getElementById('tpsl-modal-mode')||{}).value || 'price';
|
||||
const pct = mode === 'pct';
|
||||
['tpsl-modal-sl','tpsl-modal-tp'].forEach(id=>{ const el=document.getElementById(id); if(el) el.style.display=pct?'none':''; });
|
||||
['tpsl-modal-sl-pct','tpsl-modal-tp-pct'].forEach(id=>{ const el=document.getElementById(id); if(el) el.style.display=pct?'':'none'; });
|
||||
}
|
||||
function openTpslEntrustModal(orderId){
|
||||
const card = document.getElementById(`order-row-${orderId}`);
|
||||
if(!card) return;
|
||||
tpslEntrustMonitorId = orderId;
|
||||
const slEl = document.getElementById('tpsl-modal-sl');
|
||||
const tpEl = document.getElementById('tpsl-modal-tp');
|
||||
if(slEl) slEl.value = formatPriceForInput(card.getAttribute('data-plan-sl') || '');
|
||||
if(tpEl) tpEl.value = formatPriceForInput(card.getAttribute('data-plan-tp') || '');
|
||||
const modeEl = document.getElementById('tpsl-modal-mode');
|
||||
if(modeEl) modeEl.value = 'price';
|
||||
toggleTpslModalMode();
|
||||
const title = document.getElementById('tpsl-modal-title');
|
||||
if(title) title.innerText = `挂止盈止损 · ${card.getAttribute('data-symbol')||''}`;
|
||||
const modal = document.getElementById('tpsl-modal');
|
||||
if(modal) modal.classList.add('open');
|
||||
}
|
||||
function closeTpslEntrustModal(){
|
||||
tpslEntrustMonitorId = null;
|
||||
const modal = document.getElementById('tpsl-modal');
|
||||
if(modal) modal.classList.remove('open');
|
||||
}
|
||||
function submitTpslEntrust(){
|
||||
const orderId = tpslEntrustMonitorId;
|
||||
if(!orderId) return;
|
||||
const mode = (document.getElementById('tpsl-modal-mode')||{}).value || 'price';
|
||||
const body = { sltp_mode: mode };
|
||||
if(mode === 'pct'){
|
||||
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
||||
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
||||
if(rejectManualOrderRr(calcClientRrFromPct(body.sl_pct, body.tp_pct))) return;
|
||||
}else{
|
||||
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
||||
body.tp = (document.getElementById('tpsl-modal-tp')||{}).value;
|
||||
}
|
||||
const card = document.getElementById(`order-row-${orderId}`);
|
||||
const direction = (card && card.getAttribute('data-direction')) || 'long';
|
||||
const post = ()=>{
|
||||
fetch(`/api/order/${orderId}/place_tpsl`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
|
||||
.then(r=>r.json()).then(data=>{
|
||||
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
||||
alert(data.msg || '已提交');
|
||||
closeTpslEntrustModal();
|
||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
||||
refreshPriceSnapshotConditional();
|
||||
}).catch(()=>alert('委托请求失败'));
|
||||
};
|
||||
if(mode === 'pct'){ post(); return; }
|
||||
const sl = Number(body.sl), tp = Number(body.tp);
|
||||
let entry = sl;
|
||||
const sym = (card && card.getAttribute('data-symbol')) || '';
|
||||
if(!sym){ if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return; post(); return; }
|
||||
fetch(`/api/order_defaults?symbol=${encodeURIComponent(sym)}&direction=${encodeURIComponent(direction)}`)
|
||||
.then(r=>r.json()).then(data=>{
|
||||
const px = data.last_price || data.price;
|
||||
if(px) entry = Number(px);
|
||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return;
|
||||
post();
|
||||
}).catch(()=>alert('无法校验盈亏比'));
|
||||
}
|
||||
function cancelExchangeTpsl(orderId, role){
|
||||
const label = role === 'sl' ? '止损' : '止盈';
|
||||
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
|
||||
fetch(`/api/order/${orderId}/cancel_tpsl`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ role }) })
|
||||
.then(r=>r.json()).then(data=>{
|
||||
if(!data.ok){ alert(data.msg || '撤单失败'); return; }
|
||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
||||
else refreshPriceSnapshotConditional();
|
||||
}).catch(()=>alert('撤单请求失败'));
|
||||
}
|
||||
function allowManualOrderSubmit(form){
|
||||
form.dataset.rrOk = "1";
|
||||
if(window.FormSubmitGuard){
|
||||
if(FormSubmitGuard.isLocked(form)){
|
||||
FormSubmitGuard.setSubmitLabel(form, "开仓提交中…");
|
||||
} else {
|
||||
FormSubmitGuard.lock(form, "开仓提交中…");
|
||||
}
|
||||
}
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function formatSigned(v, digits=4){
|
||||
if(v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-";
|
||||
@@ -1491,6 +1710,13 @@ function formatSigned(v, digits=4){
|
||||
const sign = n > 0 ? "+" : "";
|
||||
return `${sign}${n.toFixed(digits)}`;
|
||||
}
|
||||
function formatRrRatio(rr){
|
||||
if(rr === null || typeof rr === "undefined") return "-:1";
|
||||
const n = Number(rr);
|
||||
if(Number.isNaN(n)) return "-:1";
|
||||
const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2)));
|
||||
return `${body}:1`;
|
||||
}
|
||||
|
||||
function paintPriceTrend(el, key, value){
|
||||
if(!el) return;
|
||||
@@ -1512,71 +1738,71 @@ function paintBreakevenBadge(orderId, secured){
|
||||
wrap.style.display = secured ? "inline-flex" : "none";
|
||||
}
|
||||
|
||||
function refreshPriceSnapshot(){
|
||||
function refreshPriceSnapshotConditional(){
|
||||
const page = document.body.getAttribute("data-page") || "";
|
||||
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
|
||||
const updatedEl = document.getElementById("price-last-updated");
|
||||
if(data.updated_at && updatedEl){
|
||||
updatedEl.innerText = data.updated_at;
|
||||
}
|
||||
(data.key_prices || []).forEach(k=>{
|
||||
const pEl = document.getElementById(`key-price-${k.id}`);
|
||||
if(pEl){
|
||||
pEl.innerText = Number(k.price).toFixed(6);
|
||||
paintPriceTrend(pEl, `k-${k.id}`, Number(k.price));
|
||||
}
|
||||
const upEl = document.getElementById(`key-up-diff-${k.id}`);
|
||||
if(upEl){
|
||||
upEl.innerText = `${formatSigned(k.upper_diff, 4)} (${formatSigned(k.upper_pct, 2)}%)`;
|
||||
}
|
||||
const lowEl = document.getElementById(`key-low-diff-${k.id}`);
|
||||
if(lowEl){
|
||||
lowEl.innerText = `${formatSigned(k.lower_diff, 4)} (${formatSigned(k.lower_pct, 2)}%)`;
|
||||
}
|
||||
const gateEl = document.getElementById(`key-gate-${k.id}`);
|
||||
if(gateEl){
|
||||
gateEl.innerText = k.gate_summary || "-";
|
||||
gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f";
|
||||
}
|
||||
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
|
||||
if(gateMetricEl){
|
||||
gateMetricEl.innerText = k.gate_metrics || "";
|
||||
}
|
||||
});
|
||||
(data.order_prices || []).forEach(o=>{
|
||||
const pEl = document.getElementById(`order-price-${o.id}`);
|
||||
if(pEl){
|
||||
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
|
||||
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
|
||||
const decimals = hasMark ? 8 : 6;
|
||||
pEl.innerText = px.toFixed(decimals);
|
||||
paintPriceTrend(pEl, `o-${o.id}`, px);
|
||||
}
|
||||
const exM = document.getElementById(`order-ex-margin-${o.id}`);
|
||||
if(exM){
|
||||
const mv = o.exchange_initial_margin;
|
||||
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
|
||||
if(!Number.isNaN(mn)){
|
||||
exM.innerText = `${mn.toFixed(2)}U`;
|
||||
} else {
|
||||
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
|
||||
exM.innerText = (prc === 0) ? "无仓数据" : "-";
|
||||
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
|
||||
if(page === "key_monitor"){
|
||||
(data.key_prices || []).forEach(k=>{
|
||||
const pEl = document.getElementById(`key-price-${k.id}`);
|
||||
if(pEl){
|
||||
pEl.innerText = Number(k.price).toFixed(6);
|
||||
paintPriceTrend(pEl, `k-${k.id}`, Number(k.price));
|
||||
}
|
||||
}
|
||||
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
||||
if(pnlEl){
|
||||
pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
|
||||
pnlEl.classList.remove("pnl-profit","pnl-loss","pnl-neutral");
|
||||
const fp = Number(o.float_pnl);
|
||||
if(fp > 0) pnlEl.classList.add("pnl-profit");
|
||||
else if(fp < 0) pnlEl.classList.add("pnl-loss");
|
||||
else pnlEl.classList.add("pnl-neutral");
|
||||
}
|
||||
const rrEl = document.getElementById(`order-rr-${o.id}`);
|
||||
if(rrEl){
|
||||
rrEl.innerText = (typeof o.rr_ratio !== "undefined" && o.rr_ratio !== null) ? `${Number(o.rr_ratio).toFixed(2)}:1` : "-";
|
||||
}
|
||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||
});
|
||||
const upEl = document.getElementById(`key-up-diff-${k.id}`);
|
||||
if(upEl) upEl.innerText = `${formatSigned(k.upper_diff, 4)} (${formatSigned(k.upper_pct, 2)}%)`;
|
||||
const lowEl = document.getElementById(`key-low-diff-${k.id}`);
|
||||
if(lowEl) lowEl.innerText = `${formatSigned(k.lower_diff, 4)} (${formatSigned(k.lower_pct, 2)}%)`;
|
||||
const gateEl = document.getElementById(`key-gate-${k.id}`);
|
||||
if(gateEl){
|
||||
gateEl.innerText = k.gate_summary || "-";
|
||||
gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f";
|
||||
}
|
||||
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
|
||||
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
|
||||
});
|
||||
}
|
||||
if(page === "trade"){
|
||||
(data.order_prices || []).forEach(o=>{
|
||||
const pEl = document.getElementById(`order-price-${o.id}`);
|
||||
if(pEl){
|
||||
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
|
||||
let disp = "";
|
||||
if(hasMark && o.exchange_mark_price_display) disp = o.exchange_mark_price_display;
|
||||
else if(o.price_display) disp = o.price_display;
|
||||
else {
|
||||
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
|
||||
disp = Number.isFinite(px) ? px.toFixed(hasMark ? 8 : 6) : "-";
|
||||
}
|
||||
pEl.innerText = disp;
|
||||
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
|
||||
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
|
||||
}
|
||||
const exM = document.getElementById(`order-ex-margin-${o.id}`);
|
||||
if(exM){
|
||||
const mv = o.exchange_initial_margin;
|
||||
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
|
||||
if(!Number.isNaN(mn)) exM.innerText = `${mn.toFixed(2)}U`;
|
||||
else {
|
||||
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
|
||||
exM.innerText = (prc === 0) ? "无仓数据" : "-";
|
||||
}
|
||||
}
|
||||
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
||||
if(pnlEl){
|
||||
pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
|
||||
pnlEl.classList.remove("price-up","price-down","price-flat","pnl-profit","pnl-loss","pnl-neutral");
|
||||
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
|
||||
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
|
||||
else pnlEl.classList.add("price-flat");
|
||||
}
|
||||
const rrEl = document.getElementById(`order-rr-${o.id}`);
|
||||
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
|
||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||
});
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
@@ -1620,11 +1846,12 @@ function refreshAccountSnapshot(){
|
||||
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
|
||||
latestAvailableUsdt = Number(data.available_trading_usdt);
|
||||
}
|
||||
const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00)";
|
||||
const canTradeText = data.can_trade ? "可开仓" : `不可开仓(持仓 ${data.active_count||0}/${data.max_active_positions||{{ max_active_positions }}}、有趋势回调计划,或未到北京时间 {{ reset_hour }}:00)`;
|
||||
const tip = document.getElementById("order-rule-tip");
|
||||
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : "";
|
||||
const minRr = data.manual_min_planned_rr != null ? data.manual_min_planned_rr : MANUAL_MIN_PLANNED_RR;
|
||||
if(tip){
|
||||
tip.innerText = `规则:单仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${canTradeText}${avail}`;
|
||||
tip.innerText = `规则:最大同时持仓 ${data.max_active_positions || {{ max_active_positions }}}(当前 active ${data.active_count||0});与「趋势回调」计划互斥;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${canTradeText}${avail};人工开仓盈亏比不得低于 ${minRr}:1`;
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
@@ -1676,10 +1903,65 @@ if(_journalFormEl){
|
||||
if(_jErSel) _jErSel.addEventListener("change", syncJournalEntryReasonOtherUi);
|
||||
syncJournalEntryReasonOtherUi();
|
||||
}
|
||||
const addOrderForm = document.getElementById("add-order-form");
|
||||
if(addOrderForm){
|
||||
addOrderForm.addEventListener("submit", function(ev){
|
||||
if(addOrderForm.dataset.rrOk === "1"){
|
||||
addOrderForm.dataset.rrOk = "0";
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
||||
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
||||
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
||||
if(mode === "pct"){
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||
const rr = calcClientRrFromPct(
|
||||
(document.getElementById("order-sl-pct")||{}).value,
|
||||
(document.getElementById("order-tp-pct")||{}).value
|
||||
);
|
||||
if(rejectManualOrderRr(rr)){
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||
return;
|
||||
}
|
||||
allowManualOrderSubmit(addOrderForm);
|
||||
return;
|
||||
}
|
||||
const sl = Number((document.getElementById("order-sl")||{}).value);
|
||||
const tp = Number((document.getElementById("order-tp")||{}).value);
|
||||
let entry = sl;
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||
if(!symbol){
|
||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||
return;
|
||||
}
|
||||
allowManualOrderSubmit(addOrderForm);
|
||||
return;
|
||||
}
|
||||
fetch(`/api/order_defaults?symbol=${encodeURIComponent(symbol)}&direction=${encodeURIComponent(direction)}`)
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
const px = data.last_price || data.price;
|
||||
if(px) entry = Number(px);
|
||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))){
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||
return;
|
||||
}
|
||||
allowManualOrderSubmit(addOrderForm);
|
||||
})
|
||||
.catch(()=>{
|
||||
alert("无法校验盈亏比,请稍后重试");
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refreshOrderDefaults();
|
||||
refreshPriceSnapshot();
|
||||
refreshPriceSnapshotConditional();
|
||||
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
|
||||
setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }});
|
||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user