Files
crypto_okx/lib/options/options_cond_order_lib.py
T

922 lines
31 KiB
Python

"""中控条件开仓单:客户端本地监控/开仓/出场(中控只下发不盯盘)."""
from __future__ import annotations
import json
import sqlite3
import time
from datetime import datetime, timedelta, timezone
from typing import Any
# armed:待命穿越; triggering:已穿越等偏离区内开仓; open:持仓中;
# done:终态(leave_up/leave_down/cancel/timeout/closed)
ACTIVE_STATUSES = ("armed", "triggering", "open")
def _safe_float(v: Any) -> float | None:
if v is None or v == "":
return None
try:
return float(v)
except (TypeError, ValueError):
return None
def ensure_cond_order_table(conn: sqlite3.Connection) -> None:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_cond_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hub_order_id TEXT NOT NULL UNIQUE,
underlying TEXT NOT NULL,
opt_type TEXT NOT NULL,
trigger_side TEXT NOT NULL,
trigger_px REAL NOT NULL,
deviation_pct REAL NOT NULL,
min_expiry_hours REAL NOT NULL,
target_index REAL,
profit_exit_enabled INTEGER DEFAULT 0,
profit_exit_mult REAL DEFAULT 1.0,
pending_target_index REAL,
pending_profit_exit_enabled INTEGER,
pending_profit_exit_mult REAL,
exit_update_pending INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT 'armed',
end_reason TEXT,
last_index REAL,
crossed_at TEXT,
opened_inst_id TEXT,
opened_trade_id INTEGER,
opened_at TEXT,
message TEXT,
created_at TEXT,
updated_at TEXT,
ended_at TEXT
)
"""
)
try:
conn.execute("ALTER TABLE options_cond_orders ADD COLUMN opened_at TEXT")
except Exception:
pass
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_options_cond_orders_status
ON options_cond_orders(status)
"""
)
try:
conn.execute(
"""
UPDATE options_cond_orders
SET opened_at = updated_at
WHERE opened_at IS NULL
AND status = 'open'
AND updated_at IS NOT NULL
AND TRIM(updated_at) != ''
"""
)
except Exception:
pass
def _now() -> str:
try:
from lib.options.options_time_lib import options_app_now_str
return options_app_now_str()
except Exception:
return time.strftime("%Y-%m-%d %H:%M:%S")
def _notify_cond(
send_wechat: Any,
*,
title: str,
order: dict[str, Any],
account_label: str = "",
extra: list[str] | None = None,
) -> None:
if not callable(send_wechat) or not order:
return
try:
lines = [
title,
f"账户:{account_label or 'OKX期权'}",
f"单号:{order.get('hub_order_id') or '—'}",
f"标的:{order.get('underlying') or '—'} {order.get('opt_type') or '—'}",
f"触发价:{order.get('trigger_px') if order.get('trigger_px') is not None else '—'}",
]
for line in extra or []:
if line:
lines.append(line)
send_wechat("\n".join(lines))
except Exception:
pass
def next_thursday_2300(created_at: Any) -> datetime | None:
"""下发后第一个周四 23:00(业务时区).周四 23:00 之后下发的顺延到下周四."""
from lib.options.options_time_lib import options_app_tz, options_wall_clock_to_ms
ms = options_wall_clock_to_ms(created_at)
if ms is None:
return None
tz = options_app_tz()
dt = datetime.fromtimestamp(ms / 1000.0, tz=timezone.utc).astimezone(tz)
days = (3 - dt.weekday()) % 7
cand = dt.replace(hour=23, minute=0, second=0, microsecond=0) + timedelta(days=days)
if cand <= dt:
cand += timedelta(days=7)
return cand
def expire_untriggered_cond_orders(
conn: sqlite3.Connection,
*,
send_wechat: Any = None,
account_label: str = "",
now: datetime | None = None,
) -> int:
"""仅取消仍待命(未触发)的条件单;持仓/触发中不动."""
from lib.options.options_time_lib import options_app_tz
now_dt = now or datetime.now(options_app_tz())
n = 0
for order in list_cond_orders(conn, active_only=True):
if str(order.get("status") or "") != "armed":
continue
deadline = next_thursday_2300(order.get("created_at"))
if deadline is None or now_dt < deadline:
continue
_end_order(
conn,
order,
end_reason="timeout",
message="周四23:00未触发自动取消",
send_wechat=send_wechat,
account_label=account_label,
)
n += 1
return n
def _row_to_dict(row: sqlite3.Row | None) -> dict[str, Any] | None:
if row is None:
return None
d = dict(row)
d["profit_exit_enabled"] = bool(int(d.get("profit_exit_enabled") or 0))
d["exit_update_pending"] = bool(int(d.get("exit_update_pending") or 0))
if d.get("pending_profit_exit_enabled") is not None:
d["pending_profit_exit_enabled"] = bool(int(d["pending_profit_exit_enabled"]))
return d
def list_cond_orders(conn: sqlite3.Connection, *, active_only: bool = False) -> list[dict[str, Any]]:
ensure_cond_order_table(conn)
if active_only:
rows = conn.execute(
f"""
SELECT * FROM options_cond_orders
WHERE status IN ({",".join("?" * len(ACTIVE_STATUSES))})
ORDER BY id DESC
""",
ACTIVE_STATUSES,
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM options_cond_orders ORDER BY id DESC LIMIT 100"
).fetchall()
return [_row_to_dict(r) for r in rows if r]
def get_cond_order_by_hub_id(conn: sqlite3.Connection, hub_order_id: str) -> dict[str, Any] | None:
ensure_cond_order_table(conn)
row = conn.execute(
"SELECT * FROM options_cond_orders WHERE hub_order_id = ?",
((hub_order_id or "").strip(),),
).fetchone()
return _row_to_dict(row)
def has_active_cond_order(conn: sqlite3.Connection) -> bool:
ensure_cond_order_table(conn)
row = conn.execute(
f"""
SELECT 1 FROM options_cond_orders
WHERE status IN ({",".join("?" * len(ACTIVE_STATUSES))})
LIMIT 1
""",
ACTIVE_STATUSES,
).fetchone()
return row is not None
def cond_order_blocks_manual_open_msg(conn: sqlite3.Connection) -> str | None:
if has_active_cond_order(conn):
return "中控条件单占用中,禁止手动开仓"
return None
def machine_has_option_position(cfg: dict[str, Any], ex: Any) -> bool:
try:
raw = cfg["fetch_option_positions"](ex)
except Exception:
return False
if not raw:
return False
from lib.options.options_position_limit_lib import count_live_option_positions
return count_live_option_positions(raw) > 0
def create_cond_order(
conn: sqlite3.Connection,
payload: dict[str, Any],
*,
send_wechat: Any = None,
account_label: str = "",
) -> dict[str, Any]:
"""接收中控下发;同机仅允许一张活跃条件单."""
ensure_cond_order_table(conn)
hub_order_id = (payload.get("hub_order_id") or "").strip()
if not hub_order_id:
return {"ok": False, "msg": "缺少 hub_order_id"}
existing = get_cond_order_by_hub_id(conn, hub_order_id)
if existing:
return {"ok": True, "msg": "条件单已存在", "order": existing, "deduped": True}
if has_active_cond_order(conn):
return {"ok": False, "msg": "本机已有活跃条件单"}
underlying = (payload.get("underlying") or "ETH").strip().upper() or "ETH"
opt_type = (payload.get("opt_type") or "P").strip().upper()
if opt_type not in ("P", "C"):
return {"ok": False, "msg": "opt_type 须为 P 或 C"}
# 左侧单:看跌等上穿、看涨等下穿;忽略中控传入的 trigger_side
trigger_side = "gte" if opt_type == "P" else "lte"
trigger_px = _safe_float(payload.get("trigger_px"))
if trigger_px is None or trigger_px <= 0:
return {"ok": False, "msg": "trigger_px 无效"}
deviation_pct = _safe_float(payload.get("deviation_pct"))
if deviation_pct is None or deviation_pct <= 0:
return {"ok": False, "msg": "deviation_pct 须 > 0"}
min_expiry_hours = _safe_float(payload.get("min_expiry_hours"))
if min_expiry_hours is None or min_expiry_hours <= 0:
return {"ok": False, "msg": "min_expiry_hours 须 > 0"}
exit_cfg = payload.get("exit") if isinstance(payload.get("exit"), dict) else {}
target_index = _safe_float(exit_cfg.get("target_index") or payload.get("target_index"))
pe_en = bool(exit_cfg.get("profit_exit_enabled") or payload.get("profit_exit_enabled"))
from lib.options.options_profit_exit_lib import normalize_profit_exit_mult
pe_mult = normalize_profit_exit_mult(
exit_cfg.get("profit_exit_mult", payload.get("profit_exit_mult")), default=1.0
)
if target_index is not None and target_index <= 0:
return {"ok": False, "msg": "target_index 无效"}
if not pe_en and target_index is None:
return {"ok": False, "msg": "请至少配置目标位或翻倍出场"}
now = _now()
conn.execute(
"""
INSERT INTO options_cond_orders (
hub_order_id, underlying, opt_type, trigger_side, trigger_px, deviation_pct,
min_expiry_hours, target_index, profit_exit_enabled, profit_exit_mult,
status, message, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?, 'armed', ?, ?, ?)
""",
(
hub_order_id,
underlying,
opt_type,
trigger_side,
float(trigger_px),
float(deviation_pct),
float(min_expiry_hours),
float(target_index) if target_index is not None else None,
1 if pe_en else 0,
float(pe_mult),
"已接收,待命穿越",
now,
now,
),
)
conn.commit()
order = get_cond_order_by_hub_id(conn, hub_order_id)
_notify_cond(
send_wechat,
title="【OKX期权·条件单下发】",
order=order or {},
account_label=account_label,
extra=[
f"方向:{'Put 上穿' if opt_type == 'P' else 'Call 下穿'}",
f"偏离:{deviation_pct:g}%",
f"最短到期:{float(min_expiry_hours):g}h",
"状态:待命",
],
)
return {"ok": True, "msg": "条件单已 armed", "order": order}
def cancel_cond_order(
conn: sqlite3.Connection,
hub_order_id: str,
*,
send_wechat: Any = None,
account_label: str = "",
) -> dict[str, Any]:
order = get_cond_order_by_hub_id(conn, hub_order_id)
if not order:
return {"ok": False, "msg": "条件单不存在"}
if order.get("status") not in ACTIVE_STATUSES:
return {"ok": True, "msg": "条件单已结束", "order": order}
if order.get("status") == "open":
return {"ok": False, "msg": "已开仓,请用平仓/出场结束;取消仅适用于未成交条件单"}
_end_order(
conn,
order,
end_reason="cancel",
message="中控取消",
send_wechat=send_wechat,
account_label=account_label,
)
return {"ok": True, "msg": "已取消", "order": get_cond_order_by_hub_id(conn, hub_order_id)}
def update_cond_order_exit(
conn: sqlite3.Connection,
hub_order_id: str,
*,
target_index: Any = None,
profit_exit_enabled: Any = None,
profit_exit_mult: Any = None,
) -> dict[str, Any]:
"""成交后改出场;若正在平仓挂单则先记 pending,本轮结束后再套用."""
order = get_cond_order_by_hub_id(conn, hub_order_id)
if not order:
return {"ok": False, "msg": "条件单不存在"}
if order.get("status") != "open":
return {"ok": False, "msg": "仅持仓中的条件单可改出场"}
from lib.options.options_profit_exit_lib import normalize_profit_exit_mult
new_target = _safe_float(target_index) if target_index is not None else order.get("target_index")
if target_index is not None and str(target_index).strip() == "":
new_target = None
if new_target is not None and new_target <= 0:
return {"ok": False, "msg": "target_index 无效"}
if profit_exit_enabled is None:
pe_en = bool(order.get("profit_exit_enabled"))
else:
pe_en = bool(profit_exit_enabled)
pe_mult = normalize_profit_exit_mult(
profit_exit_mult if profit_exit_mult is not None else order.get("profit_exit_mult"),
default=1.0,
)
if not pe_en and new_target is None:
return {"ok": False, "msg": "请至少配置目标位或翻倍出场"}
closing = _exit_close_in_progress(conn, str(order.get("opened_inst_id") or ""))
now = _now()
if closing:
conn.execute(
"""
UPDATE options_cond_orders SET
pending_target_index = ?,
pending_profit_exit_enabled = ?,
pending_profit_exit_mult = ?,
exit_update_pending = 1,
message = ?,
updated_at = ?
WHERE hub_order_id = ?
""",
(
float(new_target) if new_target is not None else None,
1 if pe_en else 0,
float(pe_mult),
"出场已更新,等待本轮平仓挂单结束后生效",
now,
hub_order_id,
),
)
conn.commit()
return {
"ok": True,
"msg": "已记录,本轮平仓结束后生效",
"deferred": True,
"order": get_cond_order_by_hub_id(conn, hub_order_id),
}
_apply_exit_to_position(
conn,
order,
target_index=new_target,
profit_exit_enabled=pe_en,
profit_exit_mult=pe_mult,
)
conn.execute(
"""
UPDATE options_cond_orders SET
target_index = ?,
profit_exit_enabled = ?,
profit_exit_mult = ?,
exit_update_pending = 0,
pending_target_index = NULL,
pending_profit_exit_enabled = NULL,
pending_profit_exit_mult = NULL,
message = ?,
updated_at = ?
WHERE hub_order_id = ?
""",
(
float(new_target) if new_target is not None else None,
1 if pe_en else 0,
float(pe_mult),
"出场已更新并生效",
now,
hub_order_id,
),
)
conn.commit()
return {"ok": True, "msg": "出场已更新", "order": get_cond_order_by_hub_id(conn, hub_order_id)}
def _exit_close_in_progress(conn: sqlite3.Connection, inst_id: str) -> bool:
if not inst_id:
return False
try:
from lib.options.options_target_lib import list_closing_targets
for t in list_closing_targets(conn):
if str(t.get("inst_id") or "") == inst_id:
return True
except Exception:
pass
try:
row = conn.execute(
"""
SELECT profit_exit_state FROM options_trades
WHERE inst_id = ? AND status = 'open'
ORDER BY id ASC LIMIT 1
""",
(inst_id,),
).fetchone()
if row and str(row["profit_exit_state"] or "") == "closing":
return True
except Exception:
pass
return False
def _apply_exit_to_position(
conn: sqlite3.Connection,
order: dict[str, Any],
*,
target_index: float | None,
profit_exit_enabled: bool,
profit_exit_mult: float,
) -> None:
inst_id = str(order.get("opened_inst_id") or "").strip()
if not inst_id:
return
from lib.options.options_profit_exit_lib import set_profit_exit
from lib.options.options_target_lib import cancel_target_monitor, upsert_target_monitor
set_profit_exit(conn, inst_id=inst_id, enabled=profit_exit_enabled, mult=profit_exit_mult)
if target_index is not None:
upsert_target_monitor(
conn,
inst_id=inst_id,
target_index=float(target_index),
underlying=str(order.get("underlying") or ""),
opt_type=str(order.get("opt_type") or ""),
)
else:
cancel_target_monitor(conn, inst_id=inst_id)
conn.commit()
def _end_order(
conn: sqlite3.Connection,
order: dict[str, Any],
*,
end_reason: str,
message: str,
send_wechat: Any = None,
account_label: str = "",
index_px: float | None = None,
) -> None:
now = _now()
conn.execute(
"""
UPDATE options_cond_orders SET
status = 'done',
end_reason = ?,
message = ?,
updated_at = ?,
ended_at = ?
WHERE hub_order_id = ?
""",
(end_reason, message, now, now, order["hub_order_id"]),
)
conn.commit()
if callable(send_wechat):
try:
reason_map = {
"leave_up": "上破偏离区结束",
"leave_down": "下破偏离区结束",
"cancel": "中控取消",
"timeout": "周四23:00未触发自动取消",
"closed": "出场完成",
}
_notify_cond(
send_wechat,
title="【OKX期权·条件单结束】",
order=order,
account_label=account_label,
extra=[
f"原因:{reason_map.get(end_reason, end_reason)}",
f"说明:{message}",
]
+ ([f"当前指数:{index_px:g}"] if index_px is not None else []),
)
except Exception:
pass
def band_bounds(trigger_px: float, deviation_pct: float) -> tuple[float, float]:
d = max(0.0, float(deviation_pct)) / 100.0
lo = float(trigger_px) * (1.0 - d)
hi = float(trigger_px) * (1.0 + d)
return lo, hi
def pick_nearest_itm_contract(
cfg: dict[str, Any],
ex: Any,
*,
underlying: str,
opt_type: str,
index_px: float,
min_expiry_hours: float,
) -> dict[str, Any] | None:
"""按当前指数选最近一档实值;到期 ≥ min_expiry_hours."""
from lib.exchange.okx_options_lib import build_option_chain, option_buy_liquidity_ok
from lib.options.options_pricing_lib import option_moneyness
max_dte_days = max(float(min_expiry_hours) / 24.0 + 14.0, 14.0)
# 放宽实值距离:取全部 ITM,再按距离排序
chain = build_option_chain(
ex,
underlying,
max_dte_days=max_dte_days,
itm_only=False,
itm_max_dist_usd=1e12,
index_px=index_px,
)
min_ms = time.time() * 1000 + float(min_expiry_hours) * 3600 * 1000
want = (opt_type or "P").upper()
candidates: list[tuple[float, dict[str, Any]]] = []
expiries = chain.get("expiries") or []
# build_option_chain 返回 list[{exp_time, contracts}]
if isinstance(expiries, dict):
exp_iter = [{"exp_time": k, "contracts": v} for k, v in expiries.items()]
else:
exp_iter = list(expiries)
for bucket in exp_iter:
try:
exp_ms = int(bucket.get("exp_time") or 0)
except (TypeError, ValueError):
exp_ms = 0
if exp_ms and exp_ms < min_ms:
continue
for c in bucket.get("contracts") or []:
if str(c.get("opt_type") or "").upper() != want:
continue
strike = _safe_float(c.get("strike"))
if strike is None:
continue
mny = option_moneyness(opt_type=want, strike=strike, index_px=index_px)
if mny != "itm":
continue
dist = abs(float(strike) - float(index_px))
candidates.append((dist, c))
if not candidates:
return None
candidates.sort(key=lambda x: (x[0], str(x[1].get("inst_id") or "")))
# 优先有卖一深度的最近档
for _dist, c in candidates[:12]:
inst = str(c.get("inst_id") or "")
if not inst:
continue
try:
q = cfg["quote_option_contract"](ex, inst)
except Exception:
continue
ask = q.get("ask")
ask_sz = q.get("ask_sz")
ok, _ = option_buy_liquidity_ok(ask, ask_sz)
if ok:
out = dict(c)
out["quote"] = q
return out
# 深度都不够时仍返回最近档,由上层继续等
best = dict(candidates[0][1])
return best
def _fetch_index(cfg: dict[str, Any], ex: Any, underlying: str) -> float | None:
from lib.exchange.okx_options_lib import fetch_index_price
uly = f"{(underlying or 'ETH').upper()}-USD"
try:
return fetch_index_price(ex, uly)
except Exception:
return None
def _try_open_full(
cfg: dict[str, Any],
ex: Any,
*,
inst_id: str,
order: dict[str, Any],
) -> dict[str, Any]:
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
note = f"中控条件单:{order.get('hub_order_id')}"
target_index = _safe_float(order.get("target_index"))
pe_en = bool(order.get("profit_exit_enabled"))
pe_mult = float(order.get("profit_exit_mult") or 1.0)
if is_coin_margin_mode():
return open_coin_option_buy_full(
cfg,
ex,
inst_id=inst_id,
signal_note=note,
target_index=target_index,
profit_exit_enabled=pe_en,
profit_exit_mult=pe_mult,
target_sheets=None,
)
return {
"ok": False,
"msg": "条件单开仓当前仅支持币本位客户端规则(买币→开仓)",
"need_depth": False,
}
def run_cond_orders_tick(
conn: sqlite3.Connection,
cfg: dict[str, Any],
*,
send_wechat: Any = None,
account_label: str = "",
) -> None:
"""监控循环单步:周四未触发取消 / 穿越 / 偏离区 / 开仓 / 出场收尾."""
ensure_cond_order_table(conn)
expire_untriggered_cond_orders(
conn, send_wechat=send_wechat, account_label=account_label
)
ex = cfg.get("exchange_options")
if ex is None:
return
orders = list_cond_orders(conn, active_only=True)
if not orders:
return
for order in orders:
try:
_tick_one(
conn,
cfg,
ex,
order,
send_wechat=send_wechat,
account_label=account_label,
)
except Exception as e:
try:
conn.execute(
"UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?",
(f"监控异常: {e}"[:200], _now(), order["hub_order_id"]),
)
conn.commit()
except Exception:
pass
def _tick_one(
conn: sqlite3.Connection,
cfg: dict[str, Any],
ex: Any,
order: dict[str, Any],
*,
send_wechat: Any,
account_label: str,
) -> None:
status = str(order.get("status") or "")
underlying = str(order.get("underlying") or "ETH")
idx = _fetch_index(cfg, ex, underlying)
if idx is None or idx <= 0:
return
last = _safe_float(order.get("last_index"))
conn.execute(
"UPDATE options_cond_orders SET last_index=?, updated_at=? WHERE hub_order_id=?",
(float(idx), _now(), order["hub_order_id"]),
)
conn.commit()
trigger_px = float(order["trigger_px"])
lo, hi = band_bounds(trigger_px, float(order["deviation_pct"]))
side = str(order.get("trigger_side") or "gte")
if status == "armed":
crossed = False
if side == "gte" and last is not None and last < trigger_px and idx >= trigger_px:
crossed = True
elif side == "lte" and last is not None and last > trigger_px and idx <= trigger_px:
crossed = True
# 首次无 last:只记录,等下一跳再判穿越(避免挂单时已在上方立刻开)
if last is None:
return
if crossed:
conn.execute(
"""
UPDATE options_cond_orders SET status='triggering', crossed_at=?, message=?, updated_at=?
WHERE hub_order_id=?
""",
(_now(), f"已穿越 {trigger_px:g},等待偏离区内开仓", _now(), order["hub_order_id"]),
)
conn.commit()
order["status"] = "triggering"
status = "triggering"
else:
return
if status == "triggering":
if idx > hi:
_end_order(
conn,
order,
end_reason="leave_up",
message=f"指数 {idx:g} 上破偏离上沿 {hi:g}",
send_wechat=send_wechat,
account_label=account_label,
index_px=idx,
)
return
if idx < lo:
_end_order(
conn,
order,
end_reason="leave_down",
message=f"指数 {idx:g} 下破偏离下沿 {lo:g}",
send_wechat=send_wechat,
account_label=account_label,
index_px=idx,
)
return
# 区内:有仓则不再开
if machine_has_option_position(cfg, ex):
conn.execute(
"UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?",
("偏离区内但已有持仓,等待", _now(), order["hub_order_id"]),
)
conn.commit()
return
picked = pick_nearest_itm_contract(
cfg,
ex,
underlying=underlying,
opt_type=str(order.get("opt_type") or "P"),
index_px=idx,
min_expiry_hours=float(order.get("min_expiry_hours") or 24),
)
if not picked or not picked.get("inst_id"):
conn.execute(
"UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?",
("偏离区内暂无合适实值合约", _now(), order["hub_order_id"]),
)
conn.commit()
return
inst_id = str(picked["inst_id"])
# 深度检查
from lib.exchange.okx_options_lib import option_buy_liquidity_ok
q = picked.get("quote") or cfg["quote_option_contract"](ex, inst_id)
ok_liq, msg_liq = option_buy_liquidity_ok(q.get("ask"), q.get("ask_sz"))
if not ok_liq:
conn.execute(
"UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?",
(f"深度不足继续等待: {msg_liq or '无卖一'}", _now(), order["hub_order_id"]),
)
conn.commit()
return
result = _try_open_full(cfg, ex, inst_id=inst_id, order=order)
if not result.get("ok"):
conn.execute(
"UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?",
(f"开仓未成继续等: {result.get('msg') or '失败'}"[:180], _now(), order["hub_order_id"]),
)
conn.commit()
return
trade_id = result.get("trade_id")
conn.execute(
"""
UPDATE options_cond_orders SET
status='open', opened_inst_id=?, opened_trade_id=?,
opened_at=COALESCE(opened_at, ?),
message=?, updated_at=?
WHERE hub_order_id=?
""",
(
inst_id,
int(trade_id) if trade_id is not None else None,
_now(),
f"已开仓 {inst_id}",
_now(),
order["hub_order_id"],
),
)
conn.commit()
opened = get_cond_order_by_hub_id(conn, order["hub_order_id"]) or order
_notify_cond(
send_wechat,
title="【OKX期权·条件单开仓】",
order=opened,
account_label=account_label,
extra=[
f"合约:{inst_id}",
f"说明:已开仓 {inst_id}",
],
)
order2 = opened
order2["opened_inst_id"] = inst_id
_apply_exit_to_position(
conn,
order2,
target_index=_safe_float(order2.get("target_index")),
profit_exit_enabled=bool(order2.get("profit_exit_enabled")),
profit_exit_mult=float(order2.get("profit_exit_mult") or 1.0),
)
return
if status == "open":
# 延迟出场更新
if order.get("exit_update_pending") and not _exit_close_in_progress(
conn, str(order.get("opened_inst_id") or "")
):
pe_en = order.get("pending_profit_exit_enabled")
if pe_en is None:
pe_en = order.get("profit_exit_enabled")
_apply_exit_to_position(
conn,
order,
target_index=_safe_float(order.get("pending_target_index")),
profit_exit_enabled=bool(pe_en),
profit_exit_mult=float(
order.get("pending_profit_exit_mult")
if order.get("pending_profit_exit_mult") is not None
else order.get("profit_exit_mult")
or 1.0
),
)
conn.execute(
"""
UPDATE options_cond_orders SET
target_index = pending_target_index,
profit_exit_enabled = COALESCE(pending_profit_exit_enabled, profit_exit_enabled),
profit_exit_mult = COALESCE(pending_profit_exit_mult, profit_exit_mult),
exit_update_pending = 0,
pending_target_index = NULL,
pending_profit_exit_enabled = NULL,
pending_profit_exit_mult = NULL,
message = ?,
updated_at = ?
WHERE hub_order_id = ?
""",
("延迟出场已生效", _now(), order["hub_order_id"]),
)
conn.commit()
inst_id = str(order.get("opened_inst_id") or "")
if not inst_id:
return
if not machine_has_option_position(cfg, ex):
# 确认该合约已平
raw = cfg["fetch_option_positions"](ex) or []
still = any(str(p.get("instId") or "") == inst_id for p in raw)
if still:
return
_end_order(
conn,
order,
end_reason="closed",
message="持仓已平,条件单完成",
send_wechat=send_wechat,
account_label=account_label,
index_px=idx,
)