下线永续监控与 USDC 期权,仅保留币本位期权路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-07 12:32:01 +08:00
parent 1a4eb15109
commit 7c5a6e25b2
10 changed files with 1581 additions and 2228 deletions
+1 -1
View File
@@ -116,7 +116,7 @@ OKX_OPTIONS_ENABLED=true
# OKX_OPTIONS_API_PASSPHRASE= # OKX_OPTIONS_API_PASSPHRASE=
OKX_OPTIONS_ACCOUNT_LABEL=账户·期权 OKX_OPTIONS_ACCOUNT_LABEL=账户·期权
# 固定币本位(USDT买币桥);历史 OKX_OPTIONS_MARGIN_MODE=usdc 会被忽略并视为 coin # 仅币本位期权(USDT买币桥);USDC 期权已下线,此项恒视为 coin
OKX_OPTIONS_MARGIN_MODE=coin OKX_OPTIONS_MARGIN_MODE=coin
OKX_OPTIONS_COIN_COMPOUND=true OKX_OPTIONS_COIN_COMPOUND=true
OKX_OPTIONS_COIN_BUDGET_USDT=10 OKX_OPTIONS_COIN_BUDGET_USDT=10
+6 -22
View File
@@ -5117,23 +5117,9 @@ def force_close_before_reset():
conn.commit() conn.commit()
conn.close() conn.close()
# 后台线程 # 后台线程(永续已下线:不再跑划转/强平/关键位/委托监控;期权监控见 options_monitor_loop)
def background_task(): def background_task():
while True: while True:
try:
auto_transfer_once_per_day()
# force_close 自带连接,勿套在另一未关闭连接上
force_close_before_reset()
conn = get_db()
try:
reconcile_external_closes(conn)
conn.commit()
finally:
conn.close()
check_key_monitors()
check_order_monitors()
except Exception as e:
print(f"[monitor_loop] {e}", flush=True)
time.sleep(MONITOR_POLL_SECONDS) time.sleep(MONITOR_POLL_SECONDS)
@@ -5637,16 +5623,14 @@ def index():
@app.route("/key_monitor") @app.route("/key_monitor")
@login_required @login_required
def key_monitor_page(): def key_monitor_page():
redir = redirect_to_embed_shell_if_enabled("key_monitor") # 永续关键位监控已下线
if redir is not None: return redirect("/options")
return redir
return render_main_page("key_monitor")
@app.route("/trade") @app.route("/trade")
@login_required @login_required
def trade_page(): def trade_page():
# 实盘下单界面已移除;永续下单仍由对冲计划走 place_exchange_order # 永续实盘下单已下线
return redirect("/options") return redirect("/options")
@@ -6303,7 +6287,7 @@ def order_focus():
@login_required @login_required
def add_order(): def add_order():
# 实盘下单界面已移除;永续开平仓请走对冲计划(place_exchange_order / close_exchange_order) # 实盘下单界面已移除;永续开平仓请走对冲计划(place_exchange_order / close_exchange_order)
flash("实盘下单界面已移除,请使用对冲计划或期权页") flash("永续实盘下单已下线,请使用期权页")
return redirect("/options") return redirect("/options")
@@ -7679,5 +7663,5 @@ if __name__ == "__main__":
from lib.common.flask_access_log_lib import silence_werkzeug_access_log from lib.common.flask_access_log_lib import silence_werkzeug_access_log
silence_werkzeug_access_log() silence_werkzeug_access_log()
threading.Thread(target=background_task, daemon=True).start() # 永续 background_task 已下线;期权监控由 install_options_trading 启动
app.run(host=HOST, port=PORT, debug=DEBUG, threaded=True) app.run(host=HOST, port=PORT, debug=DEBUG, threaded=True)
+8 -24
View File
@@ -415,41 +415,25 @@
function syncOptionsSectionVisibility(body) { function syncOptionsSectionVisibility(body) {
if (!body) return; if (!body) return;
var margin = (envFieldValue(body, "OKX_OPTIONS_MARGIN_MODE", "coin") || "coin").toLowerCase(); // 仅币本位期权:USDC/永续相关 env 始终隐藏
var isCoin = margin !== "usdc"; var isCoin = true;
var gateMode = (envFieldValue(body, "OKX_OPTIONS_CLOSE_GATE_MODE", "premium") || "premium").toLowerCase(); 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 coinCompoundOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COIN_COMPOUND", "true"));
var coinMaxOn = envTruthy(envFieldValue(body, "OKX_OPTIONS_COIN_MAX_USDT_ENABLED", "false")); 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) { COIN_ONLY_ENV_KEYS.forEach(function (key) {
setEnvRowHidden(envFieldRowByKey(body, key), !isCoin); setEnvRowHidden(envFieldRowByKey(body, key), !isCoin);
}); });
USDC_ONLY_ENV_KEYS.forEach(function (key) { USDC_ONLY_ENV_KEYS.forEach(function (key) {
setEnvRowHidden(envFieldRowByKey(body, key), isCoin); setEnvRowHidden(envFieldRowByKey(body, key), true);
}); });
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_MARGIN_MODE"), true);
setEnvRowHidden(envFieldRowByKey(body, "OKX_SHOW_PERP_FUNDS"), true);
// 门控子项:按模式显隐(仍受本位过滤) setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN"), gateMode !== "premium");
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"); setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT"), gateMode !== "net_pnl");
setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COIN_BUDGET_USDT"), coinCompoundOn);
if (!isCoin) { setEnvRowHidden(envFieldRowByKey(body, "OKX_OPTIONS_COIN_MAX_USDT"), !coinMaxOn);
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) { function bindOptionsSectionVisibility(body) {
+13 -2
View File
@@ -313,16 +313,27 @@ def _field_current(fields: list[dict[str, Any]], key: str, default: str = "") ->
def _mark_options_section_hidden(fields: list[dict[str, Any]]) -> list[dict[str, Any]]: def _mark_options_section_hidden(fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""按门控模式/复利/单笔上限标记 hidden.""" """按门控模式/复利/单笔上限标记 hidden;USDC/永续相关项始终隐藏."""
gate_mode = (_field_current(fields, "OKX_OPTIONS_CLOSE_GATE_MODE", "premium") or "premium").lower() gate_mode = (_field_current(fields, "OKX_OPTIONS_CLOSE_GATE_MODE", "premium") or "premium").lower()
coin_compound_on = _env_truthy(_field_current(fields, "OKX_OPTIONS_COIN_COMPOUND", "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")) coin_max_on = _env_truthy(_field_current(fields, "OKX_OPTIONS_COIN_MAX_USDT_ENABLED", "false"))
retired = {
"OKX_OPTIONS_MARGIN_MODE",
"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",
"OKX_SHOW_PERP_FUNDS",
}
out: list[dict[str, Any]] = [] out: list[dict[str, Any]] = []
for f in fields: for f in fields:
key = str(f.get("key") or "") key = str(f.get("key") or "")
hide = False hide = False
if key == "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN" and gate_mode != "premium": if key in retired:
hide = True
elif key == "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN" and gate_mode != "premium":
hide = True hide = True
elif key == "OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT" and gate_mode != "net_pnl": elif key == "OKX_OPTIONS_CLOSE_NET_PNL_MIN_USDT" and gate_mode != "net_pnl":
hide = True hide = True
+1 -1
View File
@@ -814,7 +814,7 @@ def build_option_chain(
family = inst_family_for_underlying(u, margin_mode=margin_mode) family = inst_family_for_underlying(u, margin_mode=margin_mode)
except Exception: except Exception:
family = f"{u}-USD_UM" family = f"{u}-USD"
uly = f"{u}-USD" uly = f"{u}-USD"
idx = index_px if index_px is not None else fetch_index_price(ex, uly) idx = index_px if index_px is not None else fetch_index_price(ex, uly)
now_ms = time.time() * 1000 now_ms = time.time() * 1000
+6 -9
View File
@@ -10,8 +10,8 @@ DISPLAY_RUNTIME_PREFIX = "display."
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = { DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
"show_nav_dashboard": False, "show_nav_dashboard": False,
"show_nav_account_ledger": False, "show_nav_account_ledger": False,
"show_nav_key_monitor": True, "show_nav_key_monitor": False, # 永续关键位已下线
"show_nav_trade": False, # 实盘下单界面已移除 "show_nav_trade": False, # 永续实盘下单已下线
"show_nav_strategy": False, "show_nav_strategy": False,
"show_nav_strategy_records": False, "show_nav_strategy_records": False,
"show_nav_records": False, # 永续交易记录与复盘已移除 "show_nav_records": False, # 永续交易记录与复盘已移除
@@ -22,10 +22,10 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
"show_nav_env_config": True, "show_nav_env_config": True,
"show_nav_options": True, "show_nav_options": True,
"show_nav_options_review": True, "show_nav_options_review": True,
"show_settings_transfer": False, # 永续资金划转默认隐藏 "show_settings_transfer": False, # 永续资金划转已下线
"show_settings_export": True, "show_settings_export": True,
"show_settings_password": True, "show_settings_password": True,
"show_settings_options_swap": True, "show_settings_options_swap": False, # USDC 币种兑换已下线
"show_settings_options_transfer": True, "show_settings_options_transfer": True,
} }
@@ -111,8 +111,8 @@ def display_prefs_template_context(get_db: Callable) -> dict[str, Any]:
def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool: def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
prefs = normalize_display_prefs(display or {}) prefs = normalize_display_prefs(display or {})
t = (tab or "").strip() t = (tab or "").strip()
if t in ("trade", "records", "stats", "hedge_plan"): if t in ("trade", "records", "stats", "hedge_plan", "key_monitor"):
return False # 实盘下单 / 交易记录复盘 / 统计分析 / 对冲计划 已移除 return False # 永续相关页面已下线
key = NAV_TAB_ALLOWED.get(t) key = NAV_TAB_ALLOWED.get(t)
if not key: if not key:
return True return True
@@ -123,7 +123,6 @@ def display_meta_for_ui() -> list[dict[str, Any]]:
nav_keys = [ nav_keys = [
"show_nav_dashboard", "show_nav_dashboard",
"show_nav_account_ledger", "show_nav_account_ledger",
"show_nav_key_monitor",
"show_nav_risk_policy", "show_nav_risk_policy",
"show_nav_live_guide", "show_nav_live_guide",
"show_nav_system_guide", "show_nav_system_guide",
@@ -132,10 +131,8 @@ def display_meta_for_ui() -> list[dict[str, Any]]:
"show_nav_options_review", "show_nav_options_review",
] ]
settings_keys = [ settings_keys = [
"show_settings_transfer",
"show_settings_export", "show_settings_export",
"show_settings_password", "show_settings_password",
"show_settings_options_swap",
"show_settings_options_transfer", "show_settings_options_transfer",
] ]
return [ return [
+1 -12
View File
@@ -127,21 +127,10 @@ def build_settings_tabs(display: dict[str, Any] | None, instance_settings: dict[
tabs.append({"key": "sim_funds", "title": "模拟资金"}) tabs.append({"key": "sim_funds", "title": "模拟资金"})
if disp.get("show_settings_password", True): if disp.get("show_settings_password", True):
tabs.append({"key": "password", "title": "账户密码"}) tabs.append({"key": "password", "title": "账户密码"})
if inst.get("show_transfer") and disp.get("show_settings_transfer", False): # 永续划转 / USDC 币种兑换已下线
tabs.append({"key": "transfer", "title": "永续划转"})
if disp.get("show_settings_export", True): if disp.get("show_settings_export", True):
tabs.append({"key": "export", "title": "数据导出"}) tabs.append({"key": "export", "title": "数据导出"})
tabs.append({"key": "backup", "title": "备份恢复"}) tabs.append({"key": "backup", "title": "备份恢复"})
if inst.get("options_settings_enabled") and disp.get("show_settings_options_swap", True):
_coin = False
try:
from lib.options.options_margin_mode_lib import is_coin_margin_mode
_coin = bool(is_coin_margin_mode())
except Exception:
_coin = False
if not _coin:
tabs.append({"key": "options_swap", "title": "币种兑换"})
if inst.get("options_settings_enabled") and disp.get("show_settings_options_transfer", True): if inst.get("options_settings_enabled") and disp.get("show_settings_options_transfer", True):
tabs.append({"key": "options_transfer", "title": "期权划转"}) tabs.append({"key": "options_transfer", "title": "期权划转"})
return tabs return tabs
+11 -17
View File
@@ -642,29 +642,23 @@ def _try_open_full(
inst_id: str, inst_id: str,
order: dict[str, Any], order: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
from lib.options.options_margin_mode_lib import is_coin_margin_mode
from lib.options.options_coin_open_lib import open_coin_option_buy_full from lib.options.options_coin_open_lib import open_coin_option_buy_full
note = f"中控条件单:{order.get('hub_order_id')}" note = f"中控条件单:{order.get('hub_order_id')}"
target_index = _safe_float(order.get("target_index")) target_index = _safe_float(order.get("target_index"))
pe_en = bool(order.get("profit_exit_enabled")) pe_en = bool(order.get("profit_exit_enabled"))
pe_mult = float(order.get("profit_exit_mult") or 1.0) pe_mult = float(order.get("profit_exit_mult") or 1.0)
if is_coin_margin_mode(): # USDC 期权开仓已下线,条件单仅走币本位
return open_coin_option_buy_full( return open_coin_option_buy_full(
cfg, cfg,
ex, ex,
inst_id=inst_id, inst_id=inst_id,
signal_note=note, signal_note=note,
target_index=target_index, target_index=target_index,
profit_exit_enabled=pe_en, profit_exit_enabled=pe_en,
profit_exit_mult=pe_mult, profit_exit_mult=pe_mult,
target_sheets=None, target_sheets=None,
) )
return {
"ok": False,
"msg": "条件单开仓当前仅支持币本位客户端规则(买币→开仓)",
"need_depth": False,
}
def run_cond_orders_tick( def run_cond_orders_tick(
+7 -7
View File
@@ -35,11 +35,10 @@ def is_coin_margin_mode(raw: Any = None) -> bool:
def inst_family_for_underlying(underlying: str, *, margin_mode: str | None = None) -> str: def inst_family_for_underlying(underlying: str, *, margin_mode: str | None = None) -> str:
"""仅币本位期权族 {U}-USD;USD_UM 已下线."""
del margin_mode
u = (underlying or "ETH").strip().upper() or "ETH" u = (underlying or "ETH").strip().upper() or "ETH"
mode = normalize_options_margin_mode(margin_mode) return f"{u}-USD"
if mode == MODE_COIN:
return f"{u}-USD"
return f"{u}-USD_UM"
def margin_mode_from_inst_id(inst_id: str) -> str: def margin_mode_from_inst_id(inst_id: str) -> str:
@@ -55,9 +54,10 @@ def margin_mode_from_inst_id(inst_id: str) -> str:
def premium_ccy_for_mode(margin_mode: str, underlying: str = "ETH") -> str: def premium_ccy_for_mode(margin_mode: str, underlying: str = "ETH") -> str:
if normalize_options_margin_mode(margin_mode) == MODE_COIN: """币本位权利金为标的币;历史 USDC 行(margin_mode=usdc)仍标 USDC."""
return (underlying or "ETH").strip().upper() or "ETH" if str(margin_mode or "").strip().lower() == MODE_USDC:
return "USDC" return "USDC"
return (underlying or "ETH").strip().upper() or "ETH"
def spot_quote_inst_id(underlying: str) -> str: def spot_quote_inst_id(underlying: str) -> str:
+11 -617
View File
@@ -11,18 +11,7 @@ from jinja2 import ChoiceLoader, FileSystemLoader
from lib.options.options_db import init_options_tables, sum_open_premium_paid, sum_open_sheets from lib.options.options_db import init_options_tables, sum_open_premium_paid, sum_open_sheets
from lib.options.options_monitor_lib import options_monitor_loop from lib.options.options_monitor_lib import options_monitor_loop
from lib.options.options_pricing_lib import ( from lib.exchange.okx_options_lib import _safe_float
calc_order_size,
ct_mult_from_meta,
min_sz_from_meta,
premium_per_sheet,
)
from lib.exchange.okx_options_lib import (
_safe_float,
cap_option_buy_sheets_to_ask_depth,
option_buy_liquidity_ok,
td_mode_for_option_buy,
)
def _env_bool(key: str, default: bool = False) -> bool: def _env_bool(key: str, default: bool = False) -> bool:
@@ -253,84 +242,19 @@ def _run_manual_bid_close(
return result return result
def _budget_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]:
"""打满可用额度 = min(交易户可用 USDC, 单笔预算);calc_order_size 再乘 budget_buffer."""
from lib.exchange.okx_options_lib import fetch_options_trading_usdc
from lib.options.options_pricing_lib import resolve_budget_full_usdc
raw = fetch_options_trading_usdc(ex)
if raw is None or float(raw) <= 0:
return None, "交易账户 USDC 可用余额不足"
trading = float(raw)
cap = _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", float(cfg.get("trade_budget") or 10.0))
if cap <= 0:
return None, "单笔预算无效(OKX_OPTIONS_TRADE_BUDGET_USDC)"
return resolve_budget_full_usdc(trading, float(cap)), ""
def _compound_full_enabled() -> bool: def _compound_full_enabled() -> bool:
return _env_bool("OKX_OPTIONS_COMPOUND_FULL_ENABLED", True) """USDC 全仓复利已下线;恒为 False,避免前端残留 compound_full 模式."""
return False
def _budget_full_blocked_by_compound_msg() -> str | None:
if _compound_full_enabled():
return "全仓复利已开启,不可使用单笔预算/打满;请关闭全仓复利或改用全仓复利模式"
return None
def _size_mode_budget_cap(
cfg: dict[str, Any], mode: str, budget_cap: float | None
) -> float | None:
"""全仓复利开启时禁用单笔预算封顶(sheets/eth 也不再受 trade_budget 限制)."""
if mode in ("budget_full", "compound_full"):
return budget_cap
if mode in ("sheets", "eth_amount"):
if _compound_full_enabled():
return None
return budget_cap
return None
def _normalize_size_mode(mode: str) -> tuple[str, str | None]: def _normalize_size_mode(mode: str) -> tuple[str, str | None]:
"""全仓复利关闭时强制离开 compound_full,避免前端残留选中导致无法开仓.""" """USDC 预算/全仓模式已下线;统一落到指定张数."""
m = (mode or "sheets").strip() or "sheets" m = (mode or "sheets").strip() or "sheets"
if m == "compound_full" and not _compound_full_enabled(): if m in ("compound_full", "budget_full"):
return "sheets", "全仓复利已关闭,已改用指定张数" return "sheets", "USDC 期权模式已下线,已改用指定张数"
if m == "budget_full" and _compound_full_enabled():
return "compound_full", None
return m, None return m, None
def _compound_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]:
"""全仓复利 = 期权交易户可用(可选上限封顶);再由 calc_order_size × budget_buffer."""
if not _compound_full_enabled():
return None, "全仓复利未开启(OKX_OPTIONS_COMPOUND_FULL_ENABLED)"
from lib.exchange.okx_options_lib import fetch_options_trading_usdc
from lib.options.options_pricing_lib import resolve_compound_full_usdc
raw = fetch_options_trading_usdc(ex)
if raw is None or float(raw) <= 0:
return None, "交易账户 USDC 可用余额不足"
trading = float(raw)
# 额度热更读 env(与模板启动值无关)
cap_on = _env_bool("OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", False)
cap_v = _env_float("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC", 300.0)
if cap_on and cap_v <= 0:
return None, "全仓上限无效(OKX_OPTIONS_COMPOUND_FULL_CAP_USDC)"
return (
resolve_compound_full_usdc(
trading,
cap_enabled=cap_on,
cap_usdc=cap_v,
),
"",
)
def _is_budget_mode(mode: str) -> bool:
return mode in ("budget_full", "compound_full")
def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None: def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None:
conn = cfg["get_db"]() conn = cfg["get_db"]()
try: try:
@@ -822,228 +746,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
) )
except Exception as e: except Exception as e:
return jsonify({"ok": False, "msg": f"币本位报价失败: {e}"}) return jsonify({"ok": False, "msg": f"币本位报价失败: {e}"})
return jsonify({"ok": False, "msg": "仅支持币本位期权报价", "can_open": False})
budget = cfg["trade_budget"]
budget_cap = cfg["trade_budget"]
available_usdc = None
if mode == "budget_full":
blocked = _budget_full_blocked_by_compound_msg()
if blocked:
return jsonify(
{
"ok": False,
"msg": blocked,
"compound_full_enabled": _compound_full_enabled(),
}
)
budget, budget_err = _budget_full_usdc(cfg, ex)
if budget is None:
return jsonify({"ok": False, "msg": budget_err, "compound_full_enabled": _compound_full_enabled()})
budget_cap = budget
from lib.exchange.okx_options_lib import fetch_options_trading_usdc
available_usdc = fetch_options_trading_usdc(ex)
elif mode == "compound_full":
if not _compound_full_enabled():
return jsonify(
{
"ok": False,
"msg": "全仓复利未开启(OKX_OPTIONS_COMPOUND_FULL_ENABLED)",
"compound_full_enabled": False,
}
)
budget, budget_err = _compound_full_usdc(cfg, ex)
if budget is None:
return jsonify({"ok": False, "msg": budget_err, "compound_full_enabled": True})
budget_cap = budget
from lib.exchange.okx_options_lib import fetch_options_trading_usdc
available_usdc = fetch_options_trading_usdc(ex)
elif mode in ("sheets", "eth_amount") and _compound_full_enabled():
budget_cap = None
eth_amount = None
try:
if request.args.get("eth_amount"):
eth_amount = float(request.args.get("eth_amount"))
except (TypeError, ValueError):
pass
ask = q.get("ask")
ask_sz = q.get("ask_sz")
try:
from lib.options.okx_trade_mode_lib import block_standalone_open_by_mode_msg
mode_block = block_standalone_open_by_mode_msg()
if mode_block:
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": mode_block,
"quote_per_unit": ask,
"premium_per_sheet": None,
"sizing": {
"ok": False,
"msg": mode_block,
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
"available_usdc": available_usdc,
"budget_full_usdc": budget if mode == "budget_full" else None,
"compound_full_usdc": budget if mode == "compound_full" else None,
}
)
except Exception as e:
return jsonify(
{
"ok": False,
"can_open": False,
"msg": f"交易模式校验失败: {e}",
}
)
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
if not can_open:
# 合约可报价,但不可开仓:返回参考标记价供展示
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入",
"quote_per_unit": None,
"premium_per_sheet": None,
"sizing": {
"ok": False,
"msg": block_msg or "暂无卖一深度,无法买入",
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
"available_usdc": available_usdc,
"budget_full_usdc": budget if mode == "budget_full" else None,
"compound_full_usdc": budget if mode == "compound_full" else None,
}
)
from lib.options.options_position_limit_lib import (
compound_full_single_position_block_msg,
option_position_limit_block_msg,
)
if mode == "compound_full":
compound_block = compound_full_single_position_block_msg(
ex, fetch_positions=cfg.get("fetch_option_positions")
)
if compound_block:
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": compound_block,
"quote_per_unit": ask,
"premium_per_sheet": None,
"sizing": {
"ok": False,
"msg": compound_block,
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
"available_usdc": available_usdc,
"budget_full_usdc": None,
"compound_full_usdc": budget,
}
)
pos_limit_msg = option_position_limit_block_msg(
ex,
opening_inst_id=inst_id,
fetch_positions=cfg.get("fetch_option_positions"),
)
if pos_limit_msg:
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": pos_limit_msg,
"quote_per_unit": ask,
"premium_per_sheet": None,
"sizing": {
"ok": False,
"msg": pos_limit_msg,
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
"available_usdc": available_usdc,
"budget_full_usdc": budget if mode == "budget_full" else None,
"compound_full_usdc": budget if mode == "compound_full" else None,
}
)
sizing = calc_order_size(
quote_per_unit=float(ask),
ct_mult=float(ct_mult),
min_sz=int(min_sz),
budget_usdc=budget if _is_budget_mode(mode) else None,
budget_buffer=cfg["budget_buffer"],
eth_amount=eth_amount if mode == "eth_amount" else None,
sheets=sheet_count if mode == "sheets" else None,
budget_cap=_size_mode_budget_cap(cfg, mode, budget_cap)
if mode in ("budget_full", "compound_full", "sheets", "eth_amount")
else None,
)
if sizing.get("ok"):
capped, cap_msg = cap_option_buy_sheets_to_ask_depth(
int(sizing.get("sheets") or 0),
ask_sz,
min_sz=int(min_sz),
)
if capped is None:
sizing = {
"ok": False,
"msg": cap_msg,
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
}
elif capped < int(sizing.get("sheets") or 0):
sizing = calc_order_size(
quote_per_unit=float(ask),
ct_mult=float(ct_mult),
min_sz=int(min_sz),
sheets=capped,
budget_cap=_size_mode_budget_cap(cfg, mode, budget_cap)
if mode in ("budget_full", "compound_full", "sheets", "eth_amount")
else None,
)
if sizing.get("ok"):
sizing["ask_depth_capped"] = True
sizing["ask_sz"] = ask_sz
sizing["msg"] = f"已按卖一深度限制为 {capped} 张"
q = _attach_close_preview(
cfg,
ex,
q,
sheets=int(sizing.get("sheets") or sheet_count or 0),
premium_paid=_open_premium_paid(cfg, inst_id),
)
return jsonify(
{
**q,
"can_open": True,
"quote_per_unit": ask,
"premium_per_sheet": premium_per_sheet(float(ask), float(ct_mult)),
"sizing": sizing,
"available_usdc": available_usdc,
"budget_full_usdc": budget if mode == "budget_full" else None,
"compound_full_usdc": budget if mode == "compound_full" else None,
"mode": mode,
"mode_note": mode_note,
"compound_full_enabled": _compound_full_enabled(),
}
)
@app.route("/api/options/open", methods=["POST"]) @app.route("/api/options/open", methods=["POST"])
@lr @lr
@@ -1141,272 +844,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
return jsonify(result) return jsonify(result)
except Exception as e: except Exception as e:
return jsonify({"ok": False, "msg": f"币本位开仓失败: {e}"}) return jsonify({"ok": False, "msg": f"币本位开仓失败: {e}"})
q = cfg["quote_option_contract"](ex, inst_id) return jsonify({"ok": False, "msg": "仅支持币本位期权开仓"})
if not q.get("ok"):
return jsonify(q)
ask = q.get("ask")
ask_sz = q.get("ask_sz")
can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
if not can_open:
return jsonify(
{
"ok": False,
"msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入",
"can_open": False,
"mark": q.get("mark"),
"ref_ask": q.get("ref_ask"),
}
)
from lib.options.options_position_limit_lib import (
compound_full_single_position_block_msg,
option_position_limit_block_msg,
)
if mode == "compound_full":
compound_block = compound_full_single_position_block_msg(
ex, fetch_positions=cfg.get("fetch_option_positions")
)
if compound_block:
return jsonify({"ok": False, "msg": compound_block, "can_open": False})
pos_limit_msg = option_position_limit_block_msg(
ex,
opening_inst_id=inst_id,
fetch_positions=cfg.get("fetch_option_positions"),
)
if pos_limit_msg:
return jsonify({"ok": False, "msg": pos_limit_msg, "can_open": False})
ct_mult = float(q.get("ct_mult") or 0.01)
min_sz = int(q.get("min_sz") or 1)
eth_amount = None
sheet_count = None
if mode == "eth_amount":
try:
eth_amount = float(data.get("eth_amount"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "ETH 数量无效"})
elif mode == "sheets":
try:
sheet_count = int(data.get("sheets"))
except (TypeError, ValueError):
sheet_count = None
if sheet_count is None or int(sheet_count) < 1:
# 全仓复利关闭后前端可能仍带着旧 mode 过来,归一后缺张数则默认 1
if (data.get("mode") or "").strip() == "compound_full":
sheet_count = 1
else:
return jsonify({"ok": False, "msg": "张数无效"})
budget = cfg["trade_budget"]
budget_cap = cfg["trade_budget"]
if mode == "budget_full":
blocked = _budget_full_blocked_by_compound_msg()
if blocked:
return jsonify({"ok": False, "msg": blocked, "compound_full_enabled": _compound_full_enabled()})
budget, budget_err = _budget_full_usdc(cfg, ex)
if budget is None:
return jsonify({"ok": False, "msg": budget_err})
budget_cap = budget
elif mode == "compound_full":
if not _compound_full_enabled():
return jsonify(
{
"ok": False,
"msg": "全仓复利未开启,请改用指定张数或先开启全仓复利",
"compound_full_enabled": False,
}
)
budget, budget_err = _compound_full_usdc(cfg, ex)
if budget is None:
return jsonify({"ok": False, "msg": budget_err})
budget_cap = budget
elif mode in ("sheets", "eth_amount") and _compound_full_enabled():
budget_cap = None
sizing = calc_order_size(
quote_per_unit=float(ask),
ct_mult=ct_mult,
min_sz=min_sz,
budget_usdc=budget if _is_budget_mode(mode) else None,
budget_buffer=cfg["budget_buffer"],
eth_amount=eth_amount,
sheets=sheet_count,
budget_cap=_size_mode_budget_cap(cfg, mode, budget_cap)
if mode in ("budget_full", "compound_full", "sheets", "eth_amount")
else None,
)
if not sizing.get("ok"):
return jsonify({"ok": False, "msg": sizing.get("msg") or "张数计算失败", "sizing": sizing})
sheets = int(sizing["sheets"])
capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=min_sz)
if capped is None:
return jsonify({"ok": False, "msg": cap_msg or "卖一深度不足,无法买入"})
if capped < sheets:
return jsonify(
{
"ok": False,
"msg": f"卖一深度仅 {int(capped)} 张,不足请求 {int(sheets)} 张,拒绝缩量成交",
"requested_sheets": int(sheets),
"ask_sz": ask_sz,
}
)
tick_sz = q.get("tick_sz")
order = cfg["place_option_limit_order"](
ex,
inst_id=inst_id,
side="buy",
sheets=sheets,
price=float(ask),
td_mode=td_mode_for_option_buy(cfg["td_mode"]),
tick_sz=tick_sz,
ord_type="ioc",
)
if not order.get("ok"):
return jsonify(order)
ord_id = str((order.get("data") or {}).get("ordId") or "").strip()
if not ord_id:
return jsonify({"ok": False, "msg": "下单成功但未返回订单号", "order": order})
from lib.exchange.okx_options_lib import wait_option_order_full_fill
try:
fill_timeout = max(2.0, float(os.getenv("OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC") or "12"))
except (TypeError, ValueError):
fill_timeout = 12.0
fill = wait_option_order_full_fill(
ex,
inst_id=inst_id,
ord_id=ord_id,
need_sheets=int(sheets),
timeout_sec=fill_timeout,
cancel_on_timeout=True,
)
if not fill.get("ok"):
filled_n = int(fill.get("filled_sheets") or 0)
orphan_close = None
if filled_n > 0:
try:
from lib.options.options_close_exec_lib import close_option_by_bid1
orphan_close = close_option_by_bid1(
cfg, ex, inst_id, sheets=filled_n, require_recycle_gate=False
)
except Exception as e:
orphan_close = {"ok": False, "msg": str(e)}
return jsonify(
{
"ok": False,
"msg": fill.get("msg") or "未完全成交,开仓失败",
"filled_sheets": filled_n,
"orphan_close": orphan_close,
"fill": fill,
"order": order,
}
)
fill_px = float(fill.get("avg_px") or ask)
filled_n = int(fill.get("filled_sheets") or sheets)
sheets = filled_n
sizing = dict(sizing)
sizing["sheets"] = sheets
sizing["eth_amount"] = round(sheets * ct_mult, 8)
from lib.options.options_pricing_lib import PREMIUM_DECIMALS_COIN, round_premium_amount
sizing["total_premium"] = round_premium_amount(
fill_px * sheets * ct_mult, decimals=PREMIUM_DECIMALS_COIN
)
conn = cfg["get_db"]()
trade_id = None
target_mon = None
open_underlying = ""
open_opt_type = None
try:
init_options_tables(conn)
from lib.options.options_profit_exit_lib import ensure_profit_exit_columns
ensure_profit_exit_columns(conn)
meta = q.get("meta") or {}
u = str(meta.get("uly") or inst_id).split("-")[0]
opt_type = meta.get("optType")
open_underlying = u
open_opt_type = opt_type
from lib.options.options_time_lib import options_app_now_str
cur = conn.execute(
"""
INSERT INTO options_trades
(inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount,
open_quote, premium_paid, status, signal_note, exchange_ord_id,
profit_exit_enabled, profit_exit_mult, profit_exit_state, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?)
""",
(
inst_id,
u,
opt_type,
q.get("strike"),
str(q.get("exp_time") or ""),
sheets,
sizing["eth_amount"],
fill_px,
sizing["total_premium"],
signal_note,
ord_id,
1 if profit_exit_enabled else 0,
profit_exit_mult if profit_exit_enabled else 1.0,
"active" if profit_exit_enabled else "idle",
options_app_now_str(),
),
)
trade_id = int(cur.lastrowid)
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,
underlying=u,
opt_type=str(opt_type) if opt_type else None,
trade_id=trade_id,
sheets=sheets,
)
if profit_exit_enabled:
pass # 列已由 init_options_tables / ensure 迁移
conn.commit()
finally:
conn.close()
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
from lib.options.options_notify_lib import notify_options_open
invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True)
try:
conn_n = cfg["get_db"]()
try:
notify_options_open(
cfg,
conn_n,
trade_id=trade_id,
inst_id=inst_id,
underlying=open_underlying,
opt_type=open_opt_type,
sheets=sheets,
premium_paid=sizing.get("total_premium"),
open_quote=fill_px,
target_index=target_index,
signal_note=signal_note,
premium_ccy=sizing.get("premium_ccy") or "USDC",
)
finally:
conn_n.close()
except Exception:
pass
return jsonify(
{
"ok": True,
"order": order,
"sizing": sizing,
"trade_id": trade_id,
"target_monitor": target_mon,
}
)
@app.route("/api/options/orders/pending") @app.route("/api/options/orders/pending")
@lr @lr
@@ -1762,44 +1200,12 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
@app.route("/api/options/convert/quote", methods=["POST"]) @app.route("/api/options/convert/quote", methods=["POST"])
@lr @lr
def api_options_convert_quote(): def api_options_convert_quote():
ex, err = _require_options_ex(cfg) return jsonify({"ok": False, "msg": "USDC 兑换已下线,仅支持币本位期权"})
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
try:
amount = float(data.get("amount"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "数量无效"})
return jsonify(cfg["estimate_usdt_to_usdc"](ex, amount))
@app.route("/api/options/convert/execute", methods=["POST"]) @app.route("/api/options/convert/execute", methods=["POST"])
@lr @lr
def api_options_convert_execute(): def api_options_convert_execute():
ex, err = _require_options_ex(cfg) return jsonify({"ok": False, "msg": "USDC 兑换已下线,仅支持币本位期权"})
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
quote_id = (data.get("quote_id") or "").strip()
result = cfg["execute_convert"](ex, quote_id)
if result.get("ok"):
conn = cfg["get_db"]()
try:
init_options_tables(conn)
conn.execute(
"""
INSERT INTO options_convert_log (from_ccy, to_ccy, rfq_sz, received_sz, quote_id, status, message)
VALUES ('USDT', 'USDC', ?, ?, ?, 'ok', '')
""",
(
data.get("rfq_sz"),
(result.get("data") or {}).get("baseSz"),
quote_id,
),
)
conn.commit()
finally:
conn.close()
return jsonify(result)
@app.route("/api/options/transfer", methods=["POST"]) @app.route("/api/options/transfer", methods=["POST"])
@lr @lr
@@ -1847,19 +1253,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
@app.route("/api/options/spot/swap", methods=["POST"]) @app.route("/api/options/spot/swap", methods=["POST"])
@lr @lr
def api_options_spot_swap(): def api_options_spot_swap():
ex, err = _require_options_ex(cfg) return jsonify({"ok": False, "msg": "USDT↔USDC 兑换已下线,仅支持币本位期权"})
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
direction = (data.get("direction") or "usdt_to_usdc").strip()
try:
amount = float(data.get("amount"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "数量无效"})
result = cfg["spot_market_swap_usdt_usdc"](ex, direction=direction, amount=amount)
if result.get("ok"):
_mark_balances_stale(cfg)
return jsonify(result)
@app.route("/api/options/history") @app.route("/api/options/history")
@lr @lr