diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py
index 8fe87cc..fb1da1f 100644
--- a/crypto_monitor_binance/app.py
+++ b/crypto_monitor_binance/app.py
@@ -10042,14 +10042,14 @@ def _hub_meta_bundle():
def _hub_account_bundle():
- # 中控看板高频拉取:仅走余额缓存,避免额外 fetch_balance
- funding_capital, trading_capital = get_exchange_capitals(force=False)
+ funding_capital, trading_capital = get_exchange_capitals(force=True)
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
trading_usdt = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else None
+ available = get_available_trading_usdt()
return {
"funding_usdt": funding_usdt,
"trading_usdt": trading_usdt,
- "available_trading_usdt": trading_usdt,
+ "available_trading_usdt": round(available, FUNDS_DECIMALS) if available is not None else None,
"trading_day": get_trading_day(app_now()),
}
diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py
index cdfe660..6c611ed 100644
--- a/crypto_monitor_gate/app.py
+++ b/crypto_monitor_gate/app.py
@@ -467,8 +467,6 @@ from lib.exchange.gate_ccxt_lib import gate_ccxt_class
# Gate.io USDT 永续(swap)
exchange = gate_ccxt_class()({
"enableRateLimit": True,
- # 避免关键位监控/账户拉取无限挂起拖垮中控
- "timeout": int(os.getenv("GATE_CCXT_TIMEOUT_MS", "8000")),
"options": {
"defaultType": "swap",
"defaultMarginMode": _GATE_DEFAULT_MARGIN_MODE,
@@ -2834,13 +2832,13 @@ def friendly_exchange_error(err, available_usdt=None):
return f"交易所下单失败:{clean}"
-_BALANCE_REFRESH_LOCK = threading.Lock()
-_BALANCE_REFRESH_INFLIGHT = False
-
-
-def _fetch_exchange_capitals_sync():
- """同步拉取资金/交易账户余额并写入缓存(会占用 ccxt,勿在中控热路径直接调用)."""
+def get_exchange_capitals(force=False):
+ ok_live, _ = ensure_exchange_live_ready()
+ if not ok_live:
+ return None, None
now_ts = time.time()
+ if (not force) and ACCOUNT_BALANCE_CACHE["updated_at"] and now_ts - ACCOUNT_BALANCE_CACHE["updated_at"] < BALANCE_REFRESH_SECONDS:
+ return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
try:
ACCOUNT_BALANCE_CACHE["funding_usdt"] = _fetch_gate_funding_usdt()
except Exception:
@@ -2854,43 +2852,6 @@ def _fetch_exchange_capitals_sync():
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
-def _kick_exchange_capitals_refresh():
- """后台刷新余额;与 key_rs 争用时不阻塞看板/中控请求."""
- global _BALANCE_REFRESH_INFLIGHT
- with _BALANCE_REFRESH_LOCK:
- if _BALANCE_REFRESH_INFLIGHT:
- return
- _BALANCE_REFRESH_INFLIGHT = True
-
- def _worker():
- global _BALANCE_REFRESH_INFLIGHT
- try:
- _fetch_exchange_capitals_sync()
- except Exception:
- pass
- finally:
- with _BALANCE_REFRESH_LOCK:
- _BALANCE_REFRESH_INFLIGHT = False
-
- threading.Thread(target=_worker, name="gate-balance-refresh", daemon=True).start()
-
-
-def get_exchange_capitals(force=False):
- ok_live, _ = ensure_exchange_live_ready()
- if not ok_live:
- return None, None
- now_ts = time.time()
- has_cache = bool(ACCOUNT_BALANCE_CACHE["updated_at"])
- fresh = has_cache and (now_ts - ACCOUNT_BALANCE_CACHE["updated_at"] < BALANCE_REFRESH_SECONDS)
- if (not force) and fresh:
- return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
- if not force:
- # 过期/冷缓存:先返回现有值(可空),后台刷新,避免 /api/hub/account 被 RS 监控拖死
- _kick_exchange_capitals_refresh()
- return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
- return _fetch_exchange_capitals_sync()
-
-
def execute_transfer_usdt(amount, from_account, to_account):
from lib.exchange.gate_transfer_lib import execute_transfer_usdt as _gate_execute_transfer_usdt
@@ -4650,25 +4611,14 @@ def _finalize_key_monitor_one_shot(conn, row, last_msg, close_reason):
conn.execute("DELETE FROM key_monitors WHERE id=?", (row["id"],))
-_RS_BAR_CACHE: dict[str, dict] = {}
-_RS_BAR_CACHE_TTL_SEC = float(os.getenv("GATE_RS_BAR_CACHE_SEC", "45"))
-
-
def _fetch_last_closed_bar(symbol):
- """最近一根闭合 K:[ts, o, h, l, c, v] 或 None.短缓存减轻关键位监控打爆 ccxt."""
+ """最近一根闭合 K:[ts, o, h, l, c, v] 或 None."""
ex_sym = normalize_exchange_symbol(symbol)
- now = time.time()
- cached = _RS_BAR_CACHE.get(ex_sym)
- if cached and now - float(cached.get("updated_at") or 0) < _RS_BAR_CACHE_TTL_SEC:
- return cached.get("bar")
bars = exchange.fetch_ohlcv(ex_sym, timeframe=KLINE_TIMEFRAME, limit=5) or []
if len(bars) < 2:
- _RS_BAR_CACHE[ex_sym] = {"updated_at": now, "bar": None}
return None
closed = bars[:-1]
- bar = closed[-1] if closed else None
- _RS_BAR_CACHE[ex_sym] = {"updated_at": now, "bar": bar}
- return bar
+ return closed[-1] if closed else None
def _key_rs_gate_preview(symbol, upper, lower):
@@ -9943,14 +9893,14 @@ def _hub_meta_bundle():
def _hub_account_bundle():
- # 中控看板高频拉取:仅走余额缓存;不再额外 fetch_balance(会与关键位监控争用 ccxt)
- funding_capital, trading_capital = get_exchange_capitals(force=False)
+ funding_capital, trading_capital = get_exchange_capitals(force=True)
funding_usdt = round(funding_capital, 2) if funding_capital is not None else None
trading_usdt = round(trading_capital, 2) if trading_capital is not None else None
+ available = get_available_trading_usdt()
return {
"funding_usdt": funding_usdt,
"trading_usdt": trading_usdt,
- "available_trading_usdt": trading_usdt,
+ "available_trading_usdt": round(available, 2) if available is not None else None,
"trading_day": get_trading_day(app_now()),
}
diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py
index c7be6b0..7a609be 100644
--- a/crypto_monitor_okx/app.py
+++ b/crypto_monitor_okx/app.py
@@ -9665,14 +9665,14 @@ def _hub_meta_bundle():
def _hub_account_bundle():
- # 中控看板高频拉取:仅走余额缓存,避免额外 fetch_balance
- funding_capital, trading_capital = get_exchange_capitals(force=False)
+ funding_capital, trading_capital = get_exchange_capitals(force=True)
funding_usdt = round(funding_capital, FUNDS_DECIMALS) if funding_capital is not None else None
trading_usdt = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else None
+ available = get_available_trading_usdt()
return {
"funding_usdt": funding_usdt,
"trading_usdt": trading_usdt,
- "available_trading_usdt": trading_usdt,
+ "available_trading_usdt": round(available, FUNDS_DECIMALS) if available is not None else None,
"trading_day": get_trading_day(app_now()),
}
diff --git a/docs/审计修复报告-WS回滚与中控可用-2026-08-11-R1.md b/docs/审计修复报告-WS回滚与中控可用-2026-08-11-R1.md
deleted file mode 100644
index bfa2a1e..0000000
--- a/docs/审计修复报告-WS回滚与中控可用-2026-08-11-R1.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# 审计修复报告 · WS 回滚与中控可用性(2026-08-11 · 第 1 轮)
-
-## 背景
-
-期权链接入 OKX WS 推送后,生产中控出现「期权数据不可用 / 子代理不可用」。按要求**先回滚 WS 链路**,再全量审计并修复。
-
-## 回滚
-
-| 提交 | 说明 |
-|------|------|
-| `a488e2f` | Revert fast-path(依赖 WS 热缓存) |
-| `d592632` | Revert OKX WS + SSE 推送整栈 |
-
-恢复为 **REST 拉链 + 前端约 15s soft-poll**(commit `24bb853` 行为),删除:
-
-- `lib/exchange/okx_public_ws_lib.py`
-- `lib/options/options_quote_live_lib.py`
-- `tests/test_options_quote_live_lib.py`
-
-## 根因结论(非仅 WS)
-
-| 级别 | 问题 | 证据 |
-|------|------|------|
-| Critical | Gate 关键位 RS 监控在后台线程高频 `fetch_ohlcv`,与 `/api/hub/account` 争用同一 ccxt 客户端,账户/子代理超时 | 日志 `[key_rs_level_alert] BTC/USDT id=13`;本机 `5000/api/hub/account` 25s 超时 |
-| Critical | 中控期权快照对每仓拉 books 深度,易超 `HUB_FLASK_TIMEOUT=10` | `build_display_option_positions` → `attach_close_preview` → `fetch_option_book_depth` |
-| High | Soft 拉链 3 次重试 + 无单飞,易与 SSE tick 叠打 OKX | `options_panel.js` loadChain |
-| High | 中控账户接口每轮 `force=True` 绕过余额缓存 | Gate/OKX/Binance `_hub_account_bundle` |
-| Medium | Flask 超时错误只有 `error` 无 `msg`,前端易落默认文案 | `hub.py` `_fetch_flask_json` |
-| Medium | `options` 为 null 时前端当成「0 仓」而非不可用 | `app.js` renderOptionsSectionBody |
-
-WS 部署触发的**全进程重启**放大了 Gate 争用与快照超时,表现为「全不可用」;OKX 快照在轻负载下仍可 `ok:true`。
-
-## 本轮修复
-
-1. **Hub 期权快照**关闭逐仓 `close_preview`/books(`with_close_preview=False`)
-2. **Hub 账户**三所改为 `get_exchange_capitals(force=False)`
-3. **Gate ccxt** 增加 `timeout=8000ms`;RS K 线 **45s 缓存**
-4. **期权 tickers** 恢复 **10s** 短缓存(无 WS)
-5. **前端 soft 拉链**:单飞 + soft 仅 1 次尝试;已有链不先清空表格
-6. **Hub**:超时补 `msg`;期权快照与 account/monitor **并行 gather**
-7. **中控 UI**:capabilities 含 options 且 snapshot 缺失时显式「期权数据不可用」
-
-## 测试建议
-
-- 强刷中控监控区:OKX 期权资金/持仓应恢复;Gate 子代理 status 应在数秒内恢复
-- 期权页「刷新链」不应长时间白屏;指数行显示约 15s 静默刷新
-- Gate 关键位监控日志不应再每秒刷屏 `fetch_ohlcv` 失败
-
-## 残留风险(交第 2 轮)
-
-- Gate 仍与监控共用单一 ccxt 客户端(未加全局锁)
-- 中控 board 仍可能被最慢交易所拉长整轮等待
-- Soft-poll 仍是 REST,非真·实时
diff --git a/docs/审计修复报告-WS回滚与中控可用-2026-08-11-R2.md b/docs/审计修复报告-WS回滚与中控可用-2026-08-11-R2.md
deleted file mode 100644
index da1f87f..0000000
--- a/docs/审计修复报告-WS回滚与中控可用-2026-08-11-R2.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# 审计修复报告 · WS 回滚与中控可用性(2026-08-11 · 第 2 轮)
-
-## 范围
-
-复查第 1 轮修复是否引入回归,并扫清仍会导致「中控不可用」的残留高优先级问题。
-
-## 复查结论
-
-| 项 | 结论 |
-|----|------|
-| Hub 并行 options 索引进位 | 正确(day / options 组合无错位) |
-| Hub 关闭 close_preview | UI 降级为 upl/`—`,不崩 |
-| Gate RS 缓存 / timeout | timeout 已为 int;缓存可接受 |
-| board row capabilities | `_fetch_agent_status` 始终带上 |
-| `with_close_preview` 默认 | 实例路径仍为 True |
-
-## 本轮新发现问题与修复
-
-| 级别 | 问题 | 修复 |
-|------|------|------|
-| High | Hub 账户在 `force=False` 后仍调用 `get_available_trading_usdt()` 再打一枪 `fetch_balance`,Gate 争用依旧 | 三所 `_hub_account_bundle` 改为用缓存的 `trading_usdt` 作为 `available_trading_usdt` |
-| Medium | `loadChain` soft 门禁在 `seq++` 之后,叠刷可导致 `chainLoadInFlight` 永不清理 | soft 门禁移到 `seq++` 之前 |
-
-## 与第 1 轮一并交付的状态
-
-- WS 推送链路已回滚(REST + 15s soft-poll)
-- 中控期权快照轻量化 + 并行拉取
-- Gate RS K 线短缓存 + ccxt timeout
-- 期权 tickers 10s 缓存;soft 单飞/单次尝试
-
-## 已知残留(不阻塞本次部署)
-
-- Gate 监控与账户仍共用单一 ccxt 客户端(无全局锁)
-- 中控 board 仍可能被最慢交易所拉长整轮
-- Soft-poll 非真·实时报价
-
-## 部署后验收
-
-1. 中控强刷:OKX 期权区有资金数字,不再长期「期权数据不可用」
-2. Gate 卡:子代理恢复绿色/有资金;不再长时间「子代理不可用」
-3. 期权页刷新链不白屏;约 15s 静默更新时间戳
diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js
index 868543b..dd2ecf7 100644
--- a/lib/common/static/hedge_plan.js
+++ b/lib/common/static/hedge_plan.js
@@ -1165,9 +1165,14 @@
fillExpSelect($("hp-oo-exp-select"), d);
renderListStrikes();
renderTStrikes();
- // 期期盈亏比默认 2,不再用指数自动填上破/下破
- if ($("hp-oo-rr") && !$("hp-oo-rr").value) {
- $("hp-oo-rr").value = "2";
+ if (d.index_px) {
+ const idx = Number(d.index_px);
+ if ($("hp-target-up") && !$("hp-target-up").value) {
+ $("hp-target-up").value = String(Math.round(idx * 1.03));
+ }
+ if ($("hp-target-down") && !$("hp-target-down").value) {
+ $("hp-target-down").value = String(Math.round(idx * 0.97));
+ }
}
}
@@ -1576,7 +1581,8 @@
if ($("hp-contracts")) $("hp-contracts").value = "";
if ($("hp-tp")) $("hp-tp").value = "";
if ($("hp-sl")) $("hp-sl").value = "";
- if ($("hp-oo-rr")) $("hp-oo-rr").value = "2";
+ if ($("hp-target-up")) $("hp-target-up").value = "";
+ if ($("hp-target-down")) $("hp-target-down").value = "";
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
if ($("hp-premium-line")) $("hp-premium-line").textContent = "";
if ($("hp-oo-sheets-a")) {
@@ -1612,12 +1618,16 @@
if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) {
throw new Error("期期两腿须为平值或虚值,不可选实值");
}
- const rr = numInput("hp-oo-rr", 2);
- if (!(rr > 0)) throw new Error("请填写盈亏比(相对权利金,默认2)");
+ const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0);
+ const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0);
+ if (!up || !down) throw new Error("请填写上破与下破目标价");
+ if (up <= down) throw new Error("上破目标价必须大于下破目标价");
body = {
plan_type: "options_options",
- oo_profit_rr: rr,
- index_px: indexPx() || 0,
+ target_price_up: up,
+ target_price_down: down,
+ target_price: up,
+ index_px: indexPx() || (up + down) / 2,
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
};
@@ -1709,44 +1719,28 @@
fmt(s.premium_paid) +
(s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : "");
} else {
- const rr = s.oo_profit_rr != null ? s.oo_profit_rr : s.rr_target;
- const tgt = s.target_profit != null ? s.target_profit : s.at_target_total;
- if (rr != null) {
- summary.innerHTML =
- "盈亏比 ×" +
- fmt(rr, 2) +
- " · 目标盈利 " +
- fmtPnlHtml(tgt) +
- " · 到期现价 " +
- fmtPnlHtml(s.expiry_flat_total) +
- " · 保费 " +
- fmt(s.premium_paid) +
- '(达标全平;不达标等到期)' +
- (s.expiry_is_loss ? " · 到期现价情景为亏" : "");
- } else {
- const upTot = s.at_target_up_total != null ? s.at_target_up_total : s.at_target_total;
- const dnTot = s.at_target_down_total;
- let rrLine = "";
- if (s.rr_at_up != null || s.rr_at_down != null) {
- rrLine =
- " · 盈亏比 上破 " +
- fmtRr(s.rr_at_up) +
- (dnTot != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") +
- '(亏=全额保费 ' +
- fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) +
- ")";
- }
- summary.innerHTML =
- "上破 " +
- fmtPnlHtml(upTot) +
- (dnTot != null ? " · 下破 " + fmtPnlHtml(dnTot) : "") +
- " · 到期现价 " +
- fmtPnlHtml(s.expiry_flat_total) +
- " · 保费 " +
- fmt(s.premium_paid) +
- rrLine +
- (s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : "");
+ const upTot = s.at_target_up_total != null ? s.at_target_up_total : s.at_target_total;
+ const dnTot = s.at_target_down_total;
+ let rrLine = "";
+ if (s.rr_at_up != null || s.rr_at_down != null) {
+ rrLine =
+ " · 盈亏比 上破 " +
+ fmtRr(s.rr_at_up) +
+ (dnTot != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") +
+ '(亏=全额保费 ' +
+ fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) +
+ ")";
}
+ summary.innerHTML =
+ "上破 " +
+ fmtPnlHtml(upTot) +
+ (dnTot != null ? " · 下破 " + fmtPnlHtml(dnTot) : "") +
+ " · 到期现价 " +
+ fmtPnlHtml(s.expiry_flat_total) +
+ " · 保费 " +
+ fmt(s.premium_paid) +
+ rrLine +
+ (s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : "");
}
}
if (!tbody) return;
@@ -2063,7 +2057,8 @@
"hp-tp",
"hp-sl",
"hp-sheets",
- "hp-oo-rr",
+ "hp-target-up",
+ "hp-target-down",
]);
if ($("hp-preview-btn"))
$("hp-preview-btn").addEventListener("click", function () {
@@ -2176,9 +2171,6 @@
if (p.plan_type === "perp_options") {
return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl);
}
- if (p.oo_profit_rr != null && Number(p.oo_profit_rr) > 0) {
- return "盈亏比 ×" + fmt(p.oo_profit_rr, 2) + "(达标全平)";
- }
return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price);
}
@@ -2338,8 +2330,6 @@
target_win_leg: "期期平盈利腿",
target_up_win_leg: "期期上破·平盈利腿",
target_down_win_leg: "期期下破·平盈利腿",
- oo_rr_target: "期期盈亏比达标",
- oo_rr_closing: "期期盈亏比平仓中",
oo_rest_closing: "期期全平·清残腿中",
oo_rest_closed: "期期全平·两腿已平",
orphaned_after_tp: "止盈后持有至到期",
@@ -2414,11 +2404,6 @@
"x · 张数 " +
fmt(p.perp_size, 4) +
"";
- } else if (p.oo_profit_rr != null && Number(p.oo_profit_rr) > 0) {
- html +=
- "
目标价 上破 " +
@@ -2641,13 +2626,17 @@
if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) {
throw new Error("期期两腿须为平值或虚值,不可选实值");
}
- const rr = numInput("hp-oo-rr", 2);
- if (!(rr > 0)) throw new Error("请填写盈亏比(相对权利金,默认2)");
+ const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0);
+ const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0);
+ if (!up || !down) throw new Error("请填写上破与下破目标价");
+ if (up <= down) throw new Error("上破目标价必须大于下破目标价");
body = {
plan_type: "options_options",
underlying: state.underlying,
- oo_profit_rr: rr,
- index_px: indexPx() || 0,
+ target_price_up: up,
+ target_price_down: down,
+ target_price: up,
+ index_px: indexPx() || (up + down) / 2,
oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry",
oo_sheets_mode: state.ooSheetsMode || "same_sheets",
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index 7085b12..30e00f4 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -37,15 +37,9 @@
let selectSeq = 0;
let refreshAllTimer = null;
let pendingRefreshTimer = null;
- let chainSoftTimer = null;
- let lastChainSoftAt = 0;
- let chainQuotedAt = 0;
- let chainLoadInFlight = false;
let pendingTtlSeconds = 600;
const POSITIONS_STALE_MS = 45000;
const PENDING_POLL_MS = 8000;
- /** 链卖一/买一静默刷新节流:无推送,靠拉;过密会撞 OKX 50011 */
- const CHAIN_SOFT_POLL_MS = 15000;
const orderPanelHome = (function () {
const host = document.getElementById("opt-order-panel-host");
return host ? host.parentElement : null;
@@ -327,7 +321,7 @@
[
"opt-sheets-amount",
"opt-eth-amount",
- "opt-profit-rr",
+ "opt-target-idx",
].forEach(function (id) {
harden(document.getElementById(id));
});
@@ -638,15 +632,6 @@
if (el) el.textContent = fmt(buf, 2);
}
- function fmtChainQuotedAt() {
- if (!chainQuotedAt) return "";
- const d = new Date(chainQuotedAt);
- const pad = function (n) {
- return n < 10 ? "0" + n : String(n);
- };
- return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
- }
-
function renderIndexLine() {
const idx = state.chain && state.chain.index_px;
const dte = state.chain && state.chain.chain_max_dte_days;
@@ -660,37 +645,12 @@
const line = document.getElementById("opt-index-line");
if (line) {
const liqHint = askLiqFilterOn() ? "仅显示卖一深度≥1张" : "显示全部卖一(含估算~)";
- const ageHint = chainQuotedAt ? " · 链报价 " + fmtChainQuotedAt() + "(约每15s静默刷新)" : "";
line.textContent =
"指数 " + state.underlying + " ≈ " + fmt(idx, 2) +
- " · 默认最近一期 · " + liqHint + " · 实值含平值 · 虚值=价外" + ageHint;
+ " · 默认最近一期 · " + liqHint + " · 实值含平值 · 虚值=价外";
}
}
- function softRefreshChainThrottled(force) {
- if (document.hidden) return;
- if (!document.getElementById("options-root")) return;
- if (chainLoadInFlight) return;
- const now = Date.now();
- if (!force && now - lastChainSoftAt < CHAIN_SOFT_POLL_MS) return;
- lastChainSoftAt = now;
- void loadChain({ soft: true });
- }
-
- function startChainSoftPoll() {
- if (chainSoftTimer) return;
- chainSoftTimer = setInterval(function () {
- if (!document.getElementById("options-root")) {
- if (chainSoftTimer) {
- clearInterval(chainSoftTimer);
- chainSoftTimer = null;
- }
- return;
- }
- softRefreshChainThrottled(false);
- }, CHAIN_SOFT_POLL_MS);
- }
-
function pickNearestExpiry(exps) {
if (!exps || !exps.length) return "";
const now = Date.now();
@@ -934,10 +894,11 @@
}
function updateOrderEstimates() {
+ const levEl = document.getElementById("opt-order-leverage");
const valueEl = document.getElementById("opt-est-value");
const profitEl = document.getElementById("opt-est-profit");
- const levEl = document.getElementById("opt-order-leverage");
- const rrEl = document.getElementById("opt-profit-rr");
+ const targetLevEl = document.getElementById("opt-est-leverage");
+ const targetEl = document.getElementById("opt-target-idx");
const q = state.orderQuote;
if (!q || !q.ok || !q.can_open) {
if (levEl) levEl.textContent = "—";
@@ -946,6 +907,7 @@
profitEl.textContent = "—";
profitEl.className = "v";
}
+ if (targetLevEl) targetLevEl.textContent = "—";
return;
}
const sz = q.sizing || {};
@@ -954,19 +916,30 @@
const lev = calcContractLeverage(q.index_px, ethAmount, premium);
if (levEl) levEl.textContent = fmtLeverage(lev);
- if (valueEl && profitEl && rrEl) {
- const rrRaw = rrEl.value;
- const rr = rrRaw === "" || rrRaw == null ? NaN : Number(rrRaw);
- if (!Number.isFinite(rr) || rr <= 0 || !(Number(premium) > 0)) {
+ if (valueEl && profitEl && targetEl) {
+ const targetRaw = targetEl.value;
+ if (targetRaw === "" || targetRaw == null) {
valueEl.textContent = "—";
profitEl.textContent = "—";
profitEl.className = "v";
+ if (targetLevEl) targetLevEl.textContent = "—";
} else {
- const targetProfit = Number(premium) * rr;
- const needRecycle = Number(premium) + targetProfit;
- valueEl.textContent = fmtUsdc(needRecycle) + " USDC";
- profitEl.textContent = fmtUsdcSigned(targetProfit);
- profitEl.className = "v " + pnlCls(targetProfit);
+ const value = estimateExpiryValue(q.opt_type, q.strike, Number(targetRaw), ethAmount);
+ const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), ethAmount, premium);
+ if (value == null || Number.isNaN(value)) {
+ valueEl.textContent = "—";
+ } else {
+ valueEl.textContent = fmtUsdc(value) + " USDC";
+ }
+ if (profit == null || Number.isNaN(profit)) {
+ profitEl.textContent = "—";
+ profitEl.className = "v";
+ } else {
+ profitEl.textContent = fmtUsdcSigned(profit);
+ profitEl.className = "v " + pnlCls(profit);
+ }
+ const targetLev = calcContractLeverage(Number(targetRaw), ethAmount, premium);
+ if (targetLevEl) targetLevEl.textContent = fmtLeverage(targetLev);
}
}
}
@@ -1241,16 +1214,11 @@
async function loadChain(opts) {
const soft = !!(opts && opts.soft);
- // soft 门禁必须在 seq++ 之前,否则叠刷会抬高 seq 导致 inFlight 永不清理
- if (chainLoadInFlight && soft) return;
const uly = state.underlying;
const seq = ++chainLoadSeq;
const btn = document.getElementById("opt-load-chain");
- const hadChain = chainHasExpiries(state.chain) && state.chain.underlying === uly;
- chainLoadInFlight = true;
if (btn && !soft) btn.disabled = true;
- // 已有链时不先清空,避免刷新白屏
- if (!soft && !hadChain) {
+ if (!soft) {
setExpirySelectStatus("加载到期日中…");
const tbody = document.getElementById("opt-strike-tbody");
if (tbody) {
@@ -1261,27 +1229,16 @@
try {
let d = null;
let lastMsg = "";
- // soft 只试 1 次,避免与 15s 轮询叠加重试打爆 OKX
- const maxAttempts = soft ? 1 : 3;
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ for (let attempt = 0; attempt < 2; attempt++) {
if (seq !== chainLoadSeq) return;
d = await apiJson("/api/options/chain?underlying=" + encodeURIComponent(uly));
if (seq !== chainLoadSeq) return;
if (d && d.ok && chainHasExpiries(d)) break;
lastMsg = (d && (d.msg || d.chain_error)) || "暂无到期日";
- const rateLimited =
- !!(d && d.rate_limited) ||
- /50011|Too Many Requests|过于频繁/i.test(String(lastMsg || ""));
d = null;
- if (attempt < maxAttempts - 1) {
- if (!soft && !hadChain) {
- setExpirySelectStatus(
- rateLimited ? "OKX 限频,稍后重试…" : "重试加载到期日…"
- );
- }
- await new Promise(function (resolve) {
- setTimeout(resolve, rateLimited ? 1200 * (attempt + 1) : 400);
- });
+ if (attempt === 0) {
+ if (!soft) setExpirySelectStatus("重试加载到期日…");
+ await new Promise(function (resolve) { setTimeout(resolve, 400); });
}
}
if (seq !== chainLoadSeq) return;
@@ -1296,19 +1253,13 @@
if (soft) return;
setExpirySelectStatus("选择到期日");
const tbody = document.getElementById("opt-strike-tbody");
- const friendly =
- /50011|Too Many Requests|过于频繁/i.test(String(lastMsg || ""))
- ? "OKX 请求过于频繁,请稍后再点「刷新链」"
- : lastMsg || "暂无到期日,请点「刷新链」";
if (tbody) {
tbody.innerHTML =
- '
| ' +
- friendly +
+ ' |
| ' +
+ (lastMsg || "暂无到期日,请点「刷新链」") +
" |
";
}
- alert(friendly);
+ alert(lastMsg || "加载到期日失败,请点「刷新链」重试");
return;
}
const keepExp = soft ? (document.getElementById("opt-exp-select") || {}).value : "";
@@ -1316,8 +1267,6 @@
panelCache.chain = d;
panelCache.underlying = uly;
panelCache.optType = state.optType;
- chainQuotedAt = Date.now();
- lastChainSoftAt = chainQuotedAt;
syncAskLiqFilterFromChain(d);
if (!soft) {
state.selectedInst = null;
@@ -1348,10 +1297,7 @@
"";
}
} finally {
- if (seq === chainLoadSeq) {
- chainLoadInFlight = false;
- if (btn) btn.disabled = false;
- }
+ if (seq === chainLoadSeq && btn) btn.disabled = false;
}
}
@@ -1383,13 +1329,15 @@
} else if (mode === "sheets") {
body.sheets = parseInt(document.getElementById("opt-sheets-amount").value, 10);
}
- const rrRaw = (document.getElementById("opt-profit-rr").value || "").trim();
- const rr = rrRaw === "" ? 2 : parseFloat(rrRaw);
- if (!Number.isFinite(rr) || rr <= 0) {
- alert("盈亏比无效");
- return false;
+ const tgtRaw = (document.getElementById("opt-target-idx").value || "").trim();
+ if (tgtRaw !== "") {
+ const tgt = parseFloat(tgtRaw);
+ if (!Number.isFinite(tgt) || tgt <= 0) {
+ alert("目标位无效");
+ return false;
+ }
+ body.target_index = tgt;
}
- body.profit_rr = rr;
const d = await apiJson("/api/options/open", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -1480,17 +1428,15 @@
return null;
}
- function formatRrEstimateHtml(rr, premiumPaid) {
- const r = Number(rr);
- const prem = Number(premiumPaid);
- if (!Number.isFinite(r) || r <= 0 || !Number.isFinite(prem) || prem <= 0) return "";
- const profit = Math.round(prem * r * 100) / 100;
- const need = Math.round((prem + profit) * 100) / 100;
+ function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid) {
+ const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount);
+ const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid);
+ if (value == null && profit == null) return "";
let html = '
';
- html += '目标盈利' +
- fmtUsdcSigned(profit) + "";
- html += '需回收' +
- fmtUsdc(need) + " USDC";
+ html += '价值' +
+ (value == null ? "—" : fmtUsdc(value) + " USDC") + "";
+ html += '预估盈利' +
+ (profit == null ? "—" : fmtUsdcSigned(profit)) + "";
html += "";
return html;
}
@@ -1498,73 +1444,47 @@
function renderTargetDelegateRow(p) {
const inst = p.inst_id || "";
const hedgeTarget = p.hedge_plan_target || null;
- if (hedgeTarget && hedgeTarget.managed_by === "hedge_plan") {
- const rr = hedgeTarget.oo_profit_rr != null ? Number(hedgeTarget.oo_profit_rr) : null;
- const armedTxt =
- rr != null && Number.isFinite(rr) && rr > 0
- ? "盈亏比 ×" + fmt(rr, 2)
- : hedgeTarget.target_index != null
- ? "目标 " + fmt(hedgeTarget.target_index, 1)
- : "托管中";
+ if (hedgeTarget && Number(hedgeTarget.target_index) > 0) {
+ const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
return (
'
' +
'对冲计划' +
'计划 #' +
hedgeTarget.plan_id +
" · " +
- armedTxt +
+ side +
+ " " +
+ fmt(hedgeTarget.target_index, 1) +
"" +
- '进行中 · 由对冲计划监控' +
+ '进行中 · 由对冲计划监控,到位后仅平盈利腿' +
"
"
);
}
- const rrArmed =
- p.profit_rr != null && p.profit_rr !== ""
- ? Number(p.profit_rr)
- : p.target_monitor && p.target_monitor.profit_rr != null
- ? Number(p.target_monitor.profit_rr)
- : null;
- const armed = rrArmed != null && Number.isFinite(rrArmed) && rrArmed > 0;
+ const tgt = p.target_index != null && p.target_index !== "" ? Number(p.target_index) : null;
+ const armed = tgt != null && Number.isFinite(tgt) && tgt > 0;
+ const ethAmt = posEthAmount(p);
const prem = p.premium_paid;
- const draft =
- state.targetDraftByInst[inst] != null
- ? String(state.targetDraftByInst[inst])
- : armed
- ? String(rrArmed)
- : "2";
const estHtml = armed
- ? formatRrEstimateHtml(rrArmed, prem)
+ ? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem)
: '
';
return (
- '
' +
+ '
' +
'委托' +
- '' +
- '' +
- '" +
- (armed ? '盈亏比 ×' + fmt(rrArmed, 2) + "" : "") +
+ '' +
+ '' +
+ '" +
+ (armed
+ ? '目标 ' + fmt(tgt, 1) + ""
+ : "") +
estHtml +
'' +
- (armed
- ? "监控中 · 买一浮盈达盈亏比后全平"
- : "默认2 · 买一浮盈达盈亏比×权利金后全平 · 不达标等到期") +
+ (armed ? "监控中 · 到位按买一限价平" : "输入后设定 · 到位按买一限价平 · 到期即止损") +
"" +
"
"
);
@@ -1576,14 +1496,20 @@
if (!est) return;
const inp = row.querySelector(".opt-pos-target-input");
const typed = inp ? String(inp.value || "").trim() : "";
- const armed = row.getAttribute("data-armed-rr") || "";
- const rrRaw = typed !== "" ? typed : armed;
- if (rrRaw === "") {
+ const armed = row.getAttribute("data-armed-target") || "";
+ const targetRaw = typed !== "" ? typed : armed;
+ if (targetRaw === "") {
est.className = "opt-target-est opt-target-est--idle";
est.innerHTML = "";
return;
}
- const html = formatRrEstimateHtml(rrRaw, row.getAttribute("data-prem"));
+ const html = formatTargetEstimateHtml(
+ row.getAttribute("data-opt-type"),
+ row.getAttribute("data-strike"),
+ targetRaw,
+ row.getAttribute("data-eth"),
+ row.getAttribute("data-prem")
+ );
if (!html) {
est.className = "opt-target-est opt-target-est--idle";
est.innerHTML = "";
@@ -1715,9 +1641,9 @@
const row = card ? card.querySelector(".opt-target-row") : null;
const inp = card ? card.querySelector(".opt-pos-target-input") : null;
const raw = inp ? String(inp.value || "").trim() : "";
- const rr = raw === "" ? 2 : parseFloat(raw);
- if (!Number.isFinite(rr) || rr <= 0) {
- alert("请输入有效盈亏比(相对权利金,默认2)");
+ const tgt = parseFloat(raw);
+ if (!Number.isFinite(tgt) || tgt <= 0) {
+ alert("请输入有效目标指数价");
return;
}
if (btn) btn.disabled = true;
@@ -1725,16 +1651,17 @@
const d = await apiJson("/api/options/target", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ inst_id: inst, profit_rr: rr }),
+ body: JSON.stringify({ inst_id: inst, target_index: tgt }),
});
if (!d.ok) {
alert(d.msg || "设定失败");
return;
}
delete state.targetDraftByInst[inst];
- if (inp) inp.value = String(rr);
+ if (inp) inp.value = "";
if (row) {
- row.setAttribute("data-armed-rr", String(rr));
+ row.setAttribute("data-armed-target", String(tgt));
+ updatePosTargetEstimate(row);
}
await refreshAllPositions();
} finally {
@@ -1774,22 +1701,12 @@
}
box.hidden = false;
host.innerHTML = rows.map(function (t) {
+ const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
const managed = t.managed_by === "hedge_plan";
- let rule;
- if (t.profit_rr != null && Number(t.profit_rr) > 0) {
- rule = "盈亏比 ×" + fmt(t.profit_rr, 2);
- } else if (t.oo_profit_rr != null && Number(t.oo_profit_rr) > 0) {
- rule = "盈亏比 ×" + fmt(t.oo_profit_rr, 2);
- } else if (t.target_index != null && Number(t.target_index) > 0) {
- const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
- rule = side + " " + fmt(t.target_index, 1);
- } else {
- rule = "委托中";
- }
return (
'
' +
'
' + (t.inst_id || "") + "" +
- '
' + rule + "" +
+ '
' + side + " " + fmt(t.target_index, 1) + "" +
(managed
? '
对冲计划 #' + (t.plan_id || "") + " · 进行中"
: '
') +
@@ -1970,23 +1887,20 @@
paintPositions(list);
const fromPos = list.reduce(function (targets, p) {
if (!p) return targets;
- if (p.profit_rr != null || p.target_index != null) {
+ if (p.target_index != null) {
targets.push({
id: p.target_monitor_id,
inst_id: p.inst_id,
opt_type: p.opt_type,
target_index: p.target_index,
- profit_rr: p.profit_rr,
});
}
const hedgeTarget = p.hedge_plan_target;
- if (hedgeTarget) {
+ if (hedgeTarget && hedgeTarget.target_index != null) {
targets.push({
inst_id: p.inst_id,
opt_type: p.opt_type || hedgeTarget.opt_type,
target_index: hedgeTarget.target_index,
- oo_profit_rr: hedgeTarget.oo_profit_rr,
- profit_rr: hedgeTarget.oo_profit_rr,
plan_id: hedgeTarget.plan_id,
managed_by: hedgeTarget.managed_by,
});
@@ -2258,7 +2172,6 @@
updateUnderlyingLabel();
refreshPendingOrders();
startPendingOrdersPoll();
- startChainSoftPoll();
const hasCache =
chainHasExpiries(panelCache.chain) &&
panelCache.underlying === state.underlying &&
@@ -2268,8 +2181,8 @@
renderExpiries();
renderStrikes();
refreshAllPositions();
- // 后台静默刷新,避免缓存过期后到期日变空 / 卖一过期
- softRefreshChainThrottled(true);
+ // 后台静默刷新,避免缓存过期后到期日变空
+ loadChain({ soft: true });
return;
}
requestAnimationFrame(function () {
@@ -2373,17 +2286,17 @@
}
bindOrderDialogChrome();
- ["opt-sheets-amount", "opt-eth-amount", "opt-profit-rr"].forEach(function (id) {
+ ["opt-sheets-amount", "opt-eth-amount", "opt-target-idx"].forEach(function (id) {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener("change", function () {
- if (id === "opt-profit-rr") {
+ if (id === "opt-target-idx") {
updateEstimatedProfit();
return;
}
if (state.selectedInst) selectContract(state.selectedInst, null, true);
});
- if (id === "opt-profit-rr") {
+ if (id === "opt-target-idx") {
el.addEventListener("input", updateEstimatedProfit);
}
});
@@ -2393,8 +2306,6 @@
window.OptionsPanelLive = {
refreshSoft: function () {
refreshAllPositions();
- // embed SSE 只通知「该拉了」,不推送链报价;这里节流拉新鲜卖一/买一
- softRefreshChainThrottled(false);
},
refreshChain: loadChain,
};
diff --git a/lib/common/static/options_position_cards.js b/lib/common/static/options_position_cards.js
index 3f1e46d..d2010d2 100644
--- a/lib/common/static/options_position_cards.js
+++ b/lib/common/static/options_position_cards.js
@@ -219,42 +219,38 @@
const hint = closeGateHint(closePreview);
return hint ? '
' + hint + "
" : "";
})() +
- (p.profit_rr != null || p.target_index != null
+ (p.target_index != null
? (function () {
- const hedgeTarget = p.hedge_plan_target || null;
- const managed = hedgeTarget && hedgeTarget.managed_by === "hedge_plan";
- const rr =
- managed && hedgeTarget.oo_profit_rr != null
- ? Number(hedgeTarget.oo_profit_rr)
- : p.profit_rr != null
- ? Number(p.profit_rr)
- : null;
+ const eth = p.eth_amount != null ? Number(p.eth_amount)
+ : (Number(p.pos) > 0 ? Number(p.pos) * Number(p.ct_mult || 0.01) : null);
+ const strike = Number(p.strike);
+ const tgt = Number(p.target_index);
const prem = Number(p.premium_paid);
let profit = null;
- let need = null;
- if (rr != null && Number.isFinite(rr) && rr > 0 && Number.isFinite(prem) && prem > 0) {
- profit = Math.round(prem * rr * 100) / 100;
- need = Math.round((prem + profit) * 100) / 100;
+ let value = null;
+ if (Number.isFinite(tgt) && Number.isFinite(strike) && eth > 0) {
+ const o = String(p.opt_type || "").toUpperCase();
+ const intrinsic = o === "C" ? Math.max(0, tgt - strike) : o === "P" ? Math.max(0, strike - tgt) : null;
+ if (intrinsic != null) {
+ value = Math.round(intrinsic * eth * 100) / 100;
+ if (!hidePnl && Number.isFinite(prem)) profit = Math.round((value - prem) * 100) / 100;
+ }
}
const profitTxt = profit == null ? "—" : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + " USDC");
const profitCls = profit > 0 ? " pnl-pos" : profit < 0 ? " pnl-neg" : "";
+ const hedgeTarget = p.hedge_plan_target || null;
+ const managed = hedgeTarget && hedgeTarget.managed_by === "hedge_plan";
const profitSpan = hidePnl
? ""
- : '
目标盈利 ' + profitTxt + "";
- const ruleTxt =
- rr != null && Number.isFinite(rr) && rr > 0
- ? "盈亏比 ×" + fmt(rr, 2)
- : p.target_index != null
- ? "目标 " + fmt(p.target_index, 1)
- : "委托中";
+ : '
预估盈利 ' + profitTxt + "";
return (
'
' +
'' + (managed ? "对冲计划 #" + hedgeTarget.plan_id : "委托") + "" +
- '' + ruleTxt + "" +
- (need != null ? '需回收 ' + fmtUsdc(need) + " USDC" : "") +
+ '目标 ' + fmt(p.target_index, 1) + "" +
+ '价值 ' + (value == null ? "—" : fmtUsdc(value) + " USDC") + "" +
profitSpan +
'' +
- (managed ? "进行中 · 由对冲计划监控" : "监控中 · 买一浮盈达盈亏比后全平") +
+ (managed ? "进行中 · 由对冲计划监控,到位后仅平盈利腿" : "监控中 · 到位按买一限价平") +
"
"
);
})()
diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js
index 8779dc9..d4d7e49 100644
--- a/lib/common/static/options_review.js
+++ b/lib/common/static/options_review.js
@@ -76,8 +76,6 @@
target_win_leg: "期期平盈利腿",
target_up_win_leg: "期期上破·平盈利腿",
target_down_win_leg: "期期下破·平盈利腿",
- oo_rr_target: "期期盈亏比达标",
- oo_rr_closing: "期期盈亏比平仓中",
oo_rest_closing: "期期全平·清残腿中",
oo_rest_closed: "期期全平·两腿已平",
orphaned_after_tp: "止盈后持有至到期",
diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py
index cbb27f2..d477ab3 100644
--- a/lib/exchange/okx_options_lib.py
+++ b/lib/exchange/okx_options_lib.py
@@ -3,7 +3,6 @@ from __future__ import annotations
import json
import math
-import os
import re
import threading
import time
@@ -26,14 +25,6 @@ _OKX_OPTION_ERR_ZH: dict[str, str] = {
}
_OPTIONS_BALANCE_CACHE: dict[str, Any] = {"updated_at": 0.0, "data": None}
-# 期权合约列表变化慢;短缓存+限频退避,避免 50011 拖垮期权链
-_INSTRUMENTS_CACHE: dict[str, dict[str, Any]] = {}
-_INSTRUMENTS_CACHE_LOCK = threading.Lock()
-_INSTRUMENTS_CACHE_TTL_SEC = 90.0
-_INSTRUMENTS_STALE_SEC = 600.0
-_TICKERS_CACHE: dict[str, dict[str, Any]] = {}
-_TICKERS_CACHE_LOCK = threading.Lock()
-_TICKERS_CACHE_TTL_SEC = float(os.getenv("OKX_OPTIONS_TICKERS_CACHE_SEC", "10") or "10")
def invalidate_options_balance_cache() -> None:
@@ -41,14 +32,6 @@ def invalidate_options_balance_cache() -> None:
_OPTIONS_BALANCE_CACHE["data"] = None
-def invalidate_option_instruments_cache(inst_family: str | None = None) -> None:
- with _INSTRUMENTS_CACHE_LOCK:
- if inst_family:
- _INSTRUMENTS_CACHE.pop(str(inst_family), None)
- else:
- _INSTRUMENTS_CACHE.clear()
-
-
def _okx_trade_error_message(exc: BaseException | None = None, resp: Any = None) -> str:
row: dict[str, Any] | None = None
if isinstance(resp, dict):
@@ -662,105 +645,24 @@ def fetch_index_price(ex: ccxt.okx, uly: str) -> float | None:
def fetch_option_instruments(
ex: ccxt.okx,
inst_family: str,
- *,
- force: bool = False,
) -> list[dict[str, Any]]:
- """拉取 live 期权合约列表;短 TTL 缓存,遇 50011 退避重试并可回退过期缓存."""
- family = (inst_family or "").strip()
- if not family:
- return []
- now = time.time()
- with _INSTRUMENTS_CACHE_LOCK:
- cached = _INSTRUMENTS_CACHE.get(family)
- if (
- not force
- and cached
- and now - float(cached.get("updated_at") or 0) < _INSTRUMENTS_CACHE_TTL_SEC
- and isinstance(cached.get("rows"), list)
- and cached["rows"]
- ):
- return list(cached["rows"])
-
- last_err: BaseException | None = None
- rows: list[dict[str, Any]] = []
- for attempt in range(4):
- try:
- raw = ex.public_get_public_instruments(
- {"instType": "OPTION", "instFamily": family}
- ).get("data") or []
- rows = [r for r in raw if isinstance(r, dict) and r.get("state") == "live"]
- last_err = None
- break
- except Exception as e:
- last_err = e
- if _is_okx_rate_limit(e) and attempt < 3:
- time.sleep(0.8 * (2**attempt))
- continue
- break
-
- if rows:
- with _INSTRUMENTS_CACHE_LOCK:
- _INSTRUMENTS_CACHE[family] = {"updated_at": time.time(), "rows": list(rows)}
- return rows
-
- # 限频/短暂失败:优先用未过期太久的缓存,避免整页「拉取失败」
- if cached and isinstance(cached.get("rows"), list) and cached["rows"]:
- age = now - float(cached.get("updated_at") or 0)
- if age < _INSTRUMENTS_STALE_SEC and (
- last_err is None or _is_okx_rate_limit(last_err) or not rows
- ):
- return list(cached["rows"])
-
- if last_err is not None:
- raise last_err
- return []
+ rows = ex.public_get_public_instruments(
+ {"instType": "OPTION", "instFamily": inst_family}
+ ).get("data") or []
+ return [r for r in rows if isinstance(r, dict) and r.get("state") == "live"]
-def fetch_option_tickers(
- ex: ccxt.okx,
- inst_family: str,
- *,
- force: bool = False,
-) -> dict[str, dict[str, Any]]:
- family = (inst_family or "").strip()
- if not family:
- return {}
- now = time.time()
- with _TICKERS_CACHE_LOCK:
- cached = _TICKERS_CACHE.get(family)
- if (
- not force
- and cached
- and now - float(cached.get("updated_at") or 0) < max(1.0, _TICKERS_CACHE_TTL_SEC)
- and isinstance(cached.get("rows"), dict)
- and cached["rows"]
- ):
- return dict(cached["rows"])
-
+def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, Any]]:
out: dict[str, dict[str, Any]] = {}
- last_err: BaseException | None = None
- for attempt in range(3):
- try:
- rows = ex.public_get_market_tickers(
- {"instType": "OPTION", "instFamily": family}
- ).get("data") or []
- for r in rows:
- if isinstance(r, dict) and r.get("instId"):
- out[str(r["instId"])] = r
- if out:
- with _TICKERS_CACHE_LOCK:
- _TICKERS_CACHE[family] = {"updated_at": time.time(), "rows": dict(out)}
- return out
- except Exception as e:
- last_err = e
- if _is_okx_rate_limit(e) and attempt < 2:
- time.sleep(0.6 * (attempt + 1))
- continue
- break
- if cached and isinstance(cached.get("rows"), dict) and cached["rows"]:
- return dict(cached["rows"])
- if last_err is not None and _is_okx_rate_limit(last_err):
- return out
+ try:
+ rows = ex.public_get_market_tickers(
+ {"instType": "OPTION", "instFamily": inst_family}
+ ).get("data") or []
+ for r in rows:
+ if isinstance(r, dict) and r.get("instId"):
+ out[str(r["instId"])] = r
+ except Exception:
+ pass
return out
@@ -781,17 +683,22 @@ def build_option_chain(
max_ms = now_ms + max_dte_days * 86400 * 1000
instruments_err = ""
instruments: list[dict[str, Any]] = []
- rate_limited = False
- try:
- instruments = fetch_option_instruments(ex, family)
- if not instruments:
+ for attempt in range(2):
+ try:
+ instruments = fetch_option_instruments(ex, family)
+ instruments_err = ""
+ if instruments:
+ break
instruments_err = "期权合约列表为空"
- except Exception as e:
- instruments = []
- instruments_err = str(e) or e.__class__.__name__
- rate_limited = _is_okx_rate_limit(e)
- if rate_limited:
- instruments_err = "OKX 请求过于频繁(50011),请稍后点「刷新链」重试"
+ except Exception as e:
+ instruments = []
+ instruments_err = str(e) or e.__class__.__name__
+ if attempt == 0:
+ time.sleep(0.35)
+ continue
+ break
+ if attempt == 0 and not instruments:
+ time.sleep(0.35)
tickers = fetch_option_tickers(ex, family)
expiries: dict[str, list[dict[str, Any]]] = {}
skipped_no_index = 0
@@ -870,8 +777,6 @@ def build_option_chain(
"expiries": exp_list,
"instruments_count": len(instruments),
}
- if rate_limited:
- out["rate_limited"] = True
if not exp_list:
if instruments_err:
out["chain_error"] = f"拉取期权合约失败: {instruments_err}"
diff --git a/lib/hedge_plan/hedge_plan_calc_lib.py b/lib/hedge_plan/hedge_plan_calc_lib.py
index bd55a3e..e508c21 100644
--- a/lib/hedge_plan/hedge_plan_calc_lib.py
+++ b/lib/hedge_plan/hedge_plan_calc_lib.py
@@ -444,7 +444,6 @@ def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
def build_options_options_preview(
*,
- profit_rr: float | None = None,
target_price: float | None = None,
target_price_up: float | None = None,
target_price_down: float | None = None,
@@ -452,11 +451,7 @@ def build_options_options_preview(
leg_a: dict[str, Any],
leg_b: dict[str, Any],
) -> dict[str, Any]:
- """期期情景:盈亏比达标 / 到期现价 / 最大保费损耗.
-
- profit_rr=2 表示目标盈利=2×权利金;中途不达标则等到期.
- 仍接受旧上破/下破参数仅作兼容测算.
- """
+ """期期情景:上破/下破目标价 / 到期现价 / 最大保费损耗."""
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
return option_expiry_pnl(
@@ -468,73 +463,15 @@ def build_options_options_preview(
premium_paid=float(leg.get("premium_paid") or 0),
)
- prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0)
- a_flat = _leg_pnl(leg_a, index_px)
- b_flat = _leg_pnl(leg_b, index_px)
- flat_total = a_flat + b_flat
-
- rr = None
- if profit_rr not in (None, ""):
- try:
- rr = float(profit_rr)
- except (TypeError, ValueError):
- rr = None
- if rr is not None and rr > 0:
- target_pnl = rr * prem
- return {
- "plan_type": "options_options",
- "premium_paid": round(prem, 6),
- "oo_profit_rr": round(rr, 4),
- "target_profit": round(target_pnl, 4),
- "scenarios": [
- {
- "id": "rr_target",
- "label": f"盈亏比×{rr:g}",
- "spot": None,
- "leg_a_pnl": None,
- "leg_b_pnl": None,
- "total": round(target_pnl, 4),
- "note": f"两腿合计浮盈≥{rr:g}×权利金({round(prem, 4)})时全平;不达标等到期",
- },
- {
- "id": "expiry_flat",
- "label": "到期·现价(未达标)",
- "spot": index_px,
- "leg_a_pnl": round(a_flat, 4),
- "leg_b_pnl": round(b_flat, 4),
- "total": round(flat_total, 4),
- "note": "中途未达盈亏比则持有至到期结算",
- },
- {
- "id": "max_premium_loss",
- "label": "最大保费损耗",
- "spot": None,
- "leg_a_pnl": round(-float(leg_a.get("premium_paid") or 0), 4),
- "leg_b_pnl": round(-float(leg_b.get("premium_paid") or 0), 4),
- "total": round(-prem, 4),
- "note": "双腿权利金全部损失",
- },
- ],
- "summary": {
- "oo_profit_rr": round(rr, 4),
- "target_profit": round(target_pnl, 4),
- "at_target_total": round(target_pnl, 4),
- "expiry_flat_total": round(flat_total, 4),
- "premium_paid": round(prem, 6),
- "expiry_is_loss": flat_total <= 0,
- "rr_risk_premium": round(prem, 6),
- "rr_target": round(rr, 4),
- },
- }
-
- # 兼容旧上破/下破测算
+ # 兼容旧单目标:若未传上下目标则用 target_price 填两边
up = target_price_up if target_price_up is not None else target_price
down = target_price_down if target_price_down is not None else target_price
if up is None or down is None:
- raise ValueError("请填写盈亏比(相对权利金,默认2)")
+ raise ValueError("缺少上破/下破目标价")
up_f = float(up)
down_f = float(down)
+ prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0)
a_up = _leg_pnl(leg_a, up_f)
b_up = _leg_pnl(leg_b, up_f)
at_up = a_up + b_up
@@ -545,10 +482,15 @@ def build_options_options_preview(
at_dn = a_dn + b_dn
win_dn = "a" if a_dn >= b_dn else "b"
+ a_flat = _leg_pnl(leg_a, index_px)
+ b_flat = _leg_pnl(leg_b, index_px)
+ flat_total = a_flat + b_flat
+ expiry_loss = flat_total if flat_total <= 0 else flat_total
+
return {
"plan_type": "options_options",
"premium_paid": round(prem, 6),
- "target_price": up_f,
+ "target_price": up_f, # 兼容旧字段,取上破
"target_price_up": up_f,
"target_price_down": down_f,
"winner_at_up": win_up,
@@ -596,9 +538,10 @@ def build_options_options_preview(
"at_target_up_total": round(at_up, 4),
"at_target_down_total": round(at_dn, 4),
"at_target_total": round(at_up, 4),
- "expiry_flat_total": round(flat_total, 4),
+ "expiry_flat_total": round(expiry_loss, 4),
"premium_paid": round(prem, 6),
"expiry_is_loss": flat_total <= 0,
+ # 盈亏比:盈利/全亏保费(风险=权利金全损)
"rr_risk_premium": round(prem, 6),
"rr_at_up": round(at_up / prem, 4) if prem > 0 else None,
"rr_at_down": round(at_dn / prem, 4) if prem > 0 else None,
diff --git a/lib/hedge_plan/hedge_plan_db.py b/lib/hedge_plan/hedge_plan_db.py
index 622dec0..299efa6 100644
--- a/lib/hedge_plan/hedge_plan_db.py
+++ b/lib/hedge_plan/hedge_plan_db.py
@@ -72,8 +72,6 @@ def init_hedge_plan_tables(conn: sqlite3.Connection) -> None:
)
_ensure_column(conn, "hedge_plans", "target_price_up", "REAL")
_ensure_column(conn, "hedge_plans", "target_price_down", "REAL")
- # 期期:目标盈亏比=目标盈利/权利金(如 2=盈利 2 倍权利金);不达标则等到期
- _ensure_column(conn, "hedge_plans", "oo_profit_rr", "REAL")
# close_all=盈利腿平后清残腿;hold_expiry=残腿持有至到期(现状)
_ensure_column(conn, "hedge_plans", "oo_close_mode", "TEXT")
# 永期「以期权为主」
@@ -274,7 +272,7 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
rows = conn.execute(
"""
SELECT p.id AS plan_id, p.underlying, p.target_price_up, p.target_price_down,
- p.oo_profit_rr, l.inst_id, l.opt_type
+ l.inst_id, l.opt_type
FROM hedge_plans p
JOIN hedge_plan_legs l ON l.plan_id = p.id
WHERE p.plan_type = 'options_options'
@@ -289,36 +287,10 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
for raw in rows:
row = dict(raw)
inst_id = str(row.get("inst_id") or "")
- if not inst_id or inst_id in out:
- continue
opt_type = str(row.get("opt_type") or "").upper()
- rr = _sf(row.get("oo_profit_rr"))
target = row.get("target_price_up") if opt_type == "C" else row.get("target_price_down")
target_f = _sf(target)
- # 盈亏比模式无指数目标价;旧上破/下破计划仍透出 target_index 只读展示
- if rr is not None and rr > 0:
- out[inst_id] = {
- "plan_id": int(row["plan_id"]),
- "inst_id": inst_id,
- "underlying": row.get("underlying"),
- "opt_type": opt_type,
- "target_index": None,
- "oo_profit_rr": rr,
- "plan_type": "options_options",
- "managed_by": "hedge_plan",
- }
- continue
- if target_f is None or target_f <= 0:
- # 无目标价也标记托管,避免期权页误拆组
- out[inst_id] = {
- "plan_id": int(row["plan_id"]),
- "inst_id": inst_id,
- "underlying": row.get("underlying"),
- "opt_type": opt_type,
- "target_index": None,
- "plan_type": "options_options",
- "managed_by": "hedge_plan",
- }
+ if not inst_id or target_f is None or target_f <= 0 or inst_id in out:
continue
out[inst_id] = {
"plan_id": int(row["plan_id"]),
diff --git a/lib/hedge_plan/hedge_plan_monitor_lib.py b/lib/hedge_plan/hedge_plan_monitor_lib.py
index b709bc7..53addea 100644
--- a/lib/hedge_plan/hedge_plan_monitor_lib.py
+++ b/lib/hedge_plan/hedge_plan_monitor_lib.py
@@ -999,8 +999,6 @@ def _tick_oo_close_rest(
"target_up_win_leg",
"target_down_win_leg",
"oo_rest_closing",
- "oo_rr_closing",
- "oo_rr_target",
"",
)
if reason0 not in allowed_reasons and not (
@@ -1051,113 +1049,7 @@ def _tick_oo_close_rest(
def _tick_oo_target(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
- """期期止盈:优先盈亏比(浮盈≥rr×权利金则两腿全平);否则兼容旧上破/下破."""
- rr = _sf(plan.get("oo_profit_rr"))
- if rr is not None and rr > 0:
- return _tick_oo_rr_target(cfg, conn, plan, legs, rr=float(rr))
- return _tick_oo_price_target(cfg, conn, plan, legs)
-
-
-def _tick_oo_rr_target(
- cfg: dict[str, Any],
- conn: Any,
- plan: dict[str, Any],
- legs: list[dict[str, Any]],
- *,
- rr: float,
-) -> Optional[dict[str, Any]]:
- """浮盈(买一回收−权利金)≥盈亏比×总权利金 → 两腿全平;不达标则等到期."""
- open_legs = _oo_option_legs(legs, statuses=("open",))
- if len(open_legs) < 1:
- return None
- premium = float(plan.get("premium_total") or 0)
- if premium <= 0:
- premium = sum(float(x.get("premium") or 0) for x in open_legs)
- if premium <= 0:
- return None
- need = float(rr) * premium
- quote_fn = cfg.get("quote_option_contract")
- ex = cfg.get("exchange_options")
- if not callable(quote_fn) or ex is None:
- return None
- idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
- total_pnl = 0.0
- missing_bid = 0
- for leg in open_legs:
- inst = str(leg.get("inst_id") or "")
- bid = None
- try:
- q = quote_fn(ex, inst) if inst else {}
- if isinstance(q, dict) and q.get("ok"):
- bid = _sf(q.get("bid"))
- except Exception:
- bid = None
- if bid is None or float(bid) <= 0:
- missing_bid += 1
- # 无买一时用内在价值兜底,避免短暂无盘口卡住;两腿都无买一则本轮跳过
- total_pnl += _estimate_leg_close_pnl(leg, idx, None)
- else:
- total_pnl += _estimate_leg_close_pnl(leg, idx, float(bid))
- if missing_bid >= len(open_legs):
- return None
- if total_pnl + 1e-9 < need:
- return None
-
- acted = False
- for leg in list(open_legs):
- close_r = _sell_option(
- cfg, inst_id=str(leg.get("inst_id") or ""), sheets=float(leg.get("size") or 1)
- )
- if not close_r.get("ok"):
- notify_hedge(
- cfg,
- build_hedge_alert_message(
- title="期期盈亏比达标·平仓失败(将重试)",
- plan_id=plan.get("id"),
- detail=(
- f"目标 {rr:g}×权利金={need:.4f};估算浮盈 {total_pnl:.4f}; "
- f"{close_r.get('msg') or close_r}"
- ),
- ),
- )
- update_plan(conn, int(plan["id"]), close_reason="oo_rr_closing")
- return {
- "plan_id": plan["id"],
- "msg": "盈亏比达标但平仓失败",
- "close": close_r,
- "retry": True,
- "rr": rr,
- "need": need,
- "mtm": total_pnl,
- }
- bid = _sf(close_r.get("bid"))
- est = _estimate_leg_close_pnl(leg, idx, bid)
- pnl = _option_leg_pnl_after_close(cfg, leg, fallback=est)
- conn.execute(
- "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
- ("closed", "oo_rr_target", _now(), round(pnl, 4), leg["id"]),
- )
- acted = True
-
- if not acted:
- return None
- legs2 = get_plan_legs(conn, int(plan["id"]))
- still_open = _oo_option_legs(legs2, statuses=("open", "hold_to_expiry"))
- if still_open:
- update_plan(conn, int(plan["id"]), close_reason="oo_rr_closing")
- return {
- "plan_id": plan["id"],
- "msg": "盈亏比达标·部分已平,继续重试",
- "remaining": len(still_open),
- "rr": rr,
- }
- return _finalize_oo_all_closed(cfg, conn, plan, legs2, reason="oo_rr_target")
-
-
-def _tick_oo_price_target(
- cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
-) -> Optional[dict[str, Any]]:
- """旧逻辑:触及上破或下破目标价时平盈利腿;按平仓模式处理另一腿."""
+ """期期:触及上破或下破目标价时平盈利腿;按平仓模式处理另一腿."""
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
if idx is None:
return None
diff --git a/lib/hedge_plan/hedge_plan_notify_lib.py b/lib/hedge_plan/hedge_plan_notify_lib.py
index aa440ae..2e8ce7d 100644
--- a/lib/hedge_plan/hedge_plan_notify_lib.py
+++ b/lib/hedge_plan/hedge_plan_notify_lib.py
@@ -46,22 +46,13 @@ def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[
]
)
else:
- rr = plan.get("oo_profit_rr")
- if rr not in (None, ""):
- lines.extend(
- [
- f"🎯 盈亏比:{_fmt(rr)}×权利金(达标全平;不达标等到期)",
- f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
- ]
- )
- else:
- lines.extend(
- [
- f"🎯 上破:{_fmt(plan.get('target_price_up') or plan.get('target_price'))}"
- f"|下破:{_fmt(plan.get('target_price_down') or plan.get('target_price'))}",
- f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
- ]
- )
+ lines.extend(
+ [
+ f"🎯 上破:{_fmt(plan.get('target_price_up') or plan.get('target_price'))}"
+ f"|下破:{_fmt(plan.get('target_price_down') or plan.get('target_price'))}",
+ f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
+ ]
+ )
if legs:
for leg in legs:
role = leg.get("leg_role") or ""
@@ -90,8 +81,6 @@ def build_hedge_end_message(plan: dict[str, Any]) -> str:
"target_win_leg": "期期已平盈利腿(中间态)",
"target_up_win_leg": "期期上破·已平盈利腿",
"target_down_win_leg": "期期下破·已平盈利腿",
- "oo_rr_target": "期期盈亏比达标·两腿已平",
- "oo_rr_closing": "期期盈亏比达标·平仓中",
"oo_rest_closing": "期期全平·清残腿中",
"oo_rest_closed": "期期全平·两腿已平",
"oo_expiry_loss": "期期到期无盈利·总亏损",
@@ -164,18 +153,7 @@ def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> boo
"target_up_win_leg",
"target_down_win_leg",
"oo_rest_closing",
- "oo_rr_closing",
) and (plan.get("status") or "") != "closed":
- if "oo_rr" in str(plan.get("close_reason") or ""):
- notify_hedge(
- cfg,
- build_hedge_alert_message(
- title="期期盈亏比达标·平仓进行中",
- plan_id=plan.get("id"),
- detail=f"盈亏比 {_fmt(plan.get('oo_profit_rr'))}×权利金",
- ),
- )
- return True
side = "上破" if "up" in str(plan.get("close_reason")) else (
"下破" if "down" in str(plan.get("close_reason")) else "目标价"
)
diff --git a/lib/hedge_plan/hedge_plan_orders_lib.py b/lib/hedge_plan/hedge_plan_orders_lib.py
index 8e554e9..d0180d4 100644
--- a/lib/hedge_plan/hedge_plan_orders_lib.py
+++ b/lib/hedge_plan/hedge_plan_orders_lib.py
@@ -1146,31 +1146,20 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
b = body.get("leg_b") or {}
if not a.get("inst_id") or not b.get("inst_id"):
return "请选用两条期权腿"
- rr_raw = body.get("oo_profit_rr")
- if rr_raw in (None, ""):
- rr_raw = body.get("profit_rr")
- if rr_raw not in (None, ""):
- try:
- rr = float(rr_raw)
- except (TypeError, ValueError):
- return "盈亏比无效"
- if rr <= 0:
- return "盈亏比须大于 0"
- else:
- up = body.get("target_price_up")
- down = body.get("target_price_down")
- legacy = body.get("target_price")
- if up in (None, "") and legacy not in (None, ""):
- up = legacy
- if down in (None, "") and legacy not in (None, ""):
- down = legacy
- if up in (None, "") or down in (None, ""):
- return "请填写盈亏比(相对权利金,默认2)"
- try:
- if float(up) <= float(down):
- return "上破目标价必须大于下破目标价"
- except (TypeError, ValueError):
- return "目标价无效"
+ up = body.get("target_price_up")
+ down = body.get("target_price_down")
+ legacy = body.get("target_price")
+ if up in (None, "") and legacy not in (None, ""):
+ up = legacy
+ if down in (None, "") and legacy not in (None, ""):
+ down = legacy
+ if up in (None, "") or down in (None, ""):
+ return "请填写上破与下破目标价"
+ try:
+ if float(up) <= float(down):
+ return "上破目标价必须大于下破目标价"
+ except (TypeError, ValueError):
+ return "目标价无效"
from lib.hedge_plan.hedge_plan_moneyness_lib import (
parse_strike_from_inst,
validate_oo_legs_moneyness,
@@ -1190,6 +1179,11 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
return {"opt_type": opt_type, "strike": strike}
index_px = body.get("index_px")
+ if index_px in (None, ""):
+ try:
+ index_px = (float(up) + float(down)) / 2.0
+ except (TypeError, ValueError):
+ index_px = None
money_err = validate_oo_legs_moneyness(
_leg_for_money(a),
_leg_for_money(b),
diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py
index 7d32ff0..32958b3 100644
--- a/lib/hedge_plan/hedge_plan_register.py
+++ b/lib/hedge_plan/hedge_plan_register.py
@@ -537,25 +537,27 @@ def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
premium = (float(a.get("premium") or 0) if a_ok else 0.0) + (
float(b.get("premium") or 0) if b_ok else 0.0
)
- rr_raw = body.get("oo_profit_rr")
- if rr_raw in (None, ""):
- rr_raw = body.get("profit_rr")
- try:
- oo_rr = float(rr_raw) if rr_raw not in (None, "") else 2.0
- except (TypeError, ValueError):
- oo_rr = 2.0
- if oo_rr <= 0:
- oo_rr = 2.0
plan_id = insert_plan(
conn,
{
"plan_type": "options_options",
"status": "partial" if is_partial else "active",
"underlying": str(body.get("underlying") or "ETH").upper(),
- "target_price": None,
- "target_price_up": None,
- "target_price_down": None,
- "oo_profit_rr": oo_rr,
+ "target_price": float(
+ body.get("target_price_up")
+ or body.get("target_price")
+ or 0
+ ),
+ "target_price_up": float(
+ body.get("target_price_up")
+ or body.get("target_price")
+ or 0
+ ),
+ "target_price_down": float(
+ body.get("target_price_down")
+ or body.get("target_price")
+ or 0
+ ),
"sizing_mode_at_open": load_position_sizing_mode(),
"premium_total": premium,
"oo_close_mode": _normalize_oo_close_mode(body.get("oo_close_mode")),
@@ -1236,24 +1238,20 @@ def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
from lib.hedge_plan.hedge_plan_moneyness_lib import validate_oo_legs_moneyness
- rr_raw = body.get("oo_profit_rr")
- if rr_raw in (None, ""):
- rr_raw = body.get("profit_rr")
- rr = None
- if rr_raw not in (None, ""):
- try:
- rr = float(rr_raw)
- except (TypeError, ValueError) as e:
- raise ValueError("盈亏比无效") from e
- if rr <= 0:
- raise ValueError("盈亏比须大于 0")
-
- index_px = body.get("index_px")
- try:
- index_px_f = float(index_px) if index_px not in (None, "") else 0.0
- except (TypeError, ValueError):
- index_px_f = 0.0
-
+ up = body.get("target_price_up")
+ down = body.get("target_price_down")
+ legacy = body.get("target_price")
+ if up in (None, "") and legacy not in (None, ""):
+ up = legacy
+ if down in (None, "") and legacy not in (None, ""):
+ down = legacy
+ if up in (None, "") or down in (None, ""):
+ raise ValueError("请填写上破与下破目标价")
+ up_f = float(up)
+ down_f = float(down)
+ if up_f <= down_f:
+ raise ValueError("上破目标价必须大于下破目标价")
+ index_px = float(body.get("index_px") or ((up_f + down_f) / 2))
leg_a = body.get("leg_a") or {}
leg_b = body.get("leg_b") or {}
for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)):
@@ -1267,38 +1265,13 @@ def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
)
if leg.get("premium_paid") is None:
raise ValueError(f"缺少 {name} 权利金")
- money_err = validate_oo_legs_moneyness(leg_a, leg_b, index_px=index_px_f or None)
+ money_err = validate_oo_legs_moneyness(leg_a, leg_b, index_px=index_px)
if money_err:
raise ValueError(money_err)
-
- if rr is not None:
- return build_options_options_preview(
- profit_rr=rr,
- index_px=index_px_f,
- leg_a=leg_a,
- leg_b=leg_b,
- )
-
- # 兼容旧上破/下破
- up = body.get("target_price_up")
- down = body.get("target_price_down")
- legacy = body.get("target_price")
- if up in (None, "") and legacy not in (None, ""):
- up = legacy
- if down in (None, "") and legacy not in (None, ""):
- down = legacy
- if up in (None, "") or down in (None, ""):
- raise ValueError("请填写盈亏比(相对权利金,默认2)")
- up_f = float(up)
- down_f = float(down)
- if up_f <= down_f:
- raise ValueError("上破目标价必须大于下破目标价")
- if index_px_f <= 0:
- index_px_f = (up_f + down_f) / 2
return build_options_options_preview(
target_price_up=up_f,
target_price_down=down_f,
- index_px=index_px_f,
+ index_px=index_px,
leg_a=leg_a,
leg_b=leg_b,
)
diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html
index c0ccb21..de47b03 100644
--- a/lib/hedge_plan/templates/hedge_plan_panel.html
+++ b/lib/hedge_plan/templates/hedge_plan_panel.html
@@ -213,7 +213,7 @@
账户:两腿都在期权账户。可用预算 = min(交易 USDC × 对冲缓冲 {{ '%.2f'|format(hedge_plan_budget_buffer|default(0.95)|float) }}, 单笔预算);可在 env「对冲预算缓冲比例」改。
下单:选 Call + Put 后「计算」再「启动」。启动会再拉卖一并按最新价重算张数,IOC 完全成交才算成功;资金不足可在右侧划转。
-
板块:左填盈亏比(相对权利金,默认 2=盈利 2 倍权利金)与张数模式(同张数/做多/做空);右 T 型选腿。两腿仅允许平值或虚值(禁实值)。中途浮盈达盈亏比→两腿全平;不达标→等到期。「全平/到期平」仅兼容旧上破下破计划残腿处理。
+
板块:左填上破/下破与张数模式(同张数/做多/做空);右 T 型选腿。两腿仅允许平值或虚值(禁实值)。「全平」= 盈利腿平后清另一腿;「到期平」= 另一腿持有至到期。
@@ -221,7 +221,8 @@
-
+
+
指数 —
@@ -405,4 +406,4 @@
-
+
diff --git a/lib/instance/instance_dashboard_lib.py b/lib/instance/instance_dashboard_lib.py
index 25a5c33..39c96c4 100644
--- a/lib/instance/instance_dashboard_lib.py
+++ b/lib/instance/instance_dashboard_lib.py
@@ -123,31 +123,21 @@ def _resolve_options_source(conn, inst_id: str) -> tuple[str, str, int | None]:
def _format_options_target(p: dict[str, Any]) -> str:
hedge = p.get("hedge_plan_target") if isinstance(p.get("hedge_plan_target"), dict) else None
+ opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
if hedge:
- rr = _safe_float(hedge.get("oo_profit_rr") or hedge.get("profit_rr"))
- pid = hedge.get("plan_id")
- if rr is not None and rr > 0:
- return f"对冲#{pid} 盈亏比×{rr:g}" if pid is not None else f"盈亏比×{rr:g}"
- ot = str(hedge.get("opt_type") or p.get("opt_type") or p.get("optType") or "").upper()
+ ot = str(hedge.get("opt_type") or opt_type).upper()
side = "Put ≤" if ot == "P" else "Call ≥"
tgt = _safe_float(hedge.get("target_index"))
+ pid = hedge.get("plan_id")
if tgt is not None:
return f"对冲#{pid} {side} {tgt:g}" if pid is not None else f"{side} {tgt:g}"
- mon = p.get("target_monitor") if isinstance(p.get("target_monitor"), dict) else None
- rr = _safe_float(p.get("profit_rr"))
- if rr is None and mon:
- rr = _safe_float(mon.get("profit_rr"))
- if rr is not None and rr > 0:
- return f"盈亏比×{rr:g}"
tgt = _safe_float(p.get("target_index"))
- if tgt is None and mon:
- tgt = _safe_float(mon.get("target_index"))
if tgt is not None and tgt > 0:
- opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
side = "Put ≤" if opt_type == "P" else "Call ≥"
return f"{side} {tgt:g}"
return "—"
+
def _format_options_item(p: dict[str, Any], *, conn=None) -> dict[str, Any]:
inst = str(p.get("inst_id") or p.get("instId") or "-").strip() or "-"
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
diff --git a/lib/options/options_dashboard_lib.py b/lib/options/options_dashboard_lib.py
index 1d8820a..7c1f5c8 100644
--- a/lib/options/options_dashboard_lib.py
+++ b/lib/options/options_dashboard_lib.py
@@ -72,14 +72,11 @@ def fetch_light_option_positions_for_dashboard(cfg: dict[str, Any]) -> list[dict
mon = tgt_map.get(str(row.get("inst_id") or ""))
if mon:
row["target_index"] = mon.get("target_index")
- row["profit_rr"] = mon.get("profit_rr")
row["target_monitor_id"] = mon.get("id")
row["target_monitor"] = mon
hedge_target = hedge_target_map.get(str(row.get("inst_id") or ""))
if hedge_target:
row["hedge_plan_target"] = hedge_target
- if hedge_target.get("oo_profit_rr") is not None and row.get("profit_rr") is None:
- row["profit_rr"] = hedge_target.get("oo_profit_rr")
if not mon:
row["target_index"] = hedge_target.get("target_index")
rows.append(row)
diff --git a/lib/options/options_hub_lib.py b/lib/options/options_hub_lib.py
index 4053c8a..cbe0d9b 100644
--- a/lib/options/options_hub_lib.py
+++ b/lib/options/options_hub_lib.py
@@ -22,8 +22,7 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return {"ok": False, "enabled": True, "msg": "获取期权持仓失败"}
- # 中控看板不拉逐仓 books(易超 HUB_FLASK_TIMEOUT);实例页仍走完整 preview
- positions = build_display_option_positions(cfg, ex, raw, with_close_preview=False)
+ positions = build_display_option_positions(cfg, ex, raw)
target_monitors: list[dict[str, Any]] = []
try:
conn = cfg["get_db"]()
@@ -39,15 +38,14 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
mon = tgt_map.get(str(p.get("inst_id") or ""))
if mon:
p["target_index"] = mon.get("target_index")
- p["profit_rr"] = mon.get("profit_rr")
p["target_monitor_id"] = mon.get("id")
p["target_monitor"] = mon
hedge_target = hedge_target_map.get(str(p.get("inst_id") or ""))
if hedge_target:
p["hedge_plan_target"] = hedge_target
if not mon:
+ # 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。
p["target_index"] = hedge_target.get("target_index")
- p["profit_rr"] = hedge_target.get("oo_profit_rr")
try:
from lib.instance.instance_dashboard_lib import (
_format_options_target,
diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py
index c5157cc..f77450d 100644
--- a/lib/options/options_monitor_lib.py
+++ b/lib/options/options_monitor_lib.py
@@ -455,7 +455,6 @@ def options_monitor_loop(
conn,
positions,
close_fn=target_close_fn,
- bid_fn=ticker_bid_fn,
send_wechat=send_wechat,
account_label=account_label,
cfg={"send_wechat": send_wechat, "account_label": account_label},
diff --git a/lib/options/options_notify_lib.py b/lib/options/options_notify_lib.py
index 6f99b92..7b32721 100644
--- a/lib/options/options_notify_lib.py
+++ b/lib/options/options_notify_lib.py
@@ -55,7 +55,6 @@ def build_options_open_message(
premium_paid: Any = None,
open_quote: Any = None,
target_index: Any = None,
- profit_rr: Any = None,
signal_note: str = "",
trade_id: Any = None,
) -> str:
@@ -74,12 +73,7 @@ def build_options_open_message(
f"权利金:{_fmt(premium_paid)} USDC",
]
)
- if profit_rr is not None and str(profit_rr).strip() != "":
- try:
- lines.append(f"盈亏比:×{float(profit_rr):g}(达标全平;不达标等到期)")
- except (TypeError, ValueError):
- lines.append(f"盈亏比:{profit_rr}")
- elif target_index is not None and str(target_index).strip() != "":
+ if target_index is not None and str(target_index).strip() != "":
try:
lines.append(f"目标指数:{float(target_index):g}")
except (TypeError, ValueError):
@@ -102,7 +96,6 @@ def build_options_close_message(
realized_pnl: Any = None,
close_quote: Any = None,
target_index: Any = None,
- profit_rr: Any = None,
trigger_idx: Any = None,
trade_id: Any = None,
) -> str:
@@ -123,12 +116,7 @@ def build_options_close_message(
f"实现盈亏:{_fmt(realized_pnl, 4)} USDC",
]
)
- if profit_rr is not None and str(profit_rr).strip() != "":
- try:
- lines.append(f"盈亏比:×{float(profit_rr):g}")
- except (TypeError, ValueError):
- lines.append(f"盈亏比:{profit_rr}")
- elif target_index is not None and str(target_index).strip() != "":
+ if target_index is not None and str(target_index).strip() != "":
try:
lines.append(f"目标指数:{float(target_index):g}")
except (TypeError, ValueError):
@@ -153,7 +141,6 @@ def notify_options_open(
premium_paid: Any = None,
open_quote: Any = None,
target_index: Any = None,
- profit_rr: Any = None,
signal_note: str = "",
) -> bool:
ensure_options_notify_columns(conn) if conn is not None else None
@@ -173,7 +160,6 @@ def notify_options_open(
premium_paid=premium_paid,
open_quote=open_quote,
target_index=target_index,
- profit_rr=profit_rr,
signal_note=signal_note,
trade_id=trade_id,
)
@@ -210,7 +196,6 @@ def notify_options_close(
realized_pnl: Any = None,
close_quote: Any = None,
target_index: Any = None,
- profit_rr: Any = None,
trigger_idx: Any = None,
force: bool = False,
) -> bool:
@@ -272,7 +257,6 @@ def notify_options_close(
realized_pnl=total_pnl,
close_quote=close_quote if close_quote is not None else head.get("close_quote"),
target_index=target_index,
- profit_rr=profit_rr,
trigger_idx=trigger_idx,
trade_id=head.get("id") if len(rows) == 1 else None,
)
@@ -302,7 +286,6 @@ def notify_options_close(
realized_pnl=realized_pnl,
close_quote=close_quote,
target_index=target_index,
- profit_rr=profit_rr,
trigger_idx=trigger_idx,
trade_id=trade_id,
)
diff --git a/lib/options/options_positions_lib.py b/lib/options/options_positions_lib.py
index 2367021..a917f2c 100644
--- a/lib/options/options_positions_lib.py
+++ b/lib/options/options_positions_lib.py
@@ -145,10 +145,8 @@ def build_display_option_positions(
cfg: dict[str, Any],
ex: Any,
raw_positions: list[dict[str, Any]],
- *,
- with_close_preview: bool = True,
) -> list[dict[str, Any]]:
- """与实例 /api/options/positions 相同 enrichment;中控可关 close_preview 避免逐仓拉盘口超时."""
+ """与实例 /api/options/positions 相同 enrichment + close_preview."""
meta_cache: dict[str, dict[str, Any] | None] = {}
rows: list[dict[str, Any]] = []
conn = cfg["get_db"]()
@@ -164,8 +162,7 @@ def build_display_option_positions(
meta_cache=meta_cache,
premium_override=premium_override,
)
- if with_close_preview:
- attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
+ attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
rows.append(row)
finally:
conn.close()
diff --git a/lib/options/options_register.py b/lib/options/options_register.py
index 97865e0..acf8684 100644
--- a/lib/options/options_register.py
+++ b/lib/options/options_register.py
@@ -646,17 +646,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
mode = (data.get("mode") or "budget_full").strip()
signal_note = (data.get("signal_note") or "").strip()
target_index = None
- profit_rr = None
- raw_rr = data.get("profit_rr")
- if raw_rr is None or str(raw_rr).strip() == "":
- raw_rr = data.get("oo_profit_rr")
- if raw_rr is not None and str(raw_rr).strip() != "":
- try:
- profit_rr = float(raw_rr)
- except (TypeError, ValueError):
- return jsonify({"ok": False, "msg": "盈亏比无效"})
- if profit_rr <= 0:
- return jsonify({"ok": False, "msg": "盈亏比须大于 0"})
raw_target = data.get("target_index")
if raw_target is not None and str(raw_target).strip() != "":
try:
@@ -665,9 +654,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
return jsonify({"ok": False, "msg": "目标位无效"})
if target_index <= 0:
return jsonify({"ok": False, "msg": "目标位无效"})
- # 未显式传目标时默认盈亏比 2
- if profit_rr is None and target_index is None:
- profit_rr = 2.0
if not inst_id:
return jsonify({"ok": False, "msg": "缺少 inst_id"})
q = cfg["quote_option_contract"](ex, inst_id)
@@ -834,14 +820,13 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
),
)
trade_id = int(cur.lastrowid)
- if profit_rr is not None or target_index is not None:
+ if target_index is not None:
from lib.options.options_target_lib import upsert_target_monitor
target_mon = upsert_target_monitor(
conn,
inst_id=inst_id,
target_index=target_index,
- profit_rr=profit_rr,
underlying=u,
opt_type=str(opt_type) if opt_type else None,
trade_id=trade_id,
@@ -869,7 +854,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
premium_paid=sizing.get("total_premium"),
open_quote=fill_px,
target_index=target_index,
- profit_rr=profit_rr,
signal_note=signal_note,
)
finally:
@@ -985,14 +969,11 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
mon = tgt_map.get(inst)
if mon:
row["target_index"] = mon.get("target_index")
- row["profit_rr"] = mon.get("profit_rr")
row["target_monitor_id"] = mon.get("id")
row["target_monitor"] = mon
hedge_target = hedge_target_map.get(inst)
if hedge_target:
row["hedge_plan_target"] = hedge_target
- if hedge_target.get("oo_profit_rr") is not None:
- row.setdefault("profit_rr", hedge_target.get("oo_profit_rr"))
try:
from lib.instance.instance_dashboard_lib import _resolve_options_source
@@ -1050,28 +1031,12 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
conn_h.close()
except Exception as e:
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
- profit_rr = None
- target_index = None
- raw_rr = data.get("profit_rr")
- if raw_rr is None or str(raw_rr).strip() == "":
- raw_rr = data.get("oo_profit_rr")
- if raw_rr is not None and str(raw_rr).strip() != "":
- try:
- profit_rr = float(raw_rr)
- except (TypeError, ValueError):
- return jsonify({"ok": False, "msg": "盈亏比无效"})
- if profit_rr <= 0:
- return jsonify({"ok": False, "msg": "盈亏比须大于 0"})
- raw_tgt = data.get("target_index")
- if raw_tgt is not None and str(raw_tgt).strip() != "":
- try:
- target_index = float(raw_tgt)
- except (TypeError, ValueError):
- return jsonify({"ok": False, "msg": "目标位无效"})
- if target_index <= 0:
- return jsonify({"ok": False, "msg": "目标位无效"})
- if profit_rr is None and target_index is None:
- profit_rr = 2.0
+ try:
+ target_index = float(data.get("target_index"))
+ except (TypeError, ValueError):
+ return jsonify({"ok": False, "msg": "目标位无效"})
+ if target_index <= 0:
+ return jsonify({"ok": False, "msg": "目标位无效"})
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
@@ -1100,7 +1065,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
conn,
inst_id=inst_id,
target_index=target_index,
- profit_rr=profit_rr,
underlying=str(underlying) if underlying else None,
opt_type=str(opt_type) if opt_type else None,
trade_id=trade_id,
@@ -1447,24 +1411,7 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return []
- rows = [cfg["format_position_row"](p) for p in raw]
- try:
- from lib.options.options_db import sum_open_premium_paid
-
- conn = cfg["get_db"]()
- try:
- for row in rows:
- inst = str(row.get("inst_id") or "")
- if not inst:
- continue
- paid = sum_open_premium_paid(conn, inst)
- if paid is not None:
- row["premium_paid"] = paid
- finally:
- conn.close()
- except Exception:
- pass
- return rows
+ return [cfg["format_position_row"](p) for p in raw]
def _sync(conn):
from lib.exchange.okx_options_lib import fetch_option_position_history
diff --git a/lib/options/options_target_lib.py b/lib/options/options_target_lib.py
index 75c8f76..ecbb8c6 100644
--- a/lib/options/options_target_lib.py
+++ b/lib/options/options_target_lib.py
@@ -1,14 +1,11 @@
-"""期权目标委托:盈亏比×权利金触发后按买一限价平仓(无止损,到期结算).
-
-兼容旧「目标指数」委托:无 profit_rr 时仍按指数到位触发.
-"""
+"""期权目标位委托:指数目标价仅用于监控触发;触发后按买一限价平仓(无止损,到期结算)."""
from __future__ import annotations
import sqlite3
import time
from typing import Any, Callable
-from lib.options.options_db import init_options_tables, sum_open_premium_paid
+from lib.options.options_db import init_options_tables
from lib.options.options_pricing_lib import close_ref_prices, fetch_option_mark_px
@@ -21,18 +18,6 @@ def _safe_float(v: Any) -> float | None:
return None
-def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None:
- rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
- names: set[str] = set()
- for r in rows:
- try:
- names.add(str(r["name"]))
- except (TypeError, KeyError, IndexError):
- names.add(str(r[1]))
- if col not in names:
- conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}")
-
-
def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = None) -> tuple[float | None, float | None]:
from lib.exchange.okx_options_lib import option_fields_from_inst_id
@@ -78,44 +63,21 @@ def ensure_target_tables(conn: sqlite3.Connection) -> None:
ON options_target_monitors(status)
"""
)
- # 盈亏比=目标盈利/权利金;如 2=盈利 2 倍权利金.有值时优先生效,target_index 可置 0
- _ensure_column(conn, "options_target_monitors", "profit_rr", "REAL")
def target_hit(*, opt_type: str | None, index_px: float, target_index: float) -> bool:
- """旧逻辑:Call 指数≥目标;Put 指数≤目标."""
+ """Call:指数涨到/超过目标平仓;Put:指数跌到/低于目标平仓."""
ot = (opt_type or "").strip().upper()
if ot == "P":
return index_px <= target_index
return index_px >= target_index
-def profit_rr_hit(
- *,
- premium: float,
- bid: float | None,
- sheets: float,
- ct_mult: float,
- profit_rr: float,
-) -> bool:
- """买一回收 − 权利金 ≥ 盈亏比 × 权利金."""
- if premium <= 0 or profit_rr <= 0:
- return False
- if bid is None or float(bid) <= 0:
- return False
- if sheets <= 0 or ct_mult <= 0:
- return False
- recycle = float(bid) * float(sheets) * float(ct_mult)
- pnl = recycle - float(premium)
- return pnl + 1e-9 >= float(profit_rr) * float(premium)
-
-
def upsert_target_monitor(
conn: sqlite3.Connection,
*,
inst_id: str,
- target_index: float | None = None,
- profit_rr: float | None = None,
+ target_index: float,
underlying: str | None = None,
opt_type: str | None = None,
trade_id: int | None = None,
@@ -125,18 +87,9 @@ def upsert_target_monitor(
inst_id = (inst_id or "").strip()
if not inst_id:
return {"ok": False, "msg": "缺少 inst_id"}
-
- rr = _safe_float(profit_rr)
- tgt = _safe_float(target_index)
- if rr is not None and rr > 0:
- tgt_store = float(tgt) if tgt is not None and tgt > 0 else 0.0
- rr_store = float(rr)
- elif tgt is not None and tgt > 0:
- tgt_store = float(tgt)
- rr_store = None
- else:
- return {"ok": False, "msg": "请填写盈亏比(相对权利金,默认2)"}
-
+ if target_index is None or float(target_index) <= 0:
+ return {"ok": False, "msg": "目标位无效"}
+ target_index = float(target_index)
row = conn.execute(
"""
SELECT id FROM options_target_monitors
@@ -151,7 +104,6 @@ def upsert_target_monitor(
"""
UPDATE options_target_monitors
SET target_index = ?,
- profit_rr = ?,
underlying = COALESCE(?, underlying),
opt_type = COALESCE(?, opt_type),
trade_id = COALESCE(?, trade_id),
@@ -163,13 +115,14 @@ def upsert_target_monitor(
triggered_at = NULL
WHERE id = ?
""",
- (tgt_store, rr_store, underlying, opt_type, trade_id, sheets, int(row["id"])),
+ (target_index, underlying, opt_type, trade_id, sheets, int(row["id"])),
)
mon_id = int(row["id"])
+ # 同一合约其他进行中的委托取消,避免双轨触发重复推送
conn.execute(
"""
UPDATE options_target_monitors
- SET status = 'cancelled', message = '被新目标委托覆盖'
+ SET status = 'cancelled', message = '被新目标位覆盖'
WHERE inst_id = ? AND id != ? AND status IN ('active', 'closing')
""",
(inst_id, mon_id),
@@ -178,21 +131,13 @@ def upsert_target_monitor(
cur = conn.execute(
"""
INSERT INTO options_target_monitors
- (inst_id, underlying, opt_type, target_index, profit_rr, trade_id, sheets, status)
- VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
+ (inst_id, underlying, opt_type, target_index, trade_id, sheets, status)
+ VALUES (?, ?, ?, ?, ?, ?, 'active')
""",
- (inst_id, underlying, opt_type, tgt_store, rr_store, trade_id, sheets),
+ (inst_id, underlying, opt_type, target_index, trade_id, sheets),
)
mon_id = int(cur.lastrowid)
- out: dict[str, Any] = {
- "ok": True,
- "id": mon_id,
- "inst_id": inst_id,
- "target_index": tgt_store if tgt_store > 0 else None,
- }
- if rr_store is not None:
- out["profit_rr"] = rr_store
- return out
+ return {"ok": True, "id": mon_id, "inst_id": inst_id, "target_index": target_index}
def cancel_target_monitor(conn: sqlite3.Connection, *, inst_id: str | None = None, monitor_id: int | None = None) -> int:
@@ -221,19 +166,12 @@ def cancel_target_monitor(conn: sqlite3.Connection, *, inst_id: str | None = Non
def _row_to_target(r: sqlite3.Row) -> dict[str, Any]:
- tgt = _safe_float(r["target_index"])
- rr = None
- try:
- rr = _safe_float(r["profit_rr"])
- except (KeyError, IndexError):
- rr = None
return {
"id": int(r["id"]),
"inst_id": r["inst_id"],
"underlying": r["underlying"],
"opt_type": r["opt_type"],
- "target_index": tgt if tgt is not None and tgt > 0 else None,
- "profit_rr": rr if rr is not None and rr > 0 else None,
+ "target_index": _safe_float(r["target_index"]),
"trade_id": r["trade_id"],
"sheets": r["sheets"],
"status": r["status"],
@@ -242,16 +180,16 @@ def _row_to_target(r: sqlite3.Row) -> dict[str, Any]:
}
-_TARGET_SELECT = (
- "SELECT id, inst_id, underlying, opt_type, target_index, profit_rr, trade_id, sheets, "
- "status, message, created_at FROM options_target_monitors"
-)
-
-
def list_active_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]:
ensure_target_tables(conn)
rows = conn.execute(
- f"{_TARGET_SELECT} WHERE status = 'active' ORDER BY id DESC"
+ """
+ SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets,
+ status, message, created_at
+ FROM options_target_monitors
+ WHERE status = 'active'
+ ORDER BY id DESC
+ """
).fetchall()
return [_row_to_target(r) for r in rows]
@@ -260,7 +198,13 @@ def list_closing_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]:
"""已挂出平仓单、等待成交的目标(不再重复推送微信)."""
ensure_target_tables(conn)
rows = conn.execute(
- f"{_TARGET_SELECT} WHERE status = 'closing' ORDER BY id DESC"
+ """
+ SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets,
+ status, message, created_at
+ FROM options_target_monitors
+ WHERE status = 'closing'
+ ORDER BY id DESC
+ """
).fetchall()
return [_row_to_target(r) for r in rows]
@@ -342,23 +286,23 @@ def close_option_by_bid_depth(
inst_id,
sheets=sheets,
require_recycle_gate=True,
- signal_note="盈亏比平仓",
+ signal_note="目标位平仓",
)
+
def _notify_target_close(
cfg: dict[str, Any] | None,
send_wechat: Callable[[str], None] | None,
*,
account_label: str,
inst_id: str,
- target: float | None,
- profit_rr: float | None,
- idx: float | None,
+ target: float,
+ idx: float,
result: dict[str, Any],
conn: Any = None,
) -> None:
- """目标平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
+ """目标位平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
if result.get("fully_closed") or result.get("already_flat"):
if cfg is not None:
try:
@@ -368,13 +312,12 @@ def _notify_target_close(
cfg,
conn,
inst_id=inst_id,
- reason="盈亏比平仓" if profit_rr else "目标位平仓",
+ reason="目标位平仓",
sheets=result.get("submitted_sheets"),
premium_received=result.get("premium_received"),
close_quote=result.get("locked_bid_px") or result.get("bid"),
target_index=target,
trigger_idx=idx,
- profit_rr=profit_rr,
)
return
except Exception:
@@ -382,20 +325,14 @@ def _notify_target_close(
if not send_wechat:
return
try:
- if profit_rr is not None and profit_rr > 0:
- rule = f"盈亏比×{profit_rr:g}"
- elif target is not None:
- rule = f"目标指数:{target:g}"
- else:
- rule = "目标委托"
send_wechat(
"\n".join(
[
- "【OKX期权·盈亏比平仓】" if profit_rr else "【OKX期权·目标位平仓】",
+ "【OKX期权·目标位平仓】",
f"账户:{account_label}",
f"合约:{inst_id}",
- rule,
- f"触发指数:{idx:g}" if idx is not None else "触发指数:—",
+ f"目标指数:{target:g}",
+ f"触发指数:{idx:g}",
f"提交张数:{result.get('submitted_sheets') or '—'}",
f"预估收回:{result.get('premium_received') if result.get('premium_received') is not None else '—'} USDC",
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
@@ -417,77 +354,18 @@ def _result_fully_done(result: dict[str, Any]) -> bool:
return False
-def _monitor_should_close(
- conn: sqlite3.Connection,
- mon: dict[str, Any],
- pos: dict[str, Any],
- *,
- bid_fn: Callable[[str], float | None] | None,
- index_fn: Callable[[dict[str, Any]], float | None] | None,
-) -> tuple[bool, float | None]:
- """返回 (是否触发, 当前指数)."""
- inst_id = str(mon.get("inst_id") or "")
- rr = _safe_float(mon.get("profit_rr"))
- if index_fn is not None:
- idx = index_fn(pos)
- else:
- idx = _safe_float(pos.get("idx_px") or pos.get("idxPx"))
-
- if rr is not None and rr > 0:
- premium = sum_open_premium_paid(conn, inst_id)
- if premium is None or premium <= 0:
- premium = _safe_float(pos.get("premium_paid"))
- sheets = _safe_float(mon.get("sheets"))
- if sheets is None or sheets <= 0:
- sheets = _safe_float(pos.get("pos") or pos.get("avail_pos") or pos.get("availPos"))
- ct = _safe_float(pos.get("ct_mult") or pos.get("ctMult")) or 0.01
- bid = None
- if bid_fn is not None:
- try:
- bid = bid_fn(inst_id)
- except Exception:
- bid = None
- if bid is None:
- bid = _safe_float(pos.get("bid_px") or pos.get("bidPx") or pos.get("bid"))
- preview = pos.get("close_preview") if isinstance(pos.get("close_preview"), dict) else {}
- if bid is None:
- bid = _safe_float(preview.get("bid") or preview.get("best_bid"))
- if premium is None or sheets is None:
- return False, idx
- return (
- profit_rr_hit(
- premium=float(premium),
- bid=bid,
- sheets=float(sheets),
- ct_mult=float(ct),
- profit_rr=float(rr),
- ),
- idx,
- )
-
- target = _safe_float(mon.get("target_index"))
- if target is None or target <= 0 or idx is None:
- return False, idx
- opt_type = mon.get("opt_type") or pos.get("opt_type") or pos.get("optType")
- return (
- target_hit(opt_type=str(opt_type) if opt_type else None, index_px=idx, target_index=target),
- idx,
- )
-
-
def run_options_target_closes(
conn: sqlite3.Connection,
positions: list[dict[str, Any]],
*,
close_fn: Callable[[str], dict[str, Any]],
index_fn: Callable[[dict[str, Any]], float | None] | None = None,
- bid_fn: Callable[[str], float | None] | None = None,
send_wechat: Callable[[str], None] | None = None,
account_label: str = "OKX期权",
cfg: dict[str, Any] | None = None,
) -> int:
"""
- 扫描 active 目标委托;盈亏比达标(或旧指数到位)后限价平仓.
+ 扫描 active 目标委托;指数到位后限价平仓.
状态先 commit 再推微信,避免 sync 失败回滚导致同一笔反复推送.
未完全成交进入 closing,仅重试平仓不再推送.
返回本次新触发(并推送)的条数.
@@ -534,7 +412,7 @@ def run_options_target_closes(
status="triggered",
trigger_idx=idx,
close_ord_id=result.get("close_ord_id"),
- message="盈亏比限价平仓完成",
+ message="目标位限价平仓完成",
)
_commit_monitor(conn)
continue
@@ -551,7 +429,8 @@ def run_options_target_closes(
triggered = 0
for mon in list_active_targets(conn):
inst_id = str(mon.get("inst_id") or "")
- if not inst_id:
+ target = _safe_float(mon.get("target_index"))
+ if not inst_id or target is None:
continue
if inst_id in hedge_managed:
mark_monitor(
@@ -565,15 +444,17 @@ def run_options_target_closes(
pos = pos_by_inst.get(inst_id)
if not pos:
continue
- should, idx = _monitor_should_close(
- conn, mon, pos, bid_fn=bid_fn, index_fn=index_fn
- )
- if not should:
+ if index_fn is not None:
+ idx = index_fn(pos)
+ else:
+ idx = _safe_float(pos.get("idx_px") or pos.get("idxPx"))
+ if idx is None:
+ continue
+ opt_type = mon.get("opt_type") or pos.get("opt_type") or pos.get("optType")
+ if not target_hit(opt_type=str(opt_type) if opt_type else None, index_px=idx, target_index=target):
continue
result = close_fn(inst_id)
- rr = _safe_float(mon.get("profit_rr"))
- target = _safe_float(mon.get("target_index"))
if result.get("already_flat"):
mark_monitor(conn, int(mon["id"]), status="expired", trigger_idx=idx, message="持仓已平")
_commit_monitor(conn)
@@ -591,19 +472,15 @@ def run_options_target_closes(
done = _result_fully_done(result)
status = "triggered" if done else "closing"
- hit_msg = (
- "盈亏比达标限价平仓"
- if (rr is not None and rr > 0)
- else "目标位触发限价平仓"
- )
mark_monitor(
conn,
int(mon["id"]),
status=status,
trigger_idx=idx,
close_ord_id=result.get("close_ord_id"),
- message=hit_msg if done else "已挂买一限价,等待成交",
+ message="目标位触发限价平仓" if done else "目标位已挂买一限价,等待成交",
)
+ # 关键:先落库,再推送——否则后续 sync 异常回滚会让同一笔反复推微信
_commit_monitor(conn)
triggered += 1
_notify_target_close(
@@ -612,7 +489,6 @@ def run_options_target_closes(
account_label=account_label,
inst_id=inst_id,
target=target,
- profit_rr=rr,
idx=idx,
result=result,
conn=conn,
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index 9fe468f..c9546ca 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -107,15 +107,17 @@