diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 962c6e0..801cc8b 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -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(): diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index a070a3b..6ead378 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -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(): diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index d0ffffa..d0ef7d7 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -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(): diff --git a/docs/系统说明.md b/docs/系统说明.md new file mode 100644 index 0000000..c81d6da --- /dev/null +++ b/docs/系统说明.md @@ -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` 等,不必与本说明书一一粘贴。 diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js index f15bde0..cfe57d8 100644 --- a/lib/common/static/instance_embed.js +++ b/lib/common/static/instance_embed.js @@ -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; }); diff --git a/lib/common/static/instance_settings_prefs.js b/lib/common/static/instance_settings_prefs.js index 5506999..3ed2acc 100644 --- a/lib/common/static/instance_settings_prefs.js +++ b/lib/common/static/instance_settings_prefs.js @@ -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]; diff --git a/lib/instance/instance_display_prefs_lib.py b/lib/instance/instance_display_prefs_lib.py index f75b985..6c767b4 100644 --- a/lib/instance/instance_display_prefs_lib.py +++ b/lib/instance/instance_display_prefs_lib.py @@ -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", diff --git a/lib/instance/instance_embed_lib.py b/lib/instance/instance_embed_lib.py index 0753ee7..33dfdac 100644 --- a/lib/instance/instance_embed_lib.py +++ b/lib/instance/instance_embed_lib.py @@ -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", } diff --git a/lib/instance/instance_settings_lib.py b/lib/instance/instance_settings_lib.py index eea0f6b..cf1bcd7 100644 --- a/lib/instance/instance_settings_lib.py +++ b/lib/instance/instance_settings_lib.py @@ -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) diff --git a/lib/instance/instance_system_guide_lib.py b/lib/instance/instance_system_guide_lib.py new file mode 100644 index 0000000..6a59790 --- /dev/null +++ b/lib/instance/instance_system_guide_lib.py @@ -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'

{inner}

' + + out = re.sub(r"

(.*?)

", 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 [], + } diff --git a/lib/instance/templates/display_prefs_panel.html b/lib/instance/templates/display_prefs_panel.html index 950b118..05a6622 100644 --- a/lib/instance/templates/display_prefs_panel.html +++ b/lib/instance/templates/display_prefs_panel.html @@ -10,7 +10,7 @@
{% for item in group.entries %} {% endfor %} diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html index d21cf90..efc9014 100644 --- a/lib/instance/templates/embed_page_fragment.html +++ b/lib/instance/templates/embed_page_fragment.html @@ -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 %} diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 4c64fda..53c910d 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -56,6 +56,7 @@ {% if display.show_nav_risk_policy %} 风控说明 {% endif %} + {% if display.show_nav_env_config %} env配置 {% endif %} @@ -64,7 +65,7 @@ {% 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 %} diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index c6a7170..ed256b1 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -143,6 +143,7 @@ {% if display.show_nav_risk_policy %} 风控说明 {% endif %} + {% if display.show_nav_env_config %} env配置 {% endif %} @@ -151,7 +152,7 @@ {% with msg=get_flashed_messages() %}{% if msg %}
{{ msg[0] }}
{% 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 %} diff --git a/lib/instance/templates/system_guide_panel.html b/lib/instance/templates/system_guide_panel.html new file mode 100644 index 0000000..fbb07be --- /dev/null +++ b/lib/instance/templates/system_guide_panel.html @@ -0,0 +1,87 @@ +{# 系统说明: docs/系统说明.md + h2 目录 #} +
+
+
+

系统说明

+

操作与逻辑按章节混排。默认不在顶栏显示;可在系统设置 → 导航显示中打开。

+
+
+ {% if system_guide_toc %} + + {% endif %} +
+ {{ system_guide_html|safe }} +
+
+
+
+ diff --git a/tests/test_instance_display_env_settings.py b/tests/test_instance_display_env_settings.py index 57e068b..eb7e3e4 100644 --- a/tests/test_instance_display_env_settings.py +++ b/tests/test_instance_display_env_settings.py @@ -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): diff --git a/tests/test_instance_embed_lib.py b/tests/test_instance_embed_lib.py index 9ec5eab..0af29a6 100644 --- a/tests/test_instance_embed_lib.py +++ b/tests/test_instance_embed_lib.py @@ -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" diff --git a/tests/test_instance_system_guide_lib.py b/tests/test_instance_system_guide_lib.py new file mode 100644 index 0000000..57dd029 --- /dev/null +++ b/tests/test_instance_system_guide_lib.py @@ -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"]