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:
@@ -85,6 +85,11 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
|
from manual_sltp_lib import (
|
||||||
|
normalize_open_sltp_mode,
|
||||||
|
resolve_entrust_sltp_prices,
|
||||||
|
resolve_open_sltp_prices,
|
||||||
|
)
|
||||||
from position_sizing_lib import (
|
from position_sizing_lib import (
|
||||||
OPEN_SOURCE_KEY_AUTO,
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
OPEN_SOURCE_MANUAL,
|
OPEN_SOURCE_MANUAL,
|
||||||
@@ -3539,27 +3544,7 @@ def cancel_binance_tpsl_slot(exchange_symbol, slot):
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
||||||
sltp_mode = (sltp_mode or "price").strip().lower()
|
return resolve_entrust_sltp_prices(direction, live_price, sltp_mode, data)
|
||||||
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 replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
||||||
@@ -6927,34 +6912,14 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("获取交易所实时价格失败,请稍后重试")
|
flash("获取交易所实时价格失败,请稍后重试")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
sltp_mode = (d.get("sltp_mode") or "price").strip().lower()
|
sltp_mode = normalize_open_sltp_mode(d.get("sltp_mode"))
|
||||||
if sltp_mode not in ("price", "pct"):
|
|
||||||
sltp_mode = "price"
|
|
||||||
if sltp_mode == "pct":
|
|
||||||
try:
|
try:
|
||||||
sl_pct = float(d.get("sl_pct") or 0)
|
stop_loss, take_profit = resolve_open_sltp_prices(
|
||||||
tp_pct = float(d.get("tp_pct") or 0)
|
direction, live_price, sltp_mode, d
|
||||||
if sl_pct <= 0 or tp_pct <= 0:
|
)
|
||||||
raise ValueError("pct")
|
except ValueError as e:
|
||||||
sl_ratio = sl_pct / 100.0
|
|
||||||
tp_ratio = tp_pct / 100.0
|
|
||||||
if direction == "short":
|
|
||||||
stop_loss = float(live_price) * (1 + sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 - tp_ratio)
|
|
||||||
else:
|
|
||||||
stop_loss = float(live_price) * (1 - sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 + tp_ratio)
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("百分比止盈止损参数错误,请填写正数百分比")
|
flash(str(e) or "止盈止损参数错误")
|
||||||
return redirect("/")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
stop_loss = float(d["sl"])
|
|
||||||
take_profit = float(d["tgt"])
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
|
||||||
flash("价格参数格式错误")
|
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
if stop_loss <= 0 or take_profit <= 0:
|
if stop_loss <= 0 or take_profit <= 0:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -474,6 +474,7 @@
|
|||||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="sltp-mode" name="sltp_mode">
|
<select id="sltp-mode" name="sltp_mode">
|
||||||
|
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
|
||||||
<option value="price">止盈止损:价格模式</option>
|
<option value="price">止盈止损:价格模式</option>
|
||||||
<option value="pct">止盈止损:百分比模式</option>
|
<option value="pct">止盈止损:百分比模式</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -492,7 +493,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
<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-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-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">
|
<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>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
@@ -1701,6 +1704,49 @@ setTimeout(() => {
|
|||||||
|
|
||||||
|
|
||||||
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
|
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){
|
function calcClientRr(direction, entry, sl, tp){
|
||||||
const e = Number(entry), s = Number(sl), t = Number(tp);
|
const e = Number(entry), s = Number(sl), t = Number(tp);
|
||||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
||||||
@@ -1776,14 +1822,10 @@ function submitTpslEntrust(){
|
|||||||
if(mode === 'pct'){
|
if(mode === 'pct'){
|
||||||
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
||||||
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
||||||
if(rejectManualOrderRr(calcClientRrFromPct(body.sl_pct, body.tp_pct))) return;
|
|
||||||
}else{
|
}else{
|
||||||
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
||||||
body.tp = (document.getElementById('tpsl-modal-tp')||{}).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=>{
|
.then(r=>r.json()).then(data=>{
|
||||||
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
||||||
@@ -1792,19 +1834,6 @@ function submitTpslEntrust(){
|
|||||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
||||||
refreshPriceSnapshotConditional();
|
refreshPriceSnapshotConditional();
|
||||||
}).catch(()=>alert('委托请求失败'));
|
}).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){
|
function cancelExchangeTpsl(orderId, role){
|
||||||
const label = role === 'sl' ? '止损' : '止盈';
|
const label = role === 'sl' ? '止损' : '止盈';
|
||||||
@@ -1967,6 +1996,8 @@ function refreshOrderDefaults(){
|
|||||||
marginEl.value = m;
|
marginEl.value = m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const px = data.last_price || data.price;
|
||||||
|
if(px) refreshOrderTpPreview(px);
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2008,26 +2039,37 @@ if(fullMarginEl){
|
|||||||
|
|
||||||
const sltpModeEl = document.getElementById("sltp-mode");
|
const sltpModeEl = document.getElementById("sltp-mode");
|
||||||
function toggleSltpMode(){
|
function toggleSltpMode(){
|
||||||
const mode = sltpModeEl ? sltpModeEl.value : "price";
|
const mode = sltpModeEl ? sltpModeEl.value : "fixed_rr";
|
||||||
const slEl = document.getElementById("order-sl");
|
const slEl = document.getElementById("order-sl");
|
||||||
const tpEl = document.getElementById("order-tp");
|
const tpEl = document.getElementById("order-tp");
|
||||||
|
const fixedRrEl = document.getElementById("order-fixed-rr");
|
||||||
const slPctEl = document.getElementById("order-sl-pct");
|
const slPctEl = document.getElementById("order-sl-pct");
|
||||||
const tpPctEl = document.getElementById("order-tp-pct");
|
const tpPctEl = document.getElementById("order-tp-pct");
|
||||||
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
||||||
const pct = mode === "pct";
|
const pct = mode === "pct";
|
||||||
|
const fixed = mode === "fixed_rr";
|
||||||
slEl.style.display = pct ? "none" : "";
|
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;
|
slEl.required = !pct;
|
||||||
tpEl.required = !pct;
|
tpEl.required = !pct && !fixed;
|
||||||
|
if(fixedRrEl) fixedRrEl.required = fixed;
|
||||||
slPctEl.style.display = pct ? "" : "none";
|
slPctEl.style.display = pct ? "" : "none";
|
||||||
tpPctEl.style.display = pct ? "" : "none";
|
tpPctEl.style.display = pct ? "" : "none";
|
||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
|
refreshOrderTpPreview();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
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();
|
refreshAccountSnapshot();
|
||||||
const _journalFormEl = document.getElementById("journal-form");
|
const _journalFormEl = document.getElementById("journal-form");
|
||||||
@@ -2050,8 +2092,23 @@ if(addOrderForm){
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
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();
|
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(mode === "pct"){
|
||||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
const rr = calcClientRrFromPct(
|
const rr = calcClientRrFromPct(
|
||||||
|
|||||||
+12
-47
@@ -86,6 +86,11 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
|
from manual_sltp_lib import (
|
||||||
|
normalize_open_sltp_mode,
|
||||||
|
resolve_entrust_sltp_prices,
|
||||||
|
resolve_open_sltp_prices,
|
||||||
|
)
|
||||||
from position_sizing_lib import (
|
from position_sizing_lib import (
|
||||||
OPEN_SOURCE_KEY_AUTO,
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
OPEN_SOURCE_MANUAL,
|
OPEN_SOURCE_MANUAL,
|
||||||
@@ -3293,27 +3298,7 @@ def cancel_gate_tpsl_slot(exchange_symbol, slot):
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
||||||
sltp_mode = (sltp_mode or "price").strip().lower()
|
return resolve_entrust_sltp_prices(direction, live_price, sltp_mode, data)
|
||||||
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 replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
||||||
@@ -6984,34 +6969,14 @@ def add_order():
|
|||||||
lp_r = round_price_to_exchange(exchange_symbol, live_price)
|
lp_r = round_price_to_exchange(exchange_symbol, live_price)
|
||||||
if lp_r is not None:
|
if lp_r is not None:
|
||||||
live_price = lp_r
|
live_price = lp_r
|
||||||
sltp_mode = (d.get("sltp_mode") or "price").strip().lower()
|
sltp_mode = normalize_open_sltp_mode(d.get("sltp_mode"))
|
||||||
if sltp_mode not in ("price", "pct"):
|
|
||||||
sltp_mode = "price"
|
|
||||||
if sltp_mode == "pct":
|
|
||||||
try:
|
try:
|
||||||
sl_pct = float(d.get("sl_pct") or 0)
|
stop_loss, take_profit = resolve_open_sltp_prices(
|
||||||
tp_pct = float(d.get("tp_pct") or 0)
|
direction, live_price, sltp_mode, d
|
||||||
if sl_pct <= 0 or tp_pct <= 0:
|
)
|
||||||
raise ValueError("pct")
|
except ValueError as e:
|
||||||
sl_ratio = sl_pct / 100.0
|
|
||||||
tp_ratio = tp_pct / 100.0
|
|
||||||
if direction == "short":
|
|
||||||
stop_loss = float(live_price) * (1 + sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 - tp_ratio)
|
|
||||||
else:
|
|
||||||
stop_loss = float(live_price) * (1 - sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 + tp_ratio)
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("百分比止盈止损参数错误,请填写正数百分比")
|
flash(str(e) or "止盈止损参数错误")
|
||||||
return redirect("/")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
stop_loss = float(d["sl"])
|
|
||||||
take_profit = float(d["tgt"])
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
|
||||||
flash("价格参数格式错误")
|
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
if stop_loss <= 0 or take_profit <= 0:
|
if stop_loss <= 0 or take_profit <= 0:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="sltp-mode" name="sltp_mode">
|
<select id="sltp-mode" name="sltp_mode">
|
||||||
|
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
|
||||||
<option value="price">止盈止损:价格模式</option>
|
<option value="price">止盈止损:价格模式</option>
|
||||||
<option value="pct">止盈止损:百分比模式</option>
|
<option value="pct">止盈止损:百分比模式</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -476,7 +477,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
<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-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-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">
|
<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>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
@@ -1685,6 +1688,49 @@ setTimeout(() => {
|
|||||||
|
|
||||||
|
|
||||||
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
|
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){
|
function calcClientRr(direction, entry, sl, tp){
|
||||||
const e = Number(entry), s = Number(sl), t = Number(tp);
|
const e = Number(entry), s = Number(sl), t = Number(tp);
|
||||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
||||||
@@ -1760,14 +1806,10 @@ function submitTpslEntrust(){
|
|||||||
if(mode === 'pct'){
|
if(mode === 'pct'){
|
||||||
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
||||||
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
||||||
if(rejectManualOrderRr(calcClientRrFromPct(body.sl_pct, body.tp_pct))) return;
|
|
||||||
}else{
|
}else{
|
||||||
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
||||||
body.tp = (document.getElementById('tpsl-modal-tp')||{}).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=>{
|
.then(r=>r.json()).then(data=>{
|
||||||
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
||||||
@@ -1776,19 +1818,6 @@ function submitTpslEntrust(){
|
|||||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
||||||
refreshPriceSnapshotConditional();
|
refreshPriceSnapshotConditional();
|
||||||
}).catch(()=>alert('委托请求失败'));
|
}).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){
|
function cancelExchangeTpsl(orderId, role){
|
||||||
const label = role === 'sl' ? '止损' : '止盈';
|
const label = role === 'sl' ? '止损' : '止盈';
|
||||||
@@ -1951,6 +1980,8 @@ function refreshOrderDefaults(){
|
|||||||
marginEl.value = m;
|
marginEl.value = m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const px = data.last_price || data.price;
|
||||||
|
if(px) refreshOrderTpPreview(px);
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1992,26 +2023,37 @@ if(fullMarginEl){
|
|||||||
|
|
||||||
const sltpModeEl = document.getElementById("sltp-mode");
|
const sltpModeEl = document.getElementById("sltp-mode");
|
||||||
function toggleSltpMode(){
|
function toggleSltpMode(){
|
||||||
const mode = sltpModeEl ? sltpModeEl.value : "price";
|
const mode = sltpModeEl ? sltpModeEl.value : "fixed_rr";
|
||||||
const slEl = document.getElementById("order-sl");
|
const slEl = document.getElementById("order-sl");
|
||||||
const tpEl = document.getElementById("order-tp");
|
const tpEl = document.getElementById("order-tp");
|
||||||
|
const fixedRrEl = document.getElementById("order-fixed-rr");
|
||||||
const slPctEl = document.getElementById("order-sl-pct");
|
const slPctEl = document.getElementById("order-sl-pct");
|
||||||
const tpPctEl = document.getElementById("order-tp-pct");
|
const tpPctEl = document.getElementById("order-tp-pct");
|
||||||
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
||||||
const pct = mode === "pct";
|
const pct = mode === "pct";
|
||||||
|
const fixed = mode === "fixed_rr";
|
||||||
slEl.style.display = pct ? "none" : "";
|
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;
|
slEl.required = !pct;
|
||||||
tpEl.required = !pct;
|
tpEl.required = !pct && !fixed;
|
||||||
|
if(fixedRrEl) fixedRrEl.required = fixed;
|
||||||
slPctEl.style.display = pct ? "" : "none";
|
slPctEl.style.display = pct ? "" : "none";
|
||||||
tpPctEl.style.display = pct ? "" : "none";
|
tpPctEl.style.display = pct ? "" : "none";
|
||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
|
refreshOrderTpPreview();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
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();
|
refreshAccountSnapshot();
|
||||||
const _journalFormEl = document.getElementById("journal-form");
|
const _journalFormEl = document.getElementById("journal-form");
|
||||||
@@ -2034,8 +2076,23 @@ if(addOrderForm){
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
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();
|
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(mode === "pct"){
|
||||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
const rr = calcClientRrFromPct(
|
const rr = calcClientRrFromPct(
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ from ai_review_lib import (
|
|||||||
collect_images_for_ai_review,
|
collect_images_for_ai_review,
|
||||||
journal_row_lines_for_ai,
|
journal_row_lines_for_ai,
|
||||||
)
|
)
|
||||||
|
from manual_sltp_lib import (
|
||||||
|
normalize_open_sltp_mode,
|
||||||
|
resolve_entrust_sltp_prices,
|
||||||
|
resolve_open_sltp_prices,
|
||||||
|
)
|
||||||
from position_sizing_lib import (
|
from position_sizing_lib import (
|
||||||
assert_open_source_allowed,
|
assert_open_source_allowed,
|
||||||
compute_full_margin_sizing,
|
compute_full_margin_sizing,
|
||||||
@@ -3351,33 +3356,14 @@ def cancel_gate_tpsl_slot(exchange_symbol, slot):
|
|||||||
def _resolve_tpsl_prices_for_manual(
|
def _resolve_tpsl_prices_for_manual(
|
||||||
direction, live_price, sltp_mode, data, *, fallback_sl=None, fallback_tp=None
|
direction, live_price, sltp_mode, data, *, fallback_sl=None, fallback_tp=None
|
||||||
):
|
):
|
||||||
sltp_mode = (sltp_mode or "price").strip().lower()
|
return resolve_entrust_sltp_prices(
|
||||||
if sltp_mode == "pct":
|
direction,
|
||||||
sl_pct = float(data.get("sl_pct") or 0)
|
live_price,
|
||||||
tp_pct = float(data.get("tp_pct") or 0)
|
sltp_mode,
|
||||||
if sl_pct <= 0 or tp_pct <= 0:
|
data,
|
||||||
raise ValueError("百分比止盈止损须为正数")
|
fallback_sl=fallback_sl,
|
||||||
sl_ratio = sl_pct / 100.0
|
fallback_tp=fallback_tp,
|
||||||
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 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
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_all_open_orders_for_symbol(exchange_symbol):
|
def cancel_all_open_orders_for_symbol(exchange_symbol):
|
||||||
@@ -6345,34 +6331,14 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("获取交易所实时价格失败,请稍后重试")
|
flash("获取交易所实时价格失败,请稍后重试")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
sltp_mode = (d.get("sltp_mode") or "price").strip().lower()
|
sltp_mode = normalize_open_sltp_mode(d.get("sltp_mode"))
|
||||||
if sltp_mode not in ("price", "pct"):
|
|
||||||
sltp_mode = "price"
|
|
||||||
if sltp_mode == "pct":
|
|
||||||
try:
|
try:
|
||||||
sl_pct = float(d.get("sl_pct") or 0)
|
stop_loss, take_profit = resolve_open_sltp_prices(
|
||||||
tp_pct = float(d.get("tp_pct") or 0)
|
direction, live_price, sltp_mode, d
|
||||||
if sl_pct <= 0 or tp_pct <= 0:
|
)
|
||||||
raise ValueError("pct")
|
except ValueError as e:
|
||||||
sl_ratio = sl_pct / 100.0
|
|
||||||
tp_ratio = tp_pct / 100.0
|
|
||||||
if direction == "short":
|
|
||||||
stop_loss = float(live_price) * (1 + sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 - tp_ratio)
|
|
||||||
else:
|
|
||||||
stop_loss = float(live_price) * (1 - sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 + tp_ratio)
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("百分比止盈止损参数错误,请填写正数百分比")
|
flash(str(e) or "止盈止损参数错误")
|
||||||
return redirect("/")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
stop_loss = float(d["sl"])
|
|
||||||
take_profit = float(d["tgt"])
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
|
||||||
flash("价格参数格式错误")
|
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
if stop_loss <= 0 or take_profit <= 0:
|
if stop_loss <= 0 or take_profit <= 0:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -399,6 +399,7 @@
|
|||||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="sltp-mode" name="sltp_mode">
|
<select id="sltp-mode" name="sltp_mode">
|
||||||
|
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
|
||||||
<option value="price">止盈止损:价格模式</option>
|
<option value="price">止盈止损:价格模式</option>
|
||||||
<option value="pct">止盈止损:百分比模式</option>
|
<option value="pct">止盈止损:百分比模式</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -417,7 +418,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
<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-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-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">
|
<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>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
@@ -1616,6 +1619,49 @@ setTimeout(() => {
|
|||||||
let latestAvailableUsdt = null;
|
let latestAvailableUsdt = null;
|
||||||
const lastPriceMap = {};
|
const lastPriceMap = {};
|
||||||
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
|
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){
|
function calcClientRr(direction, entry, sl, tp){
|
||||||
const e = Number(entry), s = Number(sl), t = Number(tp);
|
const e = Number(entry), s = Number(sl), t = Number(tp);
|
||||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
||||||
@@ -1724,14 +1770,10 @@ function submitTpslEntrust(){
|
|||||||
if(mode === 'pct'){
|
if(mode === 'pct'){
|
||||||
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
||||||
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
||||||
if(rejectManualOrderRr(calcClientRrFromPct(body.sl_pct, body.tp_pct))) return;
|
|
||||||
}else{
|
}else{
|
||||||
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
||||||
body.tp = (document.getElementById('tpsl-modal-tp')||{}).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=>{
|
.then(r=>r.json()).then(data=>{
|
||||||
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
||||||
@@ -1747,31 +1789,6 @@ function submitTpslEntrust(){
|
|||||||
});
|
});
|
||||||
refreshPriceSnapshotConditional();
|
refreshPriceSnapshotConditional();
|
||||||
}).catch(()=>alert('委托请求失败'));
|
}).catch(()=>alert('委托请求失败'));
|
||||||
};
|
|
||||||
if(mode === 'pct'){ post(); return; }
|
|
||||||
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')) || '';
|
|
||||||
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;
|
|
||||||
finishRr(px ? Number(px) : null);
|
|
||||||
}).catch(()=>alert('无法校验盈亏比'));
|
|
||||||
}
|
}
|
||||||
function relinkOrphanPosition(symbol, direction){
|
function relinkOrphanPosition(symbol, direction){
|
||||||
if(!confirm(`恢复 ${symbol} ${direction} 的本地监控?(接回最近一条已停止记录)`)) return;
|
if(!confirm(`恢复 ${symbol} ${direction} 的本地监控?(接回最近一条已停止记录)`)) return;
|
||||||
@@ -1937,6 +1954,8 @@ function refreshOrderDefaults(){
|
|||||||
marginEl.value = m;
|
marginEl.value = m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const px = data.last_price || data.price;
|
||||||
|
if(px) refreshOrderTpPreview(px);
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1979,26 +1998,37 @@ if(fullMarginEl){
|
|||||||
|
|
||||||
const sltpModeEl = document.getElementById("sltp-mode");
|
const sltpModeEl = document.getElementById("sltp-mode");
|
||||||
function toggleSltpMode(){
|
function toggleSltpMode(){
|
||||||
const mode = sltpModeEl ? sltpModeEl.value : "price";
|
const mode = sltpModeEl ? sltpModeEl.value : "fixed_rr";
|
||||||
const slEl = document.getElementById("order-sl");
|
const slEl = document.getElementById("order-sl");
|
||||||
const tpEl = document.getElementById("order-tp");
|
const tpEl = document.getElementById("order-tp");
|
||||||
|
const fixedRrEl = document.getElementById("order-fixed-rr");
|
||||||
const slPctEl = document.getElementById("order-sl-pct");
|
const slPctEl = document.getElementById("order-sl-pct");
|
||||||
const tpPctEl = document.getElementById("order-tp-pct");
|
const tpPctEl = document.getElementById("order-tp-pct");
|
||||||
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
||||||
const pct = mode === "pct";
|
const pct = mode === "pct";
|
||||||
|
const fixed = mode === "fixed_rr";
|
||||||
slEl.style.display = pct ? "none" : "";
|
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;
|
slEl.required = !pct;
|
||||||
tpEl.required = !pct;
|
tpEl.required = !pct && !fixed;
|
||||||
|
if(fixedRrEl) fixedRrEl.required = fixed;
|
||||||
slPctEl.style.display = pct ? "" : "none";
|
slPctEl.style.display = pct ? "" : "none";
|
||||||
tpPctEl.style.display = pct ? "" : "none";
|
tpPctEl.style.display = pct ? "" : "none";
|
||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
|
refreshOrderTpPreview();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
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();
|
refreshAccountSnapshot();
|
||||||
const _journalFormEl = document.getElementById("journal-form");
|
const _journalFormEl = document.getElementById("journal-form");
|
||||||
@@ -2020,8 +2050,23 @@ if(addOrderForm){
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
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();
|
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(mode === "pct"){
|
||||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
const rr = calcClientRrFromPct(
|
const rr = calcClientRrFromPct(
|
||||||
|
|||||||
+12
-47
@@ -86,6 +86,11 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
|
from manual_sltp_lib import (
|
||||||
|
normalize_open_sltp_mode,
|
||||||
|
resolve_entrust_sltp_prices,
|
||||||
|
resolve_open_sltp_prices,
|
||||||
|
)
|
||||||
from position_sizing_lib import (
|
from position_sizing_lib import (
|
||||||
OPEN_SOURCE_KEY_AUTO,
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
OPEN_SOURCE_MANUAL,
|
OPEN_SOURCE_MANUAL,
|
||||||
@@ -2803,27 +2808,7 @@ def parse_ccxt_position_metrics(position, order_leverage=None):
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
|
||||||
sltp_mode = (sltp_mode or "price").strip().lower()
|
return resolve_entrust_sltp_prices(direction, live_price, sltp_mode, data)
|
||||||
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 _okx_tpsl_slot_build(exchange_symbol, order_id, trigger_price, order_type=""):
|
def _okx_tpsl_slot_build(exchange_symbol, order_id, trigger_price, order_type=""):
|
||||||
@@ -6648,34 +6633,14 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("获取交易所实时价格失败,请稍后重试")
|
flash("获取交易所实时价格失败,请稍后重试")
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
sltp_mode = (d.get("sltp_mode") or "price").strip().lower()
|
sltp_mode = normalize_open_sltp_mode(d.get("sltp_mode"))
|
||||||
if sltp_mode not in ("price", "pct"):
|
|
||||||
sltp_mode = "price"
|
|
||||||
if sltp_mode == "pct":
|
|
||||||
try:
|
try:
|
||||||
sl_pct = float(d.get("sl_pct") or 0)
|
stop_loss, take_profit = resolve_open_sltp_prices(
|
||||||
tp_pct = float(d.get("tp_pct") or 0)
|
direction, live_price, sltp_mode, d
|
||||||
if sl_pct <= 0 or tp_pct <= 0:
|
)
|
||||||
raise ValueError("pct")
|
except ValueError as e:
|
||||||
sl_ratio = sl_pct / 100.0
|
|
||||||
tp_ratio = tp_pct / 100.0
|
|
||||||
if direction == "short":
|
|
||||||
stop_loss = float(live_price) * (1 + sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 - tp_ratio)
|
|
||||||
else:
|
|
||||||
stop_loss = float(live_price) * (1 - sl_ratio)
|
|
||||||
take_profit = float(live_price) * (1 + tp_ratio)
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("百分比止盈止损参数错误,请填写正数百分比")
|
flash(str(e) or "止盈止损参数错误")
|
||||||
return redirect("/trade")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
stop_loss = float(d["sl"])
|
|
||||||
take_profit = float(d["tgt"])
|
|
||||||
except Exception:
|
|
||||||
conn.close()
|
|
||||||
flash("价格参数格式错误")
|
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
if stop_loss <= 0 or take_profit <= 0:
|
if stop_loss <= 0 or take_profit <= 0:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -483,6 +483,7 @@
|
|||||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="sltp-mode" name="sltp_mode">
|
<select id="sltp-mode" name="sltp_mode">
|
||||||
|
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
|
||||||
<option value="price">止盈止损:价格模式</option>
|
<option value="price">止盈止损:价格模式</option>
|
||||||
<option value="pct">止盈止损:百分比模式</option>
|
<option value="pct">止盈止损:百分比模式</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -501,7 +502,9 @@
|
|||||||
</label>
|
</label>
|
||||||
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
<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-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-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">
|
<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>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
@@ -1711,6 +1714,49 @@ setTimeout(() => {
|
|||||||
|
|
||||||
|
|
||||||
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
|
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){
|
function calcClientRr(direction, entry, sl, tp){
|
||||||
const e = Number(entry), s = Number(sl), t = Number(tp);
|
const e = Number(entry), s = Number(sl), t = Number(tp);
|
||||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
||||||
@@ -1786,14 +1832,10 @@ function submitTpslEntrust(){
|
|||||||
if(mode === 'pct'){
|
if(mode === 'pct'){
|
||||||
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
body.sl_pct = Number((document.getElementById('tpsl-modal-sl-pct')||{}).value);
|
||||||
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
body.tp_pct = Number((document.getElementById('tpsl-modal-tp-pct')||{}).value);
|
||||||
if(rejectManualOrderRr(calcClientRrFromPct(body.sl_pct, body.tp_pct))) return;
|
|
||||||
}else{
|
}else{
|
||||||
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
body.sl = (document.getElementById('tpsl-modal-sl')||{}).value;
|
||||||
body.tp = (document.getElementById('tpsl-modal-tp')||{}).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=>{
|
.then(r=>r.json()).then(data=>{
|
||||||
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
if(!data.ok){ alert(data.msg || '委托失败'); return; }
|
||||||
@@ -1802,19 +1844,6 @@ function submitTpslEntrust(){
|
|||||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
||||||
refreshPriceSnapshotConditional();
|
refreshPriceSnapshotConditional();
|
||||||
}).catch(()=>alert('委托请求失败'));
|
}).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){
|
function cancelExchangeTpsl(orderId, role){
|
||||||
const label = role === 'sl' ? '止损' : '止盈';
|
const label = role === 'sl' ? '止损' : '止盈';
|
||||||
@@ -1977,6 +2006,8 @@ function refreshOrderDefaults(){
|
|||||||
marginEl.value = m;
|
marginEl.value = m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const px = data.last_price || data.price;
|
||||||
|
if(px) refreshOrderTpPreview(px);
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2050,26 +2081,37 @@ if(fullMarginEl){
|
|||||||
|
|
||||||
const sltpModeEl = document.getElementById("sltp-mode");
|
const sltpModeEl = document.getElementById("sltp-mode");
|
||||||
function toggleSltpMode(){
|
function toggleSltpMode(){
|
||||||
const mode = sltpModeEl ? sltpModeEl.value : "price";
|
const mode = sltpModeEl ? sltpModeEl.value : "fixed_rr";
|
||||||
const slEl = document.getElementById("order-sl");
|
const slEl = document.getElementById("order-sl");
|
||||||
const tpEl = document.getElementById("order-tp");
|
const tpEl = document.getElementById("order-tp");
|
||||||
|
const fixedRrEl = document.getElementById("order-fixed-rr");
|
||||||
const slPctEl = document.getElementById("order-sl-pct");
|
const slPctEl = document.getElementById("order-sl-pct");
|
||||||
const tpPctEl = document.getElementById("order-tp-pct");
|
const tpPctEl = document.getElementById("order-tp-pct");
|
||||||
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
if(!slEl || !tpEl || !slPctEl || !tpPctEl){ return; }
|
||||||
const pct = mode === "pct";
|
const pct = mode === "pct";
|
||||||
|
const fixed = mode === "fixed_rr";
|
||||||
slEl.style.display = pct ? "none" : "";
|
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;
|
slEl.required = !pct;
|
||||||
tpEl.required = !pct;
|
tpEl.required = !pct && !fixed;
|
||||||
|
if(fixedRrEl) fixedRrEl.required = fixed;
|
||||||
slPctEl.style.display = pct ? "" : "none";
|
slPctEl.style.display = pct ? "" : "none";
|
||||||
tpPctEl.style.display = pct ? "" : "none";
|
tpPctEl.style.display = pct ? "" : "none";
|
||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
|
refreshOrderTpPreview();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
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();
|
refreshAccountSnapshot();
|
||||||
const _journalFormEl = document.getElementById("journal-form");
|
const _journalFormEl = document.getElementById("journal-form");
|
||||||
@@ -2092,8 +2134,23 @@ if(addOrderForm){
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(addOrderForm)) return;
|
||||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
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();
|
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(mode === "pct"){
|
||||||
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
if(window.FormSubmitGuard) FormSubmitGuard.lock(addOrderForm, "校验盈亏比…");
|
||||||
const rr = calcClientRrFromPct(
|
const rr = calcClientRrFromPct(
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""实盘人工下单:止盈止损模式(价格 / 百分比 / 固定盈亏比)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
|
MANUAL_FIXED_RR_DEFAULT = 1.5
|
||||||
|
|
||||||
|
SLTP_MODE_PRICE = "price"
|
||||||
|
SLTP_MODE_PCT = "pct"
|
||||||
|
SLTP_MODE_FIXED_RR = "fixed_rr"
|
||||||
|
|
||||||
|
OPEN_SLTP_MODES = frozenset({SLTP_MODE_PRICE, SLTP_MODE_PCT, SLTP_MODE_FIXED_RR})
|
||||||
|
ENTRUST_SLTP_MODES = frozenset({SLTP_MODE_PRICE, SLTP_MODE_PCT})
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_open_sltp_mode(raw: Optional[str]) -> str:
|
||||||
|
mode = (raw or SLTP_MODE_FIXED_RR).strip().lower()
|
||||||
|
if mode in OPEN_SLTP_MODES:
|
||||||
|
return mode
|
||||||
|
return SLTP_MODE_PRICE
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_entrust_sltp_mode(raw: Optional[str]) -> str:
|
||||||
|
mode = (raw or SLTP_MODE_PRICE).strip().lower()
|
||||||
|
if mode in ENTRUST_SLTP_MODES:
|
||||||
|
return mode
|
||||||
|
return SLTP_MODE_PRICE
|
||||||
|
|
||||||
|
|
||||||
|
def parse_fixed_rr(raw: Any, *, default: float = MANUAL_FIXED_RR_DEFAULT) -> float:
|
||||||
|
try:
|
||||||
|
v = float(raw)
|
||||||
|
if v > 0:
|
||||||
|
return v
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return float(default)
|
||||||
|
|
||||||
|
|
||||||
|
def calc_tp_from_fixed_rr(
|
||||||
|
direction: str,
|
||||||
|
entry_price: float,
|
||||||
|
stop_loss: float,
|
||||||
|
rr_ratio: float,
|
||||||
|
) -> float:
|
||||||
|
entry = float(entry_price)
|
||||||
|
sl = float(stop_loss)
|
||||||
|
rr = float(rr_ratio)
|
||||||
|
if entry <= 0 or sl <= 0 or rr <= 0:
|
||||||
|
raise ValueError("固定盈亏比参数无效")
|
||||||
|
side = (direction or "long").strip().lower()
|
||||||
|
if side == "short":
|
||||||
|
risk = sl - entry
|
||||||
|
if risk <= 0:
|
||||||
|
raise ValueError("止损方向不合法:做空时止损须高于入场价")
|
||||||
|
return entry - risk * rr
|
||||||
|
risk = entry - sl
|
||||||
|
if risk <= 0:
|
||||||
|
raise ValueError("止损方向不合法:做多时止损须低于入场价")
|
||||||
|
return entry + risk * rr
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_pct_sltp(direction: str, live_price: float, data: dict[str, Any]) -> Tuple[float, float]:
|
||||||
|
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 or "long").strip().lower() == "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)
|
||||||
|
return stop_loss, take_profit
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_price_sltp(
|
||||||
|
data: dict[str, Any],
|
||||||
|
*,
|
||||||
|
fallback_sl: Optional[float] = None,
|
||||||
|
fallback_tp: Optional[float] = None,
|
||||||
|
require_tp: bool = True,
|
||||||
|
) -> Tuple[float, float]:
|
||||||
|
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 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 require_tp else "请填写止损价格")
|
||||||
|
if require_tp and take_profit <= 0:
|
||||||
|
raise ValueError("止盈止损价格须大于 0" if fallback_tp is None else "请填写止盈价格,或保留原计划止盈")
|
||||||
|
return stop_loss, take_profit
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_open_sltp_prices(
|
||||||
|
direction: str,
|
||||||
|
live_price: float,
|
||||||
|
sltp_mode: Optional[str],
|
||||||
|
data: dict[str, Any],
|
||||||
|
) -> Tuple[float, float]:
|
||||||
|
"""新开仓 /add_order:支持 price、pct、fixed_rr。"""
|
||||||
|
mode = normalize_open_sltp_mode(sltp_mode)
|
||||||
|
if mode == SLTP_MODE_PCT:
|
||||||
|
return _resolve_pct_sltp(direction, live_price, data)
|
||||||
|
if mode == SLTP_MODE_FIXED_RR:
|
||||||
|
stop_loss, _ = _resolve_price_sltp(data, require_tp=False)
|
||||||
|
rr = parse_fixed_rr(data.get("fixed_rr"))
|
||||||
|
take_profit = calc_tp_from_fixed_rr(direction, live_price, stop_loss, rr)
|
||||||
|
return stop_loss, take_profit
|
||||||
|
return _resolve_price_sltp(data, require_tp=True)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_entrust_sltp_prices(
|
||||||
|
direction: str,
|
||||||
|
live_price: float,
|
||||||
|
sltp_mode: Optional[str],
|
||||||
|
data: dict[str, Any],
|
||||||
|
*,
|
||||||
|
fallback_sl: Optional[float] = None,
|
||||||
|
fallback_tp: Optional[float] = None,
|
||||||
|
) -> Tuple[float, float]:
|
||||||
|
"""持仓委托弹窗:仅 price / pct,不校验盈亏比。"""
|
||||||
|
mode = normalize_entrust_sltp_mode(sltp_mode)
|
||||||
|
if mode == SLTP_MODE_PCT:
|
||||||
|
return _resolve_pct_sltp(direction, live_price, data)
|
||||||
|
return _resolve_price_sltp(
|
||||||
|
data,
|
||||||
|
fallback_sl=fallback_sl,
|
||||||
|
fallback_tp=fallback_tp,
|
||||||
|
require_tp=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from manual_sltp_lib import (
|
||||||
|
MANUAL_FIXED_RR_DEFAULT,
|
||||||
|
calc_tp_from_fixed_rr,
|
||||||
|
parse_fixed_rr,
|
||||||
|
resolve_open_sltp_prices,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_tp_from_fixed_rr_long():
|
||||||
|
tp = calc_tp_from_fixed_rr("long", 100.0, 95.0, 1.5)
|
||||||
|
assert tp == 107.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_tp_from_fixed_rr_short():
|
||||||
|
tp = calc_tp_from_fixed_rr("short", 100.0, 105.0, 1.5)
|
||||||
|
assert tp == 92.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_open_fixed_rr_mode():
|
||||||
|
sl, tp = resolve_open_sltp_prices(
|
||||||
|
"long",
|
||||||
|
100.0,
|
||||||
|
"fixed_rr",
|
||||||
|
{"sl": "95", "fixed_rr": "1.5"},
|
||||||
|
)
|
||||||
|
assert sl == 95.0
|
||||||
|
assert tp == 107.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_fixed_rr_default():
|
||||||
|
assert parse_fixed_rr(None) == MANUAL_FIXED_RR_DEFAULT
|
||||||
|
assert parse_fixed_rr("2") == 2.0
|
||||||
Reference in New Issue
Block a user