"""币本位单笔期权:买满 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