Use trading account and USDC/USDT market price for sim convert.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()})
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user