Files
Hermes-Skills/dividend-investing/references/akshare-dividend-history-and-push-enrichment.md
T
mike 98efe09387 lottery-hk: v1.2.6 — 特码脚本改中文文件名 lottery_特码.py
User 2026-07-30 反馈:
- 脚本名 lottery_te_ma.py → lottery_特码.py (中文, 更直观)
- 同步 SKILL.md 引用 (description + scripts 段)
- version 1.2.5 → 1.2.6
2026-07-30 20:43:39 +08:00

6.4 KiB

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 helper 实现

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 中我犯了这个错:

# 错: 显示时再 *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 一次

# ✅ 选 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)。

模式:

# 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 用户滑动时想看到名称(农业银行)。

方案: 名称作为单独行,放在代码行之上:

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 教训)