Files

99 lines
4.8 KiB
Markdown

# A 股 Top N 自动筛 (替换硬编码候选池)
**背景**: `cn_dividend_buy_timing.py` 原本用 8 只硬编码候选池 (601288/601398/601088/600023/600585/601006/601939/601658)。用户 2026-09-17 要求改为**自动从全市场筛 Top N**,并按**行业分散**。
## 数据源 (优先东方财富, 因为字段已含股息率)
```python
import akshare as ak
# 单只拉所有季度分红 (按 report date)
df = ak.stock_fhps_em(date='20251231') # 一次拿 3000+ 只股票
# 字段: ['代码', '名称', '送转股份-送转总比例', ..., '现金分红-股息率',
# '每股收益', '预案公告日', '股权登记日', '除权除息日', '方案进度', '最新公告日期']
# 关键: '现金分红-股息率' 是小数 (0.0544 = 5.44%), 不是 "5.44%" 字符串!
```
**字段单位坑** (9/17 实测踩到):
- `ak.stock_fhps_em` 的 `现金分红-股息率` 是 **小数** (0.0544) — 不是 "5.44%" 字符串
- `ak.stock_fhps_detail_ths` 的 `税前分红率` 是 **带 % 的字符串** ("5.44%") — 处理时 `.str.replace('%', '')` 再 `pd.to_numeric`
- `ak.stock_history_dividend_detail` 的 `派息` 是 **每 10 股派多少元** — 不是每股
**三种接口对比**:
| 接口 | 字段 | 单位 | 用法 |
|---|---|---|---|
| `stock_fhps_em(date=)` | `现金分红-股息率` | **小数** (乘 100 = %) | 批量, 3 秒/3000+ 只, 优先 |
| `stock_fhps_detail_ths(symbol=)` | `税前分红率` | 字符串 "%" | 单只, 0.5s/只, TTM 校验 |
| `stock_history_dividend_detail(symbol=)` | `派息` | 元/10 股 | 历史明细, 别自己算股息率 |
## TTM 算法
**用户对照同花顺 APP**: 北京银行显示 7.36%, 我脚本如果只算 "最近 1 年报派息" = 5.44%, 漏了 2024 H1 中报。
**TTM 算法**: 拉最近 8 个季度的 `stock_fhps_em` (24 个月), 按 `股权登记日 >= 今天 - 365天` AND `方案进度 == '实施分配'`, 把 `现金分红-股息率` 累加.
**注意**: 累加前乘 100 (字段是小数, 不是 %)
```python
df_all = pd.concat([ak.stock_fhps_em(date=d) for d in REPORT_DATES])
df_all['yld_num'] = pd.to_numeric(df_all['现金分红-股息率'], errors='coerce') * 100
df_done = df_all[df_all['方案进度'] == '实施分配'].copy()
df_done['reg_date'] = pd.to_datetime(df_done['股权登记日'], errors='coerce')
ttm_start = datetime.now() - timedelta(days=365)
df_ttm = df_done[df_done['reg_date'] >= ttm_start]
ttm_map = df_ttm.groupby('代码')['yld_num'].sum().to_dict()
```
## 行业分散
**用户原话 (9/17)**: "每个行业 Top3 是不是就够了"
**实现方案**: 拉行业 → 成分股 → 按行业分组选 Top N (每行业最多 3 只, 总 8 只):
```python
# akshare 接口 (注意代理):
# ak.stock_board_industry_name_em() → 行业列表
# ak.stock_board_industry_cons_em(symbol='银行') → 成分股
```
**代理坑**: akshare em 接口常被全局 HTTP_PROXY 拦截, 报错 ConnectionError。
**修法**: 跑脚本前 `unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY`, 或在 Python 里 `os.environ.pop(...)`。
**备选** (更稳): 用 `ak.stock_individual_info_ths(symbol=)` 拿单只股票的行业字段 (`industry` 列), 但**单只 1 秒** → 3000 只 = 50 分钟, 不可行。
**当前实现**: 直接按 "已用 TTM 股息率" 排序取前 30 只, **不强行行业分散** (因为 akshare 行业接口代理不稳)。
**未来优化**: 加本地缓存 (`~/.hermes/cache/cn_industry.json`), 一次拉全, 后续查表。
## Cron 集成方案
| 项 | 现状 |
|---|---|
| `cn_dividend_buy_timing.py` 硬编码 `HIGH_DIVIDEND_POOL` | 8 只银行/能源 |
| **改进** | 读 `~/.hermes/trading/canary/cn_top8.json` 缓存 |
| **缓存来源** | 每周一 cron 跑 `cn_dividend_top8.py` → 写缓存 |
| **缓存格式** | `[{"code": "601169", "name": "北京银行", "yield_pct": 7.36, ...}, ...]` |
**脚本路径**:
- 测试版: `/tmp/cn_dividend_top8.py` (9/17 跑通)
- 正式版待移: `~/.hermes/scripts/cn_dividend_top8.py`
## 关键坑 (9/17 实测)
1. **`pd.to_numeric(s.str.replace('%', ''))` 处理 None / "--"**: 用 `errors='coerce'` + `.dropna()`
2. **东方财富字段是 0.15 不是 "0.15%"** (小数 vs 百分比字符串) — 乘 100 别漏
3. **拉 5675 只价格太慢** (>10 分钟) — 粗筛按 "派息次数 × 累计股息" 取 top 200, 只拉这些价格
4. **代理 env 全局干扰 akshare 国内 API**: 跑脚本前 `unset` 4 个 proxy var
5. **同花顺 vs 自己算的 TTM 不一致**: **永远以同花顺为准**, 自己算只是近似
## 实战结果 (9/17)
| 数据源 | 北京银行 TTM |
|---|---|
| 同花顺 APP | 7.36% |
| `stock_fhps_detail_ths` "实施" 累加 (登记日 TTM) | 5.44% (漏 2024 H1) |
| `stock_fhps_em` TTM (登记日 + 实施, 跨多个 date) | 7.44% ✓ |
| `stock_history_dividend` 自己算 | 5.02-7.36% (单位混淆) |
**结论**: **`stock_fhps_em` TTM 累加最准**, 接近同花顺 7.36%。