模拟盘对齐币本位:钱包支持 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→币 → 开满期权 → 平后卖回."""
from __future__ import annotations
import os
import time
from typing import Any
from lib.exchange.okx_options_lib import (
cap_option_buy_sheets_to_ask_depth,
option_buy_liquidity_ok,
td_mode_for_option_buy,
wait_option_order_full_fill,
)
from lib.options.options_margin_mode_lib import (
calc_sheets_from_coin_balance,
compute_coin_budget_usdt,
is_coin_margin_mode,
margin_mode_from_inst_id,
normalize_options_margin_mode,
plan_coin_open_by_budget,
premium_ccy_for_mode,
)
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,
)
def coin_budget_preview(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
from lib.exchange.okx_options_lib import fetch_options_balances
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:
trading = bal.get("trading_usdt")
try:
trading_f = float(trading or 0)
except (TypeError, ValueError):
trading_f = 0.0
buf = float(cfg.get("budget_buffer") or 0.95)
return compute_coin_budget_usdt(trading_f, buffer=buf)
def open_coin_option_buy_full(
cfg: dict[str, Any],
ex: Any,
*,
inst_id: str,
signal_note: str = "",
target_index: float | None = None,
profit_exit_enabled: bool = False,
profit_exit_mult: float = 1.0,
target_sheets: int | None = None,
) -> dict[str, Any]:
"""先按最大可开张数估权利金×现货缓冲买币,再开对应张数(不全额兑换预算)."""
from lib.options.options_db import init_options_tables
from lib.options.options_position_limit_lib import (
compound_full_single_position_block_msg,
option_position_limit_block_msg,
)
if not is_coin_margin_mode():
return {"ok": False, "msg": "当前非币本位模式"}
if margin_mode_from_inst_id(inst_id) != "coin":
return {"ok": False, "msg": "合约不是币本位期权(请确认未选中 USD_UM 合约)"}
# 解析标的
parts = inst_id.split("-")
underlying = (parts[0] if parts else "ETH").upper()
conn = cfg["get_db"]()
try:
init_options_tables(conn)
block = bridge_blocks_new_open_msg(conn)
if block:
return {"ok": False, "msg": block, "can_open": False}
compound_block = compound_full_single_position_block_msg(
ex, fetch_positions=cfg.get("fetch_option_positions")
)
if compound_block:
return {"ok": False, "msg": compound_block, "can_open": False}
pos_limit_msg = option_position_limit_block_msg(
ex,
opening_inst_id=inst_id,
fetch_positions=cfg.get("fetch_option_positions"),
)
if pos_limit_msg:
return {"ok": False, "msg": pos_limit_msg, "can_open": False}
budget_info = coin_budget_preview(cfg, ex)
if not budget_info.get("ok"):
return {"ok": False, "msg": budget_info.get("msg") or "USDT 预算无效", "budget": budget_info}
budget_usdt = float(budget_info["budget_usdt"])
q = cfg["quote_option_contract"](ex, inst_id)
if not q.get("ok"):
return q
ask = q.get("ask")
ask_sz = q.get("ask_sz")
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
if not can_open:
return {
"ok": False,
"msg": block_msg or "暂无卖一深度,无法买入",
"can_open": False,
}
ct_mult = float(q.get("ct_mult") or 0.01)
min_sz = int(q.get("min_sz") or 1)
idx = None
try:
idx = float(q.get("index_px") or q.get("idxPx") or 0)
except (TypeError, ValueError):
idx = 0.0
if idx <= 0:
try:
from lib.exchange.okx_options_lib import fetch_index_price
idx = float(fetch_index_price(ex, f"{underlying}-USD") or 0)
except Exception:
idx = 0.0
plan = plan_coin_open_by_budget(
quote_per_unit=float(ask),
ct_mult=ct_mult,
min_sz=min_sz,
budget_usdt=budget_usdt,
index_px=float(idx),
ask_sz=ask_sz,
target_sheets=target_sheets,
)
if not plan.get("ok"):
return {
"ok": False,
"msg": plan.get("msg") or "无法规划买币张数",
"plan": plan,
"budget": budget_info,
"can_open": False,
}
buy_usdt = float(plan["buy_usdt"])
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)
if not buy.get("ok"):
return {
"ok": False,
"msg": f"现货买入 {underlying} 失败: {buy.get('msg')}",
"budget": budget_info,
"plan": plan,
}
bridge_id = insert_bridge(
conn,
underlying=underlying,
status=BRIDGE_BOUGHT,
budget_usdt=buy_usdt,
buy_ord_id=str(buy.get("ord_id") or ""),
inst_id=inst_id,
message="已买币,待开期权",
)
# 等余额落账
time.sleep(1.5)
try:
from lib.exchange.okx_options_lib import invalidate_options_balance_cache
invalidate_options_balance_cache()
except Exception:
pass
coin_after = fetch_trading_coin_available(ex, underlying)
if coin_after is None:
rb = rollback_bought_coin_to_usdt(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买币后读不到可用余额"
)
return {
"ok": False,
"msg": "买币后读不到可用余额,已尝试卖回 USDT",
"rollback": rb,
"budget": budget_info,
"plan": plan,
}
coin_bought = max(0.0, float(coin_after) - float(coin_before or 0))
if coin_bought <= 0:
# 落账延迟时退化为用当前可用,但仍写入上限提示
coin_bought = float(coin_after)
if coin_bought <= 0:
rb = 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))
sizing = calc_sheets_from_coin_balance(
quote_per_unit=float(ask),
ct_mult=ct_mult,
min_sz=min_sz,
coin_available=float(coin_bought),
)
if not sizing.get("ok"):
rb = rollback_bought_coin_to_usdt(
conn,
ex,
bridge_id=bridge_id,
underlying=underlying,
reason=sizing.get("msg") or "张数不足",
coin_amount=float(coin_bought),
)
return {"ok": False, "msg": sizing.get("msg"), "sizing": sizing, "rollback": rb, "budget": budget_info, "plan": plan}
# 实盘以买到的币为准,但不超过规划张数
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(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason=cap_msg or "深度不足"
)
return {"ok": False, "msg": cap_msg or "卖一深度不足", "rollback": rb}
if capped < sheets:
sheets = int(capped)
sizing = {
"ok": True,
"sheets": sheets,
"eth_amount": round(sheets * ct_mult, 8),
"coin_premium": round(sheets * float(ask) * ct_mult, 8),
"ask_depth_capped": True,
}
else:
sizing = {
"ok": True,
"sheets": sheets,
"eth_amount": round(sheets * ct_mult, 8),
"coin_premium": round(sheets * float(ask) * ct_mult, 8),
}
tick_sz = q.get("tick_sz")
order = cfg["place_option_limit_order"](
ex,
inst_id=inst_id,
side="buy",
sheets=sheets,
price=float(ask),
td_mode=td_mode_for_option_buy(cfg.get("td_mode")),
tick_sz=tick_sz,
ord_type="ioc",
)
# 51008 时自动减半张数再试一次(买币已到位,避免整笔回滚)
if (not order.get("ok")) and sheets > 1:
msg_l = str(order.get("msg") or "").lower()
if "51008" in str(order.get("raw") or "").lower() or "不足" in str(order.get("msg") or ""):
sheets2 = max(1, sheets // 2)
if sheets2 < sheets:
order2 = cfg["place_option_limit_order"](
ex,
inst_id=inst_id,
side="buy",
sheets=sheets2,
price=float(ask),
td_mode=td_mode_for_option_buy(cfg.get("td_mode")),
tick_sz=tick_sz,
ord_type="ioc",
)
if order2.get("ok"):
order = order2
sheets = sheets2
sizing = {
"ok": True,
"sheets": sheets,
"eth_amount": round(sheets * ct_mult, 8),
"coin_premium": round(sheets * float(ask) * ct_mult, 8),
"retried_half": True,
}
if not order.get("ok"):
rb = 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(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="无订单号"
)
return {"ok": False, "msg": "下单成功但未返回订单号", "rollback": rb}
try:
fill_timeout = max(2.0, float(os.getenv("OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC") or "12"))
except (TypeError, ValueError):
fill_timeout = 12.0
fill = wait_option_order_full_fill(
ex,
inst_id=inst_id,
ord_id=ord_id,
need_sheets=int(sheets),
timeout_sec=fill_timeout,
cancel_on_timeout=True,
)
if not fill.get("ok"):
filled_n = int(fill.get("filled_sheets") or 0)
if filled_n <= 0:
rb = rollback_bought_coin_to_usdt(
conn,
ex,
bridge_id=bridge_id,
underlying=underlying,
reason=fill.get("msg") or "未成交",
)
return {"ok": False, "msg": fill.get("msg") or "未完全成交", "fill": fill, "rollback": rb}
sheets = filled_n
eth_amount = round(float(sheets) * ct_mult, 8)
premium_paid = round(float(ask) * eth_amount, 8)
premium_ccy = premium_ccy_for_mode("coin", underlying)
update_bridge(
conn,
bridge_id,
status=BRIDGE_HOLDING,
inst_id=inst_id,
message="期权持仓中",
)
trade_id = _insert_coin_trade(
conn,
inst_id=inst_id,
underlying=underlying,
opt_type=str(q.get("opt_type") or ""),
strike=q.get("strike"),
exp_time=q.get("exp_time"),
sheets=int(sheets),
eth_amount=eth_amount,
open_quote=float(ask),
premium_paid=premium_paid,
signal_note=signal_note,
exchange_ord_id=ord_id,
bridge_id=bridge_id,
budget_usdt=buy_usdt,
premium_ccy=premium_ccy,
profit_exit_enabled=profit_exit_enabled,
profit_exit_mult=profit_exit_mult,
)
# 目标位 / 翻倍离场 — 复用现有逻辑若存在
try:
if target_index is not None:
from lib.options.options_target_lib import upsert_target_monitor
upsert_target_monitor(
conn,
inst_id=inst_id,
underlying=underlying,
opt_type=str(q.get("opt_type") or ""),
target_index=float(target_index),
trade_id=trade_id,
sheets=int(sheets),
)
except Exception:
pass
try:
from lib.options.options_notify_lib import notify_options_open
notify_options_open(
cfg,
conn,
trade_id=trade_id,
inst_id=inst_id,
underlying=underlying,
opt_type=str(q.get("opt_type") or ""),
sheets=int(sheets),
premium_paid=premium_paid,
open_quote=float(ask),
target_index=target_index,
signal_note=signal_note,
)
except Exception:
pass
return {
"ok": True,
"msg": f"币本位开仓成功 {sheets}",
"margin_mode": "coin",
"budget": budget_info,
"sizing": sizing,
"sheets": sheets,
"eth_amount": eth_amount,
"premium_paid": premium_paid,
"premium_ccy": premium_ccy,
"bridge_id": bridge_id,
"trade_id": trade_id,
"order": order,
"fill": fill,
}
finally:
try:
conn.close()
except Exception:
pass
def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
pe = 1 if kwargs.get("profit_exit_enabled") else 0
pe_mult = float(kwargs.get("profit_exit_mult") or 1.0)
pe_state = "active" if pe else "idle"
cur = conn.execute(
"""
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,
margin_mode, premium_ccy, bridge_id, budget_usdt,
profit_exit_enabled, profit_exit_mult, profit_exit_state
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?)
""",
(
kwargs["inst_id"],
kwargs["underlying"],
kwargs["opt_type"],
kwargs.get("strike"),
str(kwargs.get("exp_time") or ""),
kwargs["sheets"],
kwargs["eth_amount"],
kwargs.get("open_quote"),
kwargs.get("premium_paid"),
kwargs.get("signal_note") or "",
kwargs.get("exchange_ord_id"),
kwargs.get("premium_ccy") or "ETH",
kwargs.get("bridge_id"),
kwargs.get("budget_usdt"),
pe,
pe_mult,
pe_state,
),
)
conn.commit()
return int(cur.lastrowid)
def maybe_sell_spot_after_close(
cfg: dict[str, Any],
ex: Any,
*,
inst_id: str,
close_result: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
"""期权平仓后若该合约为币本位且已空仓,卖回本桥残留币."""
if margin_mode_from_inst_id(inst_id) != "coin":
return None
# 仍有仓则不卖
try:
rows = cfg["fetch_option_positions"](ex) or []
for p in rows:
if str(p.get("instId") or p.get("inst_id") or "") != inst_id:
continue
try:
if abs(float(p.get("pos") or 0)) > 1e-12:
return {"ok": True, "skipped": True, "msg": "仍有持仓,暂不卖币"}
except (TypeError, ValueError):
pass
except Exception:
pass
parts = inst_id.split("-")
underlying = (parts[0] if parts else "ETH").upper()
conn = cfg["get_db"]()
try:
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)
finally:
try:
conn.close()
except Exception:
pass
"""币本位单笔期权:买满 USDT→币 → 开满期权 → 平后卖回."""
from __future__ import annotations
import os
import time
from typing import Any
from lib.exchange.okx_options_lib import (
cap_option_buy_sheets_to_ask_depth,
option_buy_liquidity_ok,
td_mode_for_option_buy,
wait_option_order_full_fill,
)
from lib.options.options_margin_mode_lib import (
calc_sheets_from_coin_balance,
compute_coin_budget_usdt,
is_coin_margin_mode,
margin_mode_from_inst_id,
normalize_options_margin_mode,
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,
)
def coin_budget_preview(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
from lib.exchange.okx_options_lib import fetch_options_balances
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:
trading = bal.get("trading_usdt")
try:
trading_f = float(trading or 0)
except (TypeError, ValueError):
trading_f = 0.0
buf = float(cfg.get("budget_buffer") or 0.95)
return compute_coin_budget_usdt(trading_f, buffer=buf)
def open_coin_option_buy_full(
cfg: dict[str, Any],
ex: Any,
*,
inst_id: str,
signal_note: str = "",
target_index: float | None = None,
profit_exit_enabled: bool = False,
profit_exit_mult: float = 1.0,
target_sheets: int | None = None,
) -> dict[str, Any]:
"""先按最大可开张数估权利金×现货缓冲买币,再开对应张数(不全额兑换预算)."""
from lib.options.options_db import init_options_tables
from lib.options.options_position_limit_lib import (
compound_full_single_position_block_msg,
option_position_limit_block_msg,
)
if not is_coin_margin_mode():
return {"ok": False, "msg": "当前非币本位模式"}
if margin_mode_from_inst_id(inst_id) != "coin":
return {"ok": False, "msg": "合约不是币本位期权(请确认未选中 USD_UM 合约)"}
# 解析标的
parts = inst_id.split("-")
underlying = (parts[0] if parts else "ETH").upper()
conn = cfg["get_db"]()
try:
init_options_tables(conn)
block = _spot_bridge.bridge_blocks_new_open_msg(conn)
if block:
return {"ok": False, "msg": block, "can_open": False}
compound_block = compound_full_single_position_block_msg(
ex, fetch_positions=cfg.get("fetch_option_positions")
)
if compound_block:
return {"ok": False, "msg": compound_block, "can_open": False}
pos_limit_msg = option_position_limit_block_msg(
ex,
opening_inst_id=inst_id,
fetch_positions=cfg.get("fetch_option_positions"),
)
if pos_limit_msg:
return {"ok": False, "msg": pos_limit_msg, "can_open": False}
budget_info = coin_budget_preview(cfg, ex)
if not budget_info.get("ok"):
return {"ok": False, "msg": budget_info.get("msg") or "USDT 预算无效", "budget": budget_info}
budget_usdt = float(budget_info["budget_usdt"])
q = cfg["quote_option_contract"](ex, inst_id)
if not q.get("ok"):
return q
ask = q.get("ask")
ask_sz = q.get("ask_sz")
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
if not can_open:
return {
"ok": False,
"msg": block_msg or "暂无卖一深度,无法买入",
"can_open": False,
}
ct_mult = float(q.get("ct_mult") or 0.01)
min_sz = int(q.get("min_sz") or 1)
idx = None
try:
idx = float(q.get("index_px") or q.get("idxPx") or 0)
except (TypeError, ValueError):
idx = 0.0
if idx <= 0:
try:
from lib.exchange.okx_options_lib import fetch_index_price
idx = float(fetch_index_price(ex, f"{underlying}-USD") or 0)
except Exception:
idx = 0.0
plan = plan_coin_open_by_budget(
quote_per_unit=float(ask),
ct_mult=ct_mult,
min_sz=min_sz,
budget_usdt=budget_usdt,
index_px=float(idx),
ask_sz=ask_sz,
target_sheets=target_sheets,
)
if not plan.get("ok"):
return {
"ok": False,
"msg": plan.get("msg") or "无法规划买币张数",
"plan": plan,
"budget": budget_info,
"can_open": False,
}
buy_usdt = float(plan["buy_usdt"])
sheets = int(plan["sheets"])
# 1) 仅买「权利金×现货缓冲」所需 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,
"msg": f"现货买入 {underlying} 失败: {buy.get('msg')}",
"budget": budget_info,
"plan": plan,
}
bridge_id = _spot_bridge.insert_bridge(
conn,
underlying=underlying,
status=BRIDGE_BOUGHT,
budget_usdt=buy_usdt,
buy_ord_id=str(buy.get("ord_id") or ""),
inst_id=inst_id,
message="已买币,待开期权",
)
# 等余额落账
time.sleep(1.5)
try:
from lib.exchange.okx_options_lib import invalidate_options_balance_cache
invalidate_options_balance_cache()
except Exception:
pass
coin_after = _spot_bridge.fetch_trading_coin_available(ex, underlying)
if coin_after is None:
rb = _spot_bridge.rollback_bought_coin_to_usdt(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买币后读不到可用余额"
)
return {
"ok": False,
"msg": "买币后读不到可用余额,已尝试卖回 USDT",
"rollback": rb,
"budget": budget_info,
"plan": plan,
}
coin_bought = max(0.0, float(coin_after) - float(coin_before or 0))
if coin_bought <= 0:
# 落账延迟时退化为用当前可用,但仍写入上限提示
coin_bought = float(coin_after)
if coin_bought <= 0:
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}
_spot_bridge.update_bridge(conn, bridge_id, coin_bought=float(coin_bought))
sizing = calc_sheets_from_coin_balance(
quote_per_unit=float(ask),
ct_mult=ct_mult,
min_sz=min_sz,
coin_available=float(coin_bought),
)
if not sizing.get("ok"):
rb = _spot_bridge.rollback_bought_coin_to_usdt(
conn,
ex,
bridge_id=bridge_id,
underlying=underlying,
reason=sizing.get("msg") or "张数不足",
coin_amount=float(coin_bought),
)
return {"ok": False, "msg": sizing.get("msg"), "sizing": sizing, "rollback": rb, "budget": budget_info, "plan": plan}
# 实盘以买到的币为准,但不超过规划张数
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 = _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}
if capped < sheets:
sheets = int(capped)
sizing = {
"ok": True,
"sheets": sheets,
"eth_amount": round(sheets * ct_mult, 8),
"coin_premium": round(sheets * float(ask) * ct_mult, 8),
"ask_depth_capped": True,
}
else:
sizing = {
"ok": True,
"sheets": sheets,
"eth_amount": round(sheets * ct_mult, 8),
"coin_premium": round(sheets * float(ask) * ct_mult, 8),
}
tick_sz = q.get("tick_sz")
order = cfg["place_option_limit_order"](
ex,
inst_id=inst_id,
side="buy",
sheets=sheets,
price=float(ask),
td_mode=td_mode_for_option_buy(cfg.get("td_mode")),
tick_sz=tick_sz,
ord_type="ioc",
)
# 51008 时自动减半张数再试一次(买币已到位,避免整笔回滚)
if (not order.get("ok")) and sheets > 1:
msg_l = str(order.get("msg") or "").lower()
if "51008" in str(order.get("raw") or "").lower() or "不足" in str(order.get("msg") or ""):
sheets2 = max(1, sheets // 2)
if sheets2 < sheets:
order2 = cfg["place_option_limit_order"](
ex,
inst_id=inst_id,
side="buy",
sheets=sheets2,
price=float(ask),
td_mode=td_mode_for_option_buy(cfg.get("td_mode")),
tick_sz=tick_sz,
ord_type="ioc",
)
if order2.get("ok"):
order = order2
sheets = sheets2
sizing = {
"ok": True,
"sheets": sheets,
"eth_amount": round(sheets * ct_mult, 8),
"coin_premium": round(sheets * float(ask) * ct_mult, 8),
"retried_half": True,
}
if not order.get("ok"):
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 = _spot_bridge.rollback_bought_coin_to_usdt(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="无订单号"
)
return {"ok": False, "msg": "下单成功但未返回订单号", "rollback": rb}
try:
fill_timeout = max(2.0, float(os.getenv("OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC") or "12"))
except (TypeError, ValueError):
fill_timeout = 12.0
fill = wait_option_order_full_fill(
ex,
inst_id=inst_id,
ord_id=ord_id,
need_sheets=int(sheets),
timeout_sec=fill_timeout,
cancel_on_timeout=True,
)
if not fill.get("ok"):
filled_n = int(fill.get("filled_sheets") or 0)
if filled_n <= 0:
rb = _spot_bridge.rollback_bought_coin_to_usdt(
conn,
ex,
bridge_id=bridge_id,
underlying=underlying,
reason=fill.get("msg") or "未成交",
)
return {"ok": False, "msg": fill.get("msg") or "未完全成交", "fill": fill, "rollback": rb}
sheets = filled_n
eth_amount = round(float(sheets) * ct_mult, 8)
premium_paid = round(float(ask) * eth_amount, 8)
premium_ccy = premium_ccy_for_mode("coin", underlying)
_spot_bridge.update_bridge(
conn,
bridge_id,
status=BRIDGE_HOLDING,
inst_id=inst_id,
message="期权持仓中",
)
trade_id = _insert_coin_trade(
conn,
inst_id=inst_id,
underlying=underlying,
opt_type=str(q.get("opt_type") or ""),
strike=q.get("strike"),
exp_time=q.get("exp_time"),
sheets=int(sheets),
eth_amount=eth_amount,
open_quote=float(ask),
premium_paid=premium_paid,
signal_note=signal_note,
exchange_ord_id=ord_id,
bridge_id=bridge_id,
budget_usdt=buy_usdt,
premium_ccy=premium_ccy,
profit_exit_enabled=profit_exit_enabled,
profit_exit_mult=profit_exit_mult,
)
# 目标位 / 翻倍离场 — 复用现有逻辑若存在
try:
if target_index is not None:
from lib.options.options_target_lib import upsert_target_monitor
upsert_target_monitor(
conn,
inst_id=inst_id,
underlying=underlying,
opt_type=str(q.get("opt_type") or ""),
target_index=float(target_index),
trade_id=trade_id,
sheets=int(sheets),
)
except Exception:
pass
try:
from lib.options.options_notify_lib import notify_options_open
notify_options_open(
cfg,
conn,
trade_id=trade_id,
inst_id=inst_id,
underlying=underlying,
opt_type=str(q.get("opt_type") or ""),
sheets=int(sheets),
premium_paid=premium_paid,
open_quote=float(ask),
target_index=target_index,
signal_note=signal_note,
)
except Exception:
pass
return {
"ok": True,
"msg": f"币本位开仓成功 {sheets}",
"margin_mode": "coin",
"budget": budget_info,
"sizing": sizing,
"sheets": sheets,
"eth_amount": eth_amount,
"premium_paid": premium_paid,
"premium_ccy": premium_ccy,
"bridge_id": bridge_id,
"trade_id": trade_id,
"order": order,
"fill": fill,
}
finally:
try:
conn.close()
except Exception:
pass
def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
pe = 1 if kwargs.get("profit_exit_enabled") else 0
pe_mult = float(kwargs.get("profit_exit_mult") or 1.0)
pe_state = "active" if pe else "idle"
cur = conn.execute(
"""
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,
margin_mode, premium_ccy, bridge_id, budget_usdt,
profit_exit_enabled, profit_exit_mult, profit_exit_state
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, 'coin', ?, ?, ?, ?, ?, ?)
""",
(
kwargs["inst_id"],
kwargs["underlying"],
kwargs["opt_type"],
kwargs.get("strike"),
str(kwargs.get("exp_time") or ""),
kwargs["sheets"],
kwargs["eth_amount"],
kwargs.get("open_quote"),
kwargs.get("premium_paid"),
kwargs.get("signal_note") or "",
kwargs.get("exchange_ord_id"),
kwargs.get("premium_ccy") or "ETH",
kwargs.get("bridge_id"),
kwargs.get("budget_usdt"),
pe,
pe_mult,
pe_state,
),
)
conn.commit()
return int(cur.lastrowid)
def maybe_sell_spot_after_close(
cfg: dict[str, Any],
ex: Any,
*,
inst_id: str,
close_result: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
"""期权平仓后若该合约为币本位且已空仓,卖回本桥残留币."""
if margin_mode_from_inst_id(inst_id) != "coin":
return None
# 仍有仓则不卖
try:
rows = cfg["fetch_option_positions"](ex) or []
for p in rows:
if str(p.get("instId") or p.get("inst_id") or "") != inst_id:
continue
try:
if abs(float(p.get("pos") or 0)) > 1e-12:
return {"ok": True, "skipped": True, "msg": "仍有持仓,暂不卖币"}
except (TypeError, ValueError):
pass
except Exception:
pass
parts = inst_id.split("-")
underlying = (parts[0] if parts else "ETH").upper()
conn = cfg["get_db"]()
try:
from lib.options.options_db import init_options_tables
init_options_tables(conn)
return _spot_bridge.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
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-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(
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}",
+18
View File
@@ -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
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):
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."""
+55
View File
@@ -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
View File
@@ -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()