From e3e53ff7f52c262d8e187feaf50a33c0491e34d1 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 8 Jul 2026 17:06:33 +0800 Subject: [PATCH] Fix roll position display: live contracts, TP profit, and 2-decimal qty precision. Co-authored-by: Cursor --- crypto_monitor_binance/app.py | 12 +++++- crypto_monitor_gate/app.py | 12 +++++- crypto_monitor_okx/app.py | 12 +++++- lib/common/static/instance_theme.css | 9 +++++ lib/hub/hub_position_metrics.py | 22 ++++++++++- .../templates/embed_boot_scripts.html | 21 +++++++++- .../templates/embed_page_fragment.html | 6 ++- lib/instance/templates/index.html | 27 ++++++++++++- lib/strategy/strategy_config.py | 4 +- lib/strategy/strategy_register.py | 18 ++++++++- lib/strategy/strategy_roll_monitor_lib.py | 37 +++++++++++++----- lib/strategy/strategy_roll_ui_lib.py | 21 ++++++---- .../templates/strategy_roll_panel.html | 4 +- lib/trade/order_monitor_display_lib.py | 39 +++++++++++++++++-- tests/test_order_monitor_display_lib.py | 6 +++ 15 files changed, 217 insertions(+), 33 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 40c29a3..217f197 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -1981,6 +1981,8 @@ def _position_matches_wanted_contract(wanted_unified_sym, position_dict): def _position_row_effective_contracts(p): """持仓数量:优先 ccxt contracts,否则用交易所原始 positionAmt/size/pos(避免统一层为 0 时被误判空仓)。""" + from lib.hub.hub_position_metrics import normalize_contracts_qty + if not p: return 0.0 info = p.get("info") or {} @@ -1990,7 +1992,7 @@ def _position_row_effective_contracts(p): try: x = abs(float(val)) if x > 0: - return x + return normalize_contracts_qty(x) except (TypeError, ValueError): continue return 0.0 @@ -7688,6 +7690,13 @@ def api_price_snapshot(): except Exception: exchange_tpsl = {"sl": None, "tp": None} payload["exchange_tpsl"] = exchange_tpsl + avg_entry = None + if ex_metrics and ex_metrics.get("entry_price") is not None: + avg_entry = ex_metrics["entry_price"] + elif prow: + from lib.hub.hub_position_metrics import parse_position_entry_price + + avg_entry = parse_position_entry_price(prow) apply_order_price_display_fields( payload, direction=r["direction"], @@ -7705,6 +7714,7 @@ def api_price_snapshot(): contracts=abs(_position_row_effective_contracts(prow)) if prow else None, contract_size=float(get_contract_size(r["symbol"])) if r["symbol"] else 1.0, mark_price=ex_metrics.get("mark_price") if ex_metrics else price, + avg_entry_price=avg_entry, funds_decimals=FUNDS_DECIMALS, ) apply_time_close_to_payload(payload, r) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 71608de..6a7f8b6 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -1941,6 +1941,8 @@ def _position_matches_wanted_contract(wanted_unified_sym, position_dict): def _position_row_effective_contracts(p): """张数:优先 ccxt contracts,否则用 Gate 原始 size/pos(避免统一层为 0 时被误判空仓)。""" + from lib.hub.hub_position_metrics import normalize_contracts_qty + if not p: return 0.0 info = p.get("info") or {} @@ -1950,7 +1952,7 @@ def _position_row_effective_contracts(p): try: x = abs(float(val)) if x > 0: - return x + return normalize_contracts_qty(x) except (TypeError, ValueError): continue return 0.0 @@ -7551,6 +7553,13 @@ def api_price_snapshot(): except Exception: exchange_tpsl = {"sl": None, "tp": None} payload["exchange_tpsl"] = exchange_tpsl + avg_entry = None + if ex_metrics and ex_metrics.get("entry_price") is not None: + avg_entry = ex_metrics["entry_price"] + elif prow: + from lib.hub.hub_position_metrics import parse_position_entry_price + + avg_entry = parse_position_entry_price(prow) apply_order_price_display_fields( payload, direction=r["direction"], @@ -7568,6 +7577,7 @@ def api_price_snapshot(): contracts=abs(_position_row_effective_contracts(prow)) if prow else None, contract_size=float(get_contract_size(r["symbol"])) if r["symbol"] else 1.0, mark_price=ex_metrics.get("mark_price") if ex_metrics else price, + avg_entry_price=avg_entry, funds_decimals=FUNDS_DECIMALS, ) apply_time_close_to_payload(payload, r) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 12a9996..28b8815 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -2876,6 +2876,8 @@ def exchange_private_api_configured(): def _position_row_effective_contracts(p): """张数:OKX 以 info.pos 为准,再兜底 ccxt contracts 等(与 Binance/Gate 多字段一致)。""" + from lib.hub.hub_position_metrics import normalize_contracts_qty + if not p: return 0.0 info = p.get("info", {}) or {} @@ -2885,7 +2887,7 @@ def _position_row_effective_contracts(p): try: x = abs(float(val)) if x > 0: - return x + return normalize_contracts_qty(x) except (TypeError, ValueError): continue return 0.0 @@ -7248,6 +7250,13 @@ def api_price_snapshot(): except Exception: exchange_tpsl = {"sl": None, "tp": None} payload["exchange_tpsl"] = exchange_tpsl + avg_entry = None + if ex_metrics and ex_metrics.get("entry_price") is not None: + avg_entry = ex_metrics["entry_price"] + elif prow: + from lib.hub.hub_position_metrics import parse_position_entry_price + + avg_entry = parse_position_entry_price(prow) apply_order_price_display_fields( payload, direction=r["direction"], @@ -7265,6 +7274,7 @@ def api_price_snapshot(): contracts=abs(_position_row_effective_contracts(prow)) if prow else None, contract_size=float(get_contract_size(r["symbol"])) if r["symbol"] else 1.0, mark_price=ex_metrics.get("mark_price") if ex_metrics else price, + avg_entry_price=avg_entry, funds_decimals=FUNDS_DECIMALS, ) apply_time_close_to_payload(payload, r) diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index c8f42b1..86a6459 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -1635,6 +1635,15 @@ html[data-theme="light"] #strategy-roll-panel .roll-section-title { font-weight: 600; } +.pos-tp-profit { + color: #4cd97f; + font-weight: 600; +} + +html[data-theme="light"] .pos-tp-profit { + color: #1a8f4a !important; +} + html[data-theme="light"] #strategy-roll-panel .roll-active-groups-table .roll-tp-profit, html[data-theme="light"] #strategy-roll-panel .roll-active-groups-table .roll-status-active { color: #1a8f4a !important; diff --git a/lib/hub/hub_position_metrics.py b/lib/hub/hub_position_metrics.py index 1540f49..9010d5d 100644 --- a/lib/hub/hub_position_metrics.py +++ b/lib/hub/hub_position_metrics.py @@ -23,6 +23,24 @@ def _coerce_float(*values: Any) -> float | None: return None +CONTRACTS_QTY_DECIMALS = 2 + + +def normalize_contracts_qty(qty: Any, *, decimals: int = CONTRACTS_QTY_DECIMALS) -> float: + """张数统一精度(OKX 等线性永续默认两位小数)。""" + try: + q = float(qty) + except (TypeError, ValueError): + return 0.0 + if not math.isfinite(q): + return 0.0 + return round(abs(q), decimals) + + +def contracts_qty_is_open(qty: Any, *, decimals: int = CONTRACTS_QTY_DECIMALS) -> bool: + return normalize_contracts_qty(qty, decimals=decimals) > 0 + + def position_contracts(p: dict[str, Any]) -> float: info = p.get("info") or {} if not isinstance(info, dict): @@ -33,7 +51,7 @@ def position_contracts(p: dict[str, Any]) -> float: try: v = float(info[k]) if v != 0: - return abs(v) + return normalize_contracts_qty(v) except (TypeError, ValueError): pass raw = p.get("contracts") @@ -41,7 +59,7 @@ def position_contracts(p: dict[str, Any]) -> float: try: v = float(raw) if v != 0: - return abs(v) + return normalize_contracts_qty(v) except (TypeError, ValueError): pass return 0.0 diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index 6c0f6b8..671f334 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -788,6 +788,8 @@ function submitTpslEntrust(){ if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl); paintPlanTpslDisplay(orderId, data); paintLatestRiskDisplay(orderId, data); + paintContractsDisplay(orderId, data); + paintTpProfitDisplay(orderId, data); const rrEl = document.getElementById(`order-rr-${orderId}`); if(rrEl){ const rr = data.display_rr_ratio != null && data.display_rr_ratio !== "" ? data.display_rr_ratio : data.planned_rr; @@ -881,7 +883,20 @@ function paintContractsDisplay(orderId, snap){ if(!el || !snap) return; const v = snap.contracts != null && snap.contracts !== "" ? snap.contracts : snap.order_amount; const n = v != null && v !== "" ? Number(v) : NaN; - el.innerText = Number.isFinite(n) ? String(parseFloat(n.toFixed(4))) : "—"; + el.innerText = Number.isFinite(n) ? n.toFixed(2) : "—"; +} +function paintTpProfitDisplay(orderId, snap){ + const el = document.getElementById(`order-tp-profit-${orderId}`); + if(!el) return; + const v = snap && snap.reward_at_tp_usdt; + const n = v != null && v !== "" ? Number(v) : NaN; + if(Number.isFinite(n)){ + el.innerText = `${n.toFixed(2)}U`; + el.classList.add("pos-tp-profit"); + } else { + el.innerText = "—"; + el.classList.remove("pos-tp-profit"); + } } function paintPriceTrend(el, key, value){ @@ -983,6 +998,7 @@ function refreshPriceSnapshot(){ } paintLatestRiskDisplay(o.id, o); paintContractsDisplay(o.id, o); + paintTpProfitDisplay(o.id, o); paintBreakevenBadge(o.id, o.sl_breakeven_secured); if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl); paintPlanTpslDisplay(o.id, o); @@ -1317,6 +1333,9 @@ function refreshPriceSnapshotConditional(){ if(tradePage){ (data.order_prices || []).forEach(o=>{ paintOrderMarkAndPnl(o.id, o); + paintLatestRiskDisplay(o.id, o); + paintContractsDisplay(o.id, o); + paintTpProfitDisplay(o.id, o); const exM = document.getElementById(`order-ex-margin-${o.id}`); if(exM){ const mv = o.exchange_initial_margin; diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html index f06d318..7efb1d2 100644 --- a/lib/instance/templates/embed_page_fragment.html +++ b/lib/instance/templates/embed_page_fragment.html @@ -139,7 +139,11 @@
张数 - {% if o.order_amount is not none %}{{ '%g'|format(o.order_amount) }}{% else %}—{% endif %} + {% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %} +
+
+ 盈利金额 +
标记价 diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index a78ea96..3614c3c 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -211,7 +211,11 @@
张数 - {% if o.order_amount is not none %}{{ '%g'|format(o.order_amount) }}{% else %}—{% endif %} + {% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %} +
+
+ 盈利金额 +
标记价 @@ -1304,6 +1308,8 @@ function submitTpslEntrust(){ if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl); paintPlanTpslDisplay(orderId, data); paintLatestRiskDisplay(orderId, data); + paintContractsDisplay(orderId, data); + paintTpProfitDisplay(orderId, data); const rrEl = document.getElementById(`order-rr-${orderId}`); if(rrEl){ const rr = data.display_rr_ratio != null && data.display_rr_ratio !== "" ? data.display_rr_ratio : data.planned_rr; @@ -1393,7 +1399,20 @@ function paintContractsDisplay(orderId, snap){ if(!el || !snap) return; const v = snap.contracts != null && snap.contracts !== "" ? snap.contracts : snap.order_amount; const n = v != null && v !== "" ? Number(v) : NaN; - el.innerText = Number.isFinite(n) ? String(parseFloat(n.toFixed(4))) : "—"; + el.innerText = Number.isFinite(n) ? n.toFixed(2) : "—"; +} +function paintTpProfitDisplay(orderId, snap){ + const el = document.getElementById(`order-tp-profit-${orderId}`); + if(!el) return; + const v = snap && snap.reward_at_tp_usdt; + const n = v != null && v !== "" ? Number(v) : NaN; + if(Number.isFinite(n)){ + el.innerText = `${n.toFixed(2)}U`; + el.classList.add("pos-tp-profit"); + } else { + el.innerText = "—"; + el.classList.remove("pos-tp-profit"); + } } function paintPriceTrend(el, key, value){ @@ -1526,6 +1545,7 @@ function refreshPriceSnapshot(){ } paintLatestRiskDisplay(o.id, o); paintContractsDisplay(o.id, o); + paintTpProfitDisplay(o.id, o); paintBreakevenBadge(o.id, o.sl_breakeven_secured); if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl); paintPlanTpslDisplay(o.id, o); @@ -1916,6 +1936,9 @@ function refreshPriceSnapshotConditional(){ } const rrEl = document.getElementById(`order-rr-${o.id}`); if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio); + paintLatestRiskDisplay(o.id, o); + paintContractsDisplay(o.id, o); + paintTpProfitDisplay(o.id, o); paintBreakevenBadge(o.id, o.sl_breakeven_secured); paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); paintPlanTpslDisplay(o.id, o); diff --git a/lib/strategy/strategy_config.py b/lib/strategy/strategy_config.py index 53c9838..53bbd46 100644 --- a/lib/strategy/strategy_config.py +++ b/lib/strategy/strategy_config.py @@ -55,6 +55,8 @@ def build_strategy_config( return float(row["current_capital"]) def get_position(ex_sym, direction): + from lib.hub.hub_position_metrics import normalize_contracts_qty + qty = m.get_live_position_contracts(ex_sym, direction) entry = None try: @@ -77,7 +79,7 @@ def build_strategy_config( break except Exception: pass - return {"contracts": float(qty or 0), "entry_price": entry} + return {"contracts": normalize_contracts_qty(qty or 0), "entry_price": entry} def amount_to_precision(ex_sym, amount): try: diff --git a/lib/strategy/strategy_register.py b/lib/strategy/strategy_register.py index 30d202f..59da1e0 100644 --- a/lib/strategy/strategy_register.py +++ b/lib/strategy/strategy_register.py @@ -492,9 +492,23 @@ def _roll_execute(cfg: dict, data: dict) -> tuple[bool, str]: "UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?", (legs_done + 1, new_sl, cfg["app_now_str"](), rg["id"]), ) + live_qty = float(mon.get("order_amount") or 0) + float(amount) + try: + from lib.hub.hub_position_metrics import contracts_qty_is_open, normalize_contracts_qty + + pos2 = cfg["get_position"](ex_sym, direction) or {} + q2 = normalize_contracts_qty(pos2.get("contracts") or 0) + if contracts_qty_is_open(q2): + live_qty = q2 + else: + live_qty = normalize_contracts_qty(live_qty) + except Exception: + from lib.hub.hub_position_metrics import normalize_contracts_qty + + live_qty = normalize_contracts_qty(live_qty) conn.execute( - "UPDATE order_monitors SET stop_loss=? WHERE id=?", - (new_sl, mon["id"]), + "UPDATE order_monitors SET stop_loss=?, order_amount=? WHERE id=?", + (new_sl, live_qty, mon["id"]), ) conn.commit() _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, roll_is_new=roll_is_new) diff --git a/lib/strategy/strategy_roll_monitor_lib.py b/lib/strategy/strategy_roll_monitor_lib.py index 28a6efd..ad962a3 100644 --- a/lib/strategy/strategy_roll_monitor_lib.py +++ b/lib/strategy/strategy_roll_monitor_lib.py @@ -205,6 +205,8 @@ def _close_roll_group(conn, cfg: dict, group: dict, *, reason: str = "下单监 def _reconcile_roll_groups(conn, cfg: dict) -> None: + from lib.hub.hub_position_metrics import contracts_qty_is_open, normalize_contracts_qty + rows = conn.execute( """SELECT g.*, m.status AS monitor_status FROM roll_groups g @@ -217,9 +219,24 @@ def _reconcile_roll_groups(conn, cfg: dict) -> None: direction = (g.get("direction") or "long").strip().lower() ex_sym = g.get("exchange_symbol") or cfg["normalize_exchange_symbol"](symbol) mon_ok = (row["monitor_status"] or "").strip().lower() == "active" - pos = cfg["get_position"](ex_sym, direction) - qty = float(pos.get("contracts") or 0) - if not mon_ok or qty <= 0: + if not mon_ok: + _close_roll_group(conn, cfg, g, reason="下单监控已结案") + continue + pos = None + try: + pos = cfg["get_position"](ex_sym, direction) + except Exception: + pos = None + if pos is None: + continue + qty = normalize_contracts_qty(pos.get("contracts") or 0) + if not contracts_qty_is_open(qty): + try: + pos2 = cfg["get_position"](ex_sym, direction) or {} + qty = normalize_contracts_qty(pos2.get("contracts") or 0) + except Exception: + continue + if not contracts_qty_is_open(qty): _close_roll_group(conn, cfg, g) @@ -342,6 +359,8 @@ def _execute_pending_roll_leg( direction: str, mark: float, ) -> None: + from lib.hub.hub_position_metrics import contracts_qty_is_open, normalize_contracts_qty + leg_id = int(leg["id"]) gid = int(group["roll_group_id"]) if "roll_group_id" in leg else int(group["id"]) mon_id = group.get("order_monitor_id") @@ -354,9 +373,9 @@ def _execute_pending_roll_leg( return pos = cfg["get_position"](ex_sym, direction) or {} - qty = float(pos.get("contracts") or 0) + qty = normalize_contracts_qty(pos.get("contracts") or 0) entry = float(pos.get("entry_price") or mon.get("trigger_price") or 0) - if qty <= 0 or entry <= 0: + if not contracts_qty_is_open(qty) or entry <= 0: _invalidate_roll_leg(conn, cfg, group, leg, mark, reason="无持仓") return @@ -431,17 +450,17 @@ def _execute_pending_roll_leg( "UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?", (filled + 1, sl, _now(cfg), gid), ) - live_qty = qty + float(amount) + live_qty = normalize_contracts_qty(qty + float(amount)) try: pos2 = cfg["get_position"](ex_sym, direction) or {} - q2 = float(pos2.get("contracts") or 0) - if q2 > 0: + q2 = normalize_contracts_qty(pos2.get("contracts") or 0) + if contracts_qty_is_open(q2): live_qty = q2 except Exception: pass conn.execute( "UPDATE order_monitors SET stop_loss=?, order_amount=? WHERE id=? AND status='active'", - (sl, float(live_qty), mon["id"]), + (sl, live_qty, mon["id"]), ) notify = cfg.get("send_wechat") diff --git a/lib/strategy/strategy_roll_ui_lib.py b/lib/strategy/strategy_roll_ui_lib.py index 971b000..eeef71d 100644 --- a/lib/strategy/strategy_roll_ui_lib.py +++ b/lib/strategy/strategy_roll_ui_lib.py @@ -63,13 +63,18 @@ def infer_initial_position( entry_live = float(entry_live) except (TypeError, ValueError): qty_live = entry_live = 0.0 + from lib.hub.hub_position_metrics import normalize_contracts_qty + + qty_live = normalize_contracts_qty(qty_live) legs = [ lg for lg in filled_legs or [] if isinstance(lg, dict) and leg_is_filled(lg) and leg_fill_price(lg) and float(lg.get("amount") or 0) > 0 ] - add_sum = sum(float(lg.get("amount") or 0) for lg in legs) - leg_notional = sum(float(lg.get("amount") or 0) * float(leg_fill_price(lg) or 0) for lg in legs) + add_sum = sum(normalize_contracts_qty(lg.get("amount") or 0) for lg in legs) + leg_notional = sum( + normalize_contracts_qty(lg.get("amount") or 0) * float(leg_fill_price(lg) or 0) for lg in legs + ) q0 = qty_live - add_sum if q0 > 1e-12 and entry_live > 0 and qty_live > 0: e0 = (entry_live * qty_live - leg_notional) / q0 @@ -131,8 +136,8 @@ def compute_roll_chain_metrics( return per_leg, group_out qty = float(q0) avg = float(e0) - group_out["initial_qty"] = round(qty, 4) - group_out["current_qty"] = round(qty, 4) + group_out["initial_qty"] = round(qty, 2) + group_out["current_qty"] = round(qty, 2) if tp > 0: group_out["avg_entry"] = avg group_out["reward_at_tp_usdt"] = reward_at_tp_usdt( @@ -161,12 +166,12 @@ def compute_roll_chain_metrics( } group_out["avg_entry"] = round(avg, 10) group_out["reward_at_tp_usdt"] = round(reward, 4) if reward is not None else None - group_out["current_qty"] = round(qty, 4) + group_out["current_qty"] = round(qty, 2) if qty_live is not None: try: live_qty = float(qty_live) if live_qty > 0: - group_out["current_qty"] = round(live_qty, 4) + group_out["current_qty"] = round(live_qty, 2) except (TypeError, ValueError): pass return per_leg, group_out @@ -208,7 +213,9 @@ def _resolve_roll_live(cfg: dict, group: dict, monitor: dict | None) -> tuple[Op return None, None, cs try: pos = get_pos(ex_sym or sym, direction) or {} - qty = float(pos.get("contracts") or 0) + from lib.hub.hub_position_metrics import normalize_contracts_qty + + qty = normalize_contracts_qty(pos.get("contracts") or 0) entry = float(pos.get("entry_price") or 0) if qty > 0 and entry > 0: return qty, entry, cs diff --git a/lib/strategy/templates/strategy_roll_panel.html b/lib/strategy/templates/strategy_roll_panel.html index 5edf72f..37f94f8 100644 --- a/lib/strategy/templates/strategy_roll_panel.html +++ b/lib/strategy/templates/strategy_roll_panel.html @@ -63,8 +63,8 @@ {{ g.symbol }} {{ g.direction }} {{ g.leg_count }} - {% if g.initial_qty is not none %}{{ '%.4g'|format(g.initial_qty) }}{% else %}—{% endif %} - {% if g.current_qty is not none %}{{ '%.4g'|format(g.current_qty) }}{% else %}—{% endif %} + {% if g.initial_qty is not none %}{{ '%.2f'|format(g.initial_qty) }}{% else %}—{% endif %} + {% if g.current_qty is not none %}{{ '%.2f'|format(g.current_qty) }}{% else %}—{% endif %} {% if price_fmt %}{{ price_fmt(g.symbol, g.initial_take_profit) }}{% else %}{{ g.initial_take_profit }}{% endif %} {% if price_fmt %}{{ price_fmt(g.symbol, g.current_stop_loss) }}{% else %}{{ g.current_stop_loss }}{% endif %} {% if g.avg_entry_display %}{{ g.avg_entry_display }}{% elif g.avg_entry is not none %}{{ g.avg_entry }}{% else %}—{% endif %} diff --git a/lib/trade/order_monitor_display_lib.py b/lib/trade/order_monitor_display_lib.py index 7059b61..edc0c24 100644 --- a/lib/trade/order_monitor_display_lib.py +++ b/lib/trade/order_monitor_display_lib.py @@ -276,6 +276,7 @@ def apply_order_price_display_fields( contracts: Any = None, contract_size: Any = None, mark_price: Any = None, + avg_entry_price: Any = None, funds_decimals: int = 2, ) -> dict[str, Any]: disp_sl, disp_tp, _, _ = resolve_live_tpsl_prices(stop_loss, take_profit, exchange_tpsl) @@ -302,14 +303,17 @@ def apply_order_price_display_fields( payload["display_rr_ratio"] = None if contracts is not None: try: - c = abs(float(contracts)) + from lib.hub.hub_position_metrics import normalize_contracts_qty + + c = normalize_contracts_qty(contracts) if c > 0: payload["contracts"] = c except (TypeError, ValueError): pass + risk_entry = _positive_float(avg_entry_price) or _positive_float(entry_price) payload["latest_risk_amount"] = calc_latest_risk_amount( direction, - entry_price, + risk_entry, disp_sl if disp_sl is not None else stop_loss, margin_capital=margin_capital, leverage=leverage, @@ -319,6 +323,31 @@ def apply_order_price_display_fields( mark_price=mark_price, funds_decimals=funds_decimals, ) + tp_for_reward = disp_tp if disp_tp is not None else _positive_float(take_profit) + qty_for_reward = payload.get("contracts") + if qty_for_reward is None and contracts is not None: + try: + qty_for_reward = abs(float(contracts)) + except (TypeError, ValueError): + qty_for_reward = None + if risk_entry is not None and tp_for_reward is not None and qty_for_reward: + try: + from lib.strategy.strategy_roll_ui_lib import reward_at_tp_usdt + + reward = reward_at_tp_usdt( + direction, + risk_entry, + tp_for_reward, + float(qty_for_reward), + contract_size=float(contract_size or 1.0), + ) + payload["reward_at_tp_usdt"] = ( + round(reward, funds_decimals) if reward is not None else None + ) + except Exception: + payload["reward_at_tp_usdt"] = None + else: + payload["reward_at_tp_usdt"] = None if format_price_fn is not None and symbol is not None: payload["stop_loss_display"] = ( format_price_fn(symbol, disp_sl) if disp_sl is not None else "—" @@ -362,11 +391,14 @@ def enrich_active_monitor_tpsl_json( margin = _row_val("margin_capital") leverage = _row_val("leverage") if position_row is not None: - from lib.hub.hub_position_metrics import position_contracts + from lib.hub.hub_position_metrics import parse_position_entry_price, position_contracts live_c = position_contracts(position_row) if abs(live_c) >= 1e-12: contracts = abs(live_c) + avg_entry = parse_position_entry_price(position_row) + else: + avg_entry = None payload: dict[str, Any] = { "stop_loss": stop_loss, "take_profit": take_profit, @@ -388,6 +420,7 @@ def enrich_active_monitor_tpsl_json( contracts=contracts, contract_size=contract_size, mark_price=mark_price, + avg_entry_price=avg_entry, funds_decimals=funds_decimals, ) return payload diff --git a/tests/test_order_monitor_display_lib.py b/tests/test_order_monitor_display_lib.py index 90d96e6..600c12d 100644 --- a/tests/test_order_monitor_display_lib.py +++ b/tests/test_order_monitor_display_lib.py @@ -100,6 +100,9 @@ def test_apply_order_price_display_fields_live_sl(): margin_capital=100, leverage=10, exchange_notional=1000, + contracts=2.0, + contract_size=1.0, + avg_entry_price=1660.0, ) assert payload["stop_loss"] == 1661 assert payload["stop_loss_display"] == "1661.00" @@ -107,6 +110,9 @@ def test_apply_order_price_display_fields_live_sl(): assert payload["rr_ratio"] is not None assert payload["latest_risk_amount"] is not None assert payload["latest_risk_amount"] >= 0 + assert payload["contracts"] == 2.0 + assert payload["reward_at_tp_usdt"] is not None + assert payload["reward_at_tp_usdt"] > 0 def test_calc_latest_risk_amount_long():