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 @@ -