fix(gate-bot): allow profit-side stop loss on TP/SL entrust
Skip min planned RR when stop is on the winning side of entry; validate entrust against open price and fall back to plan take-profit when omitted.
This commit is contained in:
@@ -54,7 +54,9 @@ from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit
|
||||
from order_monitor_display_lib import (
|
||||
apply_order_price_display_fields,
|
||||
enrich_order_display_fields,
|
||||
stop_is_profit_protecting,
|
||||
tpsl_slot_trigger_price,
|
||||
tpsl_update_passes_rr_gate,
|
||||
)
|
||||
from journal_chart_lib import (
|
||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||
@@ -3357,7 +3359,9 @@ def cancel_gate_tpsl_slot(exchange_symbol, slot):
|
||||
exchange.cancel_order(str(oid), exchange_symbol, params)
|
||||
|
||||
|
||||
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
||||
def _resolve_tpsl_prices_for_manual(
|
||||
direction, live_price, sltp_mode, data, *, fallback_sl=None, fallback_tp=None
|
||||
):
|
||||
sltp_mode = (sltp_mode or "price").strip().lower()
|
||||
if sltp_mode == "pct":
|
||||
sl_pct = float(data.get("sl_pct") or 0)
|
||||
@@ -3376,8 +3380,14 @@ def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
||||
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")
|
||||
if stop_loss <= 0 and fallback_sl is not None:
|
||||
stop_loss = float(fallback_sl)
|
||||
if take_profit <= 0 and fallback_tp is not None:
|
||||
take_profit = float(fallback_tp)
|
||||
if stop_loss <= 0:
|
||||
raise ValueError("止损价格须大于 0")
|
||||
if take_profit <= 0:
|
||||
raise ValueError("请填写止盈价格,或保留原计划止盈")
|
||||
return stop_loss, take_profit
|
||||
|
||||
|
||||
@@ -6044,20 +6054,32 @@ def api_order_place_tpsl(order_id):
|
||||
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)
|
||||
stop_loss, take_profit = _resolve_tpsl_prices_for_manual(
|
||||
direction,
|
||||
live_price,
|
||||
sltp_mode,
|
||||
data,
|
||||
fallback_sl=row["stop_loss"],
|
||||
fallback_tp=row["take_profit"],
|
||||
)
|
||||
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:
|
||||
entry_price = float(row["trigger_price"] or live_price or 0)
|
||||
rr_ok, rr_err = tpsl_update_passes_rr_gate(
|
||||
direction,
|
||||
entry_price,
|
||||
stop_loss,
|
||||
take_profit,
|
||||
MANUAL_MIN_PLANNED_RR,
|
||||
calc_rr_ratio,
|
||||
)
|
||||
if not rr_ok:
|
||||
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
|
||||
return jsonify({"ok": False, "msg": rr_err}), 400
|
||||
planned_rr = calc_rr_ratio(direction, entry_price, stop_loss, take_profit)
|
||||
if stop_is_profit_protecting(direction, entry_price, stop_loss):
|
||||
planned_rr = None
|
||||
try:
|
||||
replace_active_monitor_tpsl_on_exchange(row, stop_loss, take_profit)
|
||||
except Exception as e:
|
||||
|
||||
@@ -1623,6 +1623,22 @@ function rejectManualOrderRr(rr){
|
||||
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单。`);
|
||||
return true;
|
||||
}
|
||||
function stopIsProfitProtecting(direction, entry, sl){
|
||||
const e = Number(entry), s = Number(sl);
|
||||
if(!Number.isFinite(e) || !Number.isFinite(s)) return false;
|
||||
return (direction || "long") === "short" ? s < e : s > e;
|
||||
}
|
||||
function entryPriceFromOrderCard(card){
|
||||
if(!card) return null;
|
||||
const raw = card.getAttribute("data-entry");
|
||||
if(raw === null || raw === "") return null;
|
||||
const e = Number(raw);
|
||||
return Number.isFinite(e) ? e : null;
|
||||
}
|
||||
function tpslRrCheckPasses(direction, entry, sl, tp){
|
||||
if(stopIsProfitProtecting(direction, entry, sl)) return true;
|
||||
return !rejectManualOrderRr(calcClientRr(direction, entry, sl, tp));
|
||||
}
|
||||
let tpslEntrustMonitorId = null;
|
||||
function formatExTpslLine(role, slot){
|
||||
const label = role === 'sl' ? '止损' : '止盈';
|
||||
@@ -1720,16 +1736,28 @@ function submitTpslEntrust(){
|
||||
}).catch(()=>alert('委托请求失败'));
|
||||
};
|
||||
if(mode === 'pct'){ post(); return; }
|
||||
const sl = Number(body.sl), tp = Number(body.tp);
|
||||
let entry = sl;
|
||||
let sl = Number(body.sl);
|
||||
let tp = Number(body.tp);
|
||||
const planTp = card && card.getAttribute('data-plan-tp');
|
||||
if((!Number.isFinite(tp) || tp <= 0) && planTp){
|
||||
const pt = Number(planTp);
|
||||
if(Number.isFinite(pt) && pt > 0) tp = pt;
|
||||
}
|
||||
if(!Number.isFinite(sl) || sl <= 0){ alert('请填写止损价格'); return; }
|
||||
if(!Number.isFinite(tp) || tp <= 0){ alert('请填写止盈价格,或保留原计划止盈'); return; }
|
||||
let entry = entryPriceFromOrderCard(card);
|
||||
const sym = (card && card.getAttribute('data-symbol')) || '';
|
||||
if(!sym){ if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return; post(); return; }
|
||||
const finishRr = (entryPx)=>{
|
||||
const e = entryPx != null ? entryPx : entry;
|
||||
if(!tpslRrCheckPasses(direction, e, sl, tp)) return;
|
||||
post();
|
||||
};
|
||||
if(entry != null){ finishRr(entry); return; }
|
||||
if(!sym){ finishRr(sl); 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();
|
||||
finishRr(px ? Number(px) : null);
|
||||
}).catch(()=>alert('无法校验盈亏比'));
|
||||
}
|
||||
function relinkOrphanPosition(symbol, direction){
|
||||
|
||||
Reference in New Issue
Block a user