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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user