Files
crypto_monitor/lib/hedge_plan/hedge_plan_register.py
T
dekun c5d3d9d6c1 期期出场改盈亏比:达目标平盈利腿,亏损腿残值20%或到期平
将上/下破目标价替换为盈亏比(盈利金额/初始权利金,默认2);残值平需买一流动性且权利金≤初始20%。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 16:44:45 +08:00

1434 lines
56 KiB
Python

"""OKX 对冲计划:P0 测算页与 API 注册."""
from __future__ import annotations
import os
from typing import Any, Optional
from flask import Flask, jsonify, request
from jinja2 import ChoiceLoader, FileSystemLoader
from lib.hedge_plan.hedge_plan_calc_lib import (
build_options_options_preview,
build_perp_options_preview,
floor_contracts_to_precision,
gate_status,
option_premium_total,
suggest_contracts_from_notional,
)
from lib.hub.hub_calculator_market_lib import amount_decimals_from_exchange
from lib.trade.position_sizing_lib import (
compute_full_margin_sizing,
load_position_sizing_mode,
)
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 attach_hedge_plan_templates(app: Flask, repo_root: str) -> None:
tpl_dir = os.path.join(repo_root, "lib", "hedge_plan", "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_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None:
attach_hedge_plan_templates(app, repo_root)
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,
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,
"login_required": app_module.login_required,
"render_main_page": app_module.render_main_page,
"exchange": getattr(app_module, "exchange", None),
"exchange_options": getattr(app_module, "exchange_options", None),
"get_available_trading_usdt": getattr(app_module, "get_available_trading_usdt", None),
"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),
"close_exchange_order": getattr(app_module, "close_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),
"funds_decimals": int(getattr(app_module, "FUNDS_DECIMALS", 2) or 2),
"options_enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
"default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
"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(),
"trade_budget_usdc": float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC") or "10"),
# 对冲专用缓冲;与期权页 OKX_OPTIONS_BUDGET_BUFFER 独立
"budget_buffer": float(os.getenv("HEDGE_PLAN_BUDGET_BUFFER") or "0.95"),
"oo_bias_split_by": _oo_bias_split_by(),
"oo_bias_ratio": _oo_bias_ratio(),
"live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
"send_wechat": getattr(app_module, "send_wechat_msg", None),
}
def _hedge_enabled() -> bool:
from lib.hedge_plan.okx_trade_mode_lib import hedge_module_enabled
return hedge_module_enabled()
def _show_perp_options() -> bool:
from lib.hedge_plan.okx_trade_mode_lib import show_perp_options
return show_perp_options()
def _show_options_options() -> bool:
from lib.hedge_plan.okx_trade_mode_lib import show_options_options
return show_options_options()
def _oo_close_mode_enabled() -> bool:
return _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True)
def _oo_bias_split_by() -> str:
from lib.hedge_plan.hedge_plan_calc_lib import _normalize_oo_bias_split_by
return _normalize_oo_bias_split_by(os.getenv("HEDGE_PLAN_OO_BIAS_SPLIT_BY") or "budget")
def _oo_bias_ratio() -> float:
from lib.hedge_plan.hedge_plan_calc_lib import _clamp_oo_bias_ratio
return _clamp_oo_bias_ratio(os.getenv("HEDGE_PLAN_OO_BIAS_RATIO") or "0.7")
def _normalize_oo_close_mode(raw: Any) -> str:
"""方案C关闭时强制 hold_expiry;开启时默认 close_all."""
if not _oo_close_mode_enabled():
return "hold_expiry"
v = str(raw or "close_all").strip().lower()
if v in ("hold_expiry", "hold_to_expiry", "expiry", "到期平"):
return "hold_expiry"
return "close_all"
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
has_standalone = False
mutual = True
try:
from lib.hedge_plan.hedge_options_exclusive_lib import (
has_standalone_option_position,
mutual_exclusive_enabled,
)
from lib.hedge_plan.hedge_plan_db import count_active_plans, init_hedge_plan_tables
mutual = mutual_exclusive_enabled()
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
active = count_active_plans(conn)
if mutual:
try:
from lib.exchange.okx_options_lib import fetch_option_positions
ex = cfg.get("exchange_options") or cfg.get("exchange")
raw = fetch_option_positions(ex) if ex is not None else []
has_standalone = has_standalone_option_position(conn, raw or [])
except Exception:
has_standalone = True # fail-closed
conn.commit()
finally:
conn.close()
except Exception:
# fail-closed:探测失败视为不可开仓
active = 10**9
has_standalone = True
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(),
show_perp_options=_show_perp_options(),
show_options_options=_show_options_options(),
mutual_exclusive=mutual,
has_standalone_option=has_standalone,
)
def _gates_public(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
g = _gates_dict(cfg, plan_type)
g["oo_close_mode_enabled"] = _oo_close_mode_enabled()
g["oo_close_mode_default"] = "close_all" if _oo_close_mode_enabled() else "hold_expiry"
g["oo_bias_split_by"] = _oo_bias_split_by()
g["oo_bias_ratio"] = _oo_bias_ratio()
g["budget_buffer"] = float(cfg.get("budget_buffer") or 0.95)
return g
def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
# 始终启动监控线程:单独期权模式下仍需收口遗留 active/partial 计划
with _hedge_start_lock():
if cfg.get("hedge_monitor_thread") is not None:
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
_start_lock = None
def _hedge_start_lock():
global _start_lock
if _start_lock is None:
import threading
_start_lock = threading.Lock()
return _start_lock
def _start_body_json(body: dict[str, Any], missing_leg: Optional[str] = None) -> str:
import json
try:
return json.dumps(
{"start_body": body, "missing_leg": missing_leg},
ensure_ascii=False,
)[:8000]
except Exception:
return ""
def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
from lib.hedge_plan.hedge_plan_db import (
get_plan,
get_plan_legs,
init_hedge_plan_tables,
insert_leg,
insert_plan,
)
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
is_partial = bool(result.get("partial"))
missing = str(result.get("missing_leg") or "") if is_partial else ""
opt = result.get("option") or {}
perp = result.get("perp") or {}
if is_partial:
opt_ok = missing != "option_hedge" and bool(result.get("option"))
perp_ok = missing != "perp" and bool(result.get("perp"))
else:
opt_ok = True
perp_ok = True
premium = float((opt or {}).get("premium") or 0) if opt_ok else 0.0
from lib.hedge_plan.hedge_plan_option_primary_lib import (
is_option_primary,
perp_direction_for_view,
)
opt_primary = is_option_primary(body)
view = str(body.get("direction") or "long")
perp_dir = (
str((perp or {}).get("direction") or "")
or (perp_direction_for_view(view) if opt_primary else view)
)
plan_row = {
"plan_type": "perp_options",
"status": "partial" if is_partial else "active",
"underlying": str(body.get("underlying") or "ETH").upper(),
"direction": view,
"entry_mark": float(body.get("entry") or 0),
"tp": float(body.get("tp") or 0) if not opt_primary else 0,
"sl": float(body.get("sl") or 0) if not opt_primary else 0,
"sizing_mode_at_open": load_position_sizing_mode(),
"perp_size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
"margin": body.get("margin"),
"leverage": float(body.get("leverage") or (100 if opt_primary else 10)),
"premium_total": premium,
"preview_json": _start_body_json(body, missing or None),
"close_reason": "partial_fail" if is_partial else None,
"opened_at": result.get("opened_at"),
"note": (result.get("msg") or "")[:500] if is_partial else None,
"option_primary": 1 if opt_primary else 0,
"perp_direction": perp_dir,
}
if opt_primary:
plan_row.update(
{
"option_target_points": float(body.get("option_target_points") or 0),
"perp_target_points": float(body.get("perp_target_points") or 0),
"option_perp_ratio": float(body.get("option_perp_ratio") or 0),
"premium_budget": float(body.get("premium_budget") or 0),
"strike_interval": float(body.get("strike_interval") or 15),
"min_option_hours": float(body.get("min_option_hours") or 36),
"option_moneyness": str(body.get("moneyness") or body.get("option_moneyness") or ""),
}
)
plan_id = insert_plan(conn, plan_row)
insert_leg(
conn,
{
"plan_id": plan_id,
"leg_role": "perp",
"symbol": str(body.get("exchange_symbol") or ""),
"side": perp_dir,
"size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
"avg_open": float(body.get("entry") or 0) if perp_ok else None,
"status": "open" if perp_ok else "pending",
"exchange_ord_id": str((perp or {}).get("exchange_ord_id") or ""),
"opened_at": result.get("opened_at") if perp_ok else None,
},
)
insert_leg(
conn,
{
"plan_id": plan_id,
"leg_role": "option_hedge",
"inst_id": str((opt or {}).get("inst_id") or body.get("opt_inst_id") or ""),
"opt_type": str((opt or {}).get("opt_type") or body.get("opt_type") or ""),
"strike": (opt or {}).get("strike") or body.get("strike"),
"side": "buy",
"size": float((opt or {}).get("sheets") or body.get("sheets") or 1),
"avg_open": float((opt or {}).get("ask") or body.get("ask") or 0) if opt_ok else None,
"premium": premium if opt_ok else 0,
"ct_mult": float(body.get("ct_mult") or (opt or {}).get("ct_mult") or 0.01),
"status": "open" if opt_ok else "pending",
"exchange_ord_id": str((opt or {}).get("exchange_ord_id") or ""),
"opened_at": result.get("opened_at") if opt_ok else None,
},
)
conn.commit()
if not is_partial:
plan = get_plan(conn, plan_id)
legs = get_plan_legs(conn, plan_id)
if plan:
notify_plan_start(cfg, conn, plan, legs)
conn.commit()
return plan_id
finally:
conn.close()
def _persist_po_watching(cfg: dict[str, Any], body: dict[str, Any]) -> int:
"""以期权为主:只落库盯盘计划,不下单."""
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_plan
from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
view = str(body.get("direction") or "long")
money = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower()
plan_id = insert_plan(
conn,
{
"plan_type": "perp_options",
"status": "watching",
"underlying": str(body.get("underlying") or "ETH").upper(),
"direction": view,
"entry_mark": float(body.get("index_px") or body.get("entry") or 0) or None,
"tp": 0,
"sl": 0,
"sizing_mode_at_open": None,
"perp_size": None,
"margin": None,
"leverage": float(body.get("leverage") or 100),
"premium_total": 0,
"preview_json": _start_body_json(body),
"close_reason": None,
"opened_at": None,
"note": "盯盘中:等待杠杆/间隔达标后自动开仓",
"option_primary": 1,
"perp_direction": perp_direction_for_view(view),
"option_target_points": float(body.get("option_target_points") or 0),
"perp_target_points": float(body.get("perp_target_points") or 0),
"option_perp_ratio": float(body.get("option_perp_ratio") or 0),
"premium_budget": float(body.get("premium_budget") or 0),
"strike_interval": float(body.get("strike_interval") or 15),
"min_option_hours": float(body.get("min_option_hours") or 36),
"option_moneyness": money,
"option_leverage": float(body.get("option_leverage") or 0),
},
)
conn.commit()
return plan_id
finally:
conn.close()
def _activate_watching_po(
cfg: dict[str, Any],
conn: Any,
plan_id: int,
result: dict[str, Any],
body: dict[str, Any],
) -> None:
"""盯盘命中后:写入腿并把 watching → active/partial."""
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, insert_leg, update_plan
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view
is_partial = bool(result.get("partial"))
missing = str(result.get("missing_leg") or "") if is_partial else ""
opt = result.get("option") or {}
perp = result.get("perp") or {}
if is_partial:
opt_ok = missing != "option_hedge" and bool(result.get("option"))
perp_ok = missing != "perp" and bool(result.get("perp"))
else:
opt_ok = True
perp_ok = True
premium = float((opt or {}).get("premium") or 0) if opt_ok else 0.0
view = str(body.get("direction") or "long")
perp_dir = (
str((perp or {}).get("direction") or "")
or perp_direction_for_view(view)
)
update_plan(
conn,
int(plan_id),
status="partial" if is_partial else "active",
entry_mark=float(body.get("entry") or body.get("index_px") or 0) or None,
perp_size=float((perp or {}).get("contracts") or body.get("contracts") or 0),
leverage=float(body.get("leverage") or 100),
premium_total=premium,
preview_json=_start_body_json(body, missing or None),
close_reason="partial_fail" if is_partial else None,
opened_at=result.get("opened_at"),
note=(result.get("msg") or "")[:500] if is_partial else "盯盘达标已开仓",
perp_direction=perp_dir,
)
insert_leg(
conn,
{
"plan_id": int(plan_id),
"leg_role": "perp",
"symbol": str(body.get("exchange_symbol") or ""),
"side": perp_dir,
"size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
"avg_open": float(body.get("entry") or 0) if perp_ok else None,
"status": "open" if perp_ok else "pending",
"exchange_ord_id": str((perp or {}).get("exchange_ord_id") or ""),
"opened_at": result.get("opened_at") if perp_ok else None,
},
)
insert_leg(
conn,
{
"plan_id": int(plan_id),
"leg_role": "option_hedge",
"inst_id": str((opt or {}).get("inst_id") or body.get("opt_inst_id") or ""),
"opt_type": str((opt or {}).get("opt_type") or body.get("opt_type") or ""),
"strike": (opt or {}).get("strike") or body.get("strike"),
"side": "buy",
"size": float((opt or {}).get("sheets") or body.get("sheets") or 1),
"avg_open": float((opt or {}).get("ask") or body.get("ask") or 0) if opt_ok else None,
"premium": premium if opt_ok else 0,
"ct_mult": float(body.get("ct_mult") or (opt or {}).get("ct_mult") or 0.01),
"status": "open" if opt_ok else "pending",
"exchange_ord_id": str((opt or {}).get("exchange_ord_id") or ""),
"opened_at": result.get("opened_at") if opt_ok else None,
},
)
if not is_partial:
plan = get_plan(conn, int(plan_id))
legs = get_plan_legs(conn, int(plan_id))
if plan:
notify_plan_start(cfg, conn, plan, legs)
def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
from lib.hedge_plan.hedge_plan_db import (
get_plan,
get_plan_legs,
init_hedge_plan_tables,
insert_leg,
insert_plan,
)
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
is_partial = bool(result.get("partial"))
missing = str(result.get("missing_leg") or "") if is_partial else ""
a = result.get("leg_a") or {}
b = result.get("leg_b") or {}
a_ok = True if not is_partial else bool(result.get("leg_a"))
b_ok = True if not is_partial else (missing != "option_b" and bool(result.get("leg_b")))
premium = (float(a.get("premium") or 0) if a_ok else 0.0) + (
float(b.get("premium") or 0) if b_ok else 0.0
)
rr_raw = body.get("profit_rr")
try:
profit_rr = float(rr_raw) if rr_raw not in (None, "") else 2.0
except (TypeError, ValueError):
profit_rr = 2.0
if profit_rr <= 0:
profit_rr = 2.0
# 旧字段兼容:不再要求上/下破;有传则原样落库
def _opt_float(key: str, *alts: str) -> float | None:
for k in (key, *alts):
v = body.get(k)
if v not in (None, ""):
try:
return float(v)
except (TypeError, ValueError):
continue
return None
up_f = _opt_float("target_price_up", "target_price")
down_f = _opt_float("target_price_down", "target_price")
plan_id = insert_plan(
conn,
{
"plan_type": "options_options",
"status": "partial" if is_partial else "active",
"underlying": str(body.get("underlying") or "ETH").upper(),
"target_price": up_f,
"target_price_up": up_f,
"target_price_down": down_f,
"profit_rr": profit_rr,
"sizing_mode_at_open": load_position_sizing_mode(),
"premium_total": premium,
"oo_close_mode": _normalize_oo_close_mode(body.get("oo_close_mode")),
"preview_json": _start_body_json(body, missing or None),
"close_reason": "partial_fail" if is_partial else None,
"opened_at": result.get("opened_at"),
"note": (result.get("msg") or "")[:500] if is_partial else None,
},
)
for role, res, src, ok in (
("option_a", a, body.get("leg_a") or {}, a_ok),
("option_b", b, body.get("leg_b") or {}, b_ok),
):
insert_leg(
conn,
{
"plan_id": plan_id,
"leg_role": role,
"inst_id": str((res or {}).get("inst_id") or src.get("inst_id") or ""),
"opt_type": str((res or {}).get("opt_type") or src.get("opt_type") or ""),
"strike": (res or {}).get("strike") or src.get("strike"),
"side": "buy",
"size": float((res or {}).get("sheets") or src.get("sheets") or 1),
"avg_open": float((res or {}).get("ask") or 0) if ok else None,
"premium": float((res or {}).get("premium") or 0) if ok else 0,
"status": "open" if ok else "pending",
"exchange_ord_id": str((res or {}).get("exchange_ord_id") or ""),
"opened_at": result.get("opened_at") if ok else None,
},
)
conn.commit()
if not is_partial:
plan = get_plan(conn, plan_id)
legs = get_plan_legs(conn, plan_id)
if plan:
notify_plan_start(cfg, conn, plan, legs)
conn.commit()
return plan_id
finally:
conn.close()
def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
lr = cfg["login_required"]
@app.route("/hedge-plan")
@lr
def page_hedge_plan():
from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled
redir = redirect_to_embed_shell_if_enabled("hedge_plan")
if redir is not None:
return redir
return cfg["render_main_page"]("hedge_plan")
@app.route("/api/hedge-plan/gates")
@lr
def api_hedge_gates():
plan_type = (request.args.get("plan_type") or "perp_options").strip()
return jsonify({"ok": True, **_gates_public(cfg, plan_type)})
@app.route("/api/hedge-plan/market")
@lr
def api_hedge_market():
base = (request.args.get("base") or cfg.get("default_underly") or "ETH").strip().upper()
if base not in ("BTC", "ETH"):
return jsonify({"ok": False, "msg": "对冲计划仅支持 BTC/ETH"}), 400
direction = (request.args.get("direction") or "long").strip().lower()
if direction not in ("long", "short"):
direction = "long"
option_primary = (request.args.get("option_primary") or "").strip().lower() in (
"1",
"true",
"yes",
"on",
)
data, err = _fetch_perp_market(cfg, base)
if err:
return jsonify({"ok": False, "msg": err}), 400
sizing_mode = load_position_sizing_mode()
gates = _gates_dict(cfg, "perp_options")
if option_primary:
from lib.hedge_plan.hedge_plan_option_primary_lib import (
opt_type_for_view,
perp_direction_for_view,
)
suggested = opt_type_for_view(direction)
perp_dir = perp_direction_for_view(direction)
acct_note = "以期权为主:看法腿买期权,永续反向对冲"
else:
suggested = "P" if direction == "long" else "C"
perp_dir = direction
acct_note = "永续腿使用合约(交易)账户可用 USDT"
out = {
"ok": True,
"base": base,
"direction": direction,
"option_primary": option_primary,
"suggested_opt_type": suggested,
"perp_direction": perp_dir,
**data,
"gates": gates,
"sizing_mode": sizing_mode,
"account_kind": "perp",
"account_label": cfg.get("perp_account_label") or "合约账户",
"account_note": acct_note,
}
return jsonify(out)
@app.route("/api/hedge-plan/options-chain")
@lr
def api_hedge_options_chain():
if not cfg.get("options_enabled"):
return jsonify({"ok": False, "msg": "期权模块未启用"}), 400
ex = cfg.get("exchange_options")
if ex is None:
return jsonify({"ok": False, "msg": "期权交易所未初始化"}), 400
u = (request.args.get("underlying") or cfg.get("default_underly") or "ETH").upper()
# 热更新:链展示天数每次读 env
chain_max_dte = float(
os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS")
or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS")
or cfg.get("chain_max_dte")
or 14
)
try:
chain = cfg["build_option_chain"](
ex,
u,
max_dte_days=chain_max_dte,
itm_only=False,
itm_max_dist_usd=float(os.getenv("OKX_OPTIONS_ITM_MAX_DIST_USD") or "30"),
)
except Exception as e:
return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500
# 可选:永期以期权为主时按最低剩余小时/行权间隔过滤(仅当请求显式带 option_primary)
# 默认拉链不再带此过滤,避免期期看不到明天到期
option_primary = (request.args.get("option_primary") or "").strip().lower() in (
"1",
"true",
"yes",
"on",
)
min_hours = None
strike_interval = None
try:
if request.args.get("min_hours") not in (None, ""):
min_hours = float(request.args.get("min_hours"))
except (TypeError, ValueError):
min_hours = 36.0 if option_primary else None
try:
if request.args.get("strike_interval") not in (None, ""):
strike_interval = float(request.args.get("strike_interval"))
except (TypeError, ValueError):
strike_interval = 15.0 if option_primary else None
if option_primary and min_hours is None:
min_hours = 36.0
if option_primary and strike_interval is None:
strike_interval = 15.0
if min_hours is not None or strike_interval is not None:
from lib.hedge_plan.hedge_plan_option_primary_lib import hours_to_expiry_from_ms
idx = None
try:
idx = float(chain.get("index_px") or 0) or None
except (TypeError, ValueError):
idx = None
filtered = []
for exp in chain.get("expiries") or []:
h = hours_to_expiry_from_ms(exp.get("exp_time"))
if min_hours is not None and h is not None and h < min_hours:
continue
contracts = []
for c in exp.get("contracts") or []:
row = dict(c)
row["hours_to_expiry"] = h
if strike_interval is not None and idx and idx > 0:
try:
k = float(row.get("strike") or 0)
except (TypeError, ValueError):
k = 0.0
if k > 0 and abs(k - idx) > strike_interval + 1e-9:
continue
contracts.append(row)
if contracts:
filtered.append({**exp, "contracts": contracts, "hours_to_expiry": h})
chain = {**chain, "expiries": filtered}
opt_acct = _options_account_snapshot(cfg)
return jsonify(
{
"ok": True,
**chain,
"underlying": u,
"chain_max_dte_days": chain_max_dte,
"account_kind": "options",
"account_label": cfg.get("options_account_label") or "期权账户",
"account_note": "期权腿使用期权账户(交易 USDC)",
"options_account": opt_acct,
"trade_budget_usdc": cfg.get("trade_budget_usdc"),
"budget_buffer": cfg.get("budget_buffer"),
"option_primary": option_primary,
"min_hours": min_hours,
"strike_interval": strike_interval,
}
)
@app.route("/api/hedge-plan/preview", methods=["POST"])
@lr
def api_hedge_preview():
body = request.get_json(silent=True) or {}
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
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:
if plan_type == "options_options":
data = _preview_oo(body)
else:
data = _preview_po(body)
except ValueError as e:
return jsonify({"ok": False, "msg": str(e)}), 400
except Exception as e:
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)
with _hedge_start_lock():
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
# 补齐永续杠杆(以期权为主默认 100;保险模式 BTC/ETH 用 btc_leverage)
if plan_type == "perp_options" and not body.get("leverage"):
from lib.hedge_plan.hedge_plan_option_primary_lib import is_option_primary
if is_option_primary(body):
body["leverage"] = 100
else:
base = str(body.get("underlying") or "ETH").upper()
if base in ("BTC", "ETH"):
body["leverage"] = int(cfg.get("btc_leverage") or 10)
else:
body["leverage"] = int(cfg.get("alt_leverage") or 5)
# 以期权为主:策略启动=盯盘,不现场开仓
if plan_type == "perp_options":
from lib.hedge_plan.hedge_plan_option_primary_lib import is_option_primary
watch = body.get("watch_entry")
watch_on = watch in (None, "", True, 1, "1", "true", "yes", "on")
if is_option_primary(body) and watch_on:
if dry_run:
return jsonify(
{
"ok": True,
"dry_run": True,
"watching": True,
"msg": "dry_run:将创建盯盘计划(不落库)",
"gates": gates,
}
)
plan_id = _persist_po_watching(cfg, body)
return jsonify(
{
"ok": True,
"watching": True,
"plan_id": plan_id,
"msg": "已启动盯盘,杠杆/间隔达标后自动开仓",
"gates": gates,
}
)
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/<int:plan_id>/end", methods=["POST"])
@lr
def api_hedge_end_plan(plan_id: int):
"""人工结束进行中计划:不自动平仓;未成交腿改为 cancelled."""
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables
from lib.hedge_plan.hedge_plan_orders_lib import execute_manual_end_plan
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
out = execute_manual_end_plan(cfg, conn, plan_id)
if not out.get("ok"):
return jsonify(out), 400
conn.commit()
finally:
conn.close()
return jsonify(out)
@app.route("/api/hedge-plan/<int:plan_id>/complete-leg", methods=["POST"])
@lr
def api_hedge_complete_leg(plan_id: int):
"""半腿待补:手动补开缺失腿,成功后升为 active."""
import json
from lib.hedge_plan.hedge_plan_db import (
get_plan,
get_plan_legs,
init_hedge_plan_tables,
update_leg,
update_plan,
)
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
from lib.hedge_plan.hedge_plan_orders_lib import execute_complete_missing_leg
body = request.get_json(silent=True) or {}
dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
if not dry_run and not _hedge_enabled():
return jsonify({"ok": False, "msg": "当前交易模式为单独期权,不可补开对冲腿"}), 400
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
pt = str(plan.get("plan_type") or "")
if pt == "perp_options" and not _show_perp_options():
return jsonify({"ok": False, "msg": "当前模式非永期对冲,不可补开"}), 400
if pt == "options_options" and not _show_options_options():
return jsonify({"ok": False, "msg": "当前模式非期期对冲,不可补开"}), 400
if str(plan.get("status") or "") != "partial":
return jsonify({"ok": False, "msg": "仅半腿待补(partial)计划可补开"}), 400
legs = get_plan_legs(conn, plan_id)
start_body: dict[str, Any] = {}
try:
meta = json.loads(plan.get("preview_json") or "{}")
if isinstance(meta, dict):
start_body = dict(meta.get("start_body") or {})
except Exception:
start_body = {}
if not start_body:
return jsonify({"ok": False, "msg": "缺少开仓参数,无法补开"}), 400
# 允许请求体覆盖少量字段
for k in ("contracts", "leverage", "sheets", "tp", "sl"):
if body.get(k) not in (None, ""):
start_body[k] = body.get(k)
out = execute_complete_missing_leg(
cfg, plan, legs, start_body, dry_run=dry_run
)
if not out.get("ok"):
return jsonify(out), 400
if dry_run:
return jsonify(out)
fill = out.get("fill") or {}
leg_id = out.get("leg_id")
role = str(out.get("leg_role") or "")
opened_at = out.get("opened_at")
if leg_id:
if role == "perp":
update_leg(
conn,
int(leg_id),
status="open",
size=float(fill.get("contracts") or start_body.get("contracts") or 0),
avg_open=float(start_body.get("entry") or plan.get("entry_mark") or 0),
exchange_ord_id=str(fill.get("exchange_ord_id") or ""),
opened_at=opened_at,
)
update_plan(
conn,
plan_id,
status="active",
close_reason=None,
note=None,
perp_size=float(fill.get("contracts") or start_body.get("contracts") or 0),
)
else:
prem = float(fill.get("premium") or 0)
update_leg(
conn,
int(leg_id),
status="open",
size=float(fill.get("sheets") or start_body.get("sheets") or 1),
avg_open=float(fill.get("ask") or 0),
premium=prem,
exchange_ord_id=str(fill.get("exchange_ord_id") or ""),
opened_at=opened_at,
inst_id=str(fill.get("inst_id") or ""),
)
old_prem = float(plan.get("premium_total") or 0)
update_plan(
conn,
plan_id,
status="active",
close_reason=None,
note=None,
premium_total=old_prem + prem,
)
conn.commit()
plan2 = get_plan(conn, plan_id)
legs2 = get_plan_legs(conn, plan_id)
if plan2:
notify_plan_start(cfg, conn, plan2, legs2)
conn.commit()
out["plan_id"] = plan_id
out["status"] = "active"
out["plan"] = plan2
out["legs"] = legs2
return jsonify(out)
finally:
conn.close()
@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 (
attach_legs_to_plans,
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)
merged = attach_legs_to_plans(conn, rows + failed + cancelled)
conn.commit()
finally:
conn.close()
return jsonify({"ok": True, "plans": merged})
@app.route("/api/hedge-plan/active")
@lr
def api_hedge_active():
from lib.hedge_plan.hedge_plan_db import (
attach_legs_to_plans,
init_hedge_plan_tables,
list_plans,
)
from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
rows = []
for status in ("watching", "opening", "active", "partial"):
rows.extend(list_plans(conn, status=status, limit=80))
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
for row in rows:
if str(row.get("status") or "") == "watching":
continue
try:
reconcile_unfilled_option_legs(cfg, conn, int(row["id"]))
except Exception:
pass
# 校正后可能 status 变化,重新拉一遍
rows = []
for status in ("watching", "opening", "active", "partial"):
rows.extend(list_plans(conn, status=status, limit=80))
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
plans = attach_legs_to_plans(conn, rows)
conn.commit()
finally:
conn.close()
return jsonify({"ok": True, "plans": plans})
@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,
legs_contract_summary,
)
from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs
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
# 打开细节时校正:无成交却标 open → cancelled
if str(plan.get("status") or "") in ("opening", "active", "partial"):
reconcile_unfilled_option_legs(cfg, conn, plan_id)
plan = get_plan(conn, plan_id) or plan
legs = get_plan_legs(conn, plan_id)
conn.commit()
finally:
conn.close()
return jsonify(
{
"ok": True,
"plan": plan,
"legs": legs,
"contracts_summary": legs_contract_summary(legs),
}
)
@app.route("/api/hedge-plan/<int:plan_id>", methods=["DELETE"])
@lr
def api_hedge_delete(plan_id: int):
from lib.hedge_plan.hedge_plan_db import delete_plan, init_hedge_plan_tables
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
out = delete_plan(conn, plan_id)
if not out.get("ok"):
return jsonify(out), 400
conn.commit()
finally:
conn.close()
return jsonify(out)
@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]:
from lib.hedge_plan.hedge_plan_moneyness_lib import validate_po_option_moneyness
from lib.hedge_plan.hedge_plan_option_primary_lib import (
build_option_primary_preview,
is_option_primary,
size_from_premium,
validate_option_primary_start,
)
if is_option_primary(body):
err = validate_option_primary_start(body)
if err:
raise ValueError(err)
sized = size_from_premium(
premium_budget=float(body.get("premium_budget") or 0),
ask=float(body.get("ask") or 0),
ct_mult=float(body.get("ct_mult") or 0.01),
ratio=float(body.get("option_perp_ratio") or 2),
contract_size=float(body.get("contract_size") or 0.01),
)
if not sized.get("ok"):
raise ValueError(sized.get("msg") or "定仓失败")
body = dict(body)
body["sheets"] = sized["sheets"]
body["contracts"] = sized["contracts"]
body["eth_qty"] = sized["eth_qty"]
if not body.get("entry"):
body["entry"] = body.get("index_px") or 0
out = build_option_primary_preview(body)
out["sizing"] = sized
return out
direction = str(body.get("direction") or "long").lower()
entry = float(body["entry"])
tp = float(body["tp"])
sl = float(body["sl"])
contracts = float(body["contracts"])
contract_size = float(body.get("contract_size") or 0.01)
opt_type = str(body.get("opt_type") or ("P" if direction == "long" else "C"))
strike = float(body["strike"])
sheets = float(body.get("sheets") or 1)
ct_mult = float(body.get("ct_mult") or 0.01)
ask = body.get("ask")
premium = body.get("premium_paid")
if premium is None:
if ask is None:
raise ValueError("缺少权利金或卖一价")
premium = option_premium_total(ask=float(ask), sheets=sheets, ct_mult=ct_mult)
index_px = body.get("index_px")
idx_for_money = float(index_px) if index_px is not None else entry
money_err = validate_po_option_moneyness(
opt_type=opt_type,
strike=strike,
index_px=idx_for_money,
ask=ask,
hours_to_expiry=body.get("hours_to_expiry"),
)
if money_err:
raise ValueError(money_err)
return build_perp_options_preview(
direction=direction,
entry=entry,
tp=tp,
sl=sl,
contracts=contracts,
contract_size=contract_size,
opt_type=opt_type,
strike=strike,
sheets=sheets,
ct_mult=ct_mult,
premium_paid=float(premium),
index_px=float(index_px) if index_px is not None else None,
)
def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
from lib.hedge_plan.hedge_plan_moneyness_lib import validate_oo_legs_moneyness
rr_raw = body.get("profit_rr")
profit_rr = None
if rr_raw not in (None, ""):
profit_rr = float(rr_raw)
if profit_rr <= 0:
raise ValueError("盈亏比须大于0")
up = body.get("target_price_up")
down = body.get("target_price_down")
legacy = body.get("target_price")
if up in (None, "") and legacy not in (None, ""):
up = legacy
if down in (None, "") and legacy not in (None, ""):
down = legacy
if profit_rr is None and (up in (None, "") or down in (None, "")):
raise ValueError("请填写盈亏比")
up_f = float(up) if up not in (None, "") else None
down_f = float(down) if down not in (None, "") else None
if profit_rr is None and up_f is not None and down_f is not None and up_f <= down_f:
raise ValueError("上破目标价必须大于下破目标价")
index_px = body.get("index_px")
if index_px in (None, ""):
if up_f is not None and down_f is not None:
index_px = (up_f + down_f) / 2
else:
raise ValueError("缺少指数价格")
index_px = float(index_px)
leg_a = body.get("leg_a") or {}
leg_b = body.get("leg_b") or {}
for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)):
if not leg.get("strike"):
raise ValueError(f"缺少 {name} 行权价")
if leg.get("premium_paid") is None and leg.get("ask") is not None:
leg["premium_paid"] = option_premium_total(
ask=float(leg["ask"]),
sheets=float(leg.get("sheets") or 1),
ct_mult=float(leg.get("ct_mult") or 0.01),
)
if leg.get("premium_paid") is None:
raise ValueError(f"缺少 {name} 权利金")
money_err = validate_oo_legs_moneyness(leg_a, leg_b, index_px=index_px)
if money_err:
raise ValueError(money_err)
return build_options_options_preview(
profit_rr=profit_rr,
target_price_up=up_f,
target_price_down=down_f,
index_px=index_px,
leg_a=leg_a,
leg_b=leg_b,
)
def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any], str | None]:
ex = cfg.get("exchange")
if ex is None:
return {}, "永续交易所未初始化"
ensure = cfg.get("ensure_markets_loaded")
if callable(ensure):
try:
ensure()
except Exception as e:
return {}, f"加载市场失败: {e}"
norm = cfg.get("normalize_exchange_symbol")
sym = f"{base}/USDT:USDT"
if callable(norm):
try:
sym = norm(f"{base}/USDT")
except Exception:
sym = f"{base}/USDT:USDT"
mark = bid = ask = last = None
try:
t = ex.fetch_ticker(sym)
last = _sf(t.get("last"))
mark = _sf(t.get("info", {}).get("markPx")) if isinstance(t.get("info"), dict) else None
if mark is None:
mark = _sf(t.get("mark")) or last
bid = _sf(t.get("bid"))
ask = _sf(t.get("ask"))
except Exception as e:
return {}, f"拉永续行情失败: {e}"
cs = 0.01
get_cs = cfg.get("get_contract_size")
if callable(get_cs):
try:
cs = float(get_cs(sym) or 0.01)
except Exception:
cs = 0.01
available = None
get_av = cfg.get("get_available_trading_usdt")
if callable(get_av):
try:
available = get_av()
except Exception:
available = None
entry = float(mark or last or 0)
sizing = None
suggest_contracts = None
amount_precision = 4
try:
amount_precision = int(amount_decimals_from_exchange(ex, sym))
except Exception:
amount_precision = 4
if available is not None and entry > 0:
sizing, _serr = compute_full_margin_sizing(
symbol=sym,
available_usdt=float(available),
capital_base=float(available),
buffer_ratio=float(cfg.get("full_margin_buffer") or 0.98),
btc_leverage=int(cfg.get("btc_leverage") or 10),
alt_leverage=int(cfg.get("alt_leverage") or 5),
funds_decimals=int(cfg.get("funds_decimals") or 2),
)
if sizing:
raw_contracts = suggest_contracts_from_notional(
notional=float(sizing["notional_value"]),
entry=entry,
contract_size=cs,
)
# 优先走交易所 amount_to_precision;失败则按精度位数向下取整
suggest_contracts = None
try:
precise = float(ex.amount_to_precision(sym, raw_contracts))
if precise > raw_contracts + 1e-12:
precise = floor_contracts_to_precision(raw_contracts, amount_precision)
suggest_contracts = precise
except Exception:
suggest_contracts = floor_contracts_to_precision(raw_contracts, amount_precision)
return {
"exchange_symbol": sym,
"mark": mark,
"last": last,
"bid": bid,
"ask": ask,
"contract_size": cs,
"available_usdt": available,
"full_margin_sizing": sizing,
"suggest_contracts": suggest_contracts,
"amount_precision": amount_precision,
"unit_quote": "USDT",
"unit_contracts": "合约张",
"unit_note": "价格单位 USDT;张数=交易所永续合约张(与下单精度一致);名义≈张数×面值×价格",
"entry_ref": entry or None,
}, None
def _options_account_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
"""期权账户资金快照(与期权页同源: exchange_options)."""
out: dict[str, Any] = {
"label": cfg.get("options_account_label") or "期权账户",
"trading_usdc": None,
"funding_usdc": None,
"trading_usdt": None,
"funding_usdt": None,
}
ex = cfg.get("exchange_options")
hdr = cfg.get("options_header_balances")
if ex is None or not callable(hdr):
return out
try:
trading_usdc, funding_usdc, funding_usdt, trading_usdt = hdr(ex, force=False)
out.update(
{
"trading_usdc": trading_usdc,
"funding_usdc": funding_usdc,
"trading_usdt": trading_usdt,
"funding_usdt": funding_usdt,
}
)
except Exception:
pass
return out
def _sf(v: Any) -> float | None:
if v is None or v == "":
return None
try:
return float(v)
except (TypeError, ValueError):
return None