实例增加实盘说明页(API/账户模式/env),系统设置可关闭导航显示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-04 10:07:05 +08:00
parent 740c99e910
commit 4bea770f5c
13 changed files with 282 additions and 2 deletions
+9
View File
@@ -5680,6 +5680,15 @@ def system_guide_page():
return render_main_page("system_guide")
@app.route("/live_guide")
@login_required
def live_guide_page():
redir = redirect_to_embed_shell_if_enabled("live_guide")
if redir is not None:
return redir
return render_main_page("live_guide")
@app.route("/env_config")
@login_required
def env_config_page():
+84
View File
@@ -0,0 +1,84 @@
# 交易所实盘说明(OKX)
本页说明如何把本实例从模拟盘切到真实交易:API 权限、OKX 账户模式、以及实例 `.env` 关键项。币本位期权开仓会先用 USDT 买 ETH/BTC,再开期权,因此 **现货交易权限** 与 **合约模式** 缺一不可。
## 一、创建 OKX API Key
1. 登录 OKX → **个人中心 → API** → 创建 API Key。
2. 权限务必勾选 **交易(Trade)**。币本位开仓会走现货买币,只开合约/期权权限不够。
3. 若有细分权限,请同时打开 **现货(Spot)**。
4. 建议开启 **IP 白名单**,填入本实例服务器出口 IP。
5. 保存后得到:`API Key` / `Secret Key` / `Passphrase`,填入本实例 env(见下文)。
常见报错:
| 现象 | 原因 | 处理 |
| --- | --- | --- |
| `50123` This API Key does not have trading permission for the Crypto | Key 无现货/交易权限 | 编辑 Key,勾选交易与现货后重试 |
| `未授权` / Bridge 探测失败 | 中控 Token 或客户端未部署接口 | 核对实例 Bridge Token;客户端需已部署 `/api/hub/version` |
## 二、OKX 账户模式
交易页 → **账户模式**(首次切换须在网页/App 手动确认)。
| 账户模式 | 是否推荐 | 说明 |
| --- | --- | --- |
| 仅现货 / 简单模式 | 否 | 无法正常开期权 |
| **合约模式(现货和合约)** | **推荐** | 现货用 `cash`,与当前程序一致 |
| 跨币种保证金 | 可用但不推荐 | 现货须 `cross`,程序目前按合约模式写 `cash`,会报 `tdMode` 错 |
| 组合保证金 | 可用但不推荐 | 同上,现货也须 `cross` |
推荐流程:先切到 **合约模式** → 再在本实例点「买币并开仓」。
相关报错:
| 现象 | 原因 | 处理 |
| --- | --- | --- |
| You can't complete this request under your current account mode (`51010`) | 账户模式不支持本次下单 | 改为合约模式 |
| Parameter tdMode error | 跨币种/组合下仍用 `cash` 下现货 | 改回合约模式,或等待程序支持按模式自动选 `tdMode` |
## 三、实例 env 配置
在顶栏 **env配置**(或改服务器 `.env` 后重启)中设置:
### 1. 实盘开关与密钥
- `LIVE_TRADING_ENABLED=true`:允许实盘下单。
- `OKX_API_KEY` / `OKX_API_SECRET` / `OKX_API_PASSPHRASE`:上一步创建的密钥(期权与永续共用,不必单独配期权 Key)。
- `SIM_DEFAULT_MODE=live`:运行模式切到实盘(顶栏显示 `okx_live`)。也可在运行时由系统把 `trading.mode` 写成 `live`。
切到实盘后:
- 顶栏「交易账户 / 总资金」只显示交易所真实余额;未充值时应为 `—` 或 `0`,不会再残留模拟盘数字。
- 模拟钱包余额不会带到实盘。
### 2. 期权相关(币本位)
- `OKX_OPTIONS_ENABLED=true`:启用期权模块。
- `OKX_OPTIONS_MARGIN_MODE=coin`:币本位(用 ETH/BTC 付权利金)。
- `OKX_OPTIONS_MAX_ACTIVE_POSITIONS`:期权持仓上限(笔);`0`=不限制;`1`=同时最多 1 个合约(同合约加仓不占新笔数)。
- `OKX_OPTIONS_SPOT_BUY_BUFFER`:现货买入相对权利金的缓冲倍数(如 `1.05`)。
- 目标平仓门控:`OKX_OPTIONS_CLOSE_GATE_MODE`、`OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN`、`OKX_OPTIONS_CLOSE_HOLD_SECONDS` 等(见 env 页「期权账户」分组)。
保存后按提示 **保存并重启**(或 PM2 重启),再刷新页面确认顶栏为 `okx_live`。
## 四、开仓前自检清单
1. OKX 账户模式 = **合约模式**。
2. API Key 有 **交易 + 现货**,IP 白名单正确。
3. `.env`:`LIVE_TRADING_ENABLED=true`,密钥已填,模式为 `live`。
4. 交易账户有足够 **USDT**(买币)以及开期权所需的标的币/保证金。
5. 期权持仓上限、全仓复利等规则符合预期。
## 五、翻倍离场与指数离场(实盘同样生效)
后台监控轮询实盘持仓,达标后按买一限价平仓:
- **翻倍离场**:买一可回收 ≥ 开仓权利金 × (1 + 倍数) 时平仓。
- **指数离场**:标的指数到位后触发,但仍须通过「目标平仓门控」(权利金倍数持续若干秒等)。
开仓时勾选/填写对应出场条件,并保证实例进程在跑。
## 六、中控远程更新注意
中控「系统更新 → 客户端远程更新」依赖实例的 `/api/hub/version` 与 `/api/hub/update`。若版本列报「非 JSON / 404」,请先在该实例服务器手动 `git pull` 并重启一次,再在中控刷新版本。
+2
View File
@@ -16,6 +16,7 @@
records: "/records",
stats: "/stats",
risk_policy: "/risk_policy",
live_guide: "/live_guide",
system_guide: "/system_guide",
env_config: "/env_config",
settings: "/settings",
@@ -361,6 +362,7 @@
const hideTopBar =
tab === "settings" ||
tab === "risk_policy" ||
tab === "live_guide" ||
tab === "system_guide" ||
tab === "env_config" ||
tab === "options_review";
+1
View File
@@ -102,6 +102,7 @@
records: "/records",
stats: "/stats",
risk_policy: "/risk_policy",
live_guide: "/live_guide",
system_guide: "/system_guide",
env_config: "/env_config",
settings: "/settings",
@@ -47,6 +47,7 @@
"options-review": "show_nav_options_review",
options_review: "show_nav_options_review",
risk_policy: "show_nav_risk_policy",
live_guide: "show_nav_live_guide",
system_guide: "show_nav_system_guide",
env_config: "show_nav_env_config",
};
@@ -87,6 +88,7 @@
"options-review": "show_nav_options_review",
options_review: "show_nav_options_review",
risk_policy: "show_nav_risk_policy",
live_guide: "show_nav_live_guide",
system_guide: "show_nav_system_guide",
env_config: "show_nav_env_config",
};
@@ -17,6 +17,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
"show_nav_records": False, # 永续交易记录与复盘已移除
"show_nav_stats": False,
"show_nav_risk_policy": True,
"show_nav_live_guide": True,
"show_nav_system_guide": False,
"show_nav_env_config": True,
"show_nav_options": True,
@@ -38,6 +39,7 @@ DISPLAY_LABELS: dict[str, str] = {
"show_nav_records": "交易记录与复盘",
"show_nav_stats": "统计分析",
"show_nav_risk_policy": "风控说明",
"show_nav_live_guide": "实盘说明",
"show_nav_system_guide": "系统说明",
"show_nav_env_config": "env配置",
"show_nav_options": "期权",
@@ -59,6 +61,7 @@ NAV_TAB_ALLOWED: dict[str, str] = {
"records": "show_nav_records",
"stats": "show_nav_stats",
"risk_policy": "show_nav_risk_policy",
"live_guide": "show_nav_live_guide",
"system_guide": "show_nav_system_guide",
"env_config": "show_nav_env_config",
"options": "show_nav_options",
@@ -122,6 +125,7 @@ def display_meta_for_ui() -> list[dict[str, Any]]:
"show_nav_account_ledger",
"show_nav_key_monitor",
"show_nav_risk_policy",
"show_nav_live_guide",
"show_nav_system_guide",
"show_nav_env_config",
"show_nav_options",
+2
View File
@@ -17,6 +17,7 @@ EMBED_TABS: tuple[str, ...] = (
"options",
"options_review",
"risk_policy",
"live_guide",
"system_guide",
"env_config",
"settings",
@@ -34,6 +35,7 @@ PATH_TO_EMBED_TAB: dict[str, str] = {
"/options/review": "options_review",
"/hedge-plan": "options", # 对冲计划已移除,旧链接落到期权
"/risk_policy": "risk_policy",
"/live_guide": "live_guide",
"/system_guide": "system_guide",
"/env_config": "env_config",
"/settings": "settings",
+68
View File
@@ -0,0 +1,68 @@
"""实例「实盘说明」:加载 Markdown,生成 h2 目录与带锚点正文."""
from __future__ import annotations
import re
from functools import lru_cache
from html import escape
from pathlib import Path
from typing import Any
from lib.common.markdown_html_lib import render_markdown_html
from lib.paths import REPO_ROOT
def live_guide_md_path() -> Path:
return REPO_ROOT / "docs" / "实盘说明.md"
def _slugify(text: str) -> str:
raw = re.sub(r"<[^>]+>", "", text or "")
raw = re.sub(r"\s+", "-", raw.strip())
raw = re.sub(r"[^\w\u4e00-\u9fff\-]+", "", raw)
return raw[:80] or "section"
def _inject_h2_ids(html: str) -> tuple[str, list[dict[str, str]]]:
toc: list[dict[str, str]] = []
used: dict[str, int] = {}
def repl(m: re.Match[str]) -> str:
inner = m.group(1)
base = _slugify(inner)
n = used.get(base, 0) + 1
used[base] = n
hid = base if n == 1 else f"{base}-{n}"
toc.append({"id": hid, "title": re.sub(r"<[^>]+>", "", inner).strip()})
return f'<h2 id="{escape(hid)}">{inner}</h2>'
out = re.sub(r"<h2>(.*?)</h2>", repl, html, flags=re.I | re.S)
return out, toc
@lru_cache(maxsize=4)
def _load_payload_cached(mtime_ns: int, path_str: str) -> dict[str, Any]:
path = Path(path_str)
try:
md_text = path.read_text(encoding="utf-8")
except OSError:
md_text = "# 实盘说明缺失\n\n未找到 `docs/实盘说明.md`。"
body = render_markdown_html(md_text)
body, toc = _inject_h2_ids(body)
return {"html": body, "toc": toc, "mtime_ns": mtime_ns}
def load_live_guide_payload() -> dict[str, Any]:
path = live_guide_md_path()
try:
mtime_ns = path.stat().st_mtime_ns
except OSError:
mtime_ns = 0
return dict(_load_payload_cached(mtime_ns, str(path)))
def live_guide_template_context() -> dict[str, Any]:
payload = load_live_guide_payload()
return {
"live_guide_html": payload.get("html") or "",
"live_guide_toc": payload.get("toc") or [],
}
+4
View File
@@ -153,6 +153,10 @@ def settings_page_context(page: str, *, instance_base_dir: str | None = None, **
from lib.instance.instance_system_guide_lib import system_guide_template_context
return system_guide_template_context()
if p == "live_guide":
from lib.instance.instance_live_guide_lib import live_guide_template_context
return live_guide_template_context()
if p not in ("settings", "risk_policy", "env_config"):
return {}
display = kwargs.pop("display", None)
@@ -143,6 +143,10 @@
{% include 'system_guide_panel.html' %}
{% endif %}
{% if page == 'live_guide' %}
{% include 'live_guide_panel.html' %}
{% endif %}
{% if page == 'settings' %}
{% include 'settings_panel.html' %}
{% endif %}
+7 -1
View File
@@ -52,6 +52,9 @@
{% if display.show_nav_risk_policy %}
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if initial_tab == 'risk_policy' %}active{% endif %}">风控说明</a>
{% endif %}
{% if display.get('show_nav_live_guide', true) %}
<a href="/live_guide" data-embed-tab="live_guide" class="{% if initial_tab == 'live_guide' %}active{% endif %}">实盘说明</a>
{% endif %}
<a href="/system_guide" data-embed-tab="system_guide" class="{% if initial_tab == 'system_guide' %}active{% endif %}"{% if not display.show_nav_system_guide %} style="display:none"{% endif %}>系统说明</a>
{% if display.show_nav_env_config %}
<a href="/env_config" data-embed-tab="env_config" class="{% if initial_tab == 'env_config' %}active{% endif %}">env配置</a>
@@ -63,7 +66,7 @@
<div id="embed-flash" class="flash" style="display:none" role="status"></div>
{% include 'instance_header_panel.html' %}
{% if initial_tab not in ('settings', 'risk_policy', 'system_guide', 'env_config', 'options_review') and include_transfer_block %}
{% if initial_tab not in ('settings', 'risk_policy', 'live_guide', 'system_guide', 'env_config', 'options_review') and include_transfer_block %}
{% include 'instance_top_bar.html' %}
{% endif %}
@@ -113,6 +116,9 @@
{% if display.show_nav_risk_policy %}
<a href="/risk_policy" data-embed-tab="risk_policy">风控说明</a>
{% endif %}
{% if display.get('show_nav_live_guide', true) %}
<a href="/live_guide" data-embed-tab="live_guide">实盘说明</a>
{% endif %}
<a href="/system_guide" data-embed-tab="system_guide"{% if not display.show_nav_system_guide %} style="display:none"{% endif %}>系统说明</a>
{% if display.show_nav_env_config %}
<a href="/env_config" data-embed-tab="env_config">env配置</a>
+8 -1
View File
@@ -180,6 +180,9 @@
{% if display.show_nav_risk_policy %}
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if page == 'risk_policy' %}active{% endif %}">风控说明</a>
{% endif %}
{% if display.get('show_nav_live_guide', true) %}
<a href="/live_guide" data-embed-tab="live_guide" class="{% if page == 'live_guide' %}active{% endif %}">实盘说明</a>
{% endif %}
<a href="/system_guide" data-embed-tab="system_guide" class="{% if page == 'system_guide' %}active{% endif %}"{% if not display.show_nav_system_guide %} style="display:none"{% endif %}>系统说明</a>
{% if display.show_nav_env_config %}
<a href="/env_config" data-embed-tab="env_config" class="{% if page == 'env_config' %}active{% endif %}">env配置</a>
@@ -191,7 +194,7 @@
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
{% include 'instance_header_panel.html' %}
{% if page not in ('settings', 'risk_policy', 'system_guide', 'env_config', 'options', 'options_review') %}
{% if page not in ('settings', 'risk_policy', 'live_guide', 'system_guide', 'env_config', 'options', 'options_review') %}
{% include 'instance_top_bar.html' %}
{% endif %}
@@ -385,6 +388,10 @@
{% include 'system_guide_panel.html' %}
{% endif %}
{% if page == 'live_guide' %}
{% include 'live_guide_panel.html' %}
{% endif %}
{% if page == 'settings' %}
{% include 'settings_panel.html' %}
{% endif %}
@@ -0,0 +1,87 @@
{# 实盘说明: docs/实盘说明.md + h2 目录 #}
<div class="system-guide-page full">
<div class="card system-guide-card">
<div class="system-guide-head">
<h2 style="margin:0">实盘说明</h2>
<p class="muted" style="margin:6px 0 0;font-size:.85rem">OKX API、账户模式与实例 env。可在系统设置 → 导航显示中关闭顶栏入口。</p>
</div>
<div class="system-guide-layout">
{% if live_guide_toc %}
<aside class="system-guide-toc" aria-label="章节目录">
<div class="system-guide-toc-title">目录</div>
<nav>
{% for item in live_guide_toc %}
<a href="#{{ item.id }}">{{ item.title }}</a>
{% endfor %}
</nav>
</aside>
{% endif %}
<article class="system-guide-body prose">
{{ live_guide_html|safe }}
</article>
</div>
</div>
</div>
<style>
.system-guide-page { grid-column: 1 / -1; }
.system-guide-card { padding: 16px 18px 28px; }
.system-guide-layout {
display: grid;
grid-template-columns: minmax(160px, 220px) minmax(0, 1fr);
gap: 18px;
margin-top: 14px;
align-items: start;
}
.system-guide-toc {
position: sticky;
top: 8px;
padding: 10px 12px;
border: 1px solid rgba(127,127,127,.25);
border-radius: 8px;
background: rgba(127,127,127,.06);
max-height: calc(100vh - 120px);
overflow: auto;
}
.system-guide-toc-title {
font-size: .78rem;
font-weight: 600;
opacity: .75;
margin-bottom: 8px;
}
.system-guide-toc a {
display: block;
font-size: .84rem;
line-height: 1.35;
padding: 5px 0;
text-decoration: none;
color: inherit;
opacity: .9;
}
.system-guide-toc a:hover { opacity: 1; text-decoration: underline; }
.system-guide-body { min-width: 0; line-height: 1.65; font-size: .92rem; }
.system-guide-body h1 { font-size: 1.35rem; margin: 0 0 12px; }
.system-guide-body h2 { font-size: 1.12rem; margin: 22px 0 10px; padding-top: 4px; scroll-margin-top: 12px; }
.system-guide-body h3 { font-size: 1rem; margin: 16px 0 8px; }
.system-guide-body p, .system-guide-body li { margin: 0 0 8px; }
.system-guide-body ul, .system-guide-body ol { padding-left: 1.35em; margin: 0 0 10px; }
.system-guide-body table { border-collapse: collapse; width: 100%; margin: 10px 0 14px; font-size: .86rem; }
.system-guide-body th, .system-guide-body td {
border: 1px solid rgba(127,127,127,.35);
padding: 7px 9px;
text-align: left;
vertical-align: top;
}
.system-guide-body code {
font-family: ui-monospace, Consolas, monospace;
font-size: .86em;
padding: 1px 4px;
border-radius: 4px;
background: rgba(127,127,127,.12);
}
.system-guide-body hr { border: 0; border-top: 1px solid rgba(127,127,127,.28); margin: 18px 0; }
@media (max-width: 820px) {
.system-guide-layout { grid-template-columns: 1fr; }
.system-guide-toc { position: static; max-height: none; }
.system-guide-toc nav { display: flex; flex-wrap: wrap; gap: 4px 12px; }
}
</style>