对齐币本位期权:现货缓冲开仓、页头 ETH/BTC 余额与默认 coin 模式。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+283
-21
@@ -59,10 +59,21 @@ def install_options_trading(app: Flask, repo_root: str, app_module: Any) -> None
|
||||
cfg = _build_cfg(app_module)
|
||||
app.extensions["options_cfg"] = cfg
|
||||
register_options_routes(app, cfg)
|
||||
_register_options_hub_bridge(app, cfg)
|
||||
if enabled:
|
||||
_start_monitor_thread(app, cfg)
|
||||
|
||||
|
||||
def _register_options_hub_bridge(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
from lib.options.options_hub_lib import build_options_hub_snapshot
|
||||
|
||||
def snapshot_fn():
|
||||
return build_options_hub_snapshot(cfg)
|
||||
|
||||
hub_ctx = dict(app.config.get("HUB_CTX") or {})
|
||||
hub_ctx["options_snapshot_fn"] = snapshot_fn
|
||||
app.config["HUB_CTX"] = hub_ctx
|
||||
|
||||
|
||||
def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
from lib.exchange.okx_options_lib import (
|
||||
@@ -83,7 +94,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
transfer_ccy,
|
||||
)
|
||||
|
||||
cfg = {
|
||||
return {
|
||||
"enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
|
||||
"get_db": app_module.get_db,
|
||||
"login_required": app_module.login_required,
|
||||
@@ -95,6 +106,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"compound_full_enabled": _env_bool("OKX_OPTIONS_COMPOUND_FULL_ENABLED", True),
|
||||
"compound_full_cap_enabled": _env_bool("OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", False),
|
||||
"compound_full_cap_usdc": _env_float("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC", 300.0),
|
||||
"margin_mode": (os.getenv("OKX_OPTIONS_MARGIN_MODE") or "coin").strip().lower(),
|
||||
"default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
|
||||
"max_dte_days": _env_float("OKX_OPTIONS_MAX_DTE_DAYS", 2.0),
|
||||
"chain_max_dte_days": _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", 14.0),
|
||||
@@ -125,12 +137,6 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"options_api_ready": options_api_ready,
|
||||
"app_module": app_module,
|
||||
}
|
||||
try:
|
||||
from lib.sim.hooks import patch_options_cfg
|
||||
|
||||
return patch_options_cfg(cfg)
|
||||
except Exception:
|
||||
return cfg
|
||||
|
||||
|
||||
def _mark_balances_stale(cfg: dict[str, Any]) -> None:
|
||||
@@ -370,10 +376,9 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
|
||||
from flask import render_template_string
|
||||
|
||||
from lib.common.markdown_html_lib import render_markdown_html
|
||||
from lib.paths import REPO_ROOT
|
||||
from lib.hub.hub_strategy_lib import render_markdown_html
|
||||
|
||||
md_path = REPO_ROOT / "docs" / "期权开平仓与监控说明.md"
|
||||
md_path = Path(__file__).resolve().parents[2] / "docs" / "期权开平仓与监控说明.md"
|
||||
try:
|
||||
md_text = md_path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
@@ -417,16 +422,27 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
return jsonify({"ok": False, "msg": err})
|
||||
force = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes")
|
||||
bal = cfg["fetch_options_balances"](ex, force=force, scope="main")
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
**bal,
|
||||
"trade_budget": _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", float(cfg.get("trade_budget") or 10)),
|
||||
"compound_full_enabled": _compound_full_enabled(),
|
||||
"compound_full_cap_enabled": _env_bool("OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", False),
|
||||
"compound_full_cap_usdc": _env_float("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC", 300.0),
|
||||
}
|
||||
)
|
||||
from lib.options.options_margin_mode_lib import is_coin_margin_mode, normalize_options_margin_mode
|
||||
|
||||
margin_mode = normalize_options_margin_mode()
|
||||
payload = {
|
||||
"ok": True,
|
||||
**bal,
|
||||
"trade_budget": _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", float(cfg.get("trade_budget") or 10)),
|
||||
"compound_full_enabled": _compound_full_enabled(),
|
||||
"compound_full_cap_enabled": _env_bool("OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", False),
|
||||
"compound_full_cap_usdc": _env_float("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC", 300.0),
|
||||
"options_margin_mode": margin_mode,
|
||||
"options_margin_mode_label": "币本位" if margin_mode == "coin" else "USDC",
|
||||
}
|
||||
if is_coin_margin_mode():
|
||||
try:
|
||||
from lib.options.options_coin_open_lib import coin_budget_preview
|
||||
|
||||
payload["coin_budget"] = coin_budget_preview(cfg, ex)
|
||||
except Exception as e:
|
||||
payload["coin_budget"] = {"ok": False, "msg": str(e)}
|
||||
return jsonify(payload)
|
||||
|
||||
@app.route("/api/options/chain")
|
||||
@lr
|
||||
@@ -438,12 +454,16 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
# 热更新:链展示天数每次读 env,保存后刷新链即可
|
||||
chain_max_dte = _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", float(cfg.get("chain_max_dte_days") or 14))
|
||||
try:
|
||||
from lib.options.options_margin_mode_lib import normalize_options_margin_mode
|
||||
|
||||
margin_mode = normalize_options_margin_mode()
|
||||
chain = cfg["build_option_chain"](
|
||||
ex,
|
||||
u,
|
||||
max_dte_days=chain_max_dte,
|
||||
itm_only=False,
|
||||
itm_max_dist_usd=cfg["itm_max_dist"],
|
||||
margin_mode=margin_mode,
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"加载期权链失败: {e}"})
|
||||
@@ -452,6 +472,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
# 热更新:每次读 env,保存配置后刷新链即可生效
|
||||
ask_liq_filter = _env_bool("OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED", True)
|
||||
budget_buffer = _env_float("OKX_OPTIONS_BUDGET_BUFFER", 0.95)
|
||||
coin_budget = None
|
||||
try:
|
||||
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
||||
from lib.options.options_coin_open_lib import coin_budget_preview
|
||||
|
||||
if is_coin_margin_mode():
|
||||
coin_budget = coin_budget_preview(cfg, ex)
|
||||
except Exception:
|
||||
coin_budget = None
|
||||
if not expiries:
|
||||
return jsonify(
|
||||
{
|
||||
@@ -462,6 +491,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
"ask_liq_filter_enabled": ask_liq_filter,
|
||||
"budget_buffer": budget_buffer,
|
||||
"trade_budget": cfg["trade_budget"],
|
||||
"options_margin_mode": chain.get("margin_mode") or margin_mode,
|
||||
"coin_budget": coin_budget,
|
||||
}
|
||||
)
|
||||
return jsonify(
|
||||
@@ -472,6 +503,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
"ask_liq_filter_enabled": ask_liq_filter,
|
||||
"budget_buffer": budget_buffer,
|
||||
"trade_budget": cfg["trade_budget"],
|
||||
"options_margin_mode": chain.get("margin_mode") or margin_mode,
|
||||
"coin_budget": coin_budget,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -500,8 +533,158 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
if mode == "close_preview":
|
||||
paid = _open_premium_paid(cfg, inst_id)
|
||||
target = sheet_count if sheet_count is not None else 0
|
||||
return jsonify(_attach_close_preview(cfg, ex, {**q, "pos": target, "premium_paid": paid}, sheets=target, premium_paid=paid))
|
||||
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
|
||||
|
||||
row_mode = margin_mode_from_inst_id(inst_id)
|
||||
prem_ccy = premium_ccy_for_mode(row_mode, (inst_id.split("-")[0] if inst_id else "ETH"))
|
||||
preview_row = {
|
||||
**q,
|
||||
"pos": target,
|
||||
"premium_paid": paid,
|
||||
"margin_mode": row_mode,
|
||||
"premium_ccy": prem_ccy,
|
||||
}
|
||||
out = _attach_close_preview(cfg, ex, preview_row, sheets=target, premium_paid=paid)
|
||||
out["options_margin_mode"] = row_mode
|
||||
out["premium_ccy"] = prem_ccy
|
||||
return jsonify(out)
|
||||
mode, mode_note = _normalize_size_mode(mode)
|
||||
|
||||
# 币本位:报价预览走 USDT 预算→估币→张数,禁止再查 USDC
|
||||
try:
|
||||
from lib.options.options_margin_mode_lib import (
|
||||
is_coin_margin_mode,
|
||||
margin_mode_from_inst_id,
|
||||
)
|
||||
from lib.options.options_coin_open_lib import coin_budget_preview
|
||||
from lib.exchange.okx_options_lib import option_buy_liquidity_ok
|
||||
|
||||
if is_coin_margin_mode():
|
||||
ask = q.get("ask")
|
||||
ask_sz = q.get("ask_sz")
|
||||
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
|
||||
try:
|
||||
from lib.hedge_plan.okx_trade_mode_lib import block_standalone_open_by_mode_msg
|
||||
|
||||
mode_block = block_standalone_open_by_mode_msg()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "can_open": False, "msg": f"交易模式校验失败: {e}"})
|
||||
if mode_block:
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
"ok": True,
|
||||
"can_open": False,
|
||||
"msg": mode_block,
|
||||
"options_margin_mode": "coin",
|
||||
"sizing": {"ok": False, "msg": mode_block, "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0},
|
||||
}
|
||||
)
|
||||
if margin_mode_from_inst_id(inst_id) != "coin":
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
"ok": True,
|
||||
"can_open": False,
|
||||
"msg": "当前为币本位模式,请选择 ETH-USD / BTC-USD 合约(非 USD_UM)",
|
||||
"options_margin_mode": "coin",
|
||||
"sizing": {
|
||||
"ok": False,
|
||||
"msg": "合约非币本位",
|
||||
"sheets": 0,
|
||||
"eth_amount": 0.0,
|
||||
"total_premium": 0.0,
|
||||
},
|
||||
}
|
||||
)
|
||||
budget_info = coin_budget_preview(cfg, ex)
|
||||
if not can_open:
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
"ok": True,
|
||||
"can_open": False,
|
||||
"msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入",
|
||||
"options_margin_mode": "coin",
|
||||
"coin_budget": budget_info,
|
||||
"sizing": {
|
||||
"ok": False,
|
||||
"msg": block_msg or "暂无卖一深度,无法买入",
|
||||
"sheets": 0,
|
||||
"eth_amount": 0.0,
|
||||
"total_premium": 0.0,
|
||||
},
|
||||
}
|
||||
)
|
||||
if not budget_info.get("ok"):
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
"ok": True,
|
||||
"can_open": False,
|
||||
"msg": budget_info.get("msg") or "交易账户 USDT 不足",
|
||||
"options_margin_mode": "coin",
|
||||
"coin_budget": budget_info,
|
||||
"sizing": {
|
||||
"ok": False,
|
||||
"msg": budget_info.get("msg") or "交易账户 USDT 不足",
|
||||
"sheets": 0,
|
||||
"eth_amount": 0.0,
|
||||
"total_premium": 0.0,
|
||||
},
|
||||
}
|
||||
)
|
||||
idx = _safe_float(q.get("index_px")) or _safe_float(q.get("idxPx"))
|
||||
budget_usdt = float(budget_info["budget_usdt"])
|
||||
target_sheets = sheet_count if mode == "sheets" and sheet_count is not None else None
|
||||
if mode == "eth" and request.args.get("eth"):
|
||||
# 指定币量:按币量反推张数后再走统一规划
|
||||
try:
|
||||
eth_want = float(request.args.get("eth"))
|
||||
except (TypeError, ValueError):
|
||||
eth_want = 0.0
|
||||
if eth_want > 0 and float(ct_mult) > 0:
|
||||
import math
|
||||
|
||||
target_sheets = max(int(min_sz), int(math.floor(eth_want / float(ct_mult) + 1e-12)))
|
||||
from lib.options.options_margin_mode_lib import plan_coin_open_by_budget
|
||||
|
||||
sizing = plan_coin_open_by_budget(
|
||||
quote_per_unit=float(ask),
|
||||
ct_mult=float(ct_mult),
|
||||
min_sz=int(min_sz),
|
||||
budget_usdt=budget_usdt,
|
||||
index_px=float(idx or 0),
|
||||
ask_sz=ask_sz,
|
||||
target_sheets=target_sheets,
|
||||
)
|
||||
if sizing.get("ok"):
|
||||
sizing["premium_ccy"] = (inst_id.split("-")[0] if inst_id else "ETH").upper()
|
||||
sizing["est_coin"] = sizing.get("buy_coin")
|
||||
q = _attach_close_preview(
|
||||
cfg,
|
||||
ex,
|
||||
q,
|
||||
sheets=int(sizing.get("sheets") or 0),
|
||||
premium_paid=_open_premium_paid(cfg, inst_id),
|
||||
)
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
"can_open": bool(sizing.get("ok")),
|
||||
"quote_per_unit": ask,
|
||||
"premium_per_sheet": round(float(ask) * float(ct_mult), 8),
|
||||
"sizing": sizing,
|
||||
"mode": mode,
|
||||
"mode_note": mode_note,
|
||||
"options_margin_mode": "coin",
|
||||
"coin_budget": budget_info,
|
||||
"compound_full_enabled": _compound_full_enabled(),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"币本位报价失败: {e}"})
|
||||
|
||||
budget = cfg["trade_budget"]
|
||||
budget_cap = cfg["trade_budget"]
|
||||
available_usdc = None
|
||||
@@ -806,6 +989,48 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
profit_exit_mult = normalize_profit_exit_mult(data.get("profit_exit_mult"), default=1.0)
|
||||
if not inst_id:
|
||||
return jsonify({"ok": False, "msg": "缺少 inst_id"})
|
||||
try:
|
||||
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
||||
from lib.options.options_coin_open_lib import open_coin_option_buy_full
|
||||
|
||||
if is_coin_margin_mode():
|
||||
want_sheets = None
|
||||
if mode == "sheets":
|
||||
try:
|
||||
want_sheets = int(data.get("sheets") or 0) or None
|
||||
except (TypeError, ValueError):
|
||||
want_sheets = None
|
||||
elif mode == "eth":
|
||||
try:
|
||||
eth_want = float(data.get("eth") or 0)
|
||||
except (TypeError, ValueError):
|
||||
eth_want = 0.0
|
||||
if eth_want > 0:
|
||||
q0 = cfg["quote_option_contract"](ex, inst_id)
|
||||
ct0 = float((q0 or {}).get("ct_mult") or 0.01)
|
||||
min0 = int((q0 or {}).get("min_sz") or 1)
|
||||
if ct0 > 0:
|
||||
import math
|
||||
|
||||
want_sheets = max(min0, int(math.floor(eth_want / ct0 + 1e-12)))
|
||||
result = open_coin_option_buy_full(
|
||||
cfg,
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
signal_note=signal_note,
|
||||
target_index=target_index,
|
||||
profit_exit_enabled=profit_exit_enabled,
|
||||
profit_exit_mult=profit_exit_mult,
|
||||
target_sheets=want_sheets,
|
||||
)
|
||||
if result.get("ok"):
|
||||
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
|
||||
|
||||
invalidate_option_positions_cache()
|
||||
_mark_balances_stale(cfg)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"币本位开仓失败: {e}"})
|
||||
q = cfg["quote_option_contract"](ex, inst_id)
|
||||
if not q.get("ok"):
|
||||
return jsonify(q)
|
||||
@@ -1430,9 +1655,46 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
conn2.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from lib.options.options_coin_open_lib import maybe_sell_spot_after_close
|
||||
|
||||
spot_sell = maybe_sell_spot_after_close(cfg, ex, inst_id=inst_id, close_result=result)
|
||||
if spot_sell is not None:
|
||||
result = dict(result)
|
||||
result["spot_sell"] = spot_sell
|
||||
if spot_sell.get("bridge_status") == "pending_sell_spot":
|
||||
result["msg"] = (
|
||||
str(result.get("msg") or "平仓成功")
|
||||
+ ";但卖回 USDT 失败,请点「重试卖回」"
|
||||
)
|
||||
except Exception as e:
|
||||
result = dict(result)
|
||||
result["spot_sell"] = {"ok": False, "msg": str(e)}
|
||||
_mark_balances_stale(cfg)
|
||||
return jsonify(result)
|
||||
|
||||
@app.route("/api/options/spot-bridge/retry-sell", methods=["POST"])
|
||||
@lr
|
||||
def api_options_spot_bridge_retry_sell():
|
||||
"""币本位:重试把残留标的币市价卖回 USDT."""
|
||||
ex, err = _require_options_ex(cfg)
|
||||
if ex is None:
|
||||
return jsonify({"ok": False, "msg": err})
|
||||
data = request.get_json(silent=True) or {}
|
||||
underlying = (data.get("underlying") or cfg.get("default_underly") or "ETH").strip().upper()
|
||||
inst_id = (data.get("inst_id") or "").strip() or None
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
from lib.options.options_spot_bridge_lib import sell_residual_after_option_flat
|
||||
|
||||
out = sell_residual_after_option_flat(conn, ex, underlying=underlying, inst_id=inst_id)
|
||||
if out.get("ok"):
|
||||
_mark_balances_stale(cfg)
|
||||
return jsonify(out)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@app.route("/api/options/convert/quote", methods=["POST"])
|
||||
@lr
|
||||
def api_options_convert_quote():
|
||||
|
||||
Reference in New Issue
Block a user