模拟盘统计改为现货买币/卖回的真实 USDT 净盈亏,对齐钱包。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-08 23:24:18 +08:00
parent 7943cd2167
commit bb9d14dd73
4 changed files with 312 additions and 2 deletions
+29 -2
View File
@@ -59,9 +59,21 @@ def _sell_excess_bridge_coin(
sell_amt = min(float(avail), excess_cap)
if not _spot_bridge.is_sellable_spot_coin(ex, underlying, sell_amt):
return None
return _spot_bridge.spot_market_sell_coin_to_usdt(
sell = _spot_bridge.spot_market_sell_coin_to_usdt(
ex, underlying=underlying, coin_amount=sell_amt
)
if sell and sell.get("ok"):
try:
recovered = float(sell.get("usdt_recovered") or 0)
except (TypeError, ValueError):
recovered = 0.0
if recovered > 0:
try:
_spot_bridge.add_bridge_usdt_recovered(conn, int(bridge_id), recovered)
conn.commit()
except Exception:
pass
return sell
def coin_budget_preview(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
@@ -650,12 +662,27 @@ def maybe_sell_spot_after_close(
pass
parts = inst_id.split("-")
underlying = (parts[0] if parts else "ETH").upper()
return _spot_bridge.sell_residual_after_option_flat(
result = _spot_bridge.sell_residual_after_option_flat(
cfg["get_db"],
ex,
underlying=underlying,
inst_id=inst_id,
)
# 模拟盘:卖回后按桥上真实 USDT 净额回写已实现盈亏
try:
from lib.sim.mode_lib import is_sim_mode
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
if is_sim_mode(cfg["get_db"]):
conn = cfg["get_db"]()
try:
sync_sim_cash_realized_pnl_usdt(conn, ex)
conn.commit()
finally:
conn.close()
except Exception:
pass
return result
def sweep_spot_after_options_flat(
+56
View File
@@ -151,6 +151,59 @@ def insert_bridge(
return int(cur.lastrowid)
def add_bridge_usdt_recovered(
conn: sqlite3.Connection,
bridge_id: int,
delta_usdt: float | None,
) -> float | None:
"""累加桥上卖币收回的 USDT(开仓多余卖回 + 平仓后卖回)."""
ensure_bridge_table(conn)
try:
d = float(delta_usdt or 0)
except (TypeError, ValueError):
return None
if d <= 0 or bridge_id is None:
return None
row = conn.execute(
"SELECT usdt_recovered FROM options_spot_bridge WHERE id=?",
(int(bridge_id),),
).fetchone()
if not row:
return None
prev = _safe_float(row["usdt_recovered"] if hasattr(row, "keys") else row[0]) or 0.0
new_v = round(float(prev) + d, 8)
conn.execute(
"""
UPDATE options_spot_bridge
SET usdt_recovered = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""",
(new_v, int(bridge_id)),
)
return new_v
def cash_pnl_usdt_from_bridge(conn: sqlite3.Connection, bridge_id: int | None) -> float | None:
"""现金盈亏 = 卖回 USDT 合计 − 开仓买币花费(budget_usdt)."""
if bridge_id is None:
return None
ensure_bridge_table(conn)
row = conn.execute(
"""
SELECT budget_usdt, usdt_recovered
FROM options_spot_bridge WHERE id = ?
""",
(int(bridge_id),),
).fetchone()
if not row:
return None
spent = _safe_float(row["budget_usdt"] if hasattr(row, "keys") else row[0])
recovered = _safe_float(row["usdt_recovered"] if hasattr(row, "keys") else row[1])
if spent is None or recovered is None:
return None
return round(float(recovered) - float(spent), 4)
def update_bridge(
conn: sqlite3.Connection,
bridge_id: int,
@@ -540,6 +593,9 @@ def sell_residual_after_option_flat(
"bridge_id": bid,
"bridge_status": BRIDGE_PENDING_SELL,
}
recovered = _safe_float(sell.get("usdt_recovered"))
if recovered is not None and recovered > 0:
add_bridge_usdt_recovered(c, bid, recovered)
update_bridge(
c,
bid,
+130
View File
@@ -465,9 +465,139 @@ def repair_double_counted_fee_pnl(conn, ex: Any = None) -> int:
return recompute_closed_realized_pnl_usdt(conn, ex)
def _sim_net_usdt_converts_for_trade(
conn,
*,
underlying: str,
start_ts: str,
end_ts: str,
) -> float | None:
"""从 sim_ledger 汇总该标的现货兑换的 USDT 净变动(买币为负、卖回为正)."""
coin = (underlying or "").strip().upper()
if coin not in ("ETH", "BTC") or not start_ts or not end_ts:
return None
try:
rows = conn.execute(
"""
SELECT amount, note FROM sim_ledger_entries
WHERE kind = 'convert'
AND UPPER(ccy) = 'USDT'
AND account = 'trading'
AND ts >= ?
AND ts <= ?
""",
(str(start_ts), str(end_ts)),
).fetchall()
except Exception:
return None
total = 0.0
n = 0
for row in rows or []:
note = str(_row_get(row, "note") or "").upper()
amt = _safe_float(_row_get(row, "amount"))
if amt is None:
continue
related = (
f"{coin}/USDT" in note
or f"{coin}-USDT" in note
or note.strip() == f"FROM {coin}"
)
if not related:
continue
total += float(amt)
n += 1
if n < 1:
return None
return round(total, 4)
def sync_sim_cash_realized_pnl_usdt(conn, ex: Any = None) -> int:
"""模拟盘:用现货买币/卖回的真实 USDT 净额覆盖冻结盈亏(对齐钱包)."""
del ex # 现金口径不依赖实时指数
init_options_tables(conn)
try:
from lib.options.options_spot_bridge_lib import (
cash_pnl_usdt_from_bridge,
ensure_bridge_table,
)
ensure_bridge_table(conn)
except Exception:
return 0
rows = conn.execute(
"""
SELECT id, bridge_id, underlying, budget_usdt, premium_ccy,
created_at, closed_at, realized_pnl_usdt
FROM options_trades
WHERE status = 'closed'
ORDER BY id DESC
LIMIT 300
"""
).fetchall()
updated = 0
for row in rows:
ccy = str(_row_get(row, "premium_ccy") or "").strip().upper()
underly = str(_row_get(row, "underlying") or "").strip().upper()
if ccy not in ("ETH", "BTC") and underly not in ("ETH", "BTC"):
continue
underly = underly or ccy
cash = None
bid = _row_get(row, "bridge_id")
try:
if bid is not None:
cash = cash_pnl_usdt_from_bridge(conn, int(bid))
except Exception:
cash = None
if cash is None:
# 旧桥未记 usdt_recovered:用流水回补(开仓前1分钟~平仓后30分钟)
start = str(_row_get(row, "created_at") or "")
end = str(_row_get(row, "closed_at") or "")
if start and end:
from datetime import datetime, timedelta
def _shift(ts: str, minutes: int) -> str:
try:
dt = datetime.fromisoformat(str(ts).strip().replace(" ", "T", 1))
return (dt + timedelta(minutes=minutes)).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return ts
cash = _sim_net_usdt_converts_for_trade(
conn,
underlying=underly,
start_ts=_shift(start, -2),
end_ts=_shift(end, 30),
)
if cash is None:
continue
old = _safe_float(_row_get(row, "realized_pnl_usdt"))
if old is not None and abs(float(old) - float(cash)) < 1e-6:
continue
conn.execute(
"""
UPDATE options_trades
SET realized_pnl_usdt = ?
WHERE id = ?
""",
(float(cash), int(row["id"])),
)
updated += 1
return updated
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
"""本地已平(模拟盘口径) + 当前 open."""
init_options_tables(conn)
try:
from lib.sim.mode_lib import is_sim_mode
# get_db 不可用时仍尝试现金同步(conn 已打开)
sync_sim_cash_realized_pnl_usdt(conn, ex)
except Exception:
try:
sync_sim_cash_realized_pnl_usdt(conn, ex)
except Exception:
pass
try:
freeze_missing_realized_pnl_usdt(conn, ex)
except Exception: