Fix hub total floating PnL by excluding OKX options from swap agent.
Option legs were scored with linear swap math and then added again from the options snapshot, inflating 总浮盈亏 and 持有仓位. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from lib.hub.hub_options_funds_lib import (
|
||||
options_float_pnl_usdt,
|
||||
options_open_position_count as count_options_positions,
|
||||
)
|
||||
from lib.hub.hub_position_metrics import is_option_like_position
|
||||
|
||||
|
||||
def _coerce_float(value: Any) -> float | None:
|
||||
@@ -27,6 +28,27 @@ def position_unrealized_pnl(pos: dict[str, Any]) -> float:
|
||||
|
||||
|
||||
def _open_positions(agent: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
if not isinstance(agent, dict):
|
||||
return []
|
||||
positions = agent.get("positions")
|
||||
if not isinstance(positions, list):
|
||||
return []
|
||||
out: list[dict[str, Any]] = []
|
||||
for p in positions:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if is_option_like_position(p):
|
||||
continue
|
||||
try:
|
||||
c = abs(float(p.get("contracts") or 0))
|
||||
except (TypeError, ValueError):
|
||||
c = 0.0
|
||||
if c > 1e-12:
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def _raw_open_positions(agent: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
if not isinstance(agent, dict):
|
||||
return []
|
||||
positions = agent.get("positions")
|
||||
@@ -79,8 +101,11 @@ def aggregate_monitor_board_totals(
|
||||
ag = row.get("agent") if isinstance(row.get("agent"), dict) else {}
|
||||
open_pos = _open_positions(ag)
|
||||
open_position_count += len(open_pos)
|
||||
raw_pos = _raw_open_positions(ag)
|
||||
contaminated = any(is_option_like_position(p) for p in raw_pos)
|
||||
agent_upnl = _coerce_float(ag.get("total_unrealized_pnl"))
|
||||
if agent_upnl is not None:
|
||||
# 子代理若把期权混进永续合计,改按过滤后腿求和;期权浮盈由下方 options 段计入
|
||||
if agent_upnl is not None and not contaminated:
|
||||
float_pnl_u += agent_upnl
|
||||
else:
|
||||
float_pnl_u += sum(position_unrealized_pnl(p) for p in open_pos)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
@@ -23,6 +24,45 @@ def _coerce_float(*values: Any) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
# OKX ccxt: ETH/USD:USD-260806-1875-C ; instId: ETH-USD-260806-1875-C
|
||||
_OPTION_SYM_RE = re.compile(
|
||||
r"(?:^|[/:])[A-Z0-9]+(?:-USD)?(?::USD)?-\d{6}-\d+-(?:C|P|CALL|PUT)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def is_option_like_position(pos: dict[str, Any] | None) -> bool:
|
||||
"""识别期权仓(子代理/中控浮盈合计须排除,避免按永续线性公式误算)."""
|
||||
if not isinstance(pos, dict):
|
||||
return False
|
||||
info = pos.get("info") if isinstance(pos.get("info"), dict) else {}
|
||||
inst_type = str(
|
||||
info.get("instType")
|
||||
or info.get("inst_type")
|
||||
or pos.get("type")
|
||||
or ""
|
||||
).upper()
|
||||
if inst_type in ("OPTION", "OPT"):
|
||||
return True
|
||||
sym = str(
|
||||
pos.get("symbol")
|
||||
or info.get("instId")
|
||||
or info.get("instrument_name")
|
||||
or info.get("contract")
|
||||
or ""
|
||||
).strip()
|
||||
if not sym:
|
||||
return False
|
||||
if _OPTION_SYM_RE.search(sym.replace(" ", "")):
|
||||
return True
|
||||
su = sym.upper()
|
||||
if su.endswith("-C") or su.endswith("-P") or su.endswith("-CALL") or su.endswith("-PUT"):
|
||||
# 永续多为 BTC/USDT:USDT;期权常带到期日段
|
||||
if re.search(r"-\d{6}-\d+-(?:C|P|CALL|PUT)$", su):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
CONTRACTS_QTY_DECIMALS = 2
|
||||
|
||||
|
||||
|
||||
@@ -578,7 +578,14 @@ def _status_inner(x_control_token: str | None) -> Any:
|
||||
positions_out: list[dict[str, Any]] = []
|
||||
total_upnl = 0.0
|
||||
try:
|
||||
raw = ex.fetch_positions() or []
|
||||
# OKX 统一账户 fetch_positions 可能混入期权;优先只要永续,再二次过滤
|
||||
if EXCHANGE_KIND == "okx":
|
||||
try:
|
||||
raw = ex.fetch_positions(params={"instType": "SWAP"}) or []
|
||||
except Exception:
|
||||
raw = ex.fetch_positions() or []
|
||||
else:
|
||||
raw = ex.fetch_positions() or []
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
{
|
||||
@@ -592,9 +599,13 @@ def _status_inner(x_control_token: str | None) -> Any:
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
from lib.hub.hub_position_metrics import is_option_like_position
|
||||
|
||||
for p in raw:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if is_option_like_position(p):
|
||||
continue
|
||||
c = _position_contracts(p)
|
||||
if abs(c) < 1e-12:
|
||||
continue
|
||||
@@ -807,13 +818,23 @@ def emergency_close_all(x_control_token: str | None = Header(default=None, alias
|
||||
closed: list[dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
raw = ex.fetch_positions() or []
|
||||
if EXCHANGE_KIND == "okx":
|
||||
try:
|
||||
raw = ex.fetch_positions(params={"instType": "SWAP"}) or []
|
||||
except Exception:
|
||||
raw = ex.fetch_positions() or []
|
||||
else:
|
||||
raw = ex.fetch_positions() or []
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=f"fetch_positions: {e}") from e
|
||||
|
||||
from lib.hub.hub_position_metrics import is_option_like_position
|
||||
|
||||
for p in raw:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
if is_option_like_position(p):
|
||||
continue
|
||||
c = _position_contracts(p)
|
||||
if abs(c) < 1e-12:
|
||||
continue
|
||||
|
||||
@@ -14,6 +14,7 @@ sys.path.insert(0, str(ROOT))
|
||||
from lib.hub.hub_position_metrics import ( # noqa: E402
|
||||
enrich_ccxt_position_metrics_out,
|
||||
estimate_linear_swap_upnl_usdt,
|
||||
is_option_like_position,
|
||||
parse_position_unrealized_pnl,
|
||||
resolve_position_display_upnl,
|
||||
)
|
||||
@@ -89,6 +90,20 @@ class TestHubAgentMarkPrice(unittest.TestCase):
|
||||
)
|
||||
self.assertAlmostEqual(shown, 7.86, places=2)
|
||||
|
||||
def test_is_option_like_position(self):
|
||||
self.assertTrue(
|
||||
is_option_like_position({"symbol": "ETH/USD:USD-260806-1875-C", "contracts": 1})
|
||||
)
|
||||
self.assertTrue(
|
||||
is_option_like_position(
|
||||
{"symbol": "x", "info": {"instType": "OPTION", "instId": "ETH-USD-260806-1875-P"}}
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
is_option_like_position({"symbol": "BTC/USDT:USDT", "contracts": 10})
|
||||
)
|
||||
self.assertFalse(is_option_like_position({"symbol": "ETH/USDT:USDT"}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,75 +1,114 @@
|
||||
"""中控监控区今日统计聚合."""
|
||||
import unittest
|
||||
|
||||
from lib.hub.hub_monitor_totals_lib import aggregate_monitor_board_totals
|
||||
from lib.hub.hub_trades_lib import summarize_trades
|
||||
|
||||
|
||||
def test_aggregate_monitor_board_totals_sums_rows():
|
||||
rows = [
|
||||
{
|
||||
"day_stats": {
|
||||
"ok": True,
|
||||
"opens_today": 2,
|
||||
"trade_stats": {
|
||||
"closed_count": 1,
|
||||
"win_count": 1,
|
||||
"loss_count": 0,
|
||||
"win_pnl_u": 5.5,
|
||||
"loss_pnl_u": 0,
|
||||
class TestHubMonitorTotals(unittest.TestCase):
|
||||
def test_aggregate_monitor_board_totals_sums_rows(self):
|
||||
rows = [
|
||||
{
|
||||
"day_stats": {
|
||||
"ok": True,
|
||||
"opens_today": 2,
|
||||
"trade_stats": {
|
||||
"closed_count": 1,
|
||||
"win_count": 1,
|
||||
"loss_count": 0,
|
||||
"win_pnl_u": 5.5,
|
||||
"loss_pnl_u": 0,
|
||||
},
|
||||
},
|
||||
"agent": {"positions": [{"contracts": 1}], "total_unrealized_pnl": 1.2},
|
||||
},
|
||||
"agent": {"positions": [{"contracts": 1}], "total_unrealized_pnl": 1.2},
|
||||
},
|
||||
{
|
||||
"day_stats": {
|
||||
"ok": True,
|
||||
"opens_today": 1,
|
||||
"trade_stats": {
|
||||
"closed_count": 2,
|
||||
"win_count": 0,
|
||||
"loss_count": 2,
|
||||
"win_pnl_u": 0,
|
||||
"loss_pnl_u": -3.0,
|
||||
{
|
||||
"day_stats": {
|
||||
"ok": True,
|
||||
"opens_today": 1,
|
||||
"trade_stats": {
|
||||
"closed_count": 2,
|
||||
"win_count": 0,
|
||||
"loss_count": 2,
|
||||
"win_pnl_u": 0,
|
||||
"loss_pnl_u": -3.0,
|
||||
},
|
||||
},
|
||||
"agent": {"positions": [], "total_unrealized_pnl": 0},
|
||||
},
|
||||
"agent": {"positions": [], "total_unrealized_pnl": 0},
|
||||
},
|
||||
]
|
||||
out = aggregate_monitor_board_totals(rows, trading_day="2026-07-04", reset_hour=8)
|
||||
assert out["open_count"] == 3
|
||||
assert out["closed_count"] == 3
|
||||
assert out["win_count"] == 1
|
||||
assert out["loss_count"] == 2
|
||||
assert out["win_pnl_u"] == 5.5
|
||||
assert out["loss_pnl_u"] == -3.0
|
||||
assert out["open_position_count"] == 1
|
||||
assert out["float_pnl_u"] == 1.2
|
||||
]
|
||||
out = aggregate_monitor_board_totals(rows, trading_day="2026-07-04", reset_hour=8)
|
||||
self.assertEqual(out["open_count"], 3)
|
||||
self.assertEqual(out["closed_count"], 3)
|
||||
self.assertEqual(out["win_count"], 1)
|
||||
self.assertEqual(out["loss_count"], 2)
|
||||
self.assertEqual(out["win_pnl_u"], 5.5)
|
||||
self.assertEqual(out["loss_pnl_u"], -3.0)
|
||||
self.assertEqual(out["open_position_count"], 1)
|
||||
self.assertEqual(out["float_pnl_u"], 1.2)
|
||||
|
||||
def test_aggregate_monitor_board_totals_includes_options(self):
|
||||
rows = [
|
||||
{
|
||||
"capabilities": ["options"],
|
||||
"options": {
|
||||
"ok": True,
|
||||
"enabled": True,
|
||||
"positions": [{"inst_id": "X"}, {"inst_id": "Y"}],
|
||||
"upl_total_usdc": 1.5,
|
||||
},
|
||||
"agent": {"positions": [], "total_unrealized_pnl": 0},
|
||||
}
|
||||
]
|
||||
out = aggregate_monitor_board_totals(rows, trading_day="2026-07-04", reset_hour=8)
|
||||
self.assertEqual(out["options_open_position_count"], 2)
|
||||
self.assertEqual(out["open_position_count"], 2)
|
||||
self.assertEqual(out["options_float_pnl_u"], 1.5)
|
||||
self.assertEqual(out["float_pnl_u"], 1.5)
|
||||
|
||||
def test_aggregate_excludes_option_like_agent_positions(self):
|
||||
"""子代理误把期权当永续上报时:不算进持仓数,浮盈只用期权 snap."""
|
||||
rows = [
|
||||
{
|
||||
"capabilities": ["options"],
|
||||
"options": {
|
||||
"ok": True,
|
||||
"enabled": True,
|
||||
"positions": [{"inst_id": "ETH-USD-260806-1875-C"}],
|
||||
"upl_total_usdc": -0.4,
|
||||
},
|
||||
"agent": {
|
||||
"positions": [
|
||||
{
|
||||
"symbol": "ETH/USD:USD-260806-1875-C",
|
||||
"contracts": 66,
|
||||
"unrealized_pnl": 27.6,
|
||||
},
|
||||
{
|
||||
"symbol": "BTC/USDT:USDT",
|
||||
"contracts": 1,
|
||||
"unrealized_pnl": -4.66,
|
||||
},
|
||||
],
|
||||
"total_unrealized_pnl": 22.94,
|
||||
},
|
||||
}
|
||||
]
|
||||
out = aggregate_monitor_board_totals(rows, trading_day="2026-08-05", reset_hour=8)
|
||||
self.assertEqual(out["open_position_count"], 2)
|
||||
self.assertEqual(out["options_open_position_count"], 1)
|
||||
self.assertEqual(out["options_float_pnl_u"], -0.4)
|
||||
self.assertEqual(out["float_pnl_u"], round(-4.66 + (-0.4), 4))
|
||||
|
||||
def test_summarize_trades_win_loss_amounts(self):
|
||||
stats = summarize_trades(
|
||||
[{"pnl_amount": 2.5}, {"pnl_amount": -1.0}, {"pnl_amount": 0}]
|
||||
)
|
||||
self.assertEqual(stats["win_count"], 1)
|
||||
self.assertEqual(stats["loss_count"], 1)
|
||||
self.assertEqual(stats["win_pnl_u"], 2.5)
|
||||
self.assertEqual(stats["loss_pnl_u"], -1.0)
|
||||
|
||||
|
||||
def test_aggregate_monitor_board_totals_includes_options():
|
||||
rows = [
|
||||
{
|
||||
"capabilities": ["options"],
|
||||
"options": {
|
||||
"ok": True,
|
||||
"enabled": True,
|
||||
"positions": [{"inst_id": "X"}, {"inst_id": "Y"}],
|
||||
"upl_total_usdc": 1.5,
|
||||
},
|
||||
"agent": {"positions": [], "total_unrealized_pnl": 0},
|
||||
}
|
||||
]
|
||||
out = aggregate_monitor_board_totals(rows, trading_day="2026-07-04", reset_hour=8)
|
||||
assert out["options_open_position_count"] == 2
|
||||
assert out["open_position_count"] == 2
|
||||
assert out["options_float_pnl_u"] == 1.5
|
||||
assert out["float_pnl_u"] == 1.5
|
||||
|
||||
|
||||
def test_summarize_trades_win_loss_amounts():
|
||||
from lib.hub.hub_trades_lib import summarize_trades
|
||||
|
||||
stats = summarize_trades(
|
||||
[{"pnl_amount": 2.5}, {"pnl_amount": -1.0}, {"pnl_amount": 0}]
|
||||
)
|
||||
assert stats["win_count"] == 1
|
||||
assert stats["loss_count"] == 1
|
||||
assert stats["win_pnl_u"] == 2.5
|
||||
assert stats["loss_pnl_u"] == -1.0
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user