统计已实现盈亏扣手续费,并按平仓时指数冻结为 U。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -377,41 +377,145 @@ def close_option_by_bid1(
|
||||
|
||||
open_rows = conn.execute(
|
||||
"""
|
||||
SELECT id, premium_paid FROM options_trades
|
||||
SELECT id, premium_paid, open_fee, premium_ccy, underlying,
|
||||
exchange_ord_id
|
||||
FROM options_trades
|
||||
WHERE inst_id = ? AND status = 'open'
|
||||
ORDER BY id ASC
|
||||
""",
|
||||
(inst_id,),
|
||||
).fetchall()
|
||||
from lib.options.options_pnl_settle_lib import (
|
||||
extract_order_fee,
|
||||
fee_to_premium_ccy,
|
||||
resolve_close_index_px,
|
||||
settle_closed_pnl,
|
||||
sum_option_order_fees_in_premium,
|
||||
)
|
||||
from lib.options.options_time_lib import options_app_now_str
|
||||
|
||||
row_ccy = premium_ccy
|
||||
if open_rows:
|
||||
row_ccy = str(open_rows[0]["premium_ccy"] or premium_ccy or "").strip() or premium_ccy
|
||||
underly = str((open_rows[0]["underlying"] if open_rows else "") or "").strip()
|
||||
close_idx = resolve_close_index_px(
|
||||
ex, inst_id=inst_id, underlying=underly, hint=index_px
|
||||
)
|
||||
# 平仓手续费:优先累计订单号查单;查不到再用本笔下单 info
|
||||
close_fee_total = 0.0
|
||||
try:
|
||||
close_fee_total = sum_option_order_fees_in_premium(
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
ord_ids=merged_ord,
|
||||
premium_ccy=row_ccy,
|
||||
index_px=close_idx,
|
||||
fetch_order_fn=cfg.get("fetch_option_order"),
|
||||
)
|
||||
except Exception:
|
||||
close_fee_total = 0.0
|
||||
if close_fee_total <= 0:
|
||||
fee_hint, fee_hint_ccy = extract_order_fee(
|
||||
order if isinstance(order, dict) else None
|
||||
)
|
||||
if fee_hint > 0:
|
||||
close_fee_total = fee_to_premium_ccy(
|
||||
fee_hint,
|
||||
fee_hint_ccy or row_ccy,
|
||||
premium_ccy=row_ccy,
|
||||
index_px=close_idx,
|
||||
)
|
||||
|
||||
total_paid = sum(float(r["premium_paid"] or 0) for r in open_rows)
|
||||
total_open_fee = sum(float(r["open_fee"] or 0) for r in open_rows)
|
||||
# 开仓费未落库时按开仓单号补查
|
||||
if total_open_fee <= 0:
|
||||
open_oids = [
|
||||
str(r["exchange_ord_id"] or "").strip()
|
||||
for r in open_rows
|
||||
if str(r["exchange_ord_id"] or "").strip()
|
||||
]
|
||||
try:
|
||||
total_open_fee = sum_option_order_fees_in_premium(
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
ord_ids=open_oids,
|
||||
premium_ccy=row_ccy,
|
||||
index_px=close_idx,
|
||||
fetch_order_fn=cfg.get("fetch_option_order"),
|
||||
)
|
||||
except Exception:
|
||||
total_open_fee = 0.0
|
||||
|
||||
allocated = 0.0
|
||||
allocated_cfee = 0.0
|
||||
allocated_ofee = 0.0
|
||||
for i, row in enumerate(open_rows):
|
||||
paid = float(row["premium_paid"] or 0)
|
||||
if i == len(open_rows) - 1:
|
||||
recv = round_premium_amount(
|
||||
total_recv - allocated, decimals=PREMIUM_DECIMALS_COIN
|
||||
) or 0.0
|
||||
row_cfee = round(close_fee_total - allocated_cfee, 8)
|
||||
stored_ofee = float(row["open_fee"] or 0)
|
||||
if stored_ofee > 0:
|
||||
row_ofee = stored_ofee
|
||||
elif total_paid > 0 and total_open_fee > 0:
|
||||
row_ofee = round(total_open_fee - allocated_ofee, 8)
|
||||
else:
|
||||
row_ofee = round(total_open_fee - allocated_ofee, 8) if total_open_fee > 0 else 0.0
|
||||
elif total_paid > 0:
|
||||
share = paid / total_paid
|
||||
recv = round_premium_amount(
|
||||
total_recv * (paid / total_paid), decimals=PREMIUM_DECIMALS_COIN
|
||||
total_recv * share, decimals=PREMIUM_DECIMALS_COIN
|
||||
) or 0.0
|
||||
allocated += recv
|
||||
row_cfee = round(close_fee_total * share, 8)
|
||||
allocated_cfee += row_cfee
|
||||
stored_ofee = float(row["open_fee"] or 0)
|
||||
if stored_ofee > 0:
|
||||
row_ofee = stored_ofee
|
||||
else:
|
||||
row_ofee = round(total_open_fee * share, 8)
|
||||
allocated_ofee += row_ofee
|
||||
else:
|
||||
recv = round_premium_amount(
|
||||
total_recv / len(open_rows), decimals=PREMIUM_DECIMALS_COIN
|
||||
) or 0.0
|
||||
allocated += recv
|
||||
pnl = round_premium_amount(recv - paid, decimals=PREMIUM_DECIMALS_COIN) or 0.0
|
||||
note_sql = ""
|
||||
from lib.options.options_time_lib import options_app_now_str
|
||||
row_cfee = round(close_fee_total / len(open_rows), 8)
|
||||
allocated_cfee += row_cfee
|
||||
stored_ofee = float(row["open_fee"] or 0)
|
||||
row_ofee = stored_ofee if stored_ofee > 0 else round(
|
||||
total_open_fee / len(open_rows), 8
|
||||
)
|
||||
if stored_ofee <= 0:
|
||||
allocated_ofee += row_ofee
|
||||
|
||||
# 占位顺序必须与 SQL 一致: close_quote/recv/pnl/ord/closed_at/[signal_note]/id
|
||||
settled = settle_closed_pnl(
|
||||
premium_paid=paid,
|
||||
premium_received=recv,
|
||||
open_fee=row_ofee,
|
||||
close_fee=row_cfee,
|
||||
premium_ccy=str(row["premium_ccy"] or row_ccy or premium_ccy),
|
||||
index_px=close_idx,
|
||||
)
|
||||
pnl = settled.get("realized_pnl")
|
||||
pnl_usdt = settled.get("realized_pnl_usdt")
|
||||
note_sql = ""
|
||||
|
||||
# 占位: close_quote/recv/pnl/ord/closed_at/open_fee/close_fee/fee_ccy/idx/pnl_u/[note]/id
|
||||
params: list[Any] = [
|
||||
close_px,
|
||||
recv,
|
||||
pnl,
|
||||
merged_ord,
|
||||
options_app_now_str(),
|
||||
row_ofee,
|
||||
row_cfee,
|
||||
str(row["premium_ccy"] or row_ccy or premium_ccy or ""),
|
||||
settled.get("close_index_px"),
|
||||
pnl_usdt,
|
||||
]
|
||||
if signal_note and i == len(open_rows) - 1:
|
||||
note_sql = """,
|
||||
@@ -426,6 +530,8 @@ def close_option_by_bid1(
|
||||
UPDATE options_trades
|
||||
SET status = 'closed', close_quote = ?, premium_received = ?,
|
||||
realized_pnl = ?, close_ord_id = ?, closed_at = ?,
|
||||
open_fee = ?, close_fee = ?, fee_ccy = ?,
|
||||
close_index_px = ?, realized_pnl_usdt = ?,
|
||||
close_recv_accum = 0, close_sheets_accum = 0,
|
||||
close_notional_accum = 0
|
||||
{note_sql}
|
||||
|
||||
@@ -424,6 +424,41 @@ def open_coin_option_buy_full(
|
||||
premium_paid = round(float(ask) * eth_amount, 8)
|
||||
premium_ccy = premium_ccy_for_mode("coin", underlying)
|
||||
|
||||
# 开仓手续费(折权利金单位);优先成交回包,否则查单
|
||||
open_fee = 0.0
|
||||
try:
|
||||
from lib.options.options_pnl_settle_lib import (
|
||||
extract_order_fee,
|
||||
fee_to_premium_ccy,
|
||||
resolve_close_index_px,
|
||||
sum_option_order_fees_in_premium,
|
||||
)
|
||||
|
||||
idx_open = resolve_close_index_px(
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
underlying=underlying,
|
||||
hint=(q.get("index_px") if isinstance(q, dict) else None),
|
||||
)
|
||||
fee_hint, fee_ccy_hint = extract_order_fee(order if isinstance(order, dict) else None)
|
||||
if fee_hint <= 0 and isinstance(fill, dict):
|
||||
fee_hint, fee_ccy_hint = extract_order_fee(fill.get("order") if isinstance(fill.get("order"), dict) else fill)
|
||||
if fee_hint > 0:
|
||||
open_fee = fee_to_premium_ccy(
|
||||
fee_hint, fee_ccy_hint or premium_ccy, premium_ccy=premium_ccy, index_px=idx_open
|
||||
)
|
||||
if open_fee <= 0:
|
||||
open_fee = sum_option_order_fees_in_premium(
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
ord_ids=[ord_id],
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=idx_open,
|
||||
fetch_order_fn=cfg.get("fetch_option_order"),
|
||||
)
|
||||
except Exception:
|
||||
open_fee = 0.0
|
||||
|
||||
# 部分成交/减半张数后卖掉多余买币(按 bridge coin_bought 封顶)
|
||||
need_coin = float(premium_paid)
|
||||
try:
|
||||
@@ -464,6 +499,7 @@ def open_coin_option_buy_full(
|
||||
bridge_id=bridge_id,
|
||||
budget_usdt=buy_usdt,
|
||||
premium_ccy=premium_ccy,
|
||||
open_fee=open_fee,
|
||||
profit_exit_enabled=profit_exit_enabled,
|
||||
profit_exit_mult=profit_exit_mult,
|
||||
)
|
||||
@@ -533,14 +569,19 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
||||
pe = 1 if kwargs.get("profit_exit_enabled") else 0
|
||||
pe_mult = float(kwargs.get("profit_exit_mult") or 1.0)
|
||||
pe_state = "active" if pe else "idle"
|
||||
open_fee = float(kwargs.get("open_fee") or 0.0)
|
||||
if open_fee < 0:
|
||||
open_fee = abs(open_fee)
|
||||
fee_ccy = str(kwargs.get("premium_ccy") or "ETH")
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO options_trades(
|
||||
inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount,
|
||||
open_quote, premium_paid, status, signal_note, exchange_ord_id,
|
||||
margin_mode, premium_ccy, bridge_id, budget_usdt,
|
||||
open_fee, fee_ccy,
|
||||
profit_exit_enabled, profit_exit_mult, profit_exit_state, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
kwargs["inst_id"],
|
||||
@@ -557,6 +598,8 @@ def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
|
||||
kwargs.get("premium_ccy") or "ETH",
|
||||
kwargs.get("bridge_id"),
|
||||
kwargs.get("budget_usdt"),
|
||||
open_fee,
|
||||
fee_ccy,
|
||||
pe,
|
||||
pe_mult,
|
||||
pe_state,
|
||||
|
||||
@@ -109,6 +109,12 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
||||
"ALTER TABLE options_trades ADD COLUMN close_recv_accum REAL DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN close_sheets_accum INTEGER DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN close_notional_accum REAL DEFAULT 0",
|
||||
# 手续费(权利金单位)与平仓冻结 U 口径
|
||||
"ALTER TABLE options_trades ADD COLUMN open_fee REAL DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN close_fee REAL DEFAULT 0",
|
||||
"ALTER TABLE options_trades ADD COLUMN fee_ccy TEXT",
|
||||
"ALTER TABLE options_trades ADD COLUMN close_index_px REAL",
|
||||
"ALTER TABLE options_trades ADD COLUMN realized_pnl_usdt REAL",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
|
||||
@@ -187,7 +187,8 @@ def backfill_closed_options_realized_pnl_from_history(
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, inst_id, sheets, premium_paid, realized_pnl, close_quote,
|
||||
created_at, closed_at, close_ord_id
|
||||
created_at, closed_at, close_ord_id, realized_pnl_usdt,
|
||||
open_fee, close_fee, premium_ccy, underlying
|
||||
FROM options_trades
|
||||
WHERE status = 'closed'
|
||||
ORDER BY id DESC
|
||||
@@ -198,8 +199,13 @@ def backfill_closed_options_realized_pnl_from_history(
|
||||
if not rows:
|
||||
return 0
|
||||
|
||||
from lib.options.options_pnl_settle_lib import resolve_close_index_px, settle_closed_pnl
|
||||
|
||||
groups: dict[str, list[Any]] = {}
|
||||
for row in rows:
|
||||
# 已冻结 U 的本地结算不再被交易所历史覆盖
|
||||
if _safe_float(row["realized_pnl_usdt"]) is not None:
|
||||
continue
|
||||
inst = str(row["inst_id"] or "").strip()
|
||||
if not inst or inst not in by_inst:
|
||||
continue
|
||||
@@ -233,6 +239,8 @@ def backfill_closed_options_realized_pnl_from_history(
|
||||
total_paid = 0.0
|
||||
for r in group:
|
||||
total_paid += float(_safe_float(r["premium_paid"]) or 0.0)
|
||||
underly = str(group[0]["underlying"] or "").strip()
|
||||
close_idx = resolve_close_index_px(None, inst_id=inst, underlying=underly)
|
||||
allocated = 0.0
|
||||
for i, r in enumerate(group):
|
||||
paid = float(_safe_float(r["premium_paid"]) or 0.0)
|
||||
@@ -255,16 +263,37 @@ def backfill_closed_options_realized_pnl_from_history(
|
||||
if pnl_ok and quote_ok and ord_ok:
|
||||
continue
|
||||
prem_recv = round(paid + share, 4)
|
||||
ofee = float(_safe_float(r["open_fee"]) or 0.0)
|
||||
cfee = float(_safe_float(r["close_fee"]) or 0.0)
|
||||
prem_ccy = str(r["premium_ccy"] or "").strip() or "ETH"
|
||||
settled = settle_closed_pnl(
|
||||
premium_paid=paid,
|
||||
premium_received=prem_recv,
|
||||
open_fee=ofee,
|
||||
close_fee=cfee,
|
||||
premium_ccy=prem_ccy,
|
||||
index_px=close_idx,
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_trades
|
||||
SET realized_pnl = ?,
|
||||
premium_received = ?,
|
||||
close_quote = COALESCE(?, close_quote),
|
||||
close_ord_id = COALESCE(?, close_ord_id)
|
||||
WHERE id = ?
|
||||
close_ord_id = COALESCE(?, close_ord_id),
|
||||
close_index_px = COALESCE(?, close_index_px),
|
||||
realized_pnl_usdt = COALESCE(?, realized_pnl_usdt)
|
||||
WHERE id = ? AND realized_pnl_usdt IS NULL
|
||||
""",
|
||||
(share, prem_recv, close_quote, matched_pos, int(r["id"])),
|
||||
(
|
||||
settled.get("realized_pnl"),
|
||||
prem_recv,
|
||||
close_quote,
|
||||
matched_pos,
|
||||
settled.get("close_index_px"),
|
||||
settled.get("realized_pnl_usdt"),
|
||||
int(r["id"]),
|
||||
),
|
||||
)
|
||||
updated += 1
|
||||
return updated
|
||||
@@ -283,7 +312,8 @@ def sync_open_options_trades(
|
||||
"""
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, inst_id, premium_paid, exp_time, created_at
|
||||
SELECT id, inst_id, premium_paid, exp_time, created_at,
|
||||
open_fee, premium_ccy, underlying, exchange_ord_id
|
||||
FROM options_trades
|
||||
WHERE status = 'open'
|
||||
"""
|
||||
@@ -291,6 +321,8 @@ def sync_open_options_trades(
|
||||
updated = 0
|
||||
closed_inst_ids: list[str] = []
|
||||
now_ms = int(time.time() * 1000)
|
||||
from lib.options.options_pnl_settle_lib import resolve_close_index_px, settle_closed_pnl
|
||||
|
||||
for row in rows:
|
||||
inst_id = str(row["inst_id"] or "")
|
||||
if not inst_id or inst_id in live_inst_ids:
|
||||
@@ -331,6 +363,24 @@ def sync_open_options_trades(
|
||||
if not closed_at:
|
||||
closed_at = options_app_now_str()
|
||||
|
||||
open_fee = float(_safe_float(row["open_fee"]) or 0.0)
|
||||
prem_ccy = str(row["premium_ccy"] or "").strip() or "ETH"
|
||||
underly = str(row["underlying"] or "").strip()
|
||||
close_idx = resolve_close_index_px(None, inst_id=inst_id, underlying=underly)
|
||||
# 交易所 pnl 多为权利金差;再扣本地开仓费后冻结(平仓费此处无单号,记 0)
|
||||
if prem_recv is None and realized_pnl is not None:
|
||||
prem_recv = round(paid + float(realized_pnl), 4)
|
||||
settled = settle_closed_pnl(
|
||||
premium_paid=paid,
|
||||
premium_received=prem_recv if prem_recv is not None else (paid + float(realized_pnl or 0)),
|
||||
open_fee=open_fee,
|
||||
close_fee=0,
|
||||
premium_ccy=prem_ccy,
|
||||
index_px=close_idx,
|
||||
)
|
||||
realized_pnl = settled.get("realized_pnl")
|
||||
pnl_usdt = settled.get("realized_pnl_usdt")
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_trades
|
||||
@@ -340,6 +390,9 @@ def sync_open_options_trades(
|
||||
realized_pnl = ?,
|
||||
close_ord_id = COALESCE(?, close_ord_id),
|
||||
closed_at = COALESCE(?, closed_at),
|
||||
close_index_px = COALESCE(?, close_index_px),
|
||||
realized_pnl_usdt = COALESCE(?, realized_pnl_usdt),
|
||||
fee_ccy = COALESCE(fee_ccy, ?),
|
||||
signal_note = CASE
|
||||
WHEN ? = 'expired' AND (signal_note IS NULL OR TRIM(signal_note) = '')
|
||||
THEN '到期结算'
|
||||
@@ -353,6 +406,9 @@ def sync_open_options_trades(
|
||||
realized_pnl,
|
||||
close_ord_id,
|
||||
closed_at,
|
||||
settled.get("close_index_px"),
|
||||
pnl_usdt,
|
||||
prem_ccy,
|
||||
close_reason,
|
||||
int(row["id"]),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""已实现盈亏结算:扣手续费,并在平仓时按指数冻结 U 口径."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _safe_float(v: Any) -> float | None:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def fee_amount_abs(fee: Any) -> float:
|
||||
"""OKX 扣费多为负;统一为正数「已付手续费」."""
|
||||
f = _safe_float(fee)
|
||||
if f is None:
|
||||
return 0.0
|
||||
return abs(float(f))
|
||||
|
||||
|
||||
def fee_to_premium_ccy(
|
||||
fee_amt: Any,
|
||||
fee_ccy: Any,
|
||||
*,
|
||||
premium_ccy: str,
|
||||
index_px: float | None,
|
||||
) -> float:
|
||||
"""把手续费折成权利金币种数量(与 premium_paid/received 同单位)."""
|
||||
amt = fee_amount_abs(fee_amt)
|
||||
if amt <= 0:
|
||||
return 0.0
|
||||
fc = str(fee_ccy or "").strip().upper()
|
||||
pc = str(premium_ccy or "").strip().upper()
|
||||
if not fc or not pc or fc == pc:
|
||||
return round(amt, 8)
|
||||
# 稳定币互兑近似 1:1
|
||||
stables = {"USDT", "USDC", "USD"}
|
||||
if fc in stables and pc in stables:
|
||||
return round(amt, 8)
|
||||
idx = _safe_float(index_px)
|
||||
if idx is None or idx <= 0:
|
||||
return 0.0
|
||||
if fc in stables and pc in ("ETH", "BTC"):
|
||||
return round(amt / float(idx), 8)
|
||||
if fc in ("ETH", "BTC") and pc in stables:
|
||||
return round(amt * float(idx), 8)
|
||||
if fc in ("ETH", "BTC") and pc in ("ETH", "BTC") and fc != pc:
|
||||
# 跨币种无可靠汇率时放弃折算,避免脏数据
|
||||
return 0.0
|
||||
return round(amt, 8)
|
||||
|
||||
|
||||
def extract_order_fee(order: dict[str, Any] | None) -> tuple[float, str]:
|
||||
"""从下单/查单结果取手续费(原始币种,正数)."""
|
||||
if not isinstance(order, dict):
|
||||
return 0.0, ""
|
||||
raw = order.get("raw") if isinstance(order.get("raw"), dict) else {}
|
||||
info = order.get("info") if isinstance(order.get("info"), dict) else {}
|
||||
fee = order.get("fee")
|
||||
if fee is None:
|
||||
fee = raw.get("fee")
|
||||
if fee is None:
|
||||
fee = info.get("fee")
|
||||
ccy = (
|
||||
order.get("fee_ccy")
|
||||
or raw.get("feeCcy")
|
||||
or raw.get("fee_ccy")
|
||||
or info.get("fee_ccy")
|
||||
or info.get("feeCcy")
|
||||
or ""
|
||||
)
|
||||
return fee_amount_abs(fee), str(ccy or "").strip().upper()
|
||||
|
||||
|
||||
def sum_option_order_fees_in_premium(
|
||||
ex: Any,
|
||||
*,
|
||||
inst_id: str,
|
||||
ord_ids: list[str] | str | None,
|
||||
premium_ccy: str,
|
||||
index_px: float | None,
|
||||
fetch_order_fn: Any = None,
|
||||
) -> float:
|
||||
"""按订单号汇总手续费并折成权利金单位."""
|
||||
if isinstance(ord_ids, str):
|
||||
ids = [p.strip() for p in ord_ids.split(",") if p.strip()]
|
||||
else:
|
||||
ids = [str(x).strip() for x in (ord_ids or []) if str(x).strip()]
|
||||
if not ids:
|
||||
return 0.0
|
||||
if fetch_order_fn is None:
|
||||
from lib.exchange.okx_options_lib import fetch_option_order
|
||||
|
||||
fetch_order_fn = fetch_option_order
|
||||
total = 0.0
|
||||
seen: set[str] = set()
|
||||
for oid in ids:
|
||||
if oid in seen:
|
||||
continue
|
||||
seen.add(oid)
|
||||
try:
|
||||
info = fetch_order_fn(ex, inst_id=inst_id, ord_id=oid)
|
||||
except Exception:
|
||||
continue
|
||||
if not isinstance(info, dict) or not info.get("ok"):
|
||||
continue
|
||||
fee_amt, fee_ccy = extract_order_fee(info)
|
||||
total += fee_to_premium_ccy(
|
||||
fee_amt, fee_ccy, premium_ccy=premium_ccy, index_px=index_px
|
||||
)
|
||||
return round(total, 8)
|
||||
|
||||
|
||||
def settle_closed_pnl(
|
||||
*,
|
||||
premium_paid: Any,
|
||||
premium_received: Any,
|
||||
open_fee: Any = 0,
|
||||
close_fee: Any = 0,
|
||||
premium_ccy: str,
|
||||
index_px: float | None,
|
||||
) -> dict[str, float | None]:
|
||||
"""
|
||||
净盈亏 = 收回 − 已付 − 开/平手续费(权利金单位).
|
||||
币本位再乘平仓指数得到冻结 U;USDC/USDT 直接冻结.
|
||||
"""
|
||||
paid = float(_safe_float(premium_paid) or 0.0)
|
||||
recv = float(_safe_float(premium_received) or 0.0)
|
||||
ofee = float(_safe_float(open_fee) or 0.0)
|
||||
cfee = float(_safe_float(close_fee) or 0.0)
|
||||
if ofee < 0:
|
||||
ofee = abs(ofee)
|
||||
if cfee < 0:
|
||||
cfee = abs(cfee)
|
||||
net = round(recv - paid - ofee - cfee, 8)
|
||||
pc = str(premium_ccy or "").strip().upper()
|
||||
idx = _safe_float(index_px)
|
||||
if pc in ("ETH", "BTC"):
|
||||
if idx is None or idx <= 0:
|
||||
return {
|
||||
"realized_pnl": net,
|
||||
"close_index_px": None,
|
||||
"realized_pnl_usdt": None,
|
||||
"total_fee": round(ofee + cfee, 8),
|
||||
}
|
||||
return {
|
||||
"realized_pnl": net,
|
||||
"close_index_px": float(idx),
|
||||
"realized_pnl_usdt": round(net * float(idx), 4),
|
||||
"total_fee": round(ofee + cfee, 8),
|
||||
}
|
||||
# USDC / USDT 权利金
|
||||
return {
|
||||
"realized_pnl": net,
|
||||
"close_index_px": float(idx) if idx is not None and idx > 0 else None,
|
||||
"realized_pnl_usdt": round(net, 4),
|
||||
"total_fee": round(ofee + cfee, 8),
|
||||
}
|
||||
|
||||
|
||||
def resolve_close_index_px(
|
||||
ex: Any,
|
||||
*,
|
||||
inst_id: str = "",
|
||||
underlying: str = "",
|
||||
hint: Any = None,
|
||||
) -> float | None:
|
||||
"""平仓冻结用指数:优先调用方传入,否则拉标的近似价."""
|
||||
h = _safe_float(hint)
|
||||
if h is not None and h > 0:
|
||||
return float(h)
|
||||
underly = (underlying or "").strip().upper()
|
||||
if not underly and inst_id:
|
||||
underly = str(inst_id).split("-")[0].strip().upper() or "ETH"
|
||||
if not underly:
|
||||
underly = "ETH"
|
||||
try:
|
||||
from lib.options.options_stats_lib import _underlying_index_usdt
|
||||
|
||||
return _underlying_index_usdt(ex, underly)
|
||||
except Exception:
|
||||
return None
|
||||
@@ -93,6 +93,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
format_position_row,
|
||||
options_api_ready,
|
||||
cancel_option_order,
|
||||
fetch_option_order,
|
||||
fetch_option_pending_orders,
|
||||
place_option_limit_order,
|
||||
place_option_market_order,
|
||||
@@ -132,6 +133,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"fetch_option_book_depth": fetch_option_book_depth,
|
||||
"place_option_limit_order": place_option_limit_order,
|
||||
"place_option_market_order": place_option_market_order,
|
||||
"fetch_option_order": fetch_option_order,
|
||||
"fetch_option_pending_orders": fetch_option_pending_orders,
|
||||
"cancel_option_order": cancel_option_order,
|
||||
"fetch_option_positions": fetch_option_positions,
|
||||
@@ -1276,20 +1278,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
ex, err = _require_options_ex(cfg)
|
||||
if ex is None:
|
||||
return jsonify({"ok": False, "msg": err})
|
||||
from lib.options.options_history_lib import load_options_history
|
||||
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
||||
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
||||
from lib.options.options_stats_lib import (
|
||||
compute_options_stats_from_history,
|
||||
history_pnl_to_usdt,
|
||||
)
|
||||
from lib.options.options_stats_lib import compute_options_stats
|
||||
|
||||
raw_live = cfg["fetch_option_positions"](ex)
|
||||
if raw_live is None:
|
||||
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
||||
history = load_options_history(ex, cfg)
|
||||
# 币本位已平盈亏按指数折 U,与持仓浮盈/合计口径一致
|
||||
stats = compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||
# 已平仓走本地库:扣手续费 + 平仓时冻结 U,不跟实时指数漂移
|
||||
stats = compute_options_stats(cfg["get_db"], ex)
|
||||
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
|
||||
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
|
||||
total_pnl = None
|
||||
|
||||
@@ -61,7 +61,8 @@ def _underlying_index_usdt(ex: Any, underly: str) -> float | None:
|
||||
|
||||
def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
统计用:币本位 realized_pnl(ETH/BTC) 按指数折成 U;USDC 原样.
|
||||
统计用:优先已冻结的 realized_pnl_usdt(平仓当时口径,含手续费后净额);
|
||||
否则币本位用 close_index_px(再否则实时指数)折 U;USDC 原样.
|
||||
折算失败的币仓剔除盈亏字段,避免把「币数量」当成 U.
|
||||
"""
|
||||
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
|
||||
@@ -73,6 +74,11 @@ def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[d
|
||||
if r.get("status") == "open":
|
||||
out.append(r)
|
||||
continue
|
||||
frozen = _safe_float(r.get("realized_pnl_usdt"))
|
||||
if frozen is not None:
|
||||
r["realized_pnl"] = round(float(frozen), 4)
|
||||
out.append(r)
|
||||
continue
|
||||
pnl = _safe_float(r.get("realized_pnl"))
|
||||
if pnl is None:
|
||||
out.append(r)
|
||||
@@ -83,9 +89,11 @@ def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[d
|
||||
if not ccy:
|
||||
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly)
|
||||
if ccy in ("ETH", "BTC"):
|
||||
if underly not in idx_cache:
|
||||
idx_cache[underly] = _underlying_index_usdt(ex, underly)
|
||||
idx = idx_cache.get(underly)
|
||||
idx = _safe_float(r.get("close_index_px"))
|
||||
if idx is None or idx <= 0:
|
||||
if underly not in idx_cache:
|
||||
idx_cache[underly] = _underlying_index_usdt(ex, underly)
|
||||
idx = idx_cache.get(underly)
|
||||
if idx is None or idx <= 0:
|
||||
r["realized_pnl"] = None
|
||||
else:
|
||||
@@ -278,6 +286,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"),
|
||||
"close_index_px": _row_get(row, "close_index_px"),
|
||||
"created_at": _row_get(row, "created_at"),
|
||||
"closed_at": _row_get(row, "closed_at"),
|
||||
"inst_id": _row_get(row, "inst_id") or "",
|
||||
@@ -288,26 +298,118 @@ def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
_CLOSED_STATS_SELECT = """
|
||||
SELECT realized_pnl, realized_pnl_usdt, close_index_px,
|
||||
created_at, closed_at, inst_id, underlying, premium_ccy
|
||||
FROM options_trades
|
||||
WHERE status = 'closed'
|
||||
AND (realized_pnl IS NOT NULL OR realized_pnl_usdt IS NOT NULL)
|
||||
"""
|
||||
|
||||
|
||||
def freeze_missing_realized_pnl_usdt(conn, ex: Any = None) -> int:
|
||||
"""旧已平仓缺冻结 U 时按 close_index/当前指数写一次,之后不再跟盘变."""
|
||||
init_options_tables(conn)
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, realized_pnl, realized_pnl_usdt, close_index_px,
|
||||
inst_id, underlying, premium_ccy, premium_paid, premium_received,
|
||||
open_fee, close_fee
|
||||
FROM options_trades
|
||||
WHERE status = 'closed'
|
||||
AND realized_pnl_usdt IS NULL
|
||||
AND realized_pnl IS NOT NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 300
|
||||
"""
|
||||
).fetchall()
|
||||
if not rows:
|
||||
return 0
|
||||
from lib.options.options_pnl_settle_lib import settle_closed_pnl, resolve_close_index_px
|
||||
|
||||
updated = 0
|
||||
for row in rows:
|
||||
ccy = str(_row_get(row, "premium_ccy") or "").strip().upper()
|
||||
idx = _safe_float(_row_get(row, "close_index_px"))
|
||||
if idx is None or idx <= 0:
|
||||
idx = resolve_close_index_px(
|
||||
ex,
|
||||
inst_id=str(_row_get(row, "inst_id") or ""),
|
||||
underlying=str(_row_get(row, "underlying") or ""),
|
||||
)
|
||||
# 有手续费列时按净额重算;否则用已有 realized_pnl 冻结
|
||||
ofee = _safe_float(_row_get(row, "open_fee")) or 0.0
|
||||
cfee = _safe_float(_row_get(row, "close_fee")) or 0.0
|
||||
paid = _row_get(row, "premium_paid")
|
||||
recv = _row_get(row, "premium_received")
|
||||
if paid is not None and recv is not None and (ofee > 0 or cfee > 0):
|
||||
settled = settle_closed_pnl(
|
||||
premium_paid=paid,
|
||||
premium_received=recv,
|
||||
open_fee=ofee,
|
||||
close_fee=cfee,
|
||||
premium_ccy=ccy or "USDC",
|
||||
index_px=idx,
|
||||
)
|
||||
pnl = settled.get("realized_pnl")
|
||||
usdt = settled.get("realized_pnl_usdt")
|
||||
close_idx = settled.get("close_index_px")
|
||||
else:
|
||||
pnl = _safe_float(_row_get(row, "realized_pnl"))
|
||||
if pnl is None:
|
||||
continue
|
||||
settled = settle_closed_pnl(
|
||||
premium_paid=0,
|
||||
premium_received=pnl,
|
||||
open_fee=0,
|
||||
close_fee=0,
|
||||
premium_ccy=ccy or "USDC",
|
||||
index_px=idx,
|
||||
)
|
||||
usdt = settled.get("realized_pnl_usdt")
|
||||
close_idx = settled.get("close_index_px")
|
||||
if usdt is None:
|
||||
continue
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_trades
|
||||
SET realized_pnl_usdt = ?,
|
||||
close_index_px = COALESCE(close_index_px, ?),
|
||||
realized_pnl = COALESCE(?, realized_pnl)
|
||||
WHERE id = ? AND realized_pnl_usdt IS NULL
|
||||
""",
|
||||
(float(usdt), close_idx, pnl, int(row["id"])),
|
||||
)
|
||||
updated += 1
|
||||
return updated
|
||||
|
||||
|
||||
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
|
||||
"""本地已平(含冻结 U/手续费净额) + 当前 open,供统计/中控窗口复用."""
|
||||
init_options_tables(conn)
|
||||
try:
|
||||
freeze_missing_realized_pnl_usdt(conn, ex)
|
||||
except Exception:
|
||||
pass
|
||||
closed_rows = conn.execute(_CLOSED_STATS_SELECT).fetchall()
|
||||
open_rows = conn.execute(
|
||||
"SELECT created_at FROM options_trades WHERE status = 'open'"
|
||||
).fetchall()
|
||||
history = _closed_trade_dicts(closed_rows)
|
||||
for row in open_rows:
|
||||
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
|
||||
return history
|
||||
|
||||
|
||||
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
||||
conn = get_db()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
closed_rows = conn.execute(
|
||||
"""
|
||||
SELECT realized_pnl, created_at, closed_at, inst_id, underlying, premium_ccy
|
||||
FROM options_trades
|
||||
WHERE status = 'closed' AND realized_pnl IS NOT NULL
|
||||
"""
|
||||
).fetchall()
|
||||
open_rows = conn.execute(
|
||||
"""
|
||||
SELECT created_at FROM options_trades WHERE status = 'open'
|
||||
"""
|
||||
).fetchall()
|
||||
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 一致)
|
||||
history = load_local_options_stats_history(conn, ex)
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
# 优先冻结 U;缺省再按指数折(与 /api/options/stats 一致)
|
||||
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -337,12 +439,17 @@ def header_options_stats_for_window(
|
||||
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, created_at, closed_at, inst_id, underlying, premium_ccy
|
||||
SELECT realized_pnl, realized_pnl_usdt, close_index_px,
|
||||
created_at, closed_at, inst_id, underlying, premium_ccy
|
||||
FROM options_trades
|
||||
WHERE status = 'closed'
|
||||
AND realized_pnl IS NOT NULL
|
||||
AND (realized_pnl IS NOT NULL OR realized_pnl_usdt IS NOT NULL)
|
||||
AND COALESCE(closed_at, created_at) >= ?
|
||||
AND COALESCE(closed_at, created_at) <= ?
|
||||
""",
|
||||
|
||||
Reference in New Issue
Block a user