Include options unrealized PnL in OKX instance header realtime PnL total.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6892,6 +6892,16 @@ def api_account_snapshot():
|
|||||||
active_pnl_rows,
|
active_pnl_rows,
|
||||||
get_live_position_exchange_metrics,
|
get_live_position_exchange_metrics,
|
||||||
)
|
)
|
||||||
|
options_unrealized_pnl = None
|
||||||
|
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
||||||
|
try:
|
||||||
|
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
|
||||||
|
from lib.instance.instance_live_pnl_lib import merge_unrealized_pnl_components
|
||||||
|
|
||||||
|
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
|
||||||
|
unrealized_pnl = merge_unrealized_pnl_components(unrealized_pnl, options_unrealized_pnl)
|
||||||
|
except Exception:
|
||||||
|
options_unrealized_pnl = None
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"funding_usdt": funding_usdt,
|
"funding_usdt": funding_usdt,
|
||||||
"current_capital": current_capital,
|
"current_capital": current_capital,
|
||||||
@@ -6907,6 +6917,7 @@ def api_account_snapshot():
|
|||||||
),
|
),
|
||||||
"available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None,
|
"available_trading_usdt": round(available_trading_usdt, FUNDS_DECIMALS) if available_trading_usdt is not None else None,
|
||||||
"unrealized_pnl": unrealized_pnl,
|
"unrealized_pnl": unrealized_pnl,
|
||||||
|
"options_unrealized_pnl": options_unrealized_pnl,
|
||||||
"recommended_capital": recommended_capital,
|
"recommended_capital": recommended_capital,
|
||||||
"active_count": position_limit_count,
|
"active_count": position_limit_count,
|
||||||
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
"max_active_positions": MAX_ACTIVE_POSITIONS,
|
||||||
@@ -7282,12 +7293,22 @@ def api_price_snapshot():
|
|||||||
format_mark_display=lambda sym, px: format_price_for_symbol(sym, px),
|
format_mark_display=lambda sym, px: format_price_for_symbol(sym, px),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
options_unrealized_pnl = None
|
||||||
|
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
||||||
|
try:
|
||||||
|
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
|
||||||
|
|
||||||
|
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
|
||||||
|
except Exception:
|
||||||
|
options_unrealized_pnl = None
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"updated_at": app_now_str(),
|
"updated_at": app_now_str(),
|
||||||
"key_prices": key_prices,
|
"key_prices": key_prices,
|
||||||
"order_prices": order_prices,
|
"order_prices": order_prices,
|
||||||
"position_marks": position_marks,
|
"position_marks": position_marks,
|
||||||
"positions_raw_count": len(all_swap_positions),
|
"positions_raw_count": len(all_swap_positions),
|
||||||
|
"options_unrealized_pnl": options_unrealized_pnl,
|
||||||
**force_close_template_context(
|
**force_close_template_context(
|
||||||
FORCE_CLOSE_ENABLED,
|
FORCE_CLOSE_ENABLED,
|
||||||
FORCE_CLOSE_BJ_HOUR,
|
FORCE_CLOSE_BJ_HOUR,
|
||||||
|
|||||||
@@ -615,6 +615,19 @@ def fetch_option_positions(ex: ccxt.okx) -> list[dict[str, Any]]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_options_unrealized_pnl_usdc(ex: ccxt.okx) -> float | None:
|
||||||
|
"""期权持仓未实现盈亏合计(USDC,统计口径与 USDT 1:1)。"""
|
||||||
|
total = 0.0
|
||||||
|
found = False
|
||||||
|
for pos in fetch_option_positions(ex):
|
||||||
|
upl = _safe_float(pos.get("upl"))
|
||||||
|
if upl is None:
|
||||||
|
continue
|
||||||
|
found = True
|
||||||
|
total += upl
|
||||||
|
return round(total, 4) if found else None
|
||||||
|
|
||||||
|
|
||||||
def estimate_usdt_to_usdc(ex: ccxt.okx, usdt_amount: float) -> dict[str, Any]:
|
def estimate_usdt_to_usdc(ex: ccxt.okx, usdt_amount: float) -> dict[str, Any]:
|
||||||
if usdt_amount <= 0:
|
if usdt_amount <= 0:
|
||||||
return {"ok": False, "msg": "兑换数量须大于 0"}
|
return {"ok": False, "msg": "兑换数量须大于 0"}
|
||||||
|
|||||||
@@ -110,3 +110,18 @@ def resolve_instance_unrealized_pnl(
|
|||||||
if active_rows and get_metrics_fn:
|
if active_rows and get_metrics_fn:
|
||||||
return sum_unrealized_pnl_from_metrics(active_rows, get_metrics_fn)
|
return sum_unrealized_pnl_from_metrics(active_rows, get_metrics_fn)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def merge_unrealized_pnl_components(*parts: float | None) -> float | None:
|
||||||
|
"""合并永续与期权等多路未实现盈亏(任一路有值即参与合计)。"""
|
||||||
|
total = 0.0
|
||||||
|
found = False
|
||||||
|
for part in parts:
|
||||||
|
if part is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
total += float(part)
|
||||||
|
found = True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return round(total, 2) if found else None
|
||||||
|
|||||||
@@ -1046,6 +1046,26 @@ function sumOrdersFloatPnl(orders){
|
|||||||
});
|
});
|
||||||
return found ? total : null;
|
return found ? total : null;
|
||||||
}
|
}
|
||||||
|
function combineRealtimeFloatPnl(perpTotal, optionsTotal){
|
||||||
|
let total = 0, found = false;
|
||||||
|
[perpTotal, optionsTotal].forEach(v=>{
|
||||||
|
if(v != null && !Number.isNaN(Number(v))){
|
||||||
|
total += Number(v);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found ? total : null;
|
||||||
|
}
|
||||||
|
function paintRealtimePnlFromSnapshot(data){
|
||||||
|
if(!data) return;
|
||||||
|
const perp = data.order_prices && data.order_prices.length
|
||||||
|
? sumOrdersFloatPnl(data.order_prices)
|
||||||
|
: null;
|
||||||
|
const combined = combineRealtimeFloatPnl(perp, data.options_unrealized_pnl);
|
||||||
|
if(combined !== null || perp !== null || data.options_unrealized_pnl != null){
|
||||||
|
paintRealtimePnl(combined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatOptionsFundingLabel(usdc, usdt) {
|
function formatOptionsFundingLabel(usdc, usdt) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -1319,7 +1339,9 @@ function refreshPriceSnapshotConditional(){
|
|||||||
tickOrderHoldDurations();
|
tickOrderHoldDurations();
|
||||||
}
|
}
|
||||||
if(data.order_prices && data.order_prices.length){
|
if(data.order_prices && data.order_prices.length){
|
||||||
paintRealtimePnl(sumOrdersFloatPnl(data.order_prices));
|
paintRealtimePnlFromSnapshot(data);
|
||||||
|
} else if (typeof data.options_unrealized_pnl !== "undefined") {
|
||||||
|
paintRealtimePnlFromSnapshot(data);
|
||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1597,6 +1597,26 @@ function sumOrdersFloatPnl(orders){
|
|||||||
});
|
});
|
||||||
return found ? total : null;
|
return found ? total : null;
|
||||||
}
|
}
|
||||||
|
function combineRealtimeFloatPnl(perpTotal, optionsTotal){
|
||||||
|
let total = 0, found = false;
|
||||||
|
[perpTotal, optionsTotal].forEach(v=>{
|
||||||
|
if(v != null && !Number.isNaN(Number(v))){
|
||||||
|
total += Number(v);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found ? total : null;
|
||||||
|
}
|
||||||
|
function paintRealtimePnlFromSnapshot(data){
|
||||||
|
if(!data) return;
|
||||||
|
const perp = data.order_prices && data.order_prices.length
|
||||||
|
? sumOrdersFloatPnl(data.order_prices)
|
||||||
|
: null;
|
||||||
|
const combined = combineRealtimeFloatPnl(perp, data.options_unrealized_pnl);
|
||||||
|
if(combined !== null || perp !== null || data.options_unrealized_pnl != null){
|
||||||
|
paintRealtimePnl(combined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatOptionsFundingLabel(usdc, usdt) {
|
function formatOptionsFundingLabel(usdc, usdt) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -1912,7 +1932,9 @@ function refreshPriceSnapshotConditional(){
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
}
|
}
|
||||||
if(data.order_prices && data.order_prices.length){
|
if(data.order_prices && data.order_prices.length){
|
||||||
paintRealtimePnl(sumOrdersFloatPnl(data.order_prices));
|
paintRealtimePnlFromSnapshot(data);
|
||||||
|
} else if (typeof data.options_unrealized_pnl !== "undefined") {
|
||||||
|
paintRealtimePnlFromSnapshot(data);
|
||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from lib.instance.instance_live_pnl_lib import (
|
from lib.instance.instance_live_pnl_lib import (
|
||||||
|
merge_unrealized_pnl_components,
|
||||||
position_row_contracts,
|
position_row_contracts,
|
||||||
resolve_instance_unrealized_pnl,
|
resolve_instance_unrealized_pnl,
|
||||||
sum_unrealized_pnl_from_metrics,
|
sum_unrealized_pnl_from_metrics,
|
||||||
@@ -53,6 +54,12 @@ class TestInstanceLivePnlLib(unittest.TestCase):
|
|||||||
total = resolve_instance_unrealized_pnl(_fetch, rows, _metrics)
|
total = resolve_instance_unrealized_pnl(_fetch, rows, _metrics)
|
||||||
self.assertEqual(total, -2.5)
|
self.assertEqual(total, -2.5)
|
||||||
|
|
||||||
|
def test_merge_unrealized_pnl_components(self):
|
||||||
|
self.assertEqual(merge_unrealized_pnl_components(-0.11, 0.02), -0.09)
|
||||||
|
self.assertEqual(merge_unrealized_pnl_components(None, 0.02), 0.02)
|
||||||
|
self.assertEqual(merge_unrealized_pnl_components(-0.11, None), -0.11)
|
||||||
|
self.assertIsNone(merge_unrealized_pnl_components(None, None))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user