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:
@@ -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()
|
||||
trading_day = datetime.now(SH).strftime("%Y-%m-%d")
|
||||
|
||||
# 顶栏「总交易 / 胜率 / 盈亏比」统一历史累计(全部已平组)
|
||||
closed = db.fetchall(
|
||||
"SELECT realized_pnl FROM groups WHERE status='closed'"
|
||||
)
|
||||
pnls = [float(r["realized_pnl"] or 0) for r in closed]
|
||||
# 顶栏「总交易 / 胜率 / 盈亏比」:用展示口径净盈亏(含到期内在价值修复)
|
||||
closed = db.fetchall("SELECT * FROM groups WHERE status='closed'")
|
||||
pnls: list[float] = []
|
||||
try:
|
||||
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)
|
||||
wins = sum(1 for x in pnls if x > 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:
|
||||
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:
|
||||
# 非 OKX LIVE:暂无统一资金接口,不回退模拟账本(避免实盘显示假资金)
|
||||
# 其它交易所:不回退模拟账本(避免实盘显示假资金)
|
||||
return {
|
||||
"ok": False,
|
||||
"mode": mode,
|
||||
"exchange": exchange,
|
||||
"detail": f"{exchange} 实盘资金摘要暂未接入,请用 OKX 或交易所 App 查看",
|
||||
"detail": f"{exchange} 实盘资金摘要暂未接入,请用交易所 App 查看",
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
|
||||
+130
-1
@@ -330,9 +330,122 @@ def _enrich_group(g: dict, fills: list) -> dict:
|
||||
if is_oo:
|
||||
g["option2_leverage"] = _option_leverage_for_leg(g, fills, leg="option2")
|
||||
g["_view_fills"] = view_fills
|
||||
g["_overlay_settle"] = float(settle) if settle is not None else None
|
||||
g["_overlaid"] = overlaid
|
||||
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")
|
||||
async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
db = get_db()
|
||||
@@ -345,7 +458,13 @@ async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
(g["group_id"],),
|
||||
)
|
||||
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("_overlay_settle", None)
|
||||
gr.pop("_overlaid", None)
|
||||
groups.append(gr)
|
||||
return {"groups": groups}
|
||||
|
||||
@@ -362,11 +481,21 @@ async def group_detail(
|
||||
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
|
||||
)
|
||||
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
|
||||
gr.pop("_overlay_settle", None)
|
||||
gr.pop("_overlaid", None)
|
||||
return {
|
||||
"group": gr,
|
||||
"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
|
||||
],
|
||||
"pnl_summary": gr.get("pnl_summary"),
|
||||
|
||||
@@ -1035,6 +1035,12 @@ class BinanceLiveExecutor(Matcher):
|
||||
),
|
||||
)
|
||||
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):
|
||||
if not inst:
|
||||
continue
|
||||
@@ -1042,6 +1048,29 @@ class BinanceLiveExecutor(Matcher):
|
||||
px, fee, notional, cash = self._live_option_settlement_fill(
|
||||
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:
|
||||
px, fee, notional, cash = 0.0, 0.0, 0.0, 0.0
|
||||
if abs(cash) > 1e-12:
|
||||
@@ -1082,12 +1111,6 @@ class BinanceLiveExecutor(Matcher):
|
||||
)
|
||||
summary = summarize_fills_pnl(list(fill_rows))
|
||||
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:
|
||||
self.db._conn.execute(
|
||||
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
|
||||
|
||||
@@ -1082,6 +1082,12 @@ class OkxLiveExecutor(Matcher):
|
||||
),
|
||||
)
|
||||
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):
|
||||
if not inst:
|
||||
continue
|
||||
@@ -1089,6 +1095,30 @@ class OkxLiveExecutor(Matcher):
|
||||
px, fee, notional, cash = self._live_option_settlement_fill(
|
||||
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:
|
||||
px, fee, notional, cash = 0.0, 0.0, 0.0, 0.0
|
||||
if abs(cash) > 1e-12:
|
||||
@@ -1129,12 +1159,6 @@ class OkxLiveExecutor(Matcher):
|
||||
)
|
||||
summary = summarize_fills_pnl(list(fill_rows))
|
||||
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:
|
||||
self.db._conn.execute(
|
||||
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
|
||||
|
||||
@@ -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 — 期期到期盈亏:错误近零成交按内在价值覆盖
|
||||
|
||||
### 变更
|
||||
|
||||
Reference in New Issue
Block a user