diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 5ad4759..be5a27e 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -3901,6 +3901,14 @@ def margin_capital_for_trade_record(order_row): lev = order_row["leverage"] if "trigger_price" in keys: trigger = order_row["trigger_price"] + stop = None + risk = None + if "initial_stop_loss" in keys: + stop = order_row["initial_stop_loss"] + if (stop is None or stop == "") and "stop_loss" in keys: + stop = order_row["stop_loss"] + if "risk_amount" in keys: + risk = order_row["risk_amount"] return resolve_trade_record_margin_usdt( exchange_margin_usdt=_order_row_exchange_margin_usdt(order_row), plan_margin_capital=plan, @@ -3908,6 +3916,8 @@ def margin_capital_for_trade_record(order_row): notional_value=notional, leverage=lev, trigger_price=trigger, + stop_loss=stop, + risk_amount=risk, ) diff --git a/lib/instance/records_list_lib.py b/lib/instance/records_list_lib.py index 379cbd6..c36e752 100644 --- a/lib/instance/records_list_lib.py +++ b/lib/instance/records_list_lib.py @@ -20,6 +20,9 @@ def enrich_trade_price_displays( trigger_price=item.get("trigger_price"), leverage=item.get("leverage"), symbol=item.get("symbol"), + stop_loss=item.get("stop_loss"), + initial_stop_loss=item.get("initial_stop_loss"), + risk_amount=item.get("risk_amount"), ) if fixed is not None: item["margin_capital"] = fixed diff --git a/lib/trade/trade_margin_record_lib.py b/lib/trade/trade_margin_record_lib.py index 0043cd4..d32612d 100644 --- a/lib/trade/trade_margin_record_lib.py +++ b/lib/trade/trade_margin_record_lib.py @@ -1,4 +1,4 @@ -"""交易记录「基数」= U 保证金;防止把折算标的(币数量)误写成/误显示为基数.""" +"""交易记录「基数」= U 保证金;防止币数量/账户保证金误入,展示时禁止用币×价÷杠杆虚增.""" from __future__ import annotations from typing import Any, Optional @@ -55,13 +55,33 @@ def looks_like_coin_amount_as_margin( return False +def margin_from_risk_amount( + risk_amount: Any, + *, + trigger_price: Any, + stop_loss: Any, + leverage: Any, +) -> Optional[float]: + """由风险金额反推保证金: risk ≈ margin × lev × |entry-sl| / entry.""" + risk = _pos_float(risk_amount) + px = _pos_float(trigger_price) + sl = _pos_float(stop_loss) + lev = _pos_float(leverage) + if risk is None or px is None or sl is None or lev is None or lev <= 0 or px <= 0: + return None + dist = abs(px - sl) + if dist <= 0: + return None + return round(risk * px / (lev * dist), 2) + + def coin_amount_to_margin_usdt( coin_amount: Any, *, trigger_price: Any, leverage: Any, ) -> Optional[float]: - """币数量 × 价格 / 杠杆 ≈ USDT 保证金.""" + """币数量 × 价格 / 杠杆 ≈ USDT 保证金(仅作末位兜底,展示层勿优先使用).""" coin = _pos_float(coin_amount) px = _pos_float(trigger_price) lev = _pos_float(leverage) @@ -77,7 +97,7 @@ def sanitize_exchange_initial_margin( 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): @@ -87,8 +107,12 @@ def sanitize_exchange_initial_margin( 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 and approx is not None and approx >= 1.0: + if m < max(1.0, approx * 0.05): + m = None + elif m > approx * 2.5: + # 常见于全仓 collateral≈账户权益,远大于本仓占用 + m = None if m is not None: return round(m, 4) if approx is not None and approx > 0: @@ -104,10 +128,22 @@ def resolve_trade_record_margin_usdt( notional_value: Any = None, leverage: Any = None, trigger_price: Any = None, + stop_loss: Any = None, + risk_amount: Any = None, ) -> Optional[float]: - """写入 trade_records.基数:优先交易所快照,异常时回退计划保证金.""" + """写入 trade_records.基数:计划保证金优先于异常交易所快照;禁止币×价÷杠杆虚增.""" plan = _pos_float(plan_margin_capital) ex = _pos_float(exchange_margin_usdt) + + if plan is not None and looks_like_coin_amount_as_margin( + plan, + trigger_price=trigger_price, + leverage=leverage, + base_amount=base_amount, + notional_value=notional_value, + ): + plan = None + if ex is not None and looks_like_coin_amount_as_margin( ex, trigger_price=trigger_price, @@ -116,19 +152,28 @@ def resolve_trade_record_margin_usdt( 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 ex is not None and plan is not None and ex > plan * 2.5: + ex = None + if plan is not None: return round(plan, 2) + if ex is not None: + return round(ex, 2) + + from_risk = margin_from_risk_amount( + risk_amount, + trigger_price=trigger_price, + stop_loss=stop_loss, + leverage=leverage, + ) + if from_risk is not None: + return from_risk + + notional = _pos_float(notional_value) + lev = _pos_float(leverage) + if notional is not None and lev is not None and lev > 0: + return round(notional / lev, 2) return None @@ -138,18 +183,31 @@ def repair_stored_margin_capital( trigger_price: Any = None, leverage: Any = None, symbol: Any = None, + stop_loss: Any = None, + risk_amount: Any = None, + initial_stop_loss: Any = None, ) -> Optional[float]: - """展示/列表:修复已入库的「币数量当基数」旧数据.""" - del symbol # 预留按币种阈值;当前用价位启发式即可 + """展示/列表:修复已入库的异常基数;禁止把币数量换算成虚高保证金.""" + del symbol # 预留按币种阈值 m = _pos_float(margin_capital) + sl = stop_loss if stop_loss not in (None, "") else initial_stop_loss + from_risk = margin_from_risk_amount( + risk_amount, + trigger_price=trigger_price, + stop_loss=sl, + leverage=leverage, + ) + if m is None: - return None + return from_risk + 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 from_risk + + if from_risk is not None and m > from_risk * 2.5: + # 已入库虚高(如曾用币×价÷杠杆「纠偏」成 384) + return from_risk + return round(m, 2) diff --git a/tests/test_trade_margin_record_lib.py b/tests/test_trade_margin_record_lib.py index eb8039e..bfa5d5a 100644 --- a/tests/test_trade_margin_record_lib.py +++ b/tests/test_trade_margin_record_lib.py @@ -1,4 +1,4 @@ -"""交易记录基数(保证金)口径:币数量误记为基数时的修复.""" +"""交易记录基数(保证金)口径:币数量误记 / 虚高纠偏.""" from __future__ import annotations import unittest @@ -6,6 +6,7 @@ import unittest from lib.trade.trade_margin_record_lib import ( coin_amount_to_margin_usdt, looks_like_coin_amount_as_margin, + margin_from_risk_amount, repair_stored_margin_capital, resolve_trade_record_margin_usdt, sanitize_exchange_initial_margin, @@ -33,13 +34,21 @@ class TestTradeMarginRecord(unittest.TestCase): ) 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_margin_from_risk(self): + # risk = 110 * 20 * 445.9 / 64054.1 ≈ 15.32 + risk = 110.0 * 20.0 * 445.9 / 64054.1 + out = margin_from_risk_amount( + risk, trigger_price=64054.1, stop_loss=64500.0, leverage=20 + ) + self.assertAlmostEqual(out, 110.0, places=1) + def test_resolve_prefers_plan_when_exchange_is_coin(self): out = resolve_trade_record_margin_usdt( exchange_margin_usdt=0.12, @@ -50,31 +59,60 @@ class TestTradeMarginRecord(unittest.TestCase): ) self.assertEqual(out, 117.71) - def test_resolve_repairs_coin_when_no_plan(self): + def test_resolve_prefers_plan_over_inflated_exchange(self): + out = resolve_trade_record_margin_usdt( + exchange_margin_usdt=384.32, + plan_margin_capital=117.71, + leverage=20, + trigger_price=64054.1, + ) + self.assertEqual(out, 117.71) + + def test_resolve_uses_risk_when_no_plan(self): + risk = 110.0 * 20.0 * 445.9 / 64054.1 + out = resolve_trade_record_margin_usdt( + exchange_margin_usdt=0.12, + plan_margin_capital=None, + leverage=20, + trigger_price=64054.1, + stop_loss=64500.0, + risk_amount=risk, + ) + self.assertAlmostEqual(out, 110.0, places=1) + + def test_resolve_does_not_invent_coin_times_price(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) + self.assertIsNone(out) - 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( + def test_repair_stored_uses_risk_not_coin_times_price(self): + risk = 110.0 * 20.0 * 445.9 / 64054.1 + self.assertAlmostEqual( repair_stored_margin_capital( - 0.12, trigger_price=64054.1, leverage=20 + 0.12, + trigger_price=64054.1, + leverage=20, + stop_loss=64500.0, + risk_amount=risk, ), - 384.32, + 110.0, + places=1, + ) + # 曾被错误「纠偏」成 384 的入库值,用风险金额压回 + self.assertAlmostEqual( + repair_stored_margin_capital( + 384.32, + trigger_price=64054.1, + leverage=20, + stop_loss=64500.0, + risk_amount=risk, + ), + 110.0, + places=1, ) self.assertEqual( repair_stored_margin_capital( @@ -82,6 +120,18 @@ class TestTradeMarginRecord(unittest.TestCase): ), 108.97, ) + # 无风险金额时:绝不把 0.12 换成 384 + self.assertIsNone( + repair_stored_margin_capital( + 0.12, trigger_price=64054.1, leverage=20 + ) + ) + + def test_sanitize_rejects_oversized_collateral(self): + out = sanitize_exchange_initial_margin( + 384.32, notional=2200.0, order_leverage=20, coin_amount=0.034 + ) + self.assertAlmostEqual(out, 110.0, places=1) def test_sanitize_exchange_margin(self): out = sanitize_exchange_initial_margin(