diff --git a/app.py b/app.py index 9dcb70f..cd1e5af 100644 --- a/app.py +++ b/app.py @@ -231,6 +231,7 @@ def _static_asset_v() -> str: "static/css/responsive.css", "static/css/trade.css", "static/css/dashboard.css", + "static/css/doc.css", "static/css/base.css", ) mtimes = [] @@ -1644,6 +1645,20 @@ def dashboard(): return render_template("dashboard.html") +@app.route("/risk-guide") +@login_required +@require_nav("risk_guide") +def risk_guide(): + from doc_render import read_doc, render_markdown + + try: + _title, raw = read_doc("risk-guide") + except FileNotFoundError: + flash("文档不存在") + return redirect(url_for("positions")) + return render_template("risk_guide.html", doc_html=render_markdown(raw)) + + @app.route("/api/dashboard/live") @login_required def api_dashboard_live(): diff --git a/dashboard_lib.py b/dashboard_lib.py index ae55f15..99e3071 100644 --- a/dashboard_lib.py +++ b/dashboard_lib.py @@ -88,6 +88,8 @@ def build_risk_overview( "daily_risk_used_pct": daily_risk_used, "limits": { "max_active_positions": max_active_positions(), + "position_mode": "single" if max_active_positions() <= 1 else "multi", + "position_mode_label": "单仓模式" if max_active_positions() <= 1 else "多仓模式", "daily_position_limit": daily_position_limit(), "daily_trading_risk_pct_limit": daily_trading_risk_pct_limit(), "manual_close_daily_limit": manual_close_daily_limit(), diff --git a/doc_render.py b/doc_render.py new file mode 100644 index 0000000..5623b99 --- /dev/null +++ b/doc_render.py @@ -0,0 +1,170 @@ +# Copyright (c) 2025-2026 马建军. All rights reserved. +# 专有软件 — 未经授权禁止复制、传播、转售。 +# 详见 LICENSE.zh-CN.txt + +"""将项目 docs 下的 Markdown 转为安全 HTML(无第三方依赖)。""" +from __future__ import annotations + +import html +import re +from pathlib import Path + +_DOCS_ROOT = Path(__file__).resolve().parent / "docs" + +ALLOWED_DOCS: dict[str, str] = { + "risk-guide": "风控说明.md", +} + + +def docs_root() -> Path: + return _DOCS_ROOT + + +def read_doc(slug: str) -> tuple[str, str]: + """返回 (title, raw_markdown)。""" + name = ALLOWED_DOCS.get(slug) + if not name: + raise FileNotFoundError(slug) + path = (_DOCS_ROOT / name).resolve() + if not path.is_file() or _DOCS_ROOT.resolve() not in path.parents: + raise FileNotFoundError(slug) + text = path.read_text(encoding="utf-8") + title = name + for line in text.splitlines(): + s = line.strip() + if s.startswith("# "): + title = s[2:].strip() + break + return title, text + + +def _inline(text: str) -> str: + s = html.escape(text) + s = re.sub(r"\*\*(.+?)\*\*", r"\1", s) + s = re.sub(r"`([^`]+)`", r"\1", s) + s = re.sub( + r"\[([^\]]+)\]\(([^)]+)\)", + lambda m: _link_html(m.group(1), m.group(2)), + s, + ) + return s + + +def _link_html(label: str, href: str) -> str: + h = html.escape(href) + lbl = _inline(label) + if href.startswith(("http://", "https://", "mailto:")): + return f'{lbl}' + if href.endswith(".md") or href.startswith("./"): + return f'{lbl}' + return f'{lbl}' + + +def render_markdown(text: str) -> str: + lines = text.splitlines() + out: list[str] = [] + i = 0 + in_ul = False + in_ol = False + + def close_lists() -> None: + nonlocal in_ul, in_ol + if in_ul: + out.append("") + in_ul = False + if in_ol: + out.append("") + in_ol = False + + while i < len(lines): + line = lines[i] + stripped = line.strip() + + if not stripped: + close_lists() + i += 1 + continue + + if stripped == "---": + close_lists() + out.append("
") + i += 1 + continue + + if stripped.startswith("|") and stripped.endswith("|"): + close_lists() + table_lines: list[str] = [] + while i < len(lines) and lines[i].strip().startswith("|"): + table_lines.append(lines[i].strip()) + i += 1 + out.append(_render_table(table_lines)) + continue + + if stripped.startswith("### "): + close_lists() + out.append(f"

{_inline(stripped[4:])}

") + i += 1 + continue + if stripped.startswith("## "): + close_lists() + out.append(f"

{_inline(stripped[3:])}

") + i += 1 + continue + if stripped.startswith("# "): + close_lists() + out.append(f"

{_inline(stripped[2:])}

") + i += 1 + continue + + if re.match(r"^[-*]\s+", stripped): + if not in_ul: + close_lists() + out.append("