9f3360e968
Co-authored-by: Cursor <cursoragent@cursor.com>
405 lines
14 KiB
Python
405 lines
14 KiB
Python
"""币本位单笔期权:买满 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,
|
|
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,
|
|
) -> dict[str, Any]:
|
|
"""先买满 USDT 预算对应的币,再按卖一尽量开满."""
|
|
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,
|
|
}
|
|
|
|
# 1) 买币(用买入前后可用差作为本轮币量,避免叠加原有现货)
|
|
coin_before = fetch_trading_coin_available(ex, underlying) or 0.0
|
|
buy = spot_market_buy_coin_with_usdt(ex, underlying=underlying, usdt_amount=budget_usdt)
|
|
if not buy.get("ok"):
|
|
return {"ok": False, "msg": f"现货买入 {underlying} 失败: {buy.get('msg')}", "budget": budget_info}
|
|
bridge_id = insert_bridge(
|
|
conn,
|
|
underlying=underlying,
|
|
status=BRIDGE_BOUGHT,
|
|
budget_usdt=budget_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,
|
|
}
|
|
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))
|
|
|
|
ct_mult = float(q.get("ct_mult") or 0.01)
|
|
min_sz = int(q.get("min_sz") or 1)
|
|
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}
|
|
|
|
sheets = int(sizing["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,
|
|
}
|
|
|
|
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",
|
|
)
|
|
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=budget_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
|