| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959 |
- from __future__ import annotations
- import json
- import re
- from html import escape
- from pathlib import Path
- from urllib.parse import urlencode
- import pandas as pd
- from dragon_branch_configs import (
- alpha_first_glued_refined_hot_cap_config,
- alpha_first_selective_veto_config,
- workbook_preserving_config,
- )
- from dragon_indicators import DragonIndicatorConfig, DragonIndicatorEngine
- from dragon_strategy import DragonRuleEngine
- BRANCH_ORDER = ["workbook_preserving", "alpha_first_selective_veto", "alpha_first_glued_refined_hot_cap"]
- BRANCH_LABELS = {
- "workbook_preserving": "workbook_preserving",
- "alpha_first_selective_veto": "alpha_first_selective_veto",
- "alpha_first_glued_refined_hot_cap": "RC1 / alpha_first_glued_refined_hot_cap",
- "system_monitor": "系统监控",
- }
- BRANCH_ROLES = {
- "workbook_preserving": "重构基线",
- "alpha_first_selective_veto": "平衡对照",
- "alpha_first_glued_refined_hot_cap": "前向默认 / RC1",
- "system_monitor": "系统汇总",
- }
- BRANCH_COLORS = {
- "workbook_preserving": "#8b6f47",
- "alpha_first_selective_veto": "#0f766e",
- "alpha_first_glued_refined_hot_cap": "#c05621",
- }
- _REPORT_INDICATOR_CACHE: tuple[pd.DataFrame, dict[str, object]] | None = None
- _REPORT_INDICATOR_FULL_CACHE: pd.DataFrame | None = None
- _REPORT_EVENT_CACHE: pd.DataFrame | None = None
- def _load_csv(path: Path) -> pd.DataFrame:
- return pd.DataFrame() if not path.exists() else pd.read_csv(path, encoding="utf-8-sig")
- def _prepare_indicator_snapshot(df: pd.DataFrame) -> pd.DataFrame:
- if df.empty:
- return pd.DataFrame()
- result = df.copy()
- if "date" not in result.columns and result.index.name == "date":
- result = result.reset_index()
- if "date" not in result.columns:
- return pd.DataFrame()
- result["date"] = pd.to_datetime(result["date"], errors="coerce")
- result = result.dropna(subset=["date"]).sort_values("date").drop_duplicates("date", keep="last")
- for col in ["close", "a1", "b1", "c1"]:
- if col not in result.columns:
- result[col] = float("nan")
- result[col] = pd.to_numeric(result[col], errors="coerce")
- for col in ["kdj_buy", "kdj_sell", "ql_buy", "ql_sell"]:
- if col not in result.columns:
- result[col] = False
- if result[col].dtype == object:
- result[col] = result[col].astype(str).str.lower().map({"true": True, "false": False}).fillna(False)
- result[col] = result[col].fillna(False).astype(bool)
- return result[["date", "close", "a1", "b1", "c1", "kdj_buy", "kdj_sell", "ql_buy", "ql_sell"]].copy()
- def _prepare_indicator_full(df: pd.DataFrame) -> pd.DataFrame:
- if df.empty:
- return pd.DataFrame()
- result = df.copy()
- if "date" not in result.columns and result.index.name == "date":
- result = result.reset_index()
- if "date" not in result.columns:
- return pd.DataFrame()
- result["date"] = pd.to_datetime(result["date"], errors="coerce")
- result = result.dropna(subset=["date"]).sort_values("date").drop_duplicates("date", keep="last")
- for bool_col in ["kdj_buy", "kdj_sell", "ql_buy", "ql_sell"]:
- if bool_col in result.columns:
- if result[bool_col].dtype == object:
- result[bool_col] = result[bool_col].astype(str).str.lower().map({"true": True, "false": False}).fillna(False)
- result[bool_col] = result[bool_col].fillna(False).astype(bool)
- return result
- def _indicator_coverage_ok(df: pd.DataFrame, min_required: pd.Timestamp, max_required: pd.Timestamp) -> bool:
- if df.empty:
- return False
- return bool(df["date"].min() <= min_required and df["date"].max() >= max_required)
- def _load_indicator_snapshot_for_report(
- base_dir: Path,
- min_required_date: str,
- max_required_date: str,
- ) -> tuple[pd.DataFrame, dict[str, object]]:
- global _REPORT_INDICATOR_CACHE, _REPORT_INDICATOR_FULL_CACHE
- min_required = pd.to_datetime(min_required_date)
- max_required = pd.to_datetime(max_required_date)
- if _REPORT_INDICATOR_CACHE is not None:
- cached_df, cached_meta = _REPORT_INDICATOR_CACHE
- if _indicator_coverage_ok(cached_df, min_required, max_required):
- return cached_df.copy(), dict(cached_meta)
- cache_candidates = [
- (base_dir / "dragon_indicator_snapshot_full.csv", "本地完整缓存"),
- (base_dir / "dragon_indicator_snapshot.csv", "本地快照"),
- ]
- local_best = pd.DataFrame()
- local_source = "无本地指标快照"
- for path, label in cache_candidates:
- if not path.exists():
- continue
- candidate = _prepare_indicator_snapshot(pd.read_csv(path, encoding="utf-8-sig"))
- if candidate.empty:
- continue
- if local_best.empty or candidate["date"].max() > local_best["date"].max():
- local_best = candidate
- local_source = label
- if _indicator_coverage_ok(candidate, min_required, max_required):
- meta = {
- "source_label": label,
- "coverage_start": candidate["date"].min().date().isoformat(),
- "coverage_end": candidate["date"].max().date().isoformat(),
- "coverage_ok": True,
- "fetch_status": "local_ok",
- }
- _REPORT_INDICATOR_CACHE = (candidate.copy(), dict(meta))
- return candidate, meta
- try:
- engine = DragonIndicatorEngine(DragonIndicatorConfig(start_date="2015-01-01"))
- live_full = engine.compute(engine.fetch_daily_data())
- if "date" not in live_full.columns:
- live_full = live_full.reset_index()
- live_full = _prepare_indicator_full(live_full)
- live = _prepare_indicator_snapshot(live_full)
- if not live.empty:
- _REPORT_INDICATOR_FULL_CACHE = live_full.copy()
- live_full.to_csv(base_dir / "dragon_indicator_snapshot_full.csv", index=False, encoding="utf-8-sig")
- meta = {
- "source_label": "实时重算完整指标",
- "coverage_start": live["date"].min().date().isoformat(),
- "coverage_end": live["date"].max().date().isoformat(),
- "coverage_ok": _indicator_coverage_ok(live, min_required, max_required),
- "fetch_status": "live_ok",
- }
- _REPORT_INDICATOR_CACHE = (live.copy(), dict(meta))
- return live, meta
- except Exception as exc: # pragma: no cover - report fallback path
- if not local_best.empty:
- meta = {
- "source_label": f"{local_source}(实时重算失败,已回退)",
- "coverage_start": local_best["date"].min().date().isoformat(),
- "coverage_end": local_best["date"].max().date().isoformat(),
- "coverage_ok": _indicator_coverage_ok(local_best, min_required, max_required),
- "fetch_status": f"fallback:{exc}",
- }
- _REPORT_INDICATOR_CACHE = (local_best.copy(), dict(meta))
- return local_best, meta
- return pd.DataFrame(), {
- "source_label": f"指标数据不可用:{exc}",
- "coverage_start": "",
- "coverage_end": "",
- "coverage_ok": False,
- "fetch_status": f"empty:{exc}",
- }
- if not local_best.empty:
- meta = {
- "source_label": local_source,
- "coverage_start": local_best["date"].min().date().isoformat(),
- "coverage_end": local_best["date"].max().date().isoformat(),
- "coverage_ok": _indicator_coverage_ok(local_best, min_required, max_required),
- "fetch_status": "local_partial",
- }
- _REPORT_INDICATOR_CACHE = (local_best.copy(), dict(meta))
- return local_best, meta
- return pd.DataFrame(), {
- "source_label": "指标数据不可用",
- "coverage_start": "",
- "coverage_end": "",
- "coverage_ok": False,
- "fetch_status": "empty",
- }
- def _load_full_indicator_for_report(base_dir: Path, min_required_date: str, max_required_date: str) -> pd.DataFrame:
- global _REPORT_INDICATOR_FULL_CACHE
- min_required = pd.to_datetime(min_required_date)
- max_required = pd.to_datetime(max_required_date)
- if _REPORT_INDICATOR_FULL_CACHE is not None and _indicator_coverage_ok(_REPORT_INDICATOR_FULL_CACHE, min_required, max_required):
- return _REPORT_INDICATOR_FULL_CACHE.copy()
- full_path = base_dir / "dragon_indicator_snapshot_full.csv"
- if full_path.exists():
- full_df = _prepare_indicator_full(pd.read_csv(full_path, encoding="utf-8-sig"))
- if _indicator_coverage_ok(full_df, min_required, max_required):
- _REPORT_INDICATOR_FULL_CACHE = full_df.copy()
- return full_df
- _load_indicator_snapshot_for_report(base_dir, min_required_date, max_required_date)
- if _REPORT_INDICATOR_FULL_CACHE is not None and _indicator_coverage_ok(_REPORT_INDICATOR_FULL_CACHE, min_required, max_required):
- return _REPORT_INDICATOR_FULL_CACHE.copy()
- if full_path.exists():
- full_df = _prepare_indicator_full(pd.read_csv(full_path, encoding="utf-8-sig"))
- _REPORT_INDICATOR_FULL_CACHE = full_df.copy()
- return full_df
- return pd.DataFrame()
- def _load_strategy_events_for_report(base_dir: Path, min_required_date: str, max_required_date: str) -> pd.DataFrame:
- global _REPORT_EVENT_CACHE
- min_required = pd.to_datetime(min_required_date)
- max_required = pd.to_datetime(max_required_date)
- if _REPORT_EVENT_CACHE is not None and not _REPORT_EVENT_CACHE.empty:
- event_dates = pd.to_datetime(_REPORT_EVENT_CACHE["date"], errors="coerce")
- if event_dates.min() <= min_required and event_dates.max() >= max_required:
- return _REPORT_EVENT_CACHE.copy()
- indicators = _load_full_indicator_for_report(base_dir, min_required_date, max_required_date)
- if indicators.empty:
- return pd.DataFrame(columns=["branch", "date", "side", "layer", "reason"])
- branch_builders = {
- "workbook_preserving": workbook_preserving_config,
- "alpha_first_selective_veto": alpha_first_selective_veto_config,
- "alpha_first_glued_refined_hot_cap": alpha_first_glued_refined_hot_cap_config,
- }
- event_frames: list[pd.DataFrame] = []
- indicator_input = indicators.set_index("date", drop=False)
- for branch, config_builder in branch_builders.items():
- engine = DragonRuleEngine(config=config_builder())
- events, _ = engine.run(indicator_input)
- if events.empty:
- continue
- branch_events = events.copy()
- branch_events["branch"] = branch
- event_frames.append(branch_events[["branch", "date", "side", "layer", "reason"]].copy())
- if not event_frames:
- return pd.DataFrame(columns=["branch", "date", "side", "layer", "reason"])
- all_events = pd.concat(event_frames, ignore_index=True)
- all_events["date"] = pd.to_datetime(all_events["date"], errors="coerce").dt.date.astype(str)
- _REPORT_EVENT_CACHE = all_events.copy()
- return all_events
- def _state(base_dir: Path) -> dict[str, object]:
- path = base_dir / "dragon_forward_observation_state.json"
- return {} if not path.exists() else json.loads(path.read_text(encoding="utf-8"))
- def _missing(value: object) -> bool:
- return value is None or pd.isna(value)
- def _fmt_pct(value: object) -> str:
- return "" if _missing(value) else f"{float(value):.2%}"
- def _fmt_num(value: object, digits: int = 2) -> str:
- return "" if _missing(value) else f"{float(value):.{digits}f}"
- def _fmt_int(value: object) -> str:
- return "" if _missing(value) else str(int(float(value)))
- def _fmt_bool(value: object) -> str:
- return "" if _missing(value) else ("是" if bool(value) else "否")
- def _badge(value: object) -> str:
- label = "" if _missing(value) else str(value)
- color = {
- "ok": "#207868",
- "warning": "#b7791f",
- "hard_breach": "#c53030",
- "missing_data": "#7b341e",
- "none": "#5a6472",
- "mild": "#b7791f",
- "material": "#c05621",
- "review_required": "#9b2c2c",
- }.get(label, "#5a6472")
- return f'<span class="badge" style="background:{color};">{escape(label or "n/a")}</span>'
- def _branch_name(branch: object) -> str:
- return BRANCH_LABELS.get(str(branch), str(branch))
- def _metric(label: str, value: str, tone: str = "plain") -> str:
- return f'<div class="metric {tone}"><div class="metric-label">{escape(label)}</div><div class="metric-value">{value}</div></div>'
- def _detail_query_href(hrefs: dict[str, str], branch: str | None = None, keyword: str | None = None, year: str | None = None) -> str:
- params: dict[str, str] = {}
- if branch:
- params["branch"] = branch
- if keyword:
- params["keyword"] = keyword
- if year:
- params["year"] = year
- query = urlencode(params)
- return hrefs["detail"] if not query else f'{hrefs["detail"]}?{query}'
- def _table(df: pd.DataFrame, formatters: dict[str, object] | None = None) -> str:
- if df.empty:
- return '<div class="empty">暂无数据</div>'
- formatters = formatters or {}
- head = "".join(f"<th>{escape(str(col))}</th>" for col in df.columns)
- body: list[str] = []
- for _, row in df.iterrows():
- cells: list[str] = []
- for col in df.columns:
- value = row[col]
- formatter = formatters.get(col)
- rendered = formatter(value) if callable(formatter) else ("" if _missing(value) else escape(str(value)))
- cells.append(f"<td>{rendered}</td>")
- body.append("<tr>" + "".join(cells) + "</tr>")
- return f"<table><thead><tr>{head}</tr></thead><tbody>{''.join(body)}</tbody></table>"
- def _hrefs(latest_bar_date: str, archive_mode: bool) -> dict[str, str]:
- if archive_mode:
- return {
- "home": "index.html",
- "daily": f"dragon_daily_signal_report_{latest_bar_date}.html",
- "weekly": f"dragon_forward_weekly_review_{latest_bar_date}.html",
- "detail": f"dragon_historical_trade_details_{latest_bar_date}.html",
- "daily_archive": f"dragon_daily_signal_report_{latest_bar_date}.html",
- "weekly_archive": f"dragon_forward_weekly_review_{latest_bar_date}.html",
- "guide": "dragon_indicator_strategy_guide_cn.html",
- "usage": "dragon_html_report_usage_cn.html",
- "quickstart": "dragon_html_report_quickstart_cn.html",
- }
- return {
- "home": "dragon_reports_index.html",
- "daily": "dragon_daily_signal_report.html",
- "weekly": "dragon_forward_weekly_review.html",
- "detail": "dragon_historical_trade_details.html",
- "daily_archive": f"html_reports/dragon_daily_signal_report_{latest_bar_date}.html",
- "weekly_archive": f"html_reports/dragon_forward_weekly_review_{latest_bar_date}.html",
- "guide": "dragon_indicator_strategy_guide_cn.html",
- "usage": "dragon_html_report_usage_cn.html",
- "quickstart": "dragon_html_report_quickstart_cn.html",
- }
- def _nav(hrefs: dict[str, str]) -> str:
- return (
- '<div class="nav">'
- f'<a href="{escape(hrefs["home"])}">总览首页</a>'
- f'<a href="{escape(hrefs["daily"])}">每日报告</a>'
- f'<a href="{escape(hrefs["weekly"])}">每周报告</a>'
- f'<a href="{escape(hrefs["detail"])}">历史明细</a>'
- f'<a href="{escape(hrefs["guide"])}">指标原理</a>'
- f'<a href="{escape(hrefs["quickstart"])}">极简说明</a>'
- f'<a href="{escape(hrefs["usage"])}">详细说明</a>'
- f'<a href="{escape(hrefs["daily_archive"])}">日报归档</a>'
- f'<a href="{escape(hrefs["weekly_archive"])}">周报归档</a>'
- "</div>"
- )
- def _shell(title: str, body: str) -> str:
- return f"""<!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>{escape(title)}</title>
- <style>
- :root{{--bg:#efe6d4;--paper:#fffaf1;--ink:#1c2430;--muted:#5f6875;--line:#d9cfbc;--deep:#173f35;--teal:#0f766e;--sand:#f4ebdc;--shadow:0 10px 24px rgba(41,52,66,.06)}}
- *{{box-sizing:border-box}} body{{margin:0;font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--ink)}}
- .wrap{{max-width:1380px;margin:0 auto;padding:22px 18px 42px}} .nav{{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}}
- .nav a{{text-decoration:none;color:var(--deep);background:#fff8ef;border:1px solid var(--line);padding:9px 14px;border-radius:999px;font-size:13px}}
- .hero{{background:linear-gradient(135deg,#183f36 0%,#216858 52%,#eadbc0 52%,#f9f4e7 100%);color:#fff;padding:28px 30px;border-radius:22px;margin-bottom:18px;box-shadow:var(--shadow)}}
- .hero h1{{margin:0 0 8px;font-size:34px}} .hero p{{margin:0;max-width:980px;line-height:1.7;font-size:14px;color:rgba(255,255,255,.92)}}
- .chips{{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px}} .chip{{background:rgba(255,255,255,.14);padding:7px 12px;border-radius:999px;font-size:12px}}
- .grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin-bottom:18px}} .panel,.section{{background:var(--paper);border:1px solid var(--line);border-radius:18px;padding:18px;box-shadow:var(--shadow)}}
- .panel h2,.section h2{{margin:0 0 12px;color:var(--teal);font-size:20px}} .metrics{{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}}
- .metric{{background:var(--sand);border-radius:12px;padding:12px}} .metric.good{{background:#dff3ee}} .metric.warn{{background:#f7ecd1}} .metric.risk{{background:#f7dede}}
- .metric-label{{font-size:12px;color:var(--muted);margin-bottom:4px}} .metric-value{{font-size:20px;font-weight:700}}
- .cards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}} .card{{border:1px solid var(--line);border-radius:16px;padding:16px;background:#fffdf9}}
- .role{{display:inline-block;background:#e4f4f0;color:var(--teal);border-radius:999px;padding:4px 10px;font-size:12px;font-weight:700;margin-bottom:8px}} .name{{font-size:18px;font-weight:700;margin-bottom:6px}}
- .desc{{font-size:13px;color:var(--muted);line-height:1.7;margin-bottom:10px}} .facts{{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}} .fact{{background:#faf4e8;border:1px solid #e6dcc9;border-radius:12px;padding:10px}}
- .callout{{margin-top:12px;padding:12px 14px;border-left:4px solid #c28a2c;background:#f8efdf;border-radius:10px;font-size:13px;line-height:1.7;color:#5b4a2f}}
- .chart-card{{background:#fffdf9;border:1px solid var(--line);border-radius:16px;padding:14px}} .chart-title{{font-size:16px;font-weight:700;margin-bottom:8px;color:var(--deep)}} .chart-sub{{font-size:12px;color:var(--muted);margin-bottom:10px}} .chart-wrap{{overflow-x:auto}}
- svg{{width:100%;height:auto;display:block}} table{{width:100%;border-collapse:collapse;font-size:13px}} th,td{{padding:10px 12px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}} th{{background:#f2e8d7}}
- .snapshot-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:14px;margin-top:14px}}
- .snapshot-card{{border:1px solid var(--line);border-radius:14px;background:#fffdf9;padding:12px}}
- .snapshot-title{{font-size:14px;font-weight:700;color:var(--deep);margin-bottom:8px}}
- .snapshot-rule{{margin-bottom:8px;font-size:12px;color:var(--muted);line-height:1.5}}
- .mini-table-wrap{{overflow-x:auto}}
- .mini-table{{width:100%;border-collapse:collapse;font-size:12px}}
- .mini-table th,.mini-table td{{padding:6px 8px;border-bottom:1px solid #eadfcd;white-space:nowrap}}
- .mini-table th{{background:#f7efe0;font-size:11px;color:var(--muted)}}
- .mini-table tr.event-row td{{background:#e8f6f1;font-weight:700}}
- .event-pill{{display:inline-block;padding:2px 8px;border-radius:999px;background:#0f766e;color:#fff;font-size:11px}}
- .signal-pill{{display:inline-block;padding:2px 6px;border-radius:999px;background:#f1e5c9;color:#6b4f1d;font-size:11px}}
- .trade-pill{{display:inline-block;padding:2px 6px;border-radius:999px;color:#fff;font-size:11px;margin-right:4px}}
- .trade-pill.real-buy{{background:#1f7a5c}}
- .trade-pill.real-sell{{background:#b45309}}
- .trade-pill.aux-buy{{background:#2563eb}}
- .trade-pill.aux-sell{{background:#7c3aed}}
- .summary-pill{{display:inline-block;padding:2px 8px;border-radius:999px;background:#e9efe8;color:#1f2937;font-size:11px;margin:0 6px 6px 0}}
- .snapshot-summary{{margin-top:10px;padding:10px 12px;border:1px solid #e6dcc9;border-radius:12px;background:#faf6ed;font-size:12px;line-height:1.7;color:#3f4956}}
- .snapshot-note{{margin-top:8px;font-size:12px;color:var(--muted);line-height:1.6}}
- .badge{{display:inline-block;padding:3px 9px;border-radius:999px;color:#fff;font-size:12px;font-weight:700}} .empty{{color:var(--muted)}} .footer{{margin-top:18px;text-align:right;color:var(--muted);font-size:12px}}
- @media (max-width:720px){{.hero h1{{font-size:28px}} .metrics,.facts{{grid-template-columns:1fr}} th,td{{padding:8px 9px;font-size:12px}}}}
- </style>
- </head>
- <body><div class="wrap">{body}<div class="footer">Dragon v2 HTML reports</div></div></body></html>"""
- def _svg_line_chart(df: pd.DataFrame, y_col: str, width: int = 1120, height: int = 320) -> str:
- if df.empty:
- return '<div class="empty">暂无图表数据</div>'
- pad_left, pad_right, pad_top, pad_bottom = 46, 20, 16, 32
- inner_w, inner_h = width - pad_left - pad_right, height - pad_top - pad_bottom
- y_min, y_max = float(df[y_col].min()), float(df[y_col].max())
- if y_min == y_max:
- y_min -= 1.0
- y_max += 1.0
- parts: list[str] = []
- for level in range(5):
- ratio = level / 4
- y_val = y_max - (y_max - y_min) * ratio
- y_px = pad_top + inner_h * ratio
- parts.append(f'<line x1="{pad_left}" y1="{y_px:.1f}" x2="{width - pad_right}" y2="{y_px:.1f}" stroke="#e8decb" stroke-width="1"/>')
- parts.append(f'<text x="{pad_left - 8}" y="{y_px + 4:.1f}" text-anchor="end" font-size="11" fill="#6b7280">{y_val:.2f}</text>')
- for idx, branch in enumerate(BRANCH_ORDER):
- sub = df[df["branch"] == branch].reset_index(drop=True)
- if sub.empty:
- continue
- coords: list[str] = []
- for pos, (_, row) in enumerate(sub.iterrows()):
- x_ratio = 0 if len(sub) == 1 else pos / (len(sub) - 1)
- x_px = pad_left + inner_w * x_ratio
- y_ratio = (float(row[y_col]) - y_min) / (y_max - y_min)
- y_px = pad_top + inner_h * (1 - y_ratio)
- coords.append(f"{x_px:.1f},{y_px:.1f}")
- parts.append(f'<polyline fill="none" stroke="{BRANCH_COLORS[branch]}" stroke-width="2.5" points="{" ".join(coords)}"/>')
- ly = pad_top + 18 + idx * 18
- parts.append(f'<line x1="{width - 240}" y1="{ly}" x2="{width - 220}" y2="{ly}" stroke="{BRANCH_COLORS[branch]}" stroke-width="3"/>')
- parts.append(f'<text x="{width - 212}" y="{ly + 4}" font-size="12" fill="#374151">{escape(_branch_name(branch))}</text>')
- return f'<div class="chart-wrap"><svg viewBox="0 0 {width} {height}">{"".join(parts)}</svg></div>'
- def _svg_bar_chart(df: pd.DataFrame, width: int = 1120, height: int = 320) -> str:
- if df.empty:
- return '<div class="empty">暂无图表数据</div>'
- years = [str(v) for v in sorted(df["sell_year"].unique())]
- pad_left, pad_right, pad_top, pad_bottom = 46, 20, 16, 36
- inner_w, inner_h = width - pad_left - pad_right, height - pad_top - pad_bottom
- y_min = float(min(0.0, df["return_pct"].min()))
- y_max = float(max(0.0, df["return_pct"].max()))
- if y_min == y_max:
- y_min -= 1.0
- y_max += 1.0
- zero_y = pad_top + inner_h * (1 - ((0.0 - y_min) / (y_max - y_min)))
- parts = [f'<line x1="{pad_left}" y1="{zero_y:.1f}" x2="{width - pad_right}" y2="{zero_y:.1f}" stroke="#c9b89f" stroke-width="1.5"/>']
- group_w = inner_w / max(1, len(years))
- bar_w = max(8.0, min(18.0, group_w / 5))
- for year_idx, year in enumerate(years):
- group_x = pad_left + group_w * year_idx
- parts.append(f'<text x="{group_x + group_w / 2:.1f}" y="{height - 12}" text-anchor="middle" font-size="11" fill="#6b7280">{escape(year)}</text>')
- for branch_idx, branch in enumerate(BRANCH_ORDER):
- sub = df[(df["sell_year"].astype(str) == year) & (df["branch"] == branch)]
- if sub.empty:
- continue
- value = float(sub.iloc[0]["return_pct"])
- value_y = pad_top + inner_h * (1 - ((value - y_min) / (y_max - y_min)))
- rect_y = value_y if value >= 0 else zero_y
- rect_h = abs(zero_y - value_y)
- rect_x = group_x + 8 + branch_idx * (bar_w + 5)
- parts.append(f'<rect x="{rect_x:.1f}" y="{rect_y:.1f}" width="{bar_w:.1f}" height="{max(rect_h,1):.1f}" fill="{BRANCH_COLORS[branch]}" rx="3"/>')
- for idx, branch in enumerate(BRANCH_ORDER):
- ly = pad_top + 18 + idx * 18
- parts.append(f'<rect x="{width - 245}" y="{ly - 8}" width="14" height="14" fill="{BRANCH_COLORS[branch]}" rx="3"/>')
- parts.append(f'<text x="{width - 223}" y="{ly + 3}" font-size="12" fill="#374151">{escape(_branch_name(branch))}</text>')
- return f'<div class="chart-wrap"><svg viewBox="0 0 {width} {height}">{"".join(parts)}</svg></div>'
- def _strategy_cards(overview: pd.DataFrame, status_df: pd.DataFrame) -> str:
- desc = {
- "workbook_preserving": "尽量保留工作簿结构,适合重构核对与审计。",
- "alpha_first_selective_veto": "平衡版本,兼顾一致性与收益质量。",
- "alpha_first_glued_refined_hot_cap": "当前量化结果最强的 RC1 前向默认分支。",
- }
- status_map = {str(row["branch"]): row for _, row in status_df.iterrows()}
- cards: list[str] = []
- for branch in BRANCH_ORDER:
- row_df = overview[overview["branch"] == branch]
- if row_df.empty:
- continue
- row = row_df.iloc[0]
- status = status_map.get(branch)
- latest_event = "暂无" if status is None else f"{status['latest_real_event_date']} {status['latest_real_event_side']} {status['latest_real_event_reason']}".strip()
- latest_pos = "" if status is None else _fmt_bool(status["in_position"])
- facts = [
- ("年化", _fmt_pct(row["cagr"])),
- ("总收益", _fmt_pct(row["compounded_return"])),
- ("PF", _fmt_num(row["profit_factor"])),
- ("交易笔数", _fmt_int(row["trades"])),
- ("BUY对齐", _fmt_int(row["real_buy_overlap"])),
- ("最新持仓", latest_pos),
- ]
- facts_html = "".join(f'<div class="fact"><div class="metric-label">{escape(k)}</div><div class="metric-value">{escape(v)}</div></div>' for k, v in facts)
- cards.append(
- f'<div class="card"><div class="role">{escape(BRANCH_ROLES.get(branch, ""))}</div><div class="name">{escape(_branch_name(branch))}</div><div class="desc">{escape(desc.get(branch, ""))}</div><div class="facts">{facts_html}</div><div class="callout">最新真实事件:{escape(latest_event)}<br>SELL 对齐:{_fmt_int(row["real_sell_overlap"])}</div></div>'
- )
- return '<div class="cards">' + "".join(cards) + "</div>"
- def _index_charts(base_dir: Path) -> str:
- equity = _load_csv(base_dir / "dragon_equity_curve_review.csv")
- drawdown = _load_csv(base_dir / "dragon_drawdown_review.csv")
- yearly = _load_csv(base_dir / "dragon_yearly_return_review.csv")
- if not equity.empty:
- equity["trade_no"] = equity.groupby("branch").cumcount() + 1
- if not drawdown.empty:
- drawdown["策略"] = drawdown["branch"].map(_branch_name)
- drawdown = drawdown[["策略", "trades", "compounded_return", "cagr", "max_drawdown", "drawdown_duration_trades", "calmar"]].rename(columns={"trades": "交易笔数", "compounded_return": "总收益", "cagr": "年化", "max_drawdown": "最大回撤", "drawdown_duration_trades": "回撤持续笔数", "calmar": "Calmar"})
- return f"""
- <div class="section">
- <h2>收益与回撤可视化</h2>
- <div class="grid">
- <div class="chart-card"><div class="chart-title">累计净值曲线</div><div class="chart-sub">按交易结束顺序绘制三条策略的累计净值。</div>{_svg_line_chart(equity, 'equity')}</div>
- <div class="chart-card"><div class="chart-title">年度收益对比</div><div class="chart-sub">按卖出年份聚合,观察不同年份的稳定性。</div>{_svg_bar_chart(yearly)}</div>
- </div>
- <div style="margin-top:16px;">{_table(drawdown, {"总收益": _fmt_pct, "年化": _fmt_pct, "最大回撤": _fmt_pct, "Calmar": _fmt_num})}</div>
- </div>
- """
- def _inline_code(text: str) -> str:
- return re.sub(r"`([^`]+)`", lambda m: f"<code>{escape(m.group(1))}</code>", escape(text))
- def _markdown_to_html(text: str) -> str:
- blocks: list[str] = []
- lines = text.splitlines()
- in_ul = False
- in_ol = False
- in_code = False
- code_lines: list[str] = []
- para_lines: list[str] = []
- def close_para() -> None:
- nonlocal para_lines
- if para_lines:
- blocks.append(f"<p>{'<br>'.join(_inline_code(line) for line in para_lines)}</p>")
- para_lines = []
- def close_lists() -> None:
- nonlocal in_ul, in_ol
- if in_ul:
- blocks.append("</ul>")
- in_ul = False
- if in_ol:
- blocks.append("</ol>")
- in_ol = False
- for raw in lines:
- line = raw.rstrip()
- stripped = line.strip()
- if stripped.startswith("```"):
- close_para()
- close_lists()
- if in_code:
- blocks.append(f"<pre><code>{escape(chr(10).join(code_lines))}</code></pre>")
- code_lines = []
- in_code = False
- else:
- in_code = True
- continue
- if in_code:
- code_lines.append(raw)
- continue
- if not stripped:
- close_para()
- close_lists()
- continue
- if stripped.startswith("# "):
- close_para()
- close_lists()
- blocks.append(f"<h1>{_inline_code(stripped[2:])}</h1>")
- continue
- if stripped.startswith("## "):
- close_para()
- close_lists()
- blocks.append(f"<h2>{_inline_code(stripped[3:])}</h2>")
- continue
- if stripped.startswith("### "):
- close_para()
- close_lists()
- blocks.append(f"<h3>{_inline_code(stripped[4:])}</h3>")
- continue
- if stripped.startswith("- "):
- close_para()
- if in_ol:
- blocks.append("</ol>")
- in_ol = False
- if not in_ul:
- blocks.append("<ul>")
- in_ul = True
- blocks.append(f"<li>{_inline_code(stripped[2:])}</li>")
- continue
- if re.match(r"^\d+\.\s", stripped):
- close_para()
- if in_ul:
- blocks.append("</ul>")
- in_ul = False
- if not in_ol:
- blocks.append("<ol>")
- in_ol = True
- item = re.sub(r"^\d+\.\s+", "", stripped)
- blocks.append(f"<li>{_inline_code(item)}</li>")
- continue
- para_lines.append(stripped)
- close_para()
- close_lists()
- if in_code:
- blocks.append(f"<pre><code>{escape(chr(10).join(code_lines))}</code></pre>")
- return "".join(blocks)
- def build_doc_html(base_dir: Path, md_name: str, title: str, subtitle: str, archive_mode: bool = False) -> str:
- s = _state(base_dir)
- latest_bar_date = str(s.get("latest_bar_date", "latest"))
- hrefs = _hrefs(latest_bar_date, archive_mode)
- text = (base_dir / md_name).read_text(encoding="utf-8")
- hero = (
- f'{_nav(hrefs)}'
- f'<div class="hero"><h1>{escape(title)}</h1><p>{escape(subtitle)}</p>'
- f'<div class="chips"><div class="chip">{escape(md_name)}</div></div></div>'
- )
- home_link = f'<a href="{escape(hrefs["home"])}">打开</a>'
- daily_link = f'<a href="{escape(hrefs["daily"])}">打开</a>'
- weekly_link = f'<a href="{escape(hrefs["weekly"])}">打开</a>'
- quick_link = f'<a href="{escape(hrefs["quickstart"])}">打开</a>'
- guide_link = f'<a href="{escape(hrefs["guide"])}">打开</a>'
- links = (
- '<div class="section"><h2>相关入口</h2><div class="metrics">'
- + _metric("总览首页", home_link)
- + _metric("每日报告", daily_link)
- + _metric("每周报告", weekly_link)
- + _metric("指标原理", guide_link)
- + _metric("极简说明", quick_link)
- + "</div></div>"
- )
- content = f'<div class="section doc">{_markdown_to_html(text)}</div>'
- return _shell(title, hero + links + content)
- def _indicator_guide_visuals() -> str:
- system_svg = """
- <div class="chart-card">
- <div class="chart-title">指标如何分工</div>
- <div class="chart-sub">先看位置,再看强弱,最后决定时点。</div>
- <div class="chart-wrap">
- <svg viewBox="0 0 1120 320">
- <rect x="40" y="70" width="200" height="110" rx="18" fill="#f8efdf" stroke="#c28a2c"/>
- <text x="140" y="110" text-anchor="middle" font-size="24" fill="#173f35">C1</text>
- <text x="140" y="140" text-anchor="middle" font-size="14" fill="#5f6875">看市场在高位、中位</text>
- <text x="140" y="162" text-anchor="middle" font-size="14" fill="#5f6875">还是低位</text>
- <rect x="310" y="70" width="240" height="110" rx="18" fill="#e4f4f0" stroke="#0f766e"/>
- <text x="430" y="110" text-anchor="middle" font-size="24" fill="#173f35">A1 / B1</text>
- <text x="430" y="140" text-anchor="middle" font-size="14" fill="#5f6875">看趋势强不强</text>
- <text x="430" y="162" text-anchor="middle" font-size="14" fill="#5f6875">动能有没有衰减</text>
- <rect x="620" y="70" width="240" height="110" rx="18" fill="#efe7fb" stroke="#7c3aed"/>
- <text x="740" y="110" text-anchor="middle" font-size="24" fill="#173f35">KDJ / QL</text>
- <text x="740" y="140" text-anchor="middle" font-size="14" fill="#5f6875">看短期拐点和突破确认</text>
- <text x="740" y="162" text-anchor="middle" font-size="14" fill="#5f6875">决定什么时候动手</text>
- <rect x="930" y="70" width="150" height="110" rx="18" fill="#ffe8e0" stroke="#c05621"/>
- <text x="1005" y="110" text-anchor="middle" font-size="24" fill="#173f35">交易动作</text>
- <text x="1005" y="140" text-anchor="middle" font-size="14" fill="#5f6875">买入 / 持有</text>
- <text x="1005" y="162" text-anchor="middle" font-size="14" fill="#5f6875">止盈 / 卖出</text>
- <line x1="240" y1="125" x2="310" y2="125" stroke="#8b6f47" stroke-width="4"/>
- <polygon points="310,125 296,117 296,133" fill="#8b6f47"/>
- <line x1="550" y1="125" x2="620" y2="125" stroke="#0f766e" stroke-width="4"/>
- <polygon points="620,125 606,117 606,133" fill="#0f766e"/>
- <line x1="860" y1="125" x2="930" y2="125" stroke="#7c3aed" stroke-width="4"/>
- <polygon points="930,125 916,117 916,133" fill="#7c3aed"/>
- </svg>
- </div>
- </div>
- """
- branch_svg = """
- <div class="chart-card">
- <div class="chart-title">三种策略版本的区别</div>
- <div class="chart-sub">不是换了一套完全不同的指标,而是对同一套规则做不同程度的过滤与取舍。</div>
- <div class="chart-wrap">
- <svg viewBox="0 0 1120 320">
- <rect x="60" y="55" width="280" height="180" rx="20" fill="#faf4e8" stroke="#8b6f47"/>
- <text x="200" y="95" text-anchor="middle" font-size="24" fill="#173f35">workbook_preserving</text>
- <text x="200" y="130" text-anchor="middle" font-size="15" fill="#5f6875">最像原始工作簿</text>
- <text x="200" y="156" text-anchor="middle" font-size="15" fill="#5f6875">优先保留历史路径</text>
- <text x="200" y="182" text-anchor="middle" font-size="15" fill="#5f6875">适合重构核对</text>
- <rect x="420" y="55" width="280" height="180" rx="20" fill="#e4f4f0" stroke="#0f766e"/>
- <text x="560" y="95" text-anchor="middle" font-size="24" fill="#173f35">alpha_first_selective_veto</text>
- <text x="560" y="130" text-anchor="middle" font-size="15" fill="#5f6875">平衡版本</text>
- <text x="560" y="156" text-anchor="middle" font-size="15" fill="#5f6875">保留一致性</text>
- <text x="560" y="182" text-anchor="middle" font-size="15" fill="#5f6875">同时删掉部分低质量交易</text>
- <rect x="780" y="55" width="280" height="180" rx="20" fill="#ffe8e0" stroke="#c05621"/>
- <text x="920" y="95" text-anchor="middle" font-size="24" fill="#173f35">RC1 / refined</text>
- <text x="920" y="130" text-anchor="middle" font-size="15" fill="#5f6875">当前收益质量最强</text>
- <text x="920" y="156" text-anchor="middle" font-size="15" fill="#5f6875">继续过滤弱短持交易</text>
- <text x="920" y="182" text-anchor="middle" font-size="15" fill="#5f6875">更偏实战 alpha</text>
- <line x1="340" y1="145" x2="420" y2="145" stroke="#8b6f47" stroke-width="4"/>
- <polygon points="420,145 406,137 406,153" fill="#8b6f47"/>
- <line x1="700" y1="145" x2="780" y2="145" stroke="#0f766e" stroke-width="4"/>
- <polygon points="780,145 766,137 766,153" fill="#0f766e"/>
- <text x="560" y="265" text-anchor="middle" font-size="14" fill="#5f6875">从左到右:越来越强调收益质量,和原始工作簿的距离也越来越大</text>
- </svg>
- </div>
- </div>
- """
- flow_svg = """
- <div class="chart-card">
- <div class="chart-title">从看到信号到完成交易的流程</div>
- <div class="chart-sub">普通投资者可以把它理解为一套分层决策流程,而不是单一指标下指令。</div>
- <div class="chart-wrap">
- <svg viewBox="0 0 1120 380">
- <rect x="80" y="40" width="190" height="70" rx="16" fill="#f8efdf" stroke="#c28a2c"/>
- <text x="175" y="82" text-anchor="middle" font-size="20" fill="#173f35">先看 C1</text>
- <text x="175" y="102" text-anchor="middle" font-size="13" fill="#5f6875">判断高位/中位/低位</text>
- <rect x="330" y="40" width="220" height="70" rx="16" fill="#e4f4f0" stroke="#0f766e"/>
- <text x="440" y="82" text-anchor="middle" font-size="20" fill="#173f35">再看 A1 / B1</text>
- <text x="440" y="102" text-anchor="middle" font-size="13" fill="#5f6875">判断趋势和动能是否支持</text>
- <rect x="610" y="40" width="220" height="70" rx="16" fill="#efe7fb" stroke="#7c3aed"/>
- <text x="720" y="82" text-anchor="middle" font-size="20" fill="#173f35">看 KDJ / QL</text>
- <text x="720" y="102" text-anchor="middle" font-size="13" fill="#5f6875">决定时点是否确认</text>
- <rect x="890" y="40" width="150" height="70" rx="16" fill="#ffe8e0" stroke="#c05621"/>
- <text x="965" y="82" text-anchor="middle" font-size="20" fill="#173f35">执行买入</text>
- <text x="965" y="102" text-anchor="middle" font-size="13" fill="#5f6875">或放弃</text>
- <rect x="360" y="190" width="190" height="90" rx="16" fill="#f9f4e7" stroke="#8b6f47"/>
- <text x="455" y="230" text-anchor="middle" font-size="18" fill="#173f35">持仓后继续观察</text>
- <text x="455" y="252" text-anchor="middle" font-size="13" fill="#5f6875">是否继续走强</text>
- <text x="455" y="272" text-anchor="middle" font-size="13" fill="#5f6875">是否出现辅助信号</text>
- <rect x="650" y="190" width="190" height="90" rx="16" fill="#ffe8e0" stroke="#c05621"/>
- <text x="745" y="230" text-anchor="middle" font-size="18" fill="#173f35">卖出或止盈</text>
- <text x="745" y="252" text-anchor="middle" font-size="13" fill="#5f6875">趋势转弱</text>
- <text x="745" y="272" text-anchor="middle" font-size="13" fill="#5f6875">或达到风险退出条件</text>
- <line x1="270" y1="75" x2="330" y2="75" stroke="#8b6f47" stroke-width="4"/><polygon points="330,75 316,67 316,83" fill="#8b6f47"/>
- <line x1="550" y1="75" x2="610" y2="75" stroke="#0f766e" stroke-width="4"/><polygon points="610,75 596,67 596,83" fill="#0f766e"/>
- <line x1="830" y1="75" x2="890" y2="75" stroke="#7c3aed" stroke-width="4"/><polygon points="890,75 876,67 876,83" fill="#7c3aed"/>
- <line x1="965" y1="110" x2="965" y2="145" stroke="#c05621" stroke-width="4"/>
- <line x1="965" y1="145" x2="455" y2="145" stroke="#c05621" stroke-width="4"/>
- <line x1="455" y1="145" x2="455" y2="190" stroke="#c05621" stroke-width="4"/>
- <polygon points="455,190 447,176 463,176" fill="#c05621"/>
- <line x1="550" y1="235" x2="650" y2="235" stroke="#8b6f47" stroke-width="4"/><polygon points="650,235 636,227 636,243" fill="#8b6f47"/>
- </svg>
- </div>
- </div>
- """
- return f'<div class="section"><h2>图解说明</h2><div class="grid">{system_svg}{branch_svg}</div><div style="margin-top:16px;">{flow_svg}</div></div>'
- def _indicator_guide_indicator_cards() -> str:
- cards = [
- {
- "name": "C1",
- "tag": "先看位置",
- "summary": "先判断现在更像高位、中位,还是低位/超跌区。",
- "facts": [
- ("主要作用", "先做市场分层"),
- ("更像什么", "市场所在楼层"),
- ("高位时", "更重风险控制"),
- ("低位时", "更重反转与恢复"),
- ],
- "callout": "同样一个金叉,放在低位和高位,含义完全不同。C1 不是告诉你今天涨不涨,而是先告诉你现在站在哪里。",
- },
- {
- "name": "A1",
- "tag": "再看趋势热度",
- "summary": "看短中期结构是在扩张,还是开始降温。",
- "facts": [
- ("主要作用", "趋势背景过滤"),
- ("更像什么", "趋势温度计"),
- ("走强时", "环境更支持持有"),
- ("回落时", "先警惕,不急着恋战"),
- ],
- "callout": "A1 更像背景,不是单独的买卖按钮。它回答的是:现在这段行情值不值得参与、值不值得继续拿。",
- },
- {
- "name": "B1",
- "tag": "再看动能后劲",
- "summary": "看这波行情还有没有继续扩张的力量。",
- "facts": [
- ("主要作用", "真假反弹过滤"),
- ("更像什么", "行情后劲表"),
- ("偏强时", "趋势更容易延续"),
- ("走坏时", "更容易触发减仓/退出"),
- ],
- "callout": "很多看起来像反弹的地方,最后没做成交易,往往不是因为没信号,而是 B1 告诉策略:这波后劲不够。",
- },
- {
- "name": "KDJ",
- "tag": "决定短期时点",
- "summary": "看短期节奏有没有拐头,适合用来点火或确认。",
- "facts": [
- ("主要作用", "短期时点确认"),
- ("更像什么", "点火器"),
- ("买入端", "低位拐头时更有用"),
- ("卖出端", "先发现节奏转弱"),
- ],
- "callout": "KDJ 更灵敏,能更早发现变化,但也更容易受短期波动影响,所以通常不会单独决定整笔交易。",
- },
- {
- "name": "QL 凤凰线",
- "tag": "决定是否真突破",
- "summary": "看价格是否真的冲出了动态边界,或真的跌破了支撑带。",
- "facts": [
- ("主要作用", "突破/跌破确认"),
- ("更像什么", "动态通道边界"),
- ("QL buy", "价格真突破"),
- ("QL sell", "价格真掉下来"),
- ],
- "callout": "QL 比 KDJ 更偏确认。KDJ 先告诉你节奏变了,QL 再告诉你价格层面也真的动了。",
- },
- ]
- parts: list[str] = ['<div class="section"><h2>指标速查卡</h2><div class="cards">']
- for item in cards:
- facts_html = "".join(
- f'<div class="fact"><div class="metric-label">{escape(label)}</div><div class="metric-value">{escape(value)}</div></div>'
- for label, value in item["facts"]
- )
- parts.append(
- '<div class="card">'
- f'<div class="role">{escape(item["tag"])}</div>'
- f'<div class="name">{escape(item["name"])}</div>'
- f'<div class="desc">{escape(item["summary"])}</div>'
- f'<div class="facts">{facts_html}</div>'
- f'<div class="callout">{escape(item["callout"])}</div>'
- "</div>"
- )
- parts.append("</div></div>")
- return "".join(parts)
- def _indicator_guide_scene_table() -> str:
- scene_df = pd.DataFrame(
- [
- {
- "市场状态": "C1 低位 / 超跌区",
- "普通话理解": "市场可能已经跌深,开始找反转或恢复机会。",
- "策略重点": "先防假反弹,再等拐点确认。",
- "A1 / B1 关注点": "A1 不一定强,但 B1 不能太差;否则只是弱反抽。",
- "KDJ / QL 作用": "KDJ 更像点火,QL 用来确认不是一日游。",
- },
- {
- "市场状态": "C1 中位区",
- "普通话理解": "最像趋势延续区,容易出现顺势型交易。",
- "策略重点": "判断趋势是否继续、有没有必要跟。",
- "A1 / B1 关注点": "A1、B1 同时较稳时,更容易走出主升或延续段。",
- "KDJ / QL 作用": "用于控制入场时点和持仓中的退出节奏。",
- },
- {
- "市场状态": "C1 高位区",
- "普通话理解": "市场不一定马上见顶,但风险回报比开始变差。",
- "策略重点": "更重保护利润,不轻易追最后一段。",
- "A1 / B1 关注点": "A1 回落、B1 走坏时,风险提示意义更强。",
- "KDJ / QL 作用": "更多用于确认转弱、止盈或风险退出。",
- },
- ]
- )
- return (
- '<div class="section"><h2>按市场状态来理解指标</h2>'
- '<div class="callout">普通投资者最容易误解的一点,是把同一个信号放到所有位置都按同样方法解释。实际不是。先看 C1 分层,再看 A1/B1 强弱,最后再让 KDJ/QL 决定什么时候动手。</div>'
- f'{_table(scene_df)}'
- "</div>"
- )
- def _indicator_guide_compare_table(base_dir: Path) -> str:
- overview = _load_csv(base_dir / "dragon_strategy_overview.csv")
- if overview.empty:
- return '<div class="section"><h2>三版本当前统计对照</h2><div class="empty">暂无策略统计数据</div></div>'
- role_desc = {
- "workbook_preserving": "最重历史一致性",
- "alpha_first_selective_veto": "平衡控制版",
- "alpha_first_glued_refined_hot_cap": "RC1,收益质量优先",
- }
- overview["策略"] = overview["branch"].map(_branch_name)
- overview["定位"] = overview["branch"].map(lambda x: role_desc.get(str(x), ""))
- overview["真实交易对齐"] = overview.apply(
- lambda row: f'{_fmt_int(row["real_buy_overlap"])}/{_fmt_int(row["real_sell_overlap"])}',
- axis=1,
- )
- table_df = overview[
- ["策略", "定位", "trades", "win_rate", "avg_return", "profit_factor", "compounded_return", "cagr", "真实交易对齐"]
- ].rename(
- columns={
- "trades": "交易笔数",
- "win_rate": "胜率",
- "avg_return": "单笔均值",
- "profit_factor": "PF",
- "compounded_return": "总收益",
- "cagr": "年化",
- }
- )
- return (
- '<div class="section"><h2>三版本当前统计对照</h2>'
- '<div class="callout">三种版本看的还是同一套指标,核心差别不在“换指标”,而在“过滤强度”和“是否为了更强收益而主动放弃一部分历史路径一致性”。</div>'
- f'{_table(table_df, {"胜率": _fmt_pct, "单笔均值": _fmt_pct, "PF": _fmt_num, "总收益": _fmt_pct, "年化": _fmt_pct})}'
- "</div>"
- )
- def _indicator_guide_threshold_tables() -> str:
- rc1 = alpha_first_glued_refined_hot_cap_config()
- base_df = pd.DataFrame(
- [
- {
- "类别": "A1 基础判读",
- "具体值 / 区间": "`|A1| < 0.02`",
- "说明": "代码里把它视为 glued 一类的粘合状态,常用于趋势延续判断。",
- },
- {
- "类别": "A1 偏强",
- "具体值 / 区间": "`A1 > 0.028`",
- "说明": "属于明显正向扩张区,持仓中更容易被视为强势环境。",
- },
- {
- "类别": "A1 明显转弱",
- "具体值 / 区间": "`A1 < -0.04`",
- "说明": "代码里属于 big negative 区,风险退出和低位反转分支都会重点关注。",
- },
- {
- "类别": "B1 明显偏强",
- "具体值 / 区间": "`B1 > 0.17`",
- "说明": "属于 strong positive,通常表示中期动能足够强。",
- },
- {
- "类别": "B1 明显偏弱",
- "具体值 / 区间": "`B1 < -0.17`",
- "说明": "属于 hard negative,风险信号意义很强。",
- },
- {
- "类别": "C1 深低位",
- "具体值 / 区间": "`C1 < 16`",
- "说明": "当前策略里是深超跌/深低位的重要参考区间,很多低位反转逻辑从这里展开。",
- },
- {
- "类别": "C1 恢复区",
- "具体值 / 区间": "`18 <= C1 < 22`",
- "说明": "是 oversold recovery 一类规则的典型区间。",
- },
- {
- "类别": "C1 中高位",
- "具体值 / 区间": "`C1 > 60`",
- "说明": "开始进入更强调风险和止盈的区间,很多卖出/辅助看空规则会加强。",
- },
- {
- "类别": "C1 很高位",
- "具体值 / 区间": "`C1 > 80` / `C1 > 85`",
- "说明": "80 以上常视为明显高位,85 以上会触发更强的高位防守和辅助卖出过滤。",
- },
- {
- "类别": "KDJ / QL",
- "具体值 / 区间": "无固定单一数值",
- "说明": "它们是“是否发生金叉/死叉、上穿/下穿”的事件信号,不是像 A1/B1/C1 那样直接看一个绝对数值。",
- },
- ]
- )
- example_df = pd.DataFrame(
- [
- {
- "规则类型": "RC1 热区 glued 过滤举例",
- "当前典型值": f'`{_fmt_num(rc1.glued_selective_hot_c1_min, 1)} <= C1 <= {_fmt_num(rc1.glued_selective_hot_c1_max, 1)}` 且 `B1 >= {_fmt_num(rc1.glued_selective_hot_b1_min, 2)}`',
- "用途": "删掉一部分热区但质量不高的 glued 短持仓交易。",
- },
- {
- "规则类型": "RC1 低位弱区过滤举例",
- "当前典型值": f'`{_fmt_num(rc1.glued_selective_low_c1_min, 1)} <= C1 < {_fmt_num(rc1.glued_selective_low_c1_max, 1)}` 且 `B1 <= {_fmt_num(rc1.glued_selective_low_b1_max, 2)}`',
- "用途": "过滤低位弱修复但后劲不足的 glued 交易。",
- },
- {
- "规则类型": "深超跌反弹入场举例",
- "当前典型值": "`C1 < 16`,`A1 >= -0.09`,`B1 >= -0.10`",
- "用途": "给 deep_oversold_rebound_buy 提供基础入场框架;但会再叠加真假反弹过滤。",
- },
- {
- "规则类型": "oversold recovery 举例",
- "当前典型值": "`18 <= C1 < 22`,`-0.03 <= A1 <= 0`,`B1 >= -0.02`",
- "用途": "识别已经跌深、但正在恢复的结构。",
- },
- {
- "规则类型": "QL 后反转入场举例",
- "当前典型值": "`20 <= C1 < 26`,`-0.04 <= A1 <= 0`,`-0.22 <= B1 < 0`",
- "用途": "对应 oversold_reversal_after_ql_buy 的典型区间。",
- },
- {
- "规则类型": "QL 后反转弱样本阻断举例",
- "当前典型值": "`23 <= C1 < 26`,`B1 > -0.12`,`A1 > -0.035`",
- "用途": "这类组合更像弱反抽,所以在当前版本里会被主动拦掉。",
- },
- {
- "规则类型": "post-sell rebound 举例",
- "当前典型值": "`18 <= C1 < 30`,`-0.045 <= A1 <= 0`,`-0.09 <= B1 <= -0.04`",
- "用途": "卖出后再恢复的一类重启单,仍要求不是太弱的假反弹。",
- },
- {
- "规则类型": "predictive break 短持仓退出举例",
- "当前典型值": "`持仓 <= 2天`,`50 < C1 < 70`,`-0.02 < A1 < 0`,`B1 < -0.13`",
- "用途": "短持仓刚买就发现后劲不对,尽快认错退出。",
- },
- {
- "规则类型": "predictive break 长持仓退出举例",
- "当前典型值": "`持仓 >= 40天`,`60 < C1 < 65`,`-0.02 < A1 < 0`,`B1 < -0.12`,且 `7天内出现过 QL sell`",
- "用途": "长持仓后期在高位衰减中确认退出。",
- },
- {
- "规则类型": "空仓后辅助看空举例",
- "当前典型值": "`真实卖出后 10 天内` 的再次确认;重复同侧信号还会受 `5 天冷却` 约束",
- "用途": "这是辅助看空信号,不是新的真实卖点。",
- },
- ]
- )
- return (
- '<div class="section"><h2>详细指标值与触发举例</h2>'
- '<div class="callout">这部分分两层来看。第一张表是相对稳定的基础判读阈值;第二张表是当前 RC1 / 研究版本里常见规则的“典型触发区间”。后者不是全市场永远不变的唯一真理,而是当前代码里经常用到的实战阈值举例。</div>'
- f'{_table(base_df)}'
- '<div style="margin-top:16px;"></div>'
- f'{_table(example_df)}'
- "</div>"
- )
- def _indicator_guide_rule_tables(base_dir: Path, hrefs: dict[str, str]) -> str:
- details = _load_csv(base_dir / "dragon_historical_trade_details.csv")
- details = details[details["branch"] == "alpha_first_glued_refined_hot_cap"].copy() if not details.empty else details
- def count_exact(col: str, value: str) -> int:
- if details.empty:
- return 0
- return int((details[col] == value).sum())
- def count_prefix(col: str, prefix: str) -> int:
- if details.empty:
- return 0
- series = details[col].fillna("").astype(str)
- return int(series.str.startswith(prefix).sum())
- def rule_link(label: str, keyword: str) -> str:
- href = _detail_query_href(hrefs, branch="alpha_first_glued_refined_hot_cap", keyword=keyword)
- return f'<a href="{escape(href)}">{escape(label)}</a>'
- buy_df = pd.DataFrame(
- [
- {
- "规则名": rule_link("glued_buy", "glued_buy"),
- "RC1 历史次数": count_exact("buy_reason", "glued_buy"),
- "触发方式": "状态型,不靠单一金叉",
- "典型值 / 条件": "|A1| < 0.02;B1 不能继续走弱;不能落入 RC1 的热区/低位弱区过滤",
- "这条规则在做什么": "寻找中位到中高位的趋势延续,不做太弱、太短命的粘合单。",
- },
- {
- "规则名": rule_link("dual_gold_resonance_buy", "dual_gold_resonance_buy"),
- "RC1 历史次数": count_exact("buy_reason", "dual_gold_resonance_buy"),
- "触发方式": "KDJ / QL 共振 + 改善确认",
- "典型值 / 条件": "需要 dual_gold 共振;A1/B1 相比上一买入拐点不变差;若 18 < C1 < 20 且 A1 > -0.05 且 B1 < -0.09,会被视作假反弹拦掉",
- "这条规则在做什么": "做低位或恢复段里的共振型反转,但要防止低质量假起势。",
- },
- {
- "规则名": rule_link("deep_oversold_rebound_buy", "deep_oversold_rebound_buy"),
- "RC1 历史次数": count_prefix("buy_reason", "deep_oversold_rebound_buy"),
- "触发方式": "深超跌后的恢复尝试",
- "典型值 / 条件": "C1 < 16;A1 > -0.09;B1 > -0.10;若 13 < C1 < 15 且 A1 > -0.04 且 B1 < -0.08,或 13 < C1 < 14.5 且 A1 > -0.04 且 B1 > -0.06,会被过滤",
- "这条规则在做什么": "专门抓跌深后的恢复,但同时过滤一部分弱反抽和浅层假启动。",
- },
- {
- "规则名": rule_link("oversold_recovery_buy", "oversold_recovery_buy"),
- "RC1 历史次数": count_exact("buy_reason", "oversold_recovery_buy"),
- "触发方式": "区间恢复型",
- "典型值 / 条件": "18 <= C1 < 22;-0.03 <= A1 < 0;B1 > -0.02",
- "这条规则在做什么": "市场已经不在最恐慌位置,但仍在恢复段,尝试接恢复而不是接最低点。",
- },
- {
- "规则名": rule_link("post_sell_rebound_buy", "post_sell_rebound_buy"),
- "RC1 历史次数": count_exact("buy_reason", "post_sell_rebound_buy"),
- "触发方式": "卖出后再恢复",
- "典型值 / 条件": "距离上次 KDJ sell 不超过 7 天;常见区间是 18 <= C1 < 30、-0.045 <= A1 < 0、-0.09 < B1 < -0.04;也有更低位版本 C1 < 19、-0.13 < B1 < -0.09",
- "这条规则在做什么": "承认前面先退出,后面发现市场并没继续坏,再重新上车。",
- },
- {
- "规则名": rule_link("early_crash_probe_buy", "early_crash_probe_buy"),
- "RC1 历史次数": count_exact("buy_reason", "early_crash_probe_buy"),
- "触发方式": "恐慌试探",
- "典型值 / 条件": "C1 < 20;-0.07 < A1 < -0.04;-0.04 < B1 < 0",
- "这条规则在做什么": "在急跌后的极少数时刻做试探性进场,风险更高,不是常规顺势单。",
- },
- {
- "规则名": rule_link("oversold_reversal_after_ql_buy", "oversold_reversal_after_ql_buy"),
- "RC1 历史次数": count_exact("buy_reason", "oversold_reversal_after_ql_buy"),
- "触发方式": "前一根先出现 QL sell,后一根做反转",
- "典型值 / 条件": "20 <= C1 < 26;-0.04 <= A1 < 0;-0.22 < B1 < 0;若 23 < C1 < 26 且 B1 > -0.12 且 A1 > -0.035,会被视为弱反抽拦掉",
- "这条规则在做什么": "处理先被砸一下、但很快重新恢复的低位反转。",
- },
- {
- "规则名": rule_link("predictive_error_reentry_buy", "predictive_error_reentry_buy"),
- "RC1 历史次数": count_exact("buy_reason", "predictive_error_reentry_buy"),
- "触发方式": "认错后回补",
- "典型值 / 条件": "常见于上次真实卖出后较短时间内;-0.02 < A1 < 0.01;B1 > -0.16;C1 > 50",
- "这条规则在做什么": "前面提前卖了,后面发现卖错了,就按更稳的恢复条件回补。",
- },
- ]
- )
- sell_df = pd.DataFrame(
- [
- {
- "规则名": rule_link("knife_take_profit_2_glued", "knife_take_profit_2_glued"),
- "RC1 历史次数": count_exact("sell_reason", "knife_take_profit_2_glued"),
- "触发方式": "持仓中的 glued 止盈",
- "典型值 / 条件": "前提是 entry_reason = glued_buy 且当前仍属 glued;若 B1 < 0.17 即可止盈;如果先出现 ql_sell 但还想再等确认,会转入 wait_ql 版本",
- "这条规则在做什么": "RC1 最核心的止盈规则,用来把很多低质量短持仓单提早收掉。",
- },
- {
- "规则名": rule_link("knife_take_profit_2_wait_ql_s", "knife_take_profit_2_wait_ql_s"),
- "RC1 历史次数": count_exact("sell_reason", "knife_take_profit_2_wait_ql_s"),
- "触发方式": "QL-only 确认止盈",
- "典型值 / 条件": "常见于 glued_buy 持仓且仍在 glued 状态;先出现 ql_sell,再作为确认版止盈",
- "这条规则在做什么": "不急着在第一个轻微信号就走,而是等价格层面的确认再止盈。",
- },
- {
- "规则名": rule_link("glued_exit:kdj_sell / ql_sell", "glued_exit:"),
- "RC1 历史次数": count_prefix("sell_reason", "glued_exit:"),
- "触发方式": "glued 状态退出",
- "典型值 / 条件": "当 A1 仍是 glued 区(|A1| < 0.02),且不再满足继续持有条件时,配合 kdj_sell 或 ql_sell 退出",
- "这条规则在做什么": "趋势延续单走完后,按节奏和价格确认离场。",
- },
- {
- "规则名": rule_link("negative_a1_no_b1_recovery:kdj_sell / ql_sell", "negative_a1_no_b1_recovery:"),
- "RC1 历史次数": count_prefix("sell_reason", "negative_a1_no_b1_recovery:"),
- "触发方式": "负 A1 且 B1 没恢复",
- "典型值 / 条件": "-0.04 < A1 < -0.02,且 B1 <= 0",
- "这条规则在做什么": "趋势环境已经转弱,中期后劲也没回来,直接退出,不再等反弹。",
- },
- {
- "规则名": rule_link("small_positive_a1_declining:ql_sell / kdj_sell", "small_positive_a1_declining:"),
- "RC1 历史次数": count_prefix("sell_reason", "small_positive_a1_declining:"),
- "触发方式": "小正 A1 区衰减",
- "典型值 / 条件": "0.02 < A1 < 0.028;A1 下降;B1 < 0.17;若伴随 ql_sell / kdj_sell 则更容易触发",
- "这条规则在做什么": "热度还没彻底坏,但已经从小正值开始往下掉,优先做谨慎止盈。",
- },
- {
- "规则名": rule_link("prewarning_reduction_exit", "prewarning_reduction_exit"),
- "RC1 历史次数": count_exact("sell_reason", "prewarning_reduction_exit"),
- "触发方式": "高位预警减仓",
- "典型值 / 条件": "0.033 <= A1 <= 0.05;C1 > 80;且从峰值回落,B1 也明显衰减;要求 kdj_sell 已出现",
- "这条规则在做什么": "高位还没完全坏,但先提高警惕,提前保护利润。",
- },
- {
- "规则名": rule_link("high_regime_confirmed_exit:kdj_sell / ql_sell", "high_regime_confirmed_exit:"),
- "RC1 历史次数": count_prefix("sell_reason", "high_regime_confirmed_exit:"),
- "触发方式": "高位确认退出",
- "典型值 / 条件": "通常发生在高位序列中,且 B1 <= 0;再叠加 A1 <= 0.022、或 C1 < 80、或多次 sell 信号确认",
- "这条规则在做什么": "不是只看到一次转弱就跑,而是等高位结构确认坏掉后退出。",
- },
- {
- "规则名": rule_link("ql_mid_zone_take_profit", "ql_mid_zone_take_profit"),
- "RC1 历史次数": count_exact("sell_reason", "ql_mid_zone_take_profit"),
- "触发方式": "QL-only 中位止盈",
- "典型值 / 条件": "必须是 ql_sell 且不是 kdj_sell;max_b1_since_entry > 0.15;max_c1_since_entry >= 78;0 < A1 <= 0.02;B1 <= 0.12",
- "这条规则在做什么": "涨过一段以后,价格通道先给出明确走弱信号,就先锁利润。",
- },
- {
- "规则名": rule_link("crash_protection_exit", "crash_protection_exit"),
- "RC1 历史次数": count_exact("sell_reason", "crash_protection_exit"),
- "触发方式": "防崩保护",
- "典型值 / 条件": "曾经出现过 C1 > 80;max_A1 > 0.05;当前 A1 < 0.03;当前 B1 < -0.08",
- "这条规则在做什么": "高位转坏后不再犹豫,优先防大幅回撤。",
- },
- ]
- )
- return (
- '<div class="section"><h2>常见真实买卖规则速查表</h2>'
- '<div class="callout">这张表只挑 RC1 里最常见、最有代表性的真实交易规则。这里的“典型值”是为了帮助普通读者理解,不表示这条规则在任何历史样本里都只有这一种触发形式。很多规则还会叠加事件条件,例如是否出现了 KDJ sell、QL sell、是否刚卖出过、是否处在持仓中等。</div>'
- '<h3>常见真实买点</h3>'
- f'{_table(buy_df, {"规则名": lambda v: str(v)})}'
- '<div style="margin-top:16px;"></div>'
- '<h3>常见真实卖点</h3>'
- f'{_table(sell_df, {"规则名": lambda v: str(v)})}'
- "</div>"
- )
- def _indicator_guide_misconceptions(hrefs: dict[str, str]) -> str:
- items = [
- {
- "title": "看到 `SELL` 不是立刻等于做空",
- "text": "在这套口径里,空仓后再次出现的 SELL,多数只是辅助看空信号,表示环境偏弱,不代表又发生了一笔新的真实交易。",
- },
- {
- "title": "持仓中再次 `BUY` 不是加仓",
- "text": "它只是辅助看多信号,说明原有多头结构得到再次确认。真实交易层仍然只认那一笔原始开仓。",
- },
- {
- "title": "RC1 更强,不是因为交易更频繁",
- "text": "恰恰相反,RC1 的优势主要来自少做了若干低质量、短持仓、容易亏损的交易,而不是多做交易。",
- },
- {
- "title": "指标不是预测神器",
- "text": "这些指标更像一套分层判断流程:先定位,再过滤,再确认。它们不是在猜明天一定涨跌,而是在提高决策质量。",
- },
- ]
- cards = []
- for item in items:
- cards.append(
- '<div class="card">'
- f'<div class="name">{escape(item["title"])}</div>'
- f'<div class="desc">{escape(item["text"])}</div>'
- "</div>"
- )
- report_map = (
- '<div class="section"><h2>看完原理后,下一步看哪里</h2>'
- '<div class="grid">'
- + _metric("每日报告", f'<a href="{escape(hrefs["daily"])}">看最新状态</a>')
- + _metric("历史明细", f'<a href="{escape(hrefs["detail"])}">核对每笔交易</a>')
- + _metric("每周报告", f'<a href="{escape(hrefs["weekly"])}">看前向观察</a>')
- + _metric("详细使用说明", f'<a href="{escape(hrefs["usage"])}">看报告怎么用</a>')
- + "</div>"
- '<div class="callout">最实用的阅读顺序通常是:先看这页理解逻辑,再去每日报告看最新状态,最后到历史明细核对某一笔交易为什么买、为什么卖。</div>'
- "</div>"
- )
- return (
- '<div class="section"><h2>普通投资者最容易误解的 4 件事</h2><div class="cards">'
- + "".join(cards)
- + "</div></div>"
- + report_map
- )
- def build_indicator_guide_html(base_dir: Path, archive_mode: bool = False) -> str:
- s = _state(base_dir)
- latest_bar_date = str(s.get("latest_bar_date", "latest"))
- hrefs = _hrefs(latest_bar_date, archive_mode)
- text = (base_dir / "dragon_indicator_strategy_guide_cn.md").read_text(encoding="utf-8")
- hero = (
- f'{_nav(hrefs)}'
- '<div class="hero"><h1>Dragon 指标与策略原理说明</h1>'
- '<p>这页专门面向普通投资者,解释这套系统到底看什么指标、这些指标各自负责什么,以及三种策略版本为什么会得出不同的交易结果。</p>'
- '<div class="chips"><div class="chip">面向普通投资者</div><div class="chip">先看图,再读正文</div></div></div>'
- )
- quick = (
- '<div class="section"><h2>先记住这 6 句话</h2><div class="grid">'
- + _metric("C1", "看位置:高位、中位、低位")
- + _metric("A1", "看趋势热度是否扩张")
- + _metric("B1", "看动能还有没有后劲")
- + _metric("KDJ", "看短期节奏拐点")
- + _metric("QL", "看价格是否真正突破/跌破")
- + _metric("三版本", "差别主要在过滤强度,不是换了全新指标")
- + "</div></div>"
- )
- body = f'<div class="section doc">{_markdown_to_html(text)}</div>'
- return _shell(
- "Dragon 指标与策略原理说明",
- hero
- + quick
- + _indicator_guide_visuals()
- + _indicator_guide_indicator_cards()
- + _indicator_guide_scene_table()
- + _indicator_guide_threshold_tables()
- + _indicator_guide_rule_tables(base_dir, hrefs)
- + _indicator_guide_compare_table(base_dir)
- + _indicator_guide_misconceptions(hrefs)
- + body,
- )
- def build_index_html(base_dir: Path, archive_mode: bool = False) -> str:
- overview = _load_csv(base_dir / "dragon_strategy_overview.csv")
- status_df = _load_csv(base_dir / "dragon_daily_branch_status.csv")
- s = _state(base_dir)
- latest_bar_date = str(s.get("latest_bar_date", "latest"))
- request_date = str(s.get("request_date", ""))
- hrefs = _hrefs(latest_bar_date, archive_mode)
- overview_map = {str(row["branch"]): row for _, row in overview.iterrows()}
- rc1 = overview_map.get("alpha_first_glued_refined_hot_cap")
- control = overview_map.get("alpha_first_selective_veto")
- base = overview_map.get("workbook_preserving")
- hero = f'{_nav(hrefs)}<div class="hero"><h1>Dragon 策略 HTML 总览</h1><p>这里是 dragon/v2 的统一 HTML 入口。当前覆盖 `399673` 的三条策略:重构基线、平衡对照、RC1 前向默认。直接打开日报和周报即可查看,不需要再翻 markdown。</p><div class="chips"><div class="chip">请求日期 {escape(request_date)}</div><div class="chip">最新交易日 {escape(latest_bar_date)}</div><div class="chip">标的 399673</div></div></div>'
- summary = f'<div class="grid"><div class="panel"><h2>RC1 摘要</h2><div class="metrics">{_metric("RC1 年化", _fmt_pct(None if rc1 is None else rc1["cagr"]), "good")}{_metric("RC1 总收益", _fmt_pct(None if rc1 is None else rc1["compounded_return"]), "good")}{_metric("RC1 PF", _fmt_num(None if rc1 is None else rc1["profit_factor"]), "good")}{_metric("RC1 交易笔数", _fmt_int(None if rc1 is None else rc1["trades"]))}</div></div><div class="panel"><h2>分支对照</h2><div class="metrics">{_metric("control 年化", _fmt_pct(None if control is None else control["cagr"]))}{_metric("baseline 年化", _fmt_pct(None if base is None else base["cagr"]))}{_metric("RC1 BUY对齐", _fmt_int(None if rc1 is None else rc1["real_buy_overlap"]))}{_metric("RC1 SELL对齐", _fmt_int(None if rc1 is None else rc1["real_sell_overlap"]))}</div></div></div>'
- links = f'<div class="section"><h2>可直接打开的文件</h2><table><thead><tr><th>页面</th><th>路径</th><th>说明</th></tr></thead><tbody><tr><td>总览首页</td><td>{escape(hrefs["home"])}</td><td>统一入口</td></tr><tr><td>每日报告</td><td>{escape(hrefs["daily"])}</td><td>三策略最新状态、监控、分歧</td></tr><tr><td>每周报告</td><td>{escape(hrefs["weekly"])}</td><td>前向观察周度汇总</td></tr><tr><td>历史明细</td><td>{escape(hrefs["detail"])}</td><td>逐笔核对历史交易与资金变化</td></tr><tr><td>指标原理</td><td>{escape(hrefs["guide"])}</td><td>面向普通投资者的指标与策略讲解</td></tr><tr><td>极简说明</td><td>{escape(hrefs["quickstart"])}</td><td>给非技术使用者看的快速上手</td></tr><tr><td>详细说明</td><td>{escape(hrefs["usage"])}</td><td>完整使用说明</td></tr><tr><td>日报归档</td><td>{escape(hrefs["daily_archive"])}</td><td>当日快照</td></tr><tr><td>周报归档</td><td>{escape(hrefs["weekly_archive"])}</td><td>当周快照</td></tr></tbody></table></div>'
- cards = f'<div class="section"><h2>三策略对照</h2>{_strategy_cards(overview, status_df)}</div>'
- return _shell("Dragon 策略 HTML 总览", hero + summary + cards + _index_charts(base_dir) + links)
- def build_daily_html(base_dir: Path, archive_mode: bool = False) -> str:
- overview = _load_csv(base_dir / "dragon_strategy_overview.csv")
- branch_status = _load_csv(base_dir / "dragon_daily_branch_status.csv")
- monitor = _load_csv(base_dir / "dragon_daily_monitor_snapshot.csv")
- signal_change = _load_csv(base_dir / "dragon_signal_change_log.csv").tail(10).copy()
- divergence = _load_csv(base_dir / "dragon_branch_divergence_log.csv").tail(10).copy()
- s = _state(base_dir)
- latest_bar_date = str(s.get("latest_bar_date", ""))
- request_date = str(s.get("request_date", ""))
- monitor_summary = s.get("monitor_summary", {}) if isinstance(s.get("monitor_summary"), dict) else {}
- divergence_info = s.get("divergence", {}) if isinstance(s.get("divergence"), dict) else {}
- hrefs = _hrefs(latest_bar_date, archive_mode)
- hero = f'{_nav(hrefs)}<div class="hero"><h1>Dragon 每日报告</h1><p>这张页面用于看最新交易日的三策略状态、监控指标以及 refined vs control 的结构分歧。当前目标是做前向观察,不再继续做历史盲调。</p><div class="chips"><div class="chip">请求日期 {escape(request_date)}</div><div class="chip">最新交易日 {escape(latest_bar_date)}</div><div class="chip">RC1 前向默认</div></div></div>'
- grid = f'<div class="grid"><div class="panel"><h2>系统监控</h2><div class="metrics">{_metric("预警数", _fmt_int(monitor_summary.get("warning_count", 0)), "warn")}{_metric("硬告警数", _fmt_int(monitor_summary.get("hard_breach_count", 0)), "risk")}{_metric("缺失指标数", _fmt_int(monitor_summary.get("missing_data_count", 0)), "risk")}{_metric("分歧等级", escape(str(divergence_info.get("divergence_level", "none"))))}{_metric("最新交易日", escape(latest_bar_date))}</div></div><div class="panel"><h2>RC1 相对优势</h2><div class="metrics">{_metric("next_open 平均收益差", _fmt_pct(divergence_info.get("next_open_avg_return_delta")), "good")}{_metric("next_open PF 差", _fmt_num(divergence_info.get("next_open_pf_delta")), "good")}{_metric("RC1 最大回撤", _fmt_pct(divergence_info.get("next_open_max_drawdown_refined")))}{_metric("RC1 最大亏损串", _fmt_int(divergence_info.get("next_open_max_loss_streak_refined")))}</div></div></div><div class="section"><h2>快速入口</h2><div class="metrics">' + _metric("历史明细", f'<a href="{escape(hrefs["detail"])}">打开明细页</a>') + _metric("详细说明", f'<a href="{escape(hrefs["usage"])}">打开说明页</a>') + "</div></div>"
- if not overview.empty:
- overview["策略"] = overview["branch"].map(_branch_name)
- overview["角色"] = overview["branch"].map(lambda x: BRANCH_ROLES.get(str(x), ""))
- overview = overview[["策略", "角色", "trades", "win_rate", "avg_return", "profit_factor", "compounded_return", "cagr", "real_buy_overlap", "real_sell_overlap"]].rename(columns={"trades": "交易笔数", "win_rate": "胜率", "avg_return": "单笔均值", "profit_factor": "PF", "compounded_return": "总收益", "cagr": "年化", "real_buy_overlap": "BUY对齐", "real_sell_overlap": "SELL对齐"})
- sec1 = f'<div class="section"><h2>策略总览对照</h2>{_table(overview, {"胜率": _fmt_pct, "单笔均值": _fmt_pct, "PF": _fmt_num, "总收益": _fmt_pct, "年化": _fmt_pct})}</div>'
- if not branch_status.empty:
- branch_status["策略"] = branch_status["branch"].map(_branch_name)
- branch_status["角色"] = branch_status["branch"].map(lambda x: BRANCH_ROLES.get(str(x), ""))
- branch_status = branch_status[["策略", "角色", "as_of_date", "latest_close", "latest_a1", "latest_b1", "latest_c1", "latest_real_event_date", "latest_real_event_side", "latest_real_event_reason", "events_today_count", "events_today", "in_position", "open_entry_date", "open_entry_reason", "open_holding_days", "open_return_pct"]].rename(columns={"as_of_date": "交易日", "latest_close": "收盘", "latest_a1": "A1", "latest_b1": "B1", "latest_c1": "C1", "latest_real_event_date": "最近真实事件日", "latest_real_event_side": "最近真实事件方向", "latest_real_event_reason": "最近真实事件原因", "events_today_count": "当日事件数", "events_today": "当日事件", "in_position": "是否持仓", "open_entry_date": "持仓开仓日", "open_entry_reason": "持仓开仓原因", "open_holding_days": "持仓天数", "open_return_pct": "持仓浮盈亏"})
- sec2 = f'<div class="section"><h2>最新分支状态</h2>{_table(branch_status, {"收盘": lambda v: _fmt_num(v, 3), "A1": lambda v: _fmt_num(v, 4), "B1": lambda v: _fmt_num(v, 4), "C1": lambda v: _fmt_num(v, 2), "是否持仓": _fmt_bool, "持仓天数": _fmt_int, "持仓浮盈亏": _fmt_pct})}</div>'
- if not monitor.empty:
- monitor = monitor[["metric", "actual_value", "warning_threshold", "hard_threshold", "status", "rationale"]].rename(columns={"metric": "指标", "actual_value": "当前值", "warning_threshold": "预警阈值", "hard_threshold": "硬阈值", "status": "状态", "rationale": "含义"})
- sec3 = f'<div class="section"><h2>监控快照</h2>{_table(monitor, {"状态": _badge})}</div>'
- if not signal_change.empty:
- signal_change["策略"] = signal_change["branch"].map(_branch_name)
- signal_change = signal_change[["latest_bar_date", "策略", "change_type", "old_value", "new_value", "reason"]].rename(columns={"latest_bar_date": "交易日", "change_type": "变化类型", "old_value": "旧值", "new_value": "新值", "reason": "原因"})
- sec4 = f'<div class="section"><h2>最近信号变化</h2>{_table(signal_change)}</div>'
- if not divergence.empty:
- divergence = divergence[["latest_bar_date", "same_position_flag", "same_latest_real_event_flag", "next_open_avg_return_delta", "next_open_pf_delta", "divergence_level"]].rename(columns={"latest_bar_date": "交易日", "same_position_flag": "同仓位", "same_latest_real_event_flag": "同最新真实事件", "next_open_avg_return_delta": "收益差", "next_open_pf_delta": "PF差", "divergence_level": "分歧等级"})
- sec5 = f'<div class="section"><h2>refined vs control 分歧记录</h2>{_table(divergence, {"同仓位": _fmt_bool, "同最新真实事件": _fmt_bool, "收益差": _fmt_pct, "PF差": _fmt_num, "分歧等级": _badge})}</div>'
- return _shell("Dragon 每日报告", hero + grid + sec1 + sec2 + sec3 + sec4 + sec5)
- def build_weekly_html(base_dir: Path, archive_mode: bool = False) -> str:
- weekly = _load_csv(base_dir / "dragon_forward_weekly_summary.csv")
- observation = _load_csv(base_dir / "dragon_forward_observation_log.csv").tail(12).copy()
- divergence = _load_csv(base_dir / "dragon_branch_divergence_log.csv").tail(12).copy()
- monitor_history = _load_csv(base_dir / "dragon_monitor_history.csv")
- s = _state(base_dir)
- latest_bar_date = str(s.get("latest_bar_date", ""))
- warning_days = 0 if monitor_history.empty else int(monitor_history[monitor_history["status"] == "warning"]["latest_bar_date"].nunique())
- hard_days = 0 if monitor_history.empty else int(monitor_history[monitor_history["status"] == "hard_breach"]["latest_bar_date"].nunique())
- latest_divergence = "none" if divergence.empty else str(divergence.iloc[-1]["divergence_level"])
- obs_days = 0 if weekly.empty else int(weekly["observation_days"].max())
- hrefs = _hrefs(latest_bar_date, archive_mode)
- hero = f'{_nav(hrefs)}<div class="hero"><h1>Dragon 每周报告</h1><p>这张页面汇总前向观察窗口的状态演化,主要看三策略有没有新事件、有没有持仓变化、监控是否连续告警,以及 refined 与 control 是否出现结构性分歧。</p><div class="chips"><div class="chip">窗口末日 {escape(latest_bar_date)}</div><div class="chip">观察样本日 {obs_days}</div><div class="chip">最新分歧等级 {escape(latest_divergence)}</div></div></div>'
- grid = f'<div class="grid"><div class="panel"><h2>监控摘要</h2><div class="metrics">{_metric("预警日数", _fmt_int(warning_days), "warn")}{_metric("硬告警日数", _fmt_int(hard_days), "risk")}{_metric("观察样本日", _fmt_int(obs_days))}{_metric("最新分歧等级", escape(latest_divergence))}</div></div><div class="panel"><h2>当前口径</h2><div class="callout">当前阶段的目标是前向观察 RC1,不是继续做历史对齐。只有在连续 warning、hard breach 或 material divergence 出现时,才重新进入研究优化。</div></div></div>'
- if not weekly.empty:
- weekly["策略"] = weekly["branch"].map(_branch_name)
- weekly["角色"] = weekly["branch"].map(lambda x: BRANCH_ROLES.get(str(x), ""))
- weekly = weekly[["策略", "角色", "window_start", "window_end", "observation_days", "days_in_position", "latest_real_event_changed_count", "new_event_days", "warning_days", "hard_breach_days", "material_divergence_days"]].rename(columns={"window_start": "窗口开始", "window_end": "窗口结束", "observation_days": "观察日数", "days_in_position": "持仓日数", "latest_real_event_changed_count": "真实事件变更次数", "new_event_days": "新事件日数", "warning_days": "预警日数", "hard_breach_days": "硬告警日数", "material_divergence_days": "显著分歧日数"})
- sec1 = f'<div class="section"><h2>周度汇总</h2>{_table(weekly)}</div>'
- if not observation.empty:
- if "monitor_missing_data_count" not in observation.columns:
- observation["monitor_missing_data_count"] = 0
- observation["策略"] = observation["branch"].map(_branch_name)
- observation = observation[["run_timestamp", "latest_bar_date", "策略", "in_position", "latest_real_event", "events_today_count", "monitor_warning_count", "monitor_hard_breach_count", "monitor_missing_data_count"]].rename(columns={"run_timestamp": "运行时间", "latest_bar_date": "交易日", "in_position": "是否持仓", "latest_real_event": "最近真实事件", "events_today_count": "当日事件数", "monitor_warning_count": "预警数", "monitor_hard_breach_count": "硬告警数", "monitor_missing_data_count": "缺失指标数"})
- sec2 = f'<div class="section"><h2>最近观察记录</h2>{_table(observation, {"是否持仓": _fmt_bool})}</div>'
- if not divergence.empty:
- if "missing_data_count" not in divergence.columns:
- divergence["missing_data_count"] = 0
- divergence = divergence[["latest_bar_date", "same_position_flag", "same_latest_real_event_flag", "next_open_avg_return_delta", "next_open_pf_delta", "warning_count", "hard_breach_count", "missing_data_count", "divergence_level"]].rename(columns={"latest_bar_date": "交易日", "same_position_flag": "同仓位", "same_latest_real_event_flag": "同最新真实事件", "next_open_avg_return_delta": "收益差", "next_open_pf_delta": "PF差", "warning_count": "预警数", "hard_breach_count": "硬告警数", "missing_data_count": "缺失指标数", "divergence_level": "分歧等级"})
- sec3 = f'<div class="section"><h2>refined vs control 分歧历史</h2>{_table(divergence, {"同仓位": _fmt_bool, "同最新真实事件": _fmt_bool, "收益差": _fmt_pct, "PF差": _fmt_num, "分歧等级": _badge})}</div>'
- return _shell("Dragon 每周报告", hero + grid + sec1 + sec2 + sec3)
- def build_historical_detail_html(base_dir: Path, archive_mode: bool = False) -> str:
- details = _load_csv(base_dir / "dragon_historical_trade_details.csv")
- s = _state(base_dir)
- latest_bar_date = str(s.get("latest_bar_date", ""))
- hrefs = _hrefs(latest_bar_date, archive_mode)
- hero = (
- f'{_nav(hrefs)}'
- '<div class="hero"><h1>Dragon 历史交易全量明细</h1>'
- '<p>这张页面用于核对完整历史交易流水。表中包含买卖时间、买卖价格、买卖触发条件、持有天数、单笔收益、交易前资金、单笔盈亏和交易后资金。</p>'
- f'<div class="chips"><div class="chip">最新交易日 {escape(latest_bar_date)}</div><div class="chip">可用于日报直接跳转核对</div></div></div>'
- )
- if details.empty:
- return _shell("Dragon 历史交易全量明细", hero + '<div class="section"><h2>历史明细</h2><div class="empty">暂无数据</div></div>')
- details = details.copy()
- details["策略"] = details["branch"].map(_branch_name)
- details["buy_year"] = details["buy_date"].astype(str).str.slice(0, 4)
- details["search_blob"] = (
- details["策略"].astype(str)
- + " "
- + details["buy_date"].astype(str)
- + " "
- + details["sell_date"].astype(str)
- + " "
- + details["buy_reason"].astype(str)
- + " "
- + details["sell_reason"].astype(str)
- ).str.lower()
- trade_min_date = str(details["buy_date"].min())
- trade_max_date = str(details["sell_date"].max())
- indicator_snapshot, indicator_meta = _load_indicator_snapshot_for_report(base_dir, trade_min_date, trade_max_date)
- indicator_payload = []
- if not indicator_snapshot.empty:
- indicator_payload = [
- {
- "date": row["date"].date().isoformat(),
- "close": None if pd.isna(row["close"]) else round(float(row["close"]), 3),
- "a1": None if pd.isna(row["a1"]) else round(float(row["a1"]), 4),
- "b1": None if pd.isna(row["b1"]) else round(float(row["b1"]), 4),
- "c1": None if pd.isna(row["c1"]) else round(float(row["c1"]), 2),
- "kdj_buy": bool(row["kdj_buy"]),
- "kdj_sell": bool(row["kdj_sell"]),
- "ql_buy": bool(row["ql_buy"]),
- "ql_sell": bool(row["ql_sell"]),
- }
- for _, row in indicator_snapshot.iterrows()
- ]
- indicator_payload_json = json.dumps(indicator_payload, ensure_ascii=False, separators=(",", ":"))
- indicator_meta_json = json.dumps(indicator_meta, ensure_ascii=False, separators=(",", ":"))
- event_payload = _load_strategy_events_for_report(base_dir, trade_min_date, trade_max_date)
- event_payload_json = json.dumps(
- [] if event_payload.empty else event_payload.to_dict(orient="records"),
- ensure_ascii=False,
- separators=(",", ":"),
- )
- coverage_suffix = "" if indicator_meta.get("coverage_ok") else "。当前覆盖不足时,超出范围的交易会显示缺失提示"
- indicator_callout = (
- '<div class="callout" style="margin-top:14px;">'
- f'详情区的“前后 10 个交易日指标快照”当前使用 <strong>{escape(str(indicator_meta.get("source_label", "")))}</strong>,'
- f'覆盖区间 <strong>{escape(str(indicator_meta.get("coverage_start", "")))}</strong> 至 '
- f'<strong>{escape(str(indicator_meta.get("coverage_end", "")))}</strong>{coverage_suffix}。'
- "</div>"
- )
- summary_rows = []
- for branch in BRANCH_ORDER:
- sub = details[details["branch"] == branch].copy()
- if sub.empty:
- continue
- summary_rows.append(
- {
- "策略": _branch_name(branch),
- "交易数": len(sub),
- "首笔买入": sub["buy_date"].min(),
- "末笔卖出": sub["sell_date"].max(),
- "期末资金": float(sub["capital_after"].iloc[-1]),
- "累计收益": float(sub["capital_after"].iloc[-1] / sub["capital_before"].iloc[0] - 1.0),
- }
- )
- summary_df = pd.DataFrame(summary_rows)
- summary_html = _table(summary_df, {"期末资金": lambda v: _fmt_num(v, 2), "累计收益": _fmt_pct})
- branch_options = ['<option value="all">全部策略</option>'] + [
- f'<option value="{escape(branch)}">{escape(_branch_name(branch))}</option>'
- for branch in BRANCH_ORDER
- if not details[details["branch"] == branch].empty
- ]
- year_values = sorted(details["buy_year"].dropna().unique().tolist())
- year_options = ['<option value="all">全部年份</option>'] + [
- f'<option value="{escape(str(year))}">{escape(str(year))}</option>' for year in year_values
- ]
- rows: list[str] = []
- sorted_details = details.sort_values(["buy_date", "branch", "trade_no"]).reset_index(drop=True)
- sorted_details["row_id"] = sorted_details.apply(lambda r: f'{r["branch"]}-{int(r["trade_no"])}', axis=1)
- sorted_details["prev_row_id"] = sorted_details.groupby("branch")["row_id"].shift(1).fillna("")
- sorted_details["next_row_id"] = sorted_details.groupby("branch")["row_id"].shift(-1).fillna("")
- for _, row in sorted_details.iterrows():
- row_id = str(row["row_id"])
- prev_row_id = str(row["prev_row_id"])
- next_row_id = str(row["next_row_id"])
- buy_rule_href = _detail_query_href(
- hrefs,
- branch=str(row["branch"]),
- keyword=str(row["buy_reason"]),
- )
- sell_rule_href = _detail_query_href(
- hrefs,
- branch=str(row["branch"]),
- keyword=str(row["sell_reason"]),
- )
- year_href = _detail_query_href(
- hrefs,
- branch=str(row["branch"]),
- year=str(row["buy_year"]),
- )
- pnl = float(row["pnl_amount"])
- points = float(row["sell_price"]) - float(row["buy_price"])
- capital_before = float(row["capital_before"])
- units = 0.0 if float(row["buy_price"]) == 0 else capital_before / float(row["buy_price"])
- holding_days = int(float(row["holding_days"]))
- if holding_days <= 5:
- holding_bucket = "00-05天"
- elif holding_days <= 10:
- holding_bucket = "06-10天"
- elif holding_days <= 20:
- holding_bucket = "11-20天"
- elif holding_days <= 40:
- holding_bucket = "21-40天"
- else:
- holding_bucket = "41天以上"
- verdict = "盈利单" if pnl > 0 else ("亏损单" if pnl < 0 else "持平单")
- verdict_tone = "good" if pnl > 0 else ("risk" if pnl < 0 else "plain")
- prev_link = (
- f'<a href="#trade-row-{escape(prev_row_id)}" class="neighbor-link" data-target-id="{escape(prev_row_id)}">上一笔同策略交易</a>'
- if prev_row_id
- else "无"
- )
- next_link = (
- f'<a href="#trade-row-{escape(next_row_id)}" class="neighbor-link" data-target-id="{escape(next_row_id)}">下一笔同策略交易</a>'
- if next_row_id
- else "无"
- )
- cells = [
- f'<button class="detail-toggle" type="button" data-detail-id="{escape(row_id)}">详情</button>',
- escape(str(row["策略"])),
- _fmt_int(row["trade_no"]),
- escape(str(row["buy_date"])),
- _fmt_num(row["buy_price"], 3),
- escape(str(row["buy_reason"])),
- escape(str(row["sell_date"])),
- _fmt_num(row["sell_price"], 3),
- escape(str(row["sell_reason"])),
- _fmt_int(row["holding_days"]),
- _fmt_pct(row["return_pct"]),
- _fmt_num(row["capital_before"], 2),
- _fmt_num(row["pnl_amount"], 2),
- _fmt_num(row["capital_after"], 2),
- ]
- row_html = "".join(f"<td>{cell}</td>" for cell in cells)
- rows.append(
- f'<tr id="trade-row-{escape(row_id)}" class="detail-row" data-branch="{escape(str(row["branch"]))}" '
- f'data-year="{escape(str(row["buy_year"]))}" '
- f'data-search="{escape(str(row["search_blob"]))}" '
- f'data-row-id="{escape(row_id)}">{row_html}</tr>'
- )
- detail_inner = (
- '<div class="grid" style="margin-bottom:0;">'
- f'{_metric("交易结论", escape(verdict), verdict_tone)}'
- f'{_metric("指数点差", _fmt_num(points, 3))}'
- f'{_metric("等效份额", _fmt_num(units, 2))}'
- f'{_metric("持有分组", escape(holding_bucket))}'
- "</div>"
- '<div class="grid" style="margin-top:12px;margin-bottom:0;">'
- + _metric("同买入规则", f'<a href="{escape(buy_rule_href)}">查看同类买点明细</a>')
- + _metric("同卖出规则", f'<a href="{escape(sell_rule_href)}">查看同类卖点明细</a>')
- + _metric("同策略同年份", f'<a href="{escape(year_href)}">查看同年度同策略</a>')
- + _metric("回到全量", f'<a href="{escape(hrefs["detail"])}">查看完整明细</a>')
- + "</div>"
- + '<div class="grid" style="margin-top:12px;margin-bottom:0;">'
- + _metric("上一笔", prev_link)
- + _metric("下一笔", next_link)
- + "</div>"
- + '<div class="callout" style="margin-top:12px;">'
- f'买入规则:<code>{escape(str(row["buy_reason"]))}</code>。'
- f' 卖出规则:<code>{escape(str(row["sell_reason"]))}</code>。'
- f' 本笔交易从 <strong>{escape(str(row["buy_date"]))}</strong> 持有到 <strong>{escape(str(row["sell_date"]))}</strong>,'
- f' 持有 <strong>{holding_days}</strong> 天,收益 <strong>{_fmt_pct(row["return_pct"])}</strong>,'
- f' 资金从 <strong>{_fmt_num(row["capital_before"], 2)}</strong> 变化到 <strong>{_fmt_num(row["capital_after"], 2)}</strong>。'
- '</div>'
- + '<div class="snapshot-grid">'
- + (
- '<div class="snapshot-card">'
- '<div class="snapshot-title">买入日前后 10 个交易日</div>'
- f'<div class="snapshot-rule">事件规则:<code>{escape(str(row["buy_reason"]))}</code></div>'
- f'<div class="snapshot-host" data-event-date="{escape(str(row["buy_date"]))}" data-marker-label="买入日">'
- '<div class="empty">展开后加载指标快照</div>'
- '</div></div>'
- )
- + (
- '<div class="snapshot-card">'
- '<div class="snapshot-title">卖出日前后 10 个交易日</div>'
- f'<div class="snapshot-rule">事件规则:<code>{escape(str(row["sell_reason"]))}</code></div>'
- f'<div class="snapshot-host" data-event-date="{escape(str(row["sell_date"]))}" data-marker-label="卖出日">'
- '<div class="empty">展开后加载指标快照</div>'
- '</div></div>'
- )
- + '</div>'
- )
- rows.append(
- f'<tr class="detail-extra" data-parent-id="{escape(row_id)}" style="display:none;">'
- f'<td colspan="14"><div class="panel">{detail_inner}</div></td></tr>'
- )
- detail_table = (
- '<table id="detail-table"><thead><tr>'
- "<th>操作</th><th>策略</th><th>序号</th><th>买入日期</th><th>买入价格</th><th>买入触发条件</th>"
- "<th>卖出日期</th><th>卖出价格</th><th>卖出触发条件</th><th>持有天数</th>"
- "<th>单笔收益</th><th>交易前资金</th><th>单笔盈亏</th><th>交易后资金</th>"
- "</tr></thead><tbody>"
- + "".join(rows)
- + "</tbody></table>"
- )
- filter_section = f"""
- <div class="section">
- <h2>筛选与搜索</h2>
- <div id="filter-origin" class="callout" style="display:none;margin-bottom:16px;"></div>
- <div class="grid">
- <div class="panel">
- <h2>筛选条件</h2>
- <div class="metrics">
- <div class="metric"><div class="metric-label">策略</div><div class="metric-value"><select id="branch-filter">{''.join(branch_options)}</select></div></div>
- <div class="metric"><div class="metric-label">年份</div><div class="metric-value"><select id="year-filter">{''.join(year_options)}</select></div></div>
- <div class="metric"><div class="metric-label">关键词</div><div class="metric-value"><input id="keyword-filter" type="text" placeholder="输入日期、触发条件、策略名"></div></div>
- <div class="metric good"><div class="metric-label">当前结果数</div><div class="metric-value" id="result-count">{len(sorted_details)}</div></div>
- </div>
- <div class="callout" style="margin-top:14px;">如果你是从指标原理页点进来的,这里会自动带好规则名和策略筛选。<a id="clear-filters-link" href="{escape(hrefs["detail"])}" style="margin-left:8px;">清空筛选,查看完整明细</a></div>
- </div>
- </div>
- </div>
- """
- detail_section = f'<div class="section"><h2>历史全量明细</h2>{detail_table}</div>'
- script = f"""
- <script>
- (function() {{
- const branchFilter = document.getElementById('branch-filter');
- const yearFilter = document.getElementById('year-filter');
- const keywordFilter = document.getElementById('keyword-filter');
- const rows = Array.from(document.querySelectorAll('.detail-row'));
- const detailRows = Array.from(document.querySelectorAll('.detail-extra'));
- const toggleButtons = Array.from(document.querySelectorAll('.detail-toggle'));
- const neighborLinks = Array.from(document.querySelectorAll('.neighbor-link'));
- const countNode = document.getElementById('result-count');
- const originNode = document.getElementById('filter-origin');
- const clearLink = document.getElementById('clear-filters-link');
- const params = new URLSearchParams(window.location.search);
- const indicatorRows = {indicator_payload_json};
- const indicatorMeta = {indicator_meta_json};
- const eventRows = {event_payload_json};
- const indicatorIndexByDate = new Map(indicatorRows.map((row, index) => [row.date, index]));
- const eventMap = new Map();
- eventRows.forEach((row) => {{
- const key = `${{row.branch}}|${{row.date}}`;
- if (!eventMap.has(key)) eventMap.set(key, []);
- eventMap.get(key).push(row);
- }});
- function escapeHtml(text) {{
- return String(text)
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
- }}
- function fmtNum(value, digits) {{
- if (value === null || value === undefined || Number.isNaN(Number(value))) return '';
- return Number(value).toFixed(digits);
- }}
- function signalLabel(buyFlag, sellFlag) {{
- if (buyFlag && sellFlag) return 'B/S';
- if (buyFlag) return 'B';
- if (sellFlag) return 'S';
- return '';
- }}
- function eventBadge(evt) {{
- const cls = evt.layer === 'real_trade'
- ? (evt.side === 'BUY' ? 'real-buy' : 'real-sell')
- : (evt.side === 'BUY' ? 'aux-buy' : 'aux-sell');
- const label = evt.layer === 'real_trade'
- ? `真实${{evt.side}}`
- : `辅助${{evt.side}}`;
- return `<span class="trade-pill ${{cls}}">${{escapeHtml(label)}}</span>${{escapeHtml(evt.reason || '')}}`;
- }}
- function eventLabel(evt) {{
- return evt.layer === 'real_trade'
- ? `真实${{evt.side}}`
- : `辅助${{evt.side}}`;
- }}
- function eventBucketKey(evt) {{
- const prefix = evt.layer === 'real_trade' ? 'real_' : 'aux_';
- return `${{prefix}}${{String(evt.side || '').toLowerCase()}}`;
- }}
- function eventSummaryLine(evt) {{
- const reason = evt.reason ? ` ${{evt.reason}}` : '';
- return `${{evt.date}} ${{eventLabel(evt)}}${{reason}}`;
- }}
- function renderWindowSummary(branch, eventDate, start, end) {{
- const counts = {{
- real_buy: 0,
- real_sell: 0,
- aux_buy: 0,
- aux_sell: 0,
- }};
- const windowEvents = [];
- indicatorRows.slice(start, end + 1).forEach((row) => {{
- const dayEvents = eventMap.get(`${{branch}}|${{row.date}}`) || [];
- dayEvents.forEach((evt) => {{
- const enriched = {{
- date: row.date,
- side: evt.side || '',
- layer: evt.layer || '',
- reason: evt.reason || '',
- }};
- const bucket = eventBucketKey(enriched);
- if (Object.prototype.hasOwnProperty.call(counts, bucket)) {{
- counts[bucket] += 1;
- }}
- windowEvents.push(enriched);
- }});
- }});
- const pills = [
- `总事件 ${{windowEvents.length}}`,
- `真实BUY ${{counts.real_buy}}`,
- `真实SELL ${{counts.real_sell}}`,
- `辅助BUY ${{counts.aux_buy}}`,
- `辅助SELL ${{counts.aux_sell}}`,
- ].map((label) => `<span class="summary-pill">${{escapeHtml(label)}}</span>`).join('');
- if (!windowEvents.length) {{
- return `
- <div class="snapshot-summary">
- ${{pills}}
- <div>这个前后 10 个交易日窗口内暂时没有这条策略的其他事件。</div>
- </div>
- `;
- }}
- const beforeEvents = windowEvents.filter((evt) => evt.date < eventDate);
- const sameDayEvents = windowEvents.filter((evt) => evt.date === eventDate);
- const afterEvents = windowEvents.filter((evt) => evt.date > eventDate);
- const beforeText = beforeEvents.length ? eventSummaryLine(beforeEvents[beforeEvents.length - 1]) : '无';
- const sameDayText = sameDayEvents.length ? sameDayEvents.map(eventSummaryLine).join(';') : '无';
- const afterText = afterEvents.length ? eventSummaryLine(afterEvents[0]) : '无';
- return `
- <div class="snapshot-summary">
- <div>${{pills}}</div>
- <div>前一条:${{escapeHtml(beforeText)}}</div>
- <div>当日:${{escapeHtml(sameDayText)}}</div>
- <div>后一条:${{escapeHtml(afterText)}}</div>
- </div>
- `;
- }}
- function renderIndicatorWindow(branch, eventDate, markerLabel) {{
- if (!indicatorRows.length) {{
- return '<div class="empty">当前没有可用的指标快照数据。</div>';
- }}
- const centerIndex = indicatorIndexByDate.get(eventDate);
- if (centerIndex === undefined) {{
- const coverage = indicatorMeta.coverage_start && indicatorMeta.coverage_end
- ? `${{indicatorMeta.coverage_start}} 至 ${{indicatorMeta.coverage_end}}`
- : '当前无可用覆盖';
- return `<div class="empty">未找到 ${{escapeHtml(eventDate)}} 的指标数据。当前覆盖:${{escapeHtml(coverage)}}</div>`;
- }}
- const start = Math.max(0, centerIndex - 10);
- const end = Math.min(indicatorRows.length - 1, centerIndex + 10);
- const summaryHtml = renderWindowSummary(branch, eventDate, start, end);
- const body = indicatorRows.slice(start, end + 1).map((row) => {{
- const isEvent = row.date === eventDate;
- const marker = isEvent ? `<span class="event-pill">${{escapeHtml(markerLabel)}}</span>` : '';
- const kdj = signalLabel(row.kdj_buy, row.kdj_sell);
- const ql = signalLabel(row.ql_buy, row.ql_sell);
- const dayEvents = eventMap.get(`${{branch}}|${{row.date}}`) || [];
- const eventHtml = dayEvents.length ? dayEvents.map(eventBadge).join('<br>') : '';
- return `
- <tr class="${{isEvent ? 'event-row' : ''}}">
- <td>${{escapeHtml(row.date)}}</td>
- <td>${{marker}}</td>
- <td>${{fmtNum(row.close, 3)}}</td>
- <td>${{fmtNum(row.a1, 4)}}</td>
- <td>${{fmtNum(row.b1, 4)}}</td>
- <td>${{fmtNum(row.c1, 2)}}</td>
- <td>${{kdj ? `<span class="signal-pill">KDJ ${{escapeHtml(kdj)}}</span>` : ''}}</td>
- <td>${{ql ? `<span class="signal-pill">QL ${{escapeHtml(ql)}}</span>` : ''}}</td>
- <td>${{eventHtml}}</td>
- </tr>
- `;
- }}).join('');
- return `
- <div class="mini-table-wrap">
- <table class="mini-table">
- <thead>
- <tr>
- <th>date</th>
- <th>标记</th>
- <th>close</th>
- <th>a1</th>
- <th>b1</th>
- <th>c1</th>
- <th>KDJ</th>
- <th>QL</th>
- <th>策略事件</th>
- </tr>
- </thead>
- <tbody>${{body}}</tbody>
- </table>
- </div>
- ${{summaryHtml}}
- <div class="snapshot-note">close 为收盘价;a1 为趋势斜率;b1 为动量差;c1 为位置温度;KDJ/QL 为指标 B/S;策略事件按当前这条策略分支显示真实交易与辅助信号。</div>
- `;
- }}
- function ensureSnapshots(rowId) {{
- const detailRow = document.querySelector(`.detail-extra[data-parent-id="${{rowId}}"]`);
- if (!detailRow || detailRow.dataset.snapshotReady === '1') return;
- const branch = document.querySelector(`.detail-row[data-row-id="${{rowId}}"]`)?.dataset.branch || '';
- detailRow.querySelectorAll('.snapshot-host').forEach((host) => {{
- host.innerHTML = renderIndicatorWindow(branch, host.dataset.eventDate || '', host.dataset.markerLabel || '事件日');
- }});
- detailRow.dataset.snapshotReady = '1';
- }}
- function closeAllDetails() {{
- detailRows.forEach((row) => {{
- row.style.display = 'none';
- row.dataset.open = '0';
- }});
- toggleButtons.forEach((btn) => {{
- btn.textContent = '详情';
- }});
- }}
- function openDetail(rowId, shouldScroll) {{
- if (!rowId) return;
- const mainRow = document.querySelector(`.detail-row[data-row-id="${{rowId}}"]`);
- const detailRow = document.querySelector(`.detail-extra[data-parent-id="${{rowId}}"]`);
- const button = document.querySelector(`.detail-toggle[data-detail-id="${{rowId}}"]`);
- if (!mainRow || !detailRow || mainRow.style.display === 'none') return;
- closeAllDetails();
- ensureSnapshots(rowId);
- detailRow.style.display = '';
- detailRow.dataset.open = '1';
- if (button) button.textContent = '收起';
- if (shouldScroll) {{
- mainRow.scrollIntoView({{ behavior: 'smooth', block: 'center' }});
- }}
- }}
- function applyFilters() {{
- const branchValue = branchFilter.value;
- const yearValue = yearFilter.value;
- const keywordValue = keywordFilter.value.trim().toLowerCase();
- let visible = 0;
- rows.forEach((row) => {{
- const matchBranch = branchValue === 'all' || row.dataset.branch === branchValue;
- const matchYear = yearValue === 'all' || row.dataset.year === yearValue;
- const matchKeyword = !keywordValue || row.dataset.search.includes(keywordValue);
- const show = matchBranch && matchYear && matchKeyword;
- row.style.display = show ? '' : 'none';
- const detailRow = document.querySelector(`.detail-extra[data-parent-id="${{row.dataset.rowId}}"]`);
- if (!show && detailRow) {{
- detailRow.style.display = 'none';
- detailRow.dataset.open = '0';
- const btn = row.querySelector('.detail-toggle');
- if (btn) btn.textContent = '详情';
- }}
- if (show) visible += 1;
- }});
- countNode.textContent = String(visible);
- }}
- const presetBranch = params.get('branch');
- const presetYear = params.get('year');
- const presetKeyword = params.get('keyword');
- if (presetBranch && Array.from(branchFilter.options).some((opt) => opt.value === presetBranch)) {{
- branchFilter.value = presetBranch;
- }}
- if (presetYear && Array.from(yearFilter.options).some((opt) => opt.value === presetYear)) {{
- yearFilter.value = presetYear;
- }}
- if (presetKeyword) {{
- keywordFilter.value = presetKeyword;
- }}
- const originParts = [];
- if (presetBranch) {{
- const branchLabel = branchFilter.options[branchFilter.selectedIndex] ? branchFilter.options[branchFilter.selectedIndex].text : presetBranch;
- originParts.push(`策略:${{branchLabel}}`);
- }}
- if (presetYear) {{
- originParts.push(`年份:${{presetYear}}`);
- }}
- if (presetKeyword) {{
- originParts.push(`关键词:${{presetKeyword}}`);
- }}
- if (originParts.length) {{
- originNode.style.display = '';
- originNode.innerHTML = `当前来自外部跳转筛选:${{originParts.join(' | ')}}。你可以直接核对下方明细,或点击右下方“清空筛选”。`;
- }}
- if (clearLink) {{
- clearLink.href = window.location.pathname;
- }}
- branchFilter.addEventListener('change', applyFilters);
- yearFilter.addEventListener('change', applyFilters);
- keywordFilter.addEventListener('input', applyFilters);
- toggleButtons.forEach((btn) => {{
- btn.addEventListener('click', () => {{
- const rowId = btn.dataset.detailId;
- const detailRow = document.querySelector(`.detail-extra[data-parent-id="${{rowId}}"]`);
- if (!detailRow) return;
- if (detailRow.dataset.open === '1') {{
- closeAllDetails();
- return;
- }}
- openDetail(rowId, false);
- }});
- }});
- neighborLinks.forEach((link) => {{
- link.addEventListener('click', (event) => {{
- event.preventDefault();
- const targetId = link.dataset.targetId;
- if (!targetId) return;
- history.replaceState(null, '', `#trade-row-${{targetId}}`);
- openDetail(targetId, true);
- }});
- }});
- applyFilters();
- if (window.location.hash.startsWith('#trade-row-')) {{
- const targetId = window.location.hash.replace('#trade-row-', '');
- openDetail(targetId, true);
- }}
- }})();
- </script>
- """
- return _shell("Dragon 历史交易全量明细", hero + f'<div class="section"><h2>明细摘要</h2>{summary_html}</div>' + filter_section + detail_section + script)
- def main() -> None:
- base_dir = Path(__file__).resolve().parent
- html_dir = base_dir / "html_reports"
- html_dir.mkdir(exist_ok=True)
- latest_bar_date = str(_state(base_dir).get("latest_bar_date", "latest"))
- (base_dir / "dragon_reports_index.html").write_text(build_index_html(base_dir, False), encoding="utf-8")
- (base_dir / "dragon_daily_signal_report.html").write_text(build_daily_html(base_dir, False), encoding="utf-8")
- (base_dir / "dragon_forward_weekly_review.html").write_text(build_weekly_html(base_dir, False), encoding="utf-8")
- (base_dir / "dragon_historical_trade_details.html").write_text(build_historical_detail_html(base_dir, False), encoding="utf-8")
- (base_dir / "dragon_indicator_strategy_guide_cn.html").write_text(build_indicator_guide_html(base_dir, False), encoding="utf-8")
- (base_dir / "dragon_html_report_usage_cn.html").write_text(
- build_doc_html(base_dir, "dragon_html_report_usage_cn.md", "Dragon HTML 报告使用说明", "完整说明,适合首次接触这套报告时阅读。", False),
- encoding="utf-8",
- )
- (base_dir / "dragon_html_report_quickstart_cn.html").write_text(
- build_doc_html(base_dir, "dragon_html_report_quickstart_cn.md", "Dragon HTML 报告极简说明", "面向非技术使用者,只保留最核心的打开与更新步骤。", False),
- encoding="utf-8",
- )
- (html_dir / "index.html").write_text(build_index_html(base_dir, True), encoding="utf-8")
- (html_dir / f"dragon_reports_index_{latest_bar_date}.html").write_text(build_index_html(base_dir, True), encoding="utf-8")
- (html_dir / f"dragon_daily_signal_report_{latest_bar_date}.html").write_text(build_daily_html(base_dir, True), encoding="utf-8")
- (html_dir / f"dragon_forward_weekly_review_{latest_bar_date}.html").write_text(build_weekly_html(base_dir, True), encoding="utf-8")
- (html_dir / f"dragon_historical_trade_details_{latest_bar_date}.html").write_text(build_historical_detail_html(base_dir, True), encoding="utf-8")
- (html_dir / "dragon_indicator_strategy_guide_cn.html").write_text(build_indicator_guide_html(base_dir, True), encoding="utf-8")
- (html_dir / "dragon_html_report_usage_cn.html").write_text(
- build_doc_html(base_dir, "dragon_html_report_usage_cn.md", "Dragon HTML 报告使用说明", "完整说明,适合首次接触这套报告时阅读。", True),
- encoding="utf-8",
- )
- (html_dir / "dragon_html_report_quickstart_cn.html").write_text(
- build_doc_html(base_dir, "dragon_html_report_quickstart_cn.md", "Dragon HTML 报告极简说明", "面向非技术使用者,只保留最核心的打开与更新步骤。", True),
- encoding="utf-8",
- )
- if __name__ == "__main__":
- main()
|