fix: show header realtime PnL from exchange positions and order metrics
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7390,6 +7390,9 @@ def api_account_snapshot():
|
|||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
risk_status = hub_account_risk_status(conn)
|
risk_status = hub_account_risk_status(conn)
|
||||||
|
active_pnl_rows = conn.execute(
|
||||||
|
"SELECT exchange_symbol, symbol, direction FROM order_monitors WHERE status='active'"
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
can_trade = can_trade_new_open(
|
can_trade = can_trade_new_open(
|
||||||
time_allows=trading_day_reset_allows_new_open(now),
|
time_allows=trading_day_reset_allows_new_open(now),
|
||||||
@@ -7402,13 +7405,17 @@ def api_account_snapshot():
|
|||||||
available_trading_usdt = get_available_trading_usdt()
|
available_trading_usdt = get_available_trading_usdt()
|
||||||
unrealized_pnl = None
|
unrealized_pnl = None
|
||||||
if exchange_private_api_configured():
|
if exchange_private_api_configured():
|
||||||
from lib.instance.instance_live_pnl_lib import fetch_unrealized_pnl
|
from lib.instance.instance_live_pnl_lib import resolve_instance_unrealized_pnl
|
||||||
|
|
||||||
def _binance_positions():
|
def _binance_positions():
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
return exchange.fetch_positions() or []
|
return exchange.fetch_positions() or []
|
||||||
|
|
||||||
unrealized_pnl = fetch_unrealized_pnl(_binance_positions)
|
unrealized_pnl = resolve_instance_unrealized_pnl(
|
||||||
|
_binance_positions,
|
||||||
|
active_pnl_rows,
|
||||||
|
get_live_position_exchange_metrics,
|
||||||
|
)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"funding_usdt": funding_usdt,
|
"funding_usdt": funding_usdt,
|
||||||
"current_capital": current_capital,
|
"current_capital": current_capital,
|
||||||
|
|||||||
@@ -7205,6 +7205,9 @@ def api_account_snapshot():
|
|||||||
position_limit_count = count_position_limit_active_monitors(conn)
|
position_limit_count = count_position_limit_active_monitors(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
risk_status = hub_account_risk_status(conn)
|
risk_status = hub_account_risk_status(conn)
|
||||||
|
active_pnl_rows = conn.execute(
|
||||||
|
"SELECT exchange_symbol, symbol, direction FROM order_monitors WHERE status='active'"
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
can_trade = can_trade_new_open(
|
can_trade = can_trade_new_open(
|
||||||
time_allows=trading_day_reset_allows_new_open(now),
|
time_allows=trading_day_reset_allows_new_open(now),
|
||||||
@@ -7217,7 +7220,7 @@ def api_account_snapshot():
|
|||||||
available_trading_usdt = get_available_trading_usdt()
|
available_trading_usdt = get_available_trading_usdt()
|
||||||
unrealized_pnl = None
|
unrealized_pnl = None
|
||||||
if exchange_private_api_configured():
|
if exchange_private_api_configured():
|
||||||
from lib.instance.instance_live_pnl_lib import fetch_unrealized_pnl
|
from lib.instance.instance_live_pnl_lib import resolve_instance_unrealized_pnl
|
||||||
|
|
||||||
def _gate_positions():
|
def _gate_positions():
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -7226,7 +7229,11 @@ def api_account_snapshot():
|
|||||||
except Exception:
|
except Exception:
|
||||||
return exchange.fetch_positions() or []
|
return exchange.fetch_positions() or []
|
||||||
|
|
||||||
unrealized_pnl = fetch_unrealized_pnl(_gate_positions)
|
unrealized_pnl = resolve_instance_unrealized_pnl(
|
||||||
|
_gate_positions,
|
||||||
|
active_pnl_rows,
|
||||||
|
get_live_position_exchange_metrics,
|
||||||
|
)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"funding_usdt": funding_usdt,
|
"funding_usdt": funding_usdt,
|
||||||
"current_capital": current_capital,
|
"current_capital": current_capital,
|
||||||
|
|||||||
@@ -6750,6 +6750,9 @@ def api_account_snapshot():
|
|||||||
open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
|
open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
|
||||||
opens_today = count_opens_for_trading_day(conn, trading_day)
|
opens_today = count_opens_for_trading_day(conn, trading_day)
|
||||||
risk_status = hub_account_risk_status(conn)
|
risk_status = hub_account_risk_status(conn)
|
||||||
|
active_pnl_rows = conn.execute(
|
||||||
|
"SELECT exchange_symbol, symbol, direction FROM order_monitors WHERE status='active'"
|
||||||
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
open_guard_blocks_now = open_guard_enabled and now.hour < TRADING_DAY_RESET_HOUR
|
open_guard_blocks_now = open_guard_enabled and now.hour < TRADING_DAY_RESET_HOUR
|
||||||
can_trade = can_trade_new_open(
|
can_trade = can_trade_new_open(
|
||||||
@@ -6763,7 +6766,7 @@ def api_account_snapshot():
|
|||||||
available_trading_usdt = get_available_trading_usdt()
|
available_trading_usdt = get_available_trading_usdt()
|
||||||
unrealized_pnl = None
|
unrealized_pnl = None
|
||||||
if exchange_private_api_configured():
|
if exchange_private_api_configured():
|
||||||
from lib.instance.instance_live_pnl_lib import fetch_unrealized_pnl
|
from lib.instance.instance_live_pnl_lib import resolve_instance_unrealized_pnl
|
||||||
|
|
||||||
def _okx_positions():
|
def _okx_positions():
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -6772,7 +6775,11 @@ def api_account_snapshot():
|
|||||||
except Exception:
|
except Exception:
|
||||||
return exchange.fetch_positions() or []
|
return exchange.fetch_positions() or []
|
||||||
|
|
||||||
unrealized_pnl = fetch_unrealized_pnl(_okx_positions)
|
unrealized_pnl = resolve_instance_unrealized_pnl(
|
||||||
|
_okx_positions,
|
||||||
|
active_pnl_rows,
|
||||||
|
get_live_position_exchange_metrics,
|
||||||
|
)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"funding_usdt": funding_usdt,
|
"funding_usdt": funding_usdt,
|
||||||
"current_capital": current_capital,
|
"current_capital": current_capital,
|
||||||
|
|||||||
@@ -4,25 +4,91 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from lib.hub.hub_monitor_totals_lib import position_unrealized_pnl
|
from lib.hub.hub_position_metrics import parse_position_unrealized_pnl
|
||||||
|
|
||||||
|
|
||||||
def _position_contracts(pos: dict[str, Any]) -> float:
|
def position_row_contracts(pos: dict[str, Any]) -> float:
|
||||||
try:
|
"""持仓张数:与三所 app 内 _position_row_effective_contracts 规则一致。"""
|
||||||
return abs(float(pos.get("contracts") or 0))
|
if not isinstance(pos, dict):
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
info = pos.get("info") or {}
|
||||||
|
if not isinstance(info, dict):
|
||||||
|
info = {}
|
||||||
|
for val in (
|
||||||
|
pos.get("contracts"),
|
||||||
|
info.get("positionAmt"),
|
||||||
|
info.get("size"),
|
||||||
|
info.get("pos"),
|
||||||
|
info.get("availPos"),
|
||||||
|
):
|
||||||
|
if val is None or val == "":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
x = abs(float(val))
|
||||||
|
if x > 0:
|
||||||
|
return x
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
def sum_unrealized_pnl_from_positions(positions: list[dict[str, Any]] | None) -> float:
|
def sum_unrealized_pnl_from_positions(positions: list[dict[str, Any]] | None) -> float | None:
|
||||||
total = 0.0
|
total = 0.0
|
||||||
|
found = False
|
||||||
for p in positions or []:
|
for p in positions or []:
|
||||||
if not isinstance(p, dict):
|
if not isinstance(p, dict):
|
||||||
continue
|
continue
|
||||||
if _position_contracts(p) <= 1e-12:
|
if position_row_contracts(p) <= 1e-12:
|
||||||
continue
|
continue
|
||||||
total += position_unrealized_pnl(p)
|
upnl = parse_position_unrealized_pnl(p)
|
||||||
return round(total, 2)
|
if upnl is None:
|
||||||
|
continue
|
||||||
|
found = True
|
||||||
|
total += float(upnl)
|
||||||
|
return round(total, 2) if found else None
|
||||||
|
|
||||||
|
|
||||||
|
def _row_field(row: Any, key: str, default: str = "") -> str:
|
||||||
|
if row is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
if hasattr(row, "keys") and key in row.keys():
|
||||||
|
val = row[key]
|
||||||
|
elif isinstance(row, dict):
|
||||||
|
val = row.get(key)
|
||||||
|
else:
|
||||||
|
val = None
|
||||||
|
except Exception:
|
||||||
|
val = None
|
||||||
|
return str(val or default).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def sum_unrealized_pnl_from_metrics(
|
||||||
|
rows: list[dict[str, Any]] | list[Any],
|
||||||
|
get_metrics_fn: Callable[[str, str], dict[str, Any] | None],
|
||||||
|
) -> float | None:
|
||||||
|
"""按活跃监控单逐笔拉交易所 metrics 汇总(与持仓卡浮盈亏一致)。"""
|
||||||
|
total = 0.0
|
||||||
|
found = False
|
||||||
|
for row in rows or []:
|
||||||
|
ex_sym = _row_field(row, "exchange_symbol")
|
||||||
|
sym = _row_field(row, "symbol")
|
||||||
|
direction = _row_field(row, "direction", "long").lower() or "long"
|
||||||
|
target = ex_sym or sym
|
||||||
|
if not target:
|
||||||
|
continue
|
||||||
|
metrics = get_metrics_fn(target, direction)
|
||||||
|
if not isinstance(metrics, dict):
|
||||||
|
continue
|
||||||
|
upnl = metrics.get("unrealized_pnl")
|
||||||
|
if upnl is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
total += float(upnl)
|
||||||
|
found = True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return round(total, 2) if found else None
|
||||||
|
|
||||||
|
|
||||||
def fetch_unrealized_pnl(fetch_positions_fn: Callable[[], list[dict[str, Any]] | None]) -> float | None:
|
def fetch_unrealized_pnl(fetch_positions_fn: Callable[[], list[dict[str, Any]] | None]) -> float | None:
|
||||||
@@ -30,3 +96,17 @@ def fetch_unrealized_pnl(fetch_positions_fn: Callable[[], list[dict[str, Any]] |
|
|||||||
return sum_unrealized_pnl_from_positions(fetch_positions_fn() or [])
|
return sum_unrealized_pnl_from_positions(fetch_positions_fn() or [])
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_instance_unrealized_pnl(
|
||||||
|
fetch_positions_fn: Callable[[], list[dict[str, Any]] | None],
|
||||||
|
active_rows: list[Any] | None,
|
||||||
|
get_metrics_fn: Callable[[str, str], dict[str, Any] | None] | None,
|
||||||
|
) -> float | None:
|
||||||
|
"""先全量持仓汇总,失败或无数据时回退到活跃监控单 metrics。"""
|
||||||
|
total = fetch_unrealized_pnl(fetch_positions_fn)
|
||||||
|
if total is not None:
|
||||||
|
return total
|
||||||
|
if active_rows and get_metrics_fn:
|
||||||
|
return sum_unrealized_pnl_from_metrics(active_rows, get_metrics_fn)
|
||||||
|
return None
|
||||||
|
|||||||
@@ -1021,6 +1021,32 @@ function refreshOrderDefaults(){
|
|||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function paintRealtimePnl(v){
|
||||||
|
const pnlEl = document.getElementById("realtime-pnl");
|
||||||
|
if(!pnlEl) return;
|
||||||
|
if(v === null || v === undefined || Number.isNaN(Number(v))){
|
||||||
|
pnlEl.innerText = "—";
|
||||||
|
pnlEl.classList.remove("pnl-pos", "pnl-neg");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(v);
|
||||||
|
const sign = n > 0 ? "+" : "";
|
||||||
|
pnlEl.innerText = `${sign}${n.toFixed(2)}U`;
|
||||||
|
pnlEl.classList.toggle("pnl-pos", n > 0);
|
||||||
|
pnlEl.classList.toggle("pnl-neg", n < 0);
|
||||||
|
}
|
||||||
|
function sumOrdersFloatPnl(orders){
|
||||||
|
if(!orders || !orders.length) return null;
|
||||||
|
let total = 0, found = false;
|
||||||
|
orders.forEach(o=>{
|
||||||
|
if(o.float_pnl != null && !Number.isNaN(Number(o.float_pnl))){
|
||||||
|
total += Number(o.float_pnl);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found ? total : null;
|
||||||
|
}
|
||||||
|
|
||||||
function refreshAccountSnapshot(){
|
function refreshAccountSnapshot(){
|
||||||
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
|
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
|
||||||
if (typeof data.funding_usdt !== "undefined") {
|
if (typeof data.funding_usdt !== "undefined") {
|
||||||
@@ -1032,19 +1058,7 @@ function refreshAccountSnapshot(){
|
|||||||
if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`;
|
if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`;
|
||||||
}
|
}
|
||||||
if (typeof data.unrealized_pnl !== "undefined") {
|
if (typeof data.unrealized_pnl !== "undefined") {
|
||||||
const pnlEl = document.getElementById("realtime-pnl");
|
paintRealtimePnl(data.unrealized_pnl);
|
||||||
if (pnlEl) {
|
|
||||||
if (data.unrealized_pnl === null || data.unrealized_pnl === undefined) {
|
|
||||||
pnlEl.innerText = "—";
|
|
||||||
pnlEl.classList.remove("pnl-pos", "pnl-neg");
|
|
||||||
} else {
|
|
||||||
const v = Number(data.unrealized_pnl);
|
|
||||||
const sign = v > 0 ? "+" : "";
|
|
||||||
pnlEl.innerText = `${sign}${v.toFixed(2)}U`;
|
|
||||||
pnlEl.classList.toggle("pnl-pos", v > 0);
|
|
||||||
pnlEl.classList.toggle("pnl-neg", v < 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
|
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
|
||||||
latestAvailableUsdt = Number(data.available_trading_usdt);
|
latestAvailableUsdt = Number(data.available_trading_usdt);
|
||||||
@@ -1281,6 +1295,9 @@ function refreshPriceSnapshotConditional(){
|
|||||||
});
|
});
|
||||||
tickOrderHoldDurations();
|
tickOrderHoldDurations();
|
||||||
}
|
}
|
||||||
|
if(data.order_prices && data.order_prices.length){
|
||||||
|
paintRealtimePnl(sumOrdersFloatPnl(data.order_prices));
|
||||||
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
||||||
|
|||||||
@@ -1567,6 +1567,32 @@ function refreshOrderDefaults(){
|
|||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function paintRealtimePnl(v){
|
||||||
|
const pnlEl = document.getElementById("realtime-pnl");
|
||||||
|
if(!pnlEl) return;
|
||||||
|
if(v === null || v === undefined || Number.isNaN(Number(v))){
|
||||||
|
pnlEl.innerText = "—";
|
||||||
|
pnlEl.classList.remove("pnl-pos", "pnl-neg");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(v);
|
||||||
|
const sign = n > 0 ? "+" : "";
|
||||||
|
pnlEl.innerText = `${sign}${n.toFixed(2)}U`;
|
||||||
|
pnlEl.classList.toggle("pnl-pos", n > 0);
|
||||||
|
pnlEl.classList.toggle("pnl-neg", n < 0);
|
||||||
|
}
|
||||||
|
function sumOrdersFloatPnl(orders){
|
||||||
|
if(!orders || !orders.length) return null;
|
||||||
|
let total = 0, found = false;
|
||||||
|
orders.forEach(o=>{
|
||||||
|
if(o.float_pnl != null && !Number.isNaN(Number(o.float_pnl))){
|
||||||
|
total += Number(o.float_pnl);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found ? total : null;
|
||||||
|
}
|
||||||
|
|
||||||
function refreshAccountSnapshot(){
|
function refreshAccountSnapshot(){
|
||||||
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
|
fetch("/api/account_snapshot").then(r=>r.json()).then(data=>{
|
||||||
if (typeof data.funding_usdt !== "undefined") {
|
if (typeof data.funding_usdt !== "undefined") {
|
||||||
@@ -1578,19 +1604,7 @@ function refreshAccountSnapshot(){
|
|||||||
if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`;
|
if(el) el.innerText = `${Number(data.current_capital).toFixed(2)}U`;
|
||||||
}
|
}
|
||||||
if (typeof data.unrealized_pnl !== "undefined") {
|
if (typeof data.unrealized_pnl !== "undefined") {
|
||||||
const pnlEl = document.getElementById("realtime-pnl");
|
paintRealtimePnl(data.unrealized_pnl);
|
||||||
if (pnlEl) {
|
|
||||||
if (data.unrealized_pnl === null || data.unrealized_pnl === undefined) {
|
|
||||||
pnlEl.innerText = "—";
|
|
||||||
pnlEl.classList.remove("pnl-pos", "pnl-neg");
|
|
||||||
} else {
|
|
||||||
const v = Number(data.unrealized_pnl);
|
|
||||||
const sign = v > 0 ? "+" : "";
|
|
||||||
pnlEl.innerText = `${sign}${v.toFixed(2)}U`;
|
|
||||||
pnlEl.classList.toggle("pnl-pos", v > 0);
|
|
||||||
pnlEl.classList.toggle("pnl-neg", v < 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
|
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
|
||||||
latestAvailableUsdt = Number(data.available_trading_usdt);
|
latestAvailableUsdt = Number(data.available_trading_usdt);
|
||||||
@@ -1870,6 +1884,9 @@ function refreshPriceSnapshotConditional(){
|
|||||||
renderOrphanRecoverBanner(data.orphan_live_positions);
|
renderOrphanRecoverBanner(data.orphan_live_positions);
|
||||||
{% endif %}
|
{% endif %}
|
||||||
}
|
}
|
||||||
|
if(data.order_prices && data.order_prices.length){
|
||||||
|
paintRealtimePnl(sumOrdersFloatPnl(data.order_prices));
|
||||||
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
function formatLiveHoldDurationFromMs(openedMs, nowMs){
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""instance_live_pnl_lib 单元测试。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from lib.instance.instance_live_pnl_lib import (
|
||||||
|
position_row_contracts,
|
||||||
|
resolve_instance_unrealized_pnl,
|
||||||
|
sum_unrealized_pnl_from_metrics,
|
||||||
|
sum_unrealized_pnl_from_positions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstanceLivePnlLib(unittest.TestCase):
|
||||||
|
def test_position_row_contracts_from_info(self):
|
||||||
|
pos = {"contracts": 0, "info": {"positionAmt": "12.5"}}
|
||||||
|
self.assertAlmostEqual(position_row_contracts(pos), 12.5)
|
||||||
|
|
||||||
|
def test_sum_from_positions_binance_style(self):
|
||||||
|
positions = [
|
||||||
|
{"unrealizedPnl": -0.14, "info": {"positionAmt": "100"}},
|
||||||
|
]
|
||||||
|
self.assertEqual(sum_unrealized_pnl_from_positions(positions), -0.14)
|
||||||
|
|
||||||
|
def test_sum_from_metrics_fallback(self):
|
||||||
|
rows = [{"exchange_symbol": "DOGE/USDT:USDT", "symbol": "DOGE/USDT", "direction": "long"}]
|
||||||
|
|
||||||
|
def _metrics(ex_sym, direction):
|
||||||
|
self.assertEqual(direction, "long")
|
||||||
|
return {"unrealized_pnl": -0.14}
|
||||||
|
|
||||||
|
self.assertEqual(sum_unrealized_pnl_from_metrics(rows, _metrics), -0.14)
|
||||||
|
|
||||||
|
def test_resolve_prefers_bulk_positions(self):
|
||||||
|
def _fetch():
|
||||||
|
return [{"unrealizedPnl": 1.2, "contracts": 1}]
|
||||||
|
|
||||||
|
def _metrics(_ex, _d):
|
||||||
|
raise AssertionError("should not call metrics when bulk works")
|
||||||
|
|
||||||
|
total = resolve_instance_unrealized_pnl(_fetch, [], _metrics)
|
||||||
|
self.assertEqual(total, 1.2)
|
||||||
|
|
||||||
|
def test_resolve_falls_back_to_metrics(self):
|
||||||
|
def _fetch():
|
||||||
|
raise RuntimeError("api down")
|
||||||
|
|
||||||
|
rows = [{"exchange_symbol": "BTC/USDT:USDT", "symbol": "BTC/USDT", "direction": "short"}]
|
||||||
|
|
||||||
|
def _metrics(_ex, direction):
|
||||||
|
return {"unrealized_pnl": -2.5} if direction == "short" else None
|
||||||
|
|
||||||
|
total = resolve_instance_unrealized_pnl(_fetch, rows, _metrics)
|
||||||
|
self.assertEqual(total, -2.5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user