From 448e88ec55cdf849a29b4771ba3d096411304773 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 25 Jun 2026 22:27:52 +0800 Subject: [PATCH] Count instance win rate by positive PnL and show external closes as manual close. Co-authored-by: Cursor --- crypto_monitor_binance/app.py | 8 ++------ crypto_monitor_gate/app.py | 8 ++------ crypto_monitor_gate_bot/app.py | 8 ++------ crypto_monitor_okx/app.py | 8 ++------ instance_embed_context_lib.py | 5 ++++- tests/test_trade_result_lib.py | 15 ++++++++++++++- trade_result_lib.py | 29 ++++++++++++++++++++++++++++- 7 files changed, 54 insertions(+), 27 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 82a6cc8..fade64c 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -193,7 +193,7 @@ from history_window_lib import ( utc_window_to_bj_sql_strings, utc_window_to_utc_sql_strings, ) -from trade_result_lib import normalize_result_with_pnl +from trade_result_lib import count_winning_trades, normalize_result_with_pnl def load_env_file(path): if not os.path.exists(path): @@ -6883,11 +6883,7 @@ def render_main_page(page="trade", embed_mode=None): records = [to_effective_trade_dict(r) for r in raw_records] total = len(records) miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过") - win = sum( - 1 - for r in records - if (r.get("effective_result") or "") in ("止盈", "保本止盈", "移动止盈") - ) + win = count_winning_trades(records) occupied_miss_total = sum( 1 for r in records diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index d06a926..da2a23a 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -193,7 +193,7 @@ from history_window_lib import ( utc_window_to_bj_sql_strings, utc_window_to_utc_sql_strings, ) -from trade_result_lib import normalize_result_with_pnl +from trade_result_lib import count_winning_trades, normalize_result_with_pnl def load_env_file(path): @@ -6766,11 +6766,7 @@ def render_main_page(page="trade", embed_mode=None): records = [to_effective_trade_dict(r) for r in raw_records] total = len(records) miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过") - win = sum( - 1 - for r in records - if (r.get("effective_result") or "") in ("止盈", "保本止盈", "移动止盈") - ) + win = count_winning_trades(records) occupied_miss_total = sum( 1 for r in records diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 2d8bc5e..a9d3530 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -193,7 +193,7 @@ from history_window_lib import ( utc_window_to_bj_sql_strings, utc_window_to_utc_sql_strings, ) -from trade_result_lib import normalize_result_with_pnl +from trade_result_lib import count_winning_trades, normalize_result_with_pnl def load_env_file(path): @@ -6766,11 +6766,7 @@ def render_main_page(page="trade", embed_mode=None): records = [to_effective_trade_dict(r) for r in raw_records] total = len(records) miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过") - win = sum( - 1 - for r in records - if (r.get("effective_result") or "") in ("止盈", "保本止盈", "移动止盈") - ) + win = count_winning_trades(records) occupied_miss_total = sum( 1 for r in records diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index a0dff1f..c4f681f 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -192,7 +192,7 @@ from history_window_lib import ( utc_window_to_bj_sql_strings, utc_window_to_utc_sql_strings, ) -from trade_result_lib import normalize_result_with_pnl +from trade_result_lib import count_winning_trades, normalize_result_with_pnl def load_env_file(path): @@ -6270,11 +6270,7 @@ def render_main_page(page="trade", embed_mode=None): records = [to_effective_trade_dict(r) for r in raw_records] total = len(records) miss_count = sum(1 for r in records if (r.get("effective_result") or "") == "错过") - win = sum( - 1 - for r in records - if (r.get("effective_result") or "") in ("止盈", "保本止盈", "移动止盈") - ) + win = count_winning_trades(records) occupied_miss_total = sum( 1 for r in records diff --git a/instance_embed_context_lib.py b/instance_embed_context_lib.py index 051b5db..dc682ca 100644 --- a/instance_embed_context_lib.py +++ b/instance_embed_context_lib.py @@ -51,12 +51,15 @@ def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan: def trade_records_summary(conn, start_bj: str, end_bj: str, tr_ts: str) -> dict[str, Any]: """顶栏统计用 COUNT,避免 embed 壳拉 1000 行交易记录。""" + from trade_result_lib import sql_effective_pnl_expr + + pnl_sql = sql_effective_pnl_expr() row = conn.execute( f""" SELECT COUNT(*) AS total, SUM(CASE WHEN result = '错过' THEN 1 ELSE 0 END) AS miss_count, - SUM(CASE WHEN result IN ('止盈','保本止盈','移动止盈') THEN 1 ELSE 0 END) AS wins, + SUM(CASE WHEN {pnl_sql} > 0 THEN 1 ELSE 0 END) AS wins, SUM(CASE WHEN result = '错过' AND COALESCE(miss_reason,'') LIKE '%持仓占用%' THEN 1 ELSE 0 END) AS occupied_miss FROM trade_records WHERE {tr_ts} >= ? AND {tr_ts} <= ? diff --git a/tests/test_trade_result_lib.py b/tests/test_trade_result_lib.py index a7e5fbb..69bbfd0 100644 --- a/tests/test_trade_result_lib.py +++ b/tests/test_trade_result_lib.py @@ -1,4 +1,4 @@ -from trade_result_lib import normalize_result_with_pnl +from trade_result_lib import normalize_result_with_pnl, normalize_display_result, is_winning_pnl def test_stop_loss_with_profit_becomes_trailing_tp(): @@ -15,3 +15,16 @@ def test_stop_loss_with_loss_unchanged(): def test_take_profit_unchanged(): assert normalize_result_with_pnl("止盈", 5) == "止盈" + + +def test_external_close_becomes_manual_close(): + assert normalize_display_result("外部平仓") == "手动平仓" + assert normalize_result_with_pnl("外部平仓", 2.5) == "手动平仓" + assert normalize_result_with_pnl("外部平仓(自动同步)", -1) == "手动平仓" + + +def test_winning_pnl_positive_only(): + assert is_winning_pnl(2.96) is True + assert is_winning_pnl(0) is False + assert is_winning_pnl(-1.05) is False + assert is_winning_pnl(None) is False diff --git a/trade_result_lib.py b/trade_result_lib.py index 3433b30..838e5fa 100644 --- a/trade_result_lib.py +++ b/trade_result_lib.py @@ -1,12 +1,39 @@ """交易结果展示与入库时的语义归一化。""" +_WIN_EPS = 1e-9 + + +def normalize_display_result(result): + """展示用:外部平仓一律视为手动平仓。""" + res = (result or "").strip() + if res == "外部平仓" or res.startswith("外部平仓"): + return "手动平仓" + return res + + +def is_winning_pnl(pnl_amount) -> bool: + """胜率统计:盈亏为正即计为盈利单。""" + try: + return float(pnl_amount or 0) > _WIN_EPS + except (TypeError, ValueError): + return False + + +def sql_effective_pnl_expr() -> str: + """与 to_effective_trade_dict / hub_trades_lib 一致的盈亏 SQL 表达式。""" + return "COALESCE(reviewed_pnl_amount, exchange_realized_pnl, pnl_amount, 0)" + + +def count_winning_trades(trades) -> int: + return sum(1 for r in trades or [] if is_winning_pnl(r.get("effective_pnl_amount"))) + def normalize_result_with_pnl(result, pnl_amount): """ 非手动平仓且实际盈利时,不应记为「止损」。 程序触发的止损类平仓若盈亏为正,归类为「移动止盈」。 """ - res = (result or "").strip() + res = normalize_display_result(result) if res == "手动平仓": return res if res == "止损":