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 (
|
from order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
apply_order_price_display_fields,
|
||||||
enrich_order_display_fields,
|
enrich_order_display_fields,
|
||||||
|
stop_is_profit_protecting,
|
||||||
tpsl_slot_trigger_price,
|
tpsl_slot_trigger_price,
|
||||||
|
tpsl_update_passes_rr_gate,
|
||||||
)
|
)
|
||||||
from journal_chart_lib import (
|
from journal_chart_lib import (
|
||||||
JOURNAL_CHART_DEFAULT_LIMIT,
|
JOURNAL_CHART_DEFAULT_LIMIT,
|
||||||
@@ -3357,7 +3359,9 @@ def cancel_gate_tpsl_slot(exchange_symbol, slot):
|
|||||||
exchange.cancel_order(str(oid), exchange_symbol, params)
|
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()
|
sltp_mode = (sltp_mode or "price").strip().lower()
|
||||||
if sltp_mode == "pct":
|
if sltp_mode == "pct":
|
||||||
sl_pct = float(data.get("sl_pct") or 0)
|
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:
|
else:
|
||||||
stop_loss = float(data.get("sl") or data.get("stop_loss") or 0)
|
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)
|
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:
|
if stop_loss <= 0 and fallback_sl is not None:
|
||||||
raise ValueError("止盈止损价格须大于 0")
|
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
|
return stop_loss, take_profit
|
||||||
|
|
||||||
|
|
||||||
@@ -6044,20 +6054,32 @@ def api_order_place_tpsl(order_id):
|
|||||||
return jsonify({"ok": False, "msg": "获取交易所实时价格失败"}), 400
|
return jsonify({"ok": False, "msg": "获取交易所实时价格失败"}), 400
|
||||||
try:
|
try:
|
||||||
sltp_mode = (data.get("sltp_mode") or "price").strip().lower()
|
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:
|
except Exception as e:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "msg": str(e)}), 400
|
return jsonify({"ok": False, "msg": str(e)}), 400
|
||||||
planned_rr = calc_rr_ratio(direction, live_price, stop_loss, take_profit)
|
entry_price = float(row["trigger_price"] or live_price or 0)
|
||||||
if planned_rr is None or planned_rr < MANUAL_MIN_PLANNED_RR:
|
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()
|
conn.close()
|
||||||
rr_txt = f"{planned_rr:.4f}" if planned_rr is not None else "无法计算"
|
return jsonify({"ok": False, "msg": rr_err}), 400
|
||||||
return jsonify(
|
planned_rr = calc_rr_ratio(direction, entry_price, stop_loss, take_profit)
|
||||||
{
|
if stop_is_profit_protecting(direction, entry_price, stop_loss):
|
||||||
"ok": False,
|
planned_rr = None
|
||||||
"msg": f"计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1",
|
|
||||||
}
|
|
||||||
), 400
|
|
||||||
try:
|
try:
|
||||||
replace_active_monitor_tpsl_on_exchange(row, stop_loss, take_profit)
|
replace_active_monitor_tpsl_on_exchange(row, stop_loss, take_profit)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1623,6 +1623,22 @@ function rejectManualOrderRr(rr){
|
|||||||
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单。`);
|
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单。`);
|
||||||
return true;
|
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;
|
let tpslEntrustMonitorId = null;
|
||||||
function formatExTpslLine(role, slot){
|
function formatExTpslLine(role, slot){
|
||||||
const label = role === 'sl' ? '止损' : '止盈';
|
const label = role === 'sl' ? '止损' : '止盈';
|
||||||
@@ -1720,16 +1736,28 @@ function submitTpslEntrust(){
|
|||||||
}).catch(()=>alert('委托请求失败'));
|
}).catch(()=>alert('委托请求失败'));
|
||||||
};
|
};
|
||||||
if(mode === 'pct'){ post(); return; }
|
if(mode === 'pct'){ post(); return; }
|
||||||
const sl = Number(body.sl), tp = Number(body.tp);
|
let sl = Number(body.sl);
|
||||||
let entry = 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')) || '';
|
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)}`)
|
fetch(`/api/order_defaults?symbol=${encodeURIComponent(sym)}&direction=${encodeURIComponent(direction)}`)
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>r.json()).then(data=>{
|
||||||
const px = data.last_price || data.price;
|
const px = data.last_price || data.price;
|
||||||
if(px) entry = Number(px);
|
finishRr(px ? Number(px) : null);
|
||||||
if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return;
|
|
||||||
post();
|
|
||||||
}).catch(()=>alert('无法校验盈亏比'));
|
}).catch(()=>alert('无法校验盈亏比'));
|
||||||
}
|
}
|
||||||
function relinkOrphanPosition(symbol, direction){
|
function relinkOrphanPosition(symbol, direction){
|
||||||
|
|||||||
@@ -46,6 +46,39 @@ def tpsl_slot_trigger_price(slot: Any) -> Optional[float]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def stop_is_profit_protecting(direction: str, entry_price: Any, stop_loss: Any) -> bool:
|
||||||
|
"""
|
||||||
|
止损是否已在盈利侧(保本/锁盈),不再适用「开仓盈亏比」风控。
|
||||||
|
做空:止损 < 成交价;做多:止损 > 成交价。
|
||||||
|
"""
|
||||||
|
entry = _positive_float(entry_price)
|
||||||
|
sl = _positive_float(stop_loss)
|
||||||
|
if entry is None or sl is None:
|
||||||
|
return False
|
||||||
|
d = (direction or "long").strip().lower()
|
||||||
|
if d == "short":
|
||||||
|
return sl < entry
|
||||||
|
return sl > entry
|
||||||
|
|
||||||
|
|
||||||
|
def tpsl_update_passes_rr_gate(
|
||||||
|
direction: str,
|
||||||
|
entry_price: Any,
|
||||||
|
stop_loss: Any,
|
||||||
|
take_profit: Any,
|
||||||
|
min_rr: float,
|
||||||
|
calc_rr_ratio_fn: Callable[..., Optional[float]],
|
||||||
|
) -> tuple[bool, Optional[str]]:
|
||||||
|
"""持仓委托改价:盈利侧止损跳过最低盈亏比;否则按开仓价几何校验。"""
|
||||||
|
if stop_is_profit_protecting(direction, entry_price, stop_loss):
|
||||||
|
return True, None
|
||||||
|
rr = calc_rr_ratio_fn(direction or "long", entry_price, stop_loss, take_profit)
|
||||||
|
if rr is not None and rr >= float(min_rr):
|
||||||
|
return True, None
|
||||||
|
rr_txt = f"{rr:.4f}" if rr is not None else "无法计算"
|
||||||
|
return False, f"计划盈亏比 {rr_txt}:1 低于最低要求 {min_rr}:1(盈利侧保本止损不受此限)"
|
||||||
|
|
||||||
|
|
||||||
def is_sl_breakeven_secured(direction: str, entry_price: Any, exchange_sl_price: Any) -> bool:
|
def is_sl_breakeven_secured(direction: str, entry_price: Any, exchange_sl_price: Any) -> bool:
|
||||||
"""
|
"""
|
||||||
交易所当前止损相对开仓成交价是否已保本。
|
交易所当前止损相对开仓成交价是否已保本。
|
||||||
|
|||||||
Reference in New Issue
Block a user