1835a79698
Co-authored-by: Cursor <cursoragent@cursor.com>
1000 lines
33 KiB
Python
1000 lines
33 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"
|
|
try:
|
|
from lib.options.options_underly_lib import resolve_default_underly, underly_not_allowed_msg
|
|
|
|
if not (payload.get("underlying") or "").strip():
|
|
underlying = resolve_default_underly()
|
|
uly_block = underly_not_allowed_msg(underlying)
|
|
if uly_block:
|
|
return {"ok": False, "msg": uly_block}
|
|
except Exception:
|
|
pass
|
|
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_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)
|
|
# USDC 期权开仓已下线,条件单仅走币本位
|
|
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,
|
|
)
|
|
|
|
|
|
def run_cond_orders_tick(
|
|
conn_or_get_db: Any,
|
|
cfg: dict[str, Any],
|
|
*,
|
|
send_wechat: Any = None,
|
|
account_label: str = "",
|
|
) -> None:
|
|
"""监控循环单步:周四未触发取消 / 穿越 / 偏离区 / 开仓 / 出场收尾.
|
|
|
|
推荐传 get_db:开仓前会关闭连接,避免与买币/等待成交嵌套锁库。
|
|
"""
|
|
get_db = conn_or_get_db if callable(conn_or_get_db) else None
|
|
if get_db is None:
|
|
conn = conn_or_get_db
|
|
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
|
|
for order in list_cond_orders(conn, active_only=True):
|
|
try:
|
|
_tick_one(
|
|
conn,
|
|
cfg,
|
|
ex,
|
|
order,
|
|
send_wechat=send_wechat,
|
|
account_label=account_label,
|
|
get_db=None,
|
|
)
|
|
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
|
|
return
|
|
|
|
ex: Any = None
|
|
orders: list[dict[str, Any]] = []
|
|
conn = get_db()
|
|
try:
|
|
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 not None:
|
|
orders = list_cond_orders(conn, active_only=True)
|
|
try:
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
try:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
if ex is None or not orders:
|
|
return
|
|
for order in orders:
|
|
conn = get_db()
|
|
try:
|
|
_tick_one(
|
|
conn,
|
|
cfg,
|
|
ex,
|
|
order,
|
|
send_wechat=send_wechat,
|
|
account_label=account_label,
|
|
get_db=get_db,
|
|
)
|
|
try:
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
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
|
|
finally:
|
|
try:
|
|
conn.close()
|
|
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,
|
|
get_db: Any = None,
|
|
) -> 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
|
|
# 开仓(买币/等成交)前释放本连接,避免嵌套锁库
|
|
try:
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
hub_id = order["hub_order_id"]
|
|
if get_db is not None:
|
|
try:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
conn = None
|
|
result = _try_open_full(cfg, ex, inst_id=inst_id, order=order)
|
|
if get_db is not None:
|
|
conn = get_db()
|
|
ensure_cond_order_table(conn)
|
|
assert conn is not None
|
|
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(), hub_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(),
|
|
hub_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,
|
|
)
|