Files
Hermes-Skills/dividend-scanner/references/cn-top8-weekly-batch-v5-ths-parallel.md

175 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# A 股 Top N 高息扫描 - v5 周批量化版(2026-09-19 实装)
## 背景
之前的 A 股高息扫描全靠 `cn_dividend_buy_timing.py` 硬编码 8 只候选池(601288/601398/601088/600023/600585/601006/601939/601658),用户要求改为自动从全市场筛 Top N。
v1-v4 失败链路:
- v1: 用 akshare `stock_history_dividend_detail` 自己算 TTM → 单位错了(/10 vs 每股)
- v3: 用东方财富 `现金分红-股息率`(小数)→ 字段单位忘了 `* 100`,差 27 倍
- v4: 改用财年滚动算法 + 含预案 → 算到 5.44%,同花顺显示 7.36%,**还是不准**
**用户原话**: "你一个AI写算法,还能比得过人家真正的专家吗?... 我也不专业,算出来我也不知道对不对,我也是去看同花顺的"
**核心教训**: **agent 算法永远追不上同花顺/东方财富专业团队** → **直接用同花顺的接口拿数据**,不自己算。
## v5 解决方案(用户接受)
**两步法**:
1. **东方财富 `stock_fhps_em(date)` 批量粗筛**(3 秒/季,1 拉 3600+ 只,1 次拿全部候选)
2. **同花顺 `stock_fhps_detail_ths(symbol)` 并行校验**(0.5s/只,8 线程并行,100 只 7.7s)
**总耗时**:~25 秒(东方财富 15s + 同花顺 8s + 长桥价 2s)
## 关键代码骨架
```python
import akshare as ak
from concurrent.futures import ThreadPoolExecutor
import os
# 0. 关代理(akshare 国内接口被全局 proxy 干扰)
for k in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY']:
os.environ.pop(k, None)
REPORT_DATES = ['20231231', '20240630', '20241231', '20250630', '20251231', '20260630']
# 1. 东方财富批量粗筛(先 TTM 365 天滚动)
def fetch_eastmoney_batch():
all_dfs = []
for date in REPORT_DATES:
df = ak.stock_fhps_em(date=date) # 1 次 3600+ 只
df['_date'] = date
all_dfs.append(df)
return pd.concat(all_dfs, ignore_index=True)
def calc_ttm_yield(df_all):
df_all['code'] = df_all['代码'].astype(str).str.zfill(6)
df_all['yld_num'] = pd.to_numeric(df_all['现金分红-股息率'], errors='coerce') * 100 # 字段是小数!
df_done = df_all[df_all['方案进度'] == '实施分配']
df_done['reg_date'] = pd.to_datetime(df_done['股权登记日'], errors='coerce')
now = datetime.now()
ttm_start = now - timedelta(days=365)
df_ttm = df_done[df_done['reg_date'] >= ttm_start]
return df_ttm.groupby('code')['yld_num'].sum().to_dict(), \
df_done.groupby('code').size().to_dict()
# 2. 同花顺并行校验(取粗筛 top 100)
def fetch_ths_one(code):
df = ak.stock_fhps_detail_ths(symbol=code)
# 算连续派息年数(按实施公告日)
if '实施公告日' in df.columns:
df['year'] = pd.to_datetime(df['实施公告日'], errors='coerce').dt.year
years = sorted(df['year'].dropna().unique(), reverse=True)
cont = 1
for i in range(1, len(years)):
if years[i-1] - years[i] == 1: cont += 1
else: break
# TTM = 实施公告日 365 天内税前分红率 sum
if '税前分红率' in df.columns:
now = datetime.now()
ttm = now - timedelta(days=365)
recent = df[pd.to_datetime(df['实施公告日'], errors='coerce') >= ttm]
yld_str = recent['税前分红率'].astype(str).str.replace('%', '', regex=False)
ttm_yield = float(pd.to_numeric(yld_str, errors='coerce').dropna().sum())
else:
ttm_yield = 0
return code, cont, ttm_yield
def ths_parallel_verify(candidates, workers=8):
results = {}
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(fetch_ths_one, code): code for code in candidates}
for fut in as_completed(futures):
code, cont, yld = fut.result()
results[code] = (cont, yld)
return results
# 3. 过滤 + 排序
def filter_and_rank(df_all, ttm_map, cnt_map, ths_results, prices):
name_map = df_all.groupby('代码')['名称'].first().to_dict()
rows = []
for code, em_yield in ttm_map.items():
name = str(name_map.get(code, ''))
if 'ST' in name or '*ST' in name: continue
if cnt_map.get(code, 0) < 5: continue
cont, ths_yield = ths_results.get(code, (0, 0))
if cont < 5: continue
# 同花顺优先(它才是权威),没查到用东方财富
final_yield = ths_yield if ths_yield > 0 else em_yield
rows.append({
'code': code, 'name': name,
'yield_pct': round(final_yield, 2),
'continuous': cont,
'div_count': cnt_map.get(code, 0),
'price': prices.get(code, 0),
'data_source': 'ths' if ths_yield > 0 else 'em',
})
rows.sort(key=lambda x: -x['yield_pct'])
return rows[:TOP_N]
```
## 实战数据 (2026-09-19 跑通)
**Top 8**(全部用同花顺 TTM 数据源):
| 代码 | 名称 | TTM 股息率 | 连续(年) | 数据源 |
|---|---|---|---|---|
| 603165 | 荣晟环保 | 13.45% | 10 | ths |
| 000915 | 华特达因 | 8.6% | 14 | ths |
| 603551 | 奥普科技 | 7.81% | 7 | ths |
| 300406 | 九强生物 | 7.08% | 12 | ths |
| 002867 | 周大生 | 6.96% | 10 | ths |
| 601919 | 中远海控 | 6.9% | 6 | ths |
| 600015 | 华夏银行 | 6.06% | 24 | ths |
| 600750 | 华润江中 | 5.88% | 26 | ths |
**耗时**: 25.3 秒(东方财富 14.5s + 同花顺并行 7.7s + 长桥 3s)
## 关键踩坑
| 坑 | 解决 |
|---|---|
| **东方财富 `现金分红-股息率` 是小数(0.0544)不是字符串** | `* 100` 转 % |
| **同花顺 `税前分红率` 是字符串("5.44%")** | `.str.replace('%', '')` + `float()` |
| **akshare 国内接口被全局 HTTP_PROXY 拦截** | 跑脚本前 `unset` 4 个 proxy env |
| **北京银行 5.44% vs 同花顺 7.36%** | **财年滚动**算法 + 含预案 = **正解 7.44%** |
| **同花顺 ths detail 没批量接口** | ThreadPoolExecutor 并行(8 线程够稳,16 触发风控) |
| **拉 5675 只价格超 10 分钟** | 粗筛 top 100 后只拉 100 只(长桥 batch 内部 15/批) |
## Cron 集成(待 user 决策)
| 项 | 现状 |
|---|---|
| **脚本路径** | `/tmp/cn_dividend_top8_v5.py`(测试版) |
| **正式版** | 待移到 `~/.hermes/scripts/cn_dividend_top8.py` |
| **cron 频率** | 每周一(用户偏好一周跑一次) |
| **缓存路径** | `~/.hermes/trading/cn_top8.json` |
| **缓存格式** | `[{"code": "601169", "name": "...", "yield_pct": 7.36, "continuous": 19, ...}]` |
| **下游消费** | `cn-dividend-buy-timing` cron 改成读 JSON,不再用 `HIGH_DIVIDEND_POOL` 硬编码 |
| **行业分散** | **不做**(akshare 行业接口代理不稳,且用户未强求) |
## 同花顺 ths 接口 vs akshare ths 接口对比
| 项 | akshare `stock_fhps_detail_ths` | 同花顺 ths 官方 API |
|---|---|---|
| 可用 | ✅ akshare 封装 | ❌ 无官方 Linux 客户端 |
| 速度 | 0.5-0.9 秒/只 | N/A |
| 字段 | 报告期 / 实施公告日 / 税前分红率 / 方案进度 | 同上 |
| 用途 | 数据验证 / 批量校验 | N/A |
**结论**:akshare ths 接口**字段口径与同花顺 APP 完全一致**——直接用这个。
## 反模式(用户骂过的)
- ❌ 自己写 TTM 算法 → "你一个AI写算法,还能比得过专家吗?"
- ❌ 反复 retry 同花顺接口 / akshare 接口 → "你是不是 API 没调通?"
- ❌ 用 `summary['年均股息'] / 当前价` 当股息率 → 错 27 倍(漏 `/ 10`)
- ❌ 用 `派息 / 当前价` 当股息率 → 错 10 倍(漏 `/ 10`)
- ❌ 5 次算法调试 vs 1 行接口验证 → 用户骂 "我不专业,算出来我也不知道对不对,我也是去看同花顺的"
## 关联 skill
- `~/.hermes/skills/prefer-data-over-computation/SKILL.md` —— 算法 vs 源数据原则
- `~/.hermes/skills/long-toolchain-pacing/references/akshare-a-share-dividend.md` —— akshare 接口细节 + 单位陷阱
- `~/.hermes/skills/trading/dividend-scanner/references/cn-top8-auto-screen.md` —— v4 算法(已废,但记录"为什么不自己算")