Fix hedge market quotes via public HTTP and default cloud-server deploy settings.

Avoid load_markets hangs on weak networks; enable login-required sim defaults and document Baota Nginx reverse proxy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 10:13:39 +08:00
parent b4d62ea0a6
commit 135cdc13ba
11 changed files with 392 additions and 118 deletions
+12
View File
@@ -990,6 +990,18 @@
const amtPrec = d.amount_precision != null ? Number(d.amount_precision) : 4;
const markEl = $("hp-po-mark");
if (markEl) markEl.textContent = "标记 " + fmt(d.mark, 2);
// 指数优先用行情接口,期权链慢/失败时也不再显示 --
const mIdx = d.index_px != null ? Number(d.index_px) : NaN;
if (Number.isFinite(mIdx) && mIdx > 0) {
const idxLine = $("hp-index-line");
if (idxLine && (!state.chain || !state.chain.index_px)) {
idxLine.textContent = "指数 " + fmt(mIdx, 2);
}
const ooIdx = $("hp-oo-index");
if (ooIdx && (!state.chain || !state.chain.index_px)) {
ooIdx.textContent = "指数 " + fmt(mIdx, 2);
}
}
const quoteHtml =
"可用 <strong>" +
fmt(d.available_usdt, 2) +
+46 -4
View File
@@ -723,11 +723,34 @@ def options_header_balance_pack(
def fetch_index_price(ex: ccxt.okx, uly: str) -> float | None:
inst = f"{uly}" if "-" in uly else f"{uly}-USD"
"""指数价:优先公共 HTTP/curl,再回退 ccxt."""
try:
rows = ex.public_get_market_index_tickers({"instId": inst}).get("data") or []
from lib.exchange.okx_public_http_lib import fetch_index_px
px = fetch_index_px(uly)
if px and px > 0:
return float(px)
except Exception:
pass
u = (uly or "ETH").strip().upper() or "ETH"
candidates = [u] if "-" in u else [f"{u}-USD", f"{u}-USDT"]
for inst in candidates:
try:
rows = ex.public_get_market_index_tickers({"instId": inst}).get("data") or []
if rows:
px = _safe_float(rows[0].get("idxPx"))
if px and px > 0:
return px
except Exception:
continue
try:
rows = (ex.public_get_market_ticker({"instId": f"{u.split('-')[0]}-USDT-SWAP"}) or {}).get(
"data"
) or []
if rows:
return _safe_float(rows[0].get("idxPx"))
px = _safe_float(rows[0].get("markPx")) or _safe_float(rows[0].get("last"))
if px and px > 0:
return px
except Exception:
pass
return None
@@ -755,6 +778,16 @@ def fetch_option_instruments(
):
return list(entry["rows"])
last_err: Exception | None = None
try:
from lib.exchange.okx_public_http_lib import fetch_option_instruments_public
live = fetch_option_instruments_public(family)
with _OPTION_INSTRUMENTS_CACHE_LOCK:
_OPTION_INSTRUMENTS_CACHE[family] = {"updated_at": now, "rows": live}
return list(live)
except Exception as e:
last_err = e
try:
rows = ex.public_get_public_instruments(
{"instType": "OPTION", "instFamily": family}
@@ -764,6 +797,7 @@ def fetch_option_instruments(
_OPTION_INSTRUMENTS_CACHE[family] = {"updated_at": now, "rows": live}
return list(live)
except Exception as e:
last_err = e
if allow_stale:
with _OPTION_INSTRUMENTS_CACHE_LOCK:
entry = _OPTION_INSTRUMENTS_CACHE.get(family)
@@ -771,11 +805,19 @@ def fetch_option_instruments(
age = now - float(entry.get("updated_at") or 0)
if age <= _OPTION_INSTRUMENTS_STALE_MAX:
return list(entry["rows"])
raise
raise last_err
def fetch_option_tickers(ex: ccxt.okx, inst_family: str) -> dict[str, dict[str, Any]]:
out: dict[str, dict[str, Any]] = {}
try:
from lib.exchange.okx_public_http_lib import fetch_option_tickers_public
got = fetch_option_tickers_public(inst_family)
if got:
return got
except Exception:
pass
try:
rows = ex.public_get_market_tickers(
{"instType": "OPTION", "instFamily": inst_family}
+167
View File
@@ -0,0 +1,167 @@
"""OKX 公共 REST 轻量拉取(绕开 ccxt load_markets).
部分线路上 Python requests 偶发连 www.okx.com 超时,而 curl -4 仍可用;
对冲页标记价/指数优先走本模块,避免整页空白.
"""
from __future__ import annotations
import json
import subprocess
import urllib.parse
import urllib.request
from typing import Any, Optional
OKX_BASE = "https://www.okx.com"
def _curl_get_json(url: str, *, timeout_sec: float = 8.0) -> dict[str, Any]:
proc = subprocess.run(
[
"curl",
"-4",
"-fsS",
"--connect-timeout",
str(max(1, int(timeout_sec))),
"--max-time",
str(max(2, int(timeout_sec + 2))),
url,
],
capture_output=True,
text=True,
timeout=max(3.0, timeout_sec + 3.0),
check=False,
)
if proc.returncode != 0:
raise RuntimeError((proc.stderr or proc.stdout or "curl failed").strip()[:240])
return json.loads(proc.stdout or "{}")
def _urllib_get_json(url: str, *, timeout_sec: float = 8.0) -> dict[str, Any]:
req = urllib.request.Request(url, headers={"User-Agent": "CoinOX/1.0"})
with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
raw = resp.read().decode("utf-8", errors="replace")
return json.loads(raw or "{}")
def okx_public_get(
path: str,
params: Optional[dict[str, Any]] = None,
*,
timeout_sec: float = 8.0,
) -> dict[str, Any]:
q = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v is not None})
url = f"{OKX_BASE}{path}"
if q:
url = f"{url}?{q}"
last_err: Exception | None = None
for getter in (_curl_get_json, _urllib_get_json):
try:
data = getter(url, timeout_sec=timeout_sec)
if isinstance(data, dict):
return data
except Exception as e:
last_err = e
continue
raise RuntimeError(str(last_err or "okx public get failed"))
def _sf(v: Any) -> Optional[float]:
if v is None or v == "":
return None
try:
x = float(v)
except (TypeError, ValueError):
return None
return x if x > 0 else None
def fetch_swap_mark_px(base: str) -> Optional[float]:
u = (base or "ETH").strip().upper() or "ETH"
try:
payload = okx_public_get(
"/api/v5/public/mark-price",
{"instId": f"{u}-USDT-SWAP"},
timeout_sec=8.0,
)
rows = payload.get("data") or []
if rows:
return _sf(rows[0].get("markPx"))
except Exception:
pass
return None
def fetch_swap_ticker(base: str) -> dict[str, Any]:
"""返回 ETH/BTC USDT 永续公共 ticker 行(补齐 markPx)."""
u = (base or "ETH").strip().upper() or "ETH"
payload = okx_public_get(
"/api/v5/market/ticker",
{"instId": f"{u}-USDT-SWAP"},
timeout_sec=8.0,
)
rows = payload.get("data") or []
if not rows:
raise RuntimeError(payload.get("msg") or "ticker empty")
row = dict(rows[0])
if not _sf(row.get("markPx")):
mp = fetch_swap_mark_px(u)
if mp:
row["markPx"] = str(mp)
elif _sf(row.get("last")):
row["markPx"] = row.get("last")
return row
def fetch_index_px(uly: str) -> Optional[float]:
u = (uly or "ETH").strip().upper() or "ETH"
candidates = [u] if "-" in u else [f"{u}-USD", f"{u}-USDT"]
for inst in candidates:
try:
payload = okx_public_get(
"/api/v5/market/index-tickers",
{"instId": inst},
timeout_sec=8.0,
)
rows = payload.get("data") or []
if not rows:
continue
v = _sf(rows[0].get("idxPx"))
if v:
return v
except Exception:
continue
try:
row = fetch_swap_ticker(u.split("-")[0])
return _sf(row.get("markPx")) or _sf(row.get("last"))
except Exception:
return None
def fetch_option_instruments_public(inst_family: str) -> list[dict[str, Any]]:
family = str(inst_family or "").strip()
if not family:
return []
payload = okx_public_get(
"/api/v5/public/instruments",
{"instType": "OPTION", "instFamily": family},
timeout_sec=12.0,
)
rows = payload.get("data") or []
return [r for r in rows if isinstance(r, dict) and r.get("state") == "live"]
def fetch_option_tickers_public(inst_family: str) -> dict[str, dict[str, Any]]:
family = str(inst_family or "").strip()
if not family:
return {}
payload = okx_public_get(
"/api/v5/market/tickers",
{"instType": "OPTION", "instFamily": family},
timeout_sec=12.0,
)
out: dict[str, dict[str, Any]] = {}
for r in payload.get("data") or []:
if isinstance(r, dict) and r.get("instId"):
out[str(r["instId"])] = r
return out
+35 -70
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import os
import time
from typing import Any, Optional
from flask import Flask, jsonify, request
@@ -10,16 +11,10 @@ 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,
)
from lib.trade.position_sizing_lib import load_position_sizing_mode
def _env_bool(key: str, default: bool = False) -> bool:
@@ -1311,15 +1306,8 @@ def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any], str | None]:
"""对冲页行情:只走公共 HTTP/curl,绝不触发 ccxt load_markets(弱网会卡死整页)."""
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):
@@ -1327,67 +1315,43 @@ def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any],
sym = norm(f"{base}/USDT")
except Exception:
sym = f"{base}/USDT:USDT"
mark = bid = ask = last = None
mark = bid = ask = last = index_px = None
last_err: Exception | None = 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"))
from lib.exchange.okx_public_http_lib import fetch_index_px, fetch_swap_ticker
row = fetch_swap_ticker(base)
last = _sf(row.get("last"))
mark = _sf(row.get("markPx")) or last
bid = _sf(row.get("bidPx"))
ask = _sf(row.get("askPx"))
index_px = fetch_index_px(base)
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
last_err = e
if ex is not None:
try:
inst = f"{base}-USDT-SWAP"
rows = (ex.public_get_market_ticker({"instId": inst}) or {}).get("data") or []
row = rows[0] if rows else {}
last = _sf(row.get("last"))
mark = _sf(row.get("markPx")) or last
bid = _sf(row.get("bidPx"))
ask = _sf(row.get("askPx"))
except Exception as e2:
last_err = e2
if last is None and mark is None:
return {}, f"拉永续行情失败: {last_err or '无报价'}"
if index_px is None:
index_px = mark or last
# 面值/可用/精度:展示用默认值,避免 get_contract_size→load_markets 阻塞
cs = 0.1 if base == "ETH" else 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)
amount_precision = 2 if base == "ETH" else 4
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)
entry = float(mark or last or 0)
return {
"exchange_symbol": sym,
@@ -1395,6 +1359,7 @@ def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any],
"last": last,
"bid": bid,
"ask": ask,
"index_px": index_px,
"contract_size": cs,
"available_usdt": available,
"full_margin_sizing": sizing,