Fix strategy-logic P0s from audit: monitor false-flat, fill-confirmed open/close, mode gates.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -187,7 +187,7 @@ def close_option_by_bid1(
|
||||
max_levels=1,
|
||||
)
|
||||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||||
_cancel_sell_pending(ex, inst_id)
|
||||
# 不撤他人挂单:仅拒绝本轮下单
|
||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
||||
return {
|
||||
"ok": False,
|
||||
@@ -248,8 +248,6 @@ def close_option_by_bid1(
|
||||
"auto_close_blocked": True,
|
||||
"close_gate": gate,
|
||||
}
|
||||
if gate.get("ready"):
|
||||
mark_close_gate_passed(inst_id)
|
||||
|
||||
locked_bid_px = level_px
|
||||
before_avail = avail
|
||||
@@ -272,6 +270,9 @@ def close_option_by_bid1(
|
||||
"locked_bid_px": locked_bid_px,
|
||||
"batch_sheets": level_sheets,
|
||||
}
|
||||
# 仅下单被接受后才记门控已通过,避免下单失败却跳过后续 2× 等待
|
||||
if require_recycle_gate and gate.get("ready"):
|
||||
mark_close_gate_passed(inst_id)
|
||||
|
||||
px = float(order.get("px", locked_bid_px))
|
||||
oid = str((order.get("data") or {}).get("ordId") or "")
|
||||
@@ -279,12 +280,20 @@ def close_option_by_bid1(
|
||||
time.sleep(0.6)
|
||||
invalidate_option_positions_cache()
|
||||
raw2 = cfg["fetch_option_positions"](ex)
|
||||
after_avail = 0
|
||||
if raw2 is not None:
|
||||
after_pos = next((p for p in raw2 if str(p.get("instId")) == inst_id), None)
|
||||
after_avail = _avail_sheets(after_pos) if after_pos else 0
|
||||
reduced = max(0, before_avail - after_avail) if raw2 is not None else 0
|
||||
remaining_pos = after_avail if raw2 is not None else max(0, before_avail - level_sheets)
|
||||
if raw2 is None:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": "下单后获取持仓失败,未确认是否成交",
|
||||
"stopped_reason": "position_fetch_failed",
|
||||
"locked_bid_px": locked_bid_px,
|
||||
"batch_sheets": level_sheets,
|
||||
"close_ord_id": oid or None,
|
||||
"fully_closed": False,
|
||||
}
|
||||
after_pos = next((p for p in raw2 if str(p.get("instId")) == inst_id), None)
|
||||
after_avail = _avail_sheets(after_pos) if after_pos else 0
|
||||
reduced = max(0, before_avail - after_avail)
|
||||
remaining_pos = after_avail
|
||||
fully_closed = remaining_pos < 1
|
||||
|
||||
if fully_closed:
|
||||
|
||||
+139
-18
@@ -480,8 +480,44 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
"budget_full_usdc": budget if mode == "budget_full" else None,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"can_open": False,
|
||||
"msg": f"交易模式校验失败: {e}",
|
||||
}
|
||||
)
|
||||
try:
|
||||
from lib.hedge_plan.hedge_options_exclusive_lib import block_standalone_option_open_msg
|
||||
|
||||
conn_q = cfg["get_db"]()
|
||||
try:
|
||||
excl = block_standalone_option_open_msg(conn_q)
|
||||
finally:
|
||||
conn_q.close()
|
||||
if excl:
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
"ok": True,
|
||||
"can_open": False,
|
||||
"msg": excl,
|
||||
"quote_per_unit": ask,
|
||||
"premium_per_sheet": None,
|
||||
"sizing": {
|
||||
"ok": False,
|
||||
"msg": excl,
|
||||
"sheets": 0,
|
||||
"eth_amount": 0.0,
|
||||
"total_premium": 0.0,
|
||||
},
|
||||
"available_usdc": available_usdc,
|
||||
"budget_full_usdc": budget if mode == "budget_full" else None,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "can_open": False, "msg": f"互斥校验失败: {e}"})
|
||||
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
|
||||
if not can_open:
|
||||
# 合约可报价,但不可开仓:返回参考标记价供展示
|
||||
@@ -598,8 +634,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
mode_block = block_standalone_open_by_mode_msg()
|
||||
if mode_block:
|
||||
return jsonify({"ok": False, "msg": mode_block, "can_open": False})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"交易模式校验失败: {e}", "can_open": False})
|
||||
try:
|
||||
from lib.hedge_plan.hedge_options_exclusive_lib import block_standalone_option_open_msg
|
||||
|
||||
@@ -610,8 +646,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
conn_gate.close()
|
||||
if block_msg:
|
||||
return jsonify({"ok": False, "msg": block_msg})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"互斥校验失败: {e}"})
|
||||
data = request.get_json(silent=True) or {}
|
||||
inst_id = (data.get("inst_id") or "").strip()
|
||||
mode = (data.get("mode") or "budget_full").strip()
|
||||
@@ -690,16 +726,14 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
if capped is None:
|
||||
return jsonify({"ok": False, "msg": cap_msg or "卖一深度不足,无法买入"})
|
||||
if capped < sheets:
|
||||
sizing = calc_order_size(
|
||||
quote_per_unit=float(ask),
|
||||
ct_mult=ct_mult,
|
||||
min_sz=min_sz,
|
||||
sheets=capped,
|
||||
budget_cap=budget_cap if mode in ("budget_full", "sheets", "eth_amount") else None,
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"msg": f"卖一深度仅 {int(capped)} 张,不足请求 {int(sheets)} 张,拒绝缩量成交",
|
||||
"requested_sheets": int(sheets),
|
||||
"ask_sz": ask_sz,
|
||||
}
|
||||
)
|
||||
if not sizing.get("ok"):
|
||||
return jsonify({"ok": False, "msg": sizing.get("msg") or "张数计算失败", "sizing": sizing})
|
||||
sheets = int(sizing["sheets"])
|
||||
tick_sz = q.get("tick_sz")
|
||||
order = cfg["place_option_limit_order"](
|
||||
ex,
|
||||
@@ -709,9 +743,56 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
price=float(ask),
|
||||
td_mode=td_mode_for_option_buy(cfg["td_mode"]),
|
||||
tick_sz=tick_sz,
|
||||
ord_type="ioc",
|
||||
)
|
||||
if not order.get("ok"):
|
||||
return jsonify(order)
|
||||
ord_id = str((order.get("data") or {}).get("ordId") or "").strip()
|
||||
if not ord_id:
|
||||
return jsonify({"ok": False, "msg": "下单成功但未返回订单号", "order": order})
|
||||
from lib.exchange.okx_options_lib import wait_option_order_full_fill
|
||||
|
||||
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)
|
||||
orphan_close = None
|
||||
if filled_n > 0:
|
||||
try:
|
||||
from lib.options.options_close_exec_lib import close_option_by_bid1
|
||||
|
||||
orphan_close = close_option_by_bid1(
|
||||
cfg, ex, inst_id, sheets=filled_n, require_recycle_gate=False
|
||||
)
|
||||
except Exception as e:
|
||||
orphan_close = {"ok": False, "msg": str(e)}
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"msg": fill.get("msg") or "未完全成交,开仓失败",
|
||||
"filled_sheets": filled_n,
|
||||
"orphan_close": orphan_close,
|
||||
"fill": fill,
|
||||
"order": order,
|
||||
}
|
||||
)
|
||||
fill_px = float(fill.get("avg_px") or ask)
|
||||
filled_n = int(fill.get("filled_sheets") or sheets)
|
||||
sheets = filled_n
|
||||
sizing = dict(sizing)
|
||||
sizing["sheets"] = sheets
|
||||
sizing["eth_amount"] = round(sheets * ct_mult, 8)
|
||||
sizing["total_premium"] = round(fill_px * sheets * ct_mult, 4)
|
||||
conn = cfg["get_db"]()
|
||||
trade_id = None
|
||||
target_mon = None
|
||||
@@ -739,10 +820,10 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
str(q.get("exp_time") or ""),
|
||||
sheets,
|
||||
sizing["eth_amount"],
|
||||
float(ask),
|
||||
fill_px,
|
||||
sizing["total_premium"],
|
||||
signal_note,
|
||||
(order.get("data") or {}).get("ordId"),
|
||||
ord_id,
|
||||
),
|
||||
)
|
||||
trade_id = int(cur.lastrowid)
|
||||
@@ -778,7 +859,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
opt_type=open_opt_type,
|
||||
sheets=sheets,
|
||||
premium_paid=sizing.get("total_premium"),
|
||||
open_quote=float(ask) if ask is not None else None,
|
||||
open_quote=fill_px,
|
||||
target_index=target_index,
|
||||
signal_note=signal_note,
|
||||
)
|
||||
@@ -937,6 +1018,26 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
inst_id = (data.get("inst_id") or "").strip()
|
||||
if not inst_id:
|
||||
return jsonify({"ok": False, "msg": "缺少 inst_id"})
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_db import (
|
||||
active_hedge_option_inst_ids,
|
||||
init_hedge_plan_tables,
|
||||
)
|
||||
|
||||
conn_h = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn_h)
|
||||
if inst_id in active_hedge_option_inst_ids(conn_h):
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"msg": "该合约属于进行中的对冲计划,请在对冲计划中管理,禁止在期权页设置目标",
|
||||
}
|
||||
)
|
||||
finally:
|
||||
conn_h.close()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
|
||||
try:
|
||||
target_index = float(data.get("target_index"))
|
||||
except (TypeError, ValueError):
|
||||
@@ -1013,6 +1114,26 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
inst_id = (data.get("inst_id") or "").strip()
|
||||
if not inst_id:
|
||||
return jsonify({"ok": False, "msg": "缺少 inst_id"})
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_db import (
|
||||
active_hedge_option_inst_ids,
|
||||
init_hedge_plan_tables,
|
||||
)
|
||||
|
||||
conn_h = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn_h)
|
||||
if inst_id in active_hedge_option_inst_ids(conn_h):
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"msg": "该合约属于进行中的对冲计划,请在对冲计划中管理,禁止在期权页平仓",
|
||||
}
|
||||
)
|
||||
finally:
|
||||
conn_h.close()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
|
||||
if data.get("market"):
|
||||
return jsonify({"ok": False, "msg": "已禁用市价平仓,仅支持买一限价"})
|
||||
sheets = data.get("sheets")
|
||||
|
||||
@@ -29,6 +29,28 @@ from lib.options.options_review_lib import (
|
||||
)
|
||||
|
||||
|
||||
def _review_source_for_mode(requested: str | None) -> str | None:
|
||||
"""按当前交易模式钳制复盘 source_type;不允许跨模式窥探."""
|
||||
try:
|
||||
from lib.hedge_plan.okx_trade_mode_lib import get_okx_trade_mode
|
||||
|
||||
mode = get_okx_trade_mode()
|
||||
except Exception:
|
||||
mode = "options"
|
||||
allowed = {
|
||||
"options": "option_spot",
|
||||
"perp_options": "perp_options",
|
||||
"options_options": "options_options",
|
||||
}.get(mode, "option_spot")
|
||||
req = (requested or "").strip()
|
||||
if not req:
|
||||
return allowed
|
||||
if req == allowed:
|
||||
return allowed
|
||||
# 显式 all=1 仍拒绝跨模式,除非管理员扩展;此处一律钳制
|
||||
return allowed
|
||||
|
||||
|
||||
def attach_options_review_templates(app: Flask, repo_root: str) -> None:
|
||||
tpl_dir = os.path.join(repo_root, "lib", "options", "templates")
|
||||
if not os.path.isdir(tpl_dir):
|
||||
@@ -138,7 +160,7 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
|
||||
ensure_local_review_synced(conn, ex=ex if ex is not None else None)
|
||||
conn.commit()
|
||||
filt = dict(
|
||||
source_type=(request.args.get("source_type") or "").strip() or None,
|
||||
source_type=_review_source_for_mode(request.args.get("source_type")),
|
||||
underlying=(request.args.get("underlying") or "").strip() or None,
|
||||
opt_type=(request.args.get("opt_type") or "").strip() or None,
|
||||
strategy_tag=(request.args.get("strategy_tag") or "").strip() or None,
|
||||
@@ -272,7 +294,7 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
|
||||
conn.commit()
|
||||
stats = compute_review_stats(
|
||||
conn,
|
||||
source_type=(request.args.get("source_type") or "").strip() or None,
|
||||
source_type=_review_source_for_mode(request.args.get("source_type")),
|
||||
underlying=(request.args.get("underlying") or "").strip() or None,
|
||||
include_hedge_legs=(request.args.get("include_hedge_legs") or "").strip().lower()
|
||||
in ("1", "true", "yes"),
|
||||
|
||||
@@ -373,6 +373,15 @@ def run_options_target_closes(
|
||||
ensure_target_tables(conn)
|
||||
pos_by_inst = {str(p.get("inst_id") or p.get("instId") or ""): p for p in positions}
|
||||
live_ids = {k for k in pos_by_inst if k}
|
||||
hedge_managed: set[str] = set()
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_db import active_hedge_option_inst_ids, init_hedge_plan_tables
|
||||
|
||||
init_hedge_plan_tables(conn)
|
||||
hedge_managed = active_hedge_option_inst_ids(conn)
|
||||
except Exception:
|
||||
# fail-closed:本轮不执行任何单独目标平仓,避免误平对冲腿
|
||||
return 0
|
||||
cancel_orphans_without_position(conn, live_inst_ids=live_ids)
|
||||
_commit_monitor(conn)
|
||||
|
||||
@@ -381,6 +390,15 @@ def run_options_target_closes(
|
||||
inst_id = str(mon.get("inst_id") or "")
|
||||
if not inst_id:
|
||||
continue
|
||||
if inst_id in hedge_managed:
|
||||
mark_monitor(
|
||||
conn,
|
||||
int(mon["id"]),
|
||||
status="expired",
|
||||
message="已移交对冲计划托管,跳过单独目标平仓",
|
||||
)
|
||||
_commit_monitor(conn)
|
||||
continue
|
||||
if inst_id not in pos_by_inst:
|
||||
mark_monitor(conn, int(mon["id"]), status="expired", message="持仓已平")
|
||||
_commit_monitor(conn)
|
||||
@@ -414,6 +432,15 @@ def run_options_target_closes(
|
||||
target = _safe_float(mon.get("target_index"))
|
||||
if not inst_id or target is None:
|
||||
continue
|
||||
if inst_id in hedge_managed:
|
||||
mark_monitor(
|
||||
conn,
|
||||
int(mon["id"]),
|
||||
status="expired",
|
||||
message="已移交对冲计划托管,跳过单独目标平仓",
|
||||
)
|
||||
_commit_monitor(conn)
|
||||
continue
|
||||
pos = pos_by_inst.get(inst_id)
|
||||
if not pos:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user