feat(order): show estimated RR before open across four exchanges
Add shared manual_order_rr_preview.js to fetch order_defaults after symbol and TP/SL inputs complete, display estimated profit-loss ratio before submit in price and percentage modes (and fixed RR), unified for risk and full-margin sizing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -396,6 +396,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
|
<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">
|
||||||
|
<span id="order-rr-preview" class="order-rr-preview" style="font-size:.82rem;color:#8fc8ff;align-self:center">预估盈亏比:—</span>
|
||||||
<button type="submit">{{ open_position_button_label }}</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -809,6 +810,7 @@
|
|||||||
<script src="/static/time_close_ui.js?v=2"></script>
|
<script src="/static/time_close_ui.js?v=2"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||||
|
<script src="/static/manual_order_rr_preview.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1695,6 +1697,7 @@ function refreshOrderTpPreview(entryPx){
|
|||||||
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
||||||
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
||||||
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -1967,6 +1970,7 @@ function refreshOrderDefaults(){
|
|||||||
}
|
}
|
||||||
const px = data.last_price || data.price;
|
const px = data.last_price || data.price;
|
||||||
if(px) refreshOrderTpPreview(px);
|
if(px) refreshOrderTpPreview(px);
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2046,12 +2050,16 @@ function toggleSltpMode(){
|
|||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
refreshOrderTpPreview();
|
refreshOrderTpPreview();
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
loadFixedRrPref();
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
toggleSltpMode();
|
||||||
}
|
}
|
||||||
|
if(window.ManualOrderRrPreview){
|
||||||
|
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
|
||||||
|
}
|
||||||
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
||||||
|
|||||||
@@ -376,6 +376,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
|
<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">
|
||||||
|
<span id="order-rr-preview" class="order-rr-preview" style="font-size:.82rem;color:#8fc8ff;align-self:center">预估盈亏比:—</span>
|
||||||
<button type="submit">{{ open_position_button_label }}</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -789,6 +790,7 @@
|
|||||||
<script src="/static/time_close_ui.js?v=2"></script>
|
<script src="/static/time_close_ui.js?v=2"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||||
|
<script src="/static/manual_order_rr_preview.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1675,6 +1677,7 @@ function refreshOrderTpPreview(entryPx){
|
|||||||
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
||||||
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
||||||
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -1947,6 +1950,7 @@ function refreshOrderDefaults(){
|
|||||||
}
|
}
|
||||||
const px = data.last_price || data.price;
|
const px = data.last_price || data.price;
|
||||||
if(px) refreshOrderTpPreview(px);
|
if(px) refreshOrderTpPreview(px);
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2026,12 +2030,16 @@ function toggleSltpMode(){
|
|||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
refreshOrderTpPreview();
|
refreshOrderTpPreview();
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
loadFixedRrPref();
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
toggleSltpMode();
|
||||||
}
|
}
|
||||||
|
if(window.ManualOrderRrPreview){
|
||||||
|
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
|
||||||
|
}
|
||||||
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
||||||
|
|||||||
@@ -376,6 +376,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
|
<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">
|
||||||
|
<span id="order-rr-preview" class="order-rr-preview" style="font-size:.82rem;color:#8fc8ff;align-self:center">预估盈亏比:—</span>
|
||||||
<button type="submit">{{ open_position_button_label }}</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -789,6 +790,7 @@
|
|||||||
<script src="/static/time_close_ui.js?v=2"></script>
|
<script src="/static/time_close_ui.js?v=2"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||||
|
<script src="/static/manual_order_rr_preview.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1675,6 +1677,7 @@ function refreshOrderTpPreview(entryPx){
|
|||||||
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
||||||
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
||||||
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -1947,6 +1950,7 @@ function refreshOrderDefaults(){
|
|||||||
}
|
}
|
||||||
const px = data.last_price || data.price;
|
const px = data.last_price || data.price;
|
||||||
if(px) refreshOrderTpPreview(px);
|
if(px) refreshOrderTpPreview(px);
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2026,12 +2030,16 @@ function toggleSltpMode(){
|
|||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
refreshOrderTpPreview();
|
refreshOrderTpPreview();
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
loadFixedRrPref();
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
toggleSltpMode();
|
||||||
}
|
}
|
||||||
|
if(window.ManualOrderRrPreview){
|
||||||
|
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
|
||||||
|
}
|
||||||
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
||||||
|
|||||||
@@ -405,6 +405,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" style="display:none">
|
<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">
|
||||||
|
<span id="order-rr-preview" class="order-rr-preview" style="font-size:.82rem;color:#8fc8ff;align-self:center">预估盈亏比:—</span>
|
||||||
<button type="submit">{{ open_position_button_label }}</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -818,6 +819,7 @@
|
|||||||
<script src="/static/time_close_ui.js?v=2"></script>
|
<script src="/static/time_close_ui.js?v=2"></script>
|
||||||
<script src="/static/ai_review_render.js?v=2"></script>
|
<script src="/static/ai_review_render.js?v=2"></script>
|
||||||
<script src="/static/form_submit_guard.js?v=2"></script>
|
<script src="/static/form_submit_guard.js?v=2"></script>
|
||||||
|
<script src="/static/manual_order_rr_preview.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
|
||||||
@@ -1705,6 +1707,7 @@ function refreshOrderTpPreview(entryPx){
|
|||||||
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
const entry = entryPx != null && Number.isFinite(Number(entryPx)) ? Number(entryPx) : sl;
|
||||||
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
const tp = calcTpFromFixedRr(direction, entry, sl, rr);
|
||||||
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
preview.textContent = tp == null ? "预估止盈:—" : ("预估止盈:" + formatPriceForInput(tp));
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -1977,6 +1980,7 @@ function refreshOrderDefaults(){
|
|||||||
}
|
}
|
||||||
const px = data.last_price || data.price;
|
const px = data.last_price || data.price;
|
||||||
if(px) refreshOrderTpPreview(px);
|
if(px) refreshOrderTpPreview(px);
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2079,12 +2083,16 @@ function toggleSltpMode(){
|
|||||||
slPctEl.required = pct;
|
slPctEl.required = pct;
|
||||||
tpPctEl.required = pct;
|
tpPctEl.required = pct;
|
||||||
refreshOrderTpPreview();
|
refreshOrderTpPreview();
|
||||||
|
if(window.ManualOrderRrPreview) ManualOrderRrPreview.schedule();
|
||||||
}
|
}
|
||||||
if(sltpModeEl){
|
if(sltpModeEl){
|
||||||
sltpModeEl.addEventListener("change", toggleSltpMode);
|
sltpModeEl.addEventListener("change", toggleSltpMode);
|
||||||
loadFixedRrPref();
|
loadFixedRrPref();
|
||||||
toggleSltpMode();
|
toggleSltpMode();
|
||||||
}
|
}
|
||||||
|
if(window.ManualOrderRrPreview){
|
||||||
|
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
|
||||||
|
}
|
||||||
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
["order-sl","order-fixed-rr","order-direction"].forEach(function(id){
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
if(el) el.addEventListener("input", function(){ refreshOrderTpPreview(); });
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 实盘下单 · 预估盈亏比
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
四所(Binance / OKX / Gate / Gate趋势)**实盘下单监控**表单中,在「开仓」按钮前显示 **预估盈亏比**。
|
||||||
|
|
||||||
|
- **价格模式**:填完币种、方向、止损价、止盈价后,调用 `GET /api/order_defaults` 取标记价,按几何距离计算 RR。
|
||||||
|
- **百分比模式**:填完币种、方向、止损%、止盈% 后拉快照校验币种,再显示 RR(`止盈% / 止损%`)。
|
||||||
|
- **固定盈亏比模式**:填完币种、方向、止损价、盈亏比后取价校验几何合法,展示填写的盈亏比。
|
||||||
|
|
||||||
|
与计仓方式无关:**以损定仓**(`POSITION_SIZING_MODE=risk`)与 **全仓杠杆**(`full_margin`)均显示同一预估。
|
||||||
|
|
||||||
|
## 前端实现
|
||||||
|
|
||||||
|
- 共享脚本:`static/manual_order_rr_preview.js`
|
||||||
|
- 各所 `templates/index.html` 引入并在 `MANUAL_MIN_PLANNED_RR` 定义后执行:
|
||||||
|
```js
|
||||||
|
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
|
||||||
|
```
|
||||||
|
- 展示元素:`#order-rr-preview`(开仓按钮左侧)
|
||||||
|
- 颜色:≥ 最低要求为绿色,低于为红色,无效/取价失败为红色或灰色
|
||||||
|
|
||||||
|
## 与提交校验
|
||||||
|
|
||||||
|
提交时仍走原有 `calcClientRr` / `calcClientRrFromPct` 与 `rejectManualOrderRr`;预估仅用于下单前参考,不替代服务端风控。
|
||||||
|
|
||||||
|
## 校验记录
|
||||||
|
|
||||||
|
- `node --check static/manual_order_rr_preview.js`
|
||||||
|
- `tests/test_manual_order_rr_preview.py`:RR 公式与四所 `calc_rr_ratio` 口径一致
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* 实盘下单:币种 + 止盈止损填完后拉 /api/order_defaults 快照,在开仓按钮前显示预估盈亏比。
|
||||||
|
* 价格 / 百分比 / 固定盈亏比模式通用;与以损定仓、全仓杠杆计仓方式无关。
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let debounceMs = 400;
|
||||||
|
let minRr = 1.5;
|
||||||
|
let debounceTimer = null;
|
||||||
|
let fetchSeq = 0;
|
||||||
|
|
||||||
|
function $(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function num(v) {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRr(rr) {
|
||||||
|
if (rr === null || typeof rr === "undefined") return "—";
|
||||||
|
const n = Number(rr);
|
||||||
|
if (!Number.isFinite(n)) return "—";
|
||||||
|
const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2)));
|
||||||
|
return body + ":1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcRr(direction, entry, sl, tp) {
|
||||||
|
const e = num(entry);
|
||||||
|
const s = num(sl);
|
||||||
|
const t = num(tp);
|
||||||
|
if (e === null || s === null || t === null) return null;
|
||||||
|
if (direction === "short") {
|
||||||
|
if (s <= e || t >= e) return null;
|
||||||
|
return (e - t) / (s - e);
|
||||||
|
}
|
||||||
|
if (s >= e || t <= e) return null;
|
||||||
|
return (t - e) / (e - s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcRrFromPct(slPct, tpPct) {
|
||||||
|
const sl = num(slPct);
|
||||||
|
const tp = num(tpPct);
|
||||||
|
if (sl === null || tp === null || sl <= 0 || tp <= 0) return null;
|
||||||
|
return tp / sl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcTpFromFixedRr(direction, entry, sl, rr) {
|
||||||
|
const e = num(entry);
|
||||||
|
const s = num(sl);
|
||||||
|
const r = num(rr);
|
||||||
|
if (e === null || s === null || r === null || 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 currentMode() {
|
||||||
|
return ($("sltp-mode") && $("sltp-mode").value) || "fixed_rr";
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentDirection() {
|
||||||
|
return ($("order-direction") && $("order-direction").value) || "long";
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentSymbol() {
|
||||||
|
return (($("order-symbol") && $("order-symbol").value) || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputsComplete(m) {
|
||||||
|
const dir = currentDirection();
|
||||||
|
if (!currentSymbol() || !dir) return false;
|
||||||
|
if (m === "pct") {
|
||||||
|
const sl = num($("order-sl-pct") && $("order-sl-pct").value);
|
||||||
|
const tp = num($("order-tp-pct") && $("order-tp-pct").value);
|
||||||
|
return sl !== null && tp !== null && sl > 0 && tp > 0;
|
||||||
|
}
|
||||||
|
if (m === "fixed_rr") {
|
||||||
|
const sl = num($("order-sl") && $("order-sl").value);
|
||||||
|
const rr = num($("order-fixed-rr") && $("order-fixed-rr").value);
|
||||||
|
return sl !== null && rr !== null && sl > 0 && rr > 0;
|
||||||
|
}
|
||||||
|
const sl = num($("order-sl") && $("order-sl").value);
|
||||||
|
const tp = num($("order-tp") && $("order-tp").value);
|
||||||
|
return sl !== null && tp !== null && sl > 0 && tp > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function paint(rr, state) {
|
||||||
|
const el = $("order-rr-preview");
|
||||||
|
if (!el) return;
|
||||||
|
const m = currentMode();
|
||||||
|
if (m !== "price" && m !== "pct" && m !== "fixed_rr") {
|
||||||
|
el.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.style.display = "";
|
||||||
|
if (state === "empty") {
|
||||||
|
el.textContent = "预估盈亏比:—";
|
||||||
|
el.style.color = "#8fc8ff";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state === "loading") {
|
||||||
|
el.textContent = "预估盈亏比:计算中…";
|
||||||
|
el.style.color = "#8fc8ff";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state === "fetch_fail") {
|
||||||
|
el.textContent = "预估盈亏比:取价失败";
|
||||||
|
el.style.color = "#ff8f8f";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rr === null) {
|
||||||
|
el.textContent = "预估盈亏比:无效";
|
||||||
|
el.style.color = "#ff8f8f";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.textContent = "预估盈亏比:" + formatRr(rr);
|
||||||
|
el.style.color = rr >= minRr ? "#4cd97f" : "#ff8f8f";
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshNow() {
|
||||||
|
const m = currentMode();
|
||||||
|
if (!inputsComplete(m)) {
|
||||||
|
paint(null, "empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sym = currentSymbol();
|
||||||
|
const dir = currentDirection();
|
||||||
|
const seq = ++fetchSeq;
|
||||||
|
paint(null, "loading");
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
"/api/order_defaults?symbol=" +
|
||||||
|
encodeURIComponent(sym) +
|
||||||
|
"&direction=" +
|
||||||
|
encodeURIComponent(dir)
|
||||||
|
)
|
||||||
|
.then(function (r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
if (seq !== fetchSeq) return;
|
||||||
|
if (!data.ok) {
|
||||||
|
paint(null, "fetch_fail");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m === "pct") {
|
||||||
|
const rr = calcRrFromPct(
|
||||||
|
$("order-sl-pct") && $("order-sl-pct").value,
|
||||||
|
$("order-tp-pct") && $("order-tp-pct").value
|
||||||
|
);
|
||||||
|
paint(rr, rr === null ? "invalid" : "ok");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry = num(data.last_price != null ? data.last_price : data.price);
|
||||||
|
if (entry === null) {
|
||||||
|
paint(null, "fetch_fail");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let rr = null;
|
||||||
|
if (m === "fixed_rr") {
|
||||||
|
const sl = num($("order-sl") && $("order-sl").value);
|
||||||
|
const fixed = num($("order-fixed-rr") && $("order-fixed-rr").value);
|
||||||
|
const tp = calcTpFromFixedRr(dir, entry, sl, fixed);
|
||||||
|
rr = tp !== null ? fixed : null;
|
||||||
|
} else {
|
||||||
|
const sl = num($("order-sl") && $("order-sl").value);
|
||||||
|
const tp = num($("order-tp") && $("order-tp").value);
|
||||||
|
rr = calcRr(dir, entry, sl, tp);
|
||||||
|
}
|
||||||
|
paint(rr, rr === null ? "invalid" : "ok");
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
if (seq !== fetchSeq) return;
|
||||||
|
paint(null, "fetch_fail");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule() {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(refreshNow, debounceMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wire(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
if (opts.minRr != null && Number.isFinite(Number(opts.minRr))) {
|
||||||
|
minRr = Number(opts.minRr);
|
||||||
|
}
|
||||||
|
if (opts.debounceMs != null && Number.isFinite(Number(opts.debounceMs))) {
|
||||||
|
debounceMs = Number(opts.debounceMs);
|
||||||
|
}
|
||||||
|
[
|
||||||
|
"order-symbol",
|
||||||
|
"order-direction",
|
||||||
|
"sltp-mode",
|
||||||
|
"order-sl",
|
||||||
|
"order-tp",
|
||||||
|
"order-sl-pct",
|
||||||
|
"order-tp-pct",
|
||||||
|
"order-fixed-rr",
|
||||||
|
].forEach(function (id) {
|
||||||
|
const el = $(id);
|
||||||
|
if (!el || el._rrPreviewBound) return;
|
||||||
|
el._rrPreviewBound = true;
|
||||||
|
el.addEventListener("input", schedule);
|
||||||
|
el.addEventListener("change", schedule);
|
||||||
|
});
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.ManualOrderRrPreview = {
|
||||||
|
wire: wire,
|
||||||
|
schedule: schedule,
|
||||||
|
refresh: refreshNow,
|
||||||
|
calcRr: calcRr,
|
||||||
|
calcRrFromPct: calcRrFromPct,
|
||||||
|
formatRr: formatRr,
|
||||||
|
};
|
||||||
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""预估盈亏比(前端 manual_order_rr_preview.js)公式与后端 calc_rr_ratio 口径一致。"""
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_rr(direction: str, entry: float, sl: float, tp: float):
|
||||||
|
if entry <= 0 or sl <= 0 or tp <= 0:
|
||||||
|
return None
|
||||||
|
if direction == "short":
|
||||||
|
risk = sl - entry
|
||||||
|
reward = entry - tp
|
||||||
|
else:
|
||||||
|
risk = entry - sl
|
||||||
|
reward = tp - entry
|
||||||
|
if risk <= 0 or reward <= 0:
|
||||||
|
return None
|
||||||
|
return round(reward / risk, 4)
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_rr_from_pct(sl_pct: float, tp_pct: float):
|
||||||
|
if sl_pct <= 0 or tp_pct <= 0:
|
||||||
|
return None
|
||||||
|
return tp_pct / sl_pct
|
||||||
|
|
||||||
|
|
||||||
|
def test_long_price_mode_rr():
|
||||||
|
assert _calc_rr("long", 100.0, 95.0, 110.0) == 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_price_mode_rr():
|
||||||
|
assert _calc_rr("short", 100.0, 105.0, 90.0) == 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_geometry_returns_none():
|
||||||
|
assert _calc_rr("long", 100.0, 105.0, 110.0) is None
|
||||||
|
assert _calc_rr("short", 100.0, 95.0, 98.0) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_pct_mode_rr():
|
||||||
|
assert _calc_rr_from_pct(2.0, 4.0) == 2.0
|
||||||
|
assert _calc_rr_from_pct(1.5, 3.0) == 2.0
|
||||||
Reference in New Issue
Block a user