Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45c7e73db5 | |||
| 37f787e6a9 |
@@ -150,6 +150,13 @@ OKX_OPTIONS_TD_MODE=isolated
|
||||
OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
|
||||
# 对冲买期权等成交超时(秒);超时撤未成交部分,未完全成交则开仓失败
|
||||
OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC=12
|
||||
# 目标平仓门控(目标位触发后按买一平仓前需满足)
|
||||
OKX_OPTIONS_CLOSE_GATE_MODE=premium
|
||||
# premium:可回收≥权利金×倍数;net_pnl:净盈亏(买一可回收−权利金)折USDT≥下限
|
||||
OKX_OPTIONS_CLOSE_RECYCLE_MULT=2
|
||||
OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN=1.05
|
||||
OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT=0
|
||||
OKX_OPTIONS_CLOSE_HOLD_SECONDS=120
|
||||
|
||||
# =============================================================================
|
||||
# 对冲计划(仅 OKX;由 OKX_TRADE_MODE 控制是否启用;详见 docs/对冲计划开发方案.md)
|
||||
|
||||
@@ -351,7 +351,7 @@
|
||||
body.appendChild(panelsWrap);
|
||||
body.dataset.envModeSectionIdx = String(modeSectionIdx);
|
||||
bindTradeModeAutoRefresh(body);
|
||||
bindCompoundBudgetVisibility(body);
|
||||
bindOptionsSectionVisibility(body);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -363,27 +363,104 @@
|
||||
return input ? input.closest(".env-field-row") : null;
|
||||
}
|
||||
|
||||
function syncCompoundBudgetVisibility(body) {
|
||||
function envFieldValue(body, key, fallback) {
|
||||
const input = body && body.querySelector('.env-field-input[data-env-key="' + key + '"]');
|
||||
if (!input) return fallback;
|
||||
return String(input.value || fallback || "").trim();
|
||||
}
|
||||
|
||||
function envTruthy(v) {
|
||||
return ["1", "true", "yes", "on"].indexOf(String(v || "").toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
function setEnvRowHidden(row, hide) {
|
||||
if (!row) return;
|
||||
row.hidden = !!hide;
|
||||
row.style.display = hide ? "none" : "";
|
||||
}
|
||||
|
||||
var COIN_ONLY_ENV_KEYS = [
|
||||
"OKX_OPTIONS_COIN_COMPOUND",
|
||||
"OKX_OPTIONS_COIN_BUDGET_USDT",
|
||||
"OKX_OPTIONS_COIN_MAX_USDT_ENABLED",
|
||||
"OKX_OPTIONS_COIN_MAX_USDT",
|
||||
"OKX_OPTIONS_COIN_SPOT_BUY_BUFFER",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
|
||||
];
|
||||
var USDC_ONLY_ENV_KEYS = [
|
||||
"OKX_OPTIONS_TRADE_BUDGET_USDC",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_ENABLED",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_CAP_USDC",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT",
|
||||
];
|
||||
|
||||
function syncOptionsSectionVisibility(body) {
|
||||
if (!body) return;
|
||||
const compoundSel = body.querySelector(
|
||||
'.env-field-input[data-env-key="OKX_OPTIONS_COMPOUND_FULL_ENABLED"]'
|
||||
);
|
||||
const budgetRow = envFieldRowByKey(body, "OKX_OPTIONS_TRADE_BUDGET_USDC");
|
||||
if (!budgetRow) return;
|
||||
const compoundOn = !compoundSel || String(compoundSel.value || "").toLowerCase() === "true";
|
||||
budgetRow.hidden = compoundOn;
|
||||
budgetRow.style.display = compoundOn ? "none" : "";
|
||||
var margin = (envFieldValue(body, "OKX_OPTIONS_MARGIN_MODE", "coin") || "coin").toLowerCase();
|
||||
var isCoin = margin !== "usdc";
|
||||
var gateMode = (envFieldValue(body, "OKX_OPTIONS_CLOSE_GATE_MODE", "premium") || "premium").toLowerCase();
|
||||
var compoundOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COMPOUND_FULL_ENABLED", "true"));
|
||||
var coinCompoundOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COIN_COMPOUND", "true"));
|
||||
var coinMaxOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COIN_MAX_USDT_ENABLED", "false"));
|
||||
var compoundCapOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", "false"));
|
||||
|
||||
COIN_ONLY_ENV_KEYS.forEach(function (key) {
|
||||
setEnvRowHidden(envFieldRowByKey(body, key), !isCoin);
|
||||
});
|
||||
USDC_ONLY_ENV_KEYS.forEach(function (key) {
|
||||
setEnvRowHidden(envFieldRowByKey(body, key), isCoin);
|
||||
});
|
||||
|
||||
// 门控子项:按模式显隐(仍受本位过滤)
|
||||
if (isCoin) {
|
||||
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN"), gateMode !== "premium");
|
||||
} else {
|
||||
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_RECYCLE_MULT"), gateMode !== "premium");
|
||||
}
|
||||
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT"), gateMode !== "net_pnl");
|
||||
|
||||
if (!isCoin) {
|
||||
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_TRADE_BUDGET_USDC"), compoundOn);
|
||||
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED"), !compoundOn);
|
||||
setEnvRowHidden(
|
||||
envFieldRowByKey(body, "OKX_OPTIONS_COMPOUND_FULL_CAP_USDC"),
|
||||
!(compoundOn && compoundCapOn)
|
||||
);
|
||||
}
|
||||
if (isCoin) {
|
||||
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COIN_BUDGET_USDT"), coinCompoundOn);
|
||||
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COIN_MAX_USDT"), !coinMaxOn);
|
||||
}
|
||||
}
|
||||
|
||||
function bindOptionsSectionVisibility(body) {
|
||||
if (!body) return;
|
||||
syncOptionsSectionVisibility(body);
|
||||
var keys = [
|
||||
"OKX_OPTIONS_MARGIN_MODE",
|
||||
"OKX_OPTIONS_CLOSE_GATE_MODE",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_ENABLED",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED",
|
||||
"OKX_OPTIONS_COIN_COMPOUND",
|
||||
"OKX_OPTIONS_COIN_MAX_USDT_ENABLED",
|
||||
];
|
||||
keys.forEach(function (key) {
|
||||
var el = body.querySelector('.env-field-input[data-env-key="' + key + '"]');
|
||||
if (!el || el.dataset.optionsVisBound === "1") return;
|
||||
el.dataset.optionsVisBound = "1";
|
||||
el.addEventListener("change", function () {
|
||||
syncOptionsSectionVisibility(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncCompoundBudgetVisibility(body) {
|
||||
syncOptionsSectionVisibility(body);
|
||||
}
|
||||
|
||||
function bindCompoundBudgetVisibility(body) {
|
||||
if (!body) return;
|
||||
syncCompoundBudgetVisibility(body);
|
||||
const compoundSel = body.querySelector(
|
||||
'.env-field-input[data-env-key="OKX_OPTIONS_COMPOUND_FULL_ENABLED"]'
|
||||
);
|
||||
if (!compoundSel || compoundSel.dataset.compoundBudgetBound === "1") return;
|
||||
compoundSel.dataset.compoundBudgetBound = "1";
|
||||
compoundSel.addEventListener("change", () => syncCompoundBudgetVisibility(body));
|
||||
bindOptionsSectionVisibility(body);
|
||||
}
|
||||
|
||||
function bindTradeModeAutoRefresh(body) {
|
||||
|
||||
@@ -832,7 +832,7 @@
|
||||
const gate = preview.close_gate || {};
|
||||
// 2× 只是目标平仓门控,本身不会自动平;手动买一平不拦截
|
||||
if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) {
|
||||
return "目标门控: " + (preview.close_gate_msg || gate.msg || "可回收需≥2×权利金并持续2分钟");
|
||||
return "目标门控: " + (preview.close_gate_msg || gate.msg || "目标平仓门控未过");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
}
|
||||
const gate = preview.close_gate || {};
|
||||
if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) {
|
||||
return "目标门控: " + (preview.close_gate_msg || gate.msg || "可回收需≥2×权利金并持续2分钟");
|
||||
return "目标门控: " + (preview.close_gate_msg || gate.msg || "目标平仓门控未过");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -106,6 +106,11 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"OKX_OPTIONS_COIN_MAX_USDT_ENABLED",
|
||||
"OKX_OPTIONS_COIN_MAX_USDT",
|
||||
"OKX_OPTIONS_COIN_SPOT_BUY_BUFFER",
|
||||
"OKX_OPTIONS_CLOSE_GATE_MODE",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
|
||||
"OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT",
|
||||
"OKX_OPTIONS_CLOSE_HOLD_SECONDS",
|
||||
"OKX_TRADE_MODE",
|
||||
"MAX_ACTIVE_HEDGE_PLANS",
|
||||
"HEDGE_PLAN_LIVE_ORDER",
|
||||
@@ -171,6 +176,10 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {
|
||||
("coin", "币本位(USDT买币桥)"),
|
||||
("usdc", "USDC(USDⓈ权利金)"),
|
||||
),
|
||||
"OKX_OPTIONS_CLOSE_GATE_MODE": (
|
||||
("premium", "权利金倍数"),
|
||||
("net_pnl", "净盈亏(USDT)"),
|
||||
),
|
||||
"HEDGE_PLAN_OPTION_PRIMARY": (
|
||||
("true", "以期权为主"),
|
||||
("false", "保险模式"),
|
||||
|
||||
Vendored
+95
-10
@@ -150,6 +150,31 @@ _OPTIONS_SECTION: dict[str, Any] = {
|
||||
"链上仅显示有卖一",
|
||||
"默认 true;开启后隐藏无卖一深度或深度不足1张的合约(含标记价估算行)",
|
||||
),
|
||||
(
|
||||
"OKX_OPTIONS_CLOSE_GATE_MODE",
|
||||
"目标平仓门控模式",
|
||||
"premium=可回收≥权利金×倍数(币本位默认×1.05);net_pnl=净盈亏折USDT≥阈值",
|
||||
),
|
||||
(
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT",
|
||||
"门控权利金倍数(USDC)",
|
||||
"仅 premium 模式;USDC 权利金仓默认 2",
|
||||
),
|
||||
(
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
|
||||
"门控权利金倍数(币本位)",
|
||||
"仅 premium 模式;ETH/BTC 权利金仓默认 1.05",
|
||||
),
|
||||
(
|
||||
"OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT",
|
||||
"门控净盈亏下限(U)",
|
||||
"仅 net_pnl 模式;买一可回收−权利金折 USDT 后需≥此值;0 或 1 等",
|
||||
),
|
||||
(
|
||||
"OKX_OPTIONS_CLOSE_HOLD_SECONDS",
|
||||
"门控持续秒数",
|
||||
"条件达标后需持续满足的秒数,默认 120",
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -259,6 +284,11 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"OKX_OPTIONS_MAX_DTE_DAYS": "2",
|
||||
"OKX_OPTIONS_MAX_ACTIVE_POSITIONS": "0",
|
||||
"OKX_OPTIONS_MARGIN_MODE": "coin",
|
||||
"OKX_OPTIONS_CLOSE_GATE_MODE": "premium",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT": "2",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN": "1.05",
|
||||
"OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT": "0",
|
||||
"OKX_OPTIONS_CLOSE_HOLD_SECONDS": "120",
|
||||
"OKX_TRADE_MODE": "options",
|
||||
"HEDGE_PLAN_ENABLED": "false",
|
||||
"MAX_ACTIVE_HEDGE_PLANS": "1",
|
||||
@@ -450,7 +480,7 @@ def build_env_ui_payload(
|
||||
_build_field(key, label, note, schema, values)
|
||||
for key, label, note in sec["fields"]
|
||||
]
|
||||
fields = _mark_compound_budget_hidden(fields)
|
||||
fields = _mark_options_section_hidden(fields)
|
||||
groups.append({
|
||||
"title": sec["title"],
|
||||
"fields": fields,
|
||||
@@ -459,18 +489,68 @@ def build_env_ui_payload(
|
||||
return groups
|
||||
|
||||
|
||||
def _mark_compound_budget_hidden(fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""全仓复利开启时标记单笔预算为 hidden(供 SSR/前端隐藏;切换开关仍可再显示)."""
|
||||
compound_on = True
|
||||
# 币本位专属 / USDC 专属(配置页互斥隐藏)
|
||||
_COIN_ONLY_ENV_KEYS = frozenset({
|
||||
"OKX_OPTIONS_COIN_COMPOUND",
|
||||
"OKX_OPTIONS_COIN_BUDGET_USDT",
|
||||
"OKX_OPTIONS_COIN_MAX_USDT_ENABLED",
|
||||
"OKX_OPTIONS_COIN_MAX_USDT",
|
||||
"OKX_OPTIONS_COIN_SPOT_BUY_BUFFER",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
|
||||
})
|
||||
_USDC_ONLY_ENV_KEYS = frozenset({
|
||||
"OKX_OPTIONS_TRADE_BUDGET_USDC",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_ENABLED",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED",
|
||||
"OKX_OPTIONS_COMPOUND_FULL_CAP_USDC",
|
||||
"OKX_OPTIONS_CLOSE_RECYCLE_MULT",
|
||||
})
|
||||
|
||||
|
||||
def _field_current(fields: list[dict[str, Any]], key: str, default: str = "") -> str:
|
||||
for f in fields:
|
||||
if f.get("key") == "OKX_OPTIONS_COMPOUND_FULL_ENABLED":
|
||||
compound_on = _env_truthy(str(f.get("current") or f.get("default") or "true"))
|
||||
break
|
||||
if not compound_on:
|
||||
return fields
|
||||
if f.get("key") == key:
|
||||
return str(f.get("current") or f.get("default") or default).strip()
|
||||
return default
|
||||
|
||||
|
||||
def _mark_options_section_hidden(fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""按本位/门控模式/全仓复利标记 hidden(供 SSR/前端;切换开关仍可再显示)."""
|
||||
margin = (_field_current(fields, "OKX_OPTIONS_MARGIN_MODE", "coin") or "coin").lower()
|
||||
is_coin = margin != "usdc"
|
||||
gate_mode = (_field_current(fields, "OKX_OPTIONS_CLOSE_GATE_MODE", "premium") or "premium").lower()
|
||||
compound_on = _env_truthy(_field_current(fields, "OKX_OPTIONS_COMPOUND_FULL_ENABLED", "true"))
|
||||
coin_compound_on = _env_truthy(_field_current(fields, "OKX_OPTIONS_COIN_COMPOUND", "true"))
|
||||
coin_max_on = _env_truthy(_field_current(fields, "OKX_OPTIONS_COIN_MAX_USDT_ENABLED", "false"))
|
||||
compound_cap_on = _env_truthy(
|
||||
_field_current(fields, "OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", "false")
|
||||
)
|
||||
|
||||
out: list[dict[str, Any]] = []
|
||||
for f in fields:
|
||||
if f.get("key") == "OKX_OPTIONS_TRADE_BUDGET_USDC":
|
||||
key = str(f.get("key") or "")
|
||||
hide = False
|
||||
if is_coin and key in _USDC_ONLY_ENV_KEYS:
|
||||
hide = True
|
||||
elif (not is_coin) and key in _COIN_ONLY_ENV_KEYS:
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_CLOSE_RECYCLE_MULT" and gate_mode != "premium":
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN" and gate_mode != "premium":
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT" and gate_mode != "net_pnl":
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_TRADE_BUDGET_USDC" and compound_on:
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED" and not compound_on:
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_COMPOUND_FULL_CAP_USDC" and not (compound_on and compound_cap_on):
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_COIN_BUDGET_USDT" and coin_compound_on:
|
||||
hide = True
|
||||
elif key == "OKX_OPTIONS_COIN_MAX_USDT" and not coin_max_on:
|
||||
hide = True
|
||||
if hide:
|
||||
item = dict(f)
|
||||
item["hidden"] = True
|
||||
out.append(item)
|
||||
@@ -479,6 +559,11 @@ def _mark_compound_budget_hidden(fields: list[dict[str, Any]]) -> list[dict[str,
|
||||
return out
|
||||
|
||||
|
||||
def _mark_compound_budget_hidden(fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""兼容旧调用名."""
|
||||
return _mark_options_section_hidden(fields)
|
||||
|
||||
|
||||
def filter_updates_for_ui(exchange_key: str, updates: dict[str, str]) -> dict[str, str]:
|
||||
allowed = ui_allowed_keys(exchange_key)
|
||||
return {k: v for k, v in (updates or {}).items() if k in allowed}
|
||||
|
||||
@@ -155,7 +155,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=23"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=24"></script>
|
||||
<script src="/static/instance_live.js?v=7"></script>
|
||||
<script src="/static/instance_embed.js?v=32"></script>
|
||||
<script src="/static/instance_mobile_nav.js?v=2"></script>
|
||||
|
||||
@@ -2097,6 +2097,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=23"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=24"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -145,6 +145,10 @@ def close_option_by_bid1(
|
||||
premium_paid = _open_premium_paid(cfg, inst_id)
|
||||
if premium_paid is None:
|
||||
premium_paid = _safe_float(pos.get("premium_paid"))
|
||||
from lib.options.options_notify_lib import resolve_premium_ccy
|
||||
|
||||
premium_ccy = resolve_premium_ccy(pos.get("premium_ccy"), inst_id=inst_id)
|
||||
index_px = _safe_float(pos.get("idxPx")) or _safe_float(q.get("index_px"))
|
||||
|
||||
# 已有未成交卖平单:等成交,不撤不重挂
|
||||
try:
|
||||
@@ -194,7 +198,13 @@ def close_option_by_bid1(
|
||||
)
|
||||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||||
# 不撤他人挂单:仅拒绝本轮下单
|
||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
||||
update_close_gate(
|
||||
inst_id,
|
||||
recycle_usdc=None,
|
||||
premium_paid=premium_paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": preview.get("bid_invalid_reason") or "暂无有效买盘,禁止平仓",
|
||||
@@ -208,7 +218,13 @@ def close_option_by_bid1(
|
||||
bid_px = _safe_float(q.get("bid"))
|
||||
stub, stub_reason = is_stub_bid_px(bid_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||
if stub or bid_px is None or bid_px <= 0:
|
||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
||||
update_close_gate(
|
||||
inst_id,
|
||||
recycle_usdc=None,
|
||||
premium_paid=premium_paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": stub_reason or "暂无买一,无法限价平仓",
|
||||
@@ -231,7 +247,13 @@ def close_option_by_bid1(
|
||||
|
||||
stub_lv, stub_lv_reason = is_stub_bid_px(level_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||
if stub_lv:
|
||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
||||
update_close_gate(
|
||||
inst_id,
|
||||
recycle_usdc=None,
|
||||
premium_paid=premium_paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": stub_lv_reason or "暂无有效买盘,禁止平仓",
|
||||
@@ -240,16 +262,18 @@ def close_option_by_bid1(
|
||||
"liquidity_blocked": True,
|
||||
}
|
||||
|
||||
# 自动平仓:2×权利金门控(首次);通过后同仓续批只验流动性
|
||||
# 自动平仓:目标门控(首次);通过后同仓续批只验流动性
|
||||
gate = update_close_gate(
|
||||
inst_id,
|
||||
recycle_usdc=_safe_float(preview.get("total_received")),
|
||||
premium_paid=premium_paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
if require_recycle_gate and not is_close_gate_passed(inst_id) and not gate.get("ready"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": gate.get("msg") or "平仓门控未就绪(需可回收≥2×权利金并持续一段时间)",
|
||||
"msg": gate.get("msg") or "平仓门控未就绪(需满足目标门控并持续一段时间)",
|
||||
"stopped_reason": "close_gate",
|
||||
"auto_close_blocked": True,
|
||||
"close_gate": gate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""期权按买盘平仓门控:可回收需 ≥ N×权利金,并持续持有一段时间后才允许平仓."""
|
||||
"""期权按买盘平仓门控:权利金倍数或净盈亏(USDT)达标并持续 hold 秒后才允许目标平仓."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -14,12 +14,32 @@ def _env_float(key: str, default: float) -> float:
|
||||
return default
|
||||
|
||||
|
||||
# 可回收 ≥ 权利金 × 倍数,且该状态持续满 hold_seconds 才允许按买盘平仓
|
||||
def _close_gate_mode() -> str:
|
||||
raw = (os.getenv("OKX_OPTIONS_CLOSE_GATE_MODE") or "premium").strip().lower()
|
||||
return raw if raw in ("premium", "net_pnl") else "premium"
|
||||
|
||||
|
||||
def _close_recycle_mult(premium_ccy: str) -> float:
|
||||
ccy = (premium_ccy or "USDC").strip().upper()
|
||||
if ccy in ("ETH", "BTC"):
|
||||
return _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN", 1.05)
|
||||
return _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT", 2.0)
|
||||
|
||||
|
||||
def _close_hold_seconds() -> float:
|
||||
return _env_float("OKX_OPTIONS_CLOSE_HOLD_SECONDS", 120.0)
|
||||
|
||||
|
||||
def _close_net_pnl_min_usdt() -> float:
|
||||
return _env_float("OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT", 0.0)
|
||||
|
||||
|
||||
# 兼容旧引用
|
||||
CLOSE_RECYCLE_MIN_MULT = _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT", 2.0)
|
||||
CLOSE_RECYCLE_HOLD_SECONDS = _env_float("OKX_OPTIONS_CLOSE_HOLD_SECONDS", 120.0)
|
||||
|
||||
_lock = threading.Lock()
|
||||
# inst_id -> {"ok_since": float|None, "recycle": float, "premium": float, "updated": float}
|
||||
# inst_id -> gate state
|
||||
_gates: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
@@ -41,7 +61,7 @@ def clear_close_gate(inst_id: str | None = None) -> None:
|
||||
|
||||
|
||||
def mark_close_gate_passed(inst_id: str) -> None:
|
||||
"""标记同仓已通过 2× 门控,续批平仓只验流动性."""
|
||||
"""标记同仓已通过门控,续批平仓只验流动性."""
|
||||
inst = (inst_id or "").strip()
|
||||
if not inst:
|
||||
return
|
||||
@@ -68,6 +88,30 @@ def _fmt_gate_amt(v: float, *, ccy: str) -> str:
|
||||
return f"{float(v):.4f}"
|
||||
|
||||
|
||||
def _fmt_usdt(v: float) -> str:
|
||||
sign = "+" if v >= 0 else ""
|
||||
return f"{sign}{float(v):.2f}"
|
||||
|
||||
|
||||
def _net_pnl_usdt(
|
||||
*,
|
||||
recv: float | None,
|
||||
prem: float | None,
|
||||
premium_ccy: str,
|
||||
index_px: float | None,
|
||||
) -> float | None:
|
||||
if recv is None or prem is None:
|
||||
return None
|
||||
net = float(recv) - float(prem)
|
||||
ccy = (premium_ccy or "USDC").strip().upper() or "USDC"
|
||||
if ccy in ("ETH", "BTC"):
|
||||
idx = _safe_float(index_px)
|
||||
if idx is None or idx <= 0:
|
||||
return None
|
||||
return net * float(idx)
|
||||
return net
|
||||
|
||||
|
||||
def update_close_gate(
|
||||
inst_id: str,
|
||||
*,
|
||||
@@ -77,9 +121,14 @@ def update_close_gate(
|
||||
min_mult: float | None = None,
|
||||
hold_seconds: float | None = None,
|
||||
premium_ccy: str | None = None,
|
||||
index_px: float | None = None,
|
||||
gate_mode: str | None = None,
|
||||
net_pnl_min_usdt: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
根据当前买盘可回收金额刷新门控.
|
||||
premium: 可回收 ≥ 权利金×倍数(币本位默认×1.05,USDC 默认×2)
|
||||
net_pnl: 净盈亏(买一可回收−权利金)折 USDT ≥ 阈值(默认 0U)
|
||||
条件不满足时重置计时;满足时从首次满足起累计持续时间.
|
||||
"""
|
||||
inst = (inst_id or "").strip()
|
||||
@@ -91,15 +140,16 @@ def update_close_gate(
|
||||
"msg": "缺少合约",
|
||||
}
|
||||
ts = float(now if now is not None else time.time())
|
||||
mult = float(min_mult if min_mult is not None else CLOSE_RECYCLE_MIN_MULT)
|
||||
hold = float(hold_seconds if hold_seconds is not None else CLOSE_RECYCLE_HOLD_SECONDS)
|
||||
if mult <= 0:
|
||||
mult = 2.0
|
||||
mode = (gate_mode or _close_gate_mode()).strip().lower()
|
||||
if mode not in ("premium", "net_pnl"):
|
||||
mode = "premium"
|
||||
hold = float(hold_seconds if hold_seconds is not None else _close_hold_seconds())
|
||||
if hold < 0:
|
||||
hold = 0.0
|
||||
|
||||
with _lock:
|
||||
prev_ccy = (_gates.get(inst) or {}).get("premium_ccy")
|
||||
prev = _gates.get(inst) or {}
|
||||
prev_ccy = prev.get("premium_ccy")
|
||||
ccy = (premium_ccy or prev_ccy or "USDC").strip().upper() or "USDC"
|
||||
if ccy not in ("ETH", "BTC", "USDC"):
|
||||
ccy = "USDC"
|
||||
@@ -107,21 +157,44 @@ def update_close_gate(
|
||||
|
||||
prem = _safe_float(premium_paid)
|
||||
recv = _safe_float(recycle_usdc)
|
||||
mult = float(min_mult if min_mult is not None else _close_recycle_mult(ccy))
|
||||
if mult <= 0:
|
||||
mult = _close_recycle_mult(ccy)
|
||||
min_pnl_u = float(net_pnl_min_usdt if net_pnl_min_usdt is not None else _close_net_pnl_min_usdt())
|
||||
idx = _safe_float(index_px)
|
||||
if idx is None:
|
||||
idx = _safe_float(prev.get("index_px"))
|
||||
|
||||
need = round(prem * mult, need_decimals) if prem is not None and prem > 0 else None
|
||||
recycle_ok = bool(
|
||||
prem is not None and prem > 0 and recv is not None and need is not None and recv + 1e-12 >= need
|
||||
)
|
||||
net_pnl_u = _net_pnl_usdt(recv=recv, prem=prem, premium_ccy=ccy, index_px=idx)
|
||||
|
||||
if mode == "net_pnl":
|
||||
if prem is None or prem <= 0:
|
||||
condition_ok = False
|
||||
elif recv is None:
|
||||
condition_ok = False
|
||||
elif net_pnl_u is None:
|
||||
condition_ok = False
|
||||
else:
|
||||
condition_ok = bool(net_pnl_u + 1e-9 >= min_pnl_u)
|
||||
else:
|
||||
condition_ok = bool(
|
||||
prem is not None and prem > 0 and recv is not None and need is not None and recv + 1e-12 >= need
|
||||
)
|
||||
|
||||
with _lock:
|
||||
prev = _gates.get(inst) or {}
|
||||
prev_mode = prev.get("gate_mode")
|
||||
ok_since = prev.get("ok_since")
|
||||
if recycle_ok:
|
||||
if prev_mode != mode:
|
||||
ok_since = None
|
||||
if condition_ok:
|
||||
if ok_since is None:
|
||||
ok_since = ts
|
||||
else:
|
||||
ok_since = None
|
||||
held = (ts - float(ok_since)) if ok_since is not None else 0.0
|
||||
ready = bool(recycle_ok and held + 1e-9 >= hold)
|
||||
ready = bool(condition_ok and held + 1e-9 >= hold)
|
||||
prev_passed = bool(prev.get("passed"))
|
||||
passed = prev_passed or ready
|
||||
state = {
|
||||
@@ -134,15 +207,38 @@ def update_close_gate(
|
||||
"hold_seconds": hold,
|
||||
"passed": passed,
|
||||
"premium_ccy": ccy,
|
||||
"gate_mode": mode,
|
||||
"net_pnl_usdt": net_pnl_u,
|
||||
"net_pnl_min_usdt": min_pnl_u,
|
||||
"index_px": idx,
|
||||
}
|
||||
_gates[inst] = state
|
||||
|
||||
remain = max(0.0, hold - held) if recycle_ok and not ready else None
|
||||
if prem is None or prem <= 0:
|
||||
remain = max(0.0, hold - held) if condition_ok and not ready else None
|
||||
if mode == "net_pnl":
|
||||
if prem is None or prem <= 0:
|
||||
msg = "缺少权利金,无法校验平仓门控"
|
||||
elif recv is None:
|
||||
msg = "暂无有效买盘可回收金额"
|
||||
elif net_pnl_u is None:
|
||||
msg = "缺少指数价,无法将币本位净盈亏折算为 USDT"
|
||||
elif not condition_ok:
|
||||
msg = (
|
||||
f"净盈亏 {_fmt_usdt(net_pnl_u)} U(估) < {min_pnl_u:g} U,"
|
||||
f"目标平仓门控未过"
|
||||
)
|
||||
elif not ready:
|
||||
msg = (
|
||||
f"净盈亏 {_fmt_usdt(net_pnl_u)} U(估) ≥ {min_pnl_u:g} U,"
|
||||
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过"
|
||||
)
|
||||
else:
|
||||
msg = f"净盈亏 ≥ {min_pnl_u:g} U 且持续≥{hold:.0f}s,目标触达后可按买一平仓"
|
||||
elif prem is None or prem <= 0:
|
||||
msg = "缺少权利金,无法校验平仓门控"
|
||||
elif recv is None:
|
||||
msg = "暂无有效买盘可回收金额"
|
||||
elif not recycle_ok:
|
||||
elif not condition_ok:
|
||||
msg = (
|
||||
f"可回收 {_fmt_gate_amt(recv, ccy=ccy)} {ccy} < 权利金×{mult:g}"
|
||||
f"({_fmt_gate_amt(need, ccy=ccy)}),目标平仓门控未过"
|
||||
@@ -160,19 +256,23 @@ def update_close_gate(
|
||||
"ok": True,
|
||||
"ready": ready,
|
||||
"passed": passed,
|
||||
"recycle_ok": recycle_ok,
|
||||
"recycle_ok": condition_ok,
|
||||
"recycle_usdc": recv,
|
||||
"premium_paid": prem,
|
||||
"need_recycle_usdc": need,
|
||||
"premium_ccy": ccy,
|
||||
"min_mult": mult,
|
||||
"hold_seconds": hold,
|
||||
"held_seconds": round(held, 1) if recycle_ok else 0.0,
|
||||
"held_seconds": round(held, 1) if condition_ok else 0.0,
|
||||
"remain_seconds": round(remain, 1) if remain is not None else None,
|
||||
"ok_since": ok_since,
|
||||
"msg": msg,
|
||||
"auto_close_blocked": auto_blocked,
|
||||
"close_gate_blocked": auto_blocked,
|
||||
"gate_mode": mode,
|
||||
"net_pnl_usdt": net_pnl_u,
|
||||
"net_pnl_min_usdt": min_pnl_u,
|
||||
"index_px": idx,
|
||||
}
|
||||
|
||||
|
||||
@@ -182,12 +282,13 @@ def check_close_gate(
|
||||
recycle_usdc: float | None = None,
|
||||
premium_paid: float | None = None,
|
||||
premium_ccy: str | None = None,
|
||||
index_px: float | None = None,
|
||||
refresh: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""检查是否允许平仓;默认先用最新回收/权利金刷新."""
|
||||
inst = (inst_id or "").strip()
|
||||
if refresh:
|
||||
if recycle_usdc is None or premium_paid is None or premium_ccy is None:
|
||||
if recycle_usdc is None or premium_paid is None or premium_ccy is None or index_px is None:
|
||||
with _lock:
|
||||
prev = _gates.get(inst) or {}
|
||||
if recycle_usdc is None:
|
||||
@@ -196,11 +297,14 @@ def check_close_gate(
|
||||
premium_paid = prev.get("premium")
|
||||
if premium_ccy is None:
|
||||
premium_ccy = prev.get("premium_ccy")
|
||||
if index_px is None:
|
||||
index_px = prev.get("index_px")
|
||||
return update_close_gate(
|
||||
inst,
|
||||
recycle_usdc=recycle_usdc,
|
||||
premium_paid=premium_paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
with _lock:
|
||||
prev = _gates.get(inst)
|
||||
@@ -210,10 +314,12 @@ def check_close_gate(
|
||||
recycle_usdc=recycle_usdc,
|
||||
premium_paid=premium_paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
return update_close_gate(
|
||||
inst,
|
||||
recycle_usdc=recycle_usdc if recycle_usdc is not None else prev.get("recycle"),
|
||||
premium_paid=premium_paid if premium_paid is not None else prev.get("premium"),
|
||||
premium_ccy=premium_ccy if premium_ccy is not None else prev.get("premium_ccy"),
|
||||
index_px=index_px if index_px is not None else prev.get("index_px"),
|
||||
)
|
||||
|
||||
@@ -54,10 +54,15 @@ def attach_close_preview(
|
||||
max_levels=1,
|
||||
)
|
||||
premium_ccy = str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC"
|
||||
# 残档时不累计 2×门控;有效买一时刷新计时(仅自动平仓需要)
|
||||
index_px = _safe_float(row.get("idx_px") or row.get("idxPx") or row.get("index_px"))
|
||||
# 残档时不累计门控;有效买一时刷新计时(仅自动平仓需要)
|
||||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||||
gate = update_close_gate(
|
||||
inst_id, recycle_usdc=None, premium_paid=paid, premium_ccy=premium_ccy
|
||||
inst_id,
|
||||
recycle_usdc=None,
|
||||
premium_paid=paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
preview["close_gate"] = gate
|
||||
preview["close_gate_blocked"] = True
|
||||
@@ -70,6 +75,7 @@ def attach_close_preview(
|
||||
recycle_usdc=_safe_float(preview.get("total_received")),
|
||||
premium_paid=paid,
|
||||
premium_ccy=premium_ccy,
|
||||
index_px=index_px,
|
||||
)
|
||||
passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready"))
|
||||
preview["close_gate"] = gate
|
||||
|
||||
Reference in New Issue
Block a user