cd23ea74a6
Co-authored-by: Cursor <cursoragent@cursor.com>
1741 lines
69 KiB
Python
1741 lines
69 KiB
Python
"""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.options.options_pricing_lib import (
|
||
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:
|
||
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, str(default)))
|
||
except (TypeError, ValueError):
|
||
return 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)
|
||
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)
|
||
|
||
hub_ctx = dict(app.config.get("HUB_CTX") or {})
|
||
hub_ctx["options_snapshot_fn"] = snapshot_fn
|
||
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),
|
||
"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 _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:
|
||
return _env_bool("OKX_OPTIONS_COMPOUND_FULL_ENABLED", True)
|
||
|
||
|
||
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 _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:
|
||
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 "")}
|
||
|
||
def _hist(inst_id: str):
|
||
return fetch_option_position_history(ex, inst_id)
|
||
|
||
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=_hist)
|
||
try:
|
||
hist_all = fetch_all_option_positions_history(ex, limit=200)
|
||
backfill_closed_options_realized_pnl_from_history(conn, hist_all)
|
||
except Exception:
|
||
pass
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
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.hub.hub_strategy_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(
|
||
"""
|
||
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||
<title>期权开平仓与监控说明</title>
|
||
<style>
|
||
:root { color-scheme: light dark; }
|
||
body { font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
max-width: 860px; margin: 24px auto; padding: 0 16px 48px; line-height: 1.65; }
|
||
h1,h2,h3 { line-height: 1.3; }
|
||
code, pre { font-family: ui-monospace, Consolas, monospace; }
|
||
pre { overflow: auto; padding: 12px; border-radius: 8px; background: rgba(127,127,127,.12); }
|
||
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
||
th, td { border: 1px solid rgba(127,127,127,.35); padding: 8px 10px; text-align: left; }
|
||
a { color: #0b6bcb; }
|
||
.top { margin-bottom: 16px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<p class="top"><a href="/options">← 返回期权</a> · <a href="/hedge_plan">对冲计划</a></p>
|
||
{{ body|safe }}
|
||
</body>
|
||
</html>
|
||
""",
|
||
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")
|
||
return jsonify({"ok": True, **bal, "trade_budget": cfg["trade_budget"]})
|
||
|
||
@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:
|
||
chain = cfg["build_option_chain"](
|
||
ex,
|
||
u,
|
||
max_dte_days=chain_max_dte,
|
||
itm_only=False,
|
||
itm_max_dist_usd=cfg["itm_max_dist"],
|
||
)
|
||
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)
|
||
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"],
|
||
}
|
||
)
|
||
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"],
|
||
}
|
||
)
|
||
|
||
@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 "budget_full").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
|
||
return jsonify(_attach_close_preview(cfg, ex, {**q, "pos": target, "premium_paid": paid}, sheets=target, premium_paid=paid))
|
||
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})
|
||
budget, budget_err = _budget_full_usdc(cfg, ex)
|
||
if budget is None:
|
||
return jsonify({"ok": False, "msg": budget_err})
|
||
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)"})
|
||
budget, budget_err = _compound_full_usdc(cfg, ex)
|
||
if budget is None:
|
||
return jsonify({"ok": False, "msg": budget_err})
|
||
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.hedge_plan.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}",
|
||
}
|
||
)
|
||
try:
|
||
from lib.hedge_plan.hedge_options_exclusive_lib import block_standalone_option_open_msg
|
||
|
||
conn_q = cfg["get_db"]()
|
||
try:
|
||
excl = block_standalone_option_open_msg(conn_q)
|
||
finally:
|
||
conn_q.close()
|
||
if excl:
|
||
return jsonify(
|
||
{
|
||
**q,
|
||
"ok": True,
|
||
"can_open": False,
|
||
"msg": excl,
|
||
"quote_per_unit": ask,
|
||
"premium_per_sheet": None,
|
||
"sizing": {
|
||
"ok": False,
|
||
"msg": excl,
|
||
"sheets": 0,
|
||
"eth_amount": 0.0,
|
||
"total_premium": 0.0,
|
||
},
|
||
"available_usdc": available_usdc,
|
||
"budget_full_usdc": budget if mode == "budget_full" else None,
|
||
"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,
|
||
}
|
||
)
|
||
|
||
@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.hedge_plan.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})
|
||
try:
|
||
from lib.hedge_plan.hedge_options_exclusive_lib import block_standalone_option_open_msg
|
||
|
||
conn_gate = cfg["get_db"]()
|
||
try:
|
||
block_msg = block_standalone_option_open_msg(conn_gate)
|
||
finally:
|
||
conn_gate.close()
|
||
if block_msg:
|
||
return jsonify({"ok": False, "msg": block_msg})
|
||
except Exception as e:
|
||
return jsonify({"ok": False, "msg": f"互斥校验失败: {e}"})
|
||
data = request.get_json(silent=True) or {}
|
||
inst_id = (data.get("inst_id") or "").strip()
|
||
mode = (data.get("mode") or "budget_full").strip()
|
||
signal_note = (data.get("signal_note") or "").strip()
|
||
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"})
|
||
q = cfg["quote_option_contract"](ex, inst_id)
|
||
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):
|
||
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})
|
||
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": "全仓复利未开启(OKX_OPTIONS_COMPOUND_FULL_ENABLED)"})
|
||
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)
|
||
sizing["total_premium"] = round(fill_px * sheets * ct_mult, 4)
|
||
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
|
||
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)
|
||
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",
|
||
),
|
||
)
|
||
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,
|
||
)
|
||
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")
|
||
@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:
|
||
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 = CURRENT_TIMESTAMP
|
||
WHERE inst_id = ? AND exchange_ord_id = ? AND status = 'open'
|
||
""",
|
||
(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
|
||
from lib.hedge_plan.hedge_plan_db import active_options_targets_by_inst
|
||
|
||
tgt_map = targets_by_inst(conn)
|
||
profit_exit_map = profit_exit_by_inst(conn)
|
||
hedge_target_map = active_options_targets_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")
|
||
hedge_target = hedge_target_map.get(inst)
|
||
if hedge_target:
|
||
row["hedge_plan_target"] = hedge_target
|
||
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()
|
||
return jsonify({"ok": True, "positions": rows})
|
||
|
||
@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:
|
||
from lib.hedge_plan.hedge_plan_db import (
|
||
active_hedge_option_inst_ids,
|
||
init_hedge_plan_tables,
|
||
)
|
||
|
||
conn_h = cfg["get_db"]()
|
||
try:
|
||
init_hedge_plan_tables(conn_h)
|
||
if inst_id in active_hedge_option_inst_ids(conn_h):
|
||
return jsonify(
|
||
{
|
||
"ok": False,
|
||
"msg": "该合约属于进行中的对冲计划,请在对冲计划中管理,禁止在期权页设置目标",
|
||
}
|
||
)
|
||
finally:
|
||
conn_h.close()
|
||
except Exception as e:
|
||
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
|
||
try:
|
||
target_index = float(data.get("target_index"))
|
||
except (TypeError, ValueError):
|
||
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"})
|
||
try:
|
||
from lib.hedge_plan.hedge_plan_db import (
|
||
active_hedge_option_inst_ids,
|
||
init_hedge_plan_tables,
|
||
)
|
||
|
||
conn_h = cfg["get_db"]()
|
||
try:
|
||
init_hedge_plan_tables(conn_h)
|
||
if inst_id in active_hedge_option_inst_ids(conn_h):
|
||
return jsonify(
|
||
{
|
||
"ok": False,
|
||
"msg": "该合约属于进行中的对冲计划,请在对冲计划中管理,禁止在期权页设置翻倍出场",
|
||
}
|
||
)
|
||
finally:
|
||
conn_h.close()
|
||
except Exception as e:
|
||
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
|
||
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():
|
||
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:
|
||
from lib.hedge_plan.hedge_plan_db import (
|
||
active_hedge_option_inst_ids,
|
||
init_hedge_plan_tables,
|
||
)
|
||
|
||
conn_h = cfg["get_db"]()
|
||
try:
|
||
init_hedge_plan_tables(conn_h)
|
||
if inst_id in active_hedge_option_inst_ids(conn_h):
|
||
return jsonify(
|
||
{
|
||
"ok": False,
|
||
"msg": "该合约属于进行中的对冲计划,请在对冲计划中管理,禁止在期权页平仓",
|
||
}
|
||
)
|
||
finally:
|
||
conn_h.close()
|
||
except Exception as e:
|
||
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
|
||
if data.get("market"):
|
||
return jsonify({"ok": False, "msg": "已禁用市价平仓,仅支持买一限价"})
|
||
sheets = data.get("sheets")
|
||
try:
|
||
sheets_i = int(sheets) if sheets is not None and str(sheets).strip() != "" else None
|
||
except (TypeError, ValueError):
|
||
return jsonify({"ok": False, "msg": "张数无效"})
|
||
from lib.options.options_close_exec_lib import close_option_by_bid1
|
||
|
||
# 手动买一平仓:只验有效流动性;2×门控仅用于自动/目标位平仓
|
||
result = close_option_by_bid1(
|
||
cfg,
|
||
ex,
|
||
inst_id,
|
||
sheets=sheets_i,
|
||
require_recycle_gate=False,
|
||
)
|
||
if result.get("ok"):
|
||
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
|
||
|
||
invalidate_option_positions_cache()
|
||
_sync_options_trades(cfg, force=True)
|
||
if result.get("fully_closed"):
|
||
try:
|
||
from lib.options.options_target_lib import cancel_target_monitor
|
||
from lib.options.options_notify_lib import notify_options_close
|
||
|
||
conn2 = cfg["get_db"]()
|
||
try:
|
||
cancel_target_monitor(conn2, inst_id=inst_id)
|
||
conn2.commit()
|
||
notify_options_close(
|
||
cfg,
|
||
conn2,
|
||
inst_id=inst_id,
|
||
reason="手动平仓",
|
||
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
|
||
_mark_balances_stale(cfg)
|
||
return jsonify(result)
|
||
|
||
@app.route("/api/options/convert/quote", methods=["POST"])
|
||
@lr
|
||
def api_options_convert_quote():
|
||
ex, err = _require_options_ex(cfg)
|
||
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"])
|
||
@lr
|
||
def api_options_convert_execute():
|
||
ex, err = _require_options_ex(cfg)
|
||
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"])
|
||
@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": "数量无效"})
|
||
result = cfg["transfer_ccy"](ex, ccy, amount, from_acct, to_acct)
|
||
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():
|
||
ex, err = _require_options_ex(cfg)
|
||
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")
|
||
@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_positions_lib import sum_options_net_pnl_usdc
|
||
from lib.options.options_stats_lib import compute_options_stats_from_history
|
||
|
||
raw_live = cfg["fetch_option_positions"](ex)
|
||
if raw_live is None:
|
||
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
||
history = load_options_history(ex, cfg)
|
||
stats = compute_options_stats_from_history(history)
|
||
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)
|
||
return jsonify(
|
||
{
|
||
"ok": True,
|
||
**stats,
|
||
"open_float_pnl": open_float,
|
||
"total_pnl": total_pnl,
|
||
}
|
||
)
|
||
|
||
@app.route("/api/options/history/<path:history_key>", 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):
|
||
from lib.exchange.okx_options_lib import fetch_option_position_history
|
||
from lib.options.options_monitor_lib import reconcile_live_open_trades, sync_open_options_trades
|
||
|
||
ex = cfg.get("exchange_options")
|
||
if ex is None:
|
||
return 0
|
||
raw = cfg["fetch_option_positions"](ex)
|
||
if raw is None:
|
||
return 0
|
||
live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")}
|
||
reconcile_live_open_trades(conn, live_inst_ids=live_ids)
|
||
return sync_open_options_trades(
|
||
conn,
|
||
live_inst_ids=live_ids,
|
||
fetch_history_fn=lambda inst_id: fetch_option_position_history(ex, inst_id),
|
||
notify_cfg=cfg,
|
||
)
|
||
|
||
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
|
||
|
||
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,
|
||
},
|
||
daemon=True,
|
||
name="options-monitor",
|
||
)
|
||
t.start()
|