feat: add fixed RR stop-loss mode for manual live orders on all instances
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -458,6 +458,7 @@
|
||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||
</select>
|
||||
<select id="sltp-mode" name="sltp_mode">
|
||||
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
|
||||
<option value="price">止盈止损:价格模式</option>
|
||||
<option value="pct">止盈止损:百分比模式</option>
|
||||
</select>
|
||||
@@ -476,7 +477,9 @@
|
||||
</label>
|
||||
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
||||
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
|
||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
||||
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
|
||||
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
|
||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
|
||||
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||
<button type="submit">{{ open_position_button_label }}</button>
|
||||
@@ -1685,6 +1688,49 @@ setTimeout(() => {
|
||||
|
||||
|
||||
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
|
||||
const MANUAL_FIXED_RR_DEFAULT = 1.5;
|
||||
const FIXED_RR_LS_KEY = "manualFixedRr";
|
||||
function loadFixedRrPref(){
|
||||
try{
|
||||
const raw = localStorage.getItem(FIXED_RR_LS_KEY);
|
||||
const el = document.getElementById("order-fixed-rr");
|
||||
if(!el || raw == null || raw === "") return;
|
||||
const v = Number(raw);
|
||||
if(Number.isFinite(v) && v > 0) el.value = raw;
|
||||
}catch(_){}
|
||||
}
|
||||
function saveFixedRrPref(){
|
||||
try{
|
||||
const el = document.getElementById("order-fixed-rr");
|
||||
if(el && el.value) localStorage.setItem(FIXED_RR_LS_KEY, el.value);
|
||||
}catch(_){}
|
||||
}
|
||||
function calcTpFromFixedRr(direction, entry, sl, rr){
|
||||
const e = Number(entry), s = Number(sl), r = Number(rr);
|
||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(r) || r <= 0) return null;
|
||||
if(direction === "short"){
|
||||
if(s <= e) return null;
|
||||
return e - (s - e) * r;
|
||||
}
|
||||
if(s >= e) return null;
|
||||
return e + (e - s) * r;
|
||||
}
|
||||
function refreshOrderTpPreview(entryPx){
|
||||
const mode = (document.getElementById("sltp-mode")||{}).value || "fixed_rr";
|
||||
const preview = document.getElementById("order-tp-preview");
|
||||
if(!preview) return;
|
||||
if(mode !== "fixed_rr"){
|
||||
preview.style.display = "none";
|
||||
return;
|
||||
}
|
||||
preview.style.display = "";
|
||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
||||
const sl = Number((document.getElementById("order-sl")||{}).value);
|
||||
const rr = Number((document.getElementById("order-fixed-rr")||{}).value) || MANUAL_FIXED_RR_DEFAULT;
|
||||
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
||||
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
||||
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
||||
}
|
||||
function calcClientRr(direction, entry, sl, tp){
|
||||
const e = Number(entry), s = Number(sl), t = Number(tp);
|
||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
||||
@@ -1760,15 +1806,11 @@ function submitTpslEntrust(){
|
||||
if(mode === 'pct'){
|
||||
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
||||
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
||||
if(rejectManualOrderRr(calcClientRrFromPct(body.sl_pct, body.tp_pct))) return;
|
||||
}else{
|
||||
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
||||
body.tp = (document.getElementById('tpsl-modal-tp')||{}).value;
|
||||
}
|
||||
const card = document.getElementById(`order-row-${orderId}`);
|
||||
const direction = (card && card.getAttribute('data-direction')) || 'long';
|
||||
const post = ()=>{
|
||||
fetch(`/api/order/${orderId}/place_tpsl`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
|
||||
fetch(`/api/order/${orderId}/place_tpsl`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) })
|
||||
.then(r=>r.json()).then(data=>{
|
||||
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
||||
alert(data.msg || '已提交');
|
||||
@@ -1776,19 +1818,6 @@ function submitTpslEntrust(){
|
||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
||||
refreshPriceSnapshotConditional();
|
||||
}).catch(()=>alert('委托请求失败'));
|
||||
};
|
||||
if(mode === 'pct'){ post(); return; }
|
||||
const sl = Number(body.sl), tp = Number(body.tp);
|
||||
let entry = sl;
|
||||
const sym = (card && card.getAttribute('data-symbol')) || '';
|
||||
if(!sym){ if(rejectManualOrderRr(calcClientRr(direction, entry, sl, tp))) return; post(); 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();
|
||||
}).catch(()=>alert('无法校验盈亏比'));
|
||||
}
|
||||
function cancelExchangeTpsl(orderId, role){
|
||||
const label = role === 'sl' ? '止损' : '止盈';
|
||||
@@ -1951,6 +1980,8 @@ function refreshOrderDefaults(){
|
||||
marginEl.value = m;
|
||||
}
|
||||
}
|
||||
const px = data.last_price || data.price;
|
||||
if(px) refreshOrderTpPreview(px);
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
@@ -1992,26 +2023,37 @@ if(fullMarginEl){
|
||||
|
||||
const sltpModeEl = document.getElementById("sltp-mode");
|
||||
function toggleSltpMode(){
|
||||
const mode = sltpModeEl ? sltpModeEl.value : "price";
|
||||
const mode = sltpModeEl ? sltpModeEl.value : "fixed_rr";
|
||||
const slEl = document.getElementById("order-sl");
|
||||
const tpEl = document.getElementById("order-tp");
|
||||
const fixedRrEl = document.getElementById("order-fixed-rr");
|
||||
const slPctEl = document.getElementById("order-sl-pct");
|
||||
const tpPctEl = document.getElementById("order-tp-pct");
|
||||
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
||||
const pct = mode === "pct";
|
||||
const fixed = mode === "fixed_rr";
|
||||
slEl.style.display = pct ? "none" : "";
|
||||
tpEl.style.display = pct ? "none" : "";
|
||||
tpEl.style.display = (pct || fixed) ? "none" : "";
|
||||
if(fixedRrEl) fixedRrEl.style.display = fixed ? "" : "none";
|
||||
slEl.required = !pct;
|
||||
tpEl.required = !pct;
|
||||
tpEl.required = !pct && !fixed;
|
||||
if(fixedRrEl) fixedRrEl.required = fixed;
|
||||
slPctEl.style.display = pct ? "" : "none";
|
||||
tpPctEl.style.display = pct ? "" : "none";
|
||||
slPctEl.required = pct;
|
||||
tpPctEl.required = pct;
|
||||
refreshOrderTpPreview();
|
||||
}
|
||||
if(sltpModeEl){
|
||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||
loadFixedRrPref();
|
||||
toggleSltpMode();
|
||||
}
|
||||
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
||||
const el = document.getElementById(id);
|
||||
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
||||
if(el) el.addEventListener("change", function(){ refreshOrderTpPreview(); });
|
||||
});
|
||||
|
||||
refreshAccountSnapshot();
|
||||
const _journalFormEl = document.getElementById("journal-form");
|
||||
@@ -2034,8 +2076,23 @@ if(addOrderForm){
|
||||
ev.preventDefault();
|
||||
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
||||
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
||||
const mode = (document.getElementById("sltp-mode")||{}).value || "fixed_rr";
|
||||
const symbol = ((document.getElementById("order-symbol")||{}).value || "").trim();
|
||||
if(mode === "fixed_rr"){
|
||||
saveFixedRrPref();
|
||||
const rr = Number((document.getElementById("order-fixed-rr")||{}).value);
|
||||
if(!Number.isFinite(rr) || rr <= 0){
|
||||
alert("请填写正数盈亏比");
|
||||
return;
|
||||
}
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||
if(rejectManualOrderRr(rr)){
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.unlock(addOrderForm);
|
||||
return;
|
||||
}
|
||||
allowManualOrderSubmit(addOrderForm);
|
||||
return;
|
||||
}
|
||||
if(mode === "pct"){
|
||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||
const rr = calcClientRrFromPct(
|
||||
|
||||
Reference in New Issue
Block a user