fix期权复盘与数据统计双口径盈亏,并修复卖回封顶导致盈利单冻成亏损。

平仓卖光可卖币后再按账户差分结算复盘;数据统计改为历史币盈亏×卖回价;复盘展示初始权利金与盈利率,并附历史修复脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 09:10:13 +08:00
parent d4de6a23ae
commit 9d77a430e5
16 changed files with 2858 additions and 2242 deletions
+96 -7
View File
@@ -92,24 +92,107 @@ def transfer_blocked_by_position_msg(conn, ex: Any = None) -> str | None:
return None
def sellable_coin_blocks_settle(
ex: Any,
underlying: str | None,
) -> str | None:
"""仍有可卖标的币则禁止账户差分结算(规格:卖光再冻结)."""
from lib.options.options_spot_bridge_lib import (
fetch_trading_coin_available,
is_sellable_spot_coin,
)
uly = (underlying or "").strip().upper()
if uly not in ("ETH", "BTC"):
return None
try:
avail = fetch_trading_coin_available(ex, uly)
except Exception:
return None
if avail is None:
return None
if is_sellable_spot_coin(ex, uly, avail):
return f"仍有可卖 {uly}({float(avail):g}),暂不结算账户盈亏"
return None
def _underlying_for_settle(
conn,
*,
bridge_id: int | None = None,
trade_id: int | None = None,
) -> str | None:
try:
if trade_id is not None:
row = conn.execute(
"SELECT underlying FROM options_trades WHERE id=?",
(int(trade_id),),
).fetchone()
if row:
return str(row["underlying"] if hasattr(row, "keys") else row[0] or "").strip().upper() or None
if bridge_id is not None:
row = conn.execute(
"""
SELECT underlying FROM options_trades
WHERE bridge_id=? AND status='closed'
ORDER BY id DESC LIMIT 1
""",
(int(bridge_id),),
).fetchone()
if row:
return str(row["underlying"] if hasattr(row, "keys") else row[0] or "").strip().upper() or None
brow = conn.execute(
"SELECT underlying FROM options_spot_bridge WHERE id=?",
(int(bridge_id),),
).fetchone()
if brow:
return str(brow["underlying"] if hasattr(brow, "keys") else brow[0] or "").strip().upper() or None
except Exception:
return None
return None
def try_settle_after_sellback(
get_db: Any,
ex: Any,
*,
bridge_id: int | None = None,
cfg: dict[str, Any] | None = None,
underlying: str | None = None,
sell_complete: bool | None = None,
) -> int:
"""卖回路径通用结算:读交易账户 after 并冻结未结算单."""
pack = cfg if isinstance(cfg, dict) else {"fetch_options_balances": None}
after = fetch_trading_usdt(pack, ex)
if after is None:
"""卖回路径通用结算:读交易账户 after 并冻结未结算单.
门禁:sell_complete 显式 False 时不结算;仍有可卖标的币时不结算.
"""
if sell_complete is False:
return 0
conn = get_db() if callable(get_db) else get_db
close_conn = callable(get_db)
pack = cfg if isinstance(cfg, dict) else {"fetch_options_balances": None}
import sqlite3
# Windows 上 sqlite3.Connection 的 callable() 可能为 True,不能据此判断 get_db
if isinstance(get_db, sqlite3.Connection):
conn = get_db
close_conn = False
elif callable(get_db):
conn = get_db()
close_conn = True
else:
conn = get_db
close_conn = False
try:
from lib.options.options_db import init_options_tables
init_options_tables(conn)
uly = (underlying or "").strip().upper() or None
if not uly:
uly = _underlying_for_settle(conn, bridge_id=bridge_id)
block = sellable_coin_blocks_settle(ex, uly)
if block:
return 0
after = fetch_trading_usdt(pack, ex)
if after is None:
return 0
n = 0
if bridge_id is not None:
n = settle_closed_trades_from_trading_account(
@@ -125,10 +208,16 @@ def try_settle_after_sellback(
"""
).fetchone()
if row:
tid = int(row["id"] if hasattr(row, "keys") else row[0])
if not uly:
uly = _underlying_for_settle(conn, trade_id=tid)
block2 = sellable_coin_blocks_settle(ex, uly)
if block2:
return 0
n = settle_closed_trades_from_trading_account(
conn,
trading_usdt_after=after,
trade_id=int(row["id"] if hasattr(row, "keys") else row[0]),
trade_id=tid,
)
try:
conn.commit()
+23 -7
View File
@@ -610,6 +610,14 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
tusdt_before = float(tusdt_before) if tusdt_before is not None else None
except (TypeError, ValueError):
tusdt_before = None
paid = kwargs.get("premium_paid")
try:
paid_f = float(paid) if paid is not None else None
except (TypeError, ValueError):
paid_f = None
initial_premium_usdt = None
if open_index_px is not None and paid_f is not None and paid_f > 0:
initial_premium_usdt = round(float(open_index_px) * float(paid_f), 4)
cur = conn.execute(
"""
INSERT INTO options_trades(
@@ -617,8 +625,9 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
open_quote, premium_paid, status, signal_note, exchange_ord_id,
margin_mode, premium_ccy, bridge_id, budget_usdt,
open_fee, fee_ccy, open_index_px, trading_usdt_before, pnl_settled,
initial_premium_usdt,
profit_exit_enabled, profit_exit_mult, profit_exit_state, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)
""",
(
kwargs["inst_id"],
@@ -639,6 +648,7 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
fee_ccy,
open_index_px,
tusdt_before,
initial_premium_usdt,
pe,
pe_mult,
pe_state,
@@ -685,17 +695,20 @@ def maybe_sell_spot_after_close(
underlying=underlying,
inst_id=inst_id,
)
# 卖回成功:交易账户 after − 开仓 before → 冻结本单盈亏
# 卖回完成(sell_complete):交易账户 after − 开仓 before → 冻结本单盈亏
try:
from lib.options.options_account_pnl_lib import try_settle_after_sellback
if (result or {}).get("ok"):
bid = (result or {}).get("bridge_id")
res = result or {}
if res.get("ok") and res.get("sell_complete", True) and not res.get("incomplete"):
bid = res.get("bridge_id")
try_settle_after_sellback(
cfg["get_db"],
ex,
bridge_id=int(bid) if bid is not None else None,
cfg=cfg,
underlying=underlying,
sell_complete=True,
)
except Exception:
pass
@@ -766,20 +779,23 @@ def sweep_spot_after_options_flat(
underlying=uly,
inst_id=None,
)
if (out or {}).get("ok"):
out = out or {}
if out.get("ok") and out.get("sell_complete", True) and not out.get("incomplete"):
try:
from lib.options.options_account_pnl_lib import try_settle_after_sellback
bid = (out or {}).get("bridge_id")
bid = out.get("bridge_id")
try_settle_after_sellback(
cfg["get_db"],
exchange,
bridge_id=int(bid) if bid is not None else None,
cfg=cfg,
underlying=uly,
sell_complete=True,
)
except Exception:
pass
sold.append({"underlying": uly, **(out or {})})
sold.append({"underlying": uly, **out})
except Exception as e:
sold.append({"underlying": uly, "ok": False, "msg": str(e)})
return {"ok": True, "results": sold, "skipped": not sold}
+2
View File
@@ -133,6 +133,8 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
"ALTER TABLE options_trades ADD COLUMN trading_usdt_before REAL",
"ALTER TABLE options_trades ADD COLUMN trading_usdt_after REAL",
"ALTER TABLE options_trades ADD COLUMN pnl_settled INTEGER DEFAULT 0",
# 初始权利金(U)=开仓指数×权利金币量,不含多买缓冲
"ALTER TABLE options_trades ADD COLUMN initial_premium_usdt REAL",
):
try:
conn.execute(ddl)
+5 -1
View File
@@ -1225,7 +1225,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
underlying=underlying,
inst_id=inst_id,
)
if out.get("ok"):
if out.get("ok") and out.get("sell_complete", True) and not out.get("incomplete"):
try:
from lib.options.options_account_pnl_lib import try_settle_after_sellback
@@ -1235,10 +1235,14 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
ex,
bridge_id=int(bid) if bid is not None else None,
cfg=cfg,
underlying=underlying,
sell_complete=True,
)
except Exception:
pass
_mark_balances_stale(cfg)
elif out.get("ok"):
_mark_balances_stale(cfg)
return jsonify(out)
@app.route("/api/options/convert/quote", methods=["POST"])
+2
View File
@@ -131,6 +131,8 @@ def init_options_review_tables(conn: sqlite3.Connection) -> None:
_ensure_column(conn, "options_review_trades", "target_price_up", "REAL")
_ensure_column(conn, "options_review_trades", "target_price_down", "REAL")
_ensure_column(conn, "options_review_trades", "profit_rr", "REAL")
_ensure_column(conn, "options_review_trades", "initial_premium_usdt", "REAL")
_ensure_column(conn, "options_review_trades", "profit_rate", "REAL")
_ensure_column(conn, "options_review_entries", "prev_entry_tf", "TEXT")
+23 -6
View File
@@ -146,6 +146,8 @@ def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) ->
"close_avg": _safe_float(row.get("close_avg_px") if row.get("close_avg_px") is not None else row.get("close_avg")),
"premium_paid": _safe_float(row.get("premium_paid")),
"realized_pnl": pnl,
"initial_premium_usdt": _safe_float(row.get("initial_premium_usdt")),
"profit_rate": _safe_float(row.get("profit_rate")),
}
cols = list(fields.keys())
if existing:
@@ -317,7 +319,7 @@ def sync_options_from_local_trades(
open_quote, close_quote, premium_paid, premium_received, realized_pnl,
realized_pnl_usdt, close_index_px, open_index_px, budget_usdt,
premium_ccy, created_at, closed_at, signal_note, status,
trading_usdt_before, pnl_settled
trading_usdt_before, pnl_settled, initial_premium_usdt
FROM options_trades
WHERE status = 'closed'
ORDER BY id DESC
@@ -439,12 +441,19 @@ def sync_options_from_local_trades(
pnl_u = round(float(frozen_usdt), 4)
else:
pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx)
# 权利金用平仓/开仓冻结指数折算,不用实时价
prem_u = _to_usdt(prem, ccy=ccy, idx=idx)
if ccy in ("ETH", "BTC") and pnl_u is None:
# 初始权利金(U)=开仓指数×权利金币量;不含多买
stored_initial = _safe_float(r["initial_premium_usdt"])
open_idx_for_prem = open_idx if (open_idx is not None and open_idx > 0) else idx
prem_u = stored_initial
if prem_u is None:
prem_u = _to_usdt(prem, ccy=ccy, idx=open_idx_for_prem)
if ccy in ("ETH", "BTC") and pnl_u is None and not acct_pending:
pnl_u = pnl
if ccy in ("ETH", "BTC") and prem_u is None:
prem_u = prem
profit_rate = None
if pnl_u is not None and prem_u is not None and abs(float(prem_u)) > 1e-12:
profit_rate = round(float(pnl_u) / float(prem_u), 6)
opened_at = r["created_at"]
closed_at = r["closed_at"]
action = upsert_option_history_row(
@@ -461,10 +470,12 @@ def sync_options_from_local_trades(
"open_avg_px": r["open_quote"],
"close_avg_px": r["close_quote"],
"premium_paid": prem_u if prem_u is not None else prem,
"realized_pnl": pnl_u if pnl_u is not None else pnl,
"realized_pnl": pnl_u if pnl_u is not None else (None if acct_pending else pnl),
"initial_premium_usdt": prem_u,
"profit_rate": profit_rate,
"created_at": opened_at,
"closed_at": closed_at,
"status_label": "已平",
"status_label": "待结算" if acct_pending else "已平",
},
)
if action == "inserted":
@@ -759,6 +770,12 @@ def _row_to_dict(row: Any) -> dict[str, Any]:
def enrich_trade_row(row: dict[str, Any], entry: dict[str, Any] | None = None) -> dict[str, Any]:
out = dict(row)
# 盈利率缺失时按 复盘盈亏/初始权利金 补算
if _safe_float(out.get("profit_rate")) is None:
pnl = _safe_float(out.get("realized_pnl_total"))
prem = _safe_float(out.get("initial_premium_usdt"))
if pnl is not None and prem is not None and abs(prem) > 1e-12:
out["profit_rate"] = round(pnl / prem, 6)
out["source_label"] = SOURCE_LABELS.get(str(out.get("source_type") or ""), out.get("source_type"))
out["is_hedge"] = str(out.get("source_type") or "") in (SOURCE_PERP_OPTIONS, SOURCE_OPTIONS_OPTIONS)
legs = []
+57 -15
View File
@@ -451,14 +451,14 @@ def sell_residual_after_option_flat(
underlying: str,
inst_id: str | None = None,
) -> dict[str, Any]:
"""期权已平:卖掉本桥残留标的币;优先关闭 matching holding/pending 桥.
"""期权已平:卖光交易户该标的可卖币(一次一仓,含盈利币).
规格:平仓卖回禁止按开仓权利金 coin_bought 封顶;仍有可卖则不关桥、不结算.
可传 get_db 可调用对象(推荐)或已打开的 Connection。
卖币(尤其模拟盘写钱包)期间绝不握着业务连接,否则会 database is locked。
"""
get_db = conn_or_get_db if callable(conn_or_get_db) else None
bid: int | None = None
sell_cap: float | None = None
want_inst = str(inst_id or "").strip()
def _pick_bridge(bridges: list[dict[str, Any]]) -> dict[str, Any] | None:
@@ -485,7 +485,6 @@ def sell_residual_after_option_flat(
target = _pick_bridge(list_open_bridges(conn))
if target is not None:
bid = int(target["id"])
sell_cap = _safe_float(target.get("coin_bought"))
try:
conn.commit()
except Exception:
@@ -501,7 +500,6 @@ def sell_residual_after_option_flat(
target = _pick_bridge(list_open_bridges(conn))
if target is not None:
bid = int(target["id"])
sell_cap = _safe_float(target.get("coin_bought"))
try:
conn.commit()
except Exception:
@@ -512,13 +510,10 @@ def sell_residual_after_option_flat(
avail_chk = fetch_trading_coin_available(ex, underlying)
except Exception:
avail_chk = None
dust_amt = avail_chk
if dust_amt is None and sell_cap is not None:
dust_amt = sell_cap
if dust_amt is not None and not is_sellable_spot_coin(ex, underlying, dust_amt):
if avail_chk is not None and not is_sellable_spot_coin(ex, underlying, avail_chk):
dust_msg = "粉尘低于现货最小可卖,已忽略"
if bid is None:
return {"ok": True, "msg": dust_msg, "skipped": True, "dust": True}
return {"ok": True, "msg": dust_msg, "skipped": True, "dust": True, "sell_complete": True}
def _close_dust(c: sqlite3.Connection) -> dict[str, Any]:
update_bridge(
c,
@@ -533,6 +528,7 @@ def sell_residual_after_option_flat(
"msg": dust_msg,
"skipped": True,
"dust": True,
"sell_complete": True,
"bridge_id": bid,
"bridge_status": BRIDGE_CLOSED,
}
@@ -547,20 +543,33 @@ def sell_residual_after_option_flat(
pass
return _close_dust(conn_or_get_db)
# 有桥时按 coin_bought 封顶,避免误卖无关余额;无桥空仓扫尾仍卖光可用
# 一次一仓空仓后卖光可用(含盈利币);禁止用 coin_bought/权利金封顶
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
sell = spot_market_sell_coin_to_usdt(
ex,
underlying=underlying,
coin_amount=sell_cap if (bid is not None and sell_cap is not None and sell_cap > 0) else None,
coin_amount=None,
)
if bid is None:
if not sell.get("ok"):
msg = str(sell.get("msg") or "")
if "无可用" in msg or "过小" in msg:
return {"ok": True, "msg": "无残留币需卖回", "skipped": True}
return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL}
return {"ok": True, "sell": sell, "bridge_status": None}
return {"ok": True, "msg": "无残留币需卖回", "skipped": True, "sell_complete": True}
return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL, "sell_complete": False}
# 再确认无可卖残留
try:
left = fetch_trading_coin_available(ex, underlying)
except Exception:
left = None
if left is not None and is_sellable_spot_coin(ex, underlying, left):
return {
"ok": False,
"msg": f"卖回未完成,仍有可卖 {str(underlying or '').upper()}",
"incomplete": True,
"sell_complete": False,
"sell": sell,
}
return {"ok": True, "sell": sell, "bridge_status": None, "sell_complete": True}
def _apply_bridge(c: sqlite3.Connection) -> dict[str, Any]:
if not sell.get("ok"):
@@ -580,6 +589,7 @@ def sell_residual_after_option_flat(
"msg": "粉尘低于现货最小可卖,已忽略",
"skipped": True,
"dust": True,
"sell_complete": True,
"bridge_id": bid,
"bridge_status": BRIDGE_CLOSED,
}
@@ -595,10 +605,36 @@ def sell_residual_after_option_flat(
"msg": msg,
"bridge_id": bid,
"bridge_status": BRIDGE_PENDING_SELL,
"sell_complete": False,
}
recovered = _safe_float(sell.get("usdt_recovered"))
if recovered is not None and recovered > 0:
add_bridge_usdt_recovered(c, bid, recovered)
# 卖后仍有可卖 → 不关桥、不结算(盈利币未卖回时曾误冻亏损)
try:
left = fetch_trading_coin_available(ex, underlying)
except Exception:
left = None
if left is not None and is_sellable_spot_coin(ex, underlying, left):
left_msg = f"卖回未完成,仍有可卖 {str(underlying or '').upper()}({float(left):g})"
update_bridge(
c,
bid,
status=BRIDGE_PENDING_SELL,
sell_ord_id=str(sell.get("ord_id") or ""),
coin_sold=_safe_float(sell.get("coin_sold")),
inst_id=inst_id,
message=left_msg,
)
return {
"ok": False,
"msg": left_msg,
"incomplete": True,
"sell_complete": False,
"sell": sell,
"bridge_id": bid,
"bridge_status": BRIDGE_PENDING_SELL,
}
update_bridge(
c,
bid,
@@ -609,7 +645,13 @@ def sell_residual_after_option_flat(
message="期权已平,币已卖回 USDT",
close=True,
)
return {"ok": True, "sell": sell, "bridge_id": bid, "bridge_status": BRIDGE_CLOSED}
return {
"ok": True,
"sell": sell,
"bridge_id": bid,
"bridge_status": BRIDGE_CLOSED,
"sell_complete": True,
}
if get_db is not None:
conn2 = get_db()
+32 -34
View File
@@ -59,11 +59,16 @@ def _underlying_index_usdt(ex: Any, underly: str) -> float | None:
return None
def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
"""
统计用:优先已冻结的 realized_pnl_usdt(平仓当时口径,含手续费后净额);
否则币本位用 close_index_px(再否则实时指数)折 U;USDC 原样.
折算失败的币仓剔除盈亏字段,避免把「币数量」当成 U.
def history_pnl_to_usdt(
history: list[dict[str, Any]],
ex: Any = None,
*,
prefer_frozen_usdt: bool = True,
) -> list[dict[str, Any]]:
"""折 U.
prefer_frozen_usdt=True:优先行内 realized_pnl_usdt(实盘 exchange freeze / 旧兼容).
prefer_frozen_usdt=False:口径 B — 币盈亏 × 平仓卖回价(close_index_px),忽略账户差分 A.
"""
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
@@ -75,7 +80,7 @@ def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[d
out.append(r)
continue
frozen = _safe_float(r.get("realized_pnl_usdt"))
if frozen is not None:
if prefer_frozen_usdt and frozen is not None:
r["realized_pnl"] = round(float(frozen), 4)
out.append(r)
continue
@@ -104,6 +109,11 @@ def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[d
return out
def history_pnl_to_usdt_stats(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
"""数据统计口径 B:期权历史币盈亏 × 平仓卖回价."""
return history_pnl_to_usdt(history, ex, prefer_frozen_usdt=False)
def _parse_ts(raw: Any) -> datetime | None:
if raw is None or raw == "":
return None
@@ -286,7 +296,8 @@ def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
{
"status": "closed",
"realized_pnl": _row_get(row, "realized_pnl"),
"realized_pnl_usdt": _row_get(row, "realized_pnl_usdt"),
# 口径 B 不读账户 realized_pnl_usdt;保留键位兼容旧调用
"realized_pnl_usdt": None,
"close_index_px": _row_get(row, "close_index_px"),
"created_at": _row_get(row, "created_at"),
"closed_at": _row_get(row, "closed_at"),
@@ -298,16 +309,13 @@ def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
return out
# 口径 B:凡有币盈亏的已平均计入;U 由 history_pnl_to_usdt_stats 按卖回价折算
_CLOSED_STATS_SELECT = """
SELECT realized_pnl, realized_pnl_usdt, close_index_px,
SELECT realized_pnl, close_index_px,
created_at, closed_at, inst_id, underlying, premium_ccy
FROM options_trades
WHERE status = 'closed'
AND (
(COALESCE(pnl_settled, 0) = 1 AND realized_pnl_usdt IS NOT NULL)
OR (trading_usdt_before IS NULL
AND (realized_pnl IS NOT NULL OR realized_pnl_usdt IS NOT NULL))
)
AND realized_pnl IS NOT NULL
"""
@@ -634,7 +642,7 @@ def apply_exchange_pnl_freeze(
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
"""模拟盘:本地交易结算.实盘请用 compute_options_stats_live_exchange."""
"""模拟盘数据统计:口径 B(历史币盈亏×卖回价)."""
conn = get_db()
try:
history = load_local_options_stats_history(conn, ex)
@@ -642,7 +650,7 @@ def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
conn.commit()
except Exception:
pass
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
return compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
finally:
conn.close()
@@ -717,9 +725,11 @@ def load_live_exchange_stats_history(cfg: dict[str, Any], ex: Any) -> list[dict[
def compute_options_stats_live_exchange(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
"""实盘:与「期权历史」同一数据源(交易所 positions-history),盈亏以交易所为准."""
"""实盘:与「期权历史」同一数据源;口径 B=币盈亏×卖回价(freeze)."""
history = load_live_exchange_stats_history(cfg, ex)
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
return compute_options_stats_from_history(
history_pnl_to_usdt(history, ex, prefer_frozen_usdt=True)
)
def _apply_exchange_pnl_freeze_with_hints(
@@ -812,7 +822,7 @@ def _stats_from_option_trade_rows(closed_rows, open_rows, *, ex: Any = None) ->
history = _closed_trade_dicts(closed_rows)
for row in open_rows or []:
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
return compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
def header_options_stats_for_window(
@@ -821,32 +831,20 @@ def header_options_stats_for_window(
app_tz,
ex: Any = None,
) -> dict[str, Any]:
"""顶栏总交易/胜率/盈亏比:与期权统计同一结算口径.
新单(有 trading_usdt_before):仅 pnl_settled=1 的账户差分计入;
旧单(无 before):仍按原 realized 兼容.币数量须折 U 后再算盈亏比.
"""
"""顶栏总交易/胜率/盈亏比:口径 B(历史币盈亏×卖回价)."""
from lib.common.history_window_lib import utc_window_to_bj_sql_strings
init_options_tables(conn)
start_bj, end_bj = utc_window_to_bj_sql_strings(
list_window["start_utc"], list_window["end_utc"], app_tz
)
try:
freeze_missing_realized_pnl_usdt(conn, ex)
except Exception:
pass
closed_rows = conn.execute(
"""
SELECT realized_pnl, realized_pnl_usdt, close_index_px,
SELECT realized_pnl, close_index_px,
created_at, closed_at, inst_id, underlying, premium_ccy
FROM options_trades
WHERE status = 'closed'
AND (
(COALESCE(pnl_settled, 0) = 1 AND realized_pnl_usdt IS NOT NULL)
OR (trading_usdt_before IS NULL
AND (realized_pnl IS NOT NULL OR realized_pnl_usdt IS NOT NULL))
)
AND realized_pnl IS NOT NULL
AND COALESCE(closed_at, created_at) >= ?
AND COALESCE(closed_at, created_at) <= ?
""",
@@ -858,7 +856,7 @@ def header_options_stats_for_window(
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))
stats = compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
return {
"total": int(stats.get("total_closed") or 0),
"rate": float(stats.get("win_rate") or 0),
@@ -76,7 +76,8 @@
}
.or-list-loading{opacity:.55;pointer-events:none;transition:opacity .12s ease}
.or-trades-table-wrap,.or-reviewed-table-wrap{min-height:9.5rem;overflow-x:auto}
.or-reviewed-table{min-width:980px}
.or-reviewed-table{min-width:1180px}
.or-trades-table{min-width:960px}
.or-kpi-row{
display:grid;grid-template-columns:repeat(6,minmax(0,1fr));
gap:8px;margin-bottom:12px;
@@ -237,7 +238,9 @@
<tr>
<th>类型</th>
<th>标的/合约</th>
<th>初始权利金</th>
<th>盈亏</th>
<th>盈利率</th>
<th>开仓时间</th>
<th>平仓时间</th>
<th>持有</th>
@@ -245,7 +248,7 @@
</tr>
</thead>
<tbody id="or-trades-tbody">
<tr><td colspan="7" class="muted">加载中…</td></tr>
<tr><td colspan="9" class="muted">加载中…</td></tr>
</tbody>
</table>
</div>
@@ -280,6 +283,8 @@
data-lpignore="true" data-1p-ignore="true" data-form-type="other">
<input type="text" id="or-f-pnl" name="or_f_pnl" placeholder="盈亏(U)"
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other">
<input type="text" id="or-f-initial-prem" name="or_f_initial_prem" placeholder="初始权利金(U)" readonly autocomplete="off">
<input type="text" id="or-f-profit-rate" name="or_f_profit_rate" placeholder="盈利率" readonly autocomplete="off">
<input type="text" id="or-f-hold" name="or_f_hold" placeholder="持有时长" readonly autocomplete="off">
</div>
<div class="or-form-grid2">
@@ -403,7 +408,9 @@
<th>类型</th>
<th>标的/合约</th>
<th>方向</th>
<th>初始权利金</th>
<th>盈亏</th>
<th>盈利率</th>
<th>开仓时间</th>
<th>平仓时间</th>
<th>持仓时长</th>
@@ -414,7 +421,7 @@
</tr>
</thead>
<tbody id="or-reviewed-tbody">
<tr><td colspan="11" class="muted">加载中…</td></tr>
<tr><td colspan="13" class="muted">加载中…</td></tr>
</tbody>
</table>
</div>