模拟盘对齐币本位:钱包支持 ETH/BTC,现货桥与期权权利金走本地撮合。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 15:16:51 +08:00
parent c8688a11ae
commit 6f721d1e6d
6 changed files with 836 additions and 516 deletions
+473 -479
View File
@@ -1,479 +1,473 @@
"""币本位单笔期权:买满 USDT→币 → 开满期权 → 平后卖回.""" """币本位单笔期权:买满 USDT→币 → 开满期权 → 平后卖回."""
from __future__ import annotations from __future__ import annotations
import os import os
import time import time
from typing import Any from typing import Any
from lib.exchange.okx_options_lib import ( from lib.exchange.okx_options_lib import (
cap_option_buy_sheets_to_ask_depth, cap_option_buy_sheets_to_ask_depth,
option_buy_liquidity_ok, option_buy_liquidity_ok,
td_mode_for_option_buy, td_mode_for_option_buy,
wait_option_order_full_fill, wait_option_order_full_fill,
) )
from lib.options.options_margin_mode_lib import ( from lib.options.options_margin_mode_lib import (
calc_sheets_from_coin_balance, calc_sheets_from_coin_balance,
compute_coin_budget_usdt, compute_coin_budget_usdt,
is_coin_margin_mode, is_coin_margin_mode,
margin_mode_from_inst_id, margin_mode_from_inst_id,
normalize_options_margin_mode, normalize_options_margin_mode,
plan_coin_open_by_budget, plan_coin_open_by_budget,
premium_ccy_for_mode, premium_ccy_for_mode,
) )
from lib.options.options_spot_bridge_lib import ( from lib.options import options_spot_bridge_lib as _spot_bridge
BRIDGE_BOUGHT, from lib.options.options_spot_bridge_lib import (
BRIDGE_HOLDING, BRIDGE_BOUGHT,
bridge_blocks_new_open_msg, BRIDGE_HOLDING,
fetch_trading_coin_available, )
insert_bridge,
rollback_bought_coin_to_usdt,
sell_residual_after_option_flat, def coin_budget_preview(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
spot_market_buy_coin_with_usdt, from lib.exchange.okx_options_lib import fetch_options_balances
update_bridge,
) bal = cfg.get("fetch_options_balances")(ex, force=True) if callable(cfg.get("fetch_options_balances")) else fetch_options_balances(ex, force=True)
trading = bal.get("trading_usdt_avail")
if trading is None:
def coin_budget_preview(cfg: dict[str, Any], ex: Any) -> dict[str, Any]: trading = bal.get("trading_usdt")
from lib.exchange.okx_options_lib import fetch_options_balances try:
trading_f = float(trading or 0)
bal = cfg.get("fetch_options_balances")(ex, force=True) if callable(cfg.get("fetch_options_balances")) else fetch_options_balances(ex, force=True) except (TypeError, ValueError):
trading = bal.get("trading_usdt_avail") trading_f = 0.0
if trading is None: buf = float(cfg.get("budget_buffer") or 0.95)
trading = bal.get("trading_usdt") return compute_coin_budget_usdt(trading_f, buffer=buf)
try:
trading_f = float(trading or 0)
except (TypeError, ValueError): def open_coin_option_buy_full(
trading_f = 0.0 cfg: dict[str, Any],
buf = float(cfg.get("budget_buffer") or 0.95) ex: Any,
return compute_coin_budget_usdt(trading_f, buffer=buf) *,
inst_id: str,
signal_note: str = "",
def open_coin_option_buy_full( target_index: float | None = None,
cfg: dict[str, Any], profit_exit_enabled: bool = False,
ex: Any, profit_exit_mult: float = 1.0,
*, target_sheets: int | None = None,
inst_id: str, ) -> dict[str, Any]:
signal_note: str = "", """先按最大可开张数估权利金×现货缓冲买币,再开对应张数(不全额兑换预算)."""
target_index: float | None = None, from lib.options.options_db import init_options_tables
profit_exit_enabled: bool = False, from lib.options.options_position_limit_lib import (
profit_exit_mult: float = 1.0, compound_full_single_position_block_msg,
target_sheets: int | None = None, option_position_limit_block_msg,
) -> dict[str, Any]: )
"""先按最大可开张数估权利金×现货缓冲买币,再开对应张数(不全额兑换预算)."""
from lib.options.options_db import init_options_tables if not is_coin_margin_mode():
from lib.options.options_position_limit_lib import ( return {"ok": False, "msg": "当前非币本位模式"}
compound_full_single_position_block_msg, if margin_mode_from_inst_id(inst_id) != "coin":
option_position_limit_block_msg, return {"ok": False, "msg": "合约不是币本位期权(请确认未选中 USD_UM 合约)"}
)
# 解析标的
if not is_coin_margin_mode(): parts = inst_id.split("-")
return {"ok": False, "msg": "当前非币本位模式"} underlying = (parts[0] if parts else "ETH").upper()
if margin_mode_from_inst_id(inst_id) != "coin":
return {"ok": False, "msg": "合约不是币本位期权(请确认未选中 USD_UM 合约)"} conn = cfg["get_db"]()
try:
# 解析标的 init_options_tables(conn)
parts = inst_id.split("-") block = _spot_bridge.bridge_blocks_new_open_msg(conn)
underlying = (parts[0] if parts else "ETH").upper() if block:
return {"ok": False, "msg": block, "can_open": False}
conn = cfg["get_db"]()
try: compound_block = compound_full_single_position_block_msg(
init_options_tables(conn) ex, fetch_positions=cfg.get("fetch_option_positions")
block = bridge_blocks_new_open_msg(conn) )
if block: if compound_block:
return {"ok": False, "msg": block, "can_open": False} return {"ok": False, "msg": compound_block, "can_open": False}
pos_limit_msg = option_position_limit_block_msg(
compound_block = compound_full_single_position_block_msg( ex,
ex, fetch_positions=cfg.get("fetch_option_positions") opening_inst_id=inst_id,
) fetch_positions=cfg.get("fetch_option_positions"),
if compound_block: )
return {"ok": False, "msg": compound_block, "can_open": False} if pos_limit_msg:
pos_limit_msg = option_position_limit_block_msg( return {"ok": False, "msg": pos_limit_msg, "can_open": False}
ex,
opening_inst_id=inst_id, budget_info = coin_budget_preview(cfg, ex)
fetch_positions=cfg.get("fetch_option_positions"), if not budget_info.get("ok"):
) return {"ok": False, "msg": budget_info.get("msg") or "USDT 预算无效", "budget": budget_info}
if pos_limit_msg: budget_usdt = float(budget_info["budget_usdt"])
return {"ok": False, "msg": pos_limit_msg, "can_open": False}
q = cfg["quote_option_contract"](ex, inst_id)
budget_info = coin_budget_preview(cfg, ex) if not q.get("ok"):
if not budget_info.get("ok"): return q
return {"ok": False, "msg": budget_info.get("msg") or "USDT 预算无效", "budget": budget_info} ask = q.get("ask")
budget_usdt = float(budget_info["budget_usdt"]) ask_sz = q.get("ask_sz")
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
q = cfg["quote_option_contract"](ex, inst_id) if not can_open:
if not q.get("ok"): return {
return q "ok": False,
ask = q.get("ask") "msg": block_msg or "暂无卖一深度,无法买入",
ask_sz = q.get("ask_sz") "can_open": False,
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz) }
if not can_open:
return { ct_mult = float(q.get("ct_mult") or 0.01)
"ok": False, min_sz = int(q.get("min_sz") or 1)
"msg": block_msg or "暂无卖一深度,无法买入", idx = None
"can_open": False, try:
} idx = float(q.get("index_px") or q.get("idxPx") or 0)
except (TypeError, ValueError):
ct_mult = float(q.get("ct_mult") or 0.01) idx = 0.0
min_sz = int(q.get("min_sz") or 1) if idx <= 0:
idx = None try:
try: from lib.exchange.okx_options_lib import fetch_index_price
idx = float(q.get("index_px") or q.get("idxPx") or 0)
except (TypeError, ValueError): idx = float(fetch_index_price(ex, f"{underlying}-USD") or 0)
idx = 0.0 except Exception:
if idx <= 0: idx = 0.0
try:
from lib.exchange.okx_options_lib import fetch_index_price plan = plan_coin_open_by_budget(
quote_per_unit=float(ask),
idx = float(fetch_index_price(ex, f"{underlying}-USD") or 0) ct_mult=ct_mult,
except Exception: min_sz=min_sz,
idx = 0.0 budget_usdt=budget_usdt,
index_px=float(idx),
plan = plan_coin_open_by_budget( ask_sz=ask_sz,
quote_per_unit=float(ask), target_sheets=target_sheets,
ct_mult=ct_mult, )
min_sz=min_sz, if not plan.get("ok"):
budget_usdt=budget_usdt, return {
index_px=float(idx), "ok": False,
ask_sz=ask_sz, "msg": plan.get("msg") or "无法规划买币张数",
target_sheets=target_sheets, "plan": plan,
) "budget": budget_info,
if not plan.get("ok"): "can_open": False,
return { }
"ok": False, buy_usdt = float(plan["buy_usdt"])
"msg": plan.get("msg") or "无法规划买币张数", sheets = int(plan["sheets"])
"plan": plan,
"budget": budget_info, # 1) 仅买「权利金×现货缓冲」所需 USDT,不全额兑换预算
"can_open": False, 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)
buy_usdt = float(plan["buy_usdt"]) if not buy.get("ok"):
sheets = int(plan["sheets"]) return {
"ok": False,
# 1) 仅买「权利金×现货缓冲」所需 USDT,不全额兑换预算 "msg": f"现货买入 {underlying} 失败: {buy.get('msg')}",
coin_before = fetch_trading_coin_available(ex, underlying) or 0.0 "budget": budget_info,
buy = spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=buy_usdt) "plan": plan,
if not buy.get("ok"): }
return { bridge_id = _spot_bridge.insert_bridge(
"ok": False, conn,
"msg": f"现货买入 {underlying} 失败: {buy.get('msg')}", underlying=underlying,
"budget": budget_info, status=BRIDGE_BOUGHT,
"plan": plan, budget_usdt=buy_usdt,
} buy_ord_id=str(buy.get("ord_id") or ""),
bridge_id = insert_bridge( inst_id=inst_id,
conn, message="已买币,待开期权",
underlying=underlying, )
status=BRIDGE_BOUGHT, # 等余额落账
budget_usdt=buy_usdt, time.sleep(1.5)
buy_ord_id=str(buy.get("ord_id") or ""), try:
inst_id=inst_id, from lib.exchange.okx_options_lib import invalidate_options_balance_cache
message="已买币,待开期权",
) invalidate_options_balance_cache()
# 等余额落账 except Exception:
time.sleep(1.5) pass
try: coin_after = _spot_bridge.fetch_trading_coin_available(ex, underlying)
from lib.exchange.okx_options_lib import invalidate_options_balance_cache if coin_after is None:
rb = _spot_bridge.rollback_bought_coin_to_usdt(
invalidate_options_balance_cache() conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买币后读不到可用余额"
except Exception: )
pass return {
coin_after = fetch_trading_coin_available(ex, underlying) "ok": False,
if coin_after is None: "msg": "买币后读不到可用余额,已尝试卖回 USDT",
rb = rollback_bought_coin_to_usdt( "rollback": rb,
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买币后读不到可用余额" "budget": budget_info,
) "plan": plan,
return { }
"ok": False, coin_bought = max(0.0, float(coin_after) - float(coin_before or 0))
"msg": "买币后读不到可用余额,已尝试卖回 USDT", if coin_bought <= 0:
"rollback": rb, # 落账延迟时退化为用当前可用,但仍写入上限提示
"budget": budget_info, coin_bought = float(coin_after)
"plan": plan, if coin_bought <= 0:
} rb = _spot_bridge.rollback_bought_coin_to_usdt(
coin_bought = max(0.0, float(coin_after) - float(coin_before or 0)) conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买入量无效"
if coin_bought <= 0: )
# 落账延迟时退化为用当前可用,但仍写入上限提示 return {"ok": False, "msg": "买币后可用增量无效", "rollback": rb, "budget": budget_info}
coin_bought = float(coin_after) _spot_bridge.update_bridge(conn, bridge_id, coin_bought=float(coin_bought))
if coin_bought <= 0:
rb = rollback_bought_coin_to_usdt( sizing = calc_sheets_from_coin_balance(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买入量无效" quote_per_unit=float(ask),
) ct_mult=ct_mult,
return {"ok": False, "msg": "买币后可用增量无效", "rollback": rb, "budget": budget_info} min_sz=min_sz,
update_bridge(conn, bridge_id, coin_bought=float(coin_bought)) coin_available=float(coin_bought),
)
sizing = calc_sheets_from_coin_balance( if not sizing.get("ok"):
quote_per_unit=float(ask), rb = _spot_bridge.rollback_bought_coin_to_usdt(
ct_mult=ct_mult, conn,
min_sz=min_sz, ex,
coin_available=float(coin_bought), bridge_id=bridge_id,
) underlying=underlying,
if not sizing.get("ok"): reason=sizing.get("msg") or "张数不足",
rb = rollback_bought_coin_to_usdt( coin_amount=float(coin_bought),
conn, )
ex, return {"ok": False, "msg": sizing.get("msg"), "sizing": sizing, "rollback": rb, "budget": budget_info, "plan": plan}
bridge_id=bridge_id,
underlying=underlying, # 实盘以买到的币为准,但不超过规划张数
reason=sizing.get("msg") or "张数不足", sheets = min(int(sizing["sheets"]), int(plan["sheets"]))
coin_amount=float(coin_bought), capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=min_sz)
) if capped is None:
return {"ok": False, "msg": sizing.get("msg"), "sizing": sizing, "rollback": rb, "budget": budget_info, "plan": plan} rb = _spot_bridge.rollback_bought_coin_to_usdt(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason=cap_msg or "深度不足"
# 实盘以买到的币为准,但不超过规划张数 )
sheets = min(int(sizing["sheets"]), int(plan["sheets"])) return {"ok": False, "msg": cap_msg or "卖一深度不足", "rollback": rb}
capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=min_sz) if capped < sheets:
if capped is None: sheets = int(capped)
rb = rollback_bought_coin_to_usdt( sizing = {
conn, ex, bridge_id=bridge_id, underlying=underlying, reason=cap_msg or "深度不足" "ok": True,
) "sheets": sheets,
return {"ok": False, "msg": cap_msg or "卖一深度不足", "rollback": rb} "eth_amount": round(sheets * ct_mult, 8),
if capped < sheets: "coin_premium": round(sheets * float(ask) * ct_mult, 8),
sheets = int(capped) "ask_depth_capped": True,
sizing = { }
"ok": True, else:
"sheets": sheets, sizing = {
"eth_amount": round(sheets * ct_mult, 8), "ok": True,
"coin_premium": round(sheets * float(ask) * ct_mult, 8), "sheets": sheets,
"ask_depth_capped": True, "eth_amount": round(sheets * ct_mult, 8),
} "coin_premium": round(sheets * float(ask) * ct_mult, 8),
else: }
sizing = {
"ok": True, tick_sz = q.get("tick_sz")
"sheets": sheets, order = cfg["place_option_limit_order"](
"eth_amount": round(sheets * ct_mult, 8), ex,
"coin_premium": round(sheets * float(ask) * ct_mult, 8), inst_id=inst_id,
} side="buy",
sheets=sheets,
tick_sz = q.get("tick_sz") price=float(ask),
order = cfg["place_option_limit_order"]( td_mode=td_mode_for_option_buy(cfg.get("td_mode")),
ex, tick_sz=tick_sz,
inst_id=inst_id, ord_type="ioc",
side="buy", )
sheets=sheets, # 51008 时自动减半张数再试一次(买币已到位,避免整笔回滚)
price=float(ask), if (not order.get("ok")) and sheets > 1:
td_mode=td_mode_for_option_buy(cfg.get("td_mode")), msg_l = str(order.get("msg") or "").lower()
tick_sz=tick_sz, if "51008" in str(order.get("raw") or "").lower() or "不足" in str(order.get("msg") or ""):
ord_type="ioc", sheets2 = max(1, sheets // 2)
) if sheets2 < sheets:
# 51008 时自动减半张数再试一次(买币已到位,避免整笔回滚) order2 = cfg["place_option_limit_order"](
if (not order.get("ok")) and sheets > 1: ex,
msg_l = str(order.get("msg") or "").lower() inst_id=inst_id,
if "51008" in str(order.get("raw") or "").lower() or "不足" in str(order.get("msg") or ""): side="buy",
sheets2 = max(1, sheets // 2) sheets=sheets2,
if sheets2 < sheets: price=float(ask),
order2 = cfg["place_option_limit_order"]( td_mode=td_mode_for_option_buy(cfg.get("td_mode")),
ex, tick_sz=tick_sz,
inst_id=inst_id, ord_type="ioc",
side="buy", )
sheets=sheets2, if order2.get("ok"):
price=float(ask), order = order2
td_mode=td_mode_for_option_buy(cfg.get("td_mode")), sheets = sheets2
tick_sz=tick_sz, sizing = {
ord_type="ioc", "ok": True,
) "sheets": sheets,
if order2.get("ok"): "eth_amount": round(sheets * ct_mult, 8),
order = order2 "coin_premium": round(sheets * float(ask) * ct_mult, 8),
sheets = sheets2 "retried_half": True,
sizing = { }
"ok": True, if not order.get("ok"):
"sheets": sheets, rb = _spot_bridge.rollback_bought_coin_to_usdt(
"eth_amount": round(sheets * ct_mult, 8), conn, ex, bridge_id=bridge_id, underlying=underlying, reason=order.get("msg") or "下单失败"
"coin_premium": round(sheets * float(ask) * ct_mult, 8), )
"retried_half": True, return {"ok": False, "msg": order.get("msg") or "期权下单失败", "order": order, "rollback": rb}
}
if not order.get("ok"): ord_id = str((order.get("data") or {}).get("ordId") or "").strip()
rb = rollback_bought_coin_to_usdt( if not ord_id:
conn, ex, bridge_id=bridge_id, underlying=underlying, reason=order.get("msg") or "下单失败" rb = _spot_bridge.rollback_bought_coin_to_usdt(
) conn, ex, bridge_id=bridge_id, underlying=underlying, reason="无订单号"
return {"ok": False, "msg": order.get("msg") or "期权下单失败", "order": order, "rollback": rb} )
return {"ok": False, "msg": "下单成功但未返回订单号", "rollback": rb}
ord_id = str((order.get("data") or {}).get("ordId") or "").strip()
if not ord_id: try:
rb = rollback_bought_coin_to_usdt( fill_timeout = max(2.0, float(os.getenv("OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC") or "12"))
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="无订单号" except (TypeError, ValueError):
) fill_timeout = 12.0
return {"ok": False, "msg": "下单成功但未返回订单号", "rollback": rb} fill = wait_option_order_full_fill(
ex,
try: inst_id=inst_id,
fill_timeout = max(2.0, float(os.getenv("OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC") or "12")) ord_id=ord_id,
except (TypeError, ValueError): need_sheets=int(sheets),
fill_timeout = 12.0 timeout_sec=fill_timeout,
fill = wait_option_order_full_fill( cancel_on_timeout=True,
ex, )
inst_id=inst_id, if not fill.get("ok"):
ord_id=ord_id, filled_n = int(fill.get("filled_sheets") or 0)
need_sheets=int(sheets), if filled_n <= 0:
timeout_sec=fill_timeout, rb = _spot_bridge.rollback_bought_coin_to_usdt(
cancel_on_timeout=True, conn,
) ex,
if not fill.get("ok"): bridge_id=bridge_id,
filled_n = int(fill.get("filled_sheets") or 0) underlying=underlying,
if filled_n <= 0: reason=fill.get("msg") or "未成交",
rb = rollback_bought_coin_to_usdt( )
conn, return {"ok": False, "msg": fill.get("msg") or "未完全成交", "fill": fill, "rollback": rb}
ex, sheets = filled_n
bridge_id=bridge_id,
underlying=underlying, eth_amount = round(float(sheets) * ct_mult, 8)
reason=fill.get("msg") or "未成交", premium_paid = round(float(ask) * eth_amount, 8)
) premium_ccy = premium_ccy_for_mode("coin", underlying)
return {"ok": False, "msg": fill.get("msg") or "未完全成交", "fill": fill, "rollback": rb}
sheets = filled_n _spot_bridge.update_bridge(
conn,
eth_amount = round(float(sheets) * ct_mult, 8) bridge_id,
premium_paid = round(float(ask) * eth_amount, 8) status=BRIDGE_HOLDING,
premium_ccy = premium_ccy_for_mode("coin", underlying) inst_id=inst_id,
message="期权持仓中",
update_bridge( )
conn,
bridge_id, trade_id = _insert_coin_trade(
status=BRIDGE_HOLDING, conn,
inst_id=inst_id, inst_id=inst_id,
message="期权持仓中", underlying=underlying,
) opt_type=str(q.get("opt_type") or ""),
strike=q.get("strike"),
trade_id = _insert_coin_trade( exp_time=q.get("exp_time"),
conn, sheets=int(sheets),
inst_id=inst_id, eth_amount=eth_amount,
underlying=underlying, open_quote=float(ask),
opt_type=str(q.get("opt_type") or ""), premium_paid=premium_paid,
strike=q.get("strike"), signal_note=signal_note,
exp_time=q.get("exp_time"), exchange_ord_id=ord_id,
sheets=int(sheets), bridge_id=bridge_id,
eth_amount=eth_amount, budget_usdt=buy_usdt,
open_quote=float(ask), premium_ccy=premium_ccy,
premium_paid=premium_paid, profit_exit_enabled=profit_exit_enabled,
signal_note=signal_note, profit_exit_mult=profit_exit_mult,
exchange_ord_id=ord_id, )
bridge_id=bridge_id,
budget_usdt=buy_usdt, # 目标位 / 翻倍离场 — 复用现有逻辑若存在
premium_ccy=premium_ccy, try:
profit_exit_enabled=profit_exit_enabled, if target_index is not None:
profit_exit_mult=profit_exit_mult, from lib.options.options_target_lib import upsert_target_monitor
)
upsert_target_monitor(
# 目标位 / 翻倍离场 — 复用现有逻辑若存在 conn,
try: inst_id=inst_id,
if target_index is not None: underlying=underlying,
from lib.options.options_target_lib import upsert_target_monitor opt_type=str(q.get("opt_type") or ""),
target_index=float(target_index),
upsert_target_monitor( trade_id=trade_id,
conn, sheets=int(sheets),
inst_id=inst_id, )
underlying=underlying, except Exception:
opt_type=str(q.get("opt_type") or ""), pass
target_index=float(target_index), try:
trade_id=trade_id, from lib.options.options_notify_lib import notify_options_open
sheets=int(sheets),
) notify_options_open(
except Exception: cfg,
pass conn,
try: trade_id=trade_id,
from lib.options.options_notify_lib import notify_options_open inst_id=inst_id,
underlying=underlying,
notify_options_open( opt_type=str(q.get("opt_type") or ""),
cfg, sheets=int(sheets),
conn, premium_paid=premium_paid,
trade_id=trade_id, open_quote=float(ask),
inst_id=inst_id, target_index=target_index,
underlying=underlying, signal_note=signal_note,
opt_type=str(q.get("opt_type") or ""), )
sheets=int(sheets), except Exception:
premium_paid=premium_paid, pass
open_quote=float(ask),
target_index=target_index, return {
signal_note=signal_note, "ok": True,
) "msg": f"币本位开仓成功 {sheets}",
except Exception: "margin_mode": "coin",
pass "budget": budget_info,
"sizing": sizing,
return { "sheets": sheets,
"ok": True, "eth_amount": eth_amount,
"msg": f"币本位开仓成功 {sheets}", "premium_paid": premium_paid,
"margin_mode": "coin", "premium_ccy": premium_ccy,
"budget": budget_info, "bridge_id": bridge_id,
"sizing": sizing, "trade_id": trade_id,
"sheets": sheets, "order": order,
"eth_amount": eth_amount, "fill": fill,
"premium_paid": premium_paid, }
"premium_ccy": premium_ccy, finally:
"bridge_id": bridge_id, try:
"trade_id": trade_id, conn.close()
"order": order, except Exception:
"fill": fill, pass
}
finally:
try: def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
conn.close() pe = 1 if kwargs.get("profit_exit_enabled") else 0
except Exception: pe_mult = float(kwargs.get("profit_exit_mult") or 1.0)
pass pe_state = "active" if pe else "idle"
cur = conn.execute(
"""
def _insert_coin_trade(conn: Any, **kwargs: Any) -> int: INSERT INTO options_trades(
pe = 1 if kwargs.get("profit_exit_enabled") else 0 inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount,
pe_mult = float(kwargs.get("profit_exit_mult") or 1.0) open_quote, premium_paid, status, signal_note, exchange_ord_id,
pe_state = "active" if pe else "idle" margin_mode, premium_ccy, bridge_id, budget_usdt,
cur = conn.execute( profit_exit_enabled, profit_exit_mult, profit_exit_state
""" ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?)
INSERT INTO options_trades( """,
inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount, (
open_quote, premium_paid, status, signal_note, exchange_ord_id, kwargs["inst_id"],
margin_mode, premium_ccy, bridge_id, budget_usdt, kwargs["underlying"],
profit_exit_enabled, profit_exit_mult, profit_exit_state kwargs["opt_type"],
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?) kwargs.get("strike"),
""", str(kwargs.get("exp_time") or ""),
( kwargs["sheets"],
kwargs["inst_id"], kwargs["eth_amount"],
kwargs["underlying"], kwargs.get("open_quote"),
kwargs["opt_type"], kwargs.get("premium_paid"),
kwargs.get("strike"), kwargs.get("signal_note") or "",
str(kwargs.get("exp_time") or ""), kwargs.get("exchange_ord_id"),
kwargs["sheets"], kwargs.get("premium_ccy") or "ETH",
kwargs["eth_amount"], kwargs.get("bridge_id"),
kwargs.get("open_quote"), kwargs.get("budget_usdt"),
kwargs.get("premium_paid"), pe,
kwargs.get("signal_note") or "", pe_mult,
kwargs.get("exchange_ord_id"), pe_state,
kwargs.get("premium_ccy") or "ETH", ),
kwargs.get("bridge_id"), )
kwargs.get("budget_usdt"), conn.commit()
pe, return int(cur.lastrowid)
pe_mult,
pe_state,
), def maybe_sell_spot_after_close(
) cfg: dict[str, Any],
conn.commit() ex: Any,
return int(cur.lastrowid) *,
inst_id: str,
close_result: dict[str, Any] | None = None,
def maybe_sell_spot_after_close( ) -> dict[str, Any] | None:
cfg: dict[str, Any], """期权平仓后若该合约为币本位且已空仓,卖回本桥残留币."""
ex: Any, if margin_mode_from_inst_id(inst_id) != "coin":
*, return None
inst_id: str, # 仍有仓则不卖
close_result: dict[str, Any] | None = None, try:
) -> dict[str, Any] | None: rows = cfg["fetch_option_positions"](ex) or []
"""期权平仓后若该合约为币本位且已空仓,卖回本桥残留币.""" for p in rows:
if margin_mode_from_inst_id(inst_id) != "coin": if str(p.get("instId") or p.get("inst_id") or "") != inst_id:
return None continue
# 仍有仓则不卖 try:
try: if abs(float(p.get("pos") or 0)) > 1e-12:
rows = cfg["fetch_option_positions"](ex) or [] return {"ok": True, "skipped": True, "msg": "仍有持仓,暂不卖币"}
for p in rows: except (TypeError, ValueError):
if str(p.get("instId") or p.get("inst_id") or "") != inst_id: pass
continue except Exception:
try: pass
if abs(float(p.get("pos") or 0)) > 1e-12: parts = inst_id.split("-")
return {"ok": True, "skipped": True, "msg": "仍有持仓,暂不卖币"} underlying = (parts[0] if parts else "ETH").upper()
except (TypeError, ValueError): conn = cfg["get_db"]()
pass try:
except Exception: from lib.options.options_db import init_options_tables
pass
parts = inst_id.split("-") init_options_tables(conn)
underlying = (parts[0] if parts else "ETH").upper() return _spot_bridge.sell_residual_after_option_flat(conn, ex, underlying=underlying, inst_id=inst_id)
conn = cfg["get_db"]() finally:
try: try:
from lib.options.options_db import init_options_tables conn.close()
except Exception:
init_options_tables(conn) pass
return sell_residual_after_option_flat(conn, ex, underlying=underlying, inst_id=inst_id)
finally:
try:
conn.close()
except Exception:
pass
+87 -3
View File
@@ -64,6 +64,19 @@ def _contract_size(exchange: Any, symbol: str) -> float:
return 1.0 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: class SimBroker:
def __init__(self, get_db: Callable) -> None: def __init__(self, get_db: Callable) -> None:
self.get_db = get_db self.get_db = get_db
@@ -377,9 +390,10 @@ class SimBroker:
qty = n * ct_mult qty = n * ct_mult
pr = option_fill(action="open", bid=bid, ask=ask, qty=qty, fee_rate=fr) pr = option_fill(action="open", bid=bid, ask=ask, qty=qty, fee_rate=fr)
cost = pr.notional + pr.fee cost = pr.notional + pr.fee
prem_ccy = _premium_ccy_for_inst(inst_id)
try: try:
self.wallets.debit_trading( self.wallets.debit_trading(
"USDC", prem_ccy,
cost, cost,
kind="option_open", kind="option_open",
note=f"buy {inst_id} x{n}@{pr.fill_px}", note=f"buy {inst_id} x{n}@{pr.fill_px}",
@@ -490,8 +504,9 @@ class SimBroker:
conn.close() conn.close()
if credit > 0: if credit > 0:
prem_ccy = _premium_ccy_for_inst(inst_id)
self.wallets.credit_trading( self.wallets.credit_trading(
"USDC", prem_ccy,
credit, credit,
kind="option_close", kind="option_close",
note=f"sell {inst_id} x{close_n}@{pr.fill_px}", note=f"sell {inst_id} x{close_n}@{pr.fill_px}",
@@ -552,6 +567,67 @@ class SimBroker:
) )
return result 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-USDTccxt 常用 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( def _index_px_for_option(
self, self,
exchange: Any, exchange: Any,
@@ -645,6 +721,14 @@ class SimBroker:
else: else:
intrinsic_u = 0.0 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() conn = self.get_db()
try: try:
conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,)) conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,))
@@ -681,7 +765,7 @@ class SimBroker:
if settle_recv > 1e-12: if settle_recv > 1e-12:
self.wallets.credit_trading( self.wallets.credit_trading(
"USDC", prem_ccy,
settle_recv, settle_recv,
kind="option_expiry", kind="option_expiry",
note=f"expiry settle {inst_id} @{spot:g} recv={settle_recv}", note=f"expiry settle {inst_id} @{spot:g} recv={settle_recv}",
+18
View File
@@ -14,6 +14,19 @@ def _env_float(key: str, default: float) -> float:
return float(default) 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: def init_sim_tables(conn: sqlite3.Connection) -> None:
conn.execute( conn.execute(
""" """
@@ -23,10 +36,15 @@ def init_sim_tables(conn: sqlite3.Connection) -> None:
trading_usdt REAL NOT NULL DEFAULT 0, trading_usdt REAL NOT NULL DEFAULT 0,
funding_usdc REAL NOT NULL DEFAULT 0, funding_usdc REAL NOT NULL DEFAULT 0,
trading_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 updated_at TEXT
) )
""" """
) )
_ensure_sim_wallet_coin_columns(conn)
conn.execute( conn.execute(
""" """
CREATE TABLE IF NOT EXISTS sim_perp_positions ( CREATE TABLE IF NOT EXISTS sim_perp_positions (
+144 -8
View File
@@ -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): if _GET_DB is not None and is_sim_mode(_GET_DB):
w = broker().balances_header() w = broker().balances_header()
return ( return (
round(float(w["trading_usdc"]), 2), round(float(w.get("trading_usdc") or 0), 2),
round(float(w["funding_usdc"]), 2), round(float(w.get("funding_usdc") or 0), 2),
round(float(w["funding_usdt"]), 2), round(float(w.get("funding_usdt") or 0), 2),
round(float(w["trading_usdt"]), 2), round(float(w.get("trading_usdt") or 0), 2),
) )
except Exception: except Exception:
pass pass
@@ -207,20 +207,32 @@ def _patch_okx_options_lib(app_module: Any) -> None:
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):
w = broker().balances_header() w = broker().balances_header()
fu = float(w["funding_usdt"]) fu = float(w.get("funding_usdt") or 0)
fc = float(w["funding_usdc"]) fc = float(w.get("funding_usdc") or 0)
tu = float(w["trading_usdt"]) tu = float(w.get("trading_usdt") or 0)
tc = float(w["trading_usdc"]) 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 { return {
"scope": "main", "scope": "main",
"funding_usdt": fu, "funding_usdt": fu,
"funding_usdc": fc, "funding_usdc": fc,
"trading_usdt": tu, "trading_usdt": tu,
"trading_usdc": tc, "trading_usdc": tc,
"funding_eth": fe,
"trading_eth": te,
"funding_btc": fb,
"trading_btc": tb,
"funding_usdt_avail": fu, "funding_usdt_avail": fu,
"funding_usdc_avail": fc, "funding_usdc_avail": fc,
"trading_usdt_avail": tu, "trading_usdt_avail": tu,
"trading_usdc_avail": tc, "trading_usdc_avail": tc,
"funding_eth_avail": fe,
"trading_eth_avail": te,
"funding_btc_avail": fb,
"trading_btc_avail": tb,
} }
except Exception: except Exception:
pass pass
@@ -355,6 +367,130 @@ def _patch_okx_options_lib(app_module: Any) -> None:
opt_lib.fetch_option_positions = fetch_option_positions opt_lib.fetch_option_positions = fetch_option_positions
opt_lib._sim_hooks_applied = True 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): def wrap_option_place_fns(get_db: Callable, live_place_limit, live_place_market):
"""返回按模式分流的 place_option_limit/market.""" """返回按模式分流的 place_option_limit/market."""
+55
View File
@@ -145,3 +145,58 @@ def spot_usdc_usdt_fill(
fee=fee, fee=fee,
) )
raise ValueError("direction 须为 usdt_to_usdc 或 usdc_to_usdt") 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
View File
@@ -1,4 +1,4 @@
"""模拟资金钱包: funding/trading × USDT/USDC.""" """模拟资金钱包: funding/trading × USDT/USDC/ETH/BTC."""
from __future__ import annotations from __future__ import annotations
@@ -11,6 +11,10 @@ WALLET_KEYS = (
"trading_usdt", "trading_usdt",
"funding_usdc", "funding_usdc",
"trading_usdc", "trading_usdc",
"funding_eth",
"trading_eth",
"funding_btc",
"trading_btc",
) )
_ACCT_MAP = { _ACCT_MAP = {
@@ -18,6 +22,10 @@ _ACCT_MAP = {
("trading", "usdt"): "trading_usdt", ("trading", "usdt"): "trading_usdt",
("funding", "usdc"): "funding_usdc", ("funding", "usdc"): "funding_usdc",
("trading", "usdc"): "trading_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: def _now(self) -> str:
return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") 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]: def snapshot(self) -> dict[str, float]:
conn = self.get_db() conn = self.get_db()
try: try:
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone() row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone()
if row is None: return self._row_to_snap(row)
return {k: 0.0 for k in WALLET_KEYS}
return {k: float(row[k] or 0) for k in WALLET_KEYS}
finally: finally:
conn.close() conn.close()
@@ -56,6 +77,7 @@ class SimWallets:
return self.snapshot() return self.snapshot()
def total_usdt_equiv(self, snap: dict[str, float] | None = None) -> float: def total_usdt_equiv(self, snap: dict[str, float] | None = None) -> float:
"""稳定币合计(不含 ETH/BTC 折算)."""
v = snap or self.view() v = snap or self.view()
return round( return round(
float(v.get("funding_usdt") or 0) float(v.get("funding_usdt") or 0)
@@ -71,23 +93,30 @@ class SimWallets:
conn = self.get_db() conn = self.get_db()
try: try:
now = self._now() now = self._now()
full = {k: float(snap.get(k) or 0) for k in WALLET_KEYS}
conn.execute( conn.execute(
""" """
UPDATE sim_wallets SET 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 WHERE id=1
""", """,
( (
float(snap["funding_usdt"]), full["funding_usdt"],
float(snap["trading_usdt"]), full["trading_usdt"],
float(snap["funding_usdc"]), full["funding_usdc"],
float(snap["trading_usdc"]), full["trading_usdc"],
full["funding_eth"],
full["trading_eth"],
full["funding_btc"],
full["trading_btc"],
now, now,
), ),
) )
if owns: if owns:
conn.commit() conn.commit()
return {k: float(snap[k]) for k in WALLET_KEYS} return full
finally: finally:
if owns: if owns:
conn.close() conn.close()
@@ -117,14 +146,14 @@ class SimWallets:
raise ValueError("扣款金额须大于 0") raise ValueError("扣款金额须大于 0")
key = _ACCT_MAP.get(("trading", (ccy or "").lower())) key = _ACCT_MAP.get(("trading", (ccy or "").lower()))
if not key: if not key:
raise ValueError("币种须为 USDT 或 USDC") raise ValueError("币种须为 USDT/USDC/ETH/BTC")
conn = self.get_db() conn = self.get_db()
try: try:
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone() 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]) bal = float(snap[key])
if amt > bal + 1e-9: if amt > bal + 1e-9:
raise InsufficientFunds(f"交易账户 {ccy.upper()} 不足(可用 {bal:.4f})") raise InsufficientFunds(f"交易账户 {ccy.upper()} 不足(可用 {bal:.8f})")
snap[key] = bal - amt snap[key] = bal - amt
self._write(snap, conn=conn) self._write(snap, conn=conn)
self._ledger( self._ledger(
@@ -149,11 +178,11 @@ class SimWallets:
return self.snapshot() return self.snapshot()
key = _ACCT_MAP.get(("trading", (ccy or "").lower())) key = _ACCT_MAP.get(("trading", (ccy or "").lower()))
if not key: if not key:
raise ValueError("币种须为 USDT 或 USDC") raise ValueError("币种须为 USDT/USDC/ETH/BTC")
conn = self.get_db() conn = self.get_db()
try: try:
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone() 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 snap[key] = float(snap[key]) + amt
self._write(snap, conn=conn) self._write(snap, conn=conn)
self._ledger( self._ledger(
@@ -191,14 +220,14 @@ class SimWallets:
src_key = _ACCT_MAP.get((fa, ccy_l)) src_key = _ACCT_MAP.get((fa, ccy_l))
dst_key = _ACCT_MAP.get((ta, ccy_l)) dst_key = _ACCT_MAP.get((ta, ccy_l))
if not src_key or not dst_key: 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() conn = self.get_db()
try: try:
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone() 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]) src_bal = float(snap[src_key])
if amt > src_bal + 1e-9: 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[src_key] = src_bal - amt
snap[dst_key] = float(snap[dst_key]) + amt snap[dst_key] = float(snap[dst_key]) + amt
self._write(snap, conn=conn) self._write(snap, conn=conn)
@@ -246,7 +275,7 @@ class SimWallets:
fee: float | None = None, fee: float | None = None,
note: str | None = None, note: str | None = None,
) -> dict[str, Any]: ) -> 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) amt = float(amount)
if amt <= 0: if amt <= 0:
return {"ok": False, "detail": "数量须大于 0"} return {"ok": False, "detail": "数量须大于 0"}
@@ -255,13 +284,14 @@ class SimWallets:
acct = normalize_sim_account(account) or "trading" 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"}: pair = {fa, ta}
return {"ok": False, "detail": "仅支持 USDT↔USDC"} if pair not in ({"usdt", "usdc"}, {"usdt", "eth"}, {"usdt", "btc"}):
return {"ok": False, "detail": "仅支持 USDT↔USDC/ETH/BTC"}
if to_amount is not None: if to_amount is not None:
got = float(to_amount) got = float(to_amount)
elif rate is not None and float(rate) > 0: elif rate is not None and float(rate) > 0:
r = float(rate) 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) got = (amt / r) if fa == "usdt" else (amt * r)
else: else:
got = amt got = amt
@@ -272,10 +302,10 @@ class SimWallets:
conn = self.get_db() conn = self.get_db()
try: try:
row = conn.execute("SELECT * FROM sim_wallets WHERE id=1").fetchone() 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]) src = float(snap[src_key])
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:.8f})"}
snap[src_key] = src - amt snap[src_key] = src - amt
snap[dst_key] = float(snap[dst_key]) + got snap[dst_key] = float(snap[dst_key]) + got
self._write(snap, conn=conn) self._write(snap, conn=conn)
@@ -341,12 +371,15 @@ class SimWallets:
conn.execute("DELETE FROM sim_perp_positions") conn.execute("DELETE FROM sim_perp_positions")
conn.execute("DELETE FROM sim_option_positions") conn.execute("DELETE FROM sim_option_positions")
conn.execute("DELETE FROM sim_option_orders") conn.execute("DELETE FROM sim_option_orders")
now = self._now()
snap = { snap = {
"funding_usdt": amt, "funding_usdt": amt,
"trading_usdt": 0.0, "trading_usdt": 0.0,
"funding_usdc": 0.0, "funding_usdc": 0.0,
"trading_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._write(snap, conn=conn)
self._ledger( self._ledger(
@@ -361,4 +394,4 @@ class SimWallets:
conn.commit() conn.commit()
return {"ok": True, "wallets": snap, "total_usdt_equiv": amt} return {"ok": True, "wallets": snap, "total_usdt_equiv": amt}
finally: finally:
conn.close() conn.close()