diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 82c38cb..82a6cc8 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -193,6 +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 def load_env_file(path): if not os.path.exists(path): @@ -2132,6 +2133,10 @@ def to_effective_trade_dict(row): item["display_pnl_source"] = "reviewed" else: item["display_pnl_source"] = "local" + item["effective_result"] = normalize_result_with_pnl( + item.get("effective_result"), + item.get("effective_pnl_amount"), + ) return item @@ -2573,19 +2578,6 @@ def calc_actual_rr(pnl_amount, risk_amount): return None -def normalize_result_with_pnl(result, pnl_amount): - """ - 触发“止损”但实际已盈利时,归类为保本止盈,避免语义混淆。 - """ - if result == "止损": - try: - if float(pnl_amount or 0) > 0: - return "保本止盈" - except Exception: - pass - return result - - def calc_breakeven_stop(direction, entry_price, risk_fraction, locked_r, offset_pct): """ 按“已锁定R”计算目标止损位: @@ -4352,7 +4344,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None): closed_at_ms=close_ms, ) return ( - guessed, + normalize_result_with_pnl(guessed, pnl2), pnl2, closed_at_str, "未能拉取成交明细,按当前市价与止盈/止损位近似归类(建议核对交易所账单)", @@ -4367,7 +4359,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None): result = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_px) if result: return ( - result, + normalize_result_with_pnl(result, pnl), pnl, closed_at_str, "按交易所成交/流水同步为止盈/止损平仓", @@ -6288,7 +6280,7 @@ def check_order_monitors(): hold_seconds = calc_hold_seconds(opened_at, now) pnl_amount = calc_pnl(direction, trigger_price, p, margin_capital, leverage) if res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(res, pnl_amount) close_order_id = "" @@ -6320,7 +6312,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: @@ -6351,7 +6343,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 11bfdf3..d06a926 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -193,6 +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 def load_env_file(path): @@ -2089,6 +2090,10 @@ def to_effective_trade_dict(row): item["display_pnl_source"] = "reviewed" else: item["display_pnl_source"] = "local" + item["effective_result"] = normalize_result_with_pnl( + item.get("effective_result"), + item.get("effective_pnl_amount"), + ) return item @@ -2287,19 +2292,6 @@ def calc_actual_rr(pnl_amount, risk_amount): return None -def normalize_result_with_pnl(result, pnl_amount): - """ - 触发“止损”但实际已盈利时,归类为保本止盈,避免语义混淆。 - """ - if result == "止损": - try: - if float(pnl_amount or 0) > 0: - return "保本止盈" - except Exception: - pass - return result - - def calc_breakeven_stop(direction, entry_price, risk_fraction, locked_r, offset_pct): """ 按“已锁定R”计算目标止损位: @@ -3992,7 +3984,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_m if guessed: pnl = calc_pnl(direction, trigger_price, p, margin_capital, leverage) return ( - guessed, + normalize_result_with_pnl(guessed, pnl), pnl, closed_at_str, "未能拉取成交明细,按当前市价与止盈/止损位近似归类(建议核对交易所账单)", @@ -4015,7 +4007,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_m ) if result: return ( - result, + normalize_result_with_pnl(result, pnl), pnl, closed_at_str, "按交易所成交记录同步为止盈/止损平仓", @@ -6047,7 +6039,7 @@ def check_order_monitors(): hold_seconds = calc_hold_seconds(opened_at, now) pnl_amount = calc_pnl(direction, trigger_price, p, margin_capital, leverage) if res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(res, pnl_amount) close_order_id = "" @@ -6078,7 +6070,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: @@ -6109,7 +6101,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 9789716..2d8bc5e 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -193,6 +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 def load_env_file(path): @@ -2089,6 +2090,10 @@ def to_effective_trade_dict(row): item["display_pnl_source"] = "reviewed" else: item["display_pnl_source"] = "local" + item["effective_result"] = normalize_result_with_pnl( + item.get("effective_result"), + item.get("effective_pnl_amount"), + ) return item @@ -2287,19 +2292,6 @@ def calc_actual_rr(pnl_amount, risk_amount): return None -def normalize_result_with_pnl(result, pnl_amount): - """ - 触发“止损”但实际已盈利时,归类为保本止盈,避免语义混淆。 - """ - if result == "止损": - try: - if float(pnl_amount or 0) > 0: - return "保本止盈" - except Exception: - pass - return result - - def calc_breakeven_stop(direction, entry_price, risk_fraction, locked_r, offset_pct): """ 按“已锁定R”计算目标止损位: @@ -3992,7 +3984,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_m if guessed: pnl = calc_pnl(direction, trigger_price, p, margin_capital, leverage) return ( - guessed, + normalize_result_with_pnl(guessed, pnl), pnl, closed_at_str, "未能拉取成交明细,按当前市价与止盈/止损位近似归类(建议核对交易所账单)", @@ -4015,7 +4007,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_m ) if result: return ( - result, + normalize_result_with_pnl(result, pnl), pnl, closed_at_str, "按交易所成交记录同步为止盈/止损平仓", @@ -6047,7 +6039,7 @@ def check_order_monitors(): hold_seconds = calc_hold_seconds(opened_at, now) pnl_amount = calc_pnl(direction, trigger_price, p, margin_capital, leverage) if res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(res, pnl_amount) close_order_id = "" @@ -6078,7 +6070,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: @@ -6109,7 +6101,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 7f8d77f..a0dff1f 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -192,6 +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 def load_env_file(path): @@ -2038,6 +2039,10 @@ def to_effective_trade_dict(row): item["display_pnl_source"] = "reviewed" else: item["display_pnl_source"] = "local" + item["effective_result"] = normalize_result_with_pnl( + item.get("effective_result"), + item.get("effective_pnl_amount"), + ) return item @@ -2183,19 +2188,6 @@ def calc_actual_rr(pnl_amount, risk_amount): return None -def normalize_result_with_pnl(result, pnl_amount): - """ - 触发“止损”但实际已盈利时,归类为保本止盈,避免语义混淆。 - """ - if result == "止损": - try: - if float(pnl_amount or 0) > 0: - return "保本止盈" - except Exception: - pass - return result - - def calc_breakeven_stop(direction, entry_price, risk_fraction, locked_r, offset_pct): """ 按“已锁定R”计算目标止损位: @@ -3448,7 +3440,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None): if guessed: pnl = calc_pnl(direction, trigger_price, p, margin_capital, leverage) return ( - guessed, + normalize_result_with_pnl(guessed, pnl), pnl, closed_at_str, "未能拉取成交明细,按当前市价与止盈/止损位近似归类(建议核对交易所账单)", @@ -3464,7 +3456,7 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None): pnl = calc_pnl(direction, trigger_price, exit_px, margin_capital, leverage) if result: return ( - result, + normalize_result_with_pnl(result, pnl), pnl, closed_at_str, "按交易所成交记录同步为止盈/止损平仓", @@ -5796,7 +5788,7 @@ def check_order_monitors(): hold_seconds = calc_hold_seconds(opened_at, now) pnl_amount = calc_pnl(direction, trigger_price, p, margin_capital, leverage) if res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(res, pnl_amount) close_order_id = "" @@ -5827,7 +5819,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: @@ -5857,7 +5849,7 @@ def check_order_monitors(): guessed_res = classify_exit_by_levels(direction, trigger_price, stop_loss, take_profit, exit_p) if guessed_res: if guessed_res == "止损" and float(pnl_amount or 0) > 0: - res = "移动止盈" if breakeven_armed else "保本止盈" + res = normalize_result_with_pnl("止损", pnl_amount) else: res = normalize_result_with_pnl(guessed_res, pnl_amount) else: diff --git a/embed_templates/embed_boot_scripts.html b/embed_templates/embed_boot_scripts.html index 8e83b57..ead5c0d 100644 --- a/embed_templates/embed_boot_scripts.html +++ b/embed_templates/embed_boot_scripts.html @@ -684,8 +684,21 @@ function toggleStatsCard(){ btn.innerText = collapsed ? "展开" : "折叠"; } +function bindListWindowDateAutoCustom(){ + const preset = document.getElementById("win-preset-select"); + const fromEl = document.getElementById("win-from-utc"); + const toEl = document.getElementById("win-to-utc"); + function toCustom(){ + if(preset) preset.value = "custom"; + toggleListWindowCustom(); + } + if(fromEl) fromEl.addEventListener("change", toCustom); + if(toEl) toEl.addEventListener("change", toCustom); +} + attachListWindowToExports(); toggleListWindowCustom(); +bindListWindowDateAutoCustom(); initStatsSegmentFromUrl(); if(document.getElementById("journal-list")) loadJournals(); if(document.getElementById("review-list")) loadReviews(); diff --git a/embed_templates/embed_shell.html b/embed_templates/embed_shell.html index bc2c254..0977200 100644 --- a/embed_templates/embed_shell.html +++ b/embed_templates/embed_shell.html @@ -115,6 +115,6 @@ {% include 'embed_boot_scripts.html' %} - + diff --git a/static/instance_embed.js b/static/instance_embed.js index e8215e1..c490072 100644 --- a/static/instance_embed.js +++ b/static/instance_embed.js @@ -127,7 +127,12 @@ function patchApplyListWindow() { if (typeof global.applyListWindow !== "function") return; global.applyListWindow = function embedApplyListWindow() { - void loadTab(getTab(), { replace: true }); + const qs = listWindowQueryString(); + const tab = getTab(); + const q = new URLSearchParams(qs); + q.set("tab", tab); + q.set("embed", "1"); + window.location.href = "/embed?" + q.toString(); }; } diff --git a/tests/test_trade_result_lib.py b/tests/test_trade_result_lib.py new file mode 100644 index 0000000..a7e5fbb --- /dev/null +++ b/tests/test_trade_result_lib.py @@ -0,0 +1,17 @@ +from trade_result_lib import normalize_result_with_pnl + + +def test_stop_loss_with_profit_becomes_trailing_tp(): + assert normalize_result_with_pnl("止损", 4.33) == "移动止盈" + + +def test_manual_close_unchanged_even_with_profit(): + assert normalize_result_with_pnl("手动平仓", 10) == "手动平仓" + + +def test_stop_loss_with_loss_unchanged(): + assert normalize_result_with_pnl("止损", -2.5) == "止损" + + +def test_take_profit_unchanged(): + assert normalize_result_with_pnl("止盈", 5) == "止盈" diff --git a/trade_result_lib.py b/trade_result_lib.py new file mode 100644 index 0000000..3433b30 --- /dev/null +++ b/trade_result_lib.py @@ -0,0 +1,18 @@ +"""交易结果展示与入库时的语义归一化。""" + + +def normalize_result_with_pnl(result, pnl_amount): + """ + 非手动平仓且实际盈利时,不应记为「止损」。 + 程序触发的止损类平仓若盈亏为正,归类为「移动止盈」。 + """ + res = (result or "").strip() + if res == "手动平仓": + return res + if res == "止损": + try: + if float(pnl_amount or 0) > 0: + return "移动止盈" + except (TypeError, ValueError): + pass + return res