From b5955f15ae7fb06a3d8cc5c9157be23e7f0459d1 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 12 Aug 2026 22:23:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=A4=E6=98=93=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=9F=BA=E6=95=B0=E8=AF=AF=E6=8A=8A=E6=8A=98=E7=AE=97?= =?UTF-8?q?=E6=A0=87=E7=9A=84=E5=B8=81=E6=95=B0=E9=87=8F=E5=BD=93=E4=BF=9D?= =?UTF-8?q?=E8=AF=81=E9=87=91=EF=BC=9A=E5=86=99=E5=85=A5=E6=B8=85=E6=B4=97?= =?UTF-8?q?+=E5=88=97=E8=A1=A8=E5=B1=95=E7=A4=BA=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=BA=A0=E5=81=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- crypto_monitor_gate/app.py | 55 +++++++-- lib/instance/records_list_lib.py | 20 +++- lib/trade/trade_margin_record_lib.py | 155 ++++++++++++++++++++++++++ tests/test_trade_margin_record_lib.py | 98 ++++++++++++++++ 4 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 lib/trade/trade_margin_record_lib.py create mode 100644 tests/test_trade_margin_record_lib.py diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 6c611ed..5ad4759 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -3803,6 +3803,24 @@ def parse_ccxt_position_metrics(position, order_leverage=None): ) mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice")) out = {} + try: + contracts = abs(float(p.get("contracts") or info.get("size") or 0)) + except (TypeError, ValueError): + contracts = 0.0 + try: + sym0 = (p.get("symbol") or "").strip() + cs0 = float(get_contract_size(sym0)) if sym0 else 1.0 + except Exception: + cs0 = 1.0 + coin_amt = contracts * cs0 if contracts > 0 and cs0 > 0 else None + from lib.trade.trade_margin_record_lib import sanitize_exchange_initial_margin + + initial = sanitize_exchange_initial_margin( + initial, + notional=notional, + order_leverage=order_leverage, + coin_amount=coin_amt, + ) if initial is not None and initial > 0: out["initial_margin"] = round(initial, 2) if notional is not None and notional > 0: @@ -3859,21 +3877,38 @@ def _order_row_exchange_margin_usdt(row): def margin_capital_for_trade_record(order_row): """trade_records.基数:优先交易所持仓保证金快照,旧数据无快照时回退计划保证金.""" - ex = _order_row_exchange_margin_usdt(order_row) - if ex is not None: - return round(ex, 2) + from lib.trade.trade_margin_record_lib import resolve_trade_record_margin_usdt + if not order_row: return None try: - v = order_row["margin_capital"] + plan = order_row["margin_capital"] except (TypeError, KeyError, IndexError): - return None - if v is None: - return None + plan = None + base = None + notional = None + lev = None + trigger = None try: - return float(v) - except (TypeError, ValueError): - return None + keys = order_row.keys() + except Exception: + keys = [] + if "base_amount" in keys: + base = order_row["base_amount"] + if "notional_value" in keys: + notional = order_row["notional_value"] + if "leverage" in keys: + lev = order_row["leverage"] + if "trigger_price" in keys: + trigger = order_row["trigger_price"] + return resolve_trade_record_margin_usdt( + exchange_margin_usdt=_order_row_exchange_margin_usdt(order_row), + plan_margin_capital=plan, + base_amount=base, + notional_value=notional, + leverage=lev, + trigger_price=trigger, + ) def try_persist_exchange_margin_for_order(conn, order_id, exchange_symbol, direction, order_leverage=None, max_attempts=6, sleep_s=0.45): diff --git a/lib/instance/records_list_lib.py b/lib/instance/records_list_lib.py index 8b8062b..379cbd6 100644 --- a/lib/instance/records_list_lib.py +++ b/lib/instance/records_list_lib.py @@ -10,7 +10,22 @@ def enrich_trade_price_displays( format_price_fn: Optional[Callable[[Any, Any], str]] = None, ) -> dict[str, Any]: """为成交/止损/止盈补交易所精度展示字段(供交易记录表直接渲染).""" - if not format_price_fn or not isinstance(item, dict): + if not isinstance(item, dict): + return item + try: + from lib.trade.trade_margin_record_lib import repair_stored_margin_capital + + fixed = repair_stored_margin_capital( + item.get("margin_capital"), + trigger_price=item.get("trigger_price"), + leverage=item.get("leverage"), + symbol=item.get("symbol"), + ) + if fixed is not None: + item["margin_capital"] = fixed + except Exception: + pass + if not format_price_fn: return item sym = item.get("symbol") stop_show = item.get("display_open_stop_loss") @@ -59,8 +74,7 @@ def list_trade_records_page( page = pages offset = (page - 1) * limit items = records[offset : offset + limit] - if format_price_fn is not None: - items = [enrich_trade_price_displays(dict(it), format_price_fn) for it in items] + items = [enrich_trade_price_displays(dict(it), format_price_fn) for it in items] return { "ok": True, "items": items, diff --git a/lib/trade/trade_margin_record_lib.py b/lib/trade/trade_margin_record_lib.py new file mode 100644 index 0000000..0043cd4 --- /dev/null +++ b/lib/trade/trade_margin_record_lib.py @@ -0,0 +1,155 @@ +"""交易记录「基数」= U 保证金;防止把折算标的(币数量)误写成/误显示为基数.""" +from __future__ import annotations + +from typing import Any, Optional + + +def _pos_float(v: Any) -> Optional[float]: + if v is None or v == "": + return None + try: + x = float(v) + except (TypeError, ValueError): + return None + if x != x or x <= 0: # NaN or non-positive + return None + return x + + +def _near(a: float, b: float, *, rel: float = 0.05, abs_tol: float = 1e-4) -> bool: + scale = max(abs(a), abs(b), abs_tol) + return abs(a - b) <= max(abs_tol, rel * scale) + + +def looks_like_coin_amount_as_margin( + margin: Any, + *, + trigger_price: Any = None, + leverage: Any = None, + base_amount: Any = None, + plan_margin: Any = None, + notional_value: Any = None, +) -> bool: + """判断 margin 是否更像折算标的币数,而非 USDT 保证金.""" + m = _pos_float(margin) + if m is None: + return False + base = _pos_float(base_amount) + if base is not None and _near(m, base, rel=0.05, abs_tol=1e-4): + return True + plan = _pos_float(plan_margin) + if plan is not None and plan >= 1.0 and m < max(1.0, plan * 0.05): + return True + notional = _pos_float(notional_value) + lev = _pos_float(leverage) + if notional is not None and lev is not None and lev > 0: + approx = notional / lev + if approx >= 1.0 and m < max(1.0, approx * 0.05): + return True + px = _pos_float(trigger_price) + if px is not None and lev is not None and lev > 0 and m < 1.0 and px >= 1000: + # 高价币:保证金 <1U 极不合理;按币数量反推的保证金却合理 + implied = m * px / lev + if implied >= 10.0: + return True + return False + + +def coin_amount_to_margin_usdt( + coin_amount: Any, + *, + trigger_price: Any, + leverage: Any, +) -> Optional[float]: + """币数量 × 价格 / 杠杆 ≈ USDT 保证金.""" + coin = _pos_float(coin_amount) + px = _pos_float(trigger_price) + lev = _pos_float(leverage) + if coin is None or px is None or lev is None or lev <= 0: + return None + return round(coin * px / lev, 2) + + +def sanitize_exchange_initial_margin( + initial: Any, + *, + notional: Any = None, + order_leverage: Any = None, + coin_amount: Any = None, +) -> Optional[float]: + """交易所快照保证金清洗:拒绝币数量误入,必要时用名义/杠杆粗算.""" + m = _pos_float(initial) + coin = _pos_float(coin_amount) + if m is not None and coin is not None and _near(m, coin, rel=0.05, abs_tol=1e-4): + m = None + notional_v = _pos_float(notional) + lev = _pos_float(order_leverage) + approx = None + if notional_v is not None and lev is not None and lev > 0: + approx = notional_v / lev + if m is not None and approx is not None and approx >= 1.0 and m < max(1.0, approx * 0.05): + m = None + if m is not None: + return round(m, 4) + if approx is not None and approx > 0: + return round(approx, 4) + return None + + +def resolve_trade_record_margin_usdt( + *, + exchange_margin_usdt: Any = None, + plan_margin_capital: Any = None, + base_amount: Any = None, + notional_value: Any = None, + leverage: Any = None, + trigger_price: Any = None, +) -> Optional[float]: + """写入 trade_records.基数:优先交易所快照,异常时回退计划保证金.""" + plan = _pos_float(plan_margin_capital) + ex = _pos_float(exchange_margin_usdt) + if ex is not None and looks_like_coin_amount_as_margin( + ex, + trigger_price=trigger_price, + leverage=leverage, + base_amount=base_amount, + plan_margin=plan, + notional_value=notional_value, + ): + # 快照像币数量:优先用计划保证金;无计划时再按币×价/杠杆反推 + if plan is not None: + return round(plan, 2) + repaired = coin_amount_to_margin_usdt( + ex, trigger_price=trigger_price, leverage=leverage + ) + if repaired is not None: + return repaired + ex = None + if ex is not None: + return round(ex, 2) + if plan is not None: + return round(plan, 2) + return None + + +def repair_stored_margin_capital( + margin_capital: Any, + *, + trigger_price: Any = None, + leverage: Any = None, + symbol: Any = None, +) -> Optional[float]: + """展示/列表:修复已入库的「币数量当基数」旧数据.""" + del symbol # 预留按币种阈值;当前用价位启发式即可 + m = _pos_float(margin_capital) + if m is None: + return None + if looks_like_coin_amount_as_margin( + m, trigger_price=trigger_price, leverage=leverage + ): + repaired = coin_amount_to_margin_usdt( + m, trigger_price=trigger_price, leverage=leverage + ) + if repaired is not None: + return repaired + return round(m, 2) diff --git a/tests/test_trade_margin_record_lib.py b/tests/test_trade_margin_record_lib.py new file mode 100644 index 0000000..eb8039e --- /dev/null +++ b/tests/test_trade_margin_record_lib.py @@ -0,0 +1,98 @@ +"""交易记录基数(保证金)口径:币数量误记为基数时的修复.""" +from __future__ import annotations + +import unittest + +from lib.trade.trade_margin_record_lib import ( + coin_amount_to_margin_usdt, + looks_like_coin_amount_as_margin, + repair_stored_margin_capital, + resolve_trade_record_margin_usdt, + sanitize_exchange_initial_margin, +) + + +class TestTradeMarginRecord(unittest.TestCase): + def test_detect_coin_as_margin_btc(self): + self.assertTrue( + looks_like_coin_amount_as_margin( + 0.12, trigger_price=64054.1, leverage=20 + ) + ) + self.assertFalse( + looks_like_coin_amount_as_margin( + 108.97, trigger_price=64054.1, leverage=20 + ) + ) + + def test_detect_when_matches_base_amount(self): + self.assertTrue( + looks_like_coin_amount_as_margin( + 0.12, base_amount=0.12, plan_margin=110.0 + ) + ) + + def test_coin_to_margin(self): + # 0.12 BTC * 64054 / 20 ≈ 384.32 + self.assertAlmostEqual( + coin_amount_to_margin_usdt(0.12, trigger_price=64054.1, leverage=20), + 384.32, + places=2, + ) + + def test_resolve_prefers_plan_when_exchange_is_coin(self): + out = resolve_trade_record_margin_usdt( + exchange_margin_usdt=0.12, + plan_margin_capital=117.71, + base_amount=0.12, + leverage=20, + trigger_price=64054.1, + ) + self.assertEqual(out, 117.71) + + def test_resolve_repairs_coin_when_no_plan(self): + out = resolve_trade_record_margin_usdt( + exchange_margin_usdt=0.12, + plan_margin_capital=None, + leverage=20, + trigger_price=64054.1, + ) + self.assertEqual(out, 384.32) + + def test_resolve_uses_plan_when_cannot_repair(self): + out = resolve_trade_record_margin_usdt( + exchange_margin_usdt=0.05, + plan_margin_capital=100.0, + leverage=None, + trigger_price=None, + base_amount=0.05, + ) + self.assertEqual(out, 100.0) + + def test_repair_stored_display(self): + self.assertEqual( + repair_stored_margin_capital( + 0.12, trigger_price=64054.1, leverage=20 + ), + 384.32, + ) + self.assertEqual( + repair_stored_margin_capital( + 108.97, trigger_price=64054.1, leverage=20 + ), + 108.97, + ) + + def test_sanitize_exchange_margin(self): + out = sanitize_exchange_initial_margin( + 0.12, notional=7686.0, order_leverage=20, coin_amount=0.12 + ) + self.assertAlmostEqual(out, 384.3, places=1) + ok = sanitize_exchange_initial_margin( + 110.0, notional=2200.0, order_leverage=20, coin_amount=0.12 + ) + self.assertEqual(ok, 110.0) + + +if __name__ == "__main__": + unittest.main()