Add instance system guide nav (default off) with overview/options/hedge manual.

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-19 09:23:30 +08:00
parent eea4d4ff8f
commit 5e0ce43415
18 changed files with 449 additions and 5 deletions
+9
View File
@@ -7482,6 +7482,15 @@ def risk_policy_page():
return render_main_page("risk_policy")
@app.route("/system_guide")
@login_required
def system_guide_page():
redir = redirect_to_embed_shell_if_enabled("system_guide")
if redir is not None:
return redir
return render_main_page("system_guide")
@app.route("/env_config")
@login_required
def env_config_page():
+9
View File
@@ -7269,6 +7269,15 @@ def risk_policy_page():
return render_main_page("risk_policy")
@app.route("/system_guide")
@login_required
def system_guide_page():
redir = redirect_to_embed_shell_if_enabled("system_guide")
if redir is not None:
return redir
return render_main_page("system_guide")
@app.route("/env_config")
@login_required
def env_config_page():
+9
View File
@@ -6899,6 +6899,15 @@ def risk_policy_page():
return render_main_page("risk_policy")
@app.route("/system_guide")
@login_required
def system_guide_page():
redir = redirect_to_embed_shell_if_enabled("system_guide")
if redir is not None:
return redir
return render_main_page("system_guide")
@app.route("/env_config")
@login_required
def env_config_page():
+197
View File
@@ -0,0 +1,197 @@
# 系统说明(实例操作与逻辑手册)
本文是实例侧的**详细说明书**:既写「点哪里、先做什么」,也写「为什么这样设计、钱怎么算、门禁如何拦」。
默认不在顶栏显示;需要时到 **系统设置 → 导航显示** 打开「系统说明」。
第一版聚焦:**总览 · 期权 · 对冲计划**。策略交易 / 复盘 / 风控细则后续可继续补章节。
---
## 一、总览:账户、资金与一天怎么用
### 1.1 两套账户(OKX
| 账户 | 典型用途 | 界面相关 |
|------|----------|----------|
| **合约账户** | 永续开仓、止盈止损 | 策略交易、永期对冲的永续腿 |
| **期权账户** | 买期权、期期双腿、权利金结算(多为 USDC) | 期权页、对冲计划期权腿 |
逻辑要点:对冲计划里「永续腿 → 合约账户」「期权腿 → 期权账户」。资金不够时,要先划转,再开仓。
### 1.2 资金流(操作顺序)
1. 确认合约可用 USDT、期权交易账户 USDC 是否够用。
2. 期期 / 单独开期权:常在期权页或期期卡片做 **资金 ↔ 交易** USDC 划转。
3. 永期:合约侧按全仓建议张数;期权侧再买保险腿。
4. 开仓后到对应页看持仓与监控状态;结束后看复盘 / 统计。
### 1.3 推荐使用节奏
| 场景 | 建议路径 |
|------|----------|
| 只做方向 + 保险 | **对冲计划 → 永期**(测算 → 启动) |
| 只做上下突破双买 | **对冲计划 → 期期** |
| 单独买一张期权并挂目标 | **期权** 页开仓 + 目标监控 |
| 看说明 / 改开关 | **系统说明** / **env 配置** / **系统设置** |
### 1.4 互斥与门禁(总原则)
- **对冲与期权互斥门控**(默认开):有进行中对冲计划时,不能再「单独开期权」;账户里已有「纯期权」持仓时,不能启动对冲计划。关掉后两边可同时开。
- **半腿失败改手动补开**(默认开):启动对冲时一腿成功、一腿失败 → **不自动平**已成腿,计划挂「半腿待补」,在「进行中」点补开。此时「半腿失败自动平期权」强制无效(避免买卖价差必亏)。
- 更多开关见 **env 配置 → 对冲计划**
---
## 二、期权模块
### 2.1 这块干什么
在期权账户上:**选合约 → 按卖一限价买入 → 持仓监控 → 买一平仓 / 目标到位平仓**。
也是对冲计划期权腿的共用能力。
### 2.2 操作:开仓
1. 打开 **期权**,选标的(ETH/BTC)、到期日、Call/Put。
2. 看清 **卖一价与深度**:无真实卖一深度时系统禁止开仓(链上带 `~` 的是参考估算,不能当真开仓价)。
3. 选张数 / 预算模式后下单。
4. 可选填写 **目标指数位**:到位后由目标监控按买一挂平(与对冲计划托管的目标不是同一套执行器)。
### 2.3 操作:平仓与目标
- **买一平仓**:按当前买一深度估算可回收金额与净盈亏;注意买卖价差,权利金一侧常见较大滑点。
- **目标监控**:手动委托的目标写在期权目标表;**期期对冲**的目标由对冲监控执行,持仓卡上会显示「由对冲计划监控」。
- 门控示例:可回收 < 权利金×2 时,目标平仓门控可能未过(保护「太亏别乱平」类规则,以页面提示为准)。
### 2.4 逻辑:持仓来源
持仓卡上的 **持仓来源** 表示这条仓和哪类计划绑定:
| 来源 | 含义 |
|------|------|
| 纯期权 | 未挂在进行中对冲计划腿上(含手动开、或计划已结束仍留着的仓) |
| 永期对冲 #N | 属于进行中永期计划的保险腿 |
| 期期对冲 #N | 属于进行中期期计划的腿 |
判定依据:数据库里进行中计划的 `open` 腿合约 ID。来源会影响互斥门控(「纯期权」会挡住新对冲启动)。
### 2.5 逻辑:盈亏怎么看
- **权利金**:买入成本(USDC)。
- **按买盘回收**:按当前买一深度卖掉大约能拿回多少。
- **净盈亏 ≈ 回收 − 权利金**(页面以买一回收为准,不是单纯看标记价浮动)。
- **到期平衡 / 平掉回本**:帮助判断「拿到到期」与「现在平掉」的盈亏分界,属于情景参考。
### 2.6 更多细则
期权开平仓字段级说明仍可打开独立页:[期权开平仓与监控说明](/options/guide)。
---
## 三、对冲计划
### 3.1 这块干什么
把「永续 + 期权」或「期权 + 期权」做成**可测算、可下单、可监控**的计划,与普通交易记录分开。
| 类型 | 组成 | 核心逻辑 |
|------|------|----------|
| **永期** | 合约账户永续 + 期权账户保险腿 | 全仓做方向;止盈/止损按规则处理期权 |
| **期期** | 期权账户两腿买方 | 上破/下破目标;盈利腿先平,残腿按模式处理 |
### 3.2 操作:永期
1. 选 ETH/BTC、做多/做空;看标记价与全仓建议张数。
2. 填开仓价、止盈、止损、张数;右侧选期权腿(列表)。
3.**计算** → 弹窗看情景测算 → **启动计划** 或取消。
4. 启动后在 **进行中的计划** 看状态;细节可点「成交细节」。
逻辑摘要:
- 永期开仓通常要求 **全仓计仓** + 实盘与对冲真实下单门禁。
- 止盈后是否强平期权、止损后是否强平期权,由 env 开关控制(止损强平默认开,止盈强平默认关)。
- 统计口径:止盈多为「永续盈利 − 权利金」;止损多为「期权盈亏 + 永续盈亏」有符号相加(以系统结案字段为准)。
### 3.3 操作:期期
1. 填上破 / 下破目标;指数价作参考。
2. 张数模式:**同张数**(默认)或 **均分预算**;平仓模式:**全平**(默认)或 **到期平**(若 env 打开方案 C)。
3. T 型报价选用两腿;可先划转 USDC。
4. **计算** → 情景测算 → **启动计划**
逻辑摘要:
- 达目标价:通常只平盈利腿。
- **全平**:盈利腿平掉后立刻尝试清另一腿(无 2× 权利金门控,失败会重试)。
- **到期平**:残腿持有至到期再结。
- 旧计划若无平仓模式字段,按「到期平」更安全的口径处理。
### 3.4 半腿失败与手动补开
启动时两腿要连续下单。若一腿成功、一腿失败:
| 配置 | 行为 |
|------|------|
| **半腿失败改手动补开 = 开**(默认) | 已成腿留下;计划状态 **半腿待补**;「进行中」出现 **补开永续 / 补开腿B / 补开期权****不会**自动买一平已成腿 |
| 手动补开 = 关,且自动平 = 开 | 尝试自动平掉已成期权腿(会吃买卖价差,几乎必亏一笔) |
操作建议:半腿出现后,先看失败原因(深度、余额、权限),再点补开;确认补开会真实下单。
### 3.5 情景测算弹窗
测算不再占页面下方大块区域:点 **计算** 弹出结果,底部 **启动计划 / 取消**
取消只关窗;启动按当前参数真实下单(仍受门禁约束)。
### 3.6 进行中 / 历史 / 统计
- **进行中**:含 `opening` / `active` / `partial`。半腿待补可补开。
- **历史**:已结束计划与成交细节。
- **统计**:按永期 / 期期分别看胜率、盈亏比、最大盈亏与回撤等(按结束时间累积)。
---
## 四、env 与系统设置(和说明书相关的部分)
### 4.1 系统设置 → 导航显示
控制顶栏是否出现各板块。「系统说明」默认关闭,打开后顶栏才显示入口。
### 4.2 env → 对冲计划(常用)
| 开关 | 作用 |
|------|------|
| 启用对冲计划 | 总开关;关则导航隐藏且不可开仓 |
| 显示永期 / 期期 | 单独隐藏某一 Tab |
| 允许对冲真实下单 | 与实盘开关一起才可启动永期 |
| 对冲与期权互斥门控 | 见 1.4 |
| 半腿失败改手动补开 | 见 3.4 |
| 半腿失败时自动平期权 | 手动补开开启时强制无效 |
| 期期平仓模式(方案 C) | 页面是否出现「全平 / 到期平」 |
含「需重启」标记的项保存后要用「保存并重启」;对冲多数开关可热更,以页面标注为准。
---
## 五、常见问题
**Q:为什么有对冲计划时单独开不了期权?**
A:互斥门控默认开启,避免计划仓与手开仓搅在一起。可在 env 关闭互斥。
**Q:为什么有一张「纯期权」就启动不了对冲?**
A:同上。先平掉或确认来源;若其实是对冲腿,看持仓来源是否显示计划编号。
**Q:半腿后为什么不自动平?**
A:默认改手动补开,避免买一平仓吃掉 ≥10% 量级价差。到「进行中」补开即可。
**Q:测算能过但启动按钮灰?**
A:看顶部门禁行:全仓、实盘、真实下单、活跃计划数、互斥、Tab 是否隐藏等。
**Q:说明书和「风控说明」什么关系?**
A:风控说明仍是独立页(冷却、当日次数等细则)。本说明书讲板块逻辑与操作;风控细则以风控说明 + env 为准。
---
## 六、版本与维护
- 文档路径:`docs/系统说明.md`
- 功能变更后应同步改本章(尤其门禁、半腿、互斥、平仓模式)。
- 更偏开发/方案的材料仍在 `docs/对冲计划*.md``docs/期权对冲方案分析.md` 等,不必与本说明书一一粘贴。
+3 -1
View File
@@ -15,6 +15,7 @@
records: "/records",
stats: "/stats",
risk_policy: "/risk_policy",
system_guide: "/system_guide",
env_config: "/env_config",
settings: "/settings",
};
@@ -296,7 +297,8 @@
}
function syncShellChrome(tab) {
const hideTopBar = tab === "settings" || tab === "risk_policy" || tab === "env_config";
const hideTopBar =
tab === "settings" || tab === "risk_policy" || tab === "system_guide" || tab === "env_config";
document.querySelectorAll(".instance-top-bar").forEach((el) => {
el.hidden = hideTopBar;
});
+3 -1
View File
@@ -20,7 +20,7 @@
}
/** 默认关闭的导航开关:缺失时按 false,不能用 !== false */
const NAV_DEFAULT_OFF = { show_nav_dashboard: true };
const NAV_DEFAULT_OFF = { show_nav_dashboard: true, show_nav_system_guide: true };
function navPrefShow(display, key) {
if (!key) return true;
@@ -41,6 +41,7 @@
"hedge-plan": "show_nav_hedge_plan",
hedge_plan: "show_nav_hedge_plan",
risk_policy: "show_nav_risk_policy",
system_guide: "show_nav_system_guide",
env_config: "show_nav_env_config",
};
document.querySelectorAll(".embed-top-nav [data-embed-tab], .top-nav a[href^='/']").forEach((a) => {
@@ -68,6 +69,7 @@
"hedge-plan": "show_nav_hedge_plan",
hedge_plan: "show_nav_hedge_plan",
risk_policy: "show_nav_risk_policy",
system_guide: "show_nav_system_guide",
env_config: "show_nav_env_config",
};
const key = map[tab];
@@ -14,6 +14,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
"show_nav_records": True,
"show_nav_stats": True,
"show_nav_risk_policy": True,
"show_nav_system_guide": False,
"show_nav_env_config": True,
"show_nav_options": True,
"show_nav_options_review": True,
@@ -32,6 +33,7 @@ DISPLAY_LABELS: dict[str, str] = {
"show_nav_records": "交易记录与复盘",
"show_nav_stats": "统计分析",
"show_nav_risk_policy": "风控说明",
"show_nav_system_guide": "系统说明",
"show_nav_env_config": "env配置",
"show_nav_options": "期权",
"show_nav_options_review": "期权复盘",
@@ -50,6 +52,7 @@ NAV_TAB_ALLOWED: dict[str, str] = {
"records": "show_nav_records",
"stats": "show_nav_stats",
"risk_policy": "show_nav_risk_policy",
"system_guide": "show_nav_system_guide",
"env_config": "show_nav_env_config",
"options": "show_nav_options",
"options_review": "show_nav_options_review",
@@ -112,6 +115,7 @@ def display_meta_for_ui() -> list[dict[str, Any]]:
"show_nav_records",
"show_nav_stats",
"show_nav_risk_policy",
"show_nav_system_guide",
"show_nav_env_config",
"show_nav_options",
"show_nav_options_review",
+2
View File
@@ -22,6 +22,7 @@ EMBED_TABS: tuple[str, ...] = (
"records",
"stats",
"risk_policy",
"system_guide",
"env_config",
"settings",
)
@@ -41,6 +42,7 @@ PATH_TO_EMBED_TAB: dict[str, str] = {
"/records": "records",
"/stats": "stats",
"/risk_policy": "risk_policy",
"/system_guide": "system_guide",
"/env_config": "env_config",
"/settings": "settings",
}
+4
View File
@@ -202,6 +202,10 @@ def build_settings_tabs(display: dict[str, Any] | None, instance_settings: dict[
def settings_page_context(page: str, *, instance_base_dir: str | None = None, **kwargs: Any) -> dict[str, Any]:
p = (page or "").strip()
if p == "system_guide":
from lib.instance.instance_system_guide_lib import system_guide_template_context
return system_guide_template_context()
if p not in ("settings", "risk_policy", "env_config"):
return {}
display = kwargs.pop("display", None)
+69
View File
@@ -0,0 +1,69 @@
"""实例「系统说明」:加载 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.hub.hub_strategy_lib import render_markdown_html
from lib.paths import REPO_ROOT
def system_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]]]:
"""为 h2 注入 id,并收集目录(仅 h2)."""
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_system_guide_payload() -> dict[str, Any]:
path = system_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 system_guide_template_context() -> dict[str, Any]:
payload = load_system_guide_payload()
return {
"system_guide_html": payload.get("html") or "",
"system_guide_toc": payload.get("toc") or [],
}
@@ -10,7 +10,7 @@
<div class="display-prefs-checks">
{% for item in group.entries %}
<label class="chk-label">
<input type="checkbox" data-pref-key="{{ item.key }}"{% if display.get(item.key, true) %} checked{% endif %}>
<input type="checkbox" data-pref-key="{{ item.key }}"{% if item.key in ('show_nav_dashboard', 'show_nav_system_guide') %}{% if display.get(item.key) %} checked{% endif %}{% elif display.get(item.key, true) %} checked{% endif %}>
{{ item.label }}
</label>
{% endfor %}
@@ -314,6 +314,10 @@
{% include 'risk_policy_panel.html' %}
{% endif %}
{% if page == 'system_guide' %}
{% include 'system_guide_panel.html' %}
{% endif %}
{% if page == 'settings' %}
{% include 'settings_panel.html' %}
{% endif %}
+2 -1
View File
@@ -56,6 +56,7 @@
{% 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 %}
<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>
{% endif %}
@@ -64,7 +65,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', 'env_config') and include_transfer_block %}
{% if initial_tab not in ('settings', 'risk_policy', 'system_guide', 'env_config') and include_transfer_block %}
{% include 'instance_top_bar.html' %}
{% endif %}
+6 -1
View File
@@ -143,6 +143,7 @@
{% if display.show_nav_risk_policy %}
<a href="/risk_policy" class="{% if page == 'risk_policy' %}active{% endif %}">风控说明</a>
{% endif %}
<a href="/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" class="{% if page == 'env_config' %}active{% endif %}">env配置</a>
{% endif %}
@@ -151,7 +152,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', 'env_config', 'options', 'options_review', 'hedge_plan') %}
{% if page not in ('settings', 'risk_policy', 'system_guide', 'env_config', 'options', 'options_review', 'hedge_plan') %}
{% include 'instance_top_bar.html' %}
{% endif %}
@@ -390,6 +391,10 @@
{% include 'risk_policy_panel.html' %}
{% endif %}
{% if page == 'system_guide' %}
{% include 'system_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">操作与逻辑按章节混排。默认不在顶栏显示;可在系统设置 → 导航显示中打开。</p>
</div>
<div class="system-guide-layout">
{% if system_guide_toc %}
<aside class="system-guide-toc" aria-label="章节目录">
<div class="system-guide-toc-title">目录</div>
<nav>
{% for item in system_guide_toc %}
<a href="#{{ item.id }}">{{ item.title }}</a>
{% endfor %}
</nav>
</aside>
{% endif %}
<article class="system-guide-body prose">
{{ system_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>
@@ -28,6 +28,13 @@ class TestInstanceDisplayPrefs(unittest.TestCase):
on = normalize_display_prefs({"show_nav_dashboard": True})
self.assertTrue(tab_allowed("dashboard", on))
def test_system_guide_nav_default_off(self):
prefs = normalize_display_prefs({})
self.assertFalse(prefs["show_nav_system_guide"])
self.assertFalse(tab_allowed("system_guide", prefs))
on = normalize_display_prefs({"show_nav_system_guide": True})
self.assertTrue(tab_allowed("system_guide", on))
class TestEnvFileLib(unittest.TestCase):
def test_upsert_and_read(self):
+2
View File
@@ -30,9 +30,11 @@ def test_embed_tabs_cover_main_nav():
assert "records" in EMBED_TABS
assert "env_config" in EMBED_TABS
assert "risk_policy" in EMBED_TABS
assert "system_guide" in EMBED_TABS
assert "settings" in EMBED_TABS
assert path_to_embed_tab("/env_config") == "env_config"
assert path_to_embed_tab("/risk_policy") == "risk_policy"
assert path_to_embed_tab("/system_guide") == "system_guide"
assert path_to_embed_tab("/settings") == "settings"
+31
View File
@@ -0,0 +1,31 @@
"""系统说明 Markdown 加载与目录."""
from __future__ import annotations
from lib.instance.instance_system_guide_lib import (
load_system_guide_payload,
system_guide_md_path,
system_guide_template_context,
)
def test_system_guide_md_exists():
assert system_guide_md_path().is_file()
def test_system_guide_payload_has_toc_and_html():
payload = load_system_guide_payload()
assert payload["html"]
assert "系统说明" in payload["html"] or "总览" in payload["html"]
toc = payload["toc"]
assert isinstance(toc, list)
assert len(toc) >= 3
assert all(item.get("id") and item.get("title") for item in toc)
assert any("总览" in (item.get("title") or "") for item in toc)
assert any("期权" in (item.get("title") or "") for item in toc)
assert any("对冲" in (item.get("title") or "") for item in toc)
def test_system_guide_template_context():
ctx = system_guide_template_context()
assert ctx["system_guide_html"]
assert ctx["system_guide_toc"]