Files
crypto_monitor/lib/hub/hub_strategy_lib.py
T
dekun 7223b54ec4 feat: add hub strategy docs page with MD and checklists
Central /strategy page shows per-exchange strategy markdown and read-only entry checklists; nav toggle in settings, print and HTML export per tab.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 03:12:09 +08:00

280 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""中控「策略说明」:读取 docs/strategy MD + checklists JSON。"""
from __future__ import annotations
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from lib.paths import REPO_ROOT
STRATEGY_EXCHANGES: tuple[str, ...] = ("binance", "okx", "gate")
STRATEGY_META: dict[str, dict[str, str]] = {
"binance": {
"label": "币安",
"title": "币安·山寨多头趋势",
"md_file": "binance-alt-trend-long.md",
},
"okx": {
"label": "OKX",
"title": "OKX·多空趋势",
"md_file": "okx-trend-both.md",
},
"gate": {
"label": "Gate",
"title": "Gate·BTC 日内",
"md_file": "gate-intraday.md",
},
}
def _strategy_dir() -> Path:
return REPO_ROOT / "docs" / "strategy"
def _checklist_path(exchange_key: str) -> Path:
return _strategy_dir() / "checklists" / f"{exchange_key.strip().lower()}.json"
def _md_path(exchange_key: str) -> Path:
meta = STRATEGY_META.get((exchange_key or "").strip().lower())
if not meta:
raise KeyError(exchange_key)
return _strategy_dir() / meta["md_file"]
def _parse_version(md_text: str) -> str:
m = re.search(r">\s*\*\*状态\*\*[:]\s*(v[\d.]+)", md_text)
if m:
return m.group(1)
m = re.search(r"\|\s*v([\d.]+)\s*\|", md_text)
if m:
return f"v{m.group(1)}"
return ""
def render_markdown_html(md_text: str) -> str:
try:
import markdown # type: ignore
return markdown.markdown(
md_text,
extensions=["tables", "fenced_code", "nl2br", "sane_lists"],
)
except Exception:
return _simple_md_html(md_text)
def _simple_md_html(md_text: str) -> str:
from html import escape
lines = md_text.replace("\r\n", "\n").replace("\r", "\n").splitlines()
out: list[str] = []
i = 0
in_code = False
code_buf: list[str] = []
list_buf: list[str] = []
list_ordered = False
def flush_list() -> None:
nonlocal list_buf, list_ordered
if not list_buf:
return
tag = "ol" if list_ordered else "ul"
out.append(f"<{tag}>")
for item in list_buf:
out.append(f"<li>{_inline_md(item)}</li>")
out.append(f"</{tag}>")
list_buf = []
def flush_code() -> None:
nonlocal code_buf, in_code
if not code_buf:
return
out.append(f"<pre><code>{escape(chr(10).join(code_buf))}</code></pre>")
code_buf = []
in_code = False
while i < len(lines):
line = lines[i]
if line.strip().startswith("```"):
flush_list()
if in_code:
flush_code()
else:
in_code = True
i += 1
continue
if in_code:
code_buf.append(line)
i += 1
continue
if re.match(r"^\s*\|", line) and i + 1 < len(lines) and re.match(r"^\s*\|?\s*[-:| ]+\|", lines[i + 1]):
flush_list()
header = [c.strip() for c in line.strip().strip("|").split("|")]
i += 2
rows: list[list[str]] = []
while i < len(lines) and re.match(r"^\s*\|", lines[i]):
rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")])
i += 1
out.append("<table><thead><tr>" + "".join(f"<th>{_inline_md(h)}</th>" for h in header) + "</tr></thead><tbody>")
for row in rows:
out.append("<tr>" + "".join(f"<td>{_inline_md(c)}</td>" for c in row) + "</tr>")
out.append("</tbody></table>")
continue
if re.match(r"^#{1,3}\s+", line):
flush_list()
m = re.match(r"^(#{1,3})\s+(.*)$", line)
if m:
level = len(m.group(1))
out.append(f"<h{level}>{_inline_md(m.group(2))}</h{level}>")
i += 1
continue
if line.strip() == "---":
flush_list()
out.append("<hr>")
i += 1
continue
if line.startswith(">"):
flush_list()
out.append(f"<blockquote>{_inline_md(line.lstrip('>').strip())}</blockquote>")
i += 1
continue
m = re.match(r"^(\d+)\.\s+(.*)$", line.strip())
if m:
if list_buf and not list_ordered:
flush_list()
list_ordered = True
list_buf.append(m.group(2))
i += 1
continue
if re.match(r"^[-*]\s+", line.strip()):
if list_buf and list_ordered:
flush_list()
list_ordered = False
list_buf.append(re.sub(r"^[-*]\s+", "", line.strip()))
i += 1
continue
if not line.strip():
flush_list()
i += 1
continue
flush_list()
out.append(f"<p>{_inline_md(line.strip())}</p>")
i += 1
flush_list()
flush_code()
return "\n".join(out)
def _inline_md(text: str) -> str:
from html import escape
s = escape(text)
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
return s
def load_checklist(exchange_key: str) -> dict[str, Any]:
path = _checklist_path(exchange_key)
if not path.is_file():
return {"exchange": exchange_key, "title": "开仓检查清单", "groups": []}
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
return {"exchange": exchange_key, "title": "开仓检查清单", "groups": []}
return data
def load_strategy_payload(exchange_key: str) -> dict[str, Any]:
key = (exchange_key or "").strip().lower()
if key not in STRATEGY_META:
raise KeyError(exchange_key)
meta = STRATEGY_META[key]
md_path = _md_path(key)
md_text = md_path.read_text(encoding="utf-8") if md_path.is_file() else ""
checklist = load_checklist(key)
version = _parse_version(md_text) or str(checklist.get("version") or "")
return {
"ok": True,
"exchange_key": key,
"label": meta["label"],
"title": meta["title"],
"version": version,
"md_source": str(md_path.relative_to(REPO_ROOT)).replace("\\", "/"),
"strategy_html": render_markdown_html(md_text),
"checklist": checklist,
}
def strategy_meta_payload() -> dict[str, Any]:
tabs = [
{"key": k, "label": STRATEGY_META[k]["label"], "title": STRATEGY_META[k]["title"]}
for k in STRATEGY_EXCHANGES
]
return {"ok": True, "exchanges": tabs}
def build_export_html(exchange_key: str) -> str:
payload = load_strategy_payload(exchange_key)
key = payload["exchange_key"]
label = payload["label"]
title = payload["title"]
version = payload.get("version") or ""
checklist = payload.get("checklist") or {}
groups = checklist.get("groups") or []
now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M")
checklist_html_parts = [
f"<h2>{escape_html(str(checklist.get('title') or '开仓检查清单'))}</h2>"
]
for grp in groups:
if not isinstance(grp, dict):
continue
gtitle = escape_html(str(grp.get("title") or ""))
checklist_html_parts.append(f"<h3>{gtitle}</h3><ul>")
for item in grp.get("items") or []:
checklist_html_parts.append(
f"<li><span class=\"box\">☐</span> {escape_html(str(item))}</li>"
)
checklist_html_parts.append("</ul>")
checklist_html = "\n".join(checklist_html_parts)
return f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{escape_html(label)} · 策略说明</title>
<style>
body{{font-family:system-ui,sans-serif;margin:24px;color:#111;line-height:1.55}}
h1{{font-size:1.35rem;margin:0 0 8px}}
.meta{{color:#555;font-size:.85rem;margin-bottom:20px}}
.grid{{display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:start}}
.strategy-doc h2{{font-size:1.05rem;margin-top:1.2em}}
.strategy-doc table{{border-collapse:collapse;width:100%;font-size:.88rem}}
.strategy-doc th,.strategy-doc td{{border:1px solid #ccc;padding:6px 8px;text-align:left}}
.checklist ul{{list-style:none;padding:0;margin:0}}
.checklist li{{margin:8px 0;padding-left:0}}
.box{{display:inline-block;width:1em;margin-right:6px}}
@media print{{body{{margin:12mm}}}}
</style>
</head>
<body>
<h1>{escape_html(title)}</h1>
<div class="meta">{escape_html(label)} · {escape_html(version)} · 导出 {now}</div>
<div class="grid">
<div class="strategy-doc">{payload.get("strategy_html") or ""}</div>
<div class="checklist">{checklist_html}</div>
</div>
</body>
</html>"""
def escape_html(text: str) -> str:
from html import escape
return escape(text, quote=True)