初始提交:高股息策略研究与回测系统

从 Point-in-Time 股票筛选到统一 Web 前端的完整链路:
筛选 → 画像 → 策略 → 回测 → Walk-forward → 绩效分析 → 报告/前端。

架构
- 数据层与策略层分离;策略代码不写 SQL,只经 data/repo.py 取数
- 所有业务阈值集中在 config/*.yml,代码零硬编码(字段写错直接报错)
- 报告只做「run_id → SQL → 渲染」,不做任何计算,数字可追溯
- 前后端分离:output/ 静态站点 + hdiv web 提供的 REST API

数据安全
- 只增不删:SQL 钩子拦截 DELETE/DROP/TRUNCATE,并有源码扫描测试守护
- qlib 原有表只读,本项目数据写入 hd_ 前缀表
- 回补使用 INSERT IGNORE,保证既有行零改动
- .env 存密钥且已 gitignore;output/、logs/、.venv/ 不入库

交付物
- 30 张 hd_* 表、7 个 YAML 配置、283 项自动化测试
- 统一 Web 前端(hash 路由 SPA)+ nginx 部署配置与 launchd 托管脚本

如实声明的限制
- 策略缺少稳定的样本外超额收益(Walk-forward 7 窗口均值 -0.95%,
  基准 +2.29%);其价值体现在回撤控制,而非超额收益
- 涨跌停/停牌约束仅覆盖 2019 年起;index_weight 尚未填充
- AI Agent 层(plan.md 第四版 P8)未实现

详见 docs/user-guide.md 与 docs/implementation-status.md。
This commit is contained in:
2026-10-03 13:54:56 +08:00
commit fce725e13c
127 changed files with 28001 additions and 0 deletions
+562
View File
@@ -0,0 +1,562 @@
"""单位换算与筛选滤网测试。
这里覆盖的都是**开发过程中真实踩到过**的坑,每个测试对应一次静默错误:
1. ``total_mv`` 是万元而阈值配的是元 → 筛选结果为空(不报错);
2. 拿季报 ROE(年初至今累计)去比「5 年年均 ROE」→ 好公司全被误杀;
3. 银行负债率天然 90%+ → 整个金融板块被误杀;
4. 分红除权晚于 asof → 稳定分红公司被误判为「连续分红 0 年」。
"""
from __future__ import annotations
from datetime import date
import pandas as pd
import pytest
from hdiv.data.units import (
normalize_financial_panel,
normalize_market_panel,
pct_to_ratio,
verify_market_units,
vol_shou_to_shares,
wan_to_shares,
wan_to_yuan,
)
# ---------------------------------------------------------------------------
# 单位换算
# ---------------------------------------------------------------------------
def test_wan_to_yuan() -> None:
assert wan_to_yuan(pd.Series([1.0, 100.0])).tolist() == [1e4, 1e6]
def test_wan_to_shares() -> None:
assert wan_to_shares(pd.Series([125619.78])).iloc[0] == pytest.approx(1.2561978e9)
def test_pct_to_ratio() -> None:
assert pct_to_ratio(pd.Series([5.08])).iloc[0] == pytest.approx(0.0508)
def test_vol_shou_to_shares() -> None:
assert vol_shou_to_shares(pd.Series([100])).iloc[0] == 10000
def test_normalize_market_panel_units() -> None:
"""茅台 2024-06-28 实测值:总市值 1.84 万亿元,总股本 12.56 亿股。"""
df = pd.DataFrame(
{
"symbol": ["600519.SH"],
"close": [1467.39],
"total_share": [125619.78], # 万股
"total_mv": [184333208.97], # 万元
"circ_mv": [184333208.97],
"dv_ttm": [5.1720], # 百分数
"turnover_rate": [0.25],
}
)
out = normalize_market_panel(df)
assert out["total_mv"].iloc[0] == pytest.approx(1.8433320897e12, rel=1e-6)
assert out["total_share"].iloc[0] == pytest.approx(1.2561978e9, rel=1e-9)
assert out["dv_ttm"].iloc[0] == pytest.approx(0.051720)
assert out["_units"].iloc[0] == "yuan/shares/ratio"
def test_normalize_market_panel_does_not_mutate_input() -> None:
df = pd.DataFrame({"total_mv": [100.0], "total_share": [10.0]})
before = df.copy()
normalize_market_panel(df)
pd.testing.assert_frame_equal(df, before)
def test_normalize_financial_panel_converts_pct() -> None:
df = pd.DataFrame({"roe": [15.13], "debt_to_assets": [90.23], "total_revenue": [1.78e11]})
out = normalize_financial_panel(df)
assert out["roe"].iloc[0] == pytest.approx(0.1513)
assert out["debt_to_assets"].iloc[0] == pytest.approx(0.9023)
assert out["total_revenue"].iloc[0] == 1.78e11, "金额列本就以元计,不得被换算"
def test_verify_market_units_detects_correct_data() -> None:
df = pd.DataFrame(
{
"close": [10.0, 20.0],
"total_share": [1e9, 5e8],
"total_mv": [1e10, 1e10],
}
)
v = verify_market_units(df)
assert v["checked"] == 2 and v["bad"] == 0
assert v["median_ratio"] == pytest.approx(1.0)
def test_identity_alone_cannot_detect_wan_vs_yuan() -> None:
"""恒等式对「整体万元/元混淆」无效 —— 因为 元/股 × 万股 = 万元。
这是一个容易误以为「有了恒等式检查就安全」的陷阱,必须显式记录:
原始单位下比值仍然恰好是 1。
"""
raw = pd.DataFrame(
{"close": [1467.39], "total_share": [125619.78], "total_mv": [184333208.97]}
)
v = verify_market_units(raw)
assert v["identity_ok"] is True, "恒等式在原始单位下同样成立"
assert v["unit_ok"] is False, "但绝对量级检查必须发现单位错误"
assert v["detected_unit"] == "wan"
def test_verify_market_units_accepts_normalized_yuan() -> None:
norm = normalize_market_panel(
pd.DataFrame(
{"close": [1467.39], "total_share": [125619.78], "total_mv": [184333208.97]}
)
)
v = verify_market_units(norm)
assert v["unit_ok"] is True and v["identity_ok"] is True
assert v["detected_unit"] == "yuan"
def test_verify_market_units_detects_partial_conversion() -> None:
"""只换算一个字段(常见疏漏)也必须被抓出来。"""
df = pd.DataFrame(
{"close": [10.0], "total_share": [1e9], "total_mv": [1e10 / 1e4]} # 市值漏换算
)
v = verify_market_units(df)
assert v["bad"] == 1
def test_verify_market_units_empty() -> None:
assert verify_market_units(pd.DataFrame())["checked"] == 0
# ---------------------------------------------------------------------------
# 滤网:行业豁免
# ---------------------------------------------------------------------------
class _FakeRepo:
def __init__(self, st: set[str] | None = None, susp: set[str] | None = None) -> None:
self._st = st or set()
self._susp = susp or set()
def st_symbols(self, asof, include_delisting=True): # noqa: ANN001, ARG002
return self._st
def suspended_on(self, asof): # noqa: ANN001, ARG002
return self._susp
def _frame(**kwargs) -> pd.DataFrame:
base = {
"symbol": ["600036.SH", "601088.SH"],
"name": ["招商银行", "中国神华"],
"industry": ["银行", "煤炭开采"],
"market": ["主板", "主板"],
"exchange": ["SSE", "SSE"],
"listed_years": [30.0, 17.0],
"is_fresh": [True, True],
"total_mv": [8.6e11, 8.8e11],
"circ_mv": [7.0e11, 7.3e11],
"avg_amount": [1e9, 8e8],
"close": [34.19, 44.37],
"debt_ratio": [0.902, 0.236],
"total_hldr_eqy_exc_min_int": [3.5e11, 4.0e11],
}
base.update(kwargs)
return pd.DataFrame(base)
def test_risk_filter_exempts_banks_from_leverage() -> None:
from hdiv.core.config import RiskFilterConfig
from hdiv.universe.filters.risk import RiskFilter
f = RiskFilter(RiskFilterConfig(max_debt_to_assets=0.80), exempt_leverage=["银行"])
out = f.compute(_frame(), _FakeRepo(), date(2024, 6, 28))
assert bool(out.passed.iloc[0]) is True, "银行必须豁免负债率上限"
assert bool(out.passed.iloc[1]) is True, "低负债公司自然通过"
assert out.values["600036.SH"]["debt_ratio_exempt"] is True
def test_risk_filter_still_rejects_high_leverage_non_exempt() -> None:
from hdiv.core.config import RiskFilterConfig
from hdiv.universe.filters.risk import RiskFilter
f = RiskFilter(RiskFilterConfig(max_debt_to_assets=0.80), exempt_leverage=["银行"])
df = _frame(industry=["房地产", "煤炭开采"])
out = f.compute(df, _FakeRepo(), date(2024, 6, 28))
assert bool(out.passed.iloc[0]) is False
assert "资产负债率" in out.reasons["600036.SH"]
def test_quality_filter_exempts_banks_from_fcf_and_leverage() -> None:
from hdiv.core.config import QualityFilterConfig
from hdiv.universe.filters.quality import FinancialQualityFilter
cfg = QualityFilterConfig(min_ocf_to_profit=0.60, max_debt_to_assets=0.80)
f = FinancialQualityFilter(cfg, exempt_leverage=["银行"], exempt_fcf=["银行"])
df = _frame(roe_avg=[0.1513, 0.1406], ocf_to_profit=[-0.03, 1.80])
out = f.compute(df, _FakeRepo(), date(2024, 6, 28))
assert bool(out.passed.iloc[0]) is True, "银行豁免 FCF 与负债率"
def test_quality_filter_uses_annual_average_not_quarterly() -> None:
"""季报 ROE 3.47% 不该被拿去比年均 8% 的阈值。"""
from hdiv.core.config import QualityFilterConfig
from hdiv.universe.filters.quality import FinancialQualityFilter
cfg = QualityFilterConfig(min_roe_5y_avg=0.08, min_ocf_to_profit=None)
f = FinancialQualityFilter(cfg)
df = _frame(
industry=["白酒", "煤炭开采"],
roe=[0.0347, 0.0380], # 季报累计值(会被 avg 覆盖)
roe_avg=[0.1513, 0.1406], # 年报 5 年平均
)
out = f.compute(df, _FakeRepo(), date(2024, 6, 28))
assert bool(out.passed.iloc[0]) is True, "应使用 roe_avg 而非季报 roe"
assert out.values["600036.SH"]["roe"] == pytest.approx(0.1513)
def test_quality_filter_falls_back_to_latest_when_no_average() -> None:
from hdiv.core.config import QualityFilterConfig
from hdiv.universe.filters.quality import FinancialQualityFilter
cfg = QualityFilterConfig(min_roe_5y_avg=0.08, min_ocf_to_profit=None)
f = FinancialQualityFilter(cfg)
df = _frame(industry=["白酒", "煤炭开采"], roe=[0.20, 0.03]) # 无 roe_avg 列
out = f.compute(df, _FakeRepo(), date(2024, 6, 28))
assert bool(out.passed.iloc[0]) is True
assert bool(out.passed.iloc[1]) is False
# ---------------------------------------------------------------------------
# 分红连续性:一年宽限期
# ---------------------------------------------------------------------------
def _div(symbol: str, end_year: int, ex_year: int, dps: float = 1.0, month: int = 7) -> dict:
return {
"symbol": symbol,
"end_date": date(end_year, 12, 31),
"imp_ann_date": date(ex_year, month - 1 if month > 1 else 12, 1),
"div_proc": "实施",
"cash_div_tax": dps,
"cash_div": dps,
"stk_div": None,
"base_share": 10000.0,
"ex_date": date(ex_year, month, 15),
}
def test_continuity_within_target_year() -> None:
"""FY2023 分红已在 2024-05 除权 → target=2023,连续 5 年。"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
recs = {2023: 2024, 2022: 2023, 2021: 2022, 2020: 2021, 2019: 2020}
rows = [_div("600036.SH", fy, ex, month=5) for fy, ex in recs.items()]
s = DividendFilter._stats(rows, target_year=2023, asof=date(2024, 6, 28),
cfg=DividendFilterConfig())
assert s["dividend_continuity_years"] == 5
assert s["continuity_grace_used"] is False
def test_continuity_grace_when_ex_date_lags() -> None:
"""神华实测情形:FY2023 分红要 2024-07 才除权,asof=2024-06-28 时不可见。
此时必须用一年宽限期从 FY2022 起算,而不是判定为「中断分红」。
"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
rows = [_div("601088.SH", fy, fy + 1, month=7) for fy in (2022, 2021, 2020, 2019, 2018)]
s = DividendFilter._stats(rows, target_year=2023, asof=date(2024, 6, 28),
cfg=DividendFilterConfig())
assert s["latest_dividend_year"] == 2022
assert s["continuity_start_year"] == 2022
assert s["continuity_grace_used"] is True
assert s["dividend_continuity_years"] == 5, "不应因为除权晚而误判中断"
def test_continuity_zero_when_genuinely_stopped() -> None:
"""最近可见分红比应考核财年早两年以上 → 视为真的中断。"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
rows = [_div("000002.SZ", fy, fy + 1) for fy in (2019, 2018, 2017, 2016, 2015)]
s = DividendFilter._stats(rows, target_year=2023, asof=date(2024, 6, 28),
cfg=DividendFilterConfig())
assert s["dividend_continuity_years"] == 0
def test_continuity_breaks_on_gap() -> None:
"""有断档:2022 有、2021 无 → 连续 1 年。"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
rows = [_div("X.SZ", fy, fy + 1) for fy in (2022, 2020, 2019)]
s = DividendFilter._stats(rows, target_year=2023, asof=date(2024, 6, 28),
cfg=DividendFilterConfig())
assert s["dividend_continuity_years"] == 1
def test_ttm_dps_only_counts_ex_date_in_window() -> None:
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
rows = [
_div("X.SZ", 2023, 2024, dps=1.5, month=4), # 窗口内
_div("X.SZ", 2022, 2023, dps=1.2, month=7), # 窗口内(>2023-06-28)
_div("X.SZ", 2021, 2022, dps=1.0, month=7), # 窗口外
]
s = DividendFilter._stats(rows, target_year=2023, asof=date(2024, 6, 28),
cfg=DividendFilterConfig())
assert s["ttm_dps"] == pytest.approx(2.7)
def test_shift_year_handles_leap_day() -> None:
from hdiv.universe.filters.dividend import _shift_year
assert _shift_year(date(2024, 2, 29), -1) == date(2023, 2, 28)
assert _shift_year(date(2024, 6, 28), -1) == date(2023, 6, 28)
def test_target_year_uses_latest_annual_report() -> None:
"""最新年报为 FY2023 时,考核目标年应为 2023。"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
df = pd.DataFrame(
{
"symbol": ["A.SH", "B.SH"],
"fin_end_date": [date(2024, 3, 31), date(2023, 12, 31)],
}
)
t = DividendFilter._target_years(df, date(2024, 6, 28))
# 能看到 2024Q1 报,说明 FY2023 年报必然已披露 → 目标年 2023
assert t["A.SH"] == 2023, "有 2024Q1 报 → FY2023 年报已出"
assert t["B.SH"] == 2023, "有 FY2023 年报 → 目标是 2023"
df2 = pd.DataFrame({"symbol": ["C.SH"], "fin_end_date": [date(2023, 9, 30)]})
assert DividendFilter._target_years(df2, date(2024, 6, 28))["C.SH"] == 2022, (
"只看到 2023Q3 → FY2023 年报未出,退回到 2022"
)
def test_payout_ratio_uses_base_share() -> None:
"""总现金分红 = 每股分红 × 基准股本(万股→股)。"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
rows = [
{
"symbol": "X.SH", "end_date": date(2023, 12, 31), "ex_date": date(2024, 6, 1),
"div_proc": "实施", "cash_div_tax": 2.0, "base_share": 10000.0, # 1 亿股
}
]
row = pd.Series({"n_income_attr_p": 4e8, "free_cashflow": 8e8})
out = DividendFilter._payout_and_cover(rows, row, date(2024, 6, 28),
DividendFilterConfig())
assert out["total_cash_dividend"] == pytest.approx(2.0 * 10000.0 * 1e4)
assert out["payout_ratio"] == pytest.approx(0.5)
# 总现金分红 = 2.0 元 × 10000 万股 × 1e4 = 2e8 元;FCF 8e8 → 覆盖 4 倍
assert out["fcf_dividend_cover"] == pytest.approx(4.0)
def test_fcf_fallback_when_tushare_missing() -> None:
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
rows = [{
"symbol": "X.SH", "end_date": date(2023, 12, 31), "ex_date": date(2024, 6, 1),
"div_proc": "实施", "cash_div_tax": 1.0, "base_share": 1000.0,
}]
row = pd.Series({
"n_income_attr_p": 5e6, "free_cashflow": None,
"n_cashflow_act": 1e7, "c_pay_dist_dpcp_int_exp": 2e6,
})
out = DividendFilter._payout_and_cover(rows, row, date(2024, 6, 28),
DividendFilterConfig())
assert out["fcf_source"] == "ocf_minus_dist"
assert out["free_cashflow"] == pytest.approx(8e6)
# ---------------------------------------------------------------------------
# 滤网结果索引契约(曾经把 DataFrame 索引当成 symbol 用的真实 bug)
# ---------------------------------------------------------------------------
def test_filter_outcome_index_is_dataframe_index_not_symbol() -> None:
"""滤网的 passed 索引必须与传入 DataFrame 的索引一致。
选择器据此用 ``live.at[i, "symbol"]`` 映射;
若误把索引当 symbol,会导致「全部淘汰」(本项目开发中确实发生过)。
"""
from hdiv.core.config import MarketFilterConfig
from hdiv.universe.filters.market import MarketFilter
df = _frame()
df.index = [10, 20] # 非默认索引
f = MarketFilter(MarketFilterConfig(min_market_cap=1e10))
out = f.compute(df, _FakeRepo(), date(2024, 6, 28))
assert list(out.passed.index) == [10, 20]
assert set(out.passed.values) <= {True, False}
# ---------------------------------------------------------------------------
# 数据缺失策略(安全边际策略的关键取舍)
# ---------------------------------------------------------------------------
def _div_cfg(**kw):
from hdiv.core.config import DividendFilterConfig
base = {
"min_dividend_yield": None,
"min_continuous_years": 0,
"min_dividend_years_in_window": 0,
"max_payout_ratio": None,
"require_positive_fcf": True,
"min_fcf_dividend_cover": None,
}
base.update(kw)
return DividendFilterConfig(**base)
def test_missing_fcf_passes_by_default() -> None:
"""默认宽松:数据缺失放行,避免因未同步而误杀。"""
from hdiv.universe.filters.dividend import DividendFilter
cfg = _div_cfg(on_missing_data="pass")
stats = {"free_cashflow": None, "dividend_continuity_years": 0,
"dividend_years_in_window": 0, "dividend_yield": 0.05}
assert DividendFilter._reject_reason(stats, cfg) is None
def test_missing_fcf_rejected_when_strict() -> None:
"""严格模式:无法验证现金流即淘汰 —— 忠于「安全边际」的策略逻辑。"""
from hdiv.universe.filters.dividend import DividendFilter
cfg = _div_cfg(on_missing_data="reject")
stats = {"free_cashflow": None, "dividend_continuity_years": 0,
"dividend_years_in_window": 0, "dividend_yield": 0.05}
why = DividendFilter._reject_reason(stats, cfg)
assert why is not None and "缺失" in why
def test_negative_fcf_rejected_in_both_modes() -> None:
"""FCF 为负时两种模式都必须淘汰 —— 宽松不等于放行已知风险。"""
from hdiv.universe.filters.dividend import DividendFilter
stats = {"free_cashflow": -1e8, "dividend_continuity_years": 0,
"dividend_years_in_window": 0, "dividend_yield": 0.05}
for mode in ("pass", "reject"):
why = DividendFilter._reject_reason(stats, _div_cfg(on_missing_data=mode))
assert why is not None and "负" in why, f"{mode} 模式必须拒绝负 FCF"
def test_missing_coverage_strict_only() -> None:
from hdiv.universe.filters.dividend import DividendFilter
stats = {"free_cashflow": 1e8, "fcf_dividend_cover": None,
"dividend_continuity_years": 0, "dividend_years_in_window": 0,
"dividend_yield": 0.05}
assert DividendFilter._reject_reason(
stats, _div_cfg(min_fcf_dividend_cover=1.0, on_missing_data="pass")
) is None
assert DividendFilter._reject_reason(
stats, _div_cfg(min_fcf_dividend_cover=1.0, on_missing_data="reject")
) is not None
def test_on_missing_data_invalid_value_rejected() -> None:
from hdiv.core.errors import SchemaValidationError
with pytest.raises(Exception) as ei:
_div_cfg(on_missing_data="maybe")
assert "on_missing_data" in str(ei.value) or "Input should be" in str(ei.value)
del SchemaValidationError
# ---------------------------------------------------------------------------
# 分红支付率必须与分红**同财年**(真实踩到的错误)
# ---------------------------------------------------------------------------
def test_payout_uses_same_fiscal_year_not_latest_quarter() -> None:
"""回归:曾用「FY2023 分红 ÷ 2024Q1 净利润」算出 230.9% 的荒谬支付率。
正确口径下美的集团 FY2023 为:分红 207.8 亿 ÷ 净利 337.2 亿 = 61.63%。
"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
recs = [{
"symbol": "000333.SZ", "end_date": date(2023, 12, 31),
"ex_date": date(2024, 5, 15), "div_proc": "实施",
"cash_div_tax": 3.0, "base_share": 692675.9241,
}]
# 同财年(FY2023)财务
fy_row = pd.Series({
"n_income_attr_p": 3.372e10, "free_cashflow": 7.16e10,
})
out = DividendFilter._payout_and_cover(
recs, fy_row, date(2024, 6, 28), DividendFilterConfig()
)
assert out["financial_year"] == 2023
assert out["payout_ratio"] == pytest.approx(207.8 / 337.2, abs=0.01)
assert out["payout_basis"] == "same_fiscal_year"
def test_payout_not_computed_without_same_year_row() -> None:
"""缺少同财年财务时必须返回「不可得」,而不是退回到最新季报。"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
recs = [{
"symbol": "X.SZ", "end_date": date(2023, 12, 31),
"ex_date": date(2024, 5, 15), "div_proc": "实施",
"cash_div_tax": 1.0, "base_share": 10000.0,
}]
out = DividendFilter._payout_and_cover(
recs, None, date(2024, 6, 28), DividendFilterConfig()
)
assert out["payout_ratio"] is None
assert out["fcf_dividend_cover"] is None
assert out["payout_basis"] == "missing_same_year_financials"
def test_total_cash_dividend_uses_base_share() -> None:
"""base_share 是**万股**,漏乘 1e4 会把支付率缩小一万倍。"""
from hdiv.core.config import DividendFilterConfig
from hdiv.universe.filters.dividend import DividendFilter
recs = [{
"symbol": "X.SZ", "end_date": date(2023, 12, 31),
"ex_date": date(2024, 5, 15), "div_proc": "实施",
"cash_div_tax": 2.0, "base_share": 10000.0, # 1 亿股
}]
row = pd.Series({"n_income_attr_p": 4e8, "free_cashflow": 8e8})
out = DividendFilter._payout_and_cover(
recs, row, date(2024, 6, 28), DividendFilterConfig()
)
assert out["total_cash_dividend"] == pytest.approx(2.0 * 10000.0 * 1e4)
assert out["payout_ratio"] == pytest.approx(0.5)
def test_dividend_records_include_base_share() -> None:
"""回归:repo.dividend_records 曾漏选 base_share,
导致 payout_ratio 与 fcf_dividend_cover 在全库范围内静默为 NULL,
进而使 max_payout_ratio / min_fcf_dividend_cover 两个筛选条件从未生效。
"""
import inspect
from hdiv.data.repo import Repo
src = inspect.getsource(Repo.dividend_records)
assert "base_share" in src, "dividend_records 必须选出 base_share"