修复顶栏盈亏比:币本位已平盈亏先折 U 再统计。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4024,7 +4024,9 @@ def render_main_page(page="options", embed_mode=None):
|
|||||||
try:
|
try:
|
||||||
from lib.options.options_stats_lib import header_options_stats_for_window
|
from lib.options.options_stats_lib import header_options_stats_for_window
|
||||||
|
|
||||||
opt_header = header_options_stats_for_window(conn, list_window, APP_TZ)
|
opt_header = header_options_stats_for_window(
|
||||||
|
conn, list_window, APP_TZ, ex=exchange_options
|
||||||
|
)
|
||||||
total = opt_header["total"]
|
total = opt_header["total"]
|
||||||
rate = opt_header["rate"]
|
rate = opt_header["rate"]
|
||||||
profit_loss_ratio = opt_header.get("profit_loss_ratio")
|
profit_loss_ratio = opt_header.get("profit_loss_ratio")
|
||||||
@@ -4427,7 +4429,7 @@ def api_account_snapshot():
|
|||||||
from lib.options.options_stats_lib import header_options_stats_for_window
|
from lib.options.options_stats_lib import header_options_stats_for_window
|
||||||
|
|
||||||
header_trade_stats = header_options_stats_for_window(
|
header_trade_stats = header_options_stats_for_window(
|
||||||
conn, _list_window_from_request(), APP_TZ
|
conn, _list_window_from_request(), APP_TZ, ex=exchange_options
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -260,13 +260,41 @@ def compute_options_stats_for_window(
|
|||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|
||||||
def compute_options_stats(get_db) -> dict[str, Any]:
|
def _row_get(row: Any, key: str, default: Any = None) -> Any:
|
||||||
|
try:
|
||||||
|
if hasattr(row, "keys") and key in row.keys():
|
||||||
|
return row[key]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if isinstance(row, dict):
|
||||||
|
return row.get(key, default)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for row in closed_rows or []:
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"realized_pnl": _row_get(row, "realized_pnl"),
|
||||||
|
"created_at": _row_get(row, "created_at"),
|
||||||
|
"closed_at": _row_get(row, "closed_at"),
|
||||||
|
"inst_id": _row_get(row, "inst_id") or "",
|
||||||
|
"underlying": _row_get(row, "underlying") or "",
|
||||||
|
"premium_ccy": _row_get(row, "premium_ccy") or "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
closed_rows = conn.execute(
|
closed_rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT realized_pnl, created_at, closed_at
|
SELECT realized_pnl, created_at, closed_at, inst_id, underlying, premium_ccy
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'closed' AND realized_pnl IS NOT NULL
|
WHERE status = 'closed' AND realized_pnl IS NOT NULL
|
||||||
"""
|
"""
|
||||||
@@ -276,71 +304,33 @@ def compute_options_stats(get_db) -> dict[str, Any]:
|
|||||||
SELECT created_at FROM options_trades WHERE status = 'open'
|
SELECT created_at FROM options_trades WHERE status = 'open'
|
||||||
"""
|
"""
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return _stats_from_option_trade_rows(closed_rows, open_rows)
|
history = _closed_trade_dicts(closed_rows)
|
||||||
|
for row in open_rows:
|
||||||
|
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
||||||
|
# 币本位盈亏按指数折 U,再算胜率/盈亏比(与 /api/options/stats 一致)
|
||||||
|
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def _stats_from_option_trade_rows(closed_rows, open_rows) -> dict[str, Any]:
|
def _stats_from_option_trade_rows(closed_rows, open_rows, *, ex: Any = None) -> dict[str, Any]:
|
||||||
wins: list[float] = []
|
"""兼容旧调用;内部统一折 U 后走 compute_options_stats_from_history."""
|
||||||
losses: list[float] = []
|
history = _closed_trade_dicts(closed_rows)
|
||||||
win_holds: list[float] = []
|
for row in open_rows or []:
|
||||||
loss_holds: list[float] = []
|
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
||||||
all_holds: list[float] = []
|
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||||
now = datetime.now()
|
|
||||||
|
|
||||||
for row in closed_rows:
|
|
||||||
pnl = float(row["realized_pnl"])
|
|
||||||
hold = _hold_seconds(row["created_at"], row["closed_at"])
|
|
||||||
if hold is not None:
|
|
||||||
all_holds.append(hold)
|
|
||||||
if pnl > 0:
|
|
||||||
wins.append(pnl)
|
|
||||||
if hold is not None:
|
|
||||||
win_holds.append(hold)
|
|
||||||
elif pnl < 0:
|
|
||||||
losses.append(pnl)
|
|
||||||
if hold is not None:
|
|
||||||
loss_holds.append(hold)
|
|
||||||
|
|
||||||
open_holds: list[float] = []
|
|
||||||
for row in open_rows:
|
|
||||||
start = _parse_ts(row["created_at"])
|
|
||||||
if start is None:
|
|
||||||
continue
|
|
||||||
sec = (now - start).total_seconds()
|
|
||||||
if sec >= 0:
|
|
||||||
open_holds.append(sec)
|
|
||||||
|
|
||||||
total_closed = len(wins) + len(losses)
|
|
||||||
win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0
|
|
||||||
avg_win = sum(wins) / len(wins) if wins else None
|
|
||||||
avg_loss = sum(losses) / len(losses) if losses else None
|
|
||||||
|
|
||||||
total_profit = round(sum(wins), 4) if wins else 0.0
|
|
||||||
total_loss = round(abs(sum(losses)), 4) if losses else 0.0
|
|
||||||
net_realized = round(sum(wins) + sum(losses), 4)
|
|
||||||
return {
|
|
||||||
"total_closed": total_closed,
|
|
||||||
"win_count": len(wins),
|
|
||||||
"loss_count": len(losses),
|
|
||||||
"win_rate": win_rate,
|
|
||||||
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
|
|
||||||
"avg_win": round(avg_win, 4) if avg_win is not None else None,
|
|
||||||
"avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None,
|
|
||||||
"total_profit": total_profit,
|
|
||||||
"total_loss": total_loss,
|
|
||||||
"net_realized_pnl": net_realized,
|
|
||||||
"avg_hold_sec": _avg_seconds(all_holds),
|
|
||||||
"avg_win_hold_sec": _avg_seconds(win_holds),
|
|
||||||
"avg_loss_hold_sec": _avg_seconds(loss_holds),
|
|
||||||
"open_count": len(open_holds),
|
|
||||||
"avg_open_hold_sec": _avg_seconds(open_holds),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def header_options_stats_for_window(conn, list_window: dict[str, Any], app_tz) -> dict[str, Any]:
|
def header_options_stats_for_window(
|
||||||
"""顶栏总交易/胜率/盈亏比:纯期权(options_trades),全站导航共用,按列表窗过滤."""
|
conn,
|
||||||
|
list_window: dict[str, Any],
|
||||||
|
app_tz,
|
||||||
|
ex: Any = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""顶栏总交易/胜率/盈亏比:纯期权(options_trades),全站导航共用,按列表窗过滤.
|
||||||
|
|
||||||
|
币本位 realized_pnl 为币数量,须折 U 后再算盈亏比,否则 BTC/ETH 混算会严重失真.
|
||||||
|
"""
|
||||||
from lib.common.history_window_lib import utc_window_to_bj_sql_strings
|
from lib.common.history_window_lib import utc_window_to_bj_sql_strings
|
||||||
|
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
@@ -349,7 +339,7 @@ def header_options_stats_for_window(conn, list_window: dict[str, Any], app_tz) -
|
|||||||
)
|
)
|
||||||
closed_rows = conn.execute(
|
closed_rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT realized_pnl, created_at, closed_at
|
SELECT realized_pnl, created_at, closed_at, inst_id, underlying, premium_ccy
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
AND realized_pnl IS NOT NULL
|
AND realized_pnl IS NOT NULL
|
||||||
@@ -361,7 +351,10 @@ def header_options_stats_for_window(conn, list_window: dict[str, Any], app_tz) -
|
|||||||
open_rows = conn.execute(
|
open_rows = conn.execute(
|
||||||
"SELECT created_at FROM options_trades WHERE status = 'open'"
|
"SELECT created_at FROM options_trades WHERE status = 'open'"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
stats = _stats_from_option_trade_rows(closed_rows, open_rows)
|
history = _closed_trade_dicts(closed_rows)
|
||||||
|
for row in open_rows:
|
||||||
|
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
||||||
|
stats = compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||||
return {
|
return {
|
||||||
"total": int(stats.get("total_closed") or 0),
|
"total": int(stats.get("total_closed") or 0),
|
||||||
"rate": float(stats.get("win_rate") or 0),
|
"rate": float(stats.get("win_rate") or 0),
|
||||||
|
|||||||
@@ -41,5 +41,76 @@ class DisplayPnlUsdtTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(display_pnl_usdt_from_option_row(row), -12.5, places=4)
|
self.assertAlmostEqual(display_pnl_usdt_from_option_row(row), -12.5, places=4)
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderPlRatioUsdtTests(unittest.TestCase):
|
||||||
|
"""顶栏盈亏比须先把币数量折 U,避免 ETH/BTC 混算失真."""
|
||||||
|
|
||||||
|
def test_mixed_coin_pnl_ratio_after_usdt(self):
|
||||||
|
from lib.options.options_stats_lib import (
|
||||||
|
compute_options_stats_from_history,
|
||||||
|
history_pnl_to_usdt,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 赢 0.01 ETH、亏 0.0001 BTC;不折 U 时盈亏比≈0.01/0.0001=100 或反过来极小
|
||||||
|
# 按指数折 U: +0.01*3000=30U, -0.0001*100000=-10U → 盈亏比=3
|
||||||
|
history = [
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"inst_id": "ETH-USD-260701-2500-C",
|
||||||
|
"underlying": "ETH",
|
||||||
|
"premium_ccy": "ETH",
|
||||||
|
"realized_pnl": 0.01,
|
||||||
|
"created_at": "2026-10-01 10:00:00",
|
||||||
|
"closed_at": "2026-10-01 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"inst_id": "BTC-USD-260701-100000-P",
|
||||||
|
"underlying": "BTC",
|
||||||
|
"premium_ccy": "BTC",
|
||||||
|
"realized_pnl": -0.0001,
|
||||||
|
"created_at": "2026-10-01 11:00:00",
|
||||||
|
"closed_at": "2026-10-01 13:00:00",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
class _Ex:
|
||||||
|
def public_get_market_ticker(self, params):
|
||||||
|
inst = (params or {}).get("instId") or ""
|
||||||
|
if inst.startswith("ETH"):
|
||||||
|
return {"data": [{"last": "3000"}]}
|
||||||
|
if inst.startswith("BTC"):
|
||||||
|
return {"data": [{"last": "100000"}]}
|
||||||
|
return {"data": []}
|
||||||
|
|
||||||
|
converted = history_pnl_to_usdt(history, _Ex())
|
||||||
|
self.assertAlmostEqual(float(converted[0]["realized_pnl"]), 30.0, places=4)
|
||||||
|
self.assertAlmostEqual(float(converted[1]["realized_pnl"]), -10.0, places=4)
|
||||||
|
stats = compute_options_stats_from_history(converted)
|
||||||
|
self.assertEqual(stats["win_count"], 1)
|
||||||
|
self.assertEqual(stats["loss_count"], 1)
|
||||||
|
self.assertAlmostEqual(float(stats["profit_loss_ratio"]), 3.0, places=2)
|
||||||
|
|
||||||
|
def test_raw_coin_mix_would_distort_without_convert(self):
|
||||||
|
from lib.options.options_stats_lib import compute_options_stats_from_history
|
||||||
|
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"realized_pnl": 0.01,
|
||||||
|
"created_at": "2026-10-01 10:00:00",
|
||||||
|
"closed_at": "2026-10-01 12:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"realized_pnl": -0.0001,
|
||||||
|
"created_at": "2026-10-01 11:00:00",
|
||||||
|
"closed_at": "2026-10-01 13:00:00",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
stats = compute_options_stats_from_history(raw)
|
||||||
|
# 未折 U: 0.01 / 0.0001 = 100, 与折 U 后的 3 截然不同
|
||||||
|
self.assertAlmostEqual(float(stats["profit_loss_ratio"]), 100.0, places=2)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user