移除一键保本bug
This commit is contained in:
+46
-76
@@ -73,7 +73,6 @@ from key_sl_tp_lib import (
|
||||
sl_tp_mode_label,
|
||||
sl_tp_plan_summary_text,
|
||||
)
|
||||
from order_manual_breakeven_lib import apply_order_manual_breakeven
|
||||
from key_monitor_lib import (
|
||||
KEY_DIRECTION_WATCH,
|
||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||
@@ -190,6 +189,8 @@ GATE_TPSL_PRICE_TYPE = int(os.getenv("GATE_TPSL_PRICE_TYPE", "0"))
|
||||
if GATE_TPSL_PRICE_TYPE < 0 or GATE_TPSL_PRICE_TYPE > 2:
|
||||
GATE_TPSL_PRICE_TYPE = 0
|
||||
GATE_TPSL_USE_POSITION_ORDER = os.getenv("GATE_TPSL_USE_POSITION_ORDER", "true").lower() in ("1", "true", "yes")
|
||||
# 仓位类触发单相对 mark/last 的最小间距(%),避免 Gate 1026 AUTO_TRIGGER_PRICE_*_LAST
|
||||
GATE_TPSL_LAST_PRICE_GAP_PCT = float(os.getenv("GATE_TPSL_LAST_PRICE_GAP_PCT", "0.05"))
|
||||
# 页面展示的交易所名称(多实例/多环境时可按需区分)
|
||||
EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "Gate.io").strip() or "Gate.io"
|
||||
_GATE_DEFAULT_MARGIN_MODE = "cross" if GATE_TD_MODE in ("cross", "cross_margin") else "isolated"
|
||||
@@ -2871,6 +2872,40 @@ def _gate_contracts_amount_for_tpsl(order, fallback_amount):
|
||||
return float(fallback_amount)
|
||||
|
||||
|
||||
def _gate_clamp_tpsl_to_last_price(exchange_symbol, direction, stop_loss, take_profit, *, sl_only=False):
|
||||
"""
|
||||
Gate price_orders 规则:空仓止损/多仓止盈 trigger>last;空仓止盈/多仓止损 trigger<last。
|
||||
计划价可能已穿过现价时,按最小间距自动微调并返回说明。
|
||||
"""
|
||||
ensure_markets_loaded()
|
||||
last = get_price(exchange_symbol)
|
||||
if last is None or float(last) <= 0:
|
||||
return float(stop_loss), float(take_profit), None
|
||||
last = float(last)
|
||||
sl = float(stop_loss)
|
||||
tp = float(take_profit)
|
||||
gap = max(0.0, float(GATE_TPSL_LAST_PRICE_GAP_PCT)) / 100.0
|
||||
if gap <= 0:
|
||||
gap = 0.0005
|
||||
notes = []
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "short":
|
||||
if sl <= last:
|
||||
sl = float(exchange.price_to_precision(exchange_symbol, last * (1 + gap)))
|
||||
notes.append(f"止损触发价须高于现价 {last},已调整为 {sl}")
|
||||
if not sl_only and tp >= last:
|
||||
tp = float(exchange.price_to_precision(exchange_symbol, last * (1 - gap)))
|
||||
notes.append(f"止盈触发价须低于现价 {last},已调整为 {tp}")
|
||||
else:
|
||||
if sl >= last:
|
||||
sl = float(exchange.price_to_precision(exchange_symbol, last * (1 - gap)))
|
||||
notes.append(f"止损触发价须低于现价 {last},已调整为 {sl}")
|
||||
if not sl_only and tp <= last:
|
||||
tp = float(exchange.price_to_precision(exchange_symbol, last * (1 + gap)))
|
||||
notes.append(f"止盈触发价须高于现价 {last},已调整为 {tp}")
|
||||
return sl, tp, (";".join(notes) if notes else None)
|
||||
|
||||
|
||||
def _gate_place_tp_sl_orders_legacy_conditional(exchange_symbol, direction, contracts_amount, stop_loss, take_profit):
|
||||
"""ccxt 市价减仓条件单(两张单分别带 stopLossPrice / takeProfitPrice),与官方仓位类触发单等价逻辑不同路径。"""
|
||||
ensure_markets_loaded()
|
||||
@@ -2900,6 +2935,9 @@ def _gate_place_tp_sl_orders_position_price_orders(exchange_symbol, direction, s
|
||||
order_type=close-long-position / close-short-position,单向全平 close+size=0;双向需 auto_size。
|
||||
与 App 内展示的「条件委托」一致,平仓后仍需 cancel_gate_swap_trigger_orders 避免残留。
|
||||
"""
|
||||
stop_loss, take_profit, _ = _gate_clamp_tpsl_to_last_price(
|
||||
exchange_symbol, direction, stop_loss, take_profit
|
||||
)
|
||||
ensure_markets_loaded()
|
||||
market = exchange.market(exchange_symbol)
|
||||
if not market.get("swap"):
|
||||
@@ -2972,6 +3010,9 @@ def _gate_place_tp_sl_orders(exchange_symbol, direction, contracts_amount, stop_
|
||||
|
||||
def _gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss):
|
||||
"""Gate 永续:仅挂仓位类止损触发单(趋势回调用)。"""
|
||||
stop_loss, _, _ = _gate_clamp_tpsl_to_last_price(
|
||||
exchange_symbol, direction, stop_loss, stop_loss, sl_only=True
|
||||
)
|
||||
ensure_markets_loaded()
|
||||
market = exchange.market(exchange_symbol)
|
||||
if not market.get("swap"):
|
||||
@@ -3298,6 +3339,9 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
||||
raise RuntimeError(reason or "实盘未就绪")
|
||||
ex_sym = resolve_monitor_exchange_symbol(order_row)
|
||||
direction = order_row["direction"]
|
||||
sl, tp, adjust_note = _gate_clamp_tpsl_to_last_price(
|
||||
ex_sym, direction, float(stop_loss), float(take_profit)
|
||||
)
|
||||
cancel_gate_swap_trigger_orders(ex_sym)
|
||||
contracts = get_live_position_contracts(ex_sym, direction)
|
||||
if contracts is None or float(contracts) <= 0:
|
||||
@@ -3310,7 +3354,7 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
||||
amt = 0
|
||||
if amt <= 0:
|
||||
raise ValueError("无法确定平仓数量")
|
||||
_gate_place_tp_sl_orders(ex_sym, direction, amt, float(stop_loss), float(take_profit))
|
||||
_gate_place_tp_sl_orders(ex_sym, direction, amt, sl, tp)
|
||||
|
||||
|
||||
def extract_trade_price_from_order(order):
|
||||
@@ -6208,80 +6252,6 @@ def api_order_cancel_tpsl(order_id):
|
||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/order/<int:order_id>/manual_breakeven", methods=["POST"])
|
||||
@login_required
|
||||
def api_order_manual_breakeven(order_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
try:
|
||||
offset_pct = float(
|
||||
data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2"))
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400
|
||||
if offset_pct < 0 or offset_pct > 10:
|
||||
return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400
|
||||
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
|
||||
ok, err, new_sl = apply_order_manual_breakeven(
|
||||
row,
|
||||
offset_pct,
|
||||
calc_stop_fn=calc_trend_manual_breakeven_stop,
|
||||
round_price_fn=round_price_to_exchange,
|
||||
resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
|
||||
get_position_fn=get_live_position_contracts,
|
||||
replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange,
|
||||
)
|
||||
if not ok:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "msg": err or "保本失败"}), 400
|
||||
conn.execute(
|
||||
"UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?",
|
||||
(new_sl, new_sl, order_id),
|
||||
)
|
||||
conn.commit()
|
||||
sym = row["symbol"]
|
||||
direction = row["direction"]
|
||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
||||
take_profit = float(row["take_profit"] or 0)
|
||||
slots = fetch_exchange_tpsl_slots(
|
||||
ex_sym, direction, plan_sl=new_sl, plan_tp=take_profit
|
||||
)
|
||||
conn.close()
|
||||
try:
|
||||
entry = float(row["trigger_price"] or 0)
|
||||
send_wechat_msg(
|
||||
"\n".join(
|
||||
[
|
||||
f"# ✅ {sym} 一键保本",
|
||||
f"**账户:{_wechat_account_label()}**",
|
||||
f"- 方向:{'做多' if direction == 'long' else '做空'}",
|
||||
f"- 成交价:{format_price_for_symbol(sym, entry)}",
|
||||
f"- 偏移:{offset_pct}%(相对成交价)",
|
||||
f"- 新止损:{format_price_for_symbol(sym, new_sl)}",
|
||||
"- 交易所:已更新止盈止损",
|
||||
]
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"msg": "已一键保本",
|
||||
"stop_loss": new_sl,
|
||||
"stop_loss_display": format_price_for_symbol(sym, new_sl),
|
||||
"breakeven_armed": True,
|
||||
"exchange_tpsl": slots,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
||||
@login_required
|
||||
def api_order_place_tpsl(order_id):
|
||||
|
||||
Reference in New Issue
Block a user