feat数据统计曲线改为权益采样,现金加现货币权利金与浮盈。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 12:52:00 +08:00
parent fc0d76d174
commit 11c67b3c44
5 changed files with 291 additions and 36 deletions
+121 -19
View File
@@ -1,4 +1,4 @@
"""实例资金曲线采样:现金余额(不含权利金/浮盈),供数据统计页使用."""
"""实例资金曲线采样:权益(现金+现货币+权利金+浮盈),供数据统计页使用."""
from __future__ import annotations
import datetime as dt
@@ -36,6 +36,10 @@ def init_cash_sample_table(conn) -> None:
)
"""
)
try:
conn.execute("ALTER TABLE instance_cash_samples ADD COLUMN equity_usdt REAL")
except Exception:
pass
def _safe_float(v: Any) -> Optional[float]:
@@ -48,7 +52,7 @@ def _safe_float(v: Any) -> Optional[float]:
def cash_usdt_from_balances(bal: dict[str, Any] | None) -> Optional[float]:
"""现金 = 交易账户 + 资金账户(优先 USDT,否则 USDC);不含权利金/浮盈."""
"""稳定币现金 = 交易账户 + 资金账户(优先 USDT,否则 USDC)."""
if not isinstance(bal, dict):
return None
parts = [_safe_float(bal.get("trading_usdt")), _safe_float(bal.get("funding_usdt"))]
@@ -60,16 +64,93 @@ def cash_usdt_from_balances(bal: dict[str, Any] | None) -> Optional[float]:
return round(sum(known), 4)
def record_cash_sample(conn, cash_usdt: float, *, ts_ms: int | None = None) -> None:
def spot_coin_usdt_from_balances(
bal: dict[str, Any] | None,
*,
btc_px: float | None = None,
eth_px: float | None = None,
) -> float:
"""交易+资金账户 BTC/ETH 现货按指数折 U(缓冲买币占用)."""
if not isinstance(bal, dict):
return 0.0
total = 0.0
btc = (_safe_float(bal.get("trading_btc")) or 0.0) + (
_safe_float(bal.get("funding_btc")) or 0.0
)
eth = (_safe_float(bal.get("trading_eth")) or 0.0) + (
_safe_float(bal.get("funding_eth")) or 0.0
)
if btc > 0 and btc_px is not None and btc_px > 0:
total += btc * float(btc_px)
if eth > 0 and eth_px is not None and eth_px > 0:
total += eth * float(eth_px)
return round(total, 4)
def option_premium_and_upl_usdt(positions: list[dict[str, Any]] | None) -> tuple[float, float]:
"""持仓权利金U + 浮盈U(权利金+浮盈≈期权市值)."""
prem = 0.0
upl = 0.0
if not positions:
return 0.0, 0.0
from lib.options.options_positions_lib import (
display_pnl_usdt_from_option_row,
premium_paid_usdt_from_option_row,
)
for p in positions:
if not isinstance(p, dict):
continue
try:
pu = premium_paid_usdt_from_option_row(p)
except Exception:
pu = None
if pu is not None:
prem += float(pu)
try:
du = display_pnl_usdt_from_option_row(p)
except Exception:
du = None
if du is not None:
upl += float(du)
return round(prem, 4), round(upl, 4)
def equity_usdt_from_snapshot(
bal: dict[str, Any] | None,
positions: list[dict[str, Any]] | None = None,
*,
btc_px: float | None = None,
eth_px: float | None = None,
) -> Optional[float]:
"""权益 = 稳定币现金 + 现货币折U + 权利金U + 浮盈U."""
cash = cash_usdt_from_balances(bal)
spot = spot_coin_usdt_from_balances(bal, btc_px=btc_px, eth_px=eth_px)
prem, upl = option_premium_and_upl_usdt(positions)
if cash is None and spot <= 0 and prem == 0 and upl == 0:
return None
return round(float(cash or 0.0) + float(spot) + float(prem) + float(upl), 4)
def record_cash_sample(
conn,
cash_usdt: float,
*,
equity_usdt: float | None = None,
ts_ms: int | None = None,
) -> None:
init_cash_sample_table(conn)
t = int(ts_ms if ts_ms is not None else time.time() * 1000)
eq = float(equity_usdt) if equity_usdt is not None else float(cash_usdt)
conn.execute(
"""
INSERT INTO instance_cash_samples(ts_ms, cash_usdt)
VALUES (?, ?)
ON CONFLICT(ts_ms) DO UPDATE SET cash_usdt=excluded.cash_usdt
INSERT INTO instance_cash_samples(ts_ms, cash_usdt, equity_usdt)
VALUES (?, ?, ?)
ON CONFLICT(ts_ms) DO UPDATE SET
cash_usdt=excluded.cash_usdt,
equity_usdt=excluded.equity_usdt
""",
(t, float(cash_usdt)),
(t, float(cash_usdt), eq),
)
keep_ms = cash_sample_keep_days() * 86400 * 1000
cutoff = t - keep_ms
@@ -95,17 +176,23 @@ def query_cash_series_with_db(conn, *, range_key: str = "7d") -> dict[str, Any]:
init_cash_sample_table(conn)
rows = conn.execute(
"""
SELECT ts_ms, cash_usdt
SELECT ts_ms, cash_usdt, equity_usdt
FROM instance_cash_samples
WHERE ts_ms >= ?
ORDER BY ts_ms ASC
""",
(start_ms,),
).fetchall()
points = [{"t": int(r["ts_ms"]), "v": float(r["cash_usdt"])} for r in rows]
points = []
for r in rows:
cash = float(r["cash_usdt"])
eq = r["equity_usdt"]
v = float(eq) if eq is not None else cash
points.append({"t": int(r["ts_ms"]), "v": v, "cash": cash})
return {
"ok": True,
"range": rk,
"metric": "equity",
"points": points,
"start_ms": start_ms,
"end_ms": now_ms,
@@ -113,20 +200,36 @@ def query_cash_series_with_db(conn, *, range_key: str = "7d") -> dict[str, Any]:
}
def sample_once(get_db: Callable, fetch_balances: Callable) -> bool:
"""拉取余额并写入采样;成功返回 True."""
def sample_once(get_db: Callable, fetch_snapshot: Callable) -> bool:
"""拉取快照并写入采样.
fetch_snapshot 可返回:
- float: 视为权益(兼容旧现金回调)
- dict: {cash_usdt, equity_usdt}
"""
global _last_sample_at
try:
bal = fetch_balances()
snap = fetch_snapshot()
except Exception:
return False
cash = cash_usdt_from_balances(bal if isinstance(bal, dict) else None)
if cash is None:
cash: float | None
equity: float | None
if isinstance(snap, dict):
cash = _safe_float(snap.get("cash_usdt"))
equity = _safe_float(snap.get("equity_usdt"))
else:
cash = _safe_float(snap)
equity = cash
if equity is None and cash is None:
return False
if cash is None:
cash = float(equity or 0.0)
if equity is None:
equity = float(cash)
with _lock:
conn = get_db()
try:
record_cash_sample(conn, cash)
record_cash_sample(conn, float(cash), equity_usdt=float(equity))
conn.commit()
finally:
conn.close()
@@ -134,23 +237,22 @@ def sample_once(get_db: Callable, fetch_balances: Callable) -> bool:
return True
def start_cash_sample_loop(get_db: Callable, fetch_balances: Callable) -> None:
def start_cash_sample_loop(get_db: Callable, fetch_snapshot: Callable) -> None:
global _thread
if _thread and _thread.is_alive():
return
_stop.clear()
def _loop() -> None:
# 启动后立刻采一笔,避免曲线长期空白
try:
sample_once(get_db, fetch_balances)
sample_once(get_db, fetch_snapshot)
except Exception:
pass
while not _stop.is_set():
if _stop.wait(cash_sample_interval_sec()):
break
try:
sample_once(get_db, fetch_balances)
sample_once(get_db, fetch_snapshot)
except Exception:
pass