Add win/loss metrics to archive stats with symbol filter sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-21 09:03:21 +08:00
parent 073a382d41
commit c05afbbedf
5 changed files with 174 additions and 36 deletions
+67 -29
View File
@@ -1249,46 +1249,86 @@ def resolve_period_bounds(
return start_ms, end_ms, d, d, f"本日 {d}"
def _pnl_side(pnl: float) -> str:
if pnl > 0.0001:
return "win"
if pnl < -0.0001:
return "loss"
return "flat"
def _empty_pnl_bucket() -> dict[str, Any]:
return {
"open_count": 0,
"sick_count": 0,
"pnl_total": 0.0,
"pnl_ex_sick": 0.0,
"win_count": 0,
"loss_count": 0,
"avg_win": None,
"avg_loss": None,
"max_win": None,
"max_loss": None,
}
def _finalize_pnl_bucket(bucket: dict[str, Any]) -> None:
wins = bucket.pop("_wins", [])
losses = bucket.pop("_losses", [])
bucket["win_count"] = len(wins)
bucket["loss_count"] = len(losses)
bucket["avg_win"] = round(sum(wins) / len(wins), 4) if wins else None
bucket["avg_loss"] = round(sum(losses) / len(losses), 4) if losses else None
bucket["max_win"] = round(max(wins), 4) if wins else None
bucket["max_loss"] = round(min(losses), 4) if losses else None
bucket["pnl_total"] = round(float(bucket.get("pnl_total") or 0), 4)
bucket["pnl_ex_sick"] = round(float(bucket.get("pnl_ex_sick") or 0), 4)
def _accumulate_trade_stat(bucket: dict[str, Any], *, pnl: float, is_sick: bool) -> None:
bucket["open_count"] += 1
bucket["pnl_total"] += pnl
if is_sick:
bucket["sick_count"] += 1
else:
bucket["pnl_ex_sick"] += pnl
side = _pnl_side(pnl)
if side == "win":
bucket.setdefault("_wins", []).append(pnl)
elif side == "loss":
bucket.setdefault("_losses", []).append(pnl)
def _compute_period_stats(trade_rows: list[dict[str, Any]]) -> dict[str, Any]:
total = len(trade_rows)
sick = 0
pnl_all = 0.0
pnl_ex = 0.0
total_bucket = _empty_pnl_bucket()
by_ex: dict[str, dict[str, Any]] = {}
for td_row in trade_rows:
ex = str(td_row.get("exchange_key") or "?")
pnl = float(td_row.get("pnl_amount") or 0)
tag = str(td_row.get("behavior_tag") or "")
is_sick = tag == "sick"
if is_sick:
sick += 1
pnl_all += pnl
if not is_sick:
pnl_ex += pnl
_accumulate_trade_stat(total_bucket, pnl=pnl, is_sick=is_sick)
if ex not in by_ex:
by_ex[ex] = {
"open_count": 0,
"sick_count": 0,
"pnl_total": 0.0,
"pnl_ex_sick": 0.0,
}
bucket = by_ex[ex]
bucket["open_count"] += 1
bucket["pnl_total"] += pnl
if is_sick:
bucket["sick_count"] += 1
else:
bucket["pnl_ex_sick"] += pnl
by_ex[ex] = _empty_pnl_bucket()
_accumulate_trade_stat(by_ex[ex], pnl=pnl, is_sick=is_sick)
_finalize_pnl_bucket(total_bucket)
for ex in by_ex:
by_ex[ex]["pnl_total"] = round(by_ex[ex]["pnl_total"], 4)
by_ex[ex]["pnl_ex_sick"] = round(by_ex[ex]["pnl_ex_sick"], 4)
_finalize_pnl_bucket(by_ex[ex])
total = int(total_bucket["open_count"] or 0)
sick = int(total_bucket["sick_count"] or 0)
sick_pct = round(sick / total * 100, 1) if total else 0.0
return {
"open_count": total,
"sick_count": sick,
"sick_pct": sick_pct,
"pnl_total": round(pnl_all, 4),
"pnl_ex_sick": round(pnl_ex, 4),
"pnl_total": total_bucket["pnl_total"],
"pnl_ex_sick": total_bucket["pnl_ex_sick"],
"win_count": total_bucket["win_count"],
"loss_count": total_bucket["loss_count"],
"avg_win": total_bucket["avg_win"],
"avg_loss": total_bucket["avg_loss"],
"max_win": total_bucket["max_win"],
"max_loss": total_bucket["max_loss"],
"by_exchange": by_ex,
}
@@ -1444,7 +1484,6 @@ def list_daily_trades(
params,
).fetchall()
overlays_by_ex: dict[str, dict[int, dict]] = {}
all_rows: list[dict[str, Any]] = []
trades: list[dict[str, Any]] = []
q = (search or "").strip().lower()
for r in rows:
@@ -1452,7 +1491,6 @@ def list_daily_trades(
if ex_k not in overlays_by_ex:
overlays_by_ex[ex_k] = load_overlays(ex_k, db_path=db_path)
td_row = _trade_row_to_dict(r, overlays_by_ex[ex_k].get(int(r["trade_id"])))
all_rows.append(td_row)
pnl = float(td_row.get("pnl_amount") or 0)
tag = td_row.get("behavior_tag") or ""
if filter_profit and pnl <= 0.0001:
@@ -1484,7 +1522,7 @@ def list_daily_trades(
"date_from": df,
"date_to": dt,
"trades": trades,
"stats": _compute_period_stats(all_rows),
"stats": _compute_period_stats(trades),
}
finally:
conn.close()