fix(hub): show contract-based unrealized PnL in monitor and chart
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,6 +56,69 @@ def position_side_from_ccxt(p: dict[str, Any], contracts: float | None = None) -
|
|||||||
return "long"
|
return "long"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_position_entry_price(p: dict[str, Any]) -> float | None:
|
||||||
|
"""四所 ccxt 持仓开仓均价。"""
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
return None
|
||||||
|
info = p.get("info") or {}
|
||||||
|
if not isinstance(info, dict):
|
||||||
|
info = {}
|
||||||
|
return _coerce_float(
|
||||||
|
p.get("entryPrice"),
|
||||||
|
p.get("entry_price"),
|
||||||
|
p.get("average"),
|
||||||
|
info.get("entryPrice"),
|
||||||
|
info.get("entry_price"),
|
||||||
|
info.get("avgPx"),
|
||||||
|
info.get("avgEntryPrice"),
|
||||||
|
info.get("avg_entry_price"),
|
||||||
|
info.get("avgPrice"),
|
||||||
|
info.get("openAvgPx"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_linear_swap_upnl_usdt(
|
||||||
|
side: str,
|
||||||
|
entry: float | None,
|
||||||
|
mark: float | None,
|
||||||
|
contracts: float | None,
|
||||||
|
contract_size: float | None = None,
|
||||||
|
) -> float | None:
|
||||||
|
"""U 本位线性永续:浮盈 = (标记价 - 开仓价) × 张数 × contractSize(空头取反)。"""
|
||||||
|
e = _finite_or_none(entry)
|
||||||
|
m = _finite_or_none(mark)
|
||||||
|
c = _finite_or_none(contracts)
|
||||||
|
if e is None or m is None or c is None or c <= 0:
|
||||||
|
return None
|
||||||
|
mult = _finite_or_none(contract_size)
|
||||||
|
if mult is None or mult <= 0:
|
||||||
|
mult = 1.0
|
||||||
|
diff = (m - e) if (side or "long").strip().lower() == "long" else (e - m)
|
||||||
|
return round(diff * abs(c) * mult, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_position_display_upnl(
|
||||||
|
side: str,
|
||||||
|
entry: float | None,
|
||||||
|
mark: float | None,
|
||||||
|
contracts: float | None,
|
||||||
|
contract_size: float | None,
|
||||||
|
exchange_upnl: float | None,
|
||||||
|
) -> float | None:
|
||||||
|
"""展示用浮盈:优先与标记价/张数一致的推算;与交易所值偏差过大时用推算值。"""
|
||||||
|
computed = estimate_linear_swap_upnl_usdt(
|
||||||
|
side, entry, mark, contracts, contract_size
|
||||||
|
)
|
||||||
|
if computed is None:
|
||||||
|
return exchange_upnl
|
||||||
|
if exchange_upnl is None:
|
||||||
|
return computed
|
||||||
|
ref = max(abs(computed), 1.0)
|
||||||
|
if abs(exchange_upnl - computed) / ref > 0.2:
|
||||||
|
return computed
|
||||||
|
return exchange_upnl
|
||||||
|
|
||||||
|
|
||||||
def parse_position_unrealized_pnl(p: dict[str, Any]) -> float | None:
|
def parse_position_unrealized_pnl(p: dict[str, Any]) -> float | None:
|
||||||
"""四所 ccxt 持仓统一解析未实现盈亏(Gate 常在 info.unrealised_pnl)。"""
|
"""四所 ccxt 持仓统一解析未实现盈亏(Gate 常在 info.unrealised_pnl)。"""
|
||||||
if not isinstance(p, dict):
|
if not isinstance(p, dict):
|
||||||
|
|||||||
+31
-25
@@ -31,7 +31,12 @@ _REPO_ROOT = Path(__file__).resolve().parents[1]
|
|||||||
if str(_REPO_ROOT) not in sys.path:
|
if str(_REPO_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(_REPO_ROOT))
|
sys.path.insert(0, str(_REPO_ROOT))
|
||||||
from hub_ohlcv_lib import format_price_by_tick, price_tick_from_market
|
from hub_ohlcv_lib import format_price_by_tick, price_tick_from_market
|
||||||
from hub_position_metrics import parse_position_mark_price, parse_position_unrealized_pnl
|
from hub_position_metrics import (
|
||||||
|
parse_position_entry_price,
|
||||||
|
parse_position_mark_price,
|
||||||
|
parse_position_unrealized_pnl,
|
||||||
|
resolve_position_display_upnl,
|
||||||
|
)
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from fastapi import FastAPI, Header, HTTPException, Request
|
from fastapi import FastAPI, Header, HTTPException, Request
|
||||||
@@ -388,25 +393,16 @@ def _position_price_fmt(ex: Any, symbol: str, price: float | None) -> tuple[floa
|
|||||||
|
|
||||||
def _position_entry_price(p: dict[str, Any]) -> float | None:
|
def _position_entry_price(p: dict[str, Any]) -> float | None:
|
||||||
"""四所 ccxt 持仓统一解析开仓均价(Binance/OKX/Gate 字段名不一致)。"""
|
"""四所 ccxt 持仓统一解析开仓均价(Binance/OKX/Gate 字段名不一致)。"""
|
||||||
info = p.get("info") or {}
|
return parse_position_entry_price(p)
|
||||||
if not isinstance(info, dict):
|
|
||||||
info = {}
|
|
||||||
for key in (
|
def _position_contract_size(ex: Any, symbol: str) -> float:
|
||||||
p.get("entryPrice"),
|
try:
|
||||||
p.get("entry_price"),
|
market = ex.market((symbol or "").strip())
|
||||||
p.get("average"),
|
cs = float(market.get("contractSize") or 1)
|
||||||
info.get("entryPrice"),
|
return cs if cs > 0 else 1.0
|
||||||
info.get("entry_price"),
|
except Exception:
|
||||||
info.get("avgPx"),
|
return 1.0
|
||||||
info.get("avgEntryPrice"),
|
|
||||||
info.get("avg_entry_price"),
|
|
||||||
info.get("avgPrice"),
|
|
||||||
info.get("openAvgPx"),
|
|
||||||
):
|
|
||||||
px = _finite_or_none(key)
|
|
||||||
if px is not None and px > 0:
|
|
||||||
return px
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _position_mark_price(p: dict[str, Any]) -> float | None:
|
def _position_mark_price(p: dict[str, Any]) -> float | None:
|
||||||
@@ -602,7 +598,20 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
continue
|
continue
|
||||||
sym = p.get("symbol") or ""
|
sym = p.get("symbol") or ""
|
||||||
side = _position_side(p, c)
|
side = _position_side(p, c)
|
||||||
upnl_f = parse_position_unrealized_pnl(p)
|
entry_f = _position_entry_price(p)
|
||||||
|
mark_f = _position_mark_price(p)
|
||||||
|
if mark_f is None and sym:
|
||||||
|
mark_f = _ticker_mark_price(ex, sym)
|
||||||
|
cs = _position_contract_size(ex, sym) if sym else 1.0
|
||||||
|
exchange_upnl = parse_position_unrealized_pnl(p)
|
||||||
|
upnl_f = resolve_position_display_upnl(
|
||||||
|
side,
|
||||||
|
entry_f,
|
||||||
|
mark_f,
|
||||||
|
abs(c),
|
||||||
|
cs,
|
||||||
|
exchange_upnl,
|
||||||
|
)
|
||||||
if upnl_f is None:
|
if upnl_f is None:
|
||||||
upnl_f = 0.0
|
upnl_f = 0.0
|
||||||
total_upnl += upnl_f
|
total_upnl += upnl_f
|
||||||
@@ -611,11 +620,7 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
notional_f = float(notional) if notional is not None else None
|
notional_f = float(notional) if notional is not None else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
notional_f = None
|
notional_f = None
|
||||||
entry_f = _position_entry_price(p)
|
|
||||||
_, entry_fmt, price_tick = _position_price_fmt(ex, sym, entry_f)
|
_, entry_fmt, price_tick = _position_price_fmt(ex, sym, entry_f)
|
||||||
mark_f = _position_mark_price(p)
|
|
||||||
if mark_f is None and sym:
|
|
||||||
mark_f = _ticker_mark_price(ex, sym)
|
|
||||||
_, mark_fmt, mark_tick = _position_price_fmt(ex, sym, mark_f)
|
_, mark_fmt, mark_tick = _position_price_fmt(ex, sym, mark_f)
|
||||||
if price_tick is None and mark_tick is not None:
|
if price_tick is None and mark_tick is not None:
|
||||||
price_tick = mark_tick
|
price_tick = mark_tick
|
||||||
@@ -631,6 +636,7 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
"entry_price_fmt": entry_fmt,
|
"entry_price_fmt": entry_fmt,
|
||||||
"mark_price": mark_f,
|
"mark_price": mark_f,
|
||||||
"mark_price_fmt": mark_fmt,
|
"mark_price_fmt": mark_fmt,
|
||||||
|
"contract_size": _finite_or_none(cs),
|
||||||
"price_tick": _finite_or_none(price_tick) if price_tick is not None else None,
|
"price_tick": _finite_or_none(price_tick) if price_tick is not None else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -336,15 +336,73 @@
|
|||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTrendFloatingPnl(pos, trendPlan) {
|
function estimateLinearSwapUpnl(side, entry, mark, contracts, contractSize) {
|
||||||
let upnl = pos && pos.unrealized_pnl;
|
const e = Number(entry);
|
||||||
if (upnl != null && upnl !== "" && Number.isFinite(Number(upnl))) return Number(upnl);
|
const m = Number(mark);
|
||||||
const t = trendPlan || {};
|
const c = Math.abs(Number(contracts));
|
||||||
if (t.floating_pnl != null && t.floating_pnl !== "") {
|
let mult = Number(contractSize);
|
||||||
const n = Number(t.floating_pnl);
|
if (!Number.isFinite(mult) || mult <= 0) mult = 1;
|
||||||
if (Number.isFinite(n)) return n;
|
if (!Number.isFinite(e) || !Number.isFinite(m) || !Number.isFinite(c) || c <= 0) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
const diff =
|
||||||
|
(side || "long").toLowerCase() === "long" ? m - e : e - m;
|
||||||
|
return Math.round(diff * c * mult * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展示浮盈:子代理 unrealized_pnl;与 entry/mark/张数 推算偏差 >20% 时用推算值 */
|
||||||
|
function resolvePositionUpnlUsdt(pos, trendPlan, markOverride) {
|
||||||
|
const p = pos || {};
|
||||||
|
const t = trendPlan || {};
|
||||||
|
let exchange =
|
||||||
|
p.unrealized_pnl != null && p.unrealized_pnl !== ""
|
||||||
|
? Number(p.unrealized_pnl)
|
||||||
|
: null;
|
||||||
|
if (exchange != null && !Number.isFinite(exchange)) exchange = null;
|
||||||
|
const entry =
|
||||||
|
p.entry_price != null && p.entry_price !== ""
|
||||||
|
? Number(p.entry_price)
|
||||||
|
: t.trigger_price != null
|
||||||
|
? Number(t.trigger_price)
|
||||||
|
: null;
|
||||||
|
let mark =
|
||||||
|
markOverride != null && Number.isFinite(Number(markOverride))
|
||||||
|
? Number(markOverride)
|
||||||
|
: p.mark_price != null && p.mark_price !== ""
|
||||||
|
? Number(p.mark_price)
|
||||||
|
: t.floating_mark != null
|
||||||
|
? Number(t.floating_mark)
|
||||||
|
: t.last_mark_price != null
|
||||||
|
? Number(t.last_mark_price)
|
||||||
|
: null;
|
||||||
|
const contracts = p.contracts;
|
||||||
|
const cs =
|
||||||
|
p.contract_size != null && p.contract_size !== ""
|
||||||
|
? Number(p.contract_size)
|
||||||
|
: 1;
|
||||||
|
const computed = estimateLinearSwapUpnl(
|
||||||
|
p.side || t.direction,
|
||||||
|
entry,
|
||||||
|
mark,
|
||||||
|
contracts,
|
||||||
|
cs
|
||||||
|
);
|
||||||
|
if (computed == null) {
|
||||||
|
if (exchange != null) return exchange;
|
||||||
|
if (t.floating_pnl != null && t.floating_pnl !== "") {
|
||||||
|
const n = Number(t.floating_pnl);
|
||||||
|
if (Number.isFinite(n)) return n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (exchange == null) return computed;
|
||||||
|
const ref = Math.max(Math.abs(computed), 1);
|
||||||
|
if (Math.abs(exchange - computed) / ref > 0.2) return computed;
|
||||||
|
return exchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTrendFloatingPnl(pos, trendPlan, markOverride) {
|
||||||
|
return resolvePositionUpnlUsdt(pos, trendPlan, markOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFloatingPnlText(upnl, notionalUsdt) {
|
function formatFloatingPnlText(upnl, notionalUsdt) {
|
||||||
@@ -1393,7 +1451,10 @@
|
|||||||
amount: num(o.amount),
|
amount: num(o.amount),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const upnl = resolveTrendFloatingPnl(pos, trendPlan);
|
const entryPx = num(pos.entry_price != null ? pos.entry_price : tpsl.entry);
|
||||||
|
const markPx = num(pos.mark_price);
|
||||||
|
const contractSize = num(pos.contract_size);
|
||||||
|
const upnl = resolvePositionUpnlUsdt(pos, trendPlan, markPx);
|
||||||
const planMargin =
|
const planMargin =
|
||||||
trendPlan && trendPlan.plan_margin_capital != null
|
trendPlan && trendPlan.plan_margin_capital != null
|
||||||
? num(trendPlan.plan_margin_capital)
|
? num(trendPlan.plan_margin_capital)
|
||||||
@@ -1410,12 +1471,14 @@
|
|||||||
exchange_id: exchangeId || null,
|
exchange_id: exchangeId || null,
|
||||||
symbol: (pos.symbol || "").trim(),
|
symbol: (pos.symbol || "").trim(),
|
||||||
side: (pos.side || "long").toLowerCase(),
|
side: (pos.side || "long").toLowerCase(),
|
||||||
entry: num(tpsl.entry),
|
entry: entryPx,
|
||||||
|
mark_price: markPx,
|
||||||
stop_loss: num(tpsl.sl),
|
stop_loss: num(tpsl.sl),
|
||||||
take_profit: num(tpsl.tp),
|
take_profit: num(tpsl.tp),
|
||||||
tp_monitored: !!tpsl.tp_monitored,
|
tp_monitored: !!tpsl.tp_monitored,
|
||||||
is_trend: !!tpsl.is_trend,
|
is_trend: !!tpsl.is_trend,
|
||||||
contracts: num(pos.contracts),
|
contracts: num(pos.contracts),
|
||||||
|
contract_size: contractSize != null ? contractSize : 1,
|
||||||
unrealized_pnl: upnl != null ? Number(upnl) : null,
|
unrealized_pnl: upnl != null ? Number(upnl) : null,
|
||||||
notional_usdt: num(pos.notional_usdt),
|
notional_usdt: num(pos.notional_usdt),
|
||||||
plan_margin: planMargin,
|
plan_margin: planMargin,
|
||||||
|
|||||||
@@ -902,18 +902,48 @@
|
|||||||
}, 3500);
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function estimateLinearSwapUpnl(side, entry, mark, contracts, contractSize) {
|
||||||
|
const e = Number(entry);
|
||||||
|
const m = Number(mark);
|
||||||
|
const c = Math.abs(Number(contracts));
|
||||||
|
let mult = Number(contractSize);
|
||||||
|
if (!Number.isFinite(mult) || mult <= 0) mult = 1;
|
||||||
|
if (!Number.isFinite(e) || !Number.isFinite(m) || !Number.isFinite(c) || c <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const diff =
|
||||||
|
(side || "long").toLowerCase() === "long" ? m - e : e - m;
|
||||||
|
return Math.round(diff * c * mult * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
function formatPosPnlText(ctx) {
|
function formatPosPnlText(ctx) {
|
||||||
const upnl = ctx && ctx.unrealized_pnl;
|
const upnl = ctx && ctx.unrealized_pnl;
|
||||||
if (upnl == null || !Number.isFinite(Number(upnl))) return { text: "—", cls: "" };
|
if (upnl == null || !Number.isFinite(Number(upnl))) return { text: "—", cls: "" };
|
||||||
const n = Number(upnl);
|
const n = Number(upnl);
|
||||||
let text = (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
|
let text = (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
|
||||||
const margin = ctx.plan_margin;
|
|
||||||
const notional = ctx.notional_usdt;
|
const notional = ctx.notional_usdt;
|
||||||
if (margin != null && Number(margin) > 1e-8) {
|
const entry = Number(ctx.entry);
|
||||||
const pct = (n / Number(margin)) * 100;
|
const contracts = Math.abs(Number(ctx.contracts));
|
||||||
|
const cs =
|
||||||
|
ctx.contract_size != null && Number(ctx.contract_size) > 0
|
||||||
|
? Number(ctx.contract_size)
|
||||||
|
: 1;
|
||||||
|
let pctBase = null;
|
||||||
|
if (notional != null && Math.abs(Number(notional)) > 1e-8) {
|
||||||
|
pctBase = Math.abs(Number(notional));
|
||||||
|
} else if (
|
||||||
|
Number.isFinite(entry) &&
|
||||||
|
entry > 0 &&
|
||||||
|
Number.isFinite(contracts) &&
|
||||||
|
contracts > 0
|
||||||
|
) {
|
||||||
|
pctBase = entry * contracts * cs;
|
||||||
|
}
|
||||||
|
if (pctBase != null && pctBase > 1e-8) {
|
||||||
|
const pct = (n / pctBase) * 100;
|
||||||
text += " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)";
|
text += " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)";
|
||||||
} else if (notional != null && Number(notional) > 1e-8) {
|
} else if (ctx.plan_margin != null && Number(ctx.plan_margin) > 1e-8) {
|
||||||
const pct = (n / Math.abs(Number(notional))) * 100;
|
const pct = (n / Number(ctx.plan_margin)) * 100;
|
||||||
text += " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)";
|
text += " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)";
|
||||||
}
|
}
|
||||||
return { text: text, cls: n > 0 ? "pnl-up" : n < 0 ? "pnl-down" : "" };
|
return { text: text, cls: n > 0 ? "pnl-up" : n < 0 ? "pnl-down" : "" };
|
||||||
@@ -963,20 +993,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 与四实例 calc_pnl 一致:保证金 × 杠杆 × 价格涨跌幅 */
|
/** U 本位线性永续:(标记价-开仓价)×张数×contractSize(空头取反) */
|
||||||
function calcPlanFloatingPnl(ctx, markPx) {
|
function calcContractsUpnl(ctx, markPx) {
|
||||||
if (!ctx || markPx == null || !Number.isFinite(Number(markPx))) return null;
|
if (!ctx || markPx == null || !Number.isFinite(Number(markPx))) return null;
|
||||||
const entry = Number(ctx.entry);
|
return estimateLinearSwapUpnl(
|
||||||
const margin = Number(ctx.plan_margin);
|
ctx.side,
|
||||||
const lev = Number(ctx.leverage);
|
ctx.entry,
|
||||||
if (!Number.isFinite(entry) || entry <= 0) return null;
|
markPx,
|
||||||
if (!Number.isFinite(margin) || margin <= 0) return null;
|
ctx.contracts,
|
||||||
if (!Number.isFinite(lev) || lev <= 0) return null;
|
ctx.contract_size
|
||||||
const mark = Number(markPx);
|
);
|
||||||
const side = (ctx.side || "long").toLowerCase();
|
|
||||||
const ratio =
|
|
||||||
side === "short" ? (entry - mark) / entry : (mark - entry) / entry;
|
|
||||||
return Math.round(margin * lev * ratio * 100) / 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function latestChartMarkPrice() {
|
function latestChartMarkPrice() {
|
||||||
@@ -996,12 +1022,22 @@
|
|||||||
? Number(posContext.mark_price)
|
? Number(posContext.mark_price)
|
||||||
: null);
|
: null);
|
||||||
if (mark == null) return false;
|
if (mark == null) return false;
|
||||||
const live = calcPlanFloatingPnl(posContext, mark);
|
const live = calcContractsUpnl(posContext, mark);
|
||||||
if (live == null) return false;
|
if (live != null) {
|
||||||
posContext.unrealized_pnl = live;
|
posContext.unrealized_pnl = live;
|
||||||
posContext.mark_price = mark;
|
posContext.mark_price = mark;
|
||||||
renderPosPnlDisplay(posContext);
|
renderPosPnlDisplay(posContext);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
posContext.unrealized_pnl != null &&
|
||||||
|
Number.isFinite(Number(posContext.unrealized_pnl))
|
||||||
|
) {
|
||||||
|
posContext.mark_price = mark;
|
||||||
|
renderPosPnlDisplay(posContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncPosTpslFromAgentPosition(p) {
|
function syncPosTpslFromAgentPosition(p) {
|
||||||
@@ -1080,6 +1116,15 @@
|
|||||||
const p = positions[j];
|
const p = positions[j];
|
||||||
if ((p.side || "").toLowerCase() !== side) continue;
|
if ((p.side || "").toLowerCase() !== side) continue;
|
||||||
if (normalizeMarketSymbol(p.symbol || "") !== sym) continue;
|
if (normalizeMarketSymbol(p.symbol || "") !== sym) continue;
|
||||||
|
if (p.entry_price != null && Number.isFinite(Number(p.entry_price))) {
|
||||||
|
posContext.entry = Number(p.entry_price);
|
||||||
|
}
|
||||||
|
if (p.contract_size != null && Number.isFinite(Number(p.contract_size))) {
|
||||||
|
posContext.contract_size = Number(p.contract_size);
|
||||||
|
}
|
||||||
|
if (p.contracts != null && Number.isFinite(Number(p.contracts))) {
|
||||||
|
posContext.contracts = Number(p.contracts);
|
||||||
|
}
|
||||||
if (p.mark_price != null && Number.isFinite(Number(p.mark_price))) {
|
if (p.mark_price != null && Number.isFinite(Number(p.mark_price))) {
|
||||||
posContext.mark_price = Number(p.mark_price);
|
posContext.mark_price = Number(p.mark_price);
|
||||||
}
|
}
|
||||||
@@ -1093,7 +1138,12 @@
|
|||||||
if (elPosTp && posContext.take_profit != null && !posContext.tp_monitored) {
|
if (elPosTp && posContext.take_profit != null && !posContext.tp_monitored) {
|
||||||
elPosTp.textContent = fmtPrice(posContext.take_profit);
|
elPosTp.textContent = fmtPrice(posContext.take_profit);
|
||||||
}
|
}
|
||||||
if (!updateLivePosPnl(p.mark_price)) {
|
const markForPnl =
|
||||||
|
latestChartMarkPrice() ||
|
||||||
|
(p.mark_price != null && Number.isFinite(Number(p.mark_price))
|
||||||
|
? Number(p.mark_price)
|
||||||
|
: null);
|
||||||
|
if (!updateLivePosPnl(markForPnl)) {
|
||||||
let upnl =
|
let upnl =
|
||||||
p.unrealized_pnl != null && Number.isFinite(Number(p.unrealized_pnl))
|
p.unrealized_pnl != null && Number.isFinite(Number(p.unrealized_pnl))
|
||||||
? Number(p.unrealized_pnl)
|
? Number(p.unrealized_pnl)
|
||||||
|
|||||||
@@ -249,7 +249,7 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260604-market-pnl-calc"></script>
|
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
|
||||||
<script src="/assets/app.js?v=20260604-mo-undef-fix"></script>
|
<script src="/assets/app.js?v=20260604-upnl-contracts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ sys.path.insert(0, str(ROOT / "manual_trading_hub"))
|
|||||||
from agent import _position_mark_price, _ticker_mark_price # noqa: E402
|
from agent import _position_mark_price, _ticker_mark_price # noqa: E402
|
||||||
|
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
from hub_position_metrics import parse_position_unrealized_pnl # noqa: E402
|
from hub_position_metrics import ( # noqa: E402
|
||||||
|
estimate_linear_swap_upnl_usdt,
|
||||||
|
parse_position_unrealized_pnl,
|
||||||
|
resolve_position_display_upnl,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestHubAgentMarkPrice(unittest.TestCase):
|
class TestHubAgentMarkPrice(unittest.TestCase):
|
||||||
@@ -48,6 +52,24 @@ class TestHubAgentMarkPrice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertAlmostEqual(pnl, 6.81)
|
self.assertAlmostEqual(pnl, 6.81)
|
||||||
|
|
||||||
|
def test_estimate_short_hype_contract_size(self):
|
||||||
|
upnl = estimate_linear_swap_upnl_usdt(
|
||||||
|
"short", 73.187, 66.038, 11, 0.1
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(upnl, 7.86, places=1)
|
||||||
|
|
||||||
|
def test_resolve_prefers_computed_when_exchange_off(self):
|
||||||
|
shown = resolve_position_display_upnl(
|
||||||
|
"short", 73.187, 66.038, 11, 1.0, 7.86
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(shown, 78.64, places=1)
|
||||||
|
|
||||||
|
def test_resolve_keeps_exchange_when_aligned(self):
|
||||||
|
shown = resolve_position_display_upnl(
|
||||||
|
"short", 73.187, 66.038, 11, 0.1, 7.86
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(shown, 7.86, places=2)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user