Files
eth_hedge_sim/backend/app/sim/funds_wallets.py
T

225 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""SIM 资金钱包:资金账户 / 交易账户 × USDT|USDC(对齐 OKX,无期权分账户)。"""
from __future__ import annotations
import time
from typing import Any
from ..models.db import Database, get_db
# DB 列名(历史兼容:USDC 存在 options_*_usdc 列,语义为资金/交易账户 USDC)
WALLET_KEYS = (
"funding_usdt",
"trading_usdt",
"options_funding_usdc", # = funding_usdc
"options_trading_usdc", # = trading_usdc
"options_funding_usdt", # 废弃,恒为 0
"options_trading_usdt", # 废弃,恒为 0
)
_ACCT_MAP = {
("funding", "usdt"): "funding_usdt",
("trading", "usdt"): "trading_usdt",
("funding", "usdc"): "options_funding_usdc",
("trading", "usdc"): "options_trading_usdc",
}
def _now_ms() -> int:
return int(time.time() * 1000)
class SimFundsWallets:
def __init__(self, db: Database | None = None) -> None:
self.db = db or get_db()
def snapshot(self) -> dict[str, float]:
row = self.db.fetchone("SELECT * FROM funds_wallets WHERE id=1")
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}
def view(self) -> dict[str, float]:
"""对外口径:funding/trading × usdt/usdc。"""
s = self.snapshot()
return {
"funding_usdt": float(s["funding_usdt"]),
"trading_usdt": float(s["trading_usdt"]),
"funding_usdc": float(s["options_funding_usdc"]),
"trading_usdc": float(s["options_trading_usdc"]),
}
def total_usdt_equiv(self, snap: dict[str, float] | None = None) -> float:
"""USDC 按 1:1 计入总资金。"""
if snap is None:
v = self.view()
elif "funding_usdc" in snap:
v = snap
else:
v = {
"funding_usdt": float(snap.get("funding_usdt") or 0),
"trading_usdt": float(snap.get("trading_usdt") or 0),
"funding_usdc": float(snap.get("options_funding_usdc") or 0),
"trading_usdc": float(snap.get("options_trading_usdc") or 0),
}
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 reset_from_equity(self, equity: float) -> dict[str, float]:
"""重置:全部放入资金账户 USDT。"""
amt = max(0.0, float(equity))
now = _now_ms()
self.db.execute(
"""UPDATE funds_wallets SET
funding_usdt=?, trading_usdt=0, options_funding_usdc=0, options_trading_usdc=0,
options_funding_usdt=0, options_trading_usdt=0, updated_at_ms=?
WHERE id=1""",
(amt, now),
)
return self.snapshot()
def _set(self, **kwargs: float) -> dict[str, float]:
snap = self.snapshot()
for k, v in kwargs.items():
if k in WALLET_KEYS:
snap[k] = float(v)
now = _now_ms()
self.db.execute(
"""UPDATE funds_wallets SET
funding_usdt=?, trading_usdt=?, options_funding_usdc=?, options_trading_usdc=?,
options_funding_usdt=0, options_trading_usdt=0, updated_at_ms=?
WHERE id=1""",
(
snap["funding_usdt"],
snap["trading_usdt"],
snap["options_funding_usdc"],
snap["options_trading_usdc"],
now,
),
)
return snap
def mirror_cash(self, amount: float, *, kind: str) -> None:
"""策略账本变动镜像到交易账户(永续 USDT / 期权 USDC)。"""
amt = float(amount)
if abs(amt) < 1e-12:
return
snap = self.snapshot()
k = (kind or "").lower()
if "option" in k:
key = "options_trading_usdc"
else:
key = "trading_usdt"
snap[key] = float(snap.get(key) or 0) + amt
self._set(**snap)
def sync_ledger_equity(self) -> float:
total = self.total_usdt_equiv()
now = _now_ms()
self.db.execute(
"UPDATE ledger_meta SET equity=?, available=?, updated_at_ms=? WHERE id=1",
(total, total, now),
)
return total
def convert(
self,
*,
direction: str,
amount: float,
rate: float = 1.0,
account: str = "funding",
) -> dict[str, Any]:
"""USDT↔USDC 兑换。默认资金账户;account=trading 时在交易账户内兑(对齐 OKX 现货)。"""
amt = float(amount)
if amt <= 0:
return {"ok": False, "detail": "数量须大于 0"}
r = float(rate) if rate and rate > 0 else 1.0
d = (direction or "").strip().lower()
acct = (account or "funding").strip().lower()
if acct not in ("funding", "trading"):
return {"ok": False, "detail": "account 须为 funding / trading"}
snap = self.snapshot()
if acct == "funding":
usdt_key, usdc_key = "funding_usdt", "options_funding_usdc"
label = "资金账户"
else:
usdt_key, usdc_key = "trading_usdt", "options_trading_usdc"
label = "交易账户"
if d == "usdt_to_usdc":
src = float(snap[usdt_key])
if amt > src + 1e-9:
return {"ok": False, "detail": f"{label} USDT 不足(可用 {src:.4f}"}
usdc = amt / r
snap[usdt_key] = src - amt
snap[usdc_key] = float(snap[usdc_key]) + usdc
elif d == "usdc_to_usdt":
src = float(snap[usdc_key])
if amt > src + 1e-9:
return {"ok": False, "detail": f"{label} USDC 不足(可用 {src:.4f}"}
usdt = amt * r
snap[usdc_key] = src - amt
snap[usdt_key] = float(snap[usdt_key]) + usdt
else:
return {"ok": False, "detail": "direction 须为 usdt_to_usdc 或 usdc_to_usdt"}
self._set(**snap)
total = self.sync_ledger_equity()
return {
"ok": True,
"detail": "converted",
"direction": d,
"amount": amt,
"rate": r,
"account": acct,
"wallets": self.view(),
"total_usdt_equiv": total,
}
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 "USDC").strip().lower()
fa = (from_account or "").strip().lower()
ta = (to_account or "").strip().lower()
allowed = {"funding", "trading"}
if fa not in allowed or ta not in allowed:
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"}
snap = self.snapshot()
src_bal = float(snap[src_key])
if amt > src_bal + 1e-9:
return {"ok": False, "detail": f"余额不足(可用 {src_bal:.4f}"}
snap[src_key] = src_bal - amt
snap[dst_key] = float(snap[dst_key]) + amt
self._set(**snap)
total = self.sync_ledger_equity()
return {
"ok": True,
"detail": "transferred",
"ccy": ccy_l.upper(),
"amount": amt,
"from": fa,
"to": ta,
"wallets": self.view(),
"total_usdt_equiv": total,
}