模拟盘对齐币本位:钱包支持 ETH/BTC,现货桥与期权权利金走本地撮合。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+87
-3
@@ -64,6 +64,19 @@ def _contract_size(exchange: Any, symbol: str) -> float:
|
||||
return 1.0
|
||||
|
||||
|
||||
def _premium_ccy_for_inst(inst_id: str) -> str:
|
||||
try:
|
||||
from lib.options.options_margin_mode_lib import (
|
||||
margin_mode_from_inst_id,
|
||||
premium_ccy_for_mode,
|
||||
)
|
||||
|
||||
underly = (str(inst_id or "").split("-")[0] or "ETH").upper()
|
||||
return premium_ccy_for_mode(margin_mode_from_inst_id(inst_id), underly)
|
||||
except Exception:
|
||||
return "USDC"
|
||||
|
||||
|
||||
class SimBroker:
|
||||
def __init__(self, get_db: Callable) -> None:
|
||||
self.get_db = get_db
|
||||
@@ -377,9 +390,10 @@ class SimBroker:
|
||||
qty = n * ct_mult
|
||||
pr = option_fill(action="open", bid=bid, ask=ask, qty=qty, fee_rate=fr)
|
||||
cost = pr.notional + pr.fee
|
||||
prem_ccy = _premium_ccy_for_inst(inst_id)
|
||||
try:
|
||||
self.wallets.debit_trading(
|
||||
"USDC",
|
||||
prem_ccy,
|
||||
cost,
|
||||
kind="option_open",
|
||||
note=f"buy {inst_id} x{n}@{pr.fill_px}",
|
||||
@@ -490,8 +504,9 @@ class SimBroker:
|
||||
conn.close()
|
||||
|
||||
if credit > 0:
|
||||
prem_ccy = _premium_ccy_for_inst(inst_id)
|
||||
self.wallets.credit_trading(
|
||||
"USDC",
|
||||
prem_ccy,
|
||||
credit,
|
||||
kind="option_close",
|
||||
note=f"sell {inst_id} x{close_n}@{pr.fill_px}",
|
||||
@@ -552,6 +567,67 @@ class SimBroker:
|
||||
)
|
||||
return result
|
||||
|
||||
def convert_usdt_coin(
|
||||
self,
|
||||
exchange: Any,
|
||||
*,
|
||||
underlying: str,
|
||||
direction: str,
|
||||
amount: float,
|
||||
fee_rate: float | None = None,
|
||||
account: str = "trading",
|
||||
) -> dict[str, Any]:
|
||||
"""模拟 ETH/BTC-USDT 现货市价兑换(交易账户)."""
|
||||
from lib.options.options_margin_mode_lib import spot_quote_inst_id
|
||||
from lib.sim.pricing_lib import spot_coin_usdt_fill
|
||||
|
||||
coin = (underlying or "ETH").strip().upper() or "ETH"
|
||||
if coin not in ("ETH", "BTC"):
|
||||
return {"ok": False, "msg": f"不支持标的 {coin}"}
|
||||
# spot_quote_inst_id → ETH-USDT;ccxt 常用 ETH/USDT
|
||||
inst = spot_quote_inst_id(coin)
|
||||
symbol = inst.replace("-", "/") if inst else f"{coin}/USDT"
|
||||
fr = sim_fee_rate(fee_rate)
|
||||
bid, ask = _ticker_bid_ask(exchange, symbol)
|
||||
fill = spot_coin_usdt_fill(
|
||||
direction=direction,
|
||||
amount=float(amount),
|
||||
bid=bid,
|
||||
ask=ask,
|
||||
fee_rate=fr,
|
||||
coin=coin,
|
||||
)
|
||||
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"{symbol} mkt {fill.fill_px:.4f} (bid {bid:.4f}/ask {ask:.4f})",
|
||||
)
|
||||
if not result.get("ok"):
|
||||
return result
|
||||
result.update(
|
||||
{
|
||||
"direction": fill.direction,
|
||||
"underlying": coin,
|
||||
"bid": bid,
|
||||
"ask": ask,
|
||||
"base_px": fill.base_px,
|
||||
"fill_px": fill.fill_px,
|
||||
"fee_rate": fr,
|
||||
"symbol": symbol,
|
||||
"inst_id": inst,
|
||||
"coin_bought": fill.to_amount if fill.direction == "usdt_to_coin" else None,
|
||||
"coin_sold": fill.from_amount if fill.direction == "coin_to_usdt" else None,
|
||||
"usdt_spent": fill.from_amount if fill.direction == "usdt_to_coin" else None,
|
||||
"usdt_recovered": fill.to_amount if fill.direction == "coin_to_usdt" else None,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def _index_px_for_option(
|
||||
self,
|
||||
exchange: Any,
|
||||
@@ -645,6 +721,14 @@ class SimBroker:
|
||||
else:
|
||||
intrinsic_u = 0.0
|
||||
|
||||
prem_ccy = _premium_ccy_for_inst(inst_id)
|
||||
# 币本位到期兑付用币数量: 实值/指数 × 张数 × 乘数
|
||||
if prem_ccy in ("ETH", "BTC") and float(spot) > 0:
|
||||
settle_recv = round(
|
||||
max(0.0, (intrinsic_u / float(spot)) * sheets * ct_mult),
|
||||
8,
|
||||
)
|
||||
|
||||
conn = self.get_db()
|
||||
try:
|
||||
conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,))
|
||||
@@ -681,7 +765,7 @@ class SimBroker:
|
||||
|
||||
if settle_recv > 1e-12:
|
||||
self.wallets.credit_trading(
|
||||
"USDC",
|
||||
prem_ccy,
|
||||
settle_recv,
|
||||
kind="option_expiry",
|
||||
note=f"expiry settle {inst_id} @{spot:g} recv={settle_recv}",
|
||||
|
||||
@@ -14,6 +14,19 @@ def _env_float(key: str, default: float) -> float:
|
||||
return float(default)
|
||||
|
||||
|
||||
def _ensure_sim_wallet_coin_columns(conn: sqlite3.Connection) -> None:
|
||||
"""旧库补齐币本位 ETH/BTC 列."""
|
||||
cols = {
|
||||
str(r[1])
|
||||
for r in conn.execute("PRAGMA table_info(sim_wallets)").fetchall()
|
||||
}
|
||||
for col in ("funding_eth", "trading_eth", "funding_btc", "trading_btc"):
|
||||
if col not in cols:
|
||||
conn.execute(
|
||||
f"ALTER TABLE sim_wallets ADD COLUMN {col} REAL NOT NULL DEFAULT 0"
|
||||
)
|
||||
|
||||
|
||||
def init_sim_tables(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -23,10 +36,15 @@ def init_sim_tables(conn: sqlite3.Connection) -> None:
|
||||
trading_usdt REAL NOT NULL DEFAULT 0,
|
||||
funding_usdc REAL NOT NULL DEFAULT 0,
|
||||
trading_usdc REAL NOT NULL DEFAULT 0,
|
||||
funding_eth REAL NOT NULL DEFAULT 0,
|
||||
trading_eth REAL NOT NULL DEFAULT 0,
|
||||
funding_btc REAL NOT NULL DEFAULT 0,
|
||||
trading_btc REAL NOT NULL DEFAULT 0,
|
||||
updated_at TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
_ensure_sim_wallet_coin_columns(conn)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS sim_perp_positions (
|
||||
|
||||
+144
-8
@@ -192,10 +192,10 @@ def _patch_okx_options_lib(app_module: Any) -> None:
|
||||
if _GET_DB is not None and is_sim_mode(_GET_DB):
|
||||
w = broker().balances_header()
|
||||
return (
|
||||
round(float(w["trading_usdc"]), 2),
|
||||
round(float(w["funding_usdc"]), 2),
|
||||
round(float(w["funding_usdt"]), 2),
|
||||
round(float(w["trading_usdt"]), 2),
|
||||
round(float(w.get("trading_usdc") or 0), 2),
|
||||
round(float(w.get("funding_usdc") or 0), 2),
|
||||
round(float(w.get("funding_usdt") or 0), 2),
|
||||
round(float(w.get("trading_usdt") or 0), 2),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -207,20 +207,32 @@ def _patch_okx_options_lib(app_module: Any) -> None:
|
||||
try:
|
||||
if _GET_DB is not None and is_sim_mode(_GET_DB):
|
||||
w = broker().balances_header()
|
||||
fu = float(w["funding_usdt"])
|
||||
fc = float(w["funding_usdc"])
|
||||
tu = float(w["trading_usdt"])
|
||||
tc = float(w["trading_usdc"])
|
||||
fu = float(w.get("funding_usdt") or 0)
|
||||
fc = float(w.get("funding_usdc") or 0)
|
||||
tu = float(w.get("trading_usdt") or 0)
|
||||
tc = float(w.get("trading_usdc") or 0)
|
||||
fe = float(w.get("funding_eth") or 0)
|
||||
te = float(w.get("trading_eth") or 0)
|
||||
fb = float(w.get("funding_btc") or 0)
|
||||
tb = float(w.get("trading_btc") or 0)
|
||||
return {
|
||||
"scope": "main",
|
||||
"funding_usdt": fu,
|
||||
"funding_usdc": fc,
|
||||
"trading_usdt": tu,
|
||||
"trading_usdc": tc,
|
||||
"funding_eth": fe,
|
||||
"trading_eth": te,
|
||||
"funding_btc": fb,
|
||||
"trading_btc": tb,
|
||||
"funding_usdt_avail": fu,
|
||||
"funding_usdc_avail": fc,
|
||||
"trading_usdt_avail": tu,
|
||||
"trading_usdc_avail": tc,
|
||||
"funding_eth_avail": fe,
|
||||
"trading_eth_avail": te,
|
||||
"funding_btc_avail": fb,
|
||||
"trading_btc_avail": tb,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
@@ -355,6 +367,130 @@ def _patch_okx_options_lib(app_module: Any) -> None:
|
||||
opt_lib.fetch_option_positions = fetch_option_positions
|
||||
opt_lib._sim_hooks_applied = True
|
||||
|
||||
_patch_spot_bridge_lib()
|
||||
|
||||
|
||||
def _patch_spot_bridge_lib() -> None:
|
||||
"""币本位现货桥:模拟盘走本地 USDT↔ETH/BTC,勿打实盘 private_post_trade_order."""
|
||||
import lib.options.options_spot_bridge_lib as bridge_lib
|
||||
|
||||
if getattr(bridge_lib, "_sim_hooks_applied", False):
|
||||
return
|
||||
|
||||
_orig_buy = bridge_lib.spot_market_buy_coin_with_usdt
|
||||
_orig_sell = bridge_lib.spot_market_sell_coin_to_usdt
|
||||
_orig_avail = bridge_lib.fetch_trading_coin_available
|
||||
|
||||
def fetch_trading_coin_available(ex, ccy: str):
|
||||
try:
|
||||
if _GET_DB is not None and is_sim_mode(_GET_DB):
|
||||
ccy_u = (ccy or "").strip().upper()
|
||||
if ccy_u not in ("ETH", "BTC"):
|
||||
return None
|
||||
w = broker().balances_header()
|
||||
return float(w.get(f"trading_{ccy_u.lower()}") or 0)
|
||||
except Exception:
|
||||
pass
|
||||
return _orig_avail(ex, ccy)
|
||||
|
||||
def spot_market_buy_coin_with_usdt(ex, *, underlying: str, usdt_amount: float):
|
||||
try:
|
||||
if _GET_DB is not None and is_sim_mode(_GET_DB):
|
||||
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_coin(
|
||||
pub,
|
||||
underlying=underlying,
|
||||
direction="usdt_to_coin",
|
||||
amount=float(usdt_amount),
|
||||
account="trading",
|
||||
)
|
||||
if not result.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": result.get("detail") or result.get("msg") or "买币失败",
|
||||
}
|
||||
try:
|
||||
from lib.instance.instance_live_push_lib import notify_instance_balance_changed
|
||||
|
||||
notify_instance_balance_changed()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"ok": True,
|
||||
"inst_id": result.get("inst_id") or "",
|
||||
"ord_id": f"sim-spot-buy-{result.get('fill_px') or 0}",
|
||||
"sim": True,
|
||||
"coin_bought": result.get("coin_bought"),
|
||||
"data": {"sCode": "0", "sMsg": "sim filled"},
|
||||
**{k: result[k] for k in ("fill_px", "usdt_spent", "underlying") if k in result},
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e)}
|
||||
return _orig_buy(ex, underlying=underlying, usdt_amount=usdt_amount)
|
||||
|
||||
def spot_market_sell_coin_to_usdt(ex, *, underlying: str, coin_amount: float | None = None):
|
||||
try:
|
||||
if _GET_DB is not None and is_sim_mode(_GET_DB):
|
||||
coin = (underlying or "ETH").strip().upper() or "ETH"
|
||||
amt = coin_amount
|
||||
if amt is None or float(amt) <= 0:
|
||||
amt = fetch_trading_coin_available(ex, coin)
|
||||
if amt is None or float(amt) <= 0:
|
||||
return {"ok": False, "msg": f"交易账户无可用 {coin}"}
|
||||
sell_sz = float(amt)
|
||||
if sell_sz > 1e-8:
|
||||
sell_sz = max(0.0, sell_sz * 0.999)
|
||||
if sell_sz <= 0:
|
||||
return {"ok": False, "msg": f"{coin} 可卖数量过小"}
|
||||
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_coin(
|
||||
pub,
|
||||
underlying=coin,
|
||||
direction="coin_to_usdt",
|
||||
amount=float(sell_sz),
|
||||
account="trading",
|
||||
)
|
||||
if not result.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": result.get("detail") or result.get("msg") or "卖币失败",
|
||||
}
|
||||
try:
|
||||
from lib.instance.instance_live_push_lib import notify_instance_balance_changed
|
||||
|
||||
notify_instance_balance_changed()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"ok": True,
|
||||
"inst_id": result.get("inst_id") or "",
|
||||
"ord_id": f"sim-spot-sell-{result.get('fill_px') or 0}",
|
||||
"coin_sold": float(sell_sz),
|
||||
"sim": True,
|
||||
"usdt_recovered": result.get("usdt_recovered"),
|
||||
"data": {"sCode": "0", "sMsg": "sim filled"},
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e)}
|
||||
return _orig_sell(ex, underlying=underlying, coin_amount=coin_amount)
|
||||
|
||||
bridge_lib.fetch_trading_coin_available = fetch_trading_coin_available
|
||||
bridge_lib.spot_market_buy_coin_with_usdt = spot_market_buy_coin_with_usdt
|
||||
bridge_lib.spot_market_sell_coin_to_usdt = spot_market_sell_coin_to_usdt
|
||||
bridge_lib._sim_hooks_applied = True
|
||||
|
||||
|
||||
def wrap_option_place_fns(get_db: Callable, live_place_limit, live_place_market):
|
||||
"""返回按模式分流的 place_option_limit/market."""
|
||||
|
||||
@@ -145,3 +145,58 @@ def spot_usdc_usdt_fill(
|
||||
fee=fee,
|
||||
)
|
||||
raise ValueError("direction 须为 usdt_to_usdc 或 usdc_to_usdt")
|
||||
|
||||
|
||||
def spot_coin_usdt_fill(
|
||||
*,
|
||||
direction: str,
|
||||
amount: float,
|
||||
bid: float,
|
||||
ask: float,
|
||||
fee_rate: float,
|
||||
coin: str = "ETH",
|
||||
) -> SpotConvertResult:
|
||||
"""
|
||||
对齐实盘 ETH-USDT / BTC-USDT 现货市价:
|
||||
- usdt_to_coin: 用 USDT 买币, 吃卖一 ×(1+f); amount=USDT
|
||||
- coin_to_usdt: 卖币换 USDT, 吃买一 ×(1-f); amount=币数量
|
||||
"""
|
||||
f = float(fee_rate)
|
||||
amt = float(amount)
|
||||
ccy = (coin or "ETH").strip().upper() or "ETH"
|
||||
d = (direction or "").strip().lower()
|
||||
if d in ("usdt_to_coin", "usdt_to_eth", "usdt_to_btc"):
|
||||
base = float(ask)
|
||||
fill = base * (1.0 + f)
|
||||
if fill <= 0:
|
||||
raise ValueError("无效卖一价")
|
||||
to_amt = amt / fill
|
||||
fee = amt * f
|
||||
return SpotConvertResult(
|
||||
direction="usdt_to_coin",
|
||||
from_ccy="USDT",
|
||||
to_ccy=ccy,
|
||||
from_amount=amt,
|
||||
to_amount=to_amt,
|
||||
base_px=base,
|
||||
fill_px=fill,
|
||||
fee=fee,
|
||||
)
|
||||
if d in ("coin_to_usdt", "eth_to_usdt", "btc_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="coin_to_usdt",
|
||||
from_ccy=ccy,
|
||||
to_ccy="USDT",
|
||||
from_amount=amt,
|
||||
to_amount=to_amt,
|
||||
base_px=base,
|
||||
fill_px=fill,
|
||||
fee=fee,
|
||||
)
|
||||
raise ValueError("direction 须为 usdt_to_coin 或 coin_to_usdt")
|
||||
|
||||
+59
-26
@@ -1,4 +1,4 @@
|
||||
"""模拟资金钱包: funding/trading × USDT/USDC."""
|
||||
"""模拟资金钱包: funding/trading × USDT/USDC/ETH/BTC."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,6 +11,10 @@ WALLET_KEYS = (
|
||||
"trading_usdt",
|
||||
"funding_usdc",
|
||||
"trading_usdc",
|
||||
"funding_eth",
|
||||
"trading_eth",
|
||||
"funding_btc",
|
||||
"trading_btc",
|
||||
)
|
||||
|
||||
_ACCT_MAP = {
|
||||
@@ -18,6 +22,10 @@ _ACCT_MAP = {
|
||||
("trading", "usdt"): "trading_usdt",
|
||||
("funding", "usdc"): "funding_usdc",
|
||||
("trading", "usdc"): "trading_usdc",
|
||||
("funding", "eth"): "funding_eth",
|
||||
("trading", "eth"): "trading_eth",
|
||||
("funding", "btc"): "funding_btc",
|
||||
("trading", "btc"): "trading_btc",
|
||||
}
|
||||
|
||||
|
||||
@@ -42,13 +50,26 @@ class SimWallets:
|
||||
def _now(self) -> str:
|
||||
return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def _row_to_snap(self, row: Any) -> dict[str, float]:
|
||||
if row is None:
|
||||
return {k: 0.0 for k in WALLET_KEYS}
|
||||
keys = set(row.keys()) if hasattr(row, "keys") else set()
|
||||
out: dict[str, float] = {}
|
||||
for k in WALLET_KEYS:
|
||||
if keys and k not in keys:
|
||||
out[k] = 0.0
|
||||
else:
|
||||
try:
|
||||
out[k] = float(row[k] or 0)
|
||||
except (KeyError, IndexError, TypeError, ValueError):
|
||||
out[k] = 0.0
|
||||
return out
|
||||
|
||||
def snapshot(self) -> dict[str, float]:
|
||||
conn = self.get_db()
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||||
if row is None:
|
||||
return {k: 0.0 for k in WALLET_KEYS}
|
||||
return {k: float(row[k] or 0) for k in WALLET_KEYS}
|
||||
return self._row_to_snap(row)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -56,6 +77,7 @@ class SimWallets:
|
||||
return self.snapshot()
|
||||
|
||||
def total_usdt_equiv(self, snap: dict[str, float] | None = None) -> float:
|
||||
"""稳定币合计(不含 ETH/BTC 折算)."""
|
||||
v = snap or self.view()
|
||||
return round(
|
||||
float(v.get("funding_usdt") or 0)
|
||||
@@ -71,23 +93,30 @@ class SimWallets:
|
||||
conn = self.get_db()
|
||||
try:
|
||||
now = self._now()
|
||||
full = {k: float(snap.get(k) or 0) for k in WALLET_KEYS}
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE sim_wallets SET
|
||||
funding_usdt=?, trading_usdt=?, funding_usdc=?, trading_usdc=?, updated_at=?
|
||||
funding_usdt=?, trading_usdt=?, funding_usdc=?, trading_usdc=?,
|
||||
funding_eth=?, trading_eth=?, funding_btc=?, trading_btc=?,
|
||||
updated_at=?
|
||||
WHERE id=1
|
||||
""",
|
||||
(
|
||||
float(snap["funding_usdt"]),
|
||||
float(snap["trading_usdt"]),
|
||||
float(snap["funding_usdc"]),
|
||||
float(snap["trading_usdc"]),
|
||||
full["funding_usdt"],
|
||||
full["trading_usdt"],
|
||||
full["funding_usdc"],
|
||||
full["trading_usdc"],
|
||||
full["funding_eth"],
|
||||
full["trading_eth"],
|
||||
full["funding_btc"],
|
||||
full["trading_btc"],
|
||||
now,
|
||||
),
|
||||
)
|
||||
if owns:
|
||||
conn.commit()
|
||||
return {k: float(snap[k]) for k in WALLET_KEYS}
|
||||
return full
|
||||
finally:
|
||||
if owns:
|
||||
conn.close()
|
||||
@@ -117,14 +146,14 @@ class SimWallets:
|
||||
raise ValueError("扣款金额须大于 0")
|
||||
key = _ACCT_MAP.get(("trading", (ccy or "").lower()))
|
||||
if not key:
|
||||
raise ValueError("币种须为 USDT 或 USDC")
|
||||
raise ValueError("币种须为 USDT/USDC/ETH/BTC")
|
||||
conn = self.get_db()
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||||
snap = {k: float(row[k] or 0) for k in WALLET_KEYS}
|
||||
snap = self._row_to_snap(row)
|
||||
bal = float(snap[key])
|
||||
if amt > bal + 1e-9:
|
||||
raise InsufficientFunds(f"交易账户 {ccy.upper()} 不足(可用 {bal:.4f})")
|
||||
raise InsufficientFunds(f"交易账户 {ccy.upper()} 不足(可用 {bal:.8f})")
|
||||
snap[key] = bal - amt
|
||||
self._write(snap, conn=conn)
|
||||
self._ledger(
|
||||
@@ -149,11 +178,11 @@ class SimWallets:
|
||||
return self.snapshot()
|
||||
key = _ACCT_MAP.get(("trading", (ccy or "").lower()))
|
||||
if not key:
|
||||
raise ValueError("币种须为 USDT 或 USDC")
|
||||
raise ValueError("币种须为 USDT/USDC/ETH/BTC")
|
||||
conn = self.get_db()
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||||
snap = {k: float(row[k] or 0) for k in WALLET_KEYS}
|
||||
snap = self._row_to_snap(row)
|
||||
snap[key] = float(snap[key]) + amt
|
||||
self._write(snap, conn=conn)
|
||||
self._ledger(
|
||||
@@ -191,14 +220,14 @@ class SimWallets:
|
||||
src_key = _ACCT_MAP.get((fa, ccy_l))
|
||||
dst_key = _ACCT_MAP.get((ta, ccy_l))
|
||||
if not src_key or not dst_key:
|
||||
return {"ok": False, "detail": "币种须为 USDT 或 USDC"}
|
||||
return {"ok": False, "detail": "币种须为 USDT/USDC/ETH/BTC"}
|
||||
conn = self.get_db()
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||||
snap = {k: float(row[k] or 0) for k in WALLET_KEYS}
|
||||
snap = self._row_to_snap(row)
|
||||
src_bal = float(snap[src_key])
|
||||
if amt > src_bal + 1e-9:
|
||||
return {"ok": False, "detail": f"余额不足(可用 {src_bal:.4f})"}
|
||||
return {"ok": False, "detail": f"余额不足(可用 {src_bal:.8f})"}
|
||||
snap[src_key] = src_bal - amt
|
||||
snap[dst_key] = float(snap[dst_key]) + amt
|
||||
self._write(snap, conn=conn)
|
||||
@@ -246,7 +275,7 @@ class SimWallets:
|
||||
fee: float | None = None,
|
||||
note: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""USDT↔USDC 兑换. 默认交易账户; to_amount 未给时按 rate(USDT/USDC) 换算, 再否则 1:1."""
|
||||
"""USDT↔USDC / USDT↔ETH / USDT↔BTC 兑换. to_amount 未给时按 rate(USDT per coin) 换算."""
|
||||
amt = float(amount)
|
||||
if amt <= 0:
|
||||
return {"ok": False, "detail": "数量须大于 0"}
|
||||
@@ -255,13 +284,14 @@ class SimWallets:
|
||||
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"}
|
||||
pair = {fa, ta}
|
||||
if pair not in ({"usdt", "usdc"}, {"usdt", "eth"}, {"usdt", "btc"}):
|
||||
return {"ok": False, "detail": "仅支持 USDT↔USDC/ETH/BTC"}
|
||||
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
|
||||
# rate = USDT per 1 coin(USDC/ETH/BTC)
|
||||
got = (amt / r) if fa == "usdt" else (amt * r)
|
||||
else:
|
||||
got = amt
|
||||
@@ -272,10 +302,10 @@ class SimWallets:
|
||||
conn = self.get_db()
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||||
snap = {k: float(row[k] or 0) for k in WALLET_KEYS}
|
||||
snap = self._row_to_snap(row)
|
||||
src = float(snap[src_key])
|
||||
if amt > src + 1e-9:
|
||||
return {"ok": False, "detail": f"{acct} {fa.upper()} 不足(可用 {src:.4f})"}
|
||||
return {"ok": False, "detail": f"{acct} {fa.upper()} 不足(可用 {src:.8f})"}
|
||||
snap[src_key] = src - amt
|
||||
snap[dst_key] = float(snap[dst_key]) + got
|
||||
self._write(snap, conn=conn)
|
||||
@@ -341,12 +371,15 @@ class SimWallets:
|
||||
conn.execute("DELETE FROM sim_perp_positions")
|
||||
conn.execute("DELETE FROM sim_option_positions")
|
||||
conn.execute("DELETE FROM sim_option_orders")
|
||||
now = self._now()
|
||||
snap = {
|
||||
"funding_usdt": amt,
|
||||
"trading_usdt": 0.0,
|
||||
"funding_usdc": 0.0,
|
||||
"trading_usdc": 0.0,
|
||||
"funding_eth": 0.0,
|
||||
"trading_eth": 0.0,
|
||||
"funding_btc": 0.0,
|
||||
"trading_btc": 0.0,
|
||||
}
|
||||
self._write(snap, conn=conn)
|
||||
self._ledger(
|
||||
@@ -361,4 +394,4 @@ class SimWallets:
|
||||
conn.commit()
|
||||
return {"ok": True, "wallets": snap, "total_usdt_equiv": amt}
|
||||
finally:
|
||||
conn.close()
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user