diff --git a/docs/hub-symbol-archive-kline.md b/docs/hub-symbol-archive-kline.md index 4463c6a..959e021 100644 --- a/docs/hub-symbol-archive-kline.md +++ b/docs/hub-symbol-archive-kline.md @@ -93,7 +93,7 @@ | `filter_profit` / `filter_loss` / `filter_sick` | 过滤列表与统计 | | `search` | 合约 / 交易所 / 备注搜索(同步过滤列表与统计) | -返回 `stats` 含 `open_count`、`win_count`、`loss_count`、`avg_win`、`avg_loss`、`max_win`、`max_loss`、`sick_count`、`sick_pct`、`pnl_total`、`pnl_ex_sick`、`by_exchange`。 +返回 `stats` 含 `open_count`、`win_count`、`loss_count`、`win_rate`、`avg_win`、`avg_loss`、`profit_loss_ratio`、`max_win`、`max_loss`、`sick_count`、`sick_pct`、`pnl_total`、`pnl_ex_sick`、`by_exchange`。 实例侧: diff --git a/hub_symbol_archive_lib.py b/hub_symbol_archive_lib.py index 893848f..2745183 100644 --- a/hub_symbol_archive_lib.py +++ b/hub_symbol_archive_lib.py @@ -1275,14 +1275,23 @@ def _empty_pnl_bucket() -> dict[str, Any]: def _finalize_pnl_bucket(bucket: dict[str, Any]) -> None: wins = bucket.pop("_wins", []) losses = bucket.pop("_losses", []) - bucket["win_count"] = len(wins) + open_count = int(bucket.get("open_count") or 0) + win_count = len(wins) + bucket["win_count"] = win_count 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 + avg_loss = round(sum(losses) / len(losses), 4) if losses else None + bucket["avg_loss"] = avg_loss 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) + bucket["win_rate"] = round(win_count / open_count * 100, 1) if open_count else None + avg_win = bucket["avg_win"] + if avg_win is not None and avg_loss is not None and avg_loss != 0: + bucket["profit_loss_ratio"] = round(avg_win / abs(avg_loss), 2) + else: + bucket["profit_loss_ratio"] = None def _accumulate_trade_stat(bucket: dict[str, Any], *, pnl: float, is_sick: bool) -> None: @@ -1329,6 +1338,8 @@ def _compute_period_stats(trade_rows: list[dict[str, Any]]) -> dict[str, Any]: "avg_loss": total_bucket["avg_loss"], "max_win": total_bucket["max_win"], "max_loss": total_bucket["max_loss"], + "win_rate": total_bucket["win_rate"], + "profit_loss_ratio": total_bucket["profit_loss_ratio"], "by_exchange": by_ex, } diff --git a/manual_trading_hub/hub_ai/archive_quote.py b/manual_trading_hub/hub_ai/archive_quote.py index d610844..6dac822 100644 --- a/manual_trading_hub/hub_ai/archive_quote.py +++ b/manual_trading_hub/hub_ai/archive_quote.py @@ -42,6 +42,22 @@ def _fmt_pnl(v: Any) -> str: return f"{sign}{n:.2f}U" +def _fmt_pct(v: Any) -> str: + try: + n = float(v) + except (TypeError, ValueError): + return "—" + return f"{n:.1f}%" + + +def _fmt_rr(v: Any) -> str: + try: + n = float(v) + except (TypeError, ValueError): + return "—" + return f"{n:.2f}:1" + + def format_archive_trades_for_ai(payload: dict[str, Any]) -> str: trades = payload.get("trades") or [] stats = payload.get("stats") or {} @@ -50,6 +66,7 @@ def format_archive_trades_for_ai(payload: dict[str, Any]) -> str: f"统计:开仓 {int(stats.get('open_count') or 0)} 笔," f"盈利 {int(stats.get('win_count') or 0)} / 亏损 {int(stats.get('loss_count') or 0)}," f"平均盈利 {_fmt_pnl(stats.get('avg_win'))},平均亏损 {_fmt_pnl(stats.get('avg_loss'))}," + f"胜率 {_fmt_pct(stats.get('win_rate'))},盈亏比 {_fmt_rr(stats.get('profit_loss_ratio'))}," f"最大盈利 {_fmt_pnl(stats.get('max_win'))},最大亏损 {_fmt_pnl(stats.get('max_loss'))}," f"犯病 {int(stats.get('sick_count') or 0)} 笔," f"盈亏合计 {_fmt_pnl(stats.get('pnl_total'))}," diff --git a/manual_trading_hub/static/archive.js b/manual_trading_hub/static/archive.js index cf4e241..9a4e690 100644 --- a/manual_trading_hub/static/archive.js +++ b/manual_trading_hub/static/archive.js @@ -427,6 +427,19 @@ return fmtPnlStat(v); } + function fmtWinRate(v, openN, winN) { + if (v != null && v !== "") return Number(v).toFixed(1) + "%"; + if (openN) return (Math.round(((winN || 0) / openN) * 1000) / 10) + "%"; + return "—"; + } + + function fmtProfitLossRatio(v) { + if (v == null || v === "") return "—"; + const n = Number(v); + if (!Number.isFinite(n)) return "—"; + return n.toFixed(2) + ":1"; + } + function renderStatsRow(label, e, isTotal) { const openN = e.open_count || 0; const sickN = e.sick_count || 0; @@ -444,10 +457,14 @@ "
| 范围 | 开仓 | 盈利单 | 亏损单 | 平均盈利 | 平均亏损 | 最大盈利 | 最大亏损 | 犯病 | 犯病占比 | 盈亏 | 剔除犯病盈亏 | " + + "范围 | 开仓 | 盈利单 | 亏损单 | 胜率 | 平均盈利 | 平均亏损 | 盈亏比 | 最大盈利 | 最大亏损 | 犯病 | 犯病占比 | 盈亏 | 剔除犯病盈亏 | " + "
|---|