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:
+18
-16
@@ -12,30 +12,36 @@
|
||||
# cp .env.backup.YYYYMMDD .env
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# 默认面向「云服务器测试」:公网监听 + 必须登录 + 模拟盘 + 直连 OKX(不配代理)
|
||||
# 局域网本机开发可将 APP_AUTH_DISABLED=true;弱网再开 OKX_*_PROXY
|
||||
# =============================================================================
|
||||
APP_ENV=production
|
||||
# 服务监听地址(云服务器通常用 0.0.0.0)
|
||||
# 服务监听地址(云服务器 / 宝塔反代后仍建议 0.0.0.0 或 127.0.0.1)
|
||||
# 仅本机 + Nginx 反代时可改为 127.0.0.1;默认 0.0.0.0 方便直连调试
|
||||
APP_HOST=0.0.0.0
|
||||
# 服务端口
|
||||
# 服务端口(宝塔反代到此端口,如 http://127.0.0.1:5010)
|
||||
APP_PORT=5010
|
||||
# 是否开启调试模式(生产建议 false)
|
||||
# 是否开启调试模式(生产 / 云上务必 false)
|
||||
APP_DEBUG=false
|
||||
|
||||
# 登录账号
|
||||
APP_USERNAME=admin
|
||||
# 登录密码(请改成你自己的强密码)
|
||||
# 登录密码(请改成你自己的强密码;首次部署脚本也可能写成 admin123)
|
||||
APP_PASSWORD=admin123
|
||||
# 是否关闭登录校验(局域网可设 true;公网务必 false)
|
||||
APP_AUTH_DISABLED=true
|
||||
# 是否关闭登录校验(云服务器 / 公网务必 false;仅信任局域网可 true)
|
||||
APP_AUTH_DISABLED=false
|
||||
# --- 多账户交易中控 manual_trading_hub ---
|
||||
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
|
||||
# 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true
|
||||
# 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问
|
||||
# HUB_BRIDGE_TOKEN=your-long-random-token
|
||||
# 允许复盘中控 iframe 内嵌本实例(与 hub 域名一致;默认已开启)
|
||||
# APP_ALLOW_HUB_EMBED=true
|
||||
# HUB_EMBED_PARENT_ORIGINS=https://hub.example.com
|
||||
# HTTPS 且经 iframe 打开时建议 true;不设则 hub-sso 在 HTTPS 下也会自动尝试 SameSite=None
|
||||
# 经宝塔 HTTPS 反代时建议打开:
|
||||
# APP_COOKIE_SECURE=true
|
||||
# Flask 会话密钥(必须替换为长随机字符串)
|
||||
# Flask 会话密钥(必须替换为长随机字符串;一键部署会自动生成)
|
||||
FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET
|
||||
|
||||
# 企业微信机器人 Webhook(用于行情/风控推送)
|
||||
@@ -301,13 +307,9 @@ OPENAI_MODEL=gemma4:e4b
|
||||
OLLAMA_API=http://127.0.0.1:11434/api/generate
|
||||
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
|
||||
|
||||
# OKX 代理(可选,仅本地开发网络受限时用;云服务器部署请留空,直连 OKX 即可)
|
||||
# 1) 先在本机建立隧道(示例):
|
||||
# ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes
|
||||
# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名):
|
||||
# OKX 代理(云服务器香港/新加坡等请留空,直连即可;不要填本机 Windows 代理地址)
|
||||
# 仅当云主机也访问不了 OKX 时再启用:
|
||||
# OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080
|
||||
#
|
||||
# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用:
|
||||
# OKX_HTTP_PROXY=http://127.0.0.1:3128
|
||||
# OKX_HTTPS_PROXY=http://127.0.0.1:3128
|
||||
|
||||
@@ -330,9 +332,9 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
|
||||
APP_TIMEZONE=Asia/Shanghai
|
||||
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
|
||||
|
||||
# --- CoinOX 模拟资金 ---
|
||||
# --- CoinOX 模拟资金(云上测试默认 sim,无需 OKX API) ---
|
||||
# sim=本地模拟资金撮合; live=实盘账户
|
||||
# 默认模拟盘(本地钱包); 切实盘请改为 live 并配置 API
|
||||
# 切实盘:填 OKX_API_* , LIVE_TRADING_ENABLED=true , SIM_DEFAULT_MODE=live , 并在 OKX 白名单加云主机出口 IP
|
||||
SIM_DEFAULT_MODE=sim
|
||||
SIM_INITIAL_EQUITY_USDT=10000
|
||||
SIM_INITIAL_USDC=0
|
||||
|
||||
@@ -439,7 +439,10 @@ from lib.trade.daily_open_limit_lib import (
|
||||
)
|
||||
|
||||
DAILY_OPEN_ALERT_THRESHOLD, DAILY_OPEN_HARD_LIMIT = load_daily_open_limits_from_env()
|
||||
RISK_PERCENT = float(os.getenv("RISK_PERCENT", "2"))
|
||||
try:
|
||||
RISK_PERCENT = float(os.getenv("RISK_PERCENT") or "2")
|
||||
except (TypeError, ValueError):
|
||||
RISK_PERCENT = 2.0
|
||||
BREAKEVEN_RR_TRIGGER = float(os.getenv("BREAKEVEN_RR_TRIGGER", "1.0"))
|
||||
BREAKEVEN_OFFSET_PCT = float(os.getenv("BREAKEVEN_OFFSET_PCT", "0.02"))
|
||||
BREAKEVEN_STEP_R = float(os.getenv("BREAKEVEN_STEP_R", "1.0"))
|
||||
@@ -500,10 +503,18 @@ os.makedirs(ORDER_CHART_DIR, exist_ok=True)
|
||||
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||
|
||||
# 同一套 OKX_API_*:swap 客户端跑永续,option 客户端跑期权(身份相同,defaultType 不同)
|
||||
exchange = ccxt.okx({
|
||||
# CoinOX: 不拉全量 SPOT instruments(国内线路易超时导致行情空白),仅 swap/option(+现货桥需要 spot)
|
||||
exchange = ccxt.okx(
|
||||
{
|
||||
"enableRateLimit": True,
|
||||
"options": {"defaultType": "swap"}, # OKX 用 swap 表示永续
|
||||
})
|
||||
"timeout": 20000,
|
||||
"options": {
|
||||
"defaultType": "swap",
|
||||
# 仅 swap:避免 SPOT 全量 instruments 在部分线路超时导致行情空白
|
||||
"fetchMarkets": ["swap"],
|
||||
},
|
||||
}
|
||||
)
|
||||
if OKX_CCXT_PROXIES:
|
||||
exchange.proxies = OKX_CCXT_PROXIES
|
||||
if OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE:
|
||||
@@ -514,7 +525,11 @@ if OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE:
|
||||
exchange_options = ccxt.okx(
|
||||
{
|
||||
"enableRateLimit": True,
|
||||
"options": {"defaultType": "option"},
|
||||
"timeout": 20000,
|
||||
"options": {
|
||||
"defaultType": "option",
|
||||
"fetchMarkets": ["option", "swap"],
|
||||
},
|
||||
}
|
||||
)
|
||||
if OKX_CCXT_PROXIES:
|
||||
@@ -2911,15 +2926,29 @@ def build_okx_order_params(direction, reduce_only=False):
|
||||
def ensure_markets_loaded(force=False):
|
||||
global MARKETS_LOADED
|
||||
if force or not MARKETS_LOADED:
|
||||
last_err = None
|
||||
for attempt in range(3):
|
||||
try:
|
||||
exchange.load_markets(reload=force)
|
||||
except Exception as e:
|
||||
if is_exchange_auth_error(e) and (getattr(exchange, "apiKey", None) or getattr(exchange, "secret", None)):
|
||||
_disable_private_api_after_auth_error(e)
|
||||
load_markets_public_fallback(exchange, reload=True)
|
||||
else:
|
||||
raise
|
||||
exchange.load_markets(reload=force or attempt > 0)
|
||||
MARKETS_LOADED = True
|
||||
return
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
if is_exchange_auth_error(e) and (
|
||||
getattr(exchange, "apiKey", None) or getattr(exchange, "secret", None)
|
||||
):
|
||||
_disable_private_api_after_auth_error(e)
|
||||
try:
|
||||
load_markets_public_fallback(exchange, reload=True)
|
||||
MARKETS_LOADED = True
|
||||
return
|
||||
except Exception as e2:
|
||||
last_err = e2
|
||||
# 瞬时超时重试
|
||||
if attempt < 2:
|
||||
time.sleep(0.4 * (attempt + 1))
|
||||
continue
|
||||
raise last_err
|
||||
|
||||
|
||||
def _okx_algo_trigger_price_str(exchange_symbol, price):
|
||||
|
||||
@@ -37,6 +37,7 @@ bash /opt/coinox/deploy/manage.sh
|
||||
- 访问: `http://<服务器IP>:5010` (端口以 `.env` 的 `APP_PORT` 为准)
|
||||
- 默认入口: `/hedge-plan`(对冲)/ `/options`(期权);默认模拟盘 sim
|
||||
- 登录账号: **admin**(密码见 `.env` 的 `APP_PASSWORD`,首次脚本可写成 `admin123`)
|
||||
- 云服务器默认: `APP_AUTH_DISABLED=false`、不配代理;宝塔反代见根目录 [部署文档.md](../部署文档.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -420,23 +420,25 @@ print_post_install_guide() {
|
||||
cat <<EOF
|
||||
|
||||
═══════════════════════════════════════════
|
||||
✅ CoinOX 部署完成(默认模拟盘 sim)
|
||||
✅ CoinOX 部署完成(云服务器测试默认)
|
||||
|
||||
访问: http://${ip}:${port}
|
||||
入口: /hedge-plan(对冲) /options(期权)
|
||||
|
||||
登录账号: admin
|
||||
登录密码: 见 .env 中 APP_PASSWORD(首次多为 admin123)
|
||||
(请尽快在系统设置 / env 配置中修改)
|
||||
鉴权: APP_AUTH_DISABLED=false(公网必须登录)
|
||||
|
||||
模式说明:
|
||||
- SIM_DEFAULT_MODE=sim(本地模拟资金,不打真实私有下单)
|
||||
- 实盘: 在页面/env 切 live,并配置 OKX_API_* 与 LIVE_TRADING_ENABLED
|
||||
- SIM_DEFAULT_MODE=sim(模拟资金,无需 OKX API)
|
||||
- 云主机请勿配置 OKX_*_PROXY(直连 OKX)
|
||||
- 宝塔反代: 目标 http://127.0.0.1:${port},对外只开 80/443
|
||||
- 实盘: 填 OKX_API_* + LIVE_TRADING_ENABLED=true + 切 live,并白名单出口 IP
|
||||
|
||||
下一步:
|
||||
1. 浏览器打开上方地址
|
||||
2. 模拟盘直接测期权 / 对冲计划
|
||||
3. 需要实盘时再填 API / 代理并切 live
|
||||
1. 浏览器打开上方地址并登录
|
||||
2. 模拟盘测期权 / 对冲计划
|
||||
3. 可选: 宝塔 Nginx 反代域名 + SSL
|
||||
|
||||
管理:
|
||||
bash ${INSTALL_ROOT}/deploy/manage.sh
|
||||
|
||||
+2
-1
@@ -207,7 +207,8 @@ fi
|
||||
|
||||
echo ""
|
||||
echo "部署完成.下一步:"
|
||||
echo " 1. 编辑 ${REPO_ROOT}/.env(默认已是模拟盘 sim;实盘再填 OKX API / 代理 / 密码)"
|
||||
echo " 1. 编辑 ${REPO_ROOT}/.env(云上默认: 模拟盘 sim + 必须登录 + 无代理;改 APP_PASSWORD)"
|
||||
echo " 2. pm2 start ecosystem.config.cjs"
|
||||
echo " 3. 可选: 宝塔 Nginx 反代到 http://127.0.0.1:5010(详见 部署文档.md)"
|
||||
echo " 或: bash deploy/manage.sh # 选 1"
|
||||
echo ""
|
||||
|
||||
@@ -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) +
|
||||
|
||||
@@ -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:
|
||||
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:
|
||||
return _safe_float(rows[0].get("idxPx"))
|
||||
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:
|
||||
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}
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
last_err = e
|
||||
if ex is not None:
|
||||
try:
|
||||
cs = float(get_cs(sym) or 0.01)
|
||||
except Exception:
|
||||
cs = 0.01
|
||||
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,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- FLASK_SECRET_KEY
|
||||
- APP_USERNAME=admin / APP_PASSWORD=admin123(仅空时)
|
||||
- HUB_BRIDGE_TOKEN(仅空时,供后期中控)
|
||||
- 强制默认模拟盘相关键(仅空/占位时)
|
||||
- 云服务器测试默认:监听/鉴权/模拟盘(仅空或占位时)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -57,7 +57,15 @@ def main() -> int:
|
||||
if not (env_get(lines, "HUB_BRIDGE_TOKEN") or "").strip():
|
||||
updates["HUB_BRIDGE_TOKEN"] = secrets.token_urlsafe(32)
|
||||
|
||||
# CoinOX: 默认模拟盘(仅当未配置或空时写入)
|
||||
# 云服务器测试默认(仅空时写入;不覆盖用户已改配置)
|
||||
if not (env_get(lines, "APP_HOST") or "").strip():
|
||||
updates["APP_HOST"] = "0.0.0.0"
|
||||
if not (env_get(lines, "APP_PORT") or "").strip():
|
||||
updates["APP_PORT"] = "5010"
|
||||
if not (env_get(lines, "APP_DEBUG") or "").strip():
|
||||
updates["APP_DEBUG"] = "false"
|
||||
if not (env_get(lines, "APP_AUTH_DISABLED") or "").strip():
|
||||
updates["APP_AUTH_DISABLED"] = "false"
|
||||
if not (env_get(lines, "SIM_DEFAULT_MODE") or "").strip():
|
||||
updates["SIM_DEFAULT_MODE"] = "sim"
|
||||
if not (env_get(lines, "OKX_OPTIONS_MARGIN_MODE") or "").strip():
|
||||
@@ -66,6 +74,8 @@ def main() -> int:
|
||||
updates["EXCHANGE_DISPLAY_NAME"] = "CoinOX"
|
||||
if not (env_get(lines, "OKX_OPTIONS_ENABLED") or "").strip():
|
||||
updates["OKX_OPTIONS_ENABLED"] = "true"
|
||||
if not (env_get(lines, "OKX_TRADE_MODE") or "").strip():
|
||||
updates["OKX_TRADE_MODE"] = "perp_options"
|
||||
live = (env_get(lines, "LIVE_TRADING_ENABLED") or "").strip().lower()
|
||||
if live in ("",):
|
||||
updates["LIVE_TRADING_ENABLED"] = "false"
|
||||
@@ -86,7 +96,7 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
apply_env_updates(env_path, updates)
|
||||
print(" 已写入初始密钥/账号与模拟盘默认(请尽快修改 APP_PASSWORD)")
|
||||
print(" 已写入云服务器测试默认(请尽快修改 APP_PASSWORD;公网保持 APP_AUTH_DISABLED=false)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -39,9 +39,52 @@ bash /opt/coinox/deploy/manage.sh
|
||||
- 访问:`http://<服务器IP>:5010`
|
||||
- 默认入口:`/hedge-plan`(对冲)、`/options`(期权)
|
||||
- 登录:`admin` / `.env` 中 `APP_PASSWORD`(首次多为 `admin123`)
|
||||
- **默认模拟盘**:本地钱包撮合,不打真实私有下单
|
||||
- **默认模拟盘**:本地钱包撮合,不打真实私有下单;**公网默认开启登录**(`APP_AUTH_DISABLED=false`)
|
||||
- **云服务器无需代理**:`.env` 里不要填 `OKX_HTTP_PROXY` / `OKX_SOCKS_PROXY`(香港/新加坡直连 OKX)
|
||||
|
||||
切实盘前请在 env / 系统设置中配置 `OKX_API_*`,并将交易模式切为 `live`,同时确认 `LIVE_TRADING_ENABLED`。
|
||||
切实盘前请在 env / 系统设置中配置 `OKX_API_*`,并将交易模式切为 `live`,同时确认 `LIVE_TRADING_ENABLED`,并在 OKX API 白名单加入云主机出口 IP。
|
||||
|
||||
---
|
||||
|
||||
## 云服务器 + 宝塔 Nginx 反代(推荐测试形态)
|
||||
|
||||
适合:1G 内存云主机,宝塔**只装 Nginx**,CoinOX 用 PM2 监听 `127.0.0.1:5010` 或 `0.0.0.0:5010`。
|
||||
|
||||
1. 按上文一键部署 CoinOX(`/opt/coinox`,端口 `5010`)
|
||||
2. 宝塔 → 网站 → 添加站点(域名或服务器 IP)
|
||||
3. 设置 → 反向代理:
|
||||
- 目标 URL:`http://127.0.0.1:5010`
|
||||
- 发送域名:`$host`
|
||||
4. (可选)申请 SSL,开启强制 HTTPS;此时建议在 `.env` 设 `APP_COOKIE_SECURE=true` 后 `pm2 restart coinox --update-env`
|
||||
5. 安全组/防火墙:对外只放行 **80/443**(不必对公网暴露 5010)
|
||||
6. 登录后改掉 `APP_PASSWORD`;保持 `APP_AUTH_DISABLED=false`
|
||||
|
||||
Nginx 反代示例(宝塔「配置文件」里亦可手改):
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5010;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
```
|
||||
|
||||
云上测试最小 `.env` 要点:
|
||||
|
||||
| 键 | 建议值 |
|
||||
|----|--------|
|
||||
| `APP_HOST` | `0.0.0.0`(或仅反代时用 `127.0.0.1`) |
|
||||
| `APP_PORT` | `5010` |
|
||||
| `APP_AUTH_DISABLED` | `false` |
|
||||
| `SIM_DEFAULT_MODE` | `sim` |
|
||||
| `LIVE_TRADING_ENABLED` | `false` |
|
||||
| `OKX_*_PROXY` | 留空 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user