feat中控市价单:实例按第一档实值与杠杆立即全仓开仓。
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -549,6 +549,77 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@app.route("/api/hub/options/market_orders", methods=["GET"])
|
||||
def api_hub_options_market_orders_list():
|
||||
if not hub_bridge_token_ok(request.headers):
|
||||
return jsonify({"ok": False, "msg": "未授权", "version": 1}), 401
|
||||
hub_ctx = app.config.get("HUB_CTX") or {}
|
||||
cfg = hub_ctx.get("options_cfg")
|
||||
if not isinstance(cfg, dict):
|
||||
return jsonify({"ok": False, "msg": "期权未就绪", "version": 1}), 503
|
||||
from lib.options.options_db import init_options_tables
|
||||
from lib.options.options_market_order_lib import list_market_orders
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
orders = list_market_orders(conn, active_only=False)
|
||||
return jsonify({"ok": True, "version": 1, "orders": orders})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@app.route("/api/hub/options/market_orders", methods=["POST"])
|
||||
def api_hub_options_market_orders_create():
|
||||
if not hub_bridge_token_ok(request.headers):
|
||||
return jsonify({"ok": False, "msg": "未授权", "version": 1}), 401
|
||||
hub_ctx = app.config.get("HUB_CTX") or {}
|
||||
cfg = hub_ctx.get("options_cfg")
|
||||
if not isinstance(cfg, dict):
|
||||
return jsonify({"ok": False, "msg": "期权未就绪", "version": 1}), 503
|
||||
from lib.options.options_market_order_lib import execute_market_open
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
ex = cfg.get("exchange_options")
|
||||
out = execute_market_open(
|
||||
cfg,
|
||||
ex,
|
||||
data,
|
||||
get_db=cfg.get("get_db"),
|
||||
send_wechat=cfg.get("send_wechat"),
|
||||
account_label=str(cfg.get("account_label") or ""),
|
||||
)
|
||||
if not isinstance(out, dict):
|
||||
out = {"ok": False, "msg": "市价开仓返回异常"}
|
||||
out["version"] = 1
|
||||
return jsonify(out)
|
||||
|
||||
@app.route("/api/hub/options/market_orders/<hub_order_id>/exit", methods=["POST"])
|
||||
def api_hub_options_market_orders_exit(hub_order_id: str):
|
||||
if not hub_bridge_token_ok(request.headers):
|
||||
return jsonify({"ok": False, "msg": "未授权", "version": 1}), 401
|
||||
hub_ctx = app.config.get("HUB_CTX") or {}
|
||||
cfg = hub_ctx.get("options_cfg")
|
||||
if not isinstance(cfg, dict):
|
||||
return jsonify({"ok": False, "msg": "期权未就绪", "version": 1}), 503
|
||||
from lib.options.options_db import init_options_tables
|
||||
from lib.options.options_market_order_lib import update_market_order_exit
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
out = update_market_order_exit(
|
||||
conn,
|
||||
hub_order_id,
|
||||
target_index=data.get("target_index"),
|
||||
profit_exit_enabled=data.get("profit_exit_enabled"),
|
||||
profit_exit_mult=data.get("profit_exit_mult"),
|
||||
)
|
||||
out["version"] = 1
|
||||
return jsonify(out)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@app.route("/api/hub/version", methods=["GET"])
|
||||
def hub_version():
|
||||
"""中控探测客户端当前 git 版本/在线."""
|
||||
|
||||
@@ -561,6 +561,22 @@ def band_bounds(trigger_px: float, deviation_pct: float) -> tuple[float, float]:
|
||||
return lo, hi
|
||||
|
||||
|
||||
def _chain_contract_leverage(c: dict[str, Any], *, index_px: float) -> float | None:
|
||||
"""用链上报价估合约杠杆(币本位≈1/ask)."""
|
||||
from lib.options.options_pricing_lib import equivalent_contract_leverage
|
||||
|
||||
ask = _safe_float(c.get("ask"))
|
||||
ct_mult = _safe_float(c.get("ct_mult")) or 0.01
|
||||
if ask is None or ask <= 0 or ct_mult <= 0:
|
||||
return None
|
||||
return equivalent_contract_leverage(
|
||||
index_px=index_px,
|
||||
eth_amount=float(ct_mult),
|
||||
total_premium=float(ask) * float(ct_mult),
|
||||
margin_mode="coin",
|
||||
)
|
||||
|
||||
|
||||
def pick_nearest_itm_contract(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
@@ -569,8 +585,12 @@ def pick_nearest_itm_contract(
|
||||
opt_type: str,
|
||||
index_px: float,
|
||||
min_expiry_hours: float,
|
||||
target_leverage: float | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""按当前指数选最近一档实值;到期 ≥ min_expiry_hours."""
|
||||
"""按当前指数选最近一档实值;到期 ≥ min_expiry_hours.
|
||||
|
||||
target_leverage 有值时:仅在第一档实值(最近行权距)内,选杠杆最接近目标的合约。
|
||||
"""
|
||||
from lib.exchange.okx_options_lib import build_option_chain, option_buy_liquidity_ok
|
||||
from lib.options.options_pricing_lib import option_moneyness
|
||||
|
||||
@@ -614,6 +634,59 @@ def pick_nearest_itm_contract(
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda x: (x[0], str(x[1].get("inst_id") or "")))
|
||||
|
||||
want_lev = _safe_float(target_leverage)
|
||||
if want_lev is not None and want_lev > 0:
|
||||
min_dist = candidates[0][0]
|
||||
rung = [c for d, c in candidates if abs(float(d) - float(min_dist)) <= 1e-9]
|
||||
scored: list[tuple[float, int, dict[str, Any], dict[str, Any] | None]] = []
|
||||
for c in rung:
|
||||
inst = str(c.get("inst_id") or "")
|
||||
if not inst:
|
||||
continue
|
||||
q: dict[str, Any] | None = None
|
||||
try:
|
||||
q = cfg["quote_option_contract"](ex, inst)
|
||||
except Exception:
|
||||
q = None
|
||||
ask = (q or {}).get("ask") if q else c.get("ask")
|
||||
ask_sz = (q or {}).get("ask_sz") if q else c.get("ask_sz")
|
||||
ok_liq, _ = option_buy_liquidity_ok(ask, ask_sz)
|
||||
lev = None
|
||||
if q and ask is not None:
|
||||
ct = _safe_float((q or {}).get("ct_mult")) or _safe_float(c.get("ct_mult")) or 0.01
|
||||
from lib.options.options_pricing_lib import equivalent_contract_leverage
|
||||
|
||||
lev = equivalent_contract_leverage(
|
||||
index_px=index_px,
|
||||
eth_amount=float(ct),
|
||||
total_premium=float(ask) * float(ct),
|
||||
margin_mode="coin",
|
||||
)
|
||||
if lev is None:
|
||||
lev = _chain_contract_leverage(c, index_px=index_px)
|
||||
if lev is None:
|
||||
continue
|
||||
scored.append((abs(float(lev) - float(want_lev)), 0 if ok_liq else 1, c, q))
|
||||
if scored:
|
||||
scored.sort(key=lambda x: (x[0], x[1], str(x[2].get("inst_id") or "")))
|
||||
best_c = dict(scored[0][2])
|
||||
if scored[0][3]:
|
||||
best_c["quote"] = scored[0][3]
|
||||
best_c["picked_leverage"] = _chain_contract_leverage(best_c, index_px=index_px)
|
||||
if scored[0][3] and scored[0][3].get("ask") is not None:
|
||||
ct = _safe_float(scored[0][3].get("ct_mult")) or _safe_float(best_c.get("ct_mult")) or 0.01
|
||||
from lib.options.options_pricing_lib import equivalent_contract_leverage
|
||||
|
||||
best_c["picked_leverage"] = equivalent_contract_leverage(
|
||||
index_px=index_px,
|
||||
eth_amount=float(ct),
|
||||
total_premium=float(scored[0][3]["ask"]) * float(ct),
|
||||
margin_mode="coin",
|
||||
)
|
||||
return best_c
|
||||
# 杠杆估不出时退回第一档最近合约
|
||||
|
||||
# 优先有卖一深度的最近档
|
||||
for _dist, c in candidates[:12]:
|
||||
inst = str(c.get("inst_id") or "")
|
||||
|
||||
@@ -152,6 +152,12 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
||||
ensure_cond_order_table(conn)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from lib.options.options_market_order_lib import ensure_market_order_table
|
||||
|
||||
ensure_market_order_table(conn)
|
||||
except Exception:
|
||||
pass
|
||||
init_options_review_tables(conn)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,651 @@
|
||||
"""中控市价开仓:按条件选第一档实值+杠杆接近,立即全仓复利开仓."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
ACTIVE_STATUSES = ("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_market_order_table(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS options_market_orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hub_order_id TEXT NOT NULL UNIQUE,
|
||||
underlying TEXT NOT NULL,
|
||||
opt_type TEXT NOT NULL,
|
||||
min_expiry_hours REAL NOT NULL,
|
||||
target_leverage REAL NOT NULL,
|
||||
picked_leverage REAL,
|
||||
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 'open',
|
||||
end_reason TEXT,
|
||||
opened_inst_id TEXT,
|
||||
opened_trade_id INTEGER,
|
||||
opened_at TEXT,
|
||||
message TEXT,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
ended_at TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_options_market_orders_status
|
||||
ON options_market_orders(status)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
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_mkt(
|
||||
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('target_leverage') if order.get('target_leverage') is not None else '—'}",
|
||||
]
|
||||
for line in extra or []:
|
||||
if line:
|
||||
lines.append(line)
|
||||
send_wechat("\n".join(lines))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
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_market_orders(conn: sqlite3.Connection, *, active_only: bool = False) -> list[dict[str, Any]]:
|
||||
ensure_market_order_table(conn)
|
||||
if active_only:
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT * FROM options_market_orders
|
||||
WHERE status IN ({",".join("?" * len(ACTIVE_STATUSES))})
|
||||
ORDER BY id DESC
|
||||
""",
|
||||
ACTIVE_STATUSES,
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM options_market_orders ORDER BY id DESC LIMIT 100"
|
||||
).fetchall()
|
||||
return [_row_to_dict(r) for r in rows if r]
|
||||
|
||||
|
||||
def get_market_order_by_hub_id(conn: sqlite3.Connection, hub_order_id: str) -> dict[str, Any] | None:
|
||||
ensure_market_order_table(conn)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM options_market_orders WHERE hub_order_id = ?",
|
||||
((hub_order_id or "").strip(),),
|
||||
).fetchone()
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def has_active_market_order(conn: sqlite3.Connection) -> bool:
|
||||
ensure_market_order_table(conn)
|
||||
row = conn.execute(
|
||||
f"""
|
||||
SELECT 1 FROM options_market_orders
|
||||
WHERE status IN ({",".join("?" * len(ACTIVE_STATUSES))})
|
||||
LIMIT 1
|
||||
""",
|
||||
ACTIVE_STATUSES,
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
|
||||
def execute_market_open(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
get_db: Any = None,
|
||||
send_wechat: Any = None,
|
||||
account_label: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""立即选约并全仓复利开仓;成功后写入市价单记录."""
|
||||
from lib.options.options_cond_order_lib import (
|
||||
_apply_exit_to_position,
|
||||
_fetch_index,
|
||||
has_active_cond_order,
|
||||
machine_has_option_position,
|
||||
pick_nearest_itm_contract,
|
||||
)
|
||||
from lib.options.options_db import init_options_tables
|
||||
from lib.options.options_position_limit_lib import option_position_limit_block_msg
|
||||
from lib.options.options_profit_exit_lib import normalize_profit_exit_mult
|
||||
|
||||
hub_order_id = (payload.get("hub_order_id") or "").strip()
|
||||
if not hub_order_id:
|
||||
return {"ok": False, "msg": "缺少 hub_order_id"}
|
||||
|
||||
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"}
|
||||
|
||||
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"}
|
||||
|
||||
target_leverage = _safe_float(payload.get("target_leverage") or payload.get("leverage"))
|
||||
if target_leverage is None or target_leverage <= 0:
|
||||
return {"ok": False, "msg": "杠杆倍数须 > 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"))
|
||||
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": "请至少配置目标位或翻倍出场"}
|
||||
|
||||
if ex is None:
|
||||
return {"ok": False, "msg": "期权交易所未就绪"}
|
||||
|
||||
db_fn = get_db or (cfg.get("get_db") if isinstance(cfg, dict) else None)
|
||||
if not callable(db_fn):
|
||||
return {"ok": False, "msg": "缺少 get_db"}
|
||||
|
||||
conn = db_fn()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
existing = get_market_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": "本机已有活跃条件单"}
|
||||
if has_active_market_order(conn):
|
||||
return {"ok": False, "msg": "本机已有活跃市价单"}
|
||||
if machine_has_option_position(cfg, ex):
|
||||
return {"ok": False, "msg": "本机已有期权仓,拒绝市价单"}
|
||||
limit_msg = option_position_limit_block_msg(
|
||||
ex,
|
||||
opening_inst_id="",
|
||||
fetch_positions=cfg.get("fetch_option_positions"),
|
||||
)
|
||||
if limit_msg:
|
||||
return {"ok": False, "msg": limit_msg}
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
idx = _fetch_index(cfg, ex, underlying)
|
||||
if idx is None or idx <= 0:
|
||||
return {"ok": False, "msg": "指数价获取失败"}
|
||||
|
||||
picked = pick_nearest_itm_contract(
|
||||
cfg,
|
||||
ex,
|
||||
underlying=underlying,
|
||||
opt_type=opt_type,
|
||||
index_px=float(idx),
|
||||
min_expiry_hours=float(min_expiry_hours),
|
||||
target_leverage=float(target_leverage),
|
||||
)
|
||||
if not picked or not picked.get("inst_id"):
|
||||
return {"ok": False, "msg": "暂无合适的第一档实值合约"}
|
||||
|
||||
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:
|
||||
return {"ok": False, "msg": f"深度不足: {msg_liq or '无卖一'}"}
|
||||
|
||||
from lib.options.options_coin_open_lib import open_coin_option_buy_full
|
||||
|
||||
note = f"中控市价单:{hub_order_id}"
|
||||
result = 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,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": result.get("msg") or "开仓失败",
|
||||
"picked_inst_id": inst_id,
|
||||
"picked_leverage": picked.get("picked_leverage"),
|
||||
}
|
||||
|
||||
trade_id = result.get("trade_id")
|
||||
picked_lev = _safe_float(picked.get("picked_leverage"))
|
||||
now = _now()
|
||||
order_stub = {
|
||||
"hub_order_id": hub_order_id,
|
||||
"underlying": underlying,
|
||||
"opt_type": opt_type,
|
||||
"target_leverage": float(target_leverage),
|
||||
"opened_inst_id": inst_id,
|
||||
"target_index": target_index,
|
||||
"profit_exit_enabled": pe_en,
|
||||
"profit_exit_mult": pe_mult,
|
||||
}
|
||||
|
||||
conn = db_fn()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
ensure_market_order_table(conn)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO options_market_orders (
|
||||
hub_order_id, underlying, opt_type, min_expiry_hours, target_leverage,
|
||||
picked_leverage, target_index, profit_exit_enabled, profit_exit_mult,
|
||||
status, opened_inst_id, opened_trade_id, opened_at, message,
|
||||
created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?, 'open', ?,?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
hub_order_id,
|
||||
underlying,
|
||||
opt_type,
|
||||
float(min_expiry_hours),
|
||||
float(target_leverage),
|
||||
float(picked_lev) if picked_lev is not None else None,
|
||||
float(target_index) if target_index is not None else None,
|
||||
1 if pe_en else 0,
|
||||
float(pe_mult),
|
||||
inst_id,
|
||||
int(trade_id) if trade_id is not None else None,
|
||||
now,
|
||||
f"已开仓 {inst_id}",
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
order = get_market_order_by_hub_id(conn, hub_order_id) or order_stub
|
||||
_apply_exit_to_position(
|
||||
conn,
|
||||
order,
|
||||
target_index=target_index,
|
||||
profit_exit_enabled=pe_en,
|
||||
profit_exit_mult=float(pe_mult),
|
||||
)
|
||||
_notify_mkt(
|
||||
send_wechat,
|
||||
title="【OKX期权·市价单开仓】",
|
||||
order=order,
|
||||
account_label=account_label,
|
||||
extra=[
|
||||
f"合约:{inst_id}",
|
||||
f"实选杠杆:{picked_lev:g}" if picked_lev is not None else "",
|
||||
f"目标杠杆:{float(target_leverage):g}",
|
||||
],
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"msg": "市价开仓成功",
|
||||
"order": order,
|
||||
"inst_id": inst_id,
|
||||
"trade_id": trade_id,
|
||||
"picked_leverage": picked_lev,
|
||||
}
|
||||
finally:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def update_market_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]:
|
||||
from lib.options.options_cond_order_lib import _apply_exit_to_position, _exit_close_in_progress
|
||||
from lib.options.options_profit_exit_lib import normalize_profit_exit_mult
|
||||
|
||||
order = get_market_order_by_hub_id(conn, hub_order_id)
|
||||
if not order:
|
||||
return {"ok": False, "msg": "市价单不存在"}
|
||||
if order.get("status") != "open":
|
||||
return {"ok": False, "msg": "仅持仓中的市价单可改出场"}
|
||||
|
||||
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_market_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_market_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_market_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_market_order_by_hub_id(conn, hub_order_id)}
|
||||
|
||||
|
||||
def _end_order(
|
||||
conn: sqlite3.Connection,
|
||||
order: dict[str, Any],
|
||||
*,
|
||||
end_reason: str,
|
||||
message: str,
|
||||
send_wechat: Any = None,
|
||||
account_label: str = "",
|
||||
) -> None:
|
||||
now = _now()
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_market_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()
|
||||
_notify_mkt(
|
||||
send_wechat,
|
||||
title="【OKX期权·市价单结束】",
|
||||
order=order,
|
||||
account_label=account_label,
|
||||
extra=[f"原因:{end_reason}", f"说明:{message}"],
|
||||
)
|
||||
|
||||
|
||||
def run_market_orders_tick(
|
||||
conn_or_get_db: Any,
|
||||
cfg: dict[str, Any],
|
||||
*,
|
||||
send_wechat: Any = None,
|
||||
account_label: str = "",
|
||||
) -> None:
|
||||
"""持仓消失时结束市价单;应用延迟出场."""
|
||||
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_market_order_table(conn)
|
||||
ex = cfg.get("exchange_options")
|
||||
if ex is None:
|
||||
return
|
||||
for order in list_market_orders(conn, active_only=True):
|
||||
try:
|
||||
_tick_one_market(
|
||||
conn,
|
||||
cfg,
|
||||
ex,
|
||||
order,
|
||||
send_wechat=send_wechat,
|
||||
account_label=account_label,
|
||||
)
|
||||
except Exception as e:
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE options_market_orders SET message=?, updated_at=? WHERE hub_order_id=?",
|
||||
(f"监控异常: {e}"[:200], _now(), order["hub_order_id"]),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
conn = get_db()
|
||||
orders: list[dict[str, Any]] = []
|
||||
ex: Any = None
|
||||
try:
|
||||
ensure_market_order_table(conn)
|
||||
ex = cfg.get("exchange_options")
|
||||
if ex is not None:
|
||||
orders = list_market_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_market(
|
||||
conn,
|
||||
cfg,
|
||||
ex,
|
||||
order,
|
||||
send_wechat=send_wechat,
|
||||
account_label=account_label,
|
||||
)
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE options_market_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_market(
|
||||
conn: sqlite3.Connection,
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
order: dict[str, Any],
|
||||
*,
|
||||
send_wechat: Any,
|
||||
account_label: str,
|
||||
) -> None:
|
||||
from lib.options.options_cond_order_lib import (
|
||||
_apply_exit_to_position,
|
||||
_exit_close_in_progress,
|
||||
machine_has_option_position,
|
||||
)
|
||||
|
||||
if str(order.get("status") or "") != "open":
|
||||
return
|
||||
|
||||
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_market_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 machine_has_option_position(cfg, ex):
|
||||
return
|
||||
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,
|
||||
)
|
||||
@@ -1546,6 +1546,7 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
|
||||
def _cond_order_tick(_conn=None) -> None:
|
||||
from lib.options.options_cond_order_lib import run_cond_orders_tick
|
||||
from lib.options.options_market_order_lib import run_market_orders_tick
|
||||
|
||||
# 传 get_db:条件单开仓前会释放连接,避免嵌套锁库
|
||||
run_cond_orders_tick(
|
||||
@@ -1554,6 +1555,12 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
send_wechat=cfg.get("send_wechat"),
|
||||
account_label=str(cfg.get("account_label") or ""),
|
||||
)
|
||||
run_market_orders_tick(
|
||||
cfg["get_db"],
|
||||
cfg,
|
||||
send_wechat=cfg.get("send_wechat"),
|
||||
account_label=str(cfg.get("account_label") or ""),
|
||||
)
|
||||
|
||||
t = threading.Thread(
|
||||
target=options_monitor_loop,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""市价单选约:第一档实值内按杠杆接近度优选."""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
class TestMarketOrderLeveragePick(unittest.TestCase):
|
||||
def test_picks_closest_leverage_on_first_itm_rung(self):
|
||||
from lib.options.options_cond_order_lib import pick_nearest_itm_contract
|
||||
|
||||
# 指数 3000, Put 实值一档=3100;同档不同 ask → 杠杆≈1/ask
|
||||
chain = {
|
||||
"expiries": [
|
||||
{
|
||||
"exp_time": 9_999_999_999_000,
|
||||
"contracts": [
|
||||
{
|
||||
"inst_id": "ETH-USD-3100-P-far",
|
||||
"strike": 3100,
|
||||
"opt_type": "P",
|
||||
"ask": 0.05,
|
||||
"ask_sz": 10,
|
||||
"ct_mult": 0.1,
|
||||
},
|
||||
{
|
||||
"inst_id": "ETH-USD-3100-P-near-lev",
|
||||
"strike": 3100,
|
||||
"opt_type": "P",
|
||||
"ask": 0.1,
|
||||
"ask_sz": 10,
|
||||
"ct_mult": 0.1,
|
||||
},
|
||||
{
|
||||
"inst_id": "ETH-USD-3200-P-deeper",
|
||||
"strike": 3200,
|
||||
"opt_type": "P",
|
||||
"ask": 0.1,
|
||||
"ask_sz": 10,
|
||||
"ct_mult": 0.1,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def quote(_ex, inst_id):
|
||||
for c in chain["expiries"][0]["contracts"]:
|
||||
if c["inst_id"] == inst_id:
|
||||
return {
|
||||
"ok": True,
|
||||
"ask": c["ask"],
|
||||
"ask_sz": c["ask_sz"],
|
||||
"ct_mult": c["ct_mult"],
|
||||
}
|
||||
return {"ok": False}
|
||||
|
||||
cfg = {"quote_option_contract": quote}
|
||||
with patch(
|
||||
"lib.exchange.okx_options_lib.build_option_chain", return_value=chain
|
||||
), patch(
|
||||
"lib.exchange.okx_options_lib.option_buy_liquidity_ok",
|
||||
return_value=(True, ""),
|
||||
):
|
||||
# 目标杠杆 10 → 1/0.1=10, 应选 near-lev 而非 far(杠杆20)
|
||||
picked = pick_nearest_itm_contract(
|
||||
cfg,
|
||||
MagicMock(),
|
||||
underlying="ETH",
|
||||
opt_type="P",
|
||||
index_px=3000.0,
|
||||
min_expiry_hours=1.0,
|
||||
target_leverage=10.0,
|
||||
)
|
||||
self.assertIsNotNone(picked)
|
||||
self.assertEqual(picked["inst_id"], "ETH-USD-3100-P-near-lev")
|
||||
self.assertAlmostEqual(float(picked.get("picked_leverage") or 0), 10.0, places=1)
|
||||
|
||||
def test_without_leverage_keeps_nearest_itm(self):
|
||||
from lib.options.options_cond_order_lib import pick_nearest_itm_contract
|
||||
|
||||
chain = {
|
||||
"expiries": [
|
||||
{
|
||||
"exp_time": 9_999_999_999_000,
|
||||
"contracts": [
|
||||
{
|
||||
"inst_id": "ETH-USD-3100-P",
|
||||
"strike": 3100,
|
||||
"opt_type": "P",
|
||||
"ask": 0.1,
|
||||
"ask_sz": 10,
|
||||
"ct_mult": 0.1,
|
||||
},
|
||||
{
|
||||
"inst_id": "ETH-USD-3200-P",
|
||||
"strike": 3200,
|
||||
"opt_type": "P",
|
||||
"ask": 0.05,
|
||||
"ask_sz": 10,
|
||||
"ct_mult": 0.1,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def quote(_ex, inst_id):
|
||||
for c in chain["expiries"][0]["contracts"]:
|
||||
if c["inst_id"] == inst_id:
|
||||
return {
|
||||
"ok": True,
|
||||
"ask": c["ask"],
|
||||
"ask_sz": c["ask_sz"],
|
||||
"ct_mult": c["ct_mult"],
|
||||
}
|
||||
return {"ok": False}
|
||||
|
||||
cfg = {"quote_option_contract": quote}
|
||||
with patch(
|
||||
"lib.exchange.okx_options_lib.build_option_chain", return_value=chain
|
||||
), patch(
|
||||
"lib.exchange.okx_options_lib.option_buy_liquidity_ok",
|
||||
return_value=(True, ""),
|
||||
):
|
||||
picked = pick_nearest_itm_contract(
|
||||
cfg,
|
||||
MagicMock(),
|
||||
underlying="ETH",
|
||||
opt_type="P",
|
||||
index_px=3000.0,
|
||||
min_expiry_hours=1.0,
|
||||
)
|
||||
self.assertEqual(picked["inst_id"], "ETH-USD-3100-P")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user