Use trading account and USDC/USDT market price for sim convert.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-14 18:52:38 +08:00
parent 9d1495658c
commit ad992ee262
7 changed files with 181 additions and 45 deletions
+8 -8
View File
@@ -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 };
+43
View File
@@ -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 = []
+16 -20
View File
@@ -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)
+68
View File
@@ -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")
+15 -6
View File
@@ -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()})
+5 -4
View File
@@ -43,9 +43,9 @@
<button type="button" class="btn" onclick="simTransfer()">划转</button>
</div>
<h3>USDT ↔ USDC (1:1)</h3>
<h3>USDT ↔ USDC(交易账户 · USDC/USDT 市价)</h3>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;margin-bottom:1rem">
<select id="sim-conv-account"><option value="funding">资金账户</option><option value="trading">交易账户</option></select>
<select id="sim-conv-account"><option value="trading">交易账户</option><option value="funding">资金账户</option></select>
<select id="sim-conv-from"><option value="USDT">USDT</option><option value="USDC">USDC</option></select>
<span></span>
<select id="sim-conv-to"><option value="USDC">USDC</option><option value="USDT">USDT</option></select>
@@ -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") {
+26 -7
View File
@@ -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),