6f721d1e6d
Co-authored-by: Cursor <cursoragent@cursor.com>
397 lines
13 KiB
Python
397 lines
13 KiB
Python
"""模拟资金钱包: funding/trading × USDT/USDC/ETH/BTC."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Any, Callable
|
||
|
||
|
||
WALLET_KEYS = (
|
||
"funding_usdt",
|
||
"trading_usdt",
|
||
"funding_usdc",
|
||
"trading_usdc",
|
||
"funding_eth",
|
||
"trading_eth",
|
||
"funding_btc",
|
||
"trading_btc",
|
||
)
|
||
|
||
_ACCT_MAP = {
|
||
("funding", "usdt"): "funding_usdt",
|
||
("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",
|
||
}
|
||
|
||
|
||
def normalize_sim_account(account: str | None) -> str:
|
||
"""统一账户别名: swap/unified → trading; spot → funding."""
|
||
a = (account or "").strip().lower()
|
||
if a in ("trading", "swap", "unified", "18"):
|
||
return "trading"
|
||
if a in ("funding", "spot", "6"):
|
||
return "funding"
|
||
return a
|
||
|
||
|
||
class InsufficientFunds(RuntimeError):
|
||
pass
|
||
|
||
|
||
class SimWallets:
|
||
def __init__(self, get_db: Callable) -> None:
|
||
self.get_db = get_db
|
||
|
||
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()
|
||
return self._row_to_snap(row)
|
||
finally:
|
||
conn.close()
|
||
|
||
def view(self) -> dict[str, float]:
|
||
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)
|
||
+ float(v.get("trading_usdt") or 0)
|
||
+ float(v.get("funding_usdc") or 0)
|
||
+ float(v.get("trading_usdc") or 0),
|
||
8,
|
||
)
|
||
|
||
def _write(self, snap: dict[str, float], conn=None) -> dict[str, float]:
|
||
owns = conn is None
|
||
if owns:
|
||
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=?,
|
||
funding_eth=?, trading_eth=?, funding_btc=?, trading_btc=?,
|
||
updated_at=?
|
||
WHERE id=1
|
||
""",
|
||
(
|
||
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 full
|
||
finally:
|
||
if owns:
|
||
conn.close()
|
||
|
||
def _ledger(
|
||
self,
|
||
conn,
|
||
*,
|
||
kind: str,
|
||
amount: float,
|
||
ccy: str,
|
||
account: str,
|
||
balance_after: float,
|
||
note: str = "",
|
||
) -> None:
|
||
conn.execute(
|
||
"""
|
||
INSERT INTO sim_ledger_entries(kind, amount, ccy, account, balance_after, note, ts)
|
||
VALUES (?,?,?,?,?,?,?)
|
||
""",
|
||
(kind, float(amount), ccy.upper(), account, float(balance_after), note or "", self._now()),
|
||
)
|
||
|
||
def debit_trading(self, ccy: str, amount: float, *, kind: str = "debit", note: str = "") -> dict[str, float]:
|
||
amt = float(amount)
|
||
if amt <= 0:
|
||
raise ValueError("扣款金额须大于 0")
|
||
key = _ACCT_MAP.get(("trading", (ccy or "").lower()))
|
||
if not key:
|
||
raise ValueError("币种须为 USDT/USDC/ETH/BTC")
|
||
conn = self.get_db()
|
||
try:
|
||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||
snap = self._row_to_snap(row)
|
||
bal = float(snap[key])
|
||
if amt > bal + 1e-9:
|
||
raise InsufficientFunds(f"交易账户 {ccy.upper()} 不足(可用 {bal:.8f})")
|
||
snap[key] = bal - amt
|
||
self._write(snap, conn=conn)
|
||
self._ledger(
|
||
conn,
|
||
kind=kind,
|
||
amount=-amt,
|
||
ccy=ccy,
|
||
account="trading",
|
||
balance_after=snap[key],
|
||
note=note,
|
||
)
|
||
conn.commit()
|
||
return snap
|
||
finally:
|
||
conn.close()
|
||
|
||
def credit_trading(self, ccy: str, amount: float, *, kind: str = "credit", note: str = "") -> dict[str, float]:
|
||
amt = float(amount)
|
||
if amt < 0:
|
||
raise ValueError("入账金额不能为负")
|
||
if amt < 1e-12:
|
||
return self.snapshot()
|
||
key = _ACCT_MAP.get(("trading", (ccy or "").lower()))
|
||
if not key:
|
||
raise ValueError("币种须为 USDT/USDC/ETH/BTC")
|
||
conn = self.get_db()
|
||
try:
|
||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||
snap = self._row_to_snap(row)
|
||
snap[key] = float(snap[key]) + amt
|
||
self._write(snap, conn=conn)
|
||
self._ledger(
|
||
conn,
|
||
kind=kind,
|
||
amount=amt,
|
||
ccy=ccy,
|
||
account="trading",
|
||
balance_after=snap[key],
|
||
note=note,
|
||
)
|
||
conn.commit()
|
||
return snap
|
||
finally:
|
||
conn.close()
|
||
|
||
def transfer(
|
||
self,
|
||
*,
|
||
ccy: str,
|
||
amount: float,
|
||
from_account: str,
|
||
to_account: str,
|
||
) -> dict[str, Any]:
|
||
amt = float(amount)
|
||
if amt <= 0:
|
||
return {"ok": False, "detail": "划转金额须大于 0"}
|
||
ccy_l = (ccy or "USDT").strip().lower()
|
||
fa = normalize_sim_account(from_account)
|
||
ta = normalize_sim_account(to_account)
|
||
if fa not in ("funding", "trading") or ta not in ("funding", "trading"):
|
||
return {"ok": False, "detail": "账户仅支持 funding / trading"}
|
||
if fa == ta:
|
||
return {"ok": False, "detail": "来源与目标账户不能相同"}
|
||
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/ETH/BTC"}
|
||
conn = self.get_db()
|
||
try:
|
||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||
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:.8f})"}
|
||
snap[src_key] = src_bal - amt
|
||
snap[dst_key] = float(snap[dst_key]) + amt
|
||
self._write(snap, conn=conn)
|
||
self._ledger(
|
||
conn,
|
||
kind="transfer",
|
||
amount=-amt,
|
||
ccy=ccy_l,
|
||
account=fa,
|
||
balance_after=snap[src_key],
|
||
note=f"to {ta}",
|
||
)
|
||
self._ledger(
|
||
conn,
|
||
kind="transfer",
|
||
amount=amt,
|
||
ccy=ccy_l,
|
||
account=ta,
|
||
balance_after=snap[dst_key],
|
||
note=f"from {fa}",
|
||
)
|
||
conn.commit()
|
||
return {
|
||
"ok": True,
|
||
"detail": "transferred",
|
||
"ccy": ccy_l.upper(),
|
||
"amount": amt,
|
||
"from": fa,
|
||
"to": ta,
|
||
"wallets": {k: float(snap[k]) for k in WALLET_KEYS},
|
||
"total_usdt_equiv": self.total_usdt_equiv(snap),
|
||
}
|
||
finally:
|
||
conn.close()
|
||
|
||
def convert(
|
||
self,
|
||
*,
|
||
from_ccy: str,
|
||
to_ccy: str,
|
||
amount: float,
|
||
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 / USDT↔ETH / USDT↔BTC 兑换. to_amount 未给时按 rate(USDT per coin) 换算."""
|
||
amt = float(amount)
|
||
if amt <= 0:
|
||
return {"ok": False, "detail": "数量须大于 0"}
|
||
fa = (from_ccy or "").strip().lower()
|
||
ta = (to_ccy or "").strip().lower()
|
||
acct = normalize_sim_account(account) or "trading"
|
||
if acct not in ("funding", "trading"):
|
||
return {"ok": False, "detail": "account 须为 funding / trading"}
|
||
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 coin(USDC/ETH/BTC)
|
||
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()
|
||
try:
|
||
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
|
||
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:.8f})"}
|
||
snap[src_key] = src - 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",
|
||
amount=-amt,
|
||
ccy=fa,
|
||
account=acct,
|
||
balance_after=snap[src_key],
|
||
note=note_s,
|
||
)
|
||
self._ledger(
|
||
conn,
|
||
kind="convert",
|
||
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,
|
||
"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),
|
||
}
|
||
finally:
|
||
conn.close()
|
||
|
||
def has_open_positions(self) -> bool:
|
||
conn = self.get_db()
|
||
try:
|
||
p = conn.execute(
|
||
"SELECT COUNT(*) AS n FROM sim_perp_positions WHERE contracts > 1e-12"
|
||
).fetchone()
|
||
o = conn.execute(
|
||
"SELECT COUNT(*) AS n FROM sim_option_positions WHERE sheets > 1e-12"
|
||
).fetchone()
|
||
pn = int(p["n"] if hasattr(p, "keys") else p[0])
|
||
on = int(o["n"] if hasattr(o, "keys") else o[0])
|
||
return pn > 0 or on > 0
|
||
finally:
|
||
conn.close()
|
||
|
||
def reset_equity(self, amount: float, *, force: bool = False) -> dict[str, Any]:
|
||
if self.has_open_positions() and not force:
|
||
return {"ok": False, "detail": "仍有模拟持仓,请先平仓或传 force=true"}
|
||
amt = max(0.0, float(amount))
|
||
conn = self.get_db()
|
||
try:
|
||
if force:
|
||
conn.execute("DELETE FROM sim_perp_positions")
|
||
conn.execute("DELETE FROM sim_option_positions")
|
||
conn.execute("DELETE FROM sim_option_orders")
|
||
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(
|
||
conn,
|
||
kind="reset",
|
||
amount=amt,
|
||
ccy="USDT",
|
||
account="funding",
|
||
balance_after=amt,
|
||
note="reset equity",
|
||
)
|
||
conn.commit()
|
||
return {"ok": True, "wallets": snap, "total_usdt_equiv": amt}
|
||
finally:
|
||
conn.close() |