From 61cf007883ae7d6e8d98f7a0a34f94cabaa79da1 Mon Sep 17 00:00:00 2001
From: Cai <cai@nbcai.cc>
Date: Thu, 20 Aug 2026 18:43:12 +0800
Subject: [PATCH] feat(valuation): rank semiconductors by lower-bound discount
---
ai-valuation-analyst/tools/rank_semiconductor_valuations.py | 33 +++++++++++++++++++++++----------
1 files changed, 23 insertions(+), 10 deletions(-)
diff --git a/ai-valuation-analyst/tools/rank_semiconductor_valuations.py b/ai-valuation-analyst/tools/rank_semiconductor_valuations.py
index 5fbef31..58ee494 100644
--- a/ai-valuation-analyst/tools/rank_semiconductor_valuations.py
+++ b/ai-valuation-analyst/tools/rank_semiconductor_valuations.py
@@ -16,6 +16,7 @@
RESULT_ROOT = PROJECT_ROOT / "ana-data/result/股票估值"
OUTPUT_CSV = OUTPUT_DIR / "半导体估值排序.csv"
OUTPUT_MD = OUTPUT_DIR / "半导体估值排序.md"
+OUTPUT_MANIFEST = OUTPUT_DIR / "全部行业调研标的估值覆盖manifest.json"
FIELDS = [
"代码",
@@ -23,6 +24,7 @@
"所属行业",
"收盘价",
"基准区间",
+ "低于基准下沿",
"原判定",
"泡沫判定",
"高于基准上沿",
@@ -33,9 +35,6 @@
"相对MA60",
"正式报告",
]
-LABEL_RANK = {"偏低": 0, "基本合理": 1, "偏贵": 2, "明显偏贵": 3}
-
-
def read_csv(path: Path) -> list[dict[str, str]]:
with path.open("r", encoding="utf-8-sig", newline="") as handle:
return list(csv.DictReader(handle))
@@ -57,13 +56,12 @@
return "—" if number is None else f"{number:+.2f}%"
-def interval_position(row: dict[str, str]) -> float:
+def below_base_low_pct(row: dict[str, str]) -> float | None:
close = as_float(row.get("close", ""))
low = as_float(row.get("base_low", ""))
- high = as_float(row.get("base_high", ""))
- if close is None or low is None or high is None or high <= low:
- return float("inf")
- return (close - low) / (high - low)
+ if close is None or low is None or low <= 0:
+ return None
+ return (low - close) / low * 100.0
def display_industries(value: str) -> str:
@@ -131,6 +129,7 @@
"所属行业": display_industries(source["industries"]),
"收盘价": "—",
"基准区间": "—",
+ "低于基准下沿": "—",
"原判定": "特殊结论",
"泡沫判定": "不适用",
"高于基准上沿": "—",
@@ -148,12 +147,14 @@
low = as_float(current["base_low"])
high = as_float(current["base_high"])
premium = as_float(current["bubble_premium_pct"])
+ discount = below_base_low_pct(current)
output = {
"代码": source["ticker"],
"公司": source["company"],
"所属行业": display_industries(source["industries"]),
"收盘价": "—" if close is None else f"{close:.2f}",
"基准区间": "—" if low is None or high is None else f"{low:.2f}—{high:.2f}",
+ "低于基准下沿": "—" if discount is None else f"{discount:.2f}%",
"原判定": current["label"],
"泡沫判定": current["bubble_status"],
"高于基准上沿": "—" if premium is None else f"{max(0.0, premium):.2f}%",
@@ -164,7 +165,7 @@
"相对MA60": signed_pct(current["distance_to_ma60_pct"]),
"正式报告": report_path,
}
- key = (LABEL_RANK.get(current["label"], 4), interval_position(current), source["ticker"])
+ key = (0, float("inf") if discount is None else -discount, source["ticker"])
ranked.append((key, output))
ranked.sort(key=lambda item: item[0])
@@ -179,8 +180,9 @@
"# 半导体股票估值排序",
"",
f"- 价格日:{'、'.join(trade_dates)};半导体研究证券共{len(rows)}只。",
- "- 排序:偏低 → 基本合理 → 偏贵 → 明显偏贵 → 特殊结论;同档按 `(收盘价-基准下沿)/(基准上沿-基准下沿)` 从低到高排列。",
+ "- 排序:按 `低于基准下沿 = (基准下沿-收盘价)/基准下沿` 从高到低排列,数值越大表示相对基准下沿越便宜;无法计算的特殊结论置于末尾。",
f"- 分布:偏低{label_counts['偏低']}只、基本合理{label_counts['基本合理']}只、偏贵{label_counts['偏贵']}只、明显偏贵{label_counts['明显偏贵']}只、特殊结论{label_counts['特殊结论']}只。",
+ "- “低于基准下沿”为正表示收盘价低于下沿,为负表示收盘价高于下沿;该字段是价格位置比较,不代表经营风险已经消除。",
"- “高于基准上沿”为0表示没有超过基准合理区间上沿;泡沫原因除价格位置外属于最可能解释,不是已经证实的资金因果。",
f"- 输入:`全部行业调研标的估值覆盖清单.csv` SHA-256 `{sha256(COVERAGE_PATH)}`;`../估值台账/latest.csv` SHA-256 `{sha256(LATEST_PATH)}`。",
"- 合理区间是条件化估值,不是目标价、交易指令或收益承诺。",
@@ -199,6 +201,15 @@
lines.append("")
atomic_write_text(OUTPUT_MD, "\n".join(lines))
+ manifest = json.loads(OUTPUT_MANIFEST.read_text(encoding="utf-8"))
+ manifest["semiconductor_ranking_trade_date"] = "、".join(trade_dates)
+ manifest["semiconductor_ranking_md_sha256"] = sha256(OUTPUT_MD).lower()
+ manifest["semiconductor_ranking_csv_sha256"] = sha256(OUTPUT_CSV).lower()
+ atomic_write_text(
+ OUTPUT_MANIFEST,
+ json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
+ )
+
print(json.dumps({
"rows": len(rows),
"label_counts": dict(label_counts),
@@ -207,6 +218,8 @@
"csv_sha256": sha256(OUTPUT_CSV),
"markdown": str(OUTPUT_MD.relative_to(PROJECT_ROOT)),
"markdown_sha256": sha256(OUTPUT_MD),
+ "manifest": str(OUTPUT_MANIFEST.relative_to(PROJECT_ROOT)),
+ "manifest_sha256": sha256(OUTPUT_MANIFEST),
}, ensure_ascii=False))
return 0
--
Gitblit v1.9.3