212 lines
7.7 KiB
Markdown
212 lines
7.7 KiB
Markdown
# AKShare 股息历史 API + Push 字段扩展模式 (2026-07-29 实战)
|
||
|
||
## 1. AKShare 两套历史股息 API
|
||
|
||
A 股 和 港股 用**不同的接口**(2026-07-29 验证可用):
|
||
|
||
| 市场 | API | 输入 | 列 | 来源 |
|
||
|------|------|------|------|------|
|
||
| **A 股** (6 位) | `ak.stock_history_dividend_detail(code, indicator='分红')` | 6 位代码 | 公告日期/送股/转增/派息/进度/除权除息日/股权登记日/红股上市日 | 新浪财经 |
|
||
| **港股** (5 位) | `ak.stock_hk_dividend_payout_em(code)` | 5 位代码 | 最新公告日期/财政年度/分红方案/分配类型/除净日/截至过户日/发放日 | 东方财富港股 |
|
||
|
||
**注意**: 美股 Nasdaq API (`api.nasdaq.com/api/calendar/dividends?date=YYYY-MM-DD`) **只返回当天** — 没有历史。
|
||
要美股历史需 `stock_us_dividend_em` (akshare) 或 yfinance。
|
||
|
||
### 1.1 美股"连续(估年)"近似列 (2026-07-30 实战)
|
||
|
||
**问题**: 美股没有可靠的**历史派息年数 API**(Nasdaq 当天, yfinance 国内 VPS 不稳定)。
|
||
用户 cron `366934c1474c` 美股推送缺"派息稳定"列。
|
||
|
||
**近似方案**: `连续(估年) = 年化 / 每股派` —— 当年总派息 ÷ 每股单次派息 ≈ 派息频率(可能 4=季度派, 12=月派, 24=双周派)。
|
||
|
||
**坑**: 这是**派息频率**, **不是**真实连续年数!
|
||
- STRC 月派 ×12 = 24 (24 表示一年 24 次派息, 不是 24 年连续)
|
||
- 季度派票显示 4.0 (合理)
|
||
- 半年派票显示 2.0 (合理)
|
||
|
||
**实现** (`dividend_alert.py` fmt 函数):
|
||
```python
|
||
cont_years = ann / d if d > 0 else 0 # ann=年化派息总额, d=每股派
|
||
if cont_years >= 10:
|
||
cont_str = f'🔥{cont_years:.0f}' # 可能假阳性(月派)
|
||
elif cont_years >= 5:
|
||
cont_str = f'⚡{cont_years:.0f}'
|
||
elif cont_years >= 1:
|
||
cont_str = f'{cont_years:.1f}'
|
||
else:
|
||
cont_str = '-'
|
||
```
|
||
|
||
**正确做法**: 推送时**显式标注估算** + 提示月派票会偏高:
|
||
```
|
||
⚠️ 连续(估年) = 年化 / 每股派 (月派票会偏高, 仅作参考; 真连续年数需 yfinance API 不稳定)
|
||
```
|
||
|
||
**未来**: 真要做精确,装 yfinance + `yf.Ticker(code).dividends` 拉历史, 算真实连续年数 (2026-07-30 国内 VPS 装 yfinance 失败, 暂用近似)。
|
||
|
||
### 1.1 helper 实现
|
||
|
||
```python
|
||
def get_dividend_history(code):
|
||
"""返回: (annual_count_avg, continuous_years, is_recent)
|
||
code: 6 位 A 股 OR 5 位 港股
|
||
"""
|
||
is_hk = code.isdigit() and len(code) == 5
|
||
is_a = code.isdigit() and len(code) == 6
|
||
try:
|
||
if is_hk:
|
||
df = ak.stock_hk_dividend_payout_em(code)
|
||
elif is_a:
|
||
df = ak.stock_history_dividend_detail(code)
|
||
else:
|
||
return (0, 0, False)
|
||
# 过滤 + 提取年份 (用除权日 / 除净日)
|
||
done = df.dropna(subset=['除权除息日' if is_a else '除净日']).copy()
|
||
if is_a:
|
||
done = done[(done['进度'] == '实施') & (done['送股'] == 0) & (done['转增'] == 0)]
|
||
if done.empty:
|
||
return (0, 0, False)
|
||
date_col = '除权除息日' if is_a else '除净日'
|
||
done['year'] = done[date_col].astype(str).str[:4].astype(int)
|
||
# 平均年度派息次数 (近 3 年)
|
||
last_3y = done[done['year'] >= 2024]
|
||
annual_count = round(last_3y.groupby('year').size().mean(), 1) if not last_3y.empty else 0
|
||
# 连续派息年数
|
||
years_with_div = sorted(set(done['year'].tolist()))
|
||
continuous = 0
|
||
for y in range(2025, 2010, -1):
|
||
if y in years_with_div:
|
||
continuous += 1
|
||
else:
|
||
break
|
||
return (annual_count, continuous, not done[done['year'] == 2026].empty)
|
||
except Exception:
|
||
return (0, 0, False)
|
||
```
|
||
|
||
**性能**: 5 票 ~1.2s, 12 票 ~3s (cron 推送时间内可接受)。
|
||
|
||
## 2. Push 字段扩展: 次/年 + 连续(年) + vs MA50
|
||
|
||
dividend_alert.py 在 2026-07-29 加了 3 列,显著提升推送价值。
|
||
|
||
### 2.1 最终表结构 (8 列)
|
||
|
||
| 票 | 名称 | 每10股派 | 现价 | 股息率 | 次/年 | 连续(年) | vs MA50 |
|
||
|
||
**字段说明**:
|
||
- **次/年**: 近 3 年平均派息次数。1.0 = 年度派, 1.5 = 半年+年度, 1.7 = 多季度(高息稳定)
|
||
- **连续(年)**: 派息连续年数。🔥10+ = 10 年以上稳定, ⚡5-9 = 5-9 年
|
||
- **vs MA50**: 当前价对 50 日均线的偏离 (用长桥 K 线算)
|
||
|
||
### 2.2 emoji 等级系统
|
||
|
||
| 状态 | 阈值 | emoji |
|
||
|------|------|------|
|
||
| 连续派息 ≥10 年 | 10+ | 🔥 |
|
||
| 连续派息 5-9 年 | 5-9 | ⚡ |
|
||
| 离 MA50 > +5% | 显著高估 | 🟢(但下行风险) |
|
||
| 离 MA50 0 ~ +5% | 略高 | 🟡 |
|
||
| 离 MA50 -10 ~ 0 | 略低 | 🔴(买入机会) |
|
||
| 离 MA50 < -10% | 大幅低于 | ⛔(危险/可能趋势反转) |
|
||
|
||
⚠️ 注意: 🟢/🔴 在此场景**语义反转** — 通常用 🟢 表示"好",但 vs MA50 偏离=大代表下行风险高。需在推送头部加注释说明。
|
||
|
||
## 3. ⚠️ 必看 Bug: `ma_off` double *100
|
||
|
||
**陷阱**: 在 `cn_dividend_buy_timing.py` 中我犯了这个错:
|
||
|
||
```python
|
||
# 错: 显示时再 *100
|
||
'ma_off': (current/ma50-1)*100, # 存储为 0.05 (= 5%)
|
||
# ...
|
||
L.append(f"| 离均线 | {'+' if r['ma_off']>=0 else ''}{ r['ma_off']*100:.1f}% |")
|
||
# 输出: +982.7% (应该是 +9.8%)
|
||
```
|
||
|
||
**正解**: 要么存 fraction (0.05), 要么存 percent (5.0), **但只能 *100 一次**。
|
||
|
||
```python
|
||
# ✅ 选 1: 存 percent, 显示直接用
|
||
'ma_off_pct': (current/ma50-1)*100,
|
||
# 显示: f"{'+' if r['ma_off_pct']>=0 else ''}{r['ma_off_pct']:.1f}%"
|
||
|
||
# ✅ 选 2: 存 fraction, 显示时 *100
|
||
'ma_off': (current/ma50-1), # 0.05
|
||
# 显示: f"{r['ma_off']*100:+.1f}%" # 显式 + 避免符号混乱
|
||
```
|
||
|
||
## 4. 数据累积 + 循环外拼装表 模式
|
||
|
||
**为什么**: 在循环内 `lines.append(f"| ...")` 难维护,容易出现 **状态混乱 bug**(像上面 ma_off)。
|
||
|
||
**模式**:
|
||
|
||
```python
|
||
# 1. 累积 (在循环内)
|
||
row_data = []
|
||
for c in candidates[:8]:
|
||
feats = compute(c)
|
||
row_data.append({
|
||
'code': c['code'],
|
||
'name': c['name'][:6],
|
||
'price': c['price'],
|
||
'ma_off_pct': (c['price']/feats['ma50']-1)*100,
|
||
# ... 一行所有字段
|
||
})
|
||
|
||
# 2. 输出 (在循环外)
|
||
if row_data:
|
||
cols = [r['code'] for r in row_data]
|
||
n = len(cols)
|
||
hdr = "| 项目 | " + " | ".join(cols) + " |"
|
||
sep = "|:---|" + "|".join([":---"] * n) + "|"
|
||
L.append(hdr)
|
||
L.append(sep)
|
||
L.append("| 现价 | " + " | ".join([f"{r['price']:.2f}" for r in row_data]) + " |")
|
||
L.append("| 离均线 | " + " | ".join([f"{r['ma_off_pct']:+.1f}%" for r in row_data]) + " |")
|
||
# ...
|
||
```
|
||
|
||
**好处**:
|
||
- 字段名在 dict 里出现一次
|
||
- 显示公式跟存储值**显式对应**(可单元测试)
|
||
- 多张表 (基本面/点位/性价比) 都是同一模式
|
||
- bug 容易被肉眼发现
|
||
|
||
## 5. 名称列单独一行 (横向表 4-8 列最佳实践)
|
||
|
||
**问题**: 横向表列是票代码 (601288),**但 QQ 用户滑动时想看到名称**(农业银行)。
|
||
|
||
**方案**: 名称作为单独行,放在代码行之上:
|
||
|
||
```python
|
||
L.append(name_hdr) # | 名称 | 农业银行 | 工商银行 | ...
|
||
L.append(name_sep)
|
||
L.append(hdr) # | 项目 | 601288 | 601398 | ...
|
||
L.append(sep)
|
||
L.append("| 现价 | ...")
|
||
```
|
||
|
||
**效果**:
|
||
```
|
||
| 名称 | 农业银行 | 工商银行 | 中国神华 | ...
|
||
|:---|:---|:---|:---|:---|
|
||
| 项目 | 601288 | 601398 | 601088 | ...
|
||
|:---|:---|:---|:---|:---|
|
||
| 现价 | 7.05 | 8.14 | 45.62 | ...
|
||
```
|
||
|
||
QQ mobile 用户**滑动时同时看到名称和代码**,**不需要查文档**。
|
||
|
||
## 6. 不要做
|
||
|
||
- ❌ 把每行字段全写成一行 (无换行, QQ 滚得累)
|
||
- ❌ 列里塞 `f"{x:.6f}"` 这种 6 位小数
|
||
- ❌ 列名用全角字符 (渲染乱)
|
||
- ❌ 一次性输出 20+ 列 (挤, 滑动难受)
|
||
- ❌ 混用 emoji 前缀 (⭐600809 和 600809 ⭐ 风格不一致)
|
||
- ❌ 在表格里用 `|` 字符 (转义难, 破表)
|
||
- ❌ `head -c 1800` 字节截断 UTF-8 文本 (Chinese 字符 3 字节,可能切坏)
|
||
- ❌ **每个数值单元都 *100 两次**(ma_off 教训)
|