feat数据统计曲线改为累计已实现盈亏,划转提现不再影响。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 12:55:41 +08:00
parent 11c67b3c44
commit b31f861596
6 changed files with 194 additions and 148 deletions
+86 -16
View File
@@ -170,36 +170,106 @@ def _range_start_ms(range_key: str) -> tuple[str, int]:
return "7d", now_ms - 7 * 86400 * 1000
def query_cash_series_with_db(conn, *, range_key: str = "7d") -> dict[str, Any]:
def _closed_at_to_ms(raw: Any) -> int | None:
if raw is None or raw == "":
return None
try:
from lib.options.options_stats_lib import _parse_ts
d = _parse_ts(raw)
if d is None:
return None
return int(d.timestamp() * 1000)
except Exception:
return None
def _trade_realized_usdt(row: Any) -> float | None:
u = _safe_float(row["realized_pnl_usdt"] if hasattr(row, "keys") else None)
if u is not None:
return round(float(u), 4)
# 兼容未冻结 U:币盈亏×平仓价
try:
coin = _safe_float(row["realized_pnl"])
px = _safe_float(row["close_index_px"])
except Exception:
return None
if coin is None or px is None or px <= 0:
return None
return round(float(coin) * float(px), 4)
def query_realized_pnl_series_with_db(conn, *, range_key: str = "7d") -> dict[str, Any]:
"""累计已实现盈亏曲线(只随平仓变化,划转/提现不影响)."""
rk, start_ms = _range_start_ms(range_key)
now_ms = int(time.time() * 1000)
init_cash_sample_table(conn)
try:
from lib.options.options_db import init_options_tables
init_options_tables(conn)
except Exception:
pass
rows = conn.execute(
"""
SELECT ts_ms, cash_usdt, equity_usdt
FROM instance_cash_samples
WHERE ts_ms >= ?
ORDER BY ts_ms ASC
""",
(start_ms,),
SELECT id, closed_at, created_at, realized_pnl_usdt, realized_pnl, close_index_px
FROM options_trades
WHERE status = 'closed'
ORDER BY COALESCE(closed_at, created_at) ASC, id ASC
"""
).fetchall()
points = []
events: list[tuple[int, float]] = []
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})
pnl_u = _trade_realized_usdt(r)
if pnl_u is None:
continue
ts = _closed_at_to_ms(r["closed_at"]) or _closed_at_to_ms(r["created_at"])
if ts is None:
continue
events.append((ts, float(pnl_u)))
cum = 0.0
all_points: list[dict[str, Any]] = []
for ts, pnl in events:
cum = round(cum + pnl, 4)
all_points.append({"t": int(ts), "v": cum})
baseline = 0.0
series: list[dict[str, Any]] = []
for p in all_points:
if int(p["t"]) < start_ms:
baseline = float(p["v"])
continue
if not series:
series.append({"t": int(start_ms), "v": baseline})
series.append({"t": int(p["t"]), "v": float(p["v"])})
if not series:
# 窗口内无新平仓:画水平线(历史累计或 0)
series = [
{"t": int(start_ms), "v": baseline},
{"t": int(now_ms), "v": baseline},
]
else:
last_v = float(series[-1]["v"])
if int(series[-1]["t"]) < now_ms:
series.append({"t": int(now_ms), "v": last_v})
return {
"ok": True,
"range": rk,
"metric": "equity",
"points": points,
"metric": "realized_pnl",
"points": series,
"start_ms": start_ms,
"end_ms": now_ms,
"latest": points[-1]["v"] if points else None,
"latest": series[-1]["v"] if series else None,
"trade_count": len(events),
}
def query_cash_series_with_db(conn, *, range_key: str = "7d") -> dict[str, Any]:
"""兼容旧名:数据统计曲线改为累计已实现盈亏."""
return query_realized_pnl_series_with_db(conn, range_key=range_key)
def sample_once(get_db: Callable, fetch_snapshot: Callable) -> bool:
"""拉取快照并写入采样.
+4 -85
View File
@@ -1,4 +1,4 @@
"""实例数据统计:复盘 KPI + 权益曲线 API,以及后台权益采样."""
"""实例数据统计:复盘 KPI + 已实现盈亏曲线 API."""
from __future__ import annotations
from typing import Any, Callable
@@ -10,97 +10,16 @@ def install_instance_data_stats(app: Flask, app_module: Any) -> None:
get_db: Callable = app_module.get_db
login_required = app_module.login_required
def _fetch_equity_snapshot() -> dict[str, Any]:
from lib.instance.instance_cash_sample_lib import (
cash_usdt_from_balances,
equity_usdt_from_snapshot,
)
ex = getattr(app_module, "exchange_options", None)
bal: dict[str, Any] = {}
if ex is not None:
try:
from lib.exchange.okx_options_lib import fetch_options_balances
bal = fetch_options_balances(ex, force=False) or {}
except Exception:
bal = {}
cash = cash_usdt_from_balances(bal if isinstance(bal, dict) else None)
positions: list[dict[str, Any]] = []
cfg = None
try:
cfg = app.extensions.get("options_cfg")
except Exception:
cfg = None
if isinstance(cfg, dict) and cfg.get("enabled"):
try:
from lib.options.options_dashboard_lib import (
fetch_light_option_positions_for_dashboard,
)
positions = fetch_light_option_positions_for_dashboard(cfg) or []
except Exception:
positions = []
btc_px = eth_px = None
if ex is not None:
try:
from lib.options.options_stats_lib import _underlying_index_usdt
btc_px = _underlying_index_usdt(ex, "BTC")
eth_px = _underlying_index_usdt(ex, "ETH")
except Exception:
btc_px = eth_px = None
if (btc_px is None or eth_px is None) and positions:
for p in positions:
if not isinstance(p, dict):
continue
under = str(p.get("underlying") or "").upper()
idx = None
try:
idx = float(p.get("idx_px") or p.get("index_px") or 0)
except (TypeError, ValueError):
idx = None
if idx is None or idx <= 0:
continue
if under == "BTC" and btc_px is None:
btc_px = idx
elif under == "ETH" and eth_px is None:
eth_px = idx
equity = equity_usdt_from_snapshot(
bal if isinstance(bal, dict) else None,
positions,
btc_px=btc_px,
eth_px=eth_px,
)
return {"cash_usdt": cash, "equity_usdt": equity}
@app.route("/api/instance/cash_curve")
@login_required
def api_instance_cash_curve():
from lib.instance.instance_cash_sample_lib import (
query_cash_series_with_db,
sample_once,
)
from lib.instance.instance_cash_sample_lib import query_realized_pnl_series_with_db
try:
sample_once(get_db, _fetch_equity_snapshot)
except Exception:
pass
range_key = (request.args.get("range") or "7d").strip()
conn = get_db()
try:
return jsonify(query_cash_series_with_db(conn, range_key=range_key))
return jsonify(query_realized_pnl_series_with_db(conn, range_key=range_key))
except Exception as e:
return jsonify({"ok": False, "msg": f"加载资金曲线失败: {e}"}), 500
return jsonify({"ok": False, "msg": f"加载盈亏曲线失败: {e}"}), 500
finally:
conn.close()
try:
from lib.instance.instance_cash_sample_lib import start_cash_sample_loop
start_cash_sample_loop(get_db, _fetch_equity_snapshot)
except Exception as e:
print(f"[instance_cash_sample] start skipped: {e}", flush=True)
@@ -68,8 +68,8 @@
</section>
<section class="ods-section" aria-labelledby="ods-curve-heading">
<div class="ods-section-title" id="ods-curve-heading">权益曲线</div>
<p class="ods-section-desc">权益=稳定币现金+现货币折U+持仓权利金U+浮盈U;后台定时采样。</p>
<div class="ods-section-title" id="ods-curve-heading">已实现盈亏曲线</div>
<p class="ods-section-desc">累计已实现盈亏(期权平仓真盈亏U累加);划转/提现不影响。</p>
<div class="ods-curve-toolbar">
<button type="button" class="ods-range-btn active" data-range="today">今日</button>
<button type="button" class="ods-range-btn" data-range="7d">7日</button>
@@ -78,8 +78,8 @@
<span class="ods-curve-meta" id="ods-curve-meta">—</span>
</div>
<div class="ods-curve-wrap">
<svg id="ods-curve-svg" class="ods-curve-svg" viewBox="0 0 640 240" preserveAspectRatio="none" role="img" aria-label="权益曲线"></svg>
<svg id="ods-curve-svg" class="ods-curve-svg" viewBox="0 0 640 240" preserveAspectRatio="none" role="img" aria-label="已实现盈亏曲线"></svg>
</div>
</section>
</div>
<script src="/static/options_data_stats.js?v=2"></script>
<script src="/static/options_data_stats.js?v=3"></script>