Fix roll position display: live contracts, TP profit, and 2-decimal qty precision.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -139,7 +139,11 @@
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">张数</span>
|
||||
<span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%g'|format(o.order_amount) }}{% else %}—{% endif %}</span>
|
||||
<span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %}</span>
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">盈利金额</span>
|
||||
<span class="pos-value pos-tp-profit" id="order-tp-profit-{{ o.id }}">—</span>
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">标记价</span>
|
||||
|
||||
@@ -211,7 +211,11 @@
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">张数</span>
|
||||
<span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%g'|format(o.order_amount) }}{% else %}—{% endif %}</span>
|
||||
<span class="pos-value" id="order-contracts-{{ o.id }}">{% if o.order_amount is not none %}{{ '%.2f'|format(o.order_amount) }}{% else %}—{% endif %}</span>
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">盈利金额</span>
|
||||
<span class="pos-value pos-tp-profit" id="order-tp-profit-{{ o.id }}">—</span>
|
||||
</div>
|
||||
<div class="pos-cell">
|
||||
<span class="pos-label">标记价</span>
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
<td>{{ g.symbol }}</td>
|
||||
<td>{{ g.direction }}</td>
|
||||
<td>{{ g.leg_count }}</td>
|
||||
<td>{% if g.initial_qty is not none %}{{ '%.4g'|format(g.initial_qty) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if g.current_qty is not none %}{{ '%.4g'|format(g.current_qty) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if g.initial_qty is not none %}{{ '%.2f'|format(g.initial_qty) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if g.current_qty is not none %}{{ '%.2f'|format(g.current_qty) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if price_fmt %}{{ price_fmt(g.symbol, g.initial_take_profit) }}{% else %}{{ g.initial_take_profit }}{% endif %}</td>
|
||||
<td>{% if price_fmt %}{{ price_fmt(g.symbol, g.current_stop_loss) }}{% else %}{{ g.current_stop_loss }}{% endif %}</td>
|
||||
<td>{% if g.avg_entry_display %}{{ g.avg_entry_display }}{% elif g.avg_entry is not none %}{{ g.avg_entry }}{% else %}—{% endif %}</td>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user