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:
Vendored
+13
@@ -250,6 +250,19 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
|
||||
|
||||
def _effective_env_value(key: str, file_values: dict[str, str], schema_default: str = "") -> str:
|
||||
if key == "OKX_TRADE_MODE":
|
||||
# 展示值必须与运行时 get_okx_trade_mode() 一致,避免未写入时默认 options 静默改模式
|
||||
file_val = str(file_values.get(key) or "").strip() if key in file_values else ""
|
||||
if file_val:
|
||||
from lib.hedge_plan.okx_trade_mode_lib import normalize_okx_trade_mode
|
||||
|
||||
return normalize_okx_trade_mode(file_val) or file_val
|
||||
try:
|
||||
from lib.hedge_plan.okx_trade_mode_lib import get_okx_trade_mode
|
||||
|
||||
return get_okx_trade_mode()
|
||||
except Exception:
|
||||
pass
|
||||
if key in file_values:
|
||||
file_val = str(file_values.get(key) or "").strip()
|
||||
if file_val:
|
||||
|
||||
@@ -31,7 +31,7 @@ def block_standalone_option_open_msg(conn: Any) -> Optional[str]:
|
||||
if count_active_plans(conn) > 0:
|
||||
return "存在进行中对冲计划,禁止单独开期权(可在 env「对冲与期权互斥门控」关闭)"
|
||||
except Exception:
|
||||
return None
|
||||
return "互斥门控校验失败,暂禁止单独开期权"
|
||||
return None
|
||||
|
||||
|
||||
@@ -77,10 +77,10 @@ def block_hedge_plan_start_msg(
|
||||
try:
|
||||
rows = fetch_positions(exchange) or []
|
||||
except Exception:
|
||||
return None
|
||||
return "获取期权持仓失败,暂禁止启动对冲计划"
|
||||
try:
|
||||
if has_standalone_option_position(conn, rows):
|
||||
return "存在单独期权持仓,禁止启动对冲计划(可在 env「对冲与期权互斥门控」关闭)"
|
||||
except Exception:
|
||||
return None
|
||||
return "互斥门控校验失败,暂禁止启动对冲计划"
|
||||
return None
|
||||
|
||||
@@ -272,6 +272,26 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
|
||||
return out
|
||||
|
||||
|
||||
def active_hedge_option_inst_ids(conn: sqlite3.Connection) -> set[str]:
|
||||
"""进行中对冲计划托管的期权合约,禁止单独期权页 close/target 拆组."""
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT DISTINCT l.inst_id
|
||||
FROM hedge_plan_legs l
|
||||
JOIN hedge_plans p ON p.id = l.plan_id
|
||||
WHERE p.status IN ('opening', 'active', 'partial')
|
||||
AND l.status IN ('open', 'hold_to_expiry')
|
||||
AND l.inst_id IS NOT NULL
|
||||
AND TRIM(l.inst_id) != ''
|
||||
AND (
|
||||
l.leg_role LIKE 'option%'
|
||||
OR (l.opt_type IS NOT NULL AND TRIM(l.opt_type) != '')
|
||||
)
|
||||
"""
|
||||
).fetchall()
|
||||
return {str(r[0]).strip() for r in rows if r and r[0]}
|
||||
|
||||
|
||||
def _sf(v: Any) -> Optional[float]:
|
||||
try:
|
||||
if v is None or v == "":
|
||||
|
||||
@@ -66,8 +66,63 @@ def _index_px(cfg: dict[str, Any], underlying: str) -> Optional[float]:
|
||||
return None
|
||||
|
||||
|
||||
def _plan_open_grace_sec() -> float:
|
||||
try:
|
||||
return max(0.0, float(os.getenv("HEDGE_PLAN_OPEN_GRACE_SEC") or "90"))
|
||||
except (TypeError, ValueError):
|
||||
return 90.0
|
||||
|
||||
|
||||
def _within_open_grace(plan: dict[str, Any]) -> bool:
|
||||
"""开仓后宽限期:仓位尚未同步到交易所时禁止按「已平」收口."""
|
||||
grace = _plan_open_grace_sec()
|
||||
if grace <= 0:
|
||||
return False
|
||||
raw = str(plan.get("opened_at") or plan.get("created_at") or "").strip()
|
||||
if not raw:
|
||||
return True
|
||||
try:
|
||||
# "YYYY-MM-DD HH:MM:SS" 本地墙钟
|
||||
opened = datetime.strptime(raw[:19], "%Y-%m-%d %H:%M:%S")
|
||||
age = (datetime.now() - opened).total_seconds()
|
||||
return age < grace
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def _classify_po_flat_reason(
|
||||
*,
|
||||
direction: str,
|
||||
entry: float,
|
||||
mark: Optional[float],
|
||||
tp: Optional[float],
|
||||
sl: Optional[float],
|
||||
) -> str:
|
||||
"""永续已平时分类 TP/SL.歧义时偏 SL(触发强平期权),避免误判 TP 跳过强平."""
|
||||
d = (direction or "long").lower()
|
||||
if mark is None or not entry:
|
||||
return "perp_flat_unknown"
|
||||
if sl is not None:
|
||||
if d == "long" and mark <= sl:
|
||||
return "perp_sl"
|
||||
if d == "short" and mark >= sl:
|
||||
return "perp_sl"
|
||||
if tp is not None:
|
||||
if d == "long" and mark >= tp:
|
||||
return "perp_tp"
|
||||
if d == "short" and mark <= tp:
|
||||
return "perp_tp"
|
||||
if sl is not None and tp is not None:
|
||||
return "perp_sl" if abs(mark - sl) <= abs(mark - tp) else "perp_tp"
|
||||
if sl is not None:
|
||||
return "perp_sl"
|
||||
if tp is not None:
|
||||
return "perp_tp"
|
||||
return "perp_flat_unknown"
|
||||
|
||||
|
||||
def tick_active_plans(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
"""扫描 active 计划 + 止盈后遗留期权到期收口.返回处理摘要."""
|
||||
"""扫描 active/partial 计划 + 止盈后遗留期权到期收口.返回处理摘要."""
|
||||
get_db = cfg.get("get_db")
|
||||
if not callable(get_db):
|
||||
return {"ok": False, "msg": "get_db missing"}
|
||||
@@ -79,7 +134,14 @@ def tick_active_plans(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
init_hedge_plan_tables(conn)
|
||||
plans = list_plans(conn, status="active", limit=40)
|
||||
# partial:裸永续/半腿也需侦测永续 TP/SL
|
||||
plans.extend(list_plans(conn, status="partial", limit=20))
|
||||
seen: set[int] = set()
|
||||
for plan in plans:
|
||||
pid = int(plan.get("id") or 0)
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
r = _tick_one(cfg, conn, plan)
|
||||
if r:
|
||||
acted.append(r)
|
||||
@@ -184,10 +246,16 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
|
||||
symbol = perp.get("symbol") or ""
|
||||
direction = (plan.get("direction") or "long").lower()
|
||||
live = _perp_live_contracts(cfg, symbol, direction)
|
||||
# 仍有仓 → 未触达交易所 TP/SL
|
||||
if live is not None and live > 0:
|
||||
# API 失败 / 未注入 → 本轮跳过,绝不当「已平」
|
||||
if live is None:
|
||||
return None
|
||||
# 仓已平:用标记/最新粗判 TP or SL
|
||||
# 仍有仓 → 未触达交易所 TP/SL
|
||||
if live > 0:
|
||||
return None
|
||||
# 开仓后宽限期:仓位同步延迟可误读为 0
|
||||
if _within_open_grace(plan):
|
||||
return None
|
||||
|
||||
entry = _sf(plan.get("entry_mark")) or _sf(perp.get("avg_open")) or 0
|
||||
tp = _sf(plan.get("tp"))
|
||||
sl = _sf(plan.get("sl"))
|
||||
@@ -199,17 +267,19 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
|
||||
mark = _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last"))
|
||||
except Exception:
|
||||
mark = None
|
||||
reason = "perp_tp"
|
||||
if mark is not None and sl is not None and entry:
|
||||
if direction == "long" and mark <= sl:
|
||||
reason = "perp_sl"
|
||||
elif direction == "short" and mark >= sl:
|
||||
reason = "perp_sl"
|
||||
elif tp is not None:
|
||||
if direction == "long" and mark >= tp:
|
||||
reason = "perp_tp"
|
||||
elif direction == "short" and mark <= tp:
|
||||
reason = "perp_tp"
|
||||
reason = _classify_po_flat_reason(
|
||||
direction=direction, entry=float(entry or 0), mark=mark, tp=tp, sl=sl
|
||||
)
|
||||
# 上一轮止损强平未完成:粘滞为 SL,避免 mark 反弹误判 TP 跳过强平
|
||||
pending_reason = str(plan.get("close_reason") or "")
|
||||
if pending_reason == "perp_sl_pending_opt":
|
||||
reason = "perp_sl"
|
||||
elif pending_reason == "perp_tp_pending_opt":
|
||||
reason = "perp_tp"
|
||||
# 未明确 TP/SL 时不收口,下轮再判
|
||||
if reason == "perp_flat_unknown":
|
||||
return None
|
||||
|
||||
premium = float(plan.get("premium_total") or 0)
|
||||
cs = float(cfg.get("default_contract_size") or 0.01)
|
||||
get_cs = cfg.get("get_contract_size")
|
||||
@@ -227,66 +297,107 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
|
||||
perp_pnl = (exit_px - entry) * coins
|
||||
|
||||
opt_pnl = -premium
|
||||
if reason == "perp_sl" and opt and _env_bool("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", True):
|
||||
close_r = _sell_option(
|
||||
cfg,
|
||||
inst_id=str(opt.get("inst_id") or ""),
|
||||
sheets=float(opt.get("size") or 1),
|
||||
)
|
||||
if not close_r.get("ok"):
|
||||
notify_hedge(
|
||||
if reason == "perp_sl" and opt and str(opt.get("status") or "") == "open":
|
||||
if _env_bool("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", True):
|
||||
close_r = _sell_option(
|
||||
cfg,
|
||||
build_hedge_alert_message(
|
||||
title="永续止损后期权强制平仓失败",
|
||||
plan_id=plan.get("id"),
|
||||
detail=str(close_r.get("msg") or close_r),
|
||||
),
|
||||
inst_id=str(opt.get("inst_id") or ""),
|
||||
sheets=float(opt.get("size") or 1),
|
||||
)
|
||||
if close_r.get("ok"):
|
||||
bid = _sf(close_r.get("bid"))
|
||||
ask_open = _sf(opt.get("avg_open"))
|
||||
if bid is not None and ask_open is not None:
|
||||
ct = float(opt.get("ct_mult") or 0.01)
|
||||
est = (bid - ask_open) * float(opt.get("size") or 1) * ct
|
||||
else:
|
||||
est = -premium
|
||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=est)
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), opt_pnl, opt["id"]),
|
||||
)
|
||||
elif reason == "perp_tp" and opt:
|
||||
if _env_bool("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", False):
|
||||
close_r = _sell_option(cfg, inst_id=str(opt.get("inst_id") or ""), sheets=float(opt.get("size") or 1))
|
||||
if not close_r.get("ok"):
|
||||
if close_r.get("already_flat"):
|
||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=-premium)
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), opt_pnl, opt["id"]),
|
||||
)
|
||||
elif not close_r.get("ok") or not close_r.get("fully_closed", True):
|
||||
notify_hedge(
|
||||
cfg,
|
||||
build_hedge_alert_message(
|
||||
title="永续止盈后期权平仓失败",
|
||||
title="永续止损后期权强制平仓失败(将重试)",
|
||||
plan_id=plan.get("id"),
|
||||
detail=str(close_r.get("msg") or close_r),
|
||||
),
|
||||
)
|
||||
update_plan(conn, int(plan["id"]), close_reason="perp_sl_pending_opt")
|
||||
return {
|
||||
"plan_id": plan["id"],
|
||||
"msg": "止损后期权未平完",
|
||||
"close": close_r,
|
||||
"retry": True,
|
||||
}
|
||||
else:
|
||||
bid = _sf(close_r.get("bid") or close_r.get("locked_bid_px"))
|
||||
ask_open = _sf(opt.get("avg_open"))
|
||||
if bid is not None and ask_open is not None:
|
||||
ct = float(opt.get("ct_mult") or 0.01)
|
||||
est = (bid - ask_open) * float(opt.get("size") or 1) * ct
|
||||
else:
|
||||
est = -premium
|
||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=est)
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), opt_pnl, opt["id"]),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=? WHERE id=?",
|
||||
("closed", reason, _now(), opt["id"]),
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=? WHERE id=?",
|
||||
("hold_to_expiry", "orphaned_after_sl", opt["id"]),
|
||||
)
|
||||
opt_pnl = -premium
|
||||
elif reason == "perp_tp" and opt and str(opt.get("status") or "") == "open":
|
||||
if _env_bool("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", False):
|
||||
close_r = _sell_option(
|
||||
cfg, inst_id=str(opt.get("inst_id") or ""), sheets=float(opt.get("size") or 1)
|
||||
)
|
||||
if close_r.get("already_flat"):
|
||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=-premium)
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), opt_pnl, opt["id"]),
|
||||
)
|
||||
elif not close_r.get("ok") or not close_r.get("fully_closed", True):
|
||||
notify_hedge(
|
||||
cfg,
|
||||
build_hedge_alert_message(
|
||||
title="永续止盈后期权平仓失败(将重试)",
|
||||
plan_id=plan.get("id"),
|
||||
detail=str(close_r.get("msg") or close_r),
|
||||
),
|
||||
)
|
||||
update_plan(conn, int(plan["id"]), close_reason="perp_tp_pending_opt")
|
||||
return {
|
||||
"plan_id": plan["id"],
|
||||
"msg": "止盈后期权未平完",
|
||||
"close": close_r,
|
||||
"retry": True,
|
||||
}
|
||||
else:
|
||||
bid = _sf(close_r.get("bid") or close_r.get("locked_bid_px"))
|
||||
ask_open = _sf(opt.get("avg_open"))
|
||||
if bid is not None and ask_open is not None:
|
||||
ct = float(opt.get("ct_mult") or 0.01)
|
||||
est = (bid - ask_open) * float(opt.get("size") or 1) * ct
|
||||
else:
|
||||
est = -premium
|
||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=est)
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), opt_pnl, opt["id"]),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=? WHERE id=?",
|
||||
("hold_to_expiry", "orphaned_after_tp", opt["id"]),
|
||||
)
|
||||
opt_pnl = -premium
|
||||
|
||||
if reason == "perp_tp":
|
||||
total = perp_pnl + opt_pnl
|
||||
else:
|
||||
total = opt_pnl + perp_pnl
|
||||
opt_pnl = -premium
|
||||
|
||||
total = perp_pnl + opt_pnl
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), perp_pnl, perp["id"]),
|
||||
)
|
||||
# partial → closed 也走同一收口
|
||||
update_plan(
|
||||
conn,
|
||||
int(plan["id"]),
|
||||
|
||||
@@ -135,10 +135,20 @@ def _buy_option(
|
||||
if pos_limit_msg:
|
||||
return {"ok": False, "msg": pos_limit_msg, "quote": q, "can_open": False}
|
||||
sheets_i = max(1, int(round(float(sheets))))
|
||||
requested_sheets = sheets_i
|
||||
capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets_i, ask_sz, min_sz=1)
|
||||
if capped is None:
|
||||
return {"ok": False, "msg": cap_msg or "卖一深度不足,无法买入", "quote": q}
|
||||
sheets_i = capped
|
||||
if int(capped) < requested_sheets:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": f"卖一深度仅 {int(capped)} 张,不足请求 {requested_sheets} 张,拒绝缩量成交",
|
||||
"quote": q,
|
||||
"can_open": False,
|
||||
"requested_sheets": requested_sheets,
|
||||
"ask_sz": ask_sz,
|
||||
}
|
||||
sheets_i = int(capped)
|
||||
ct_mult = float(q.get("ct_mult") or 0.01)
|
||||
premium = float(ask) * sheets_i * ct_mult
|
||||
if dry_run:
|
||||
@@ -188,6 +198,14 @@ def _buy_option(
|
||||
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 and not dry_run:
|
||||
# 部分成交后撤单:尝试立刻平掉已成交,避免孤儿多头
|
||||
try:
|
||||
orphan_close = _sell_option(cfg, inst_id=inst_id, sheets=float(filled_n))
|
||||
except Exception as e:
|
||||
orphan_close = {"ok": False, "msg": str(e)}
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": fill.get("msg") or "未完全成交,开仓失败",
|
||||
@@ -195,7 +213,8 @@ def _buy_option(
|
||||
"sheets": sheets_i,
|
||||
"ask": float(ask),
|
||||
"exchange_ord_id": ord_id,
|
||||
"filled_sheets": fill.get("filled_sheets"),
|
||||
"filled_sheets": filled_n,
|
||||
"orphan_close": orphan_close,
|
||||
"order": order,
|
||||
"fill": fill,
|
||||
"can_open": False,
|
||||
@@ -289,9 +308,17 @@ def _sell_option(
|
||||
sheets: float,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""平期权:走买一限价 + 验仓;仅 fully_closed/already_flat 视为成功.
|
||||
|
||||
对冲强平/目标平仓不启用 2× 回收门控(require_recycle_gate=False).
|
||||
"""
|
||||
from lib.exchange.okx_options_lib import fetch_option_book_depth, fetch_option_positions
|
||||
from lib.options.options_close_exec_lib import close_option_by_bid1
|
||||
|
||||
ex = cfg.get("exchange_options")
|
||||
quote_fn = cfg.get("quote_option_contract")
|
||||
place_fn = cfg.get("place_option_limit_order")
|
||||
if not inst_id:
|
||||
return {"ok": False, "msg": "缺少期权合约"}
|
||||
if not callable(quote_fn) or ex is None:
|
||||
return {"ok": False, "msg": "期权报价能力未就绪"}
|
||||
q = quote_fn(ex, inst_id)
|
||||
@@ -300,20 +327,77 @@ def _sell_option(
|
||||
return {"ok": False, "msg": "暂无买一价,无法平期权"}
|
||||
sheets_i = max(1, int(round(float(sheets))))
|
||||
if dry_run:
|
||||
return {"ok": True, "dry_run": True, "inst_id": inst_id, "sheets": sheets_i, "bid": float(bid)}
|
||||
if not callable(place_fn):
|
||||
return {
|
||||
"ok": True,
|
||||
"dry_run": True,
|
||||
"inst_id": inst_id,
|
||||
"sheets": sheets_i,
|
||||
"bid": float(bid),
|
||||
"fully_closed": True,
|
||||
}
|
||||
if not callable(cfg.get("place_option_limit_order")):
|
||||
return {"ok": False, "msg": "期权平仓未注入"}
|
||||
order = place_fn(
|
||||
close_cfg = dict(cfg)
|
||||
if not callable(close_cfg.get("fetch_option_positions")):
|
||||
close_cfg["fetch_option_positions"] = fetch_option_positions
|
||||
if not callable(close_cfg.get("fetch_option_book_depth")):
|
||||
close_cfg["fetch_option_book_depth"] = fetch_option_book_depth
|
||||
if "td_mode" not in close_cfg:
|
||||
close_cfg["td_mode"] = close_cfg.get("options_td_mode") or "isolated"
|
||||
result = close_option_by_bid1(
|
||||
close_cfg,
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
side="sell",
|
||||
inst_id,
|
||||
sheets=sheets_i,
|
||||
price=float(bid),
|
||||
td_mode="isolated",
|
||||
tick_sz=q.get("tick_sz"),
|
||||
reduce_only=True,
|
||||
require_recycle_gate=False,
|
||||
)
|
||||
return order if order.get("ok") else order
|
||||
out = dict(result or {})
|
||||
if out.get("already_flat"):
|
||||
# 二次验仓,避免一次空列表误判已平
|
||||
import time as _time
|
||||
|
||||
_time.sleep(0.35)
|
||||
try:
|
||||
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
|
||||
|
||||
invalidate_option_positions_cache()
|
||||
except Exception:
|
||||
pass
|
||||
rows2 = close_cfg["fetch_option_positions"](ex)
|
||||
if rows2 is None:
|
||||
return {"ok": False, "msg": "二次验仓失败,未确认是否已平", "fully_closed": False}
|
||||
still = next((p for p in rows2 if str(p.get("instId")) == inst_id), None)
|
||||
still_sz = 0.0
|
||||
if still is not None:
|
||||
try:
|
||||
still_sz = abs(float(still.get("availPos") or still.get("pos") or 0))
|
||||
except (TypeError, ValueError):
|
||||
still_sz = 0.0
|
||||
if still is not None and still_sz >= 1:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": "二次验仓仍有持仓,拒绝 already_flat",
|
||||
"fully_closed": False,
|
||||
}
|
||||
out["ok"] = True
|
||||
out["fully_closed"] = True
|
||||
out.setdefault("bid", float(bid))
|
||||
return out
|
||||
if not out.get("ok"):
|
||||
out.setdefault("bid", float(bid))
|
||||
return out
|
||||
if not out.get("fully_closed"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": out.get("msg") or "期权尚未完全平仓,将下轮重试",
|
||||
"bid": out.get("locked_bid_px") or float(bid),
|
||||
"fully_closed": False,
|
||||
"partial": True,
|
||||
"close": out,
|
||||
}
|
||||
out["bid"] = out.get("locked_bid_px") or float(bid)
|
||||
out["fully_closed"] = True
|
||||
return out
|
||||
|
||||
|
||||
def _notify_partial(cfg: dict[str, Any], plan_type: str, msg: str, results: list[dict[str, Any]]) -> None:
|
||||
@@ -853,10 +937,34 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
|
||||
try:
|
||||
if float(body["contracts"]) <= 0 or float(body["sheets"]) <= 0:
|
||||
return "张数必须大于 0"
|
||||
if float(body["tp"]) <= 0 or float(body["sl"]) <= 0:
|
||||
return "止盈/止损无效"
|
||||
entry = float(body["entry"])
|
||||
tp = float(body["tp"])
|
||||
sl = float(body["sl"])
|
||||
if tp <= 0 or sl <= 0 or entry <= 0:
|
||||
return "止盈/止损/入场无效"
|
||||
except (TypeError, ValueError):
|
||||
return "数值字段无效"
|
||||
direction = str(body.get("direction") or "").strip().lower()
|
||||
if direction not in ("long", "short"):
|
||||
return "方向须为 long 或 short"
|
||||
opt_type = str(body.get("opt_type") or "").strip().upper()
|
||||
if not opt_type:
|
||||
# 允许从合约名推断 ETH-USD-...-P / -C
|
||||
inst = str(body.get("opt_inst_id") or "")
|
||||
if inst.upper().endswith("-P"):
|
||||
opt_type = "P"
|
||||
elif inst.upper().endswith("-C"):
|
||||
opt_type = "C"
|
||||
if opt_type not in ("P", "C"):
|
||||
return "缺少期权类型(Put/Call)"
|
||||
if direction == "long" and opt_type != "P":
|
||||
return "做多永期对冲须用 Put"
|
||||
if direction == "short" and opt_type != "C":
|
||||
return "做空永期对冲须用 Call"
|
||||
if direction == "long" and not (sl < entry < tp):
|
||||
return "做多须满足 止损 < 入场 < 止盈"
|
||||
if direction == "short" and not (tp < entry < sl):
|
||||
return "做空须满足 止盈 < 入场 < 止损"
|
||||
return None
|
||||
if pt == "options_options":
|
||||
a = body.get("leg_a") or {}
|
||||
|
||||
@@ -186,13 +186,14 @@ def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
|
||||
raw = fetch_option_positions(ex) if ex is not None else []
|
||||
has_standalone = has_standalone_option_position(conn, raw or [])
|
||||
except Exception:
|
||||
has_standalone = False
|
||||
has_standalone = True # fail-closed
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
active = 0
|
||||
has_standalone = False
|
||||
# fail-closed:探测失败视为不可开仓
|
||||
active = 10**9
|
||||
has_standalone = True
|
||||
return gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=load_position_sizing_mode(),
|
||||
@@ -220,31 +221,45 @@ def _gates_public(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
|
||||
if not _hedge_enabled():
|
||||
return
|
||||
try:
|
||||
secs = float(os.getenv("HEDGE_PLAN_MONITOR_POLL_SECONDS") or "15")
|
||||
except ValueError:
|
||||
secs = 15.0
|
||||
secs = max(5.0, secs)
|
||||
# 始终启动监控线程:单独期权模式下仍需收口遗留 active/partial 计划
|
||||
with _hedge_start_lock():
|
||||
if cfg.get("hedge_monitor_thread") is not None:
|
||||
return
|
||||
try:
|
||||
secs = float(os.getenv("HEDGE_PLAN_MONITOR_POLL_SECONDS") or "15")
|
||||
except ValueError:
|
||||
secs = 15.0
|
||||
secs = max(5.0, secs)
|
||||
|
||||
def _loop() -> None:
|
||||
import time
|
||||
def _loop() -> None:
|
||||
import time
|
||||
|
||||
from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans
|
||||
from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans
|
||||
|
||||
while True:
|
||||
try:
|
||||
tick_active_plans(cfg)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(secs)
|
||||
while True:
|
||||
try:
|
||||
tick_active_plans(cfg)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(secs)
|
||||
|
||||
import threading
|
||||
import threading
|
||||
|
||||
t = threading.Thread(target=_loop, name="hedge-plan-monitor", daemon=True)
|
||||
t.start()
|
||||
cfg["hedge_monitor_thread"] = t
|
||||
t = threading.Thread(target=_loop, name="hedge-plan-monitor", daemon=True)
|
||||
t.start()
|
||||
cfg["hedge_monitor_thread"] = t
|
||||
|
||||
|
||||
_start_lock = None
|
||||
|
||||
|
||||
def _hedge_start_lock():
|
||||
global _start_lock
|
||||
if _start_lock is None:
|
||||
import threading
|
||||
|
||||
_start_lock = threading.Lock()
|
||||
return _start_lock
|
||||
|
||||
|
||||
def _start_body_json(body: dict[str, Any], missing_leg: Optional[str] = None) -> str:
|
||||
@@ -566,37 +581,38 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
body = request.get_json(silent=True) or {}
|
||||
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
|
||||
dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
|
||||
gates = _gates_dict(cfg, plan_type)
|
||||
if not dry_run and not gates.get("can_start"):
|
||||
return jsonify(
|
||||
{"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可开仓"]), "gates": gates}
|
||||
), 400
|
||||
err = validate_start_body(plan_type, body)
|
||||
if err:
|
||||
return jsonify({"ok": False, "msg": err, "gates": gates}), 400
|
||||
# 补齐永续杠杆
|
||||
if plan_type == "perp_options" and not body.get("leverage"):
|
||||
base = str(body.get("underlying") or "ETH").upper()
|
||||
body["leverage"] = cfg.get("btc_leverage") if base == "BTC" else (cfg.get("btc_leverage") or 10)
|
||||
# ETH 也用 BTC 档 10x 按方案;ALT 为 alt_leverage 仅非 BTC/ETH
|
||||
if base in ("BTC", "ETH"):
|
||||
body["leverage"] = int(cfg.get("btc_leverage") or 10)
|
||||
if plan_type == "options_options":
|
||||
out = execute_options_options_start(
|
||||
cfg,
|
||||
body,
|
||||
dry_run=dry_run,
|
||||
persist=(None if dry_run else (lambda r, b: _persist_oo(cfg, r, b))),
|
||||
)
|
||||
else:
|
||||
out = execute_perp_options_start(
|
||||
cfg,
|
||||
body,
|
||||
dry_run=dry_run,
|
||||
persist=(None if dry_run else (lambda r, b: _persist_po(cfg, r, b))),
|
||||
)
|
||||
out["gates"] = gates
|
||||
return jsonify(out), (200 if out.get("ok") else 400)
|
||||
with _hedge_start_lock():
|
||||
gates = _gates_dict(cfg, plan_type)
|
||||
if not dry_run and not gates.get("can_start"):
|
||||
return jsonify(
|
||||
{"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可开仓"]), "gates": gates}
|
||||
), 400
|
||||
err = validate_start_body(plan_type, body)
|
||||
if err:
|
||||
return jsonify({"ok": False, "msg": err, "gates": gates}), 400
|
||||
# 补齐永续杠杆
|
||||
if plan_type == "perp_options" and not body.get("leverage"):
|
||||
base = str(body.get("underlying") or "ETH").upper()
|
||||
body["leverage"] = cfg.get("btc_leverage") if base == "BTC" else (cfg.get("btc_leverage") or 10)
|
||||
# ETH 也用 BTC 档 10x 按方案;ALT 为 alt_leverage 仅非 BTC/ETH
|
||||
if base in ("BTC", "ETH"):
|
||||
body["leverage"] = int(cfg.get("btc_leverage") or 10)
|
||||
if plan_type == "options_options":
|
||||
out = execute_options_options_start(
|
||||
cfg,
|
||||
body,
|
||||
dry_run=dry_run,
|
||||
persist=(None if dry_run else (lambda r, b: _persist_oo(cfg, r, b))),
|
||||
)
|
||||
else:
|
||||
out = execute_perp_options_start(
|
||||
cfg,
|
||||
body,
|
||||
dry_run=dry_run,
|
||||
persist=(None if dry_run else (lambda r, b: _persist_po(cfg, r, b))),
|
||||
)
|
||||
out["gates"] = gates
|
||||
return jsonify(out), (200 if out.get("ok") else 400)
|
||||
|
||||
@app.route("/api/hedge-plan/<int:plan_id>/end", methods=["POST"])
|
||||
@lr
|
||||
@@ -634,12 +650,19 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
|
||||
if not dry_run and not _hedge_enabled():
|
||||
return jsonify({"ok": False, "msg": "当前交易模式为单独期权,不可补开对冲腿"}), 400
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
plan = get_plan(conn, plan_id)
|
||||
if not plan:
|
||||
return jsonify({"ok": False, "msg": "计划不存在"}), 404
|
||||
pt = str(plan.get("plan_type") or "")
|
||||
if pt == "perp_options" and not _show_perp_options():
|
||||
return jsonify({"ok": False, "msg": "当前模式非永期对冲,不可补开"}), 400
|
||||
if pt == "options_options" and not _show_options_options():
|
||||
return jsonify({"ok": False, "msg": "当前模式非期期对冲,不可补开"}), 400
|
||||
if str(plan.get("status") or "") != "partial":
|
||||
return jsonify({"ok": False, "msg": "仅半腿待补(partial)计划可补开"}), 400
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
data-default-underly="{{ options_default_underly | default('ETH') }}"
|
||||
data-hedge-enabled="{{ '1' if hedge_plan_enabled else '0' }}"
|
||||
data-options-enabled="{{ '1' if options_enabled else '0' }}"
|
||||
data-show-perp="{{ '1' if hedge_plan_show_perp_options | default(true) else '0' }}"
|
||||
data-show-oo="{{ '1' if hedge_plan_show_options_options | default(true) else '0' }}"
|
||||
data-oo-close-mode-enabled="{{ '1' if hedge_plan_oo_close_mode_enabled | default(true) else '0' }}"
|
||||
data-show-perp="{{ '1' if hedge_plan_show_perp_options else '0' }}"
|
||||
data-show-oo="{{ '1' if hedge_plan_show_options_options else '0' }}"
|
||||
data-oo-close-mode-enabled="{{ '1' if hedge_plan_oo_close_mode_enabled else '0' }}"
|
||||
data-budget-buffer="{{ hedge_plan_budget_buffer | default(0.95) }}"
|
||||
data-sizing-mode="{{ position_sizing_mode | default('risk') }}"
|
||||
data-is-full-margin="{{ '1' if position_sizing_mode == 'full_margin' else '0' }}">
|
||||
@@ -14,8 +14,8 @@
|
||||
{% if not options_enabled %}
|
||||
<div class="flash" style="margin-bottom:12px">期权模块未启用,无法拉期权链.请先配置期权账户.</div>
|
||||
{% endif %}
|
||||
{% if hedge_plan_enabled and not (hedge_plan_show_perp_options | default(true)) and not (hedge_plan_show_options_options | default(true)) %}
|
||||
<div class="flash" style="margin-bottom:12px">永期与期期 Tab 均已隐藏:可在 <code>env配置 → 对冲计划</code> 打开显示开关;进行中/历史仍可查看.</div>
|
||||
{% if hedge_plan_enabled and not hedge_plan_show_perp_options and not hedge_plan_show_options_options %}
|
||||
<div class="flash" style="margin-bottom:12px">永期与期期 Tab 均已隐藏:请在 env「期权/对冲模式」切换交易模式;进行中/历史仍可查看.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card hp-head-card">
|
||||
@@ -26,10 +26,10 @@
|
||||
<button type="button" class="btn-secondary" id="hp-refresh" title="刷新永续行情与期权链">刷新行情</button>
|
||||
</div>
|
||||
<div class="hp-tabs" role="tablist" aria-label="对冲计划分类">
|
||||
{% if hedge_plan_show_perp_options | default(true) %}
|
||||
{% if hedge_plan_show_perp_options %}
|
||||
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="perp_options">永期对冲</button>
|
||||
{% endif %}
|
||||
{% if hedge_plan_show_options_options | default(true) %}
|
||||
{% if hedge_plan_show_options_options %}
|
||||
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="options_options">期期对冲</button>
|
||||
{% endif %}
|
||||
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="active">进行中的计划</button>
|
||||
|
||||
@@ -402,7 +402,8 @@ def build_instance_dashboard_payload(
|
||||
rolls = collect_rolls(conn)
|
||||
strategy_items = trends + rolls
|
||||
options_items = collect_options_items(fetch_options_positions, conn=conn)
|
||||
hedge_items = collect_hedge_plans(conn) if hedge_enabled else []
|
||||
hedge_items = collect_hedge_plans(conn) # 始终展示进行中计划,与当前交易模式无关
|
||||
# hedge_enabled 仅影响「新建」入口,不隐藏已有仓
|
||||
now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||
return {
|
||||
"ok": True,
|
||||
|
||||
@@ -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