原型验证
原型验证方案 — 单模块穿刺
版本: v1.0
日期: 2026-07-09
定位: 对 design.md 进行初步设计评审,提炼主流程,定义单模块穿刺原型方案
一、设计评审(速评)
1.1 整体判断:方案可行,架构合理
| 维度 | 评价 | 风险 |
|---|---|---|
| 数据源 | AKShare 200+ 接口,免费,DataFrame 输出 — 可靠 | 低 |
| 技术栈 | Python + pandas + Streamlit + SQLite — 成熟稳定 | 低 |
| 架构分层 | 五层分离(展示→应用→引擎→数据访问→持久),职责清晰 | 低 |
| 核心算法 | DCF / PE分位 / Piotroski / 行业对比 — 金融学术界成熟方法 | 低 |
| 实施路径 | 12 周 4 阶段 — 时间充裕,有缓冲 | 低 |
1.2 一个待确认的关键风险
| 风险 | 影响 | 验证方式 |
|---|---|---|
| AKShare 财报数据的字段可用性 | 如果 stock_financial_analysis_indicator_em() 返回的字段与 DCF/F-Score 需要的字段不匹配,需要大量字段映射工作 |
原型阶段必须第一个验证 |
1.3 评审结论
方案可行,不需要大改。 接下来的正确做法是:不是继续完善设计文档,而是立即动手做原型验证——用最少代码走通一条完整链路,验证架构假设和数据可用性。
二、主流程提炼
2.1 系统核心主流程
用户输入股票代码
│
▼
┌──────────────────┐
│ 1. 获取基础信息 │ ← AKShare: 股票名称、行业、市值、是否ST
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 2. 行业方法选择 │ ← IndustryMethodSelector: 银行→PB+剩余收益 / 消费→DCF+PE
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 3. 相对估值 │ ← PE/PB 历史分位 + 行业排名 + PEG + 股息率 → 评分(0-100)
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 4. 绝对估值(DCF) │ ← FCF推算 → 两阶段DCF → 内在价值 → 安全边际
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 5. 质量评分 │ ← Piotroski F-Score(0-9) + 财务造假3信号
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 6. 综合估值评级 │ ← 加权计算 → A/B/C/D + 综合建议文本
└──────┬───────────┘
│
▼
┌──────────────────┐
│ 7. 输出报告 │ ← CLI 文本 / JSON / Streamlit 页面
└──────────────────┘
2.2 单模块穿刺:选择"相对估值"作为穿刺模块
| 候选模块 | 依赖复杂度 | 独立可验证 | 业务价值 | 穿刺推荐 |
|---|---|---|---|---|
| 相对估值 | ⭐ 低(只需行情+行业分类) | ✅ 高分位即有意义 | ⭐⭐⭐ 最常见问题"贵不贵" | ✅ 首选 |
| DCF | ⭐⭐⭐ 高(需三张报表+FCF推算+WACC) | ⚠️ 需要参数校准 | ⭐⭐ 绝对估值补充 | ❌ 依赖太多 |
| F-Score | ⭐⭐ 中(需两期财报比较) | ✅ 分数即有意义 | ⭐⭐ 质量维度 | 备选 |
| 行业对比 | ⭐ 低(需行业分类+PE/PB列表) | ✅ | ⭐⭐ 辅助 | 与相对估值合并验证 |
结论:选相对估值模块作为穿刺目标。原因是:
- 依赖最少(AKShare 的行情接口
stock_zh_a_spot_em+ 历史估值 + 行业分类 三个接口) - 产出最直观(PE 分位 20% → 比 80% 的时间便宜 → 用户立刻理解)
- 验证最核心的架构假设(AKShare 数据可用性、适配器模式、缓存、分位计算)
三、原型范围与边界
3.1 原型目标
2-3 天内,写出一个能从 AKShare 拉数据、计算 PE/PB 分位和行业排名、输出评分的可运行脚本,并在 3 只真实 A 股上验证结果合理。
3.2 明确做什么(In Scope)
✅ AKShare 数据拉取(聚焦 3 个接口)
└── stock_zh_a_spot_em() — 全 A 股实时行情(含 PE/PB)
└── stock_zh_a_hist() — 单只股票历史日线(含 PE/PB 历史)
└── stock_individual_info_em() — 个股信息(行业分类)
✅ 数据缓存(SQLite,最简单的 key-value)
└── 缓存实时行情(4 小时 TTL)
└── 缓存历史 PE/PB(1 天 TTL)
✅ 行业分类
└── 从 AKShare 获取申万行业
└── 硬编码 31 个一级行业列表
✅ 相对估值计算
└── PE (TTM) 历史分位(5 年)
└── PB 历史分位(5 年)
└── PE 行业排名(在同行业股票中的位置)
└── 简单评分(PE 分位 + PB 分位 + 行业排名 → 0-100 分)
✅ 命令行输出
└── python -m prototype.valuate 600519
└── 输出:股票名、行业、当前PE、PE分位、PB分位、行业排名、评分、一句话总结
3.3 明确不做什么(Out of Scope)
❌ DCF 绝对估值 → 原型阶段不碰,依赖太多
❌ Piotroski F-Score → 第 2 个穿刺模块
❌ LLM / AI 点评 → 原型阶段完全不需要
❌ Streamlit 前端 → 原型只用 CLI 输出
❌ 异步批处理 → 原型只做单只股票
❌ 完整的错误处理/降级 → 原型用 try/except 即可
❌ 12 个行业的完整方法选择器 → 原型只验证逻辑框架
❌ 单元测试 → 原型用手工验证
3.4 原型文件结构
gupiao/
└── prototype/ # 原型目录(独立于 src/)
├── __init__.py
├── adapter.py # AKShare 最小适配器(3 个方法)
├── cache.py # SQLite 简单缓存
├── industry.py # 行业分类 + 同行 PE/PB 列表
├── relative.py # 相对估值计算(分位 + 评分)
├── engine.py # 最小估值引擎(组合上述模块)
├── main.py # CLI 入口
└── verify.py # 验证脚本:3 只股票结果合理性检查
四、原型模块设计(最小化)
4.1 adapter.py — AKShare 最小适配器
"""
原型适配器:只实现 3 个必须的 AKShare 接口。
不抽象、不 ABC、不工厂模式 — 只要能用。
"""
import akshare as ak
import pandas as pd
class ProtoAdapter:
"""原型阶段:直接调 AKShare,不做抽象层。生产阶段再套 DataAdapter(ABC)。"""
def get_all_stocks_spot(self) -> pd.DataFrame:
"""全 A 股实时行情(含 PE/PB/PS/行业/总市值)"""
df = ak.stock_zh_a_spot_em()
return df
def get_stock_history(self, code: str, period: str = "daily",
start_date: str = "20200101",
end_date: str = "20260709",
adjust: str = "qfq") -> pd.DataFrame:
"""单只股票历史日线(含 PE/PB 历史列)"""
df = ak.stock_zh_a_hist(symbol=code, period=period,
start_date=start_date, end_date=end_date,
adjust=adjust)
return df
def get_stock_info(self, code: str) -> pd.Series:
"""个股基础信息(行业分类、上市日期等)"""
df = ak.stock_individual_info_em(symbol=code)
return df.set_index("item")["value"]
4.2 cache.py — SQLite 简单缓存
"""
原型缓存:最简单的 SQLite key-value 存储。
key = 描述性字符串,value = pickled DataFrame。
"""
import sqlite3
import pickle
import time
class ProtoCache:
def __init__(self, db_path: str = "prototype/cache.db"):
self.conn = sqlite3.connect(db_path)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
value BLOB,
created_at REAL
)
""")
def get(self, key: str, ttl_seconds: float = 3600) -> bytes | None:
row = self.conn.execute(
"SELECT value, created_at FROM cache WHERE key = ?", (key,)
).fetchone()
if row and (time.time() - row[1] < ttl_seconds):
return row[0]
return None
def set(self, key: str, value: bytes):
self.conn.execute(
"INSERT OR REPLACE INTO cache VALUES (?, ?, ?)",
(key, value, time.time())
)
self.conn.commit()
# 便捷方法
def get_df(self, key: str, ttl: float = 3600) -> "pd.DataFrame | None":
data = self.get(key, ttl)
return pickle.loads(data) if data else None
def set_df(self, key: str, df: "pd.DataFrame"):
self.set(key, pickle.dumps(df))
4.3 industry.py — 行业分类与同行对比
"""
原型行业模块:
1. 映射股票代码 → 申万一级行业
2. 收集同行业所有股票的 PE/PB
3. 计算行业排名
"""
import pandas as pd
class ProtoIndustry:
# 硬编码的申万一级行业列表(31 个)
SW_INDUSTRIES = [
"银行", "非银金融", "房地产", "建筑装饰", "建筑材料",
"机械设备", "电力设备", "国防军工", "汽车", "家用电器",
"食品饮料", "纺织服饰", "轻工制造", "农林牧渔",
"医药生物", "电子", "计算机", "通信", "传媒",
"煤炭", "石油石化", "钢铁", "有色金属", "基础化工",
"公用事业", "交通运输", "环保", "商贸零售", "社会服务",
"综合", "美容护理"
]
def classify_stock(self, spot_df: pd.DataFrame, code: str) -> str | None:
"""从全市场行情 DataFrame 中查找股票的申万行业"""
row = spot_df[spot_df["代码"] == code]
if row.empty:
return None
for col in ["所处行业", "行业", "industry"]:
if col in row.columns:
return row.iloc[0][col]
return None
def get_industry_peers(self, spot_df: pd.DataFrame, industry: str) -> pd.DataFrame:
"""获取同行业所有股票的 PE/PB/市值数据"""
industry_col = None
for col in ["所处行业", "行业", "industry"]:
if col in spot_df.columns:
industry_col = col
break
if industry_col is None:
return pd.DataFrame()
peers = spot_df[spot_df[industry_col] == industry].copy()
peers = peers[peers["市盈率-动态"] > 0]
peers = peers[peers["市净率"] > 0]
return peers
def compute_industry_rank(self, spot_df: pd.DataFrame, code: str) -> dict:
"""计算股票在行业内的 PE/PB 排名"""
industry = self.classify_stock(spot_df, code)
if industry is None:
return {"error": "无法确定行业"}
peers = self.get_industry_peers(spot_df, industry)
stock_row = spot_df[spot_df["代码"] == code]
if stock_row.empty or peers.empty:
return {"error": "无同行数据"}
stock_pe = stock_row.iloc[0]["市盈率-动态"]
stock_pb = stock_row.iloc[0]["市净率"]
# PE 排名:PE 越低排名越靠前(越低越好)
pe_rank = (peers["市盈率-动态"] < stock_pe).sum()
pe_total = len(peers)
pe_percentile = (pe_rank / pe_total) * 100 if pe_total > 0 else 0
# PB 排名
pb_rank = (peers["市净率"] < stock_pb).sum()
pb_total = len(peers)
pb_percentile = (pb_rank / pb_total) * 100 if pb_total > 0 else 0
return {
"industry": industry,
"pe_rank": f"{pe_rank + 1}/{pe_total}",
"pe_percentile": pe_percentile,
"pb_rank": f"{pb_rank + 1}/{pb_total}",
"pb_percentile": pb_percentile,
"peer_count": pe_total,
"industry_pe_median": peers["市盈率-动态"].median(),
"industry_pb_median": peers["市净率"].median(),
}
4.4 relative.py — 相对估值计算
"""
原型相对估值:PE/PB 历史分位 + 简单评分。
这是整个原型最核心的模块。
"""
import pandas as pd
from scipy import stats
from dataclasses import dataclass
@dataclass
class RelativeResult:
stock_code: str
stock_name: str
current_pe: float
current_pb: float
pe_percentile_5y: float # PE 5年历史分位 (0-100, 越低越便宜)
pb_percentile_5y: float # PB 5年历史分位
pe_percentile_3y: float # PE 3年历史分位
pb_percentile_3y: float # PB 3年历史分位
industry_pe_percentile: float # PE 行业分位
industry_pb_percentile: float # PB 行业分位
industry_name: str
peer_count: int
score: float # 0-100 综合评分
level: str # 低估/合理偏低/合理偏高/高估/数据不足
summary: str
class ProtoRelativeValuation:
"""
原型相对估值计算器。
输入:历史 PE/PB 序列 + 行业对比数据
输出:分位值 + 评分 + 一句话总结
"""
def compute_percentile(self, hist_df: pd.DataFrame) -> dict:
"""
从历史日线 DataFrame 计算 PE/PB 分位。
hist_df 来自 adapter.get_stock_history()。
注意:AKShare 返回的列名需要运行时确认。
"""
result = {}
current_pe = (hist_df.iloc[-1].get("市盈率", None) or
hist_df.iloc[-1].get("peTTM", None))
current_pb = (hist_df.iloc[-1].get("市净率", None) or
hist_df.iloc[-1].get("pbMRQ", None))
pe_col = None
for col in ["市盈率", "peTTM", "pe_ttm"]:
if col in hist_df.columns:
pe_col = col
break
pb_col = None
for col in ["市净率", "pbMRQ", "pb"]:
if col in hist_df.columns:
pb_col = col
break
if pe_col:
pe_series = hist_df[pe_col].dropna()
pe_series = pe_series[pe_series > 0]
if len(pe_series) > 0 and current_pe is not None:
result["pe_percentile_5y"] = stats.percentileofscore(pe_series, current_pe)
pe_3y = pe_series.tail(min(750, len(pe_series)))
result["pe_percentile_3y"] = stats.percentileofscore(pe_3y, current_pe)
result["current_pe"] = current_pe
if pb_col:
pb_series = hist_df[pb_col].dropna()
pb_series = pb_series[pb_series > 0]
if len(pb_series) > 0 and current_pb is not None:
result["pb_percentile_5y"] = stats.percentileofscore(pb_series, current_pb)
pb_3y = pb_series.tail(min(750, len(pb_series)))
result["pb_percentile_3y"] = stats.percentileofscore(pb_3y, current_pb)
result["current_pb"] = current_pb
return result
def score(self, hist_percentiles: dict, industry_ranks: dict) -> RelativeResult:
"""
综合评分逻辑(简化版):
- PE 5年分位:40%
- PB 5年分位:20%
- PE 行业排名:40%
分数越高越"便宜"。
"""
score = 0.0
# PE 5年分位
pe_pct = hist_percentiles.get("pe_percentile_5y", 50)
if pe_pct <= 20: score += 40
elif pe_pct <= 40: score += 30
elif pe_pct <= 60: score += 20
elif pe_pct <= 80: score += 10
# PB 5年分位
pb_pct = hist_percentiles.get("pb_percentile_5y", 50)
if pb_pct <= 20: score += 20
elif pb_pct <= 40: score += 15
elif pb_pct <= 60: score += 10
elif pb_pct <= 80: score += 5
# PE 行业排名
ind_pe_pct = industry_ranks.get("pe_percentile", 50)
if ind_pe_pct <= 20: score += 40
elif ind_pe_pct <= 40: score += 30
elif ind_pe_pct <= 60: score += 20
elif ind_pe_pct <= 80: score += 10
# 评级
if score >= 80:
level, summary = "低估", "PE和PB均处于历史低位,且显著低于行业均值"
elif score >= 60:
level, summary = "合理偏低", "估值低于历史中枢,有安全边际"
elif score >= 40:
level, summary = "合理偏高", "估值处于或略高于历史中枢"
elif score >= 20:
level, summary = "高估", "估值显著高于历史中枢和行业均值"
else:
level, summary = "严重高估", "PE/PB均处于历史高位,需警惕泡沫风险"
return RelativeResult(
stock_code="", stock_name="",
current_pe=hist_percentiles.get("current_pe", 0),
current_pb=hist_percentiles.get("current_pb", 0),
pe_percentile_5y=pe_pct,
pb_percentile_5y=pb_pct,
pe_percentile_3y=hist_percentiles.get("pe_percentile_3y", 0),
pb_percentile_3y=hist_percentiles.get("pb_percentile_3y", 0),
industry_pe_percentile=ind_pe_pct,
industry_pb_percentile=industry_ranks.get("pb_percentile", 50),
industry_name=industry_ranks.get("industry", "未知"),
peer_count=industry_ranks.get("peer_count", 0),
score=score, level=level, summary=summary,
)
4.5 engine.py — 最小估值引擎
"""
原型估值引擎:把 adapter + cache + industry + relative 串起来。
"""
from prototype.adapter import ProtoAdapter
from prototype.cache import ProtoCache
from prototype.industry import ProtoIndustry
from prototype.relative import ProtoRelativeValuation, RelativeResult
class ProtoValuationEngine:
def __init__(self):
self.adapter = ProtoAdapter()
self.cache = ProtoCache()
self.industry = ProtoIndustry()
self.relative = ProtoRelativeValuation()
def evaluate(self, code: str) -> RelativeResult:
# Step 1: 全市场行情(带缓存,4h TTL)
spot_df = self.cache.get_df("all_spot", ttl=14400)
if spot_df is None:
spot_df = self.adapter.get_all_stocks_spot()
self.cache.set_df("all_spot", spot_df)
# Step 2: 查找股票
stock_row = spot_df[spot_df["代码"] == code]
if stock_row.empty:
raise ValueError(f"未找到股票: {code}")
name = stock_row.iloc[0].get("名称", code)
# Step 3: 行业同行对比
industry_ranks = self.industry.compute_industry_rank(spot_df, code)
# Step 4: 历史 PE/PB(带缓存,1d TTL)
hist_df = self.cache.get_df(f"history:{code}", ttl=86400)
if hist_df is None:
hist_df = self.adapter.get_stock_history(code)
self.cache.set_df(f"history:{code}", hist_df)
# Step 5: 分位 + 评分
percentiles = self.relative.compute_percentile(hist_df)
result = self.relative.score(percentiles, industry_ranks)
result.stock_code = code
result.stock_name = name
return result
4.6 main.py — CLI 入口
"""
原型 CLI:python -m prototype.main 600519
"""
import sys
from prototype.engine import ProtoValuationEngine
def main():
if len(sys.argv) < 2:
print("用法: python -m prototype.main <股票代码>")
print("示例: python -m prototype.main 600519")
sys.exit(1)
code = sys.argv[1]
engine = ProtoValuationEngine()
print(f"\n正在评估 {code} ...\n")
result = engine.evaluate(code)
print("=" * 50)
print(f" {result.stock_name} ({result.stock_code})")
print(f" 行业: {result.industry_name}(同行 {result.peer_count} 只)")
print("=" * 50)
print(f" 当前 PE (TTM): {result.current_pe:.1f}")
print(f" 当前 PB: {result.current_pb:.2f}")
print("-" * 50)
print(f" PE 5年历史分位: {result.pe_percentile_5y:.0f}% (值越小越便宜)")
print(f" PB 5年历史分位: {result.pb_percentile_5y:.0f}%")
print(f" PE 3年历史分位: {result.pe_percentile_3y:.0f}%")
print(f" PB 3年历史分位: {result.pb_percentile_3y:.0f}%")
print(f" PE 行业排名: {result.industry_pe_percentile:.0f}%")
print(f" PB 行业排名: {result.industry_pb_percentile:.0f}%")
print("-" * 50)
print(f" 综合评分: {result.score:.0f} / 100")
print(f" 评级: {result.level}")
print(f" 摘要: {result.summary}")
print("=" * 50)
if __name__ == "__main__":
main()
4.7 verify.py — 验证脚本
"""
原型验证:在 3 只真实 A 股上跑一遍,检查结果是否合理。
验证目标:
1. 数据能成功从 AKShare 拉取(不断连、不报错)
2. PE/PB 分位值在合理范围(0-100)
3. 行业分类正确(贵州茅台 → 食品饮料)
4. 缓存第二次命中(速度显著快于第一次)
"""
import time
from prototype.engine import ProtoValuationEngine
VERIFY_STOCKS = [
("600519", "贵州茅台", "食品饮料"),
("600036", "招商银行", "银行"),
("600900", "长江电力", "公用事业"),
]
def verify():
engine = ProtoValuationEngine()
passed, failed = 0, 0
for code, expected_name, expected_industry in VERIFY_STOCKS:
print(f"\n{'='*60}")
print(f"验证 {code} ({expected_name})")
print(f"{'='*60}")
try:
t0 = time.time()
result = engine.evaluate(code)
cold_time = time.time() - t0
t1 = time.time()
engine.evaluate(code)
hot_time = time.time() - t1
checks = []
# 名称
checks.append(("名称", expected_name in result.stock_name))
# 行业
checks.append(("行业", expected_industry in result.industry_name))
# 分位范围
checks.append(("PE分位", 0 <= result.pe_percentile_5y <= 100))
checks.append(("PB分位", 0 <= result.pb_percentile_5y <= 100))
checks.append(("评分", 0 <= result.score <= 100))
# 同行数
checks.append(("同行数", result.peer_count > 0))
# 缓存加速
checks.append(("缓存加速", hot_time < cold_time * 0.5))
# 评级
valid = ["低估", "合理偏低", "合理偏高", "高估", "严重高估"]
checks.append(("评级", result.level in valid))
all_ok = True
for name, ok in checks:
symbol = "✅" if ok else "❌"
print(f" {symbol} {name}")
all_ok = all_ok and ok
if all_ok:
passed += 1
print(f"\n 🎉 {code} 全部通过")
else:
failed += 1
print(f"\n ⚠️ {code} 部分未通过")
except Exception as e:
failed += 1
print(f" ❌ 异常: {e}")
import traceback
traceback.print_exc()
print(f"\n{'='*60}")
print(f"结果: {passed}/{len(VERIFY_STOCKS)} 通过, {failed}/{len(VERIFY_STOCKS)} 失败")
return failed == 0
if __name__ == "__main__":
exit(0 if verify() else 1)
五、验证清单
5.1 层级验证目标
| 层级 | 验证目标 | 通过标准 | 验证方法 |
|---|---|---|---|
| L0 连通性 | AKShare 3 个接口能正常返回数据 | 无异常、无超时 | adapter.get_all_stocks_spot() 返回 > 4000 行 |
| L1 数据正确性 | PE/PB 历史数据与同花顺/东方财富一致 | 偏差 < 5% | 手工对比 3 只股票的 PE 值 |
| L2 计算正确性 | PE 分位、行业排名在合理范围 | 0 ≤ 分位 ≤ 100,行业内 PE 低则排名好 | 茅台 PE 在食品饮料中是否处于合理位置 |
| L3 评分合理性 | 明显高估/低估的股票得分差异大 | 公用事业龙头得分 > 70;概念炒作股应 < 30 | 对比不同类型股票 |
| L4 缓存有效性 | 缓存命中后不再调用 AKShare | 第二次 evaluate() 用时 < 第一次的 30% | time.time() 计时 |
| L5 鲁棒性 | 异常输入不崩溃 | ST 股票、亏损股票 → 返回结果而非抛异常 | 测试边界 case |
5.2 必须验证的 AKShare 字段
| AKShare 接口 | 必须存在的列/字段 | 用于 |
|---|---|---|
stock_zh_a_spot_em() |
代码, 名称, 市盈率-动态, 市净率, 所处行业(或类似) |
全市场筛选、行业对比 |
stock_zh_a_hist() |
日期, 收盘, 市盈率, 市净率(或 peTTM, pbMRQ) |
历史分位计算 |
stock_individual_info_em() |
行业(或类似) |
个股行业分类 |
⚠️ 如果以上任一字段不存在或命名不一致,原型阶段就能暴露问题,避免后面所有模块返工。
六、原型执行计划(2-3 天)
Day 1(上午):环境搭建 + adapter.py 验证
├── pip install akshare pandas scipy
├── 在 Jupyter/ipython 中逐个测试 3 个 AKShare 接口
├── 记下返回 DataFrame 的列名(这是最重要的一步!)
└── 产出:确认 AKShare 接口可用,列名映射表
Day 1(下午):cache.py + industry.py
├── 实现 ProtoCache
├── 实现 ProtoIndustry(重点:确认行业列名)
└── 产出:能缓存全市场行情、能查找行业同行
Day 2(上午):relative.py
├── 实现 compute_percentile()
├── 实现 score()
└── 产出:能输出单只股票的分位和评分
Day 2(下午):engine.py + main.py
├── 实现 ProtoValuationEngine.evaluate()
├── 实现 CLI 入口
├── 手工运行 3 只股票看输出
└── 产出:可运行的 CLI 原型
Day 3(上午):verify.py + 边界情况
├── 实现验证脚本
├── 测试 ST 股票、亏损股票(PE 为负)、新股(数据不足)
├── 记录发现的问题
└── 产出:验证报告 + 问题清单
Day 3(下午):评审 + 决策
├── 评审原型验证结果
├── 决定:是否需要调整设计、是否进入量产开发
├── 如通过:启动 src/gupiao/data/ 正式开发
└── 产出:Go/No-Go 决策
七、成功标准(Go / No-Go)
| 标准 | 通过条件 | 权重 |
|---|---|---|
| AKShare 3 个接口全部正常调用 | 无报错,返回数据量合理 | 必须 |
| PE/PB 历史数据字段可用 | stock_zh_a_hist() 返回中包含 PE 和 PB 相关列 |
必须 |
| 行业分类字段可用 | 能从全市场数据中确定股票的申万行业 | 必须 |
| 分位计算结果合理 | 3 只股票分位在 0-100 之间,符合常识 | 必须 |
| 缓存加速效果明显 | 热调用速度 ≥ 冷调用的 5 倍 | 重要 |
| 异常输入不崩溃 | ST/亏损/新股 → 返回结果 + 警告,不抛未处理异常 | 重要 |
如果 4 项"必须"全部通过 → Go,进入正式开发。
如果"必须"项任一不通过 → 调整技术方案后再试。
如果"重要"项不通过 → 记录为已知问题,仍可 Go。
八、原型之后的路径
原型验证通过 (2-3天)
│
▼
┌──────────────────────────┐
│ 第 1 个穿刺完成 ✅ │
│ 验证了什么: │
│ - AKShare 数据可用 │
│ - 适配器 + 缓存模式有效 │
│ - PE/PB 分位计算正确 │
│ - 行业分类 + 同行对比可行 │
│ - 评分逻辑产出合理结果 │
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 第 2 个穿刺:质量评估 │
│ - Piotroski F-Score │
│ - 需要两期财报对比 │
│ - 验证 AKShare 财报接口 │
│ - 预计 2-3 天 │
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 第 3 个穿刺:DCF 估值 │
│ - FCF 推算 + 两阶段模型 │
│ - 需要现金流量表 │
│ - 验证报表数据可用性 │
│ - 预计 3-4 天 │
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 串联 + 正式开发 │
│ - 三个模块串联为估值引擎 │
│ - 引入 ResilienceLayer │
│ - 进入 src/gupiao/ 开发 │
│ - 按设计文档 12 周路线 │
└──────────────────────────┘
核心理念:一个原型 = 一个工作日 = 一次决策。不要在文档里论证"能不能做",用代码跑一遍就知道了。

浙公网安备 33010602011771号