修复交易记录基数误把折算标的币数量当保证金:写入清洗+列表展示自动纠偏。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+45
-10
@@ -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"))
|
mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice"))
|
||||||
out = {}
|
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:
|
if initial is not None and initial > 0:
|
||||||
out["initial_margin"] = round(initial, 2)
|
out["initial_margin"] = round(initial, 2)
|
||||||
if notional is not None and notional > 0:
|
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):
|
def margin_capital_for_trade_record(order_row):
|
||||||
"""trade_records.基数:优先交易所持仓保证金快照,旧数据无快照时回退计划保证金."""
|
"""trade_records.基数:优先交易所持仓保证金快照,旧数据无快照时回退计划保证金."""
|
||||||
ex = _order_row_exchange_margin_usdt(order_row)
|
from lib.trade.trade_margin_record_lib import resolve_trade_record_margin_usdt
|
||||||
if ex is not None:
|
|
||||||
return round(ex, 2)
|
|
||||||
if not order_row:
|
if not order_row:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
v = order_row["margin_capital"]
|
plan = order_row["margin_capital"]
|
||||||
except (TypeError, KeyError, IndexError):
|
except (TypeError, KeyError, IndexError):
|
||||||
return None
|
plan = None
|
||||||
if v is None:
|
base = None
|
||||||
return None
|
notional = None
|
||||||
|
lev = None
|
||||||
|
trigger = None
|
||||||
try:
|
try:
|
||||||
return float(v)
|
keys = order_row.keys()
|
||||||
except (TypeError, ValueError):
|
except Exception:
|
||||||
return None
|
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):
|
def try_persist_exchange_margin_for_order(conn, order_id, exchange_symbol, direction, order_leverage=None, max_attempts=6, sleep_s=0.45):
|
||||||
|
|||||||
@@ -10,7 +10,22 @@ def enrich_trade_price_displays(
|
|||||||
format_price_fn: Optional[Callable[[Any, Any], str]] = None,
|
format_price_fn: Optional[Callable[[Any, Any], str]] = None,
|
||||||
) -> dict[str, Any]:
|
) -> 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
|
return item
|
||||||
sym = item.get("symbol")
|
sym = item.get("symbol")
|
||||||
stop_show = item.get("display_open_stop_loss")
|
stop_show = item.get("display_open_stop_loss")
|
||||||
@@ -59,7 +74,6 @@ def list_trade_records_page(
|
|||||||
page = pages
|
page = pages
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
items = records[offset : offset + 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 {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user