模拟盘对齐币本位:钱包支持 ETH/BTC,现货桥与期权权利金走本地撮合。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,16 +20,10 @@ from lib.options.options_margin_mode_lib import (
|
||||
plan_coin_open_by_budget,
|
||||
premium_ccy_for_mode,
|
||||
)
|
||||
from lib.options import options_spot_bridge_lib as _spot_bridge
|
||||
from lib.options.options_spot_bridge_lib import (
|
||||
BRIDGE_BOUGHT,
|
||||
BRIDGE_HOLDING,
|
||||
bridge_blocks_new_open_msg,
|
||||
fetch_trading_coin_available,
|
||||
insert_bridge,
|
||||
rollback_bought_coin_to_usdt,
|
||||
sell_residual_after_option_flat,
|
||||
spot_market_buy_coin_with_usdt,
|
||||
update_bridge,
|
||||
)
|
||||
|
||||
|
||||
@@ -78,7 +72,7 @@ def open_coin_option_buy_full(
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
block = bridge_blocks_new_open_msg(conn)
|
||||
block = _spot_bridge.bridge_blocks_new_open_msg(conn)
|
||||
if block:
|
||||
return {"ok": False, "msg": block, "can_open": False}
|
||||
|
||||
@@ -149,8 +143,8 @@ def open_coin_option_buy_full(
|
||||
sheets = int(plan["sheets"])
|
||||
|
||||
# 1) 仅买「权利金×现货缓冲」所需 USDT,不全额兑换预算
|
||||
coin_before = fetch_trading_coin_available(ex, underlying) or 0.0
|
||||
buy = spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=buy_usdt)
|
||||
coin_before = _spot_bridge.fetch_trading_coin_available(ex, underlying) or 0.0
|
||||
buy = _spot_bridge.spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=buy_usdt)
|
||||
if not buy.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
@@ -158,7 +152,7 @@ def open_coin_option_buy_full(
|
||||
"budget": budget_info,
|
||||
"plan": plan,
|
||||
}
|
||||
bridge_id = insert_bridge(
|
||||
bridge_id = _spot_bridge.insert_bridge(
|
||||
conn,
|
||||
underlying=underlying,
|
||||
status=BRIDGE_BOUGHT,
|
||||
@@ -175,9 +169,9 @@ def open_coin_option_buy_full(
|
||||
invalidate_options_balance_cache()
|
||||
except Exception:
|
||||
pass
|
||||
coin_after = fetch_trading_coin_available(ex, underlying)
|
||||
coin_after = _spot_bridge.fetch_trading_coin_available(ex, underlying)
|
||||
if coin_after is None:
|
||||
rb = rollback_bought_coin_to_usdt(
|
||||
rb = _spot_bridge.rollback_bought_coin_to_usdt(
|
||||
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买币后读不到可用余额"
|
||||
)
|
||||
return {
|
||||
@@ -192,11 +186,11 @@ def open_coin_option_buy_full(
|
||||
# 落账延迟时退化为用当前可用,但仍写入上限提示
|
||||
coin_bought = float(coin_after)
|
||||
if coin_bought <= 0:
|
||||
rb = rollback_bought_coin_to_usdt(
|
||||
rb = _spot_bridge.rollback_bought_coin_to_usdt(
|
||||
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买入量无效"
|
||||
)
|
||||
return {"ok": False, "msg": "买币后可用增量无效", "rollback": rb, "budget": budget_info}
|
||||
update_bridge(conn, bridge_id, coin_bought=float(coin_bought))
|
||||
_spot_bridge.update_bridge(conn, bridge_id, coin_bought=float(coin_bought))
|
||||
|
||||
sizing = calc_sheets_from_coin_balance(
|
||||
quote_per_unit=float(ask),
|
||||
@@ -205,7 +199,7 @@ def open_coin_option_buy_full(
|
||||
coin_available=float(coin_bought),
|
||||
)
|
||||
if not sizing.get("ok"):
|
||||
rb = rollback_bought_coin_to_usdt(
|
||||
rb = _spot_bridge.rollback_bought_coin_to_usdt(
|
||||
conn,
|
||||
ex,
|
||||
bridge_id=bridge_id,
|
||||
@@ -219,7 +213,7 @@ def open_coin_option_buy_full(
|
||||
sheets = min(int(sizing["sheets"]), int(plan["sheets"]))
|
||||
capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=min_sz)
|
||||
if capped is None:
|
||||
rb = rollback_bought_coin_to_usdt(
|
||||
rb = _spot_bridge.rollback_bought_coin_to_usdt(
|
||||
conn, ex, bridge_id=bridge_id, underlying=underlying, reason=cap_msg or "深度不足"
|
||||
)
|
||||
return {"ok": False, "msg": cap_msg or "卖一深度不足", "rollback": rb}
|
||||
@@ -278,14 +272,14 @@ def open_coin_option_buy_full(
|
||||
"retried_half": True,
|
||||
}
|
||||
if not order.get("ok"):
|
||||
rb = rollback_bought_coin_to_usdt(
|
||||
rb = _spot_bridge.rollback_bought_coin_to_usdt(
|
||||
conn, ex, bridge_id=bridge_id, underlying=underlying, reason=order.get("msg") or "下单失败"
|
||||
)
|
||||
return {"ok": False, "msg": order.get("msg") or "期权下单失败", "order": order, "rollback": rb}
|
||||
|
||||
ord_id = str((order.get("data") or {}).get("ordId") or "").strip()
|
||||
if not ord_id:
|
||||
rb = rollback_bought_coin_to_usdt(
|
||||
rb = _spot_bridge.rollback_bought_coin_to_usdt(
|
||||
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="无订单号"
|
||||
)
|
||||
return {"ok": False, "msg": "下单成功但未返回订单号", "rollback": rb}
|
||||
@@ -305,7 +299,7 @@ def open_coin_option_buy_full(
|
||||
if not fill.get("ok"):
|
||||
filled_n = int(fill.get("filled_sheets") or 0)
|
||||
if filled_n <= 0:
|
||||
rb = rollback_bought_coin_to_usdt(
|
||||
rb = _spot_bridge.rollback_bought_coin_to_usdt(
|
||||
conn,
|
||||
ex,
|
||||
bridge_id=bridge_id,
|
||||
@@ -319,7 +313,7 @@ def open_coin_option_buy_full(
|
||||
premium_paid = round(float(ask) * eth_amount, 8)
|
||||
premium_ccy = premium_ccy_for_mode("coin", underlying)
|
||||
|
||||
update_bridge(
|
||||
_spot_bridge.update_bridge(
|
||||
conn,
|
||||
bridge_id,
|
||||
status=BRIDGE_HOLDING,
|
||||
@@ -471,7 +465,7 @@ def maybe_sell_spot_after_close(
|
||||
from lib.options.options_db import init_options_tables
|
||||
|
||||
init_options_tables(conn)
|
||||
return sell_residual_after_option_flat(conn, ex, underlying=underlying, inst_id=inst_id)
|
||||
return _spot_bridge.sell_residual_after_option_flat(conn, ex, underlying=underlying, inst_id=inst_id)
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
|
||||
+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")
|
||||
|
||||
+58
-25
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user