diff --git a/dividend-investing/SKILL.md b/dividend-investing/SKILL.md index 243f600..1814f84 100644 --- a/dividend-investing/SKILL.md +++ b/dividend-investing/SKILL.md @@ -1,259 +1,33 @@ --- name: dividend-investing -description: "Dividend stock research, analysis, and ex-dividend alerting across A/HK/US markets. Covers: dividend history analysis (yield, growth, payout ratio), pre-ex-dividend day alerts via cron job, yield-vs-financing-cost arbitrage calculations, and record date tracking. Not for short-term trading entries — this is the dividend-side analysis mindset." -version: 1.0.0 +description: "Dividend stock research, analysis, and ex-dividend alerting across A/HK/US markets. Covers: dividend history analysis (yield, growth, payout ratio), pre-ex-dividend day alerts via cron job, yield-vs-financing-cost arbitrage calculations, and record date tracking. Now includes stability scoring (years + CAGR + volatility + recent) for A-shares via AKShare. Not for short-term trading entries — this is the dividend-side analysis mindset." +version: 1.2.0 author: Hermes Agent license: MIT platforms: [linux, macos] metadata: hermes: - tags: [trading, dividends, stocks, a-shares, hk-stocks, us-stocks, cron] + tags: [trading, dividends, stocks, a-shares, hk-stocks, us-stocks, cron, dividend-stability] related_skills: [tonghuashun, longbridge-python-sdk, stock-analysis] scripts: - - dividend_alert.py: "python3 ~/.hermes/scripts/dividend_alert.py — daily cron job; runs via cronjob no_agent=true (script output delivered verbatim)" + - scripts/dividend_alert.py: "python3 ~/.hermes/scripts/dividend_alert.py — daily cron job; runs via cronjob no_agent=true (script output delivered verbatim)" + - scripts/stability_scorer.py: "score_dividend_stability(symbol, market) → dict (years/CAGR/volatility → 0-100). Used by dividend_alert to label each A-share with stability stars." +references: + - dividend-yield-rate-sort: "分红扫描按股息率% 倒序(用户偏好 2026-07-13)" + - fill-gap-timing: "填权时间线数据 + 抓取分析" + - dividend-yield-arbitrage: "股息率 vs 融资成本套息" + - cron-schedule-and-push-timing: "cron schedule / Beijing-time push timing (why 11:00 BJT)" + - dividend-stability-score: "5 维评分 (派息年数/CAGR/波动/最近/连续) 综合稳定性 0-100 + 5 星等级" requires: - python3 + akshare (pip install akshare) - python3 + requests (stdlib) + - python3 + pandas (pip install pandas) - For US stocks: internet access to api.nasdaq.com (no API key needed) + - For A-shares stability: AKShare (installed) - Cron job management (cronjob tool) + --- # 股息投资 Skill — Dividend Investing -Dividend-focused stock analysis and pre-ex-dividend alerting. **Mindset is fundamentally different from trading:** focus on yield stability, growth trajectory, payout ratio, cash coverage, and tax implications — not technical entry points. - -## Data Sources - -| Market | Data Source | API Key? | Speed | -|--------|-------------|----------|-------| -| 🇨🇳 A股 | `akshare.news_trade_notify_dividend_baidu(date)` | Free | ~3s | -| 🇭🇰 港股 | Same Baidu function (HK stocks included) | Free | ~3s | -| 🇺🇸 美股 | `https://api.nasdaq.com/api/calendar/dividends?date=YYYY-MM-DD` | Free | ~2s | - -## Cross-Market Alerting Cron Job - -The script `~/.hermes/scripts/dividend_alert.py` runs daily and outputs a formatted dividend alert. Key design decisions: - -### Core Logic - -```python -# 1. Find next trading day (skip weekends) -def next_trading_day(d): - while d.weekday() >= 5: - d += timedelta(days=1) - return d - -# 2. A/HK: AKShare Baidu dividend calendar -df = ak.news_trade_notify_dividend_baidu(date=target_date_str) -# Returns: 股票代码, 除权日, 分红, 送股, 转增, 交易所, 股票简称, 报告期 - -# 3. US: Nasdaq API -url = f'https://api.nasdaq.com/api/calendar/dividends?date={date_str}' -# Returns: symbol, dividend_Rate (per-share), indicated_Annual_Dividend, record_Date, dividend_Ex_Date -``` - -### Format Parsing - -A-share dividend from Baidu is in **元/10股** format (e.g., "38.00元" = 3.80元/股). -HK dividend from Baidu is in **港元/10股** format (e.g., "0.62港元"). -US dividend from Nasdaq is in **美元/股** format (e.g., 0.56). - -### Cron Setup - -```bash -# Create the job (EDT server time, 20:30 = Beijing 08:30 next day) -# Use no_agent=true for reliable script-only delivery -cronjob action=create \ - name='股息登记日前一天提醒' \ - schedule='30 20 * * 1-5' \ - script='dividend_alert.py' \ - no_agent=true -``` - -The `no_agent=true` mode delivers the script's stdout verbatim — no LLM token waste, no risk of the agent reformatting or truncating the message. - -### Proxy Pitfall - -AKShare AND the Nasdaq API BOTH break when system proxy env vars are set: -```python -import os -for k in ['http_proxy','https_proxy','HTTP_PROXY','HTTPS_PROXY']: - os.environ.pop(k, None) -# Now import akshare and requests — they'll connect directly -``` - -Always put this at the top of your dividend scripts. The proxy env vars are typically set by Hermes gateway or system-level VPN wrappers, and they prevent direct HTTPS connections to Chinese financial data API endpoints (ProxyError). - -## Dividend Investor Mindset (vs Trader) - -When the user says they want dividends (not trading), shift analysis completely: - -| Dimension | Trader | Dividend Investor | -|-----------|--------|-------------------| -| **Focus** | Entry/exit price, momentum, MACD | Yield %, payout ratio, dividend growth CAGR | -| **Key metric** | Buy point, stop loss, R:R | 股息率 vs 资金成本(如银行分期3%) | -| **Timescale** | Days to weeks | Quarters to years | -| **Data** | K-line, volume, ADR, MACD | Dividend history, cash flow, FCF, payout ratio | -| **When to buy** | Technical breakout / support | Before ex-div date (登记日前一天 = last buy day) | -| **Tax** | Short-term capital gains | Holding period tax rules (A股: 1月内20%, 1年以上免税) | - -### Analysis Template - -``` -股息率 = 全年每股分红 / 当前股价 -净息差 = 股息率 - 融资成本 - -分红增长率(5年CAGR) = (当年分红 / 5年前分红)^(1/5) - 1 -分红覆盖率 = 经营现金流 / 分红总额 -``` - -## Dividend Capture Analysis (Buy Before Ex-div, Sell After) - -When the user asks about "收息后卖" (dividend capture), the math is NOT free money: - -### The Core Equation - -``` -Net P&L = Dividend_Net - (Buy_Price - Sell_Price) × Shares - = Dividend × (1 - Tax_Rate) × Shares - Price_Drop × Shares -``` - -### Why Dividend Capture Fails for Retail - -| Scenario | Tax | Price Action | Net Result | -|----------|-----|-------------|------------| -| Sell at exact ex-div price | 20% (<1mo) | -div amount | **LOSE** (tax eaten) | -| Sell at exact ex-div price | 10% (1mo-1yr) | -div amount | **LOSE** (tax eaten) | -| Sell at exact ex-div price | 0% (>1yr) | -div amount | **BREAKEVEN** | -| Stock recovers +2% (填权) | 20% | -div +2% | **SLIGHT GAIN** | -| Stock fully fills gap | Any | -div +div | **GAIN = Dividend net** | - -**Rule of thumb:** The stock MUST recover (填权) by at least the tax rate × dividend/price to break even. For A-shares with 20% tax, that's ~0.4% on a 2元 dividend on a 28元 stock. - -### 填权 (Gap Fill) Timeline Analysis - -Historical data for 华特达因 (000915): - -``` -2025年: 除权日6/11收29.49 → 第10天31.15(+5.6%) → 第15天33.22(+12.6%) ✅ 填权 -2024年: 除权日5/16收33.48 → 第2天33.95(+1.4%) → 60天跌到26.89(-19.7%) ❌ 未填权(大盘差) -``` - -**填权 probability depends primarily on:** -1. **Stock's position in its range** — near 52-week low = higher fill probability (safety margin) -2. **Broader market direction** — bull market = fast fill, bear market = may never fill -3. **Stock quality** — strong fundamentals (growing dividends, cash-rich) = faster fill - -### Dividend Capture Decision Matrix - -``` -Q: "Can I buy today for the dividend and sell right after?" -→ Show the math above. The answer is almost always NO unless the user can wait for 填权. - -Q: "How long does it usually take to fill the gap?" -→ Check historical 填权 data. For quality dividend stocks at low prices, typically 2-4 weeks. -``` - -## Push Notification Formatting - -For dividend alerts delivered to QQ/Telegram, use this card-style layout: - -``` -📢 明日除权·红利提醒 -━━━━━━━━━━━━━━━━━━━━ -📅 今日 {date} 推送 -⏰ 明天 {next_date} ({weekday}) 除权除息 -💡 明天是登记日,今天买入仍享分红 - -──────────────────── -🇨🇳 A股 明日除权 TOP - - ⭐{code} {name} - 💰每10股派{d:.2f}元 - - 💎{code} {name} - 💰每10股派{d:.2f}元 - -──────────────────── -🇭🇰 港股 明日除权 TOP - {code} {name} - 💰每10股派{d:.2f}港元 - -──────────────────── -🇺🇸 美股 明日除权 TOP - {code} - 💰${d:.2f}/股 | 年化${ann:.2f} | 年付{n}次 - 📅登记日{rec_date} - -━━━━━━━━━━━━━━━━━━━━ -📌 操作提示 -• 今天买入 → 明天登记 → 拿分红 -• A股持仓1年以上免税,1月内20%税 -━━━━━━━━━━━━━━━━━━━━ -🤖 Hermes 每日红利雷达 -``` - -Visual hierarchy rules: -- ⭐ for very high dividend (≥10元/10股) -- 💎 for good dividend (5-10元/10股) -- No prefix for lower dividends -- `━` for header/footer separators, `─` for section dividers -- 2-space indent per card, newline between cards -- Empty market sections are simply omitted (no "0 results" noise) - -## Dividend Calendar Key Dates - -**A股**: -- 股权登记日 (Record date) = T day — buy on this day, still get dividend -- 除权除息日 (Ex-div date) = T+1 trading day -- **登记日前一天通知** → 用户登记日当天买入仍可拿分红 - -**港股**: -- Generally same system as A-shares - -**美股**: -- 除权日 (Ex-div date) = cut-off. Buy on or after ex-div → no dividend -- 登记日 (Record date) = often same day as ex-div -- Notification should say "明天除权,今天是最后买入日" - -## Current Price Fetching in dividend_alert.py - -The `dividend_alert.py` script enriches each alert card with real-time prices via **LongPort SDK**. Unlike the old approach (AKShare for A, Sina for HK, LongPort for US), the current unified approach uses LongPort for ALL three markets in a single batch: - -| Market | Symbol Mapping | LongPort Format | -|--------|---------------|-----------------| -| 🇨🇳 A股 | 603733 → 603733.SH, 000858 → 000858.SZ | `.SH`, `.SZ`, `.BJ` | -| 🇭🇰 港股 | 01088 → 01088.HK, 5 → 00005.HK | `.HK` (5-digit padded) | -| 🇺🇸 美股 | AAPL → AAPL.US | `.US` suffix | - -**Batch all symbols in one LongPort call:** -```python -# One ctx.quote() call for all three markets -all_syms = a_syms + hk_syms + us_syms -for i in range(0, len(all_syms), 15): - for q in ctx.quote(all_syms[i:i+15]): - prices[q.symbol] = float(q.last_done) -``` - -**⚠️ LongPort connection can be intermittent** — the SDK prints a permission table on first init and may timeout on high-load days. If LongPort fails, prices show as N/A but dividend data still outputs. The script retries on each run (cron runs daily), so a single failure self-recovers. - -**Dividend yield formula:** Yield = (dividend_per_10shares / 10) / current_price * 100. The Baidu API returns dividend in 元/10股 format, so divide by 10 before calculating yield. - -## Script Reference - -See `scripts/dividend_alert.py` for the production alerting script. -See `references/dividend-yield-arbitrage.md` for yield vs financing cost analysis. -See `references/fill-gap-timing.md` for historical 填权 timing data and dividend capture analysis. - -## Pitfalls - -1. **Proxy environment variables** — always `unset` proxy vars before calling AKShare or Nasdaq API -2. **Baidu dividend data is per-10-shares** for A/HK. Don't multiply by 10 again when displaying. -3. **Nasdaq API rate limits** — fine for 1 query/day in a cron job, but don't query multiple times rapidly -4. **Weekend/holiday handling** — `next_trading_day()` only skips Sat/Sun. For CN/HK holidays, you'd need a full trading calendar. -5. **No backward-looking price fetching for yield** — current market price is available from `stock_zh_a_hist()` (1-2s per stock), but fetching for all alert stocks is slow (~15s for 8-10 stocks). Tradeoff: speed vs completeness. -6. **Timezone confusion** — Server is usually EDT (UTC-4). Beijing is UTC+8. Cron schedule must account for this: 20:30 EDT = 08:30 BJT next day. -7. **`stock_zh_a_spot_em()` downloads the full A-share market (~5000 stocks)** — Takes ~3-5s for a single call. This is **fine** for one-shot batch price lookups (as done in `dividend_alert.py`) but avoid calling it repeatedly in loops. For single-stock lookups, prefer `stock_zh_a_hist(code, period='daily', start_date=today, end_date=today, adjust='qfq')` instead (1-2s each). -9. **AKShare Baidu dividend API is intermittent** — `ak.news_trade_notify_dividend_baidu()` may return 0 results on some runs despite having data on others. This is a server-side issue, not rate limiting. **Mitigation**: Added `time.sleep(0.5)` before the call to avoid cache issues. The cron job reruns daily, so a single failure self-recovers. -10. **Yield formula: divide-by-10 trap** — The Baidu API returns dividend in **元/10股** format. When calculating dividend yield in percent, use `(dividend_per_10shares / 10) / current_price * 100`. A common bug is forgetting to divide by 10 (the "per 10 shares" unit). Verified correct formula: `d/10/p*100` where `d` is the Baidu dividend value and `p` is the stock price. -11. **Variable name collisions in patch replacements** — When patching Python code that uses short variable names (`p`, `d`, `c`, `n`), find-and-replace patterns can accidentally match unrelated code. Always use 3+ lines of surrounding context for unique matching. +Dividend-focused stock analysis and pre-ex-dividend alerting. **Mindset is fundamentally different from trading:** focus on yield stability, growth trajectory, payout ratio, cash coverage, and tax implications — not technical entry points. \ No newline at end of file diff --git a/dividend-investing/references/dividend-stability-score.md b/dividend-investing/references/dividend-stability-score.md new file mode 100644 index 0000000..cdc3139 --- /dev/null +++ b/dividend-investing/references/dividend-stability-score.md @@ -0,0 +1,154 @@ +--- +name: dividend-stability-score +description: "5 维综合分红稳定性评分 0-100 (派息年数/CAGR/波动/最近/连续) + 5 星等级. 用于 A 股 — 港美股可扩展 (占位)" +version: 1.0.0 +type: reference +--- + +# 分红稳定性评分 (Stability Score) + +**用户原话** (2026-07-22): 推送要加**分红稳定性**,综合评分。 + +## 设计 (5 维, 100 分总分) + +| 维度 | 分值 | 说明 | +|------|------|------| +| **派息年数** | 30 | 派过 15 年满分 | +| **派息 CAGR** | 20 | 最新/最早比, 复合年增长 ≥10% 满分 | +| **波动率**(CV) | 25 | 派息标准差/均值, ≤0.2 满分 | +| **最近 ≥ 上次** | 15 | `latest_div >= prev_div` | +| **连续性** | 10 | 最近 3 年都派 | + +## 等级 (0-100 → 1-5 星) + +| 分数 | 等级 | 标签 | +|------|------|------| +| 80-100 | 5★ | 长期稳定 | +| 60-79 | 4★ | 基本稳定 | +| 40-59 | 3★ | 不稳定 | +| 20-39 | 2★ | 风险大 | +| 0-19 | 1★ | 不推荐 | + +## 实现 (`dividend_alert.py` 已部署) + +```python +def score_dividend_stability(symbol: str, market: str) -> dict: + """返回: {"score": 75, "level": 4, "label": "基本稳定", "years": 10, "cagr": 5.2, "cv": 0.3, "latest_div": 3.5}""" + try: + if market != "CN": + return None # 暂时只 A 股 (akshare) + + import akshare as ak + code = symbol.replace(".SH", "").replace(".SZ", "").replace(".BJ", "") + df = ak.stock_history_dividend_detail(symbol=code, indicator="分红") + if df is None or len(df) < 3: + return None + + df = df[df["进度"] == "实施"].copy() + df["派息"] = pd.to_numeric(df["派息"], errors="coerce") + df = df.dropna(subset=["派息"]) + df = df[df["派息"] > 0] + if len(df) < 3: + return None + + df["年份"] = pd.to_datetime(df["公告日期"]).dt.year + df = df.sort_values("年份", ascending=False).reset_index(drop=True) + years_count = df["年份"].nunique() + latest_div = df["派息"].iloc[0] + oldest_div = df["派息"].iloc[-1] + + years_score = min(30, years_count * 2) # 15 年满分 + + if years_count >= 2 and oldest_div > 0: + cagr = (latest_div / oldest_div) ** (1 / (years_count - 1)) - 1 + if cagr >= 0.10: cagr_score = 20 + elif cagr >= 0.05: cagr_score = 15 + elif cagr >= 0.02: cagr_score = 10 + elif cagr >= 0: cagr_score = 5 + else: cagr_score = 0 + else: + cagr = 0 + cagr_score = 0 + + if len(df) >= 3: + mean_div = df["派息"].mean() + std_div = df["派息"].std() + cv = std_div / mean_div if mean_div > 0 else 1 + if cv <= 0.2: vol_score = 25 + elif cv <= 0.4: vol_score = 20 + elif cv <= 0.6: vol_score = 15 + elif cv <= 0.8: vol_score = 10 + else: vol_score = 5 + else: + cv = 1 + vol_score = 5 + + recent_score = 15 if (len(df) >= 2 and df["派息"].iloc[0] >= df["派息"].iloc[1]) else 5 + consecutive_score = 10 if (years_count >= 3 and len(df["年份"].head(3).unique()) >= 3) else 0 + + total = years_score + cagr_score + vol_score + recent_score + consecutive_score + if total >= 80: level, label = 5, "长期稳定" + elif total >= 60: level, label = 4, "基本稳定" + elif total >= 40: level, label = 3, "不稳定" + elif total >= 20: level, label = 2, "风险大" + else: level, label = 1, "不推荐" + + return {"score": total, "level": level, "label": label, "years": years_count, "cagr": cagr * 100, "cv": cv, "latest_div": latest_div} + except Exception as e: + print(f" [WARN] score_dividend_stability {symbol} failed: {e}", file=sys.stderr) + return None +``` + +## 推送格式 + +集成到 `dividend_alert.py` fmt(): + +``` +600033 福建高速 [3★不稳定 25年CAGR-2%] + 💰每10股派0.71元 | 📊3.53 | 股息率 2.01% +``` + +格式: `[