修复币本位期权历史权利金/盈亏显示0.00;复盘盈亏按指数换算为U

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 21:51:03 +08:00
parent 1ddfe3f72e
commit e261df0009
7 changed files with 319 additions and 21 deletions
+164 -8
View File
@@ -99,8 +99,140 @@ def _purge_review_trade_by_key(conn: sqlite3.Connection, history_key: str) -> bo
return True
def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) -> str:
"""幂等写入纯期权快照;不触碰 options_review_entries;已隐藏的不再导入."""
def _is_coin_option_row(row: dict[str, Any]) -> bool:
ccy = str(row.get("premium_ccy") or "").strip().upper()
if ccy in ("ETH", "BTC"):
return True
if str(row.get("margin_mode") or "").strip().lower() == "coin":
return True
inst = str(row.get("inst_id") or "").strip().upper()
return bool(inst) and "-USD-" in inst and "_UM" not in inst
def _resolve_review_index_px(
row: dict[str, Any],
*,
ex: Any = None,
cache: dict[str, float | None] | None = None,
) -> Optional[float]:
px = _safe_float(row.get("idx_px") or row.get("index_px") or row.get("options_index_px"))
if px is not None and px > 0:
return px
underly = str(row.get("underlying") or "").strip().upper()
if not underly:
inst = str(row.get("inst_id") or "")
underly = (inst.split("-")[0] if inst else "ETH").upper() or "ETH"
if cache is not None and underly in cache:
return cache[underly]
if ex is None:
return None
try:
from lib.exchange.okx_options_lib import fetch_index_price
got = fetch_index_price(ex, underly)
px = _safe_float(got)
if cache is not None:
cache[underly] = px if px is not None and px > 0 else None
return px if px is not None and px > 0 else None
except Exception:
if cache is not None:
cache[underly] = None
return None
def convert_option_amounts_to_usdt(
row: dict[str, Any],
*,
index_px: float | None = None,
ex: Any = None,
cache: dict[str, float | None] | None = None,
) -> dict[str, Any]:
"""币本位权利金/盈亏换算为 USDT;已标记 pnl_quote_ccy=USDT 则跳过."""
out = dict(row)
quote = str(out.get("pnl_quote_ccy") or "").strip().upper()
if quote in ("USDT", "USDC", "U"):
return out
if not _is_coin_option_row(out):
out["pnl_quote_ccy"] = "USDT"
return out
px = index_px if index_px is not None and index_px > 0 else _resolve_review_index_px(
out, ex=ex, cache=cache
)
if px is None or px <= 0:
return out
for key in ("realized_pnl", "premium_paid", "realized_pnl_total"):
v = _safe_float(out.get(key))
if v is not None:
out[key] = round(float(v) * float(px), 4)
out["idx_px"] = float(px)
out["pnl_quote_ccy"] = "USDT"
out["premium_ccy"] = "USDC"
return out
def repair_coin_review_rows_to_usdt(
conn: sqlite3.Connection,
*,
ex: Any = None,
) -> int:
"""把仍按币计价落库的复盘纯期权行换算成 U(幂等)."""
init_options_review_tables(conn)
rows = conn.execute(
"""
SELECT * FROM options_review_trades
WHERE source_type = ?
AND (pnl_quote_ccy IS NULL OR TRIM(pnl_quote_ccy) = '' OR UPPER(pnl_quote_ccy) NOT IN ('USDT','USDC','U'))
ORDER BY id DESC
LIMIT 500
""",
(SOURCE_OPTION,),
).fetchall()
cache: dict[str, float | None] = {}
fixed = 0
for raw in rows:
row = dict(raw)
if not _is_coin_option_row(row):
conn.execute(
"UPDATE options_review_trades SET pnl_quote_ccy='USDT' WHERE id=?",
(int(row["id"]),),
)
continue
converted = convert_option_amounts_to_usdt(row, ex=ex, cache=cache)
if str(converted.get("pnl_quote_ccy") or "").upper() not in ("USDT", "USDC", "U"):
continue
conn.execute(
"""
UPDATE options_review_trades
SET premium_paid=?, realized_pnl=?, realized_pnl_total=?,
premium_ccy=?, pnl_quote_ccy=?, idx_px=?
WHERE id=?
""",
(
converted.get("premium_paid"),
converted.get("realized_pnl"),
converted.get("realized_pnl")
if converted.get("realized_pnl") is not None
else converted.get("realized_pnl_total"),
converted.get("premium_ccy") or "USDC",
"USDT",
converted.get("idx_px"),
int(row["id"]),
),
)
fixed += 1
return fixed
def upsert_option_history_row(
conn: sqlite3.Connection,
row: dict[str, Any],
*,
ex: Any = None,
index_cache: dict[str, float | None] | None = None,
) -> str:
"""幂等写入纯期权快照;不触碰 options_review_entries;已隐藏的不再导入.
币本位金额在写入前换算为 USDT.
"""
history_key = str(row.get("history_key") or "").strip()
if not history_key:
return "skip"
@@ -112,6 +244,7 @@ def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) ->
):
# 若此前已导入,清掉,避免列表残留
return "purged" if _purge_review_trade_by_key(conn, history_key) else "hidden"
row = convert_option_amounts_to_usdt(row, ex=ex, cache=index_cache)
opened_at = row.get("created_at") or row.get("opened_at")
closed_at = row.get("closed_at")
pnl = _safe_float(row.get("realized_pnl"))
@@ -139,6 +272,9 @@ 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,
"premium_ccy": str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC",
"pnl_quote_ccy": str(row.get("pnl_quote_ccy") or "USDT").strip().upper() or "USDT",
"idx_px": _safe_float(row.get("idx_px")),
}
cols = list(fields.keys())
if existing:
@@ -271,8 +407,14 @@ def hide_review_trade(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any]
def sync_options_from_local_trades(conn: sqlite3.Connection) -> dict[str, Any]:
"""从本地 options_trades 已平仓记录导入复盘快照(不访问交易所)."""
def sync_options_from_local_trades(
conn: sqlite3.Connection,
*,
ex: Any = None,
) -> dict[str, Any]:
"""从本地 options_trades 已平仓记录导入复盘快照(不访问交易所).
币本位金额按指数换算为 USDT 后入库.
"""
init_options_review_tables(conn)
from lib.options.options_db import init_options_tables
@@ -281,7 +423,8 @@ def sync_options_from_local_trades(conn: sqlite3.Connection) -> dict[str, Any]:
"""
SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets,
open_quote, close_quote, premium_paid, realized_pnl,
created_at, closed_at, signal_note, status
created_at, closed_at, signal_note, status,
margin_mode, premium_ccy
FROM options_trades
WHERE status = 'closed'
ORDER BY id DESC
@@ -289,6 +432,7 @@ def sync_options_from_local_trades(conn: sqlite3.Connection) -> dict[str, Any]:
"""
).fetchall()
inserted = updated = skipped = 0
index_cache: dict[str, float | None] = {}
for r in rows:
trade_id = int(r["id"])
history_key = f"local_opt:{trade_id}"
@@ -313,7 +457,11 @@ def sync_options_from_local_trades(conn: sqlite3.Connection) -> dict[str, Any]:
"created_at": opened_at,
"closed_at": closed_at,
"status_label": "已平",
"margin_mode": r["margin_mode"] if "margin_mode" in r.keys() else None,
"premium_ccy": r["premium_ccy"] if "premium_ccy" in r.keys() else None,
},
ex=ex,
index_cache=index_cache,
)
if action == "inserted":
inserted += 1
@@ -354,6 +502,7 @@ def sync_options_from_exchange(
fmt = format_fn or format_option_history_row
raw_rows = fetch(ex, limit=limit)
meta_cache: dict[str, dict[str, Any] | None] = {}
index_cache: dict[str, float | None] = {}
inserted = updated = skipped = 0
for raw in raw_rows:
inst_id = str(raw.get("instId") or "").strip()
@@ -363,7 +512,9 @@ def sync_options_from_exchange(
except Exception:
pass
formatted = fmt(raw, tick_sz=tick_sz, ct_mult=ct_mult)
action = upsert_option_history_row(conn, formatted)
action = upsert_option_history_row(
conn, formatted, ex=ex, index_cache=index_cache
)
if action == "inserted":
inserted += 1
elif action == "updated":
@@ -552,7 +703,7 @@ def sync_all_review_sources(
conn, ex, limit=options_limit, fetch_fn=fetch_fn, format_fn=format_fn
)
else:
out["options"] = sync_options_from_local_trades(conn)
out["options"] = sync_options_from_local_trades(conn, ex=ex)
out["hedge"] = sync_hedge_plans_closed(conn)
return out
@@ -579,7 +730,12 @@ def ensure_local_review_synced(
backfill_hedge_option_legs_realized_pnl(conn, hist)
except Exception:
pass
return sync_all_review_sources(conn, from_exchange=False)
out = sync_all_review_sources(conn, ex=ex, from_exchange=False)
try:
out["repaired_usdt"] = repair_coin_review_rows_to_usdt(conn, ex=ex)
except Exception:
out["repaired_usdt"] = 0
return out
def _row_to_dict(row: Any) -> dict[str, Any]: