Align funds bar and win-rate with OO expiry intrinsic repair; wire Binance balances.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-11 17:18:16 +08:00
parent 3520fc0214
commit fc74b8e913
5 changed files with 254 additions and 20 deletions
+51 -7
View File
@@ -66,11 +66,26 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st
exchange = str(st.get("exchange") or s.exchange or "okx").upper() exchange = str(st.get("exchange") or s.exchange or "okx").upper()
trading_day = datetime.now(SH).strftime("%Y-%m-%d") trading_day = datetime.now(SH).strftime("%Y-%m-%d")
# 顶栏「总交易 / 胜率 / 盈亏比」统一历史累计(全部已平组 # 顶栏「总交易 / 胜率 / 盈亏比」:用展示口径净盈亏(含到期内在价值修复
closed = db.fetchall( closed = db.fetchall("SELECT * FROM groups WHERE status='closed'")
"SELECT realized_pnl FROM groups WHERE status='closed'" pnls: list[float] = []
) try:
pnls = [float(r["realized_pnl"] or 0) for r in closed] from .trades import _enrich_group, persist_expiry_overlay_if_needed
for r in closed:
g = dict(r)
fills = db.fetchall(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC",
(g["group_id"],),
)
gr = _enrich_group(g, fills)
try:
persist_expiry_overlay_if_needed(db, gr, list(fills))
except Exception:
pass
pnls.append(float(gr.get("net_pnl") or gr.get("realized_pnl") or 0))
except Exception:
pnls = [float(r["realized_pnl"] or 0) for r in closed]
n = len(pnls) n = len(pnls)
wins = sum(1 for x in pnls if x > 0) wins = sum(1 for x in pnls if x > 0)
win_rate = (wins / n) if n else 0.0 win_rate = (wins / n) if n else 0.0
@@ -124,13 +139,42 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st
} }
finally: finally:
client.close() client.close()
elif exchange in ("BINANCE", "BN"):
from ..live.binance_trade import BinanceTradeClient
client = BinanceTradeClient()
try:
bal = client.fetch_balances()
funding_usdt = bal.get("funding_usdt")
trading_usdt = bal.get("trading_usdt")
funding_usdc = bal.get("funding_usdc")
trading_usdc = bal.get("trading_usdc")
r = float(rate) if rate and rate > 0 else 1.0
total = round(
(funding_usdt or 0.0)
+ (trading_usdt or 0.0)
+ ((funding_usdc or 0.0) + (trading_usdc or 0.0)) * r,
2,
)
except Exception as e:
return {
"ok": False,
"mode": mode,
"exchange": exchange,
"detail": str(e),
}
finally:
try:
client.close()
except Exception:
pass
else: else:
# 非 OKX LIVE:暂无统一资金接口,不回退模拟账本(避免实盘显示假资金) # 其它交易所:不回退模拟账本(避免实盘显示假资金)
return { return {
"ok": False, "ok": False,
"mode": mode, "mode": mode,
"exchange": exchange, "exchange": exchange,
"detail": f"{exchange} 实盘资金摘要暂未接入,请用 OKX 或交易所 App 查看", "detail": f"{exchange} 实盘资金摘要暂未接入,请用交易所 App 查看",
} }
return { return {
"ok": True, "ok": True,
+130 -1
View File
@@ -330,9 +330,122 @@ def _enrich_group(g: dict, fills: list) -> dict:
if is_oo: if is_oo:
g["option2_leverage"] = _option_leverage_for_leg(g, fills, leg="option2") g["option2_leverage"] = _option_leverage_for_leg(g, fills, leg="option2")
g["_view_fills"] = view_fills g["_view_fills"] = view_fills
g["_overlay_settle"] = float(settle) if settle is not None else None
g["_overlaid"] = overlaid
return g return g
def persist_expiry_overlay_if_needed(db: Any, g: dict, raw_fills: list) -> None:
"""把内在价值覆盖写回库:fills / realized_pnl / settle_index,并补本地账本差额。"""
if not g.get("_overlaid"):
return
view_fills = g.get("_view_fills") or []
net = g.get("net_pnl")
if net is None:
return
settle = g.get("_overlay_settle")
group_id = str(g.get("group_id") or "")
if not group_id:
return
old_net = float(g.get("realized_pnl") or 0)
# 已对齐则跳过(避免每次列表刷库)
if abs(old_net - float(net)) < 0.02:
stored = g.get("settle_index_px")
try:
if settle is None or (
stored is not None and abs(float(stored) - float(settle)) < 0.05
):
# 仍可能 fills 未写回;检查是否还有 overlay 标记需要落库
pass
else:
pass
except (TypeError, ValueError):
pass
raw_close = {
str(dict(f).get("leg")): dict(f)
for f in raw_fills
if str(dict(f).get("action") or "") == "close"
and str(dict(f).get("leg") or "") in ("option", "option2")
}
cash_delta = 0.0
updates: list[tuple] = []
for vf in view_fills:
if not isinstance(vf, dict) or not vf.get("_overlay_intrinsic"):
continue
leg = str(vf.get("leg") or "")
old = raw_close.get(leg)
if not old or old.get("id") is None:
continue
try:
old_px = float(old.get("fill_px") or 0)
new_px = float(vf.get("fill_px") or 0)
qty = float(vf.get("qty_eth") or old.get("qty_eth") or 0)
except (TypeError, ValueError):
continue
if abs(old_px - new_px) <= 1e-9:
continue
cash_delta += (new_px - old_px) * qty
updates.append(
(
new_px,
new_px,
new_px * qty,
int(old["id"]),
)
)
if not updates and abs(old_net - float(net)) < 0.02:
# 只缺 settle
if settle is None:
return
try:
if g.get("settle_index_px") is not None and abs(
float(g["settle_index_px"]) - float(settle)
) < 0.05:
return
except (TypeError, ValueError):
pass
with db._lock:
for base, fill, notional, fid in updates:
db._conn.execute(
"UPDATE fills SET base_px=?, fill_px=?, notional=?, slip=0 WHERE id=?",
(base, fill, notional, fid),
)
db._conn.execute(
"""UPDATE groups SET realized_pnl=?,
settle_index_px=COALESCE(?, settle_index_px),
note=CASE
WHEN instr(COALESCE(note,''), 'expiry_intrinsic_repair')>0 THEN note
ELSE trim(COALESCE(note,'') || ' | expiry_intrinsic_repair')
END
WHERE group_id=? AND status='closed'""",
(
float(net),
float(settle) if settle is not None else None,
group_id,
),
)
db._conn.commit()
if abs(cash_delta) > 1e-9:
try:
from ..sim.ledger import Ledger
Ledger(db).apply_cash(
cash_delta,
kind="repair_option_intrinsic",
group_id=group_id,
note=f"expiry intrinsic overlay cash_delta={cash_delta:.4f}",
allow_negative=True,
)
except Exception:
pass
# 刷新内存中的 realized,供同请求后续使用
g["realized_pnl"] = float(net)
@router.get("/groups") @router.get("/groups")
async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict: async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict:
db = get_db() db = get_db()
@@ -345,7 +458,13 @@ async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict:
(g["group_id"],), (g["group_id"],),
) )
gr = _enrich_group(g, fills) gr = _enrich_group(g, fills)
try:
persist_expiry_overlay_if_needed(db, gr, list(fills))
except Exception:
pass
gr.pop("_view_fills", None) gr.pop("_view_fills", None)
gr.pop("_overlay_settle", None)
gr.pop("_overlaid", None)
groups.append(gr) groups.append(gr)
return {"groups": groups} return {"groups": groups}
@@ -362,11 +481,21 @@ async def group_detail(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,) "SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
) )
gr = _enrich_group(_row(g), fills) gr = _enrich_group(_row(g), fills)
try:
persist_expiry_overlay_if_needed(db, gr, list(fills))
except Exception:
pass
view_fills = gr.pop("_view_fills", None) or fills view_fills = gr.pop("_view_fills", None) or fills
gr.pop("_overlay_settle", None)
gr.pop("_overlaid", None)
return { return {
"group": gr, "group": gr,
"fills": [ "fills": [
{k: v for k, v in (dict(x) if not isinstance(x, dict) else x).items() if k != "_overlay_intrinsic"} {
k: v
for k, v in (dict(x) if not isinstance(x, dict) else x).items()
if k != "_overlay_intrinsic"
}
for x in view_fills for x in view_fills
], ],
"pnl_summary": gr.get("pnl_summary"), "pnl_summary": gr.get("pnl_summary"),
+29 -6
View File
@@ -1035,6 +1035,12 @@ class BinanceLiveExecutor(Matcher):
), ),
) )
now = int(time.time() * 1000) now = int(time.time() * 1000)
settle_px = None
if reason == "expiry":
try:
settle_px = self._close_spot_px(get_session().snapshot())
except Exception:
settle_px = None
for i, (leg, inst, qty, contracts) in enumerate(legs): for i, (leg, inst, qty, contracts) in enumerate(legs):
if not inst: if not inst:
continue continue
@@ -1042,6 +1048,29 @@ class BinanceLiveExecutor(Matcher):
px, fee, notional, cash = self._live_option_settlement_fill( px, fee, notional, cash = self._live_option_settlement_fill(
option_inst_id=inst, qty_eth=qty, group_id=group_id option_inst_id=inst, qty_eth=qty, group_id=group_id
) )
if settle_px is not None and qty > 0:
if leg == "option":
side = str(pos.get("option_side") or "call")
strike = self._group_strike(group_id, inst)
else:
side = str(pos.get("option2_side") or "put")
try:
strike = float(pos.get("strike2") or 0) or None
except (TypeError, ValueError):
strike = None
if strike is not None:
iv = float(
option_intrinsic(
option_side=side,
strike=float(strike),
spot=float(settle_px),
)
)
tol = max(0.5, abs(iv) * 0.05)
if abs(float(px) - iv) > tol:
px = iv
notional = iv * float(qty)
cash = notional - float(fee or 0)
else: else:
px, fee, notional, cash = 0.0, 0.0, 0.0, 0.0 px, fee, notional, cash = 0.0, 0.0, 0.0, 0.0
if abs(cash) > 1e-12: if abs(cash) > 1e-12:
@@ -1082,12 +1111,6 @@ class BinanceLiveExecutor(Matcher):
) )
summary = summarize_fills_pnl(list(fill_rows)) summary = summarize_fills_pnl(list(fill_rows))
net = float(summary.get("net_pnl") or 0.0) net = float(summary.get("net_pnl") or 0.0)
settle_px = None
if reason == "expiry":
try:
settle_px = self._close_spot_px(get_session().snapshot())
except Exception:
settle_px = None
with self.db._lock: with self.db._lock:
self.db._conn.execute( self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?, """UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
+30 -6
View File
@@ -1082,6 +1082,12 @@ class OkxLiveExecutor(Matcher):
), ),
) )
now = int(time.time() * 1000) now = int(time.time() * 1000)
settle_px = None
if reason == "expiry":
try:
settle_px = self._close_spot_px(get_session().snapshot())
except Exception:
settle_px = None
for i, (leg, inst, qty, contracts) in enumerate(legs): for i, (leg, inst, qty, contracts) in enumerate(legs):
if not inst: if not inst:
continue continue
@@ -1089,6 +1095,30 @@ class OkxLiveExecutor(Matcher):
px, fee, notional, cash = self._live_option_settlement_fill( px, fee, notional, cash = self._live_option_settlement_fill(
option_inst_id=inst, qty_eth=qty, group_id=group_id option_inst_id=inst, qty_eth=qty, group_id=group_id
) )
# 账单近零但指数已知:按内在价值对齐本地成交与入账
if settle_px is not None and qty > 0:
if leg == "option":
side = str(pos.get("option_side") or "call")
strike = self._group_strike(group_id, inst)
else:
side = str(pos.get("option2_side") or "put")
try:
strike = float(pos.get("strike2") or 0) or None
except (TypeError, ValueError):
strike = None
if strike is not None:
iv = float(
option_intrinsic(
option_side=side,
strike=float(strike),
spot=float(settle_px),
)
)
tol = max(0.5, abs(iv) * 0.05)
if abs(float(px) - iv) > tol:
px = iv
notional = iv * float(qty)
cash = notional - float(fee or 0)
else: else:
px, fee, notional, cash = 0.0, 0.0, 0.0, 0.0 px, fee, notional, cash = 0.0, 0.0, 0.0, 0.0
if abs(cash) > 1e-12: if abs(cash) > 1e-12:
@@ -1129,12 +1159,6 @@ class OkxLiveExecutor(Matcher):
) )
summary = summarize_fills_pnl(list(fill_rows)) summary = summarize_fills_pnl(list(fill_rows))
net = float(summary.get("net_pnl") or 0.0) net = float(summary.get("net_pnl") or 0.0)
settle_px = None
if reason == "expiry":
try:
settle_px = self._close_spot_px(get_session().snapshot())
except Exception:
settle_px = None
with self.db._lock: with self.db._lock:
self.db._conn.execute( self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?, """UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
+14
View File
@@ -5,6 +5,20 @@
--- ---
## 2026-08-11 — 顶栏资金/胜率与期期到期账对齐
### 变更
1. 到期内在价值覆盖写回 `fills` / `realized_pnl` / `settle_index`,并补本地账本差额。
2. 顶栏胜率按修复后净盈亏统计(不再因库内错误 realized 显示 0%)。
3. LIVE 币安接入 `fetch_balances` 资金摘要;期期到期账单近零时按内在价值入账。
### 审计
详情已显示 +115.62,但顶栏胜率 0%、资金仍按旧账/OKX 余额 → 展示盈亏未落库,且币安成交对不上 OKX 资金条。
---
## 2026-08-11 — 期期到期盈亏:错误近零成交按内在价值覆盖 ## 2026-08-11 — 期期到期盈亏:错误近零成交按内在价值覆盖
### 变更 ### 变更