Enable hedge-plan live opens with path validation, DB, and monitor.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,10 +48,24 @@ def install_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None:
|
||||
cfg = _build_cfg(app_module)
|
||||
app.extensions["hedge_plan_cfg"] = cfg
|
||||
register_hedge_plan_routes(app, cfg)
|
||||
_maybe_start_monitor(cfg)
|
||||
|
||||
|
||||
def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
from lib.exchange.okx_options_lib import build_option_chain, options_header_balances
|
||||
from lib.exchange.okx_options_lib import (
|
||||
build_option_chain,
|
||||
fetch_index_price,
|
||||
options_header_balances,
|
||||
place_option_limit_order,
|
||||
quote_option_contract,
|
||||
td_mode_for_option_buy,
|
||||
)
|
||||
|
||||
def _amount_to_precision(sym: str, amt: float) -> float:
|
||||
ex = getattr(app_module, "exchange", None)
|
||||
if ex is None:
|
||||
return float(amt)
|
||||
return float(ex.amount_to_precision(sym, amt))
|
||||
|
||||
return {
|
||||
"get_db": app_module.get_db,
|
||||
@@ -63,8 +77,17 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"get_contract_size": getattr(app_module, "get_contract_size", None),
|
||||
"normalize_exchange_symbol": getattr(app_module, "normalize_exchange_symbol", None),
|
||||
"ensure_markets_loaded": getattr(app_module, "ensure_markets_loaded", None),
|
||||
"ensure_okx_live_ready": getattr(app_module, "ensure_okx_live_ready", None),
|
||||
"place_exchange_order": getattr(app_module, "place_exchange_order", None),
|
||||
"get_live_position_contracts": getattr(app_module, "get_live_position_contracts", None),
|
||||
"amount_to_precision": _amount_to_precision,
|
||||
"build_option_chain": build_option_chain,
|
||||
"options_header_balances": options_header_balances,
|
||||
"quote_option_contract": quote_option_contract,
|
||||
"place_option_limit_order": place_option_limit_order,
|
||||
"td_mode_for_option_buy": td_mode_for_option_buy,
|
||||
"fetch_index_price": fetch_index_price,
|
||||
"options_td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "isolated").strip(),
|
||||
"btc_leverage": int(getattr(app_module, "BTC_LEVERAGE", 10) or 10),
|
||||
"alt_leverage": int(getattr(app_module, "ALT_LEVERAGE", 5) or 5),
|
||||
"full_margin_buffer": float(getattr(app_module, "FULL_MARGIN_BUFFER_RATIO", 0.98) or 0.98),
|
||||
@@ -74,6 +97,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"),
|
||||
"perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
|
||||
"options_account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "期权账户").strip(),
|
||||
"live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +105,180 @@ def _hedge_enabled() -> bool:
|
||||
return _env_bool("HEDGE_PLAN_ENABLED", False)
|
||||
|
||||
|
||||
def _live_order() -> bool:
|
||||
return _env_bool("HEDGE_PLAN_LIVE_ORDER", False)
|
||||
|
||||
|
||||
def _max_active() -> int:
|
||||
try:
|
||||
return max(1, int(os.getenv("MAX_ACTIVE_HEDGE_PLANS") or "1"))
|
||||
except ValueError:
|
||||
return 1
|
||||
|
||||
|
||||
def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
|
||||
active = 0
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_db import count_active_plans, init_hedge_plan_tables
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
active = count_active_plans(conn)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
active = 0
|
||||
return gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=load_position_sizing_mode(),
|
||||
plan_type=plan_type,
|
||||
options_enabled=bool(cfg.get("options_enabled")),
|
||||
live_order=_live_order(),
|
||||
live_trading=bool(cfg.get("live_trading")) or _env_bool("LIVE_TRADING_ENABLED", False),
|
||||
active_count=active,
|
||||
max_active=_max_active(),
|
||||
)
|
||||
|
||||
|
||||
def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
|
||||
if not _hedge_enabled():
|
||||
return
|
||||
try:
|
||||
secs = float(os.getenv("HEDGE_PLAN_MONITOR_POLL_SECONDS") or "15")
|
||||
except ValueError:
|
||||
secs = 15.0
|
||||
secs = max(5.0, secs)
|
||||
|
||||
def _loop() -> None:
|
||||
import time
|
||||
|
||||
from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans
|
||||
|
||||
while True:
|
||||
try:
|
||||
tick_active_plans(cfg)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(secs)
|
||||
|
||||
import threading
|
||||
|
||||
t = threading.Thread(target=_loop, name="hedge-plan-monitor", daemon=True)
|
||||
t.start()
|
||||
cfg["hedge_monitor_thread"] = t
|
||||
|
||||
|
||||
def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_leg, insert_plan
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
opt = result.get("option") or {}
|
||||
perp = result.get("perp") or {}
|
||||
premium = float(opt.get("premium") or 0)
|
||||
plan_id = insert_plan(
|
||||
conn,
|
||||
{
|
||||
"plan_type": "perp_options",
|
||||
"status": "active",
|
||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
||||
"direction": str(body.get("direction") or "long"),
|
||||
"entry_mark": float(body.get("entry") or 0),
|
||||
"tp": float(body.get("tp") or 0),
|
||||
"sl": float(body.get("sl") or 0),
|
||||
"sizing_mode_at_open": load_position_sizing_mode(),
|
||||
"perp_size": float(perp.get("contracts") or body.get("contracts") or 0),
|
||||
"margin": body.get("margin"),
|
||||
"leverage": float(body.get("leverage") or 10),
|
||||
"premium_total": premium,
|
||||
"opened_at": result.get("opened_at"),
|
||||
},
|
||||
)
|
||||
insert_leg(
|
||||
conn,
|
||||
{
|
||||
"plan_id": plan_id,
|
||||
"leg_role": "perp",
|
||||
"symbol": str(body.get("exchange_symbol") or ""),
|
||||
"side": str(body.get("direction") or "long"),
|
||||
"size": float(perp.get("contracts") or body.get("contracts") or 0),
|
||||
"avg_open": float(body.get("entry") or 0),
|
||||
"status": "open",
|
||||
"exchange_ord_id": str(perp.get("exchange_ord_id") or ""),
|
||||
"opened_at": result.get("opened_at"),
|
||||
},
|
||||
)
|
||||
insert_leg(
|
||||
conn,
|
||||
{
|
||||
"plan_id": plan_id,
|
||||
"leg_role": "option_hedge",
|
||||
"inst_id": str(opt.get("inst_id") or body.get("opt_inst_id") or ""),
|
||||
"opt_type": str(opt.get("opt_type") or body.get("opt_type") or ""),
|
||||
"strike": opt.get("strike") or body.get("strike"),
|
||||
"side": "buy",
|
||||
"size": float(opt.get("sheets") or body.get("sheets") or 1),
|
||||
"avg_open": float(opt.get("ask") or 0),
|
||||
"premium": premium,
|
||||
"status": "open",
|
||||
"exchange_ord_id": str(opt.get("exchange_ord_id") or ""),
|
||||
"opened_at": result.get("opened_at"),
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
return plan_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_leg, insert_plan
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
a = result.get("leg_a") or {}
|
||||
b = result.get("leg_b") or {}
|
||||
premium = float(a.get("premium") or 0) + float(b.get("premium") or 0)
|
||||
plan_id = insert_plan(
|
||||
conn,
|
||||
{
|
||||
"plan_type": "options_options",
|
||||
"status": "active",
|
||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
||||
"target_price": float(body.get("target_price") or 0),
|
||||
"sizing_mode_at_open": load_position_sizing_mode(),
|
||||
"premium_total": premium,
|
||||
"opened_at": result.get("opened_at"),
|
||||
},
|
||||
)
|
||||
for role, res, src in (("option_a", a, body.get("leg_a") or {}), ("option_b", b, body.get("leg_b") or {})):
|
||||
insert_leg(
|
||||
conn,
|
||||
{
|
||||
"plan_id": plan_id,
|
||||
"leg_role": role,
|
||||
"inst_id": str(res.get("inst_id") or src.get("inst_id") or ""),
|
||||
"opt_type": str(res.get("opt_type") or src.get("opt_type") or ""),
|
||||
"strike": res.get("strike") or src.get("strike"),
|
||||
"side": "buy",
|
||||
"size": float(res.get("sheets") or src.get("sheets") or 1),
|
||||
"avg_open": float(res.get("ask") or 0),
|
||||
"premium": float(res.get("premium") or 0),
|
||||
"status": "open",
|
||||
"exchange_ord_id": str(res.get("exchange_ord_id") or ""),
|
||||
"opened_at": result.get("opened_at"),
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
return plan_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
lr = cfg["login_required"]
|
||||
|
||||
@@ -98,17 +296,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
@lr
|
||||
def api_hedge_gates():
|
||||
plan_type = (request.args.get("plan_type") or "perp_options").strip()
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
**gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=load_position_sizing_mode(),
|
||||
plan_type=plan_type,
|
||||
options_enabled=bool(cfg.get("options_enabled")),
|
||||
),
|
||||
}
|
||||
)
|
||||
return jsonify({"ok": True, **_gates_dict(cfg, plan_type)})
|
||||
|
||||
@app.route("/api/hedge-plan/market")
|
||||
@lr
|
||||
@@ -123,12 +311,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
if err:
|
||||
return jsonify({"ok": False, "msg": err}), 400
|
||||
sizing_mode = load_position_sizing_mode()
|
||||
gates = gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=sizing_mode,
|
||||
plan_type="perp_options",
|
||||
options_enabled=bool(cfg.get("options_enabled")),
|
||||
)
|
||||
gates = _gates_dict(cfg, "perp_options")
|
||||
out = {
|
||||
"ok": True,
|
||||
"base": base,
|
||||
@@ -181,12 +364,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
def api_hedge_preview():
|
||||
body = request.get_json(silent=True) or {}
|
||||
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
|
||||
gates = gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=load_position_sizing_mode(),
|
||||
plan_type=plan_type,
|
||||
options_enabled=bool(cfg.get("options_enabled")),
|
||||
)
|
||||
gates = _gates_dict(cfg, plan_type)
|
||||
if not gates.get("can_preview"):
|
||||
return jsonify({"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可测算"]), "gates": gates}), 400
|
||||
try:
|
||||
@@ -200,6 +378,144 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
return jsonify({"ok": False, "msg": f"测算失败: {e}"}), 500
|
||||
return jsonify({"ok": True, "gates": gates, **data})
|
||||
|
||||
@app.route("/api/hedge-plan/validate-path", methods=["POST"])
|
||||
@lr
|
||||
def api_hedge_validate_path():
|
||||
"""只校验下单路径(强制 dry_run),不真实成交."""
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import (
|
||||
execute_options_options_start,
|
||||
execute_perp_options_start,
|
||||
validate_start_body,
|
||||
)
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
|
||||
err = validate_start_body(plan_type, body)
|
||||
if err:
|
||||
return jsonify({"ok": False, "msg": err}), 400
|
||||
if plan_type == "options_options":
|
||||
out = execute_options_options_start(cfg, body, dry_run=True)
|
||||
else:
|
||||
out = execute_perp_options_start(cfg, body, dry_run=True)
|
||||
return jsonify(out), (200 if out.get("ok") else 400)
|
||||
|
||||
@app.route("/api/hedge-plan/start", methods=["POST"])
|
||||
@lr
|
||||
def api_hedge_start():
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import (
|
||||
execute_options_options_start,
|
||||
execute_perp_options_start,
|
||||
validate_start_body,
|
||||
)
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
|
||||
dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
|
||||
gates = _gates_dict(cfg, plan_type)
|
||||
if not dry_run and not gates.get("can_start"):
|
||||
return jsonify(
|
||||
{"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可开仓"]), "gates": gates}
|
||||
), 400
|
||||
err = validate_start_body(plan_type, body)
|
||||
if err:
|
||||
return jsonify({"ok": False, "msg": err, "gates": gates}), 400
|
||||
# 补齐永续杠杆
|
||||
if plan_type == "perp_options" and not body.get("leverage"):
|
||||
base = str(body.get("underlying") or "ETH").upper()
|
||||
body["leverage"] = cfg.get("btc_leverage") if base == "BTC" else (cfg.get("btc_leverage") or 10)
|
||||
# ETH 也用 BTC 档 10x 按方案;ALT 为 alt_leverage 仅非 BTC/ETH
|
||||
if base in ("BTC", "ETH"):
|
||||
body["leverage"] = int(cfg.get("btc_leverage") or 10)
|
||||
if plan_type == "options_options":
|
||||
out = execute_options_options_start(
|
||||
cfg,
|
||||
body,
|
||||
dry_run=dry_run,
|
||||
persist=(None if dry_run else (lambda r, b: _persist_oo(cfg, r, b))),
|
||||
)
|
||||
else:
|
||||
out = execute_perp_options_start(
|
||||
cfg,
|
||||
body,
|
||||
dry_run=dry_run,
|
||||
persist=(None if dry_run else (lambda r, b: _persist_po(cfg, r, b))),
|
||||
)
|
||||
out["gates"] = gates
|
||||
return jsonify(out), (200 if out.get("ok") else 400)
|
||||
|
||||
@app.route("/api/hedge-plan/list")
|
||||
@lr
|
||||
def api_hedge_list():
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, list_plans
|
||||
|
||||
status = (request.args.get("status") or "").strip() or None
|
||||
plan_type = (request.args.get("plan_type") or "").strip() or None
|
||||
underlying = (request.args.get("underlying") or "").strip() or None
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
rows = list_plans(
|
||||
conn, status=status, plan_type=plan_type, underlying=underlying, limit=80
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "plans": rows})
|
||||
|
||||
@app.route("/api/hedge-plan/history")
|
||||
@lr
|
||||
def api_hedge_history():
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, list_plans
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
rows = list_plans(conn, status="closed", limit=100)
|
||||
failed = list_plans(conn, status="failed", limit=50)
|
||||
cancelled = list_plans(conn, status="cancelled", limit=50)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "plans": rows + failed + cancelled})
|
||||
|
||||
@app.route("/api/hedge-plan/stats")
|
||||
@lr
|
||||
def api_hedge_stats():
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, stats_summary
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
s = stats_summary(conn)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({"ok": True, **s})
|
||||
|
||||
@app.route("/api/hedge-plan/<int:plan_id>")
|
||||
@lr
|
||||
def api_hedge_detail(plan_id: int):
|
||||
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, init_hedge_plan_tables
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
plan = get_plan(conn, plan_id)
|
||||
if not plan:
|
||||
return jsonify({"ok": False, "msg": "计划不存在"}), 404
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "plan": plan, "legs": legs})
|
||||
|
||||
@app.route("/api/hedge-plan/monitor-tick", methods=["POST"])
|
||||
@lr
|
||||
def api_hedge_monitor_tick():
|
||||
from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans
|
||||
|
||||
return jsonify(tick_active_plans(cfg))
|
||||
|
||||
|
||||
def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
|
||||
direction = str(body.get("direction") or "long").lower()
|
||||
|
||||
Reference in New Issue
Block a user