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) {
|
async function resolveSwapMaxAmount(dir) {
|
||||||
const bal = await loadBalances(true, "main");
|
const bal = await loadBalances(true, "main");
|
||||||
const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT";
|
const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT";
|
||||||
// 币种兑换走资金账户现货;统一账户下 USDT 有时在交易户,市价单仍可能成交
|
// 现货 USDC-USDT 走交易账户(cash); 交易户不足时再看资金户
|
||||||
let amount = pickBalance(bal, "funding", ccy);
|
let amount = pickBalance(bal, "trading", ccy);
|
||||||
let source = "funding";
|
let source = "trading";
|
||||||
if (!amount && ccy === "USDT") {
|
if (!amount) {
|
||||||
const tradingAmt = pickBalance(bal, "trading", ccy);
|
const fundingAmt = pickBalance(bal, "funding", ccy);
|
||||||
if (tradingAmt) {
|
if (fundingAmt) {
|
||||||
amount = tradingAmt;
|
amount = fundingAmt;
|
||||||
source = "trading";
|
source = "funding";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { amount, bal, ccy, source };
|
return { amount, bal, ccy, source };
|
||||||
|
|||||||
@@ -509,6 +509,49 @@ class SimBroker:
|
|||||||
"info": {"sim": True, "fee": pr.fee, "fill": pr.to_dict()},
|
"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]]:
|
def option_positions_okx_rows(self) -> list[dict[str, Any]]:
|
||||||
"""对齐 OKX positions 行字段, 供 format_position_row 使用."""
|
"""对齐 OKX positions 行字段, 供 format_position_row 使用."""
|
||||||
rows = []
|
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):
|
def spot_market_swap_usdt_usdc(ex, *, direction: str = "usdt_to_usdc", amount: float = 0):
|
||||||
try:
|
try:
|
||||||
if _GET_DB is not None and is_sim_mode(_GET_DB):
|
if _GET_DB is not None and is_sim_mode(_GET_DB):
|
||||||
from lib.sim.wallets_lib import SimWallets
|
# 对齐实盘: 交易账户 + USDC/USDT 公开买卖一(含手续费滑点)
|
||||||
|
pub = ex
|
||||||
d = (direction or "usdt_to_usdc").strip().lower()
|
if pub is None and _APP_MODULE is not None:
|
||||||
if d == "usdc_to_usdt":
|
pub = getattr(_APP_MODULE, "exchange", None) or getattr(
|
||||||
from_ccy, to_ccy = "USDC", "USDT"
|
_APP_MODULE, "exchange_options", None
|
||||||
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",
|
|
||||||
)
|
)
|
||||||
|
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"):
|
if not result.get("ok"):
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
@@ -295,7 +289,9 @@ def _patch_okx_options_lib(app_module: Any) -> None:
|
|||||||
notify_instance_balance_changed()
|
notify_instance_balance_changed()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
except Exception as e:
|
||||||
return {"ok": False, "msg": str(e)}
|
return {"ok": False, "msg": str(e)}
|
||||||
return _orig_swap(ex, direction=direction, amount=amount)
|
return _orig_swap(ex, direction=direction, amount=amount)
|
||||||
|
|||||||
@@ -77,3 +77,71 @@ def option_fill(
|
|||||||
fee = notional * f
|
fee = notional * f
|
||||||
slip = abs(fill - base) * float(qty)
|
slip = abs(fill - base) * float(qty)
|
||||||
return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=slip, notional=notional)
|
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)
|
amount = float(body.get("amount") or 0)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return jsonify({"ok": False, "msg": "amount 无效"}), 400
|
return jsonify({"ok": False, "msg": "amount 无效"}), 400
|
||||||
result = SimWallets(get_db).convert(
|
from_ccy = str(body.get("from_ccy") or "USDT").strip().upper()
|
||||||
from_ccy=str(body.get("from_ccy") or ""),
|
to_ccy = str(body.get("to_ccy") or "USDC").strip().upper()
|
||||||
to_ccy=str(body.get("to_ccy") or ""),
|
if {from_ccy, to_ccy} != {"USDT", "USDC"}:
|
||||||
amount=amount,
|
return jsonify({"ok": False, "msg": "仅支持 USDT↔USDC"}), 400
|
||||||
account=str(body.get("account") or "funding"),
|
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"):
|
if not result.get("ok"):
|
||||||
return jsonify(result), 400
|
return jsonify(result), 400
|
||||||
return jsonify({**result, **_status_payload()})
|
return jsonify({**result, **_status_payload()})
|
||||||
|
|||||||
@@ -43,9 +43,9 @@
|
|||||||
<button type="button" class="btn" onclick="simTransfer()">划转</button>
|
<button type="button" class="btn" onclick="simTransfer()">划转</button>
|
||||||
</div>
|
</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">
|
<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>
|
<select id="sim-conv-from"><option value="USDT">USDT</option><option value="USDC">USDC</option></select>
|
||||||
<span>→</span>
|
<span>→</span>
|
||||||
<select id="sim-conv-to"><option value="USDC">USDC</option><option value="USDT">USDT</option></select>
|
<select id="sim-conv-to"><option value="USDC">USDC</option><option value="USDT">USDT</option></select>
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
};
|
};
|
||||||
window.simConvert = function () {
|
window.simConvert = function () {
|
||||||
var body = {
|
var body = {
|
||||||
account: ($("sim-conv-account") || {}).value || "funding",
|
account: ($("sim-conv-account") || {}).value || "trading",
|
||||||
from_ccy: ($("sim-conv-from") || {}).value || "USDT",
|
from_ccy: ($("sim-conv-from") || {}).value || "USDT",
|
||||||
to_ccy: ($("sim-conv-to") || {}).value || "USDC",
|
to_ccy: ($("sim-conv-to") || {}).value || "USDC",
|
||||||
amount: parseFloat(($("sim-conv-amt") || {}).value || "0")
|
amount: parseFloat(($("sim-conv-amt") || {}).value || "0")
|
||||||
@@ -155,7 +155,8 @@
|
|||||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||||
if (!d.ok) { msg(d.detail || d.msg || "兑换失败", false); return; }
|
if (!d.ok) { msg(d.detail || d.msg || "兑换失败", false); return; }
|
||||||
applyStatus(d);
|
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); });
|
}).catch(function (e) { msg(String(e), false); });
|
||||||
};
|
};
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
|
|||||||
+26
-7
@@ -240,18 +240,33 @@ class SimWallets:
|
|||||||
from_ccy: str,
|
from_ccy: str,
|
||||||
to_ccy: str,
|
to_ccy: str,
|
||||||
amount: float,
|
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]:
|
) -> dict[str, Any]:
|
||||||
|
"""USDT↔USDC 兑换. 默认交易账户; to_amount 未给时按 rate(USDT/USDC) 换算, 再否则 1:1."""
|
||||||
amt = float(amount)
|
amt = float(amount)
|
||||||
if amt <= 0:
|
if amt <= 0:
|
||||||
return {"ok": False, "detail": "数量须大于 0"}
|
return {"ok": False, "detail": "数量须大于 0"}
|
||||||
fa = (from_ccy or "").strip().lower()
|
fa = (from_ccy or "").strip().lower()
|
||||||
ta = (to_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"):
|
if acct not in ("funding", "trading"):
|
||||||
return {"ok": False, "detail": "account 须为 funding / trading"}
|
return {"ok": False, "detail": "account 须为 funding / trading"}
|
||||||
if {fa, ta} != {"usdt", "usdc"}:
|
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)]
|
src_key = _ACCT_MAP[(acct, fa)]
|
||||||
dst_key = _ACCT_MAP[(acct, ta)]
|
dst_key = _ACCT_MAP[(acct, ta)]
|
||||||
conn = self.get_db()
|
conn = self.get_db()
|
||||||
@@ -262,8 +277,9 @@ class SimWallets:
|
|||||||
if amt > src + 1e-9:
|
if amt > src + 1e-9:
|
||||||
return {"ok": False, "detail": f"{acct} {fa.upper()} 不足(可用 {src:.4f})"}
|
return {"ok": False, "detail": f"{acct} {fa.upper()} 不足(可用 {src:.4f})"}
|
||||||
snap[src_key] = src - amt
|
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)
|
self._write(snap, conn=conn)
|
||||||
|
note_s = note or f"to {ta} @{rate if rate is not None else '1:1'}"
|
||||||
self._ledger(
|
self._ledger(
|
||||||
conn,
|
conn,
|
||||||
kind="convert",
|
kind="convert",
|
||||||
@@ -271,25 +287,28 @@ class SimWallets:
|
|||||||
ccy=fa,
|
ccy=fa,
|
||||||
account=acct,
|
account=acct,
|
||||||
balance_after=snap[src_key],
|
balance_after=snap[src_key],
|
||||||
note=f"to {ta}",
|
note=note_s,
|
||||||
)
|
)
|
||||||
self._ledger(
|
self._ledger(
|
||||||
conn,
|
conn,
|
||||||
kind="convert",
|
kind="convert",
|
||||||
amount=amt,
|
amount=got,
|
||||||
ccy=ta,
|
ccy=ta,
|
||||||
account=acct,
|
account=acct,
|
||||||
balance_after=snap[dst_key],
|
balance_after=snap[dst_key],
|
||||||
note=f"from {fa}",
|
note=f"from {fa}",
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
eff_rate = (amt / got) if fa == "usdt" and got > 0 else ((got / amt) if amt > 0 else None)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"detail": "converted",
|
"detail": "converted",
|
||||||
"from_ccy": fa.upper(),
|
"from_ccy": fa.upper(),
|
||||||
"to_ccy": ta.upper(),
|
"to_ccy": ta.upper(),
|
||||||
"amount": amt,
|
"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,
|
"account": acct,
|
||||||
"wallets": {k: float(snap[k]) for k in WALLET_KEYS},
|
"wallets": {k: float(snap[k]) for k in WALLET_KEYS},
|
||||||
"total_usdt_equiv": self.total_usdt_equiv(snap),
|
"total_usdt_equiv": self.total_usdt_equiv(snap),
|
||||||
|
|||||||
Reference in New Issue
Block a user