From ad992ee262b40d4cc4fdaaa0536eb1c73b21546c Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 14 Aug 2026 18:52:38 +0800 Subject: [PATCH] Use trading account and USDC/USDT market price for sim convert. Co-authored-by: Cursor --- lib/common/static/options_settings.js | 16 +++--- lib/sim/broker_lib.py | 43 ++++++++++++++++ lib/sim/hooks.py | 36 ++++++-------- lib/sim/pricing_lib.py | 68 ++++++++++++++++++++++++++ lib/sim/register.py | 21 +++++--- lib/sim/templates/sim_funds_panel.html | 9 ++-- lib/sim/wallets_lib.py | 33 ++++++++++--- 7 files changed, 181 insertions(+), 45 deletions(-) diff --git a/lib/common/static/options_settings.js b/lib/common/static/options_settings.js index 6a0895a..4648b5b 100644 --- a/lib/common/static/options_settings.js +++ b/lib/common/static/options_settings.js @@ -101,14 +101,14 @@ async function resolveSwapMaxAmount(dir) { const bal = await loadBalances(true, "main"); const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT"; - // 币种兑换走资金账户现货;统一账户下 USDT 有时在交易户,市价单仍可能成交 - let amount = pickBalance(bal, "funding", ccy); - let source = "funding"; - if (!amount && ccy === "USDT") { - const tradingAmt = pickBalance(bal, "trading", ccy); - if (tradingAmt) { - amount = tradingAmt; - source = "trading"; + // 现货 USDC-USDT 走交易账户(cash); 交易户不足时再看资金户 + let amount = pickBalance(bal, "trading", ccy); + let source = "trading"; + if (!amount) { + const fundingAmt = pickBalance(bal, "funding", ccy); + if (fundingAmt) { + amount = fundingAmt; + source = "funding"; } } return { amount, bal, ccy, source }; diff --git a/lib/sim/broker_lib.py b/lib/sim/broker_lib.py index 6beb577..9e24afb 100644 --- a/lib/sim/broker_lib.py +++ b/lib/sim/broker_lib.py @@ -509,6 +509,49 @@ class SimBroker: "info": {"sim": True, "fee": pr.fee, "fill": pr.to_dict()}, } + def convert_usdt_usdc( + self, + exchange: Any, + *, + direction: str, + amount: float, + fee_rate: float | None = None, + account: str = "trading", + symbol: str = "USDC/USDT", + ) -> dict[str, Any]: + """模拟 USDC-USDT 现货市价兑换, 默认扣交易账户.""" + from lib.sim.pricing_lib import spot_usdc_usdt_fill + + fr = sim_fee_rate(fee_rate) + bid, ask = _ticker_bid_ask(exchange, symbol) + fill = spot_usdc_usdt_fill( + direction=direction, amount=float(amount), bid=bid, ask=ask, fee_rate=fr + ) + result = SimWallets(self.get_db).convert( + from_ccy=fill.from_ccy, + to_ccy=fill.to_ccy, + amount=fill.from_amount, + account=account or "trading", + to_amount=fill.to_amount, + rate=fill.fill_px, + fee=fill.fee, + note=f"USDC/USDT mkt {fill.fill_px:.6f} (bid {bid:.6f}/ask {ask:.6f})", + ) + if not result.get("ok"): + return result + result.update( + { + "direction": fill.direction, + "bid": bid, + "ask": ask, + "base_px": fill.base_px, + "fill_px": fill.fill_px, + "fee_rate": fr, + "symbol": symbol, + } + ) + return result + def option_positions_okx_rows(self) -> list[dict[str, Any]]: """对齐 OKX positions 行字段, 供 format_position_row 使用.""" rows = [] diff --git a/lib/sim/hooks.py b/lib/sim/hooks.py index f85e37c..df999e5 100644 --- a/lib/sim/hooks.py +++ b/lib/sim/hooks.py @@ -264,26 +264,20 @@ def _patch_okx_options_lib(app_module: Any) -> None: def spot_market_swap_usdt_usdc(ex, *, direction: str = "usdt_to_usdc", amount: float = 0): try: if _GET_DB is not None and is_sim_mode(_GET_DB): - from lib.sim.wallets_lib import SimWallets - - d = (direction or "usdt_to_usdc").strip().lower() - if d == "usdc_to_usdt": - from_ccy, to_ccy = "USDC", "USDT" - else: - from_ccy, to_ccy = "USDT", "USDC" - result = SimWallets(_GET_DB).convert( - from_ccy=from_ccy, - to_ccy=to_ccy, - amount=float(amount), - account="funding", - ) - if not result.get("ok"): - result = SimWallets(_GET_DB).convert( - from_ccy=from_ccy, - to_ccy=to_ccy, - amount=float(amount), - account="trading", + # 对齐实盘: 交易账户 + USDC/USDT 公开买卖一(含手续费滑点) + pub = ex + if pub is None and _APP_MODULE is not None: + pub = getattr(_APP_MODULE, "exchange", None) or getattr( + _APP_MODULE, "exchange_options", None ) + if pub is None: + return {"ok": False, "msg": "sim: 无公开行情 exchange"} + result = broker().convert_usdt_usdc( + pub, + direction=direction, + amount=float(amount), + account="trading", + ) if not result.get("ok"): return { "ok": False, @@ -295,7 +289,9 @@ def _patch_okx_options_lib(app_module: Any) -> None: notify_instance_balance_changed() except Exception: pass - return {"ok": True, "msg": "sim 兑换成功(1:1)", "sim": True, **result} + px = result.get("fill_px") + msg = f"sim 兑换成功 @ {px:.6f}" if px else "sim 兑换成功" + return {"ok": True, "msg": msg, "sim": True, **result} except Exception as e: return {"ok": False, "msg": str(e)} return _orig_swap(ex, direction=direction, amount=amount) diff --git a/lib/sim/pricing_lib.py b/lib/sim/pricing_lib.py index dfdc8a9..83d2939 100644 --- a/lib/sim/pricing_lib.py +++ b/lib/sim/pricing_lib.py @@ -77,3 +77,71 @@ def option_fill( fee = notional * f slip = abs(fill - base) * float(qty) return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=slip, notional=notional) + + +@dataclass(slots=True) +class SpotConvertResult: + """USDC/USDT 现货兑换: price = USDT per USDC.""" + + direction: str + from_ccy: str + to_ccy: str + from_amount: float + to_amount: float + base_px: float + fill_px: float + fee: float + + +def spot_usdc_usdt_fill( + *, + direction: str, + amount: float, + bid: float, + ask: float, + fee_rate: float, +) -> SpotConvertResult: + """ + 对齐实盘 USDC-USDT 现货市价: + - usdt_to_usdc: 用 USDT 买 USDC, 吃卖一 ×(1+f) + - usdc_to_usdt: 卖 USDC 换 USDT, 吃买一 ×(1-f) + amount 为付出币种数量. + """ + f = float(fee_rate) + amt = float(amount) + d = (direction or "").strip().lower() + if d == "usdt_to_usdc": + base = float(ask) + fill = base * (1.0 + f) + if fill <= 0: + raise ValueError("无效卖一价") + to_amt = amt / fill + fee = amt * f + return SpotConvertResult( + direction=d, + from_ccy="USDT", + to_ccy="USDC", + from_amount=amt, + to_amount=to_amt, + base_px=base, + fill_px=fill, + fee=fee, + ) + if d == "usdc_to_usdt": + base = float(bid) + fill = base * (1.0 - f) + if fill <= 0: + raise ValueError("无效买一价") + to_amt = amt * fill + fee = to_amt * f + return SpotConvertResult( + direction=d, + from_ccy="USDC", + to_ccy="USDT", + from_amount=amt, + to_amount=to_amt, + base_px=base, + fill_px=fill, + fee=fee, + ) + raise ValueError("direction 须为 usdt_to_usdc 或 usdc_to_usdt") diff --git a/lib/sim/register.py b/lib/sim/register.py index b64b3af..4e71898 100644 --- a/lib/sim/register.py +++ b/lib/sim/register.py @@ -140,12 +140,21 @@ def install_sim_trading(app: Flask, repo_root: str, app_module: Any = None) -> N amount = float(body.get("amount") or 0) except (TypeError, ValueError): return jsonify({"ok": False, "msg": "amount 无效"}), 400 - result = SimWallets(get_db).convert( - from_ccy=str(body.get("from_ccy") or ""), - to_ccy=str(body.get("to_ccy") or ""), - amount=amount, - account=str(body.get("account") or "funding"), - ) + from_ccy = str(body.get("from_ccy") or "USDT").strip().upper() + to_ccy = str(body.get("to_ccy") or "USDC").strip().upper() + if {from_ccy, to_ccy} != {"USDT", "USDC"}: + return jsonify({"ok": False, "msg": "仅支持 USDT↔USDC"}), 400 + direction = "usdt_to_usdc" if from_ccy == "USDT" else "usdc_to_usdt" + account = str(body.get("account") or "trading") + ex = getattr(app_module, "exchange", None) or getattr(app_module, "exchange_options", None) + if ex is None: + return jsonify({"ok": False, "msg": "无公开行情 exchange"}), 500 + try: + result = SimBroker(get_db).convert_usdt_usdc( + ex, direction=direction, amount=amount, account=account + ) + except Exception as e: + return jsonify({"ok": False, "msg": str(e)}), 400 if not result.get("ok"): return jsonify(result), 400 return jsonify({**result, **_status_payload()}) diff --git a/lib/sim/templates/sim_funds_panel.html b/lib/sim/templates/sim_funds_panel.html index 7d7cd38..02d39f7 100644 --- a/lib/sim/templates/sim_funds_panel.html +++ b/lib/sim/templates/sim_funds_panel.html @@ -43,9 +43,9 @@ -

USDT ↔ USDC (1:1)

+

USDT ↔ USDC(交易账户 · USDC/USDT 市价)

- + @@ -142,7 +142,7 @@ }; window.simConvert = function () { var body = { - account: ($("sim-conv-account") || {}).value || "funding", + account: ($("sim-conv-account") || {}).value || "trading", from_ccy: ($("sim-conv-from") || {}).value || "USDT", to_ccy: ($("sim-conv-to") || {}).value || "USDC", amount: parseFloat(($("sim-conv-amt") || {}).value || "0") @@ -155,7 +155,8 @@ }).then(function (r) { return r.json(); }).then(function (d) { if (!d.ok) { msg(d.detail || d.msg || "兑换失败", false); return; } applyStatus(d); - msg("兑换成功", true); + var px = d.fill_px != null ? (" @" + Number(d.fill_px).toFixed(6)) : ""; + msg("兑换成功" + px, true); }).catch(function (e) { msg(String(e), false); }); }; if (document.readyState === "loading") { diff --git a/lib/sim/wallets_lib.py b/lib/sim/wallets_lib.py index 94d0672..8c729f6 100644 --- a/lib/sim/wallets_lib.py +++ b/lib/sim/wallets_lib.py @@ -240,18 +240,33 @@ class SimWallets: from_ccy: str, to_ccy: str, amount: float, - account: str = "funding", + account: str = "trading", + to_amount: float | None = None, + rate: float | None = None, + fee: float | None = None, + note: str | None = None, ) -> dict[str, Any]: + """USDT↔USDC 兑换. 默认交易账户; to_amount 未给时按 rate(USDT/USDC) 换算, 再否则 1:1.""" amt = float(amount) if amt <= 0: return {"ok": False, "detail": "数量须大于 0"} fa = (from_ccy or "").strip().lower() ta = (to_ccy or "").strip().lower() - acct = (account or "funding").strip().lower() + acct = normalize_sim_account(account) or "trading" if acct not in ("funding", "trading"): return {"ok": False, "detail": "account 须为 funding / trading"} if {fa, ta} != {"usdt", "usdc"}: - return {"ok": False, "detail": "仅支持 USDT↔USDC 1:1"} + return {"ok": False, "detail": "仅支持 USDT↔USDC"} + if to_amount is not None: + got = float(to_amount) + elif rate is not None and float(rate) > 0: + r = float(rate) + # rate = USDT per 1 USDC + got = (amt / r) if fa == "usdt" else (amt * r) + else: + got = amt + if got <= 0: + return {"ok": False, "detail": "兑换所得须大于 0"} src_key = _ACCT_MAP[(acct, fa)] dst_key = _ACCT_MAP[(acct, ta)] conn = self.get_db() @@ -262,8 +277,9 @@ class SimWallets: if amt > src + 1e-9: return {"ok": False, "detail": f"{acct} {fa.upper()} 不足(可用 {src:.4f})"} snap[src_key] = src - amt - snap[dst_key] = float(snap[dst_key]) + amt + snap[dst_key] = float(snap[dst_key]) + got self._write(snap, conn=conn) + note_s = note or f"to {ta} @{rate if rate is not None else '1:1'}" self._ledger( conn, kind="convert", @@ -271,25 +287,28 @@ class SimWallets: ccy=fa, account=acct, balance_after=snap[src_key], - note=f"to {ta}", + note=note_s, ) self._ledger( conn, kind="convert", - amount=amt, + amount=got, ccy=ta, account=acct, balance_after=snap[dst_key], note=f"from {fa}", ) conn.commit() + eff_rate = (amt / got) if fa == "usdt" and got > 0 else ((got / amt) if amt > 0 else None) return { "ok": True, "detail": "converted", "from_ccy": fa.upper(), "to_ccy": ta.upper(), "amount": amt, - "rate": 1.0, + "to_amount": got, + "rate": float(rate) if rate is not None else eff_rate, + "fee": float(fee or 0), "account": acct, "wallets": {k: float(snap[k]) for k in WALLET_KEYS}, "total_usdt_equiv": self.total_usdt_equiv(snap),