Fix funds bar day stats: align trades, win rate, and PL ratio.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+10
-15
@@ -66,26 +66,21 @@ 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(
|
# 顶栏「总交易 / 胜率 / 盈亏比」与交易日同一口径:上海自然日开仓组 G-YYYYMMDD-*
|
||||||
"SELECT realized_pnl, close_at_ms FROM groups WHERE status='closed'"
|
|
||||||
)
|
|
||||||
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
|
|
||||||
|
|
||||||
# 当日成交组
|
|
||||||
day_prefix = trading_day.replace("-", "")
|
day_prefix = trading_day.replace("-", "")
|
||||||
day_groups = db.fetchall(
|
day_groups = db.fetchall(
|
||||||
"SELECT realized_pnl FROM groups WHERE group_id LIKE ? AND status='closed'",
|
"SELECT realized_pnl FROM groups WHERE group_id LIKE ? AND status='closed'",
|
||||||
(f"G-{day_prefix}-%",),
|
(f"G-{day_prefix}-%",),
|
||||||
)
|
)
|
||||||
day_n = len(day_groups)
|
day_pnls = [float(r["realized_pnl"] or 0) for r in day_groups]
|
||||||
|
day_n = len(day_pnls)
|
||||||
|
day_wins = sum(1 for x in day_pnls if x > 0)
|
||||||
|
day_win_rate = (day_wins / day_n) if day_n else 0.0
|
||||||
|
|
||||||
pos = st.get("position") or {}
|
pos = st.get("position") or {}
|
||||||
upl = pos
|
|
||||||
realtime = None
|
realtime = None
|
||||||
if str(pos.get("status") or "") == "open":
|
pos_st = str(pos.get("status") or "")
|
||||||
|
if pos_st in ("open", "half_open", "option_closed_perp_pending"):
|
||||||
realtime = float(pos.get("net_pnl") or 0)
|
realtime = float(pos.get("net_pnl") or 0)
|
||||||
|
|
||||||
if s.is_sim:
|
if s.is_sim:
|
||||||
@@ -144,9 +139,9 @@ async def funds_summary(_user: Annotated[str, Depends(require_user)]) -> dict[st
|
|||||||
"mode": mode,
|
"mode": mode,
|
||||||
"exchange": exchange,
|
"exchange": exchange,
|
||||||
"trading_day": trading_day,
|
"trading_day": trading_day,
|
||||||
"total_trades": day_n if day_n else n,
|
"total_trades": day_n,
|
||||||
"win_rate": win_rate,
|
"win_rate": day_win_rate,
|
||||||
"profit_loss_ratio": _pl_ratio(pnls),
|
"profit_loss_ratio": _pl_ratio(day_pnls),
|
||||||
"total_funds": total,
|
"total_funds": total,
|
||||||
"funding_usdt": funding_usdt,
|
"funding_usdt": funding_usdt,
|
||||||
"trading_usdt": trading_usdt,
|
"trading_usdt": trading_usdt,
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""顶栏资金摘要:总交易/胜率/盈亏比与交易日同口径。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from app.api import funds as funds_api
|
||||||
|
from app.models.db import Database, set_db
|
||||||
|
from app.strategy.engine import set_engine
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_closed(db: Database, *, group_id: str, pnl: float) -> None:
|
||||||
|
now = 1_700_000_000_000
|
||||||
|
with db._lock:
|
||||||
|
db._conn.execute(
|
||||||
|
"""INSERT INTO groups(
|
||||||
|
group_id, status, bias, option_side, perp_side, option_inst_id,
|
||||||
|
strike, expiry_ymd, initial_premium, open_at_ms, close_at_ms,
|
||||||
|
close_reason, realized_pnl, fees, slip_cost
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(
|
||||||
|
group_id,
|
||||||
|
"closed",
|
||||||
|
"test",
|
||||||
|
"call",
|
||||||
|
"short",
|
||||||
|
"ETH-USD_UM-260801-2000-C",
|
||||||
|
2000.0,
|
||||||
|
"260801",
|
||||||
|
100.0,
|
||||||
|
now,
|
||||||
|
now + 1000,
|
||||||
|
"manual",
|
||||||
|
pnl,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
db._conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_day_stats_not_mixed_with_history(tmp_path, monkeypatch) -> None:
|
||||||
|
monkeypatch.setenv("MODE", "SIM")
|
||||||
|
db = Database(tmp_path / "funds_sum.db")
|
||||||
|
set_db(db)
|
||||||
|
try:
|
||||||
|
# 历史 3 胜 1 负 → 75%;当日仅 1 笔亏损
|
||||||
|
_insert_closed(db, group_id="G-20260101-01", pnl=10.0)
|
||||||
|
_insert_closed(db, group_id="G-20260101-02", pnl=20.0)
|
||||||
|
_insert_closed(db, group_id="G-20260101-03", pnl=5.0)
|
||||||
|
_insert_closed(db, group_id="G-20260101-04", pnl=-10.0)
|
||||||
|
_insert_closed(db, group_id="G-20260802-01", pnl=-8.0)
|
||||||
|
|
||||||
|
class _FakeDT:
|
||||||
|
@staticmethod
|
||||||
|
def now(tz=None):
|
||||||
|
from datetime import timezone
|
||||||
|
|
||||||
|
if tz is timezone.utc:
|
||||||
|
return datetime(2026, 8, 2, 4, 0, tzinfo=timezone.utc)
|
||||||
|
return datetime(2026, 8, 2, 12, 0, tzinfo=tz or ZoneInfo("Asia/Shanghai"))
|
||||||
|
|
||||||
|
monkeypatch.setattr(funds_api, "datetime", _FakeDT)
|
||||||
|
|
||||||
|
eng = SimpleNamespace(
|
||||||
|
state=lambda: {
|
||||||
|
"exchange": "okx",
|
||||||
|
"position": {"status": "flat", "net_pnl": None},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
set_engine(eng) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
body = asyncio.run(funds_api.funds_summary(_user="admin"))
|
||||||
|
assert body["ok"] is True
|
||||||
|
assert body["trading_day"] == "2026-08-02"
|
||||||
|
assert body["total_trades"] == 1
|
||||||
|
assert body["win_rate"] == 0.0
|
||||||
|
assert body["profit_loss_ratio"] is None
|
||||||
|
finally:
|
||||||
|
set_engine(None) # type: ignore[arg-type]
|
||||||
|
set_db(None)
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_day_win_rate_matches_day_trades(tmp_path, monkeypatch) -> None:
|
||||||
|
monkeypatch.setenv("MODE", "SIM")
|
||||||
|
db = Database(tmp_path / "funds_sum2.db")
|
||||||
|
set_db(db)
|
||||||
|
try:
|
||||||
|
_insert_closed(db, group_id="G-20260802-01", pnl=10.0)
|
||||||
|
_insert_closed(db, group_id="G-20260802-02", pnl=20.0)
|
||||||
|
_insert_closed(db, group_id="G-20260802-03", pnl=-5.0)
|
||||||
|
|
||||||
|
class _FakeDT:
|
||||||
|
@staticmethod
|
||||||
|
def now(tz=None):
|
||||||
|
from datetime import timezone
|
||||||
|
|
||||||
|
if tz is timezone.utc:
|
||||||
|
return datetime(2026, 8, 2, 4, 0, tzinfo=timezone.utc)
|
||||||
|
return datetime(2026, 8, 2, 12, 0, tzinfo=tz or ZoneInfo("Asia/Shanghai"))
|
||||||
|
|
||||||
|
monkeypatch.setattr(funds_api, "datetime", _FakeDT)
|
||||||
|
set_engine(
|
||||||
|
SimpleNamespace(
|
||||||
|
state=lambda: {
|
||||||
|
"exchange": "okx",
|
||||||
|
"position": {"status": "flat"},
|
||||||
|
}
|
||||||
|
) # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
body = asyncio.run(funds_api.funds_summary(_user="admin"))
|
||||||
|
assert body["total_trades"] == 3
|
||||||
|
assert abs(body["win_rate"] - (2 / 3)) < 1e-9
|
||||||
|
assert body["profit_loss_ratio"] is not None
|
||||||
|
finally:
|
||||||
|
set_engine(None) # type: ignore[arg-type]
|
||||||
|
set_db(None)
|
||||||
|
db.close()
|
||||||
Reference in New Issue
Block a user