增加前端委托
This commit is contained in:
@@ -2692,6 +2692,189 @@ def cancel_binance_futures_open_orders(exchange_symbol):
|
||||
pass
|
||||
|
||||
|
||||
def _binance_list_raw_open_orders(exchange_symbol):
|
||||
"""普通挂单 + Algo 条件单(止盈/止损)。"""
|
||||
ensure_markets_loaded()
|
||||
market = exchange.market(exchange_symbol)
|
||||
contract_id = market.get("id")
|
||||
out = []
|
||||
try:
|
||||
for o in exchange.fetch_open_orders(exchange_symbol) or []:
|
||||
item = dict(o)
|
||||
item["_channel"] = "regular"
|
||||
out.append(item)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if contract_id and hasattr(exchange, "fapiPrivateGetOpenAlgoOrders"):
|
||||
raw = exchange.fapiPrivateGetOpenAlgoOrders({"symbol": contract_id})
|
||||
items = raw if isinstance(raw, list) else (raw.get("orders") or raw.get("data") or [])
|
||||
for info in items or []:
|
||||
if not isinstance(info, dict):
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"id": info.get("algoId") or info.get("orderId"),
|
||||
"info": info,
|
||||
"_channel": "algo",
|
||||
"type": info.get("orderType") or info.get("type"),
|
||||
"positionSide": info.get("positionSide"),
|
||||
"stopPrice": info.get("triggerPrice") or info.get("stopPrice"),
|
||||
"amount": info.get("quantity") or info.get("origQty"),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def _binance_order_type_str(order):
|
||||
info = order.get("info") or {}
|
||||
if isinstance(info, dict):
|
||||
for key in ("orderType", "type", "origType", "algoType"):
|
||||
val = info.get(key)
|
||||
if val:
|
||||
return str(val).upper()
|
||||
return str(order.get("type") or "").upper()
|
||||
|
||||
|
||||
def _binance_order_matches_direction(order, direction):
|
||||
if BINANCE_POSITION_MODE != "hedge":
|
||||
return True
|
||||
info = order.get("info") or {}
|
||||
ps = str(order.get("positionSide") or info.get("positionSide") or "").upper()
|
||||
want = "LONG" if direction == "long" else "SHORT"
|
||||
if ps and ps not in ("", "BOTH") and ps != want:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _binance_order_trigger_price(order):
|
||||
for key in ("stopPrice", "triggerPrice", "activatePrice"):
|
||||
try:
|
||||
v = float(order.get(key) or 0)
|
||||
if v > 0:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
info = order.get("info") or {}
|
||||
if isinstance(info, dict):
|
||||
for key in ("triggerPrice", "stopPrice", "activatePrice"):
|
||||
try:
|
||||
v = float(info.get(key) or 0)
|
||||
if v > 0:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _binance_tpsl_role_from_order(order):
|
||||
typ = _binance_order_type_str(order)
|
||||
if "TAKE_PROFIT" in typ:
|
||||
return "tp"
|
||||
if "STOP" in typ:
|
||||
return "sl"
|
||||
return None
|
||||
|
||||
|
||||
def _binance_tpsl_slot_from_order(order, exchange_symbol):
|
||||
trig = _binance_order_trigger_price(order)
|
||||
try:
|
||||
amt = float(order.get("amount") or order.get("remaining") or 0)
|
||||
except Exception:
|
||||
amt = None
|
||||
if amt is not None and amt <= 0:
|
||||
amt = None
|
||||
channel = order.get("_channel") or "regular"
|
||||
oid = order.get("id")
|
||||
if oid is None and isinstance(order.get("info"), dict):
|
||||
oid = order["info"].get("algoId") or order["info"].get("orderId")
|
||||
disp = format_price_for_symbol(exchange_symbol, trig) if trig else "-"
|
||||
return {
|
||||
"order_id": str(oid) if oid is not None else "",
|
||||
"channel": channel,
|
||||
"trigger_price": trig,
|
||||
"trigger_display": disp,
|
||||
"amount": amt,
|
||||
"type": _binance_order_type_str(order),
|
||||
}
|
||||
|
||||
|
||||
def fetch_exchange_tpsl_slots(exchange_symbol, direction):
|
||||
"""返回 { sl: slot|None, tp: slot|None },供页面展示与单笔撤单。"""
|
||||
slots = {"sl": None, "tp": None}
|
||||
if not exchange_symbol:
|
||||
return slots
|
||||
ok, _ = ensure_exchange_live_ready()
|
||||
if not ok:
|
||||
return slots
|
||||
try:
|
||||
for order in _binance_list_raw_open_orders(exchange_symbol):
|
||||
if not _binance_order_matches_direction(order, direction):
|
||||
continue
|
||||
role = _binance_tpsl_role_from_order(order)
|
||||
if role not in ("sl", "tp") or slots[role] is not None:
|
||||
continue
|
||||
slots[role] = _binance_tpsl_slot_from_order(order, exchange_symbol)
|
||||
except Exception:
|
||||
pass
|
||||
return slots
|
||||
|
||||
|
||||
def cancel_binance_tpsl_slot(exchange_symbol, slot):
|
||||
if not slot or not exchange_symbol:
|
||||
return
|
||||
ensure_markets_loaded()
|
||||
market = exchange.market(exchange_symbol)
|
||||
contract_id = market.get("id")
|
||||
oid = slot.get("order_id")
|
||||
if not oid:
|
||||
return
|
||||
if slot.get("channel") == "algo" and contract_id and hasattr(exchange, "fapiPrivateDeleteAlgoOrder"):
|
||||
exchange.fapiPrivateDeleteAlgoOrder({"symbol": contract_id, "algoId": oid})
|
||||
return
|
||||
exchange.cancel_order(str(oid), exchange_symbol)
|
||||
|
||||
|
||||
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
||||
sltp_mode = (sltp_mode or "price").strip().lower()
|
||||
if sltp_mode == "pct":
|
||||
sl_pct = float(data.get("sl_pct") or 0)
|
||||
tp_pct = float(data.get("tp_pct") or 0)
|
||||
if sl_pct <= 0 or tp_pct <= 0:
|
||||
raise ValueError("百分比止盈止损须为正数")
|
||||
sl_ratio = sl_pct / 100.0
|
||||
tp_ratio = tp_pct / 100.0
|
||||
entry = float(live_price)
|
||||
if direction == "short":
|
||||
stop_loss = entry * (1 + sl_ratio)
|
||||
take_profit = entry * (1 - tp_ratio)
|
||||
else:
|
||||
stop_loss = entry * (1 - sl_ratio)
|
||||
take_profit = entry * (1 + tp_ratio)
|
||||
else:
|
||||
stop_loss = float(data.get("sl") or data.get("stop_loss") or 0)
|
||||
take_profit = float(data.get("tp") or data.get("take_profit") or data.get("tgt") or 0)
|
||||
if stop_loss <= 0 or take_profit <= 0:
|
||||
raise ValueError("止盈止损价格须大于 0")
|
||||
return stop_loss, take_profit
|
||||
|
||||
|
||||
def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
||||
"""先撤该合约全部 TP/SL,再按新价重挂(与交易所 App 一致)。"""
|
||||
ok, reason = ensure_exchange_live_ready()
|
||||
if not ok:
|
||||
raise RuntimeError(reason or "实盘未就绪")
|
||||
ex_sym = resolve_monitor_exchange_symbol(order_row)
|
||||
direction = order_row["direction"]
|
||||
cancel_binance_futures_open_orders(ex_sym)
|
||||
pos_amt = get_live_position_contracts(ex_sym, direction)
|
||||
if pos_amt is None or float(pos_amt) <= 0:
|
||||
raise ValueError("交易所当前无该方向持仓,无法挂止盈止损")
|
||||
_binance_place_tp_sl_orders(ex_sym, direction, float(pos_amt), float(stop_loss), float(take_profit))
|
||||
|
||||
|
||||
def extract_trade_price_from_order(order):
|
||||
if not order:
|
||||
return None
|
||||
@@ -4514,6 +4697,13 @@ def api_price_snapshot():
|
||||
payload["float_pct"] = (
|
||||
round((payload["float_pnl"] / float(denom)) * 100, 2) if denom and float(denom) > 0 else pnl_pct
|
||||
)
|
||||
if exchange_private_api_configured():
|
||||
try:
|
||||
payload["exchange_tpsl"] = fetch_exchange_tpsl_slots(ex_sym, r["direction"])
|
||||
except Exception:
|
||||
payload["exchange_tpsl"] = {"sl": None, "tp": None}
|
||||
else:
|
||||
payload["exchange_tpsl"] = {"sl": None, "tp": None}
|
||||
order_prices.append(payload)
|
||||
|
||||
return jsonify({
|
||||
@@ -4524,6 +4714,95 @@ def api_price_snapshot():
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/order/<int:order_id>/cancel_tpsl", methods=["POST"])
|
||||
@login_required
|
||||
def api_order_cancel_tpsl(order_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
role = (data.get("role") or "").strip().lower()
|
||||
if role not in ("sl", "tp"):
|
||||
return jsonify({"ok": False, "msg": "role 须为 sl 或 tp"}), 400
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
||||
(order_id,),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
||||
ok, reason = ensure_exchange_live_ready()
|
||||
if not ok:
|
||||
return jsonify({"ok": False, "msg": reason}), 400
|
||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
||||
slots = fetch_exchange_tpsl_slots(ex_sym, row["direction"])
|
||||
slot = slots.get(role)
|
||||
if not slot:
|
||||
return jsonify({"ok": False, "msg": f"交易所未找到{'止损' if role == 'sl' else '止盈'}委托"}), 404
|
||||
try:
|
||||
cancel_binance_tpsl_slot(ex_sym, slot)
|
||||
return jsonify({"ok": True, "msg": "已撤单", "exchange_tpsl": fetch_exchange_tpsl_slots(ex_sym, row["direction"])})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
||||
@login_required
|
||||
def api_order_place_tpsl(order_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
||||
(order_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
||||
symbol = row["symbol"]
|
||||
direction = row["direction"]
|
||||
live_price = get_price(symbol)
|
||||
if live_price is None:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": "获取交易所实时价格失败"}), 400
|
||||
try:
|
||||
sltp_mode = (data.get("sltp_mode") or "price").strip().lower()
|
||||
stop_loss, take_profit = _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data)
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": str(e)}), 400
|
||||
planned_rr = calc_rr_ratio(direction, live_price, stop_loss, take_profit)
|
||||
if planned_rr is None or planned_rr < MANUAL_MIN_PLANNED_RR:
|
||||
conn.close()
|
||||
rr_txt = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算"
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"msg": f"计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1",
|
||||
}
|
||||
), 400
|
||||
try:
|
||||
replace_active_monitor_tpsl_on_exchange(row, stop_loss, take_profit)
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?",
|
||||
(stop_loss, take_profit, order_id),
|
||||
)
|
||||
conn.commit()
|
||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
||||
slots = fetch_exchange_tpsl_slots(ex_sym, direction)
|
||||
conn.close()
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"msg": "已先撤后挂止盈止损",
|
||||
"stop_loss": stop_loss,
|
||||
"take_profit": take_profit,
|
||||
"planned_rr": planned_rr,
|
||||
"exchange_tpsl": slots,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/symbol_liquidity_rank")
|
||||
@login_required
|
||||
def api_symbol_liquidity_rank():
|
||||
|
||||
@@ -129,8 +129,26 @@
|
||||
.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-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}
|
||||
.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}
|
||||
@@ -357,13 +375,22 @@
|
||||
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||
<div class="panel-scroll pos-list">
|
||||
{% for o in order %}
|
||||
<div class="pos-card" id="order-row-{{ o.id }}">
|
||||
<div class="pos-card" id="order-row-{{ o.id }}"
|
||||
data-monitor-id="{{ o.id }}"
|
||||
data-symbol="{{ o.symbol }}"
|
||||
data-direction="{{ o.direction }}"
|
||||
data-plan-sl="{{ o.stop_loss or '' }}"
|
||||
data-plan-tp="{{ o.take_profit or '' }}"
|
||||
data-entry="{{ o.trigger_price or '' }}">
|
||||
<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="pos-close-btn" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||
<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="pos-meta">
|
||||
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}</span>
|
||||
@@ -413,12 +440,49 @@
|
||||
<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="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>
|
||||
{% endif %}
|
||||
|
||||
@@ -607,7 +671,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if page == 'stats' %}
|
||||
@@ -1218,6 +1285,102 @@ function rejectManualOrderRr(rr){
|
||||
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 = card.getAttribute('data-plan-sl') || '';
|
||||
if(tpEl) tpEl.value = 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";
|
||||
form.submit();
|
||||
@@ -1325,6 +1488,7 @@ function refreshPriceSnapshot(){
|
||||
if(rrEl){
|
||||
rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||
}
|
||||
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
|
||||
});
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user