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:
@@ -237,6 +237,7 @@ ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
||||
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
||||
DAILY_OPEN_ALERT_THRESHOLD = int(os.getenv("DAILY_OPEN_ALERT_THRESHOLD", "5"))
|
||||
RISK_PERCENT = float(os.getenv("RISK_PERCENT", "2"))
|
||||
MANUAL_MIN_PLANNED_RR = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
|
||||
BREAKEVEN_RR_TRIGGER = float(os.getenv("BREAKEVEN_RR_TRIGGER", "1.0"))
|
||||
BREAKEVEN_OFFSET_PCT = float(os.getenv("BREAKEVEN_OFFSET_PCT", "0.02"))
|
||||
BREAKEVEN_STEP_R = float(os.getenv("BREAKEVEN_STEP_R", "1.0"))
|
||||
@@ -3344,6 +3345,41 @@ def fetch_exchange_tpsl_slots(exchange_symbol, direction, plan_sl=None, plan_tp=
|
||||
return slots
|
||||
|
||||
|
||||
def cancel_gate_tpsl_slot(exchange_symbol, slot):
|
||||
if not slot or not exchange_symbol:
|
||||
return
|
||||
ensure_markets_loaded()
|
||||
oid = slot.get("order_id")
|
||||
if not oid:
|
||||
return
|
||||
params = _gate_swap_trigger_order_params()
|
||||
exchange.cancel_order(str(oid), exchange_symbol, params)
|
||||
|
||||
|
||||
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 cancel_all_open_orders_for_symbol(exchange_symbol):
|
||||
"""策略结束时:尽量撤掉该合约下条件单与普通挂单。"""
|
||||
cancel_gate_swap_trigger_orders(exchange_symbol)
|
||||
@@ -5537,6 +5573,7 @@ def render_main_page(page="trade"):
|
||||
price_refresh_seconds=PRICE_REFRESH_SECONDS,
|
||||
active_count=active_count,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
|
||||
can_trade=can_trade,
|
||||
trend_plans=trend_plans,
|
||||
preview_snapshots=preview_snapshots,
|
||||
@@ -5642,8 +5679,15 @@ def api_account_snapshot():
|
||||
current_capital = round(trading_capital, 2) if trading_capital is not None else round(local_current_capital, 2)
|
||||
recommended_capital = round(get_recommended_capital(current_capital), 2)
|
||||
active_count = get_active_position_count(conn)
|
||||
trend_active = conn.execute(
|
||||
"SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
|
||||
).fetchone()[0]
|
||||
conn.close()
|
||||
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
|
||||
can_trade = (
|
||||
trading_day_reset_allows_new_open(now)
|
||||
and active_count < MAX_ACTIVE_POSITIONS
|
||||
and int(trend_active or 0) == 0
|
||||
)
|
||||
available_trading_usdt = get_available_trading_usdt()
|
||||
return jsonify({
|
||||
"funding_usdt": funding_usdt,
|
||||
@@ -5653,6 +5697,7 @@ def api_account_snapshot():
|
||||
"active_count": active_count,
|
||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||
"can_trade": can_trade,
|
||||
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
|
||||
"trading_day": trading_day
|
||||
})
|
||||
|
||||
@@ -5821,6 +5866,100 @@ 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"], plan_sl=row["stop_loss"], plan_tp=row["take_profit"]
|
||||
)
|
||||
slot = slots.get(role)
|
||||
if not slot:
|
||||
return jsonify({"ok": False, "msg": f"交易所未找到{'止损' if role == 'sl' else '止盈'}委托"}), 404
|
||||
try:
|
||||
cancel_gate_tpsl_slot(ex_sym, slot)
|
||||
slots = fetch_exchange_tpsl_slots(
|
||||
ex_sym, row["direction"], plan_sl=row["stop_loss"], plan_tp=row["take_profit"]
|
||||
)
|
||||
return jsonify({"ok": True, "msg": "已撤单", "exchange_tpsl": slots})
|
||||
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, plan_sl=stop_loss, plan_tp=take_profit)
|
||||
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():
|
||||
@@ -6212,12 +6351,11 @@ def add_order():
|
||||
conn.close()
|
||||
flash("价格参数必须大于0")
|
||||
return redirect("/")
|
||||
_min_rr = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
|
||||
planned_rr_manual = calc_rr_ratio(direction, live_price, stop_loss, take_profit)
|
||||
if planned_rr_manual is None or planned_rr_manual < _min_rr:
|
||||
if planned_rr_manual is None or planned_rr_manual < MANUAL_MIN_PLANNED_RR:
|
||||
conn.close()
|
||||
rr_txt = f"{planned_rr_manual:.4f}" if planned_rr_manual is not None else "无法计算"
|
||||
flash(f"风控拒绝下单:计划盈亏比 {rr_txt}:1 低于最低要求 {_min_rr}:1")
|
||||
flash(f"风控拒绝下单:计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1")
|
||||
return redirect("/")
|
||||
risk_fraction = calc_risk_fraction(direction, live_price, stop_loss)
|
||||
if risk_fraction is None:
|
||||
@@ -7684,7 +7822,7 @@ def _hub_meta_bundle():
|
||||
"trend_manual_breakeven_offset_pct": TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT,
|
||||
"trend_pullback_dca_legs": TREND_PULLBACK_DCA_LEGS,
|
||||
"trend_preview_max_drift_pct": TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT,
|
||||
"manual_min_planned_rr": float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4")),
|
||||
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
|
||||
"max_active_positions": max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", "1"))),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user