增加前端委托

This commit is contained in:
dekun
2026-05-17 13:51:53 +08:00
parent 1642f07dfc
commit 25ab1fa900
4 changed files with 911 additions and 5 deletions
+279
View File
@@ -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():