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:
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
@@ -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` 等,不必与本说明书一一粘贴。
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user