"""交易结果展示与入库时的语义归一化.""" from __future__ import annotations from typing import Optional _WIN_EPS = 1e-9 def classify_exit_by_levels( direction, trigger_price, stop_loss, take_profit, exit_price, ) -> Optional[str]: """根据成交价相对止盈/止损位归类;无法可靠归类时返回 None. 交易所条件止盈常按标记价触发、市价成交,成交价可能偏离计划止盈数个 tick. 因此先用窄带,失败后再用宽带;仍失败则看是否落在入场→止盈/止损的「盈利/亏损侧」。 """ try: tp = float(take_profit) sl = float(stop_loss) ex = float(exit_price) trig = float(trigger_price) except (TypeError, ValueError): return None d = (direction or "").strip().lower() if d not in ("long", "short"): return None band = max(abs(trig) * 0.0008, abs(tp - sl) * 0.003, 1e-12) # 宽带:覆盖 BTC 等高价币种条件单滑点(实测 Gate 止盈成交可偏出窄带 ~100U) band_loose = max(abs(trig) * 0.003, abs(tp - sl) * 0.05, band * 4.0, 1e-12) def _is_tp(b: float) -> bool: return ex >= tp - b if d == "long" else ex <= tp + b def _is_sl(b: float) -> bool: return ex <= sl + b if d == "long" else ex >= sl - b if _is_tp(band): return "止盈" if _is_sl(band): return "止损" if _is_tp(band_loose): return "止盈" if _is_sl(band_loose): return "止损" # 盈利侧且更靠近止盈 → 止盈; 亏损侧且更靠近止损 → 止损 if d == "long": if ex > trig and abs(ex - tp) <= abs(ex - trig): return "止盈" if ex < trig and abs(ex - sl) <= abs(ex - trig): return "止损" else: if ex < trig and abs(ex - tp) <= abs(ex - trig): return "止盈" if ex > trig and abs(ex - sl) <= abs(ex - trig): return "止损" return None 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"))) MISS_TRADE_RESULT = "错过" def is_miss_trade_result(result) -> bool: return (result or "").strip() == MISS_TRADE_RESULT def filter_trade_records_excluding_miss(records): """列表/统计:不展示,不计入「错过」类交易记录.""" return [ r for r in (records or []) if not is_miss_trade_result(r.get("effective_result") or r.get("result")) ] def normalize_result_with_pnl(result, pnl_amount): """ 非手动平仓且实际盈利时,不应记为「止损」. 程序触发的止损类平仓若盈亏为正,归类为「移动止盈」. """ res = normalize_display_result(result) if res == "手动平仓": return res if res == "止损": try: if float(pnl_amount or 0) > 0: return "移动止盈" except (TypeError, ValueError): pass return res