"""OKX 期权模块:Flask 路由注册."""
from __future__ import annotations
import os
import threading
import time
from typing import Any
from flask import Flask, jsonify, redirect, request, url_for
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_monitor_lib import options_monitor_loop
from lib.exchange.okx_options_lib import _safe_float
def _env_bool(key: str, default: bool = False) -> bool:
raw = (os.getenv(key) or "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "yes", "on")
def _env_float(key: str, default: float) -> float:
try:
return float(os.getenv(key) or default)
except (TypeError, ValueError):
return float(default)
def attach_options_templates(app: Flask, repo_root: str) -> None:
tpl_dir = os.path.join(repo_root, "lib", "options", "templates")
if not os.path.isdir(tpl_dir):
return
existing = app.jinja_loader
loaders = [FileSystemLoader(tpl_dir)]
if existing is not None:
if isinstance(existing, ChoiceLoader):
loaders = list(existing.loaders) + loaders
else:
loaders.insert(0, existing)
app.jinja_loader = ChoiceLoader(loaders)
def install_options_trading(app: Flask, repo_root: str, app_module: Any) -> None:
enabled = _env_bool("OKX_OPTIONS_ENABLED", False)
attach_options_templates(app, repo_root)
cfg = _build_cfg(app_module)
try:
from lib.sim.hooks import patch_options_cfg
patch_options_cfg(cfg)
except Exception:
pass
try:
from lib.options.options_notify_lib import bind_options_notify_cfg
bind_options_notify_cfg(cfg)
except Exception:
pass
app.extensions["options_cfg"] = cfg
register_options_routes(app, cfg)
_register_options_hub_bridge(app, cfg)
if enabled:
_start_monitor_thread(app, cfg)
def _register_options_hub_bridge(app: Flask, cfg: dict[str, Any]) -> None:
from lib.options.options_hub_lib import build_options_hub_snapshot
def snapshot_fn():
return build_options_hub_snapshot(cfg)
def close_fn(inst_id: str) -> dict[str, Any]:
# 中控总览只全平(不改张数);无效买一在执行层拒绝
return _run_manual_bid_close(cfg, inst_id, sheets=None, reason="中控平仓")
hub_ctx = dict(app.config.get("HUB_CTX") or {})
hub_ctx["options_snapshot_fn"] = snapshot_fn
hub_ctx["options_close_fn"] = close_fn
hub_ctx["options_cfg"] = cfg
app.config["HUB_CTX"] = hub_ctx
def _build_cfg(app_module: Any) -> dict[str, Any]:
from lib.exchange.okx_options_lib import (
build_option_chain,
estimate_usdt_to_usdc,
execute_convert,
fetch_option_book_depth,
fetch_option_positions,
fetch_options_balances,
format_position_row,
options_api_ready,
cancel_option_order,
fetch_option_pending_orders,
place_option_limit_order,
place_option_market_order,
quote_option_contract,
spot_market_swap_usdt_usdc,
transfer_ccy,
)
return {
"enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
"get_db": app_module.get_db,
"login_required": app_module.login_required,
"exchange_options": getattr(app_module, "exchange_options", None),
"send_wechat": app_module.send_wechat_msg,
"render_main_page": app_module.render_main_page,
"trade_budget": _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", 10.0),
"budget_buffer": _env_float("OKX_OPTIONS_BUDGET_BUFFER", 0.95),
"compound_full_enabled": _env_bool("OKX_OPTIONS_COMPOUND_FULL_ENABLED", True),
"compound_full_cap_enabled": _env_bool("OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", False),
"compound_full_cap_usdc": _env_float("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC", 300.0),
"margin_mode": (os.getenv("OKX_OPTIONS_MARGIN_MODE") or "coin").strip().lower(),
"default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
"max_dte_days": _env_float("OKX_OPTIONS_MAX_DTE_DAYS", 2.0),
"chain_max_dte_days": _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", 14.0),
"chain_ask_liq_filter": _env_bool("OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED", True),
"itm_max_dist": _env_float("OKX_OPTIONS_ITM_MAX_DIST_USD", 30.0),
"td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "isolated").strip(),
# 市价平仓已硬关闭(忽略 env),仅买一限价
"allow_market_close": False,
# 平仓限价挂单超时自动撤单(秒);默认 600=10 分钟,联调可设 60
"pending_ttl_seconds": _env_float("OKX_OPTIONS_PENDING_TTL_SECONDS", 600.0),
"profit_ratio": _env_float("OKX_OPTIONS_PROFIT_ALERT_RATIO", 1.0),
"poll_seconds": _env_float("OKX_OPTIONS_POLL_SECONDS", 15.0),
"account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "OKX期权").strip(),
"build_option_chain": build_option_chain,
"quote_option_contract": quote_option_contract,
"fetch_option_book_depth": fetch_option_book_depth,
"place_option_limit_order": place_option_limit_order,
"place_option_market_order": place_option_market_order,
"fetch_option_pending_orders": fetch_option_pending_orders,
"cancel_option_order": cancel_option_order,
"fetch_option_positions": fetch_option_positions,
"fetch_options_balances": fetch_options_balances,
"format_position_row": format_position_row,
"estimate_usdt_to_usdc": estimate_usdt_to_usdc,
"execute_convert": execute_convert,
"transfer_ccy": transfer_ccy,
"spot_market_swap_usdt_usdc": spot_market_swap_usdt_usdc,
"options_api_ready": options_api_ready,
"app_module": app_module,
}
def _mark_balances_stale(cfg: dict[str, Any]) -> None:
from lib.exchange.okx_options_lib import invalidate_options_balance_cache
from lib.instance.instance_live_push_lib import notify_instance_balance_changed
invalidate_options_balance_cache()
app_mod = cfg.get("app_module")
if app_mod is not None and hasattr(app_mod, "invalidate_account_balance_cache"):
app_mod.invalidate_account_balance_cache()
try:
notify_instance_balance_changed()
except Exception:
pass
def _require_options_ex(cfg: dict[str, Any]):
if not cfg.get("enabled"):
return None, "期权模块未启用,请在 .env 设置 OKX_OPTIONS_ENABLED=true 并重启 PM2"
ex = cfg.get("exchange_options")
ok, reason = cfg["options_api_ready"](ex)
if not ok:
return None, reason or "期权 API 未配置"
return ex, ""
def _run_manual_bid_close(
cfg: dict[str, Any],
inst_id: str,
*,
sheets: int | None = None,
reason: str = "手动平仓",
) -> dict[str, Any]:
"""手动买一限价平仓(全平或指定张数);残档/无效买一由 close_option_by_bid1 拒绝."""
ex, err = _require_options_ex(cfg)
if ex is None:
return {"ok": False, "msg": err}
inst_id = (inst_id or "").strip()
if not inst_id:
return {"ok": False, "msg": "缺少 inst_id"}
from lib.options.options_close_exec_lib import close_option_by_bid1
result = close_option_by_bid1(
cfg,
ex,
inst_id,
sheets=sheets,
require_recycle_gate=False,
)
if not result.get("ok"):
return result
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True)
try:
from lib.options.options_notify_lib import notify_options_close
from lib.options.options_target_lib import cancel_target_monitor
conn2 = cfg["get_db"]()
try:
if result.get("fully_closed"):
cancel_target_monitor(conn2, inst_id=inst_id)
conn2.commit()
note = reason if result.get("fully_closed") else f"{reason}(部分)"
notify_options_close(
cfg,
conn2,
inst_id=inst_id,
reason=note,
sheets=result.get("submitted_sheets"),
premium_received=result.get("premium_received"),
close_quote=result.get("locked_bid_px") or result.get("bid"),
)
finally:
conn2.close()
except Exception:
pass
if result.get("fully_closed"):
try:
from lib.options.options_coin_open_lib import maybe_sell_spot_after_close
spot_sell = maybe_sell_spot_after_close(cfg, ex, inst_id=inst_id, close_result=result)
if spot_sell is not None:
result = dict(result)
result["spot_sell"] = spot_sell
if spot_sell.get("bridge_status") == "pending_sell_spot":
result["msg"] = (
str(result.get("msg") or "平仓成功")
+ ";但卖回 USDT 失败,请点「重试卖回」"
)
except Exception as e:
result = dict(result)
result["spot_sell"] = {"ok": False, "msg": str(e)}
_mark_balances_stale(cfg)
return result
def _compound_full_enabled() -> bool:
"""USDC 全仓复利已下线;恒为 False,避免前端残留 compound_full 模式."""
return False
def _normalize_size_mode(mode: str) -> tuple[str, str | None]:
"""USDC 预算/全仓模式已下线;统一落到指定张数."""
m = (mode or "sheets").strip() or "sheets"
if m in ("compound_full", "budget_full"):
return "sheets", "USDC 期权模式已下线,已改用指定张数"
return m, None
def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None:
conn = cfg["get_db"]()
try:
init_options_tables(conn)
return sum_open_premium_paid(conn, inst_id)
finally:
conn.close()
def _position_avail_sheets(pos: dict[str, Any]) -> int:
avail = _safe_float(pos.get("availPos"))
if avail is None or avail <= 0:
avail = abs(_safe_float(pos.get("pos")) or 0)
return max(0, int(avail or 0))
def _find_position(rows: list[dict[str, Any]] | None, inst_id: str) -> dict[str, Any] | None:
return next((p for p in rows or [] if str(p.get("instId")) == inst_id), None)
def _refresh_position_avail(cfg: dict[str, Any], ex: Any, inst_id: str) -> int | None:
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
invalidate_option_positions_cache()
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return None
pos = _find_position(raw, inst_id)
if not pos:
return 0
return _position_avail_sheets(pos)
def _enrich_position_row_display(
cfg: dict[str, Any],
ex: Any,
raw_pos: dict[str, Any],
*,
meta_cache: dict[str, dict[str, Any] | None] | None = None,
premium_override: float | None = None,
) -> dict[str, Any]:
from lib.options.options_history_lib import enrich_position_row_display
return enrich_position_row_display(
cfg,
ex,
raw_pos,
meta_cache=meta_cache,
premium_override=premium_override,
)
def _attach_close_preview(
cfg: dict[str, Any],
ex: Any,
row: dict[str, Any],
*,
sheets: int | None = None,
premium_paid: float | None = None,
) -> dict[str, Any]:
from lib.options.options_positions_lib import attach_close_preview
return attach_close_preview(
cfg,
ex,
row,
sheets=sheets,
premium_paid=premium_paid,
)
_OPTIONS_SYNC_LOCK = threading.Lock()
_OPTIONS_SYNC_LAST_AT = 0.0
_OPTIONS_SYNC_INTERVAL_SEC = 15.0
def _sync_options_trades(
cfg: dict[str, Any],
*,
raw_positions: list[dict[str, Any]] | None = None,
force: bool = False,
) -> None:
global _OPTIONS_SYNC_LAST_AT
ex = cfg.get("exchange_options")
if ex is None:
return
now = time.time()
with _OPTIONS_SYNC_LOCK:
if not force and now - _OPTIONS_SYNC_LAST_AT < _OPTIONS_SYNC_INTERVAL_SEC:
return
_OPTIONS_SYNC_LAST_AT = now
from lib.exchange.okx_options_lib import fetch_all_option_positions_history, fetch_option_position_history
from lib.options.options_monitor_lib import (
backfill_closed_options_realized_pnl_from_history,
reconcile_live_open_trades,
sync_open_options_trades,
)
if raw_positions is None:
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return
else:
raw = raw_positions
live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")}
# 网络拉取历史必须在 DB 事务外,否则握锁期间卖币/中控快照会 database is locked
pending_insts: list[str] = []
conn = cfg["get_db"]()
try:
init_options_tables(conn)
rows = conn.execute(
"SELECT inst_id FROM options_trades WHERE status = 'open'"
).fetchall()
for r in rows:
iid = str(r["inst_id"] or "").strip()
if iid and iid not in live_ids:
pending_insts.append(iid)
try:
conn.commit()
except Exception:
pass
finally:
conn.close()
hist_by_inst: dict[str, list] = {}
for iid in pending_insts:
try:
hist_by_inst[iid] = fetch_option_position_history(ex, iid) or []
except Exception:
hist_by_inst[iid] = []
hist_all = None
try:
hist_all = fetch_all_option_positions_history(ex, limit=200)
except Exception:
hist_all = None
conn = cfg["get_db"]()
try:
init_options_tables(conn)
reconcile_live_open_trades(conn, live_inst_ids=live_ids)
sync_open_options_trades(
conn,
live_inst_ids=live_ids,
fetch_history_fn=lambda inst_id: hist_by_inst.get(str(inst_id) or [], []),
notify_cfg=cfg,
)
if hist_all is not None:
try:
backfill_closed_options_realized_pnl_from_history(conn, hist_all)
except Exception:
pass
conn.commit()
except Exception:
try:
conn.rollback()
except Exception:
pass
finally:
try:
conn.close()
except Exception:
pass
try:
from lib.options.options_monitor_lib import sell_spot_after_options_sync
sell_spot_after_options_sync(cfg)
_mark_balances_stale(cfg)
except Exception:
pass
def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
lr = cfg["login_required"]
@app.route("/options/guide")
@lr
def options_trade_guide():
"""期权开平仓与监控说明(独立页)."""
from pathlib import Path
from flask import render_template_string
from lib.common.markdown_html_lib import render_markdown_html
md_path = Path(__file__).resolve().parents[2] / "docs" / "期权开平仓与监控说明.md"
try:
md_text = md_path.read_text(encoding="utf-8")
except OSError:
md_text = "# 说明文档缺失\n\n未找到 `docs/期权开平仓与监控说明.md`."
body = render_markdown_html(md_text)
return render_template_string(
"""
期权开平仓与监控说明
← 返回期权
{{ body|safe }}
""",
body=body,
)
@app.route("/api/options/balances")
@lr
def api_options_balances():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
force = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes")
bal = cfg["fetch_options_balances"](ex, force=force, scope="main")
from lib.options.options_margin_mode_lib import is_coin_margin_mode, normalize_options_margin_mode
margin_mode = normalize_options_margin_mode()
payload = {
"ok": True,
**bal,
"trade_budget": _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", float(cfg.get("trade_budget") or 10)),
"compound_full_enabled": _compound_full_enabled(),
"compound_full_cap_enabled": _env_bool("OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED", False),
"compound_full_cap_usdc": _env_float("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC", 300.0),
"options_margin_mode": margin_mode,
"options_margin_mode_label": "币本位" if margin_mode == "coin" else "USDC",
}
if is_coin_margin_mode():
try:
from lib.options.options_coin_open_lib import coin_budget_preview
payload["coin_budget"] = coin_budget_preview(cfg, ex)
except Exception as e:
payload["coin_budget"] = {"ok": False, "msg": str(e)}
return jsonify(payload)
@app.route("/api/options/chain")
@lr
def api_options_chain():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
u = (request.args.get("underlying") or cfg["default_underly"]).upper()
# 热更新:链展示天数每次读 env,保存后刷新链即可
chain_max_dte = _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", float(cfg.get("chain_max_dte_days") or 14))
try:
from lib.options.options_margin_mode_lib import normalize_options_margin_mode
margin_mode = normalize_options_margin_mode()
chain = cfg["build_option_chain"](
ex,
u,
max_dte_days=chain_max_dte,
itm_only=False,
itm_max_dist_usd=cfg["itm_max_dist"],
margin_mode=margin_mode,
)
except Exception as e:
return jsonify({"ok": False, "msg": f"加载期权链失败: {e}"})
expiries = chain.get("expiries") or []
chain_err = chain.get("chain_error")
# 热更新:每次读 env,保存配置后刷新链即可生效
ask_liq_filter = _env_bool("OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED", True)
budget_buffer = _env_float("OKX_OPTIONS_BUDGET_BUFFER", 0.95)
coin_budget = None
try:
from lib.options.options_margin_mode_lib import is_coin_margin_mode
from lib.options.options_coin_open_lib import coin_budget_preview
if is_coin_margin_mode():
coin_budget = coin_budget_preview(cfg, ex)
except Exception:
coin_budget = None
if not expiries:
return jsonify(
{
"ok": False,
"msg": chain_err or "暂无到期日,请稍后点「刷新链」",
**chain,
"chain_max_dte_days": chain_max_dte,
"ask_liq_filter_enabled": ask_liq_filter,
"budget_buffer": budget_buffer,
"trade_budget": cfg["trade_budget"],
"options_margin_mode": chain.get("margin_mode") or margin_mode,
"coin_budget": coin_budget,
}
)
return jsonify(
{
"ok": True,
**chain,
"chain_max_dte_days": chain_max_dte,
"ask_liq_filter_enabled": ask_liq_filter,
"budget_buffer": budget_buffer,
"trade_budget": cfg["trade_budget"],
"options_margin_mode": chain.get("margin_mode") or margin_mode,
"coin_budget": coin_budget,
}
)
@app.route("/api/options/quote")
@lr
def api_options_quote():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
inst_id = (request.args.get("inst_id") or "").strip()
if not inst_id:
return jsonify({"ok": False, "msg": "缺少 inst_id"})
q = cfg["quote_option_contract"](ex, inst_id)
if not q.get("ok"):
return jsonify(q)
ask = q.get("ask")
ct_mult = q.get("ct_mult") or 0.01
min_sz = q.get("min_sz") or 1
mode = (request.args.get("mode") or "sheets").strip()
sheet_count = None
try:
if request.args.get("sheets"):
sheet_count = int(request.args.get("sheets"))
except (TypeError, ValueError):
pass
if mode == "close_preview":
paid = _open_premium_paid(cfg, inst_id)
target = sheet_count if sheet_count is not None else 0
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
row_mode = margin_mode_from_inst_id(inst_id)
prem_ccy = premium_ccy_for_mode(row_mode, (inst_id.split("-")[0] if inst_id else "ETH"))
preview_row = {
**q,
"pos": target,
"premium_paid": paid,
"margin_mode": row_mode,
"premium_ccy": prem_ccy,
}
out = _attach_close_preview(cfg, ex, preview_row, sheets=target, premium_paid=paid)
out["options_margin_mode"] = row_mode
out["premium_ccy"] = prem_ccy
return jsonify(out)
mode, mode_note = _normalize_size_mode(mode)
# 币本位:报价预览走 USDT 预算→估币→张数,禁止再查 USDC
try:
from lib.options.options_margin_mode_lib import (
is_coin_margin_mode,
margin_mode_from_inst_id,
)
from lib.options.options_coin_open_lib import coin_budget_preview
from lib.exchange.okx_options_lib import option_buy_liquidity_ok
if is_coin_margin_mode():
ask = q.get("ask")
ask_sz = q.get("ask_sz")
can_open, block_msg = option_buy_liquidity_ok(ask, 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()
except Exception as e:
return jsonify({"ok": False, "can_open": False, "msg": f"交易模式校验失败: {e}"})
if mode_block:
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": mode_block,
"options_margin_mode": "coin",
"sizing": {"ok": False, "msg": mode_block, "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0},
}
)
if margin_mode_from_inst_id(inst_id) != "coin":
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": "当前为币本位模式,请选择 ETH-USD / BTC-USD 合约(非 USD_UM)",
"options_margin_mode": "coin",
"sizing": {
"ok": False,
"msg": "合约非币本位",
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
}
)
budget_info = coin_budget_preview(cfg, ex)
if not can_open:
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入",
"options_margin_mode": "coin",
"coin_budget": budget_info,
"sizing": {
"ok": False,
"msg": block_msg or "暂无卖一深度,无法买入",
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
}
)
if not budget_info.get("ok"):
return jsonify(
{
**q,
"ok": True,
"can_open": False,
"msg": budget_info.get("msg") or "交易账户 USDT 不足",
"options_margin_mode": "coin",
"coin_budget": budget_info,
"sizing": {
"ok": False,
"msg": budget_info.get("msg") or "交易账户 USDT 不足",
"sheets": 0,
"eth_amount": 0.0,
"total_premium": 0.0,
},
}
)
idx = _safe_float(q.get("index_px")) or _safe_float(q.get("idxPx"))
budget_usdt = float(budget_info["budget_usdt"])
target_sheets = sheet_count if mode == "sheets" and sheet_count is not None else None
if mode == "eth" and request.args.get("eth"):
# 指定币量:按币量反推张数后再走统一规划
try:
eth_want = float(request.args.get("eth"))
except (TypeError, ValueError):
eth_want = 0.0
if eth_want > 0 and float(ct_mult) > 0:
import math
target_sheets = max(int(min_sz), int(math.floor(eth_want / float(ct_mult) + 1e-12)))
from lib.options.options_margin_mode_lib import plan_coin_open_by_budget
sizing = plan_coin_open_by_budget(
quote_per_unit=float(ask),
ct_mult=float(ct_mult),
min_sz=int(min_sz),
budget_usdt=budget_usdt,
index_px=float(idx or 0),
ask_sz=ask_sz,
target_sheets=target_sheets,
)
if sizing.get("ok"):
sizing["premium_ccy"] = (inst_id.split("-")[0] if inst_id else "ETH").upper()
sizing["est_coin"] = sizing.get("buy_coin")
q = _attach_close_preview(
cfg,
ex,
q,
sheets=int(sizing.get("sheets") or 0),
premium_paid=_open_premium_paid(cfg, inst_id),
)
return jsonify(
{
**q,
"can_open": bool(sizing.get("ok")),
"quote_per_unit": ask,
"premium_per_sheet": round(float(ask) * float(ct_mult), 8),
"sizing": sizing,
"mode": mode,
"mode_note": mode_note,
"options_margin_mode": "coin",
"coin_budget": budget_info,
"compound_full_enabled": _compound_full_enabled(),
}
)
except Exception as e:
return jsonify({"ok": False, "msg": f"币本位报价失败: {e}"})
return jsonify({"ok": False, "msg": "仅支持币本位期权报价", "can_open": False})
@app.route("/api/options/open", methods=["POST"])
@lr
def api_options_open():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
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({"ok": False, "msg": mode_block, "can_open": False})
except Exception as e:
return jsonify({"ok": False, "msg": f"交易模式校验失败: {e}", "can_open": False})
data = request.get_json(silent=True) or {}
inst_id = (data.get("inst_id") or "").strip()
mode = (data.get("mode") or "sheets").strip()
mode, mode_note = _normalize_size_mode(mode)
signal_note = (data.get("signal_note") or "").strip()
# 中控条件单占用时禁止手动开仓
try:
from lib.options.options_cond_order_lib import cond_order_blocks_manual_open_msg
from lib.options.options_db import init_options_tables
conn_blk = cfg["get_db"]()
try:
init_options_tables(conn_blk)
block = cond_order_blocks_manual_open_msg(conn_blk)
if block:
return jsonify({"ok": False, "msg": block, "cond_order_locked": True})
finally:
conn_blk.close()
except Exception:
pass
if mode_note and mode == "sheets" and (data.get("mode") or "").strip() == "compound_full":
# 前端残留全仓复利选中时,已自动改指定张数;继续开仓
pass
target_index = None
raw_target = data.get("target_index")
if raw_target is not None and str(raw_target).strip() != "":
try:
target_index = float(raw_target)
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "目标位无效"})
if target_index <= 0:
return jsonify({"ok": False, "msg": "目标位无效"})
profit_exit_enabled = bool(data.get("profit_exit_enabled"))
profit_exit_mult = 1.0
if profit_exit_enabled:
from lib.options.options_profit_exit_lib import normalize_profit_exit_mult
profit_exit_mult = normalize_profit_exit_mult(data.get("profit_exit_mult"), default=1.0)
if not inst_id:
return jsonify({"ok": False, "msg": "缺少 inst_id"})
try:
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
if is_coin_margin_mode():
want_sheets = None
if mode == "sheets":
try:
want_sheets = int(data.get("sheets") or 0) or None
except (TypeError, ValueError):
want_sheets = None
elif mode == "eth":
try:
eth_want = float(data.get("eth") or 0)
except (TypeError, ValueError):
eth_want = 0.0
if eth_want > 0:
q0 = cfg["quote_option_contract"](ex, inst_id)
ct0 = float((q0 or {}).get("ct_mult") or 0.01)
min0 = int((q0 or {}).get("min_sz") or 1)
if ct0 > 0:
import math
want_sheets = max(min0, int(math.floor(eth_want / ct0 + 1e-12)))
result = open_coin_option_buy_full(
cfg,
ex,
inst_id=inst_id,
signal_note=signal_note,
target_index=target_index,
profit_exit_enabled=profit_exit_enabled,
profit_exit_mult=profit_exit_mult,
target_sheets=want_sheets,
)
if result.get("ok"):
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
invalidate_option_positions_cache()
_mark_balances_stale(cfg)
return jsonify(result)
except Exception as e:
return jsonify({"ok": False, "msg": f"币本位开仓失败: {e}"})
return jsonify({"ok": False, "msg": "仅支持币本位期权开仓"})
@app.route("/api/options/orders/pending")
@lr
def api_options_orders_pending():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
inst_id = (request.args.get("inst_id") or "").strip() or None
try:
orders = cfg["fetch_option_pending_orders"](ex, inst_id)
except Exception as e:
return jsonify({"ok": False, "msg": f"获取委托失败: {e}"})
from lib.options.options_pending_lib import enrich_pending_orders
ttl = float(cfg.get("pending_ttl_seconds") or 600.0)
enriched = enrich_pending_orders(orders, ttl_seconds=ttl)
return jsonify(
{
"ok": True,
"orders": enriched,
"count": len(enriched),
"pending_ttl_seconds": ttl,
}
)
@app.route("/api/options/orders/cancel", methods=["POST"])
@lr
def api_options_orders_cancel():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
inst_id = (data.get("inst_id") or "").strip()
ord_id = (data.get("ord_id") or "").strip()
if not inst_id or not ord_id:
return jsonify({"ok": False, "msg": "缺少 inst_id 或 ord_id"})
out = cfg["cancel_option_order"](ex, inst_id=inst_id, ord_id=ord_id)
if out.get("ok"):
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
invalidate_option_positions_cache()
# 本地未成交开仓记录标记取消,避免假 open
try:
conn = cfg["get_db"]()
try:
from lib.options.options_time_lib import options_app_now_str
init_options_tables(conn)
conn.execute(
"""
UPDATE options_trades
SET status = 'cancelled',
signal_note = CASE
WHEN signal_note IS NULL OR TRIM(signal_note) = '' THEN '委托撤销'
ELSE signal_note
END,
closed_at = ?
WHERE inst_id = ? AND exchange_ord_id = ? AND status = 'open'
""",
(options_app_now_str(), inst_id, ord_id),
)
conn.commit()
finally:
conn.close()
except Exception:
pass
_sync_options_trades(cfg, force=True)
return jsonify(out), (200 if out.get("ok") else 400)
@app.route("/api/options/positions")
@lr
def api_options_positions():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
_sync_options_trades(cfg, raw_positions=raw)
meta_cache: dict[str, dict[str, Any] | None] = {}
conn = cfg["get_db"]()
try:
from lib.options.options_target_lib import targets_by_inst
from lib.options.options_profit_exit_lib import profit_exit_by_inst
tgt_map = targets_by_inst(conn)
profit_exit_map = profit_exit_by_inst(conn)
rows = []
for p in raw:
inst = str(p.get("instId") or "").strip()
premium_override = sum_open_premium_paid(conn, inst) if inst else None
row = _enrich_position_row_display(
cfg,
ex,
p,
meta_cache=meta_cache,
premium_override=premium_override,
)
_attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
mon = tgt_map.get(inst)
if mon:
row["target_index"] = mon.get("target_index")
row["target_monitor_id"] = mon.get("id")
row["target_monitor"] = mon
pe = profit_exit_map.get(inst)
if pe:
row["profit_exit_enabled"] = pe.get("profit_exit_enabled")
row["profit_exit_mult"] = pe.get("profit_exit_mult")
row["profit_exit_state"] = pe.get("profit_exit_state")
row["profit_exit_required_recycle"] = pe.get("required_recycle")
try:
from lib.instance.instance_dashboard_lib import _resolve_options_source
source_key, source_label, source_plan_id = _resolve_options_source(conn, inst)
row["source"] = source_key
row["source_label"] = source_label
row["source_plan_id"] = source_plan_id
except Exception:
row.setdefault("source", "option")
row.setdefault("source_label", "纯期权")
row.setdefault("source_plan_id", None)
rows.append(row)
finally:
conn.close()
spot_sell_hint = None
try:
from lib.options.options_margin_mode_lib import is_coin_margin_mode
if is_coin_margin_mode():
from lib.options.options_spot_bridge_lib import list_open_bridges
conn_b = cfg["get_db"]()
try:
from lib.options.options_db import init_options_tables
init_options_tables(conn_b)
open_bridges = list_open_bridges(conn_b)
bal = cfg["fetch_options_balances"](ex, scope="main")
bridge = open_bridges[0] if open_bridges else None
from lib.options.options_spot_bridge_lib import (
display_sellable_coin,
spot_min_sell_sz,
)
raw_eth = bal.get("trading_eth_avail") or bal.get("trading_eth")
raw_btc = bal.get("trading_btc_avail") or bal.get("trading_btc")
spot_sell_hint = {
"bridge_status": str(bridge.get("status") or "") if bridge else None,
"bridge_underlying": str(bridge.get("underlying") or "").upper() if bridge else None,
# 粉尘归零:不够现货最小可卖则前端不展示、不提示重试
"trading_eth": display_sellable_coin(ex, "ETH", raw_eth),
"trading_btc": display_sellable_coin(ex, "BTC", raw_btc),
"min_sz_eth": spot_min_sell_sz(ex, "ETH"),
"min_sz_btc": spot_min_sell_sz(ex, "BTC"),
"default_underly": (cfg.get("default_underly") or "ETH").strip().upper(),
}
finally:
conn_b.close()
except Exception:
spot_sell_hint = None
cond_locked = False
cond_msg = ""
cond_order = None
try:
from lib.options.options_cond_order_lib import has_active_cond_order, list_cond_orders
conn_c = cfg["get_db"]()
try:
init_options_tables(conn_c)
cond_locked = has_active_cond_order(conn_c)
if cond_locked:
act = list_cond_orders(conn_c, active_only=True)
if act:
cond_order = act[0]
cond_msg = str(act[0].get("message") or act[0].get("status") or "")
finally:
conn_c.close()
except Exception:
pass
return jsonify(
{
"ok": True,
"positions": rows,
"spot_sell": spot_sell_hint,
"cond_order_locked": cond_locked,
"cond_order_msg": cond_msg,
"cond_order": cond_order,
}
)
@app.route("/api/options/targets")
@lr
def api_options_targets():
conn = cfg["get_db"]()
try:
from lib.options.options_target_lib import list_active_targets, list_closing_targets
return jsonify({"ok": True, "targets": list_active_targets(conn) + list_closing_targets(conn)})
finally:
conn.close()
@app.route("/api/options/target", methods=["POST"])
@lr
def api_options_target_set():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
inst_id = (data.get("inst_id") or "").strip()
if not inst_id:
return jsonify({"ok": False, "msg": "缺少 inst_id"})
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": "获取期权持仓失败"})
pos = _find_position(raw, inst_id)
if not pos:
return jsonify({"ok": False, "msg": "未找到持仓"})
from lib.options.options_target_lib import upsert_target_monitor
fmt = cfg["format_position_row"](pos)
conn = cfg["get_db"]()
try:
trade = conn.execute(
"""
SELECT id, opt_type, underlying FROM options_trades
WHERE inst_id = ? AND status = 'open'
ORDER BY id DESC LIMIT 1
""",
(inst_id,),
).fetchone()
trade_id = int(trade["id"]) if trade else None
sheets_sum = sum_open_sheets(conn, inst_id)
sheets = sheets_sum if sheets_sum is not None else int(fmt.get("pos") or 0)
opt_type = (trade["opt_type"] if trade else None) or fmt.get("opt_type")
underlying = (trade["underlying"] if trade else None) or fmt.get("underlying")
out = upsert_target_monitor(
conn,
inst_id=inst_id,
target_index=target_index,
underlying=str(underlying) if underlying else None,
opt_type=str(opt_type) if opt_type else None,
trade_id=trade_id,
sheets=sheets,
)
conn.commit()
return jsonify(out)
finally:
conn.close()
@app.route("/api/options/target/cancel", methods=["POST"])
@lr
def api_options_target_cancel():
data = request.get_json(silent=True) or {}
inst_id = (data.get("inst_id") or "").strip() or None
monitor_id = data.get("id")
try:
mid = int(monitor_id) if monitor_id is not None and str(monitor_id).strip() != "" else None
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "监控 id 无效"})
if not inst_id and mid is None:
return jsonify({"ok": False, "msg": "缺少 inst_id 或 id"})
from lib.options.options_target_lib import cancel_target_monitor
conn = cfg["get_db"]()
try:
n = cancel_target_monitor(conn, inst_id=inst_id, monitor_id=mid)
conn.commit()
return jsonify({"ok": True, "cancelled": n})
finally:
conn.close()
@app.route("/api/options/profit-exit", methods=["POST"])
@lr
def api_options_profit_exit_set():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
inst_id = (data.get("inst_id") or "").strip()
if not inst_id:
return jsonify({"ok": False, "msg": "缺少 inst_id"})
enabled_raw = data.get("enabled")
if enabled_raw is None:
enabled_raw = data.get("profit_exit_enabled")
enabled = bool(enabled_raw) and str(enabled_raw).strip().lower() not in (
"0",
"false",
"off",
"no",
)
from lib.options.options_profit_exit_lib import normalize_profit_exit_mult, set_profit_exit
mult = normalize_profit_exit_mult(data.get("mult", data.get("profit_exit_mult")), default=1.0)
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
if not _find_position(raw, inst_id):
return jsonify({"ok": False, "msg": "未找到持仓"})
conn = cfg["get_db"]()
try:
out = set_profit_exit(conn, inst_id=inst_id, enabled=enabled, mult=mult)
if out.get("ok"):
conn.commit()
return jsonify(out)
finally:
conn.close()
@app.route("/api/options/close", methods=["POST"])
@lr
def api_options_close():
data = request.get_json(silent=True) or {}
inst_id = (data.get("inst_id") or "").strip()
if not inst_id:
return jsonify({"ok": False, "msg": "缺少 inst_id"})
if data.get("market"):
return jsonify({"ok": False, "msg": "已禁用市价平仓,仅支持买一限价"})
sheets = data.get("sheets")
try:
sheets_i = int(sheets) if sheets is not None and str(sheets).strip() != "" else None
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "张数无效"})
# 手动买一平仓:只验有效流动性;2×门控仅用于自动/目标位平仓
return jsonify(_run_manual_bid_close(cfg, inst_id, sheets=sheets_i, reason="手动平仓"))
@app.route("/api/options/spot-bridge/retry-sell", methods=["POST"])
@lr
def api_options_spot_bridge_retry_sell():
"""币本位:重试把残留标的币市价卖回 USDT."""
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
underlying = (data.get("underlying") or cfg.get("default_underly") or "ETH").strip().upper()
inst_id = (data.get("inst_id") or "").strip() or None
from lib.options.options_spot_bridge_lib import sell_residual_after_option_flat
out = sell_residual_after_option_flat(
cfg["get_db"],
ex,
underlying=underlying,
inst_id=inst_id,
)
if out.get("ok"):
_mark_balances_stale(cfg)
return jsonify(out)
@app.route("/api/options/convert/quote", methods=["POST"])
@lr
def api_options_convert_quote():
return jsonify({"ok": False, "msg": "USDC 兑换已下线,仅支持币本位期权"})
@app.route("/api/options/convert/execute", methods=["POST"])
@lr
def api_options_convert_execute():
return jsonify({"ok": False, "msg": "USDC 兑换已下线,仅支持币本位期权"})
@app.route("/api/options/transfer", methods=["POST"])
@lr
def api_options_transfer():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
data = request.get_json(silent=True) or {}
ccy = (data.get("ccy") or "USDC").upper()
from_acct = (data.get("from") or "funding").strip()
to_acct = (data.get("to") or "trading").strip()
try:
amount = float(data.get("amount"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "数量无效"})
try:
import lib.exchange.okx_options_lib as _opt_lib
xfer = getattr(_opt_lib, "transfer_ccy", None)
except Exception:
xfer = None
if not callable(xfer):
xfer = cfg.get("transfer_ccy")
result = xfer(ex, ccy, amount, from_acct, to_acct) if callable(xfer) else {
"ok": False,
"msg": "划转不可用",
}
if result.get("ok"):
conn = cfg["get_db"]()
try:
init_options_tables(conn)
conn.execute(
"""
INSERT INTO options_transfer_log (ccy, amount, from_account, to_account, status, message)
VALUES (?, ?, ?, ?, 'ok', '')
""",
(ccy, amount, from_acct, to_acct),
)
conn.commit()
finally:
conn.close()
_mark_balances_stale(cfg)
return jsonify(result)
@app.route("/api/options/spot/swap", methods=["POST"])
@lr
def api_options_spot_swap():
return jsonify({"ok": False, "msg": "USDT↔USDC 兑换已下线,仅支持币本位期权"})
@app.route("/api/options/history")
@lr
def api_options_history():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
from lib.options.options_history_lib import load_options_history
raw_live = cfg["fetch_option_positions"](ex)
if raw_live is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
history = load_options_history(ex, cfg)
live_ids = {str(x.get("inst_id") or "") for x in history if x.get("status") == "open"}
return jsonify({"ok": True, "history": history, "live_inst_ids": sorted(live_ids)})
@app.route("/api/options/stats")
@lr
def api_options_stats():
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
from lib.options.options_history_lib import load_options_history
from lib.options.options_margin_mode_lib import is_coin_margin_mode
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
from lib.options.options_stats_lib import (
compute_options_stats_from_history,
history_pnl_to_usdt,
)
raw_live = cfg["fetch_option_positions"](ex)
if raw_live is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
history = load_options_history(ex, cfg)
# 币本位已平盈亏按指数折 U,与持仓浮盈/合计口径一致
stats = compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
total_pnl = None
if open_float is not None:
total_pnl = round(net_realized + float(open_float), 4)
elif stats.get("total_closed"):
total_pnl = round(net_realized, 4)
# 币本位统计统一标 U;USDC 模式仍标 USDC
pnl_unit = "U" if is_coin_margin_mode() else "USDC"
return jsonify(
{
"ok": True,
**stats,
"open_float_pnl": open_float,
"total_pnl": total_pnl,
"pnl_unit": pnl_unit,
}
)
@app.route("/api/options/history/", methods=["DELETE"])
@lr
def api_options_history_delete(history_key: str):
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
key = (history_key or "").strip()
if not key:
return jsonify({"ok": False, "msg": "缺少 history_key"})
data = request.get_json(silent=True) or {}
inst_id = str(data.get("inst_id") or request.args.get("inst_id") or "").strip() or None
closed_at = str(data.get("closed_at") or request.args.get("closed_at") or "").strip() or None
conn = cfg["get_db"]()
try:
init_options_tables(conn)
conn.execute(
"INSERT OR IGNORE INTO options_history_hidden (history_key) VALUES (?)",
(key,),
)
# 同步隐藏期权复盘,避免本地已平记录刷新后又出现
try:
from lib.options.options_review_lib import hide_review_keys
hide_review_keys(
conn,
history_key=key,
inst_id=inst_id,
closed_at=closed_at,
)
if inst_id:
# 去掉已导入的复盘快照(按合约+平仓时间)
if closed_at:
rows = conn.execute(
"""
SELECT id, history_key FROM options_review_trades
WHERE inst_id = ?
AND substr(COALESCE(closed_at,''),1,16) = substr(?,1,16)
""",
(inst_id, closed_at),
).fetchall()
else:
rows = conn.execute(
"""
SELECT id, history_key FROM options_review_trades
WHERE inst_id = ?
""",
(inst_id,),
).fetchall()
for r in rows:
conn.execute(
"DELETE FROM options_review_entries WHERE trade_id=?",
(int(r["id"]),),
)
conn.execute(
"DELETE FROM options_review_trades WHERE id=?",
(int(r["id"]),),
)
conn.execute(
"INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at) VALUES (?,?,?)",
(str(r["history_key"]), inst_id, (closed_at or "")[:19] or None),
)
fps = []
if closed_at:
fps.append(f"inst_close:{inst_id}:{closed_at[:16]}")
fps.append(f"inst:{inst_id}")
for fp in fps:
conn.execute(
"INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at) VALUES (?,?,?)",
(fp, inst_id, (closed_at or "")[:19] or None),
)
except Exception:
pass
conn.commit()
finally:
conn.close()
return jsonify({"ok": True})
def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
if app.extensions.get("options_monitor_started"):
return
app.extensions["options_monitor_started"] = True
def _bid(inst_id: str) -> float | None:
ex = cfg.get("exchange_options")
if ex is None:
return None
try:
q = cfg["quote_option_contract"](ex, inst_id)
return q.get("bid")
except Exception:
return None
def _positions():
ex = cfg.get("exchange_options")
if ex is None:
return []
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return []
return [cfg["format_position_row"](p) for p in raw]
def _sync(_conn=None):
# 不用监控线程传入的连接;force=False 走 15s 节流,卖币由监控循环在关闭连接后另扫
try:
_sync_options_trades(cfg, force=False)
return 1
except Exception:
return 0
def _target_close(inst_id: str) -> dict[str, Any]:
from lib.options.options_target_lib import close_option_by_bid_depth
ex = cfg.get("exchange_options")
if ex is None:
return {"ok": False, "msg": "期权 exchange 未就绪"}
result = close_option_by_bid_depth(cfg, ex, inst_id)
if result.get("ok"):
try:
_sync_options_trades(cfg, force=True)
except Exception:
pass
try:
_mark_balances_stale(cfg)
except Exception:
pass
return result
def _profit_exit_close(inst_id: str) -> dict[str, Any]:
from lib.options.options_profit_exit_lib import close_option_by_bid_profit_exit
ex = cfg.get("exchange_options")
if ex is None:
return {"ok": False, "msg": "期权 exchange 未就绪"}
result = close_option_by_bid_profit_exit(cfg, ex, inst_id)
if result.get("ok"):
try:
_sync_options_trades(cfg, force=True)
except Exception:
pass
try:
_mark_balances_stale(cfg)
except Exception:
pass
return result
def _stale_pending() -> dict[str, Any]:
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
from lib.options.options_pending_lib import cancel_stale_close_pending_orders
ex = cfg.get("exchange_options")
if ex is None:
return {"ok": False, "msg": "期权 exchange 未就绪"}
ttl = float(cfg.get("pending_ttl_seconds") or 600.0)
out = cancel_stale_close_pending_orders(
fetch_pending=lambda _ex: cfg["fetch_option_pending_orders"](_ex),
cancel_order=lambda _ex, inst_id, ord_id: cfg["cancel_option_order"](
_ex, inst_id=inst_id, ord_id=ord_id
),
ttl_seconds=ttl,
ex=ex,
)
if out.get("cancelled"):
try:
invalidate_option_positions_cache()
except Exception:
pass
try:
send = cfg.get("send_wechat")
if callable(send):
parts = [
"【OKX期权·挂单超时撤销】",
f"账户:{cfg.get('account_label') or 'OKX期权'}",
f"超时:{ttl:g}s",
f"撤销:{out.get('cancelled')} 笔",
]
for o in out.get("orders") or []:
parts.append(f"- {o.get('inst_id')} #{o.get('ord_id')}")
send("\n".join(parts))
except Exception:
pass
return out
def _cond_order_tick(_conn=None) -> None:
from lib.options.options_cond_order_lib import run_cond_orders_tick
# 传 get_db:条件单开仓前会释放连接,避免嵌套锁库
run_cond_orders_tick(
cfg["get_db"],
cfg,
send_wechat=cfg.get("send_wechat"),
account_label=str(cfg.get("account_label") or ""),
)
t = threading.Thread(
target=options_monitor_loop,
kwargs={
"enabled": True,
"poll_seconds": cfg["poll_seconds"],
"get_db": cfg["get_db"],
"fetch_positions": _positions,
"ticker_bid_fn": _bid,
"send_wechat": cfg["send_wechat"],
"account_label": cfg["account_label"],
"profit_ratio": cfg["profit_ratio"],
"sync_trades_fn": _sync,
"target_close_fn": _target_close,
"profit_exit_close_fn": _profit_exit_close,
"profit_exit_cfg": cfg,
"stale_pending_fn": _stale_pending,
"cond_order_tick_fn": _cond_order_tick,
},
daemon=True,
name="options-monitor",
)
t.start()