Add options index target monitors that auto limit-close on hit.

Position and order forms can arm a target; right-side and hub panels show active monitors; expiry remains the stop with no separate SL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-15 15:27:46 +08:00
parent 8d2da921ee
commit d6823a2903
13 changed files with 1011 additions and 18 deletions
+171 -3
View File
@@ -404,6 +404,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
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": "目标位无效"})
if not inst_id:
return jsonify({"ok": False, "msg": "缺少 inst_id"})
q = cfg["quote_option_contract"](ex, inst_id)
@@ -459,11 +468,14 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
if not order.get("ok"):
return jsonify(order)
conn = cfg["get_db"]()
trade_id = None
target_mon = None
try:
init_options_tables(conn)
meta = q.get("meta") or {}
u = str(meta.get("uly") or inst_id).split("-")[0]
conn.execute(
opt_type = meta.get("optType")
cur = conn.execute(
"""
INSERT INTO options_trades
(inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount,
@@ -473,7 +485,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
(
inst_id,
u,
meta.get("optType"),
opt_type,
q.get("strike"),
str(q.get("exp_time") or ""),
sheets,
@@ -484,6 +496,19 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
(order.get("data") or {}).get("ordId"),
),
)
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,
)
conn.commit()
finally:
conn.close()
@@ -491,7 +516,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True)
return jsonify({"ok": True, "order": order, "sizing": sizing})
return jsonify(
{
"ok": True,
"order": order,
"sizing": sizing,
"trade_id": trade_id,
"target_monitor": target_mon,
}
)
@app.route("/api/options/positions")
@lr
@@ -506,6 +539,9 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
meta_cache: dict[str, dict[str, Any] | None] = {}
conn = cfg["get_db"]()
try:
from lib.options.options_target_lib import targets_by_inst
tgt_map = targets_by_inst(conn)
rows = []
for p in raw:
inst = str(p.get("instId") or "").strip()
@@ -529,11 +565,102 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
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
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
return jsonify({"ok": True, "targets": list_active_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, sheets, 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 = int(trade["sheets"]) if trade and trade["sheets"] 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/close", methods=["POST"])
@lr
def api_options_close():
@@ -677,6 +804,17 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True)
try:
from lib.options.options_target_lib import cancel_target_monitor
conn2 = cfg["get_db"]()
try:
cancel_target_monitor(conn2, inst_id=inst_id)
conn2.commit()
finally:
conn2.close()
except Exception:
pass
return jsonify(
{
"ok": True,
@@ -739,6 +877,17 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True)
try:
from lib.options.options_target_lib import cancel_target_monitor
conn2 = cfg["get_db"]()
try:
cancel_target_monitor(conn2, inst_id=inst_id)
conn2.commit()
finally:
conn2.close()
except Exception:
pass
return jsonify({"ok": True, "order": order, "bid": bid, "sheets": close_sheets})
@app.route("/api/options/convert/quote", methods=["POST"])
@@ -974,6 +1123,24 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
fetch_history_fn=lambda inst_id: fetch_option_position_history(ex, inst_id),
)
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
t = threading.Thread(
target=options_monitor_loop,
kwargs={
@@ -986,6 +1153,7 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
"account_label": cfg["account_label"],
"profit_ratio": cfg["profit_ratio"],
"sync_trades_fn": _sync,
"target_close_fn": _target_close,
},
daemon=True,
name="options-monitor",