diff --git a/.gitignore b/.gitignore index b6cefab..47fcff5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Thumbs.db *.tmp *.bak *~ +__pycache__/ diff --git a/dividend-scanner/scripts/dca_scanner.py b/dividend-scanner/scripts/dca_scanner.py new file mode 100755 index 0000000..69e73b5 --- /dev/null +++ b/dividend-scanner/scripts/dca_scanner.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +""" +高股息股票扫描器 +- 港股/美股/ A股:全部走LongPort API +- A股股息率:使用预设数据(定期更新) +""" +import os, sys, json +from datetime import datetime + +# === 读取LongPort环境变量 === +bashrc = open(os.path.expanduser("~/.bashrc")).read() +for line in bashrc.splitlines(): + if line.startswith("export LONGPORT_") or line.startswith("export LONGBRIDGE_"): + parts = line.replace("export ", "").split("=", 1) + if len(parts) == 2: + os.environ[parts[0]] = parts[1].strip('"').strip("'") + +from longport import openapi +from longport.openapi import CalcIndex + +cfg = openapi.Config.from_env() +ctx = openapi.QuoteContext(config=cfg) + +# === A股候选池(股息率+派息频率,定期更新) === +A_SHARE_POOL = { + "601088.SH": {"name": "中国神华", "yield": 6.7, "freq": "年+中期", "sector": "煤炭"}, + "601328.SH": {"name": "交通银行", "yield": 6.2, "freq": "年度", "sector": "银行"}, + "601398.SH": {"name": "工商银行", "yield": 5.9, "freq": "年度", "sector": "银行"}, + "601288.SH": {"name": "农业银行", "yield": 5.8, "freq": "年度", "sector": "银行"}, + "601939.SH": {"name": "建设银行", "yield": 6.0, "freq": "年度", "sector": "银行"}, + "601988.SH": {"name": "中国银行", "yield": 5.7, "freq": "年度", "sector": "银行"}, + "600900.SH": {"name": "长江电力", "yield": 3.8, "freq": "年度", "sector": "电力"}, + "601857.SH": {"name": "中国石油", "yield": 5.5, "freq": "年度", "sector": "能源"}, + "600028.SH": {"name": "中国石化", "yield": 5.2, "freq": "年度", "sector": "能源"}, + "601728.SH": {"name": "中国电信", "yield": 4.8, "freq": "年度", "sector": "电信"}, + "600036.SH": {"name": "招商银行", "yield": 4.5, "freq": "年度", "sector": "银行"}, + "601166.SH": {"name": "兴业银行", "yield": 5.8, "freq": "年度", "sector": "银行"}, + "601818.SH": {"name": "光大银行", "yield": 5.9, "freq": "年度", "sector": "银行"}, + "600377.SH": {"name": "宁沪高速", "yield": 6.2, "freq": "年度", "sector": "高速"}, + "601666.SH": {"name": "平煤股份", "yield": 6.2, "freq": "年度", "sector": "煤炭"}, + "600023.SH": {"name": "浙能电力", "yield": 5.5, "freq": "年度", "sector": "电力"}, + "000858.SZ": {"name": "五粮液", "yield": 10.5, "freq": "年度", "sector": "白酒"}, + "000568.SZ": {"name": "泸州老窖", "yield": 7.0, "freq": "年度", "sector": "白酒"}, + "000937.SZ": {"name": "冀中能源", "yield": 11.0, "freq": "年度", "sector": "煤炭"}, + "002304.SZ": {"name": "洋河股份", "yield": 10.8, "freq": "年度", "sector": "白酒"}, + "000596.SZ": {"name": "古井贡酒", "yield": 6.9, "freq": "年度", "sector": "白酒"}, + "000001.SZ": {"name": "平安银行", "yield": 5.4, "freq": "年度", "sector": "银行"}, + "600519.SH": {"name": "贵州茅台", "yield": 5.0, "freq": "年+中期", "sector": "白酒"}, +} + +# === 港股/美股候选池 === +STOCK_POOLS = { + "hk": { + "flag": "🇭🇰", "label": "港股", "min_yield": 7.0, + "tickers": [ + "00939.HK", "01398.HK", "00857.HK", "00883.HK", "01088.HK", + "00941.HK", "06823.HK", "00823.HK", "02191.HK", "00778.HK", + "00405.HK", "02778.HK", "01898.HK", "03988.HK", "01288.HK", + "00006.HK", "00002.HK", "00003.HK", "00012.HK", "00016.HK", + "00388.HK", "02318.HK", "00027.HK", "01299.HK", + ], + "freq": { + "00939.HK": "半年", "01398.HK": "半年", "00857.HK": "半年", + "00883.HK": "半年", "01088.HK": "半年", "00941.HK": "半年", + "06823.HK": "半年", "00823.HK": "半年", "02191.HK": "季度", + "00778.HK": "半年", "00405.HK": "半年", "02778.HK": "半年", + "01898.HK": "半年", "03988.HK": "半年", "01288.HK": "半年", + "00006.HK": "半年", "00002.HK": "半年", "00003.HK": "半年", + "00012.HK": "半年", "00016.HK": "半年", "00388.HK": "半年", + "02318.HK": "半年", "00027.HK": "半年", "01299.HK": "半年", + }, + }, + "us": { + "flag": "🇺🇸", "label": "美股", "min_yield": 7.0, + "tickers": [ + "NLY.US", "HTGC.US", "ARCC.US", "AGNC.US", "PSEC.US", + "TWO.US", "STWD.US", "OFS.US", "MAIN.US", "ABR.US", + "CIM.US", "NYMT.US", "SBR.US", "PDI.US", "PTY.US", + ], + "freq": { + "NLY.US": "季度", "HTGC.US": "季度", "ARCC.US": "季度", + "AGNC.US": "月度", "PSEC.US": "月度", "TWO.US": "季度", + "STWD.US": "季度", "OFS.US": "季度", "MAIN.US": "月度", + "ABR.US": "季度", "CIM.US": "季度", "NYMT.US": "季度", + "SBR.US": "月度", "PDI.US": "月度", "PTY.US": "季度", + }, + }, +} + + +def scan_with_longport(market_key): + """用LongPort扫描港股/美股,获取实时价格+股息率""" + pool = STOCK_POOLS[market_key] + tickers = pool["tickers"] + min_yield = pool["min_yield"] + flag = pool["flag"] + freq_map = pool["freq"] + results = [] + all_quotes = {} + + for i in range(0, len(tickers), 15): + batch = tickers[i:i+15] + try: + quotes = ctx.quote(batch) + for q in quotes: + all_quotes[q.symbol] = { + "price": float(q.last_done), + "prev_close": float(q.prev_close), + "name": q.name if hasattr(q, 'name') else q.symbol, + } + except: + pass + try: + indexes = ctx.calc_indexes(batch, [CalcIndex.DividendYield]) + for idx in indexes: + if idx.symbol in all_quotes: + dy = getattr(idx, 'dividend_yield', None) + if dy is not None: + all_quotes[idx.symbol]["yield"] = float(dy) * 100 + except: + pass + + for sym, info in all_quotes.items(): + dy = info.get("yield", 0) + if dy >= min_yield: + change_pct = (info["price"] - info["prev_close"]) / info["prev_close"] * 100 + results.append({ + "symbol": sym, "name": info.get("name", sym), + "price": info["price"], "change_pct": change_pct, + "yield": dy, "freq": freq_map.get(sym, "未知"), + "flag": flag, + }) + + results.sort(key=lambda x: x["yield"], reverse=True) + return results[:5] + + +def scan_a_share(): + """用LongPort获取A股实时价格,配合预设股息率数据""" + flag = "🇨🇳" + min_yield = 5.0 + tickers = list(A_SHARE_POOL.keys()) + all_quotes = {} + + # LongPort获取A股实时价格 + for i in range(0, len(tickers), 15): + batch = tickers[i:i+15] + try: + quotes = ctx.quote(batch) + for q in quotes: + all_quotes[q.symbol] = { + "price": float(q.last_done), + "prev_close": float(q.prev_close), + "name": q.name if hasattr(q, 'name') else q.symbol, + } + except: + pass + + results = [] + for sym, info in A_SHARE_POOL.items(): + if info["yield"] < min_yield: + continue + q = all_quotes.get(sym) + if not q or q["price"] <= 0: + continue + + change_pct = (q["price"] - q["prev_close"]) / q["prev_close"] * 100 + results.append({ + "symbol": sym, "name": q.get("name", info["name"]), + "price": q["price"], "change_pct": change_pct, + "yield": info["yield"], "freq": info["freq"], + "flag": flag, "sector": info.get("sector", ""), + }) + + results.sort(key=lambda x: x["yield"], reverse=True) + return results[:10] + + +def calc_ladder(price): + return [ + {"tier": 1, "pct": -3, "price": round(price * 0.97, 2)}, + {"tier": 2, "pct": -6, "price": round(price * 0.94, 2)}, + {"tier": 3, "pct": -10, "price": round(price * 0.90, 2)}, + ] + + +def format_result(label, flag, results, total_scanned): + now = datetime.now().strftime("%Y-%m-%d %H:%M") + if not results: + return "" # 无候选时不输出,避免空推送 + if "error" in results[0]: + return f"{flag} {label}高息扫描 | {now}\n\n❌ {results[0]['error']}" + + lines = [f"{flag} {label}高息TOP | {now}", ""] + for i, r in enumerate(results, 1): + medal = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"][min(i-1, 9)] + chg = "📈" if r["change_pct"] >= 0 else "📉" + sector = f" [{r['sector']}]" if r.get("sector") else "" + lines.append(f"{medal} {r['symbol']}{sector}") + lines.append(f" {r['name']}") + lines.append(f" 💰 现价: {r['price']:.2f} {chg} {r['change_pct']:+.1f}%") + lines.append(f" 📊 股息率: {r['yield']:.1f}% 派息: {r['freq']}") + ladder = calc_ladder(r["price"]) + lstr = " → ".join([f"T{l['tier']}:{l['price']:.2f}({l['pct']}%)" for l in ladder]) + lines.append(f" 🪜 阶梯: {lstr}") + lines.append("") + + lines.append("━━━━━━━━━━━━━") + lines.append(f"📋 共扫描 {total_scanned} 只,筛出 {len(results)} 只") + return "\n".join(lines) + + +if __name__ == "__main__": + market = sys.argv[1] if len(sys.argv) > 1 else "all" + + if market == "cn": + results = scan_a_share() + output = format_result("A股", "🇨🇳", results, len(A_SHARE_POOL)) + if output.strip(): + print(output) + elif market == "hk": + results = scan_with_longport("hk") + output = format_result("港股", "🇭🇰", results, len(STOCK_POOLS["hk"]["tickers"])) + if output.strip(): + print(output) + elif market == "us": + results = scan_with_longport("us") + output = format_result("美股", "🇺🇸", results, len(STOCK_POOLS["us"]["tickers"])) + if output.strip(): + print(output) + elif market == "all": + outputs = [] + outputs.append(format_result("港股", "🇭🇰", scan_with_longport("hk"), len(STOCK_POOLS["hk"]["tickers"]))) + outputs.append(format_result("美股", "🇺🇸", scan_with_longport("us"), len(STOCK_POOLS["us"]["tickers"]))) + outputs.append(format_result("A股", "🇨🇳", scan_a_share(), len(A_SHARE_POOL))) + # 过滤空结果 + outputs = [o for o in outputs if o.strip()] + if outputs: + print("\n\n".join(outputs)) + # 无候选时不输出任何内容 + else: + print(f"用法: python3 {sys.argv[0]} [hk|us|cn|all]") diff --git a/dividend-scanner/scripts/scan_cn.sh b/dividend-scanner/scripts/scan_cn.sh index 1701ec1..33dcac3 100755 --- a/dividend-scanner/scripts/scan_cn.sh +++ b/dividend-scanner/scripts/scan_cn.sh @@ -1,3 +1,4 @@ #!/bin/bash -cd /home/openclaw/.hermes/scripts +# 改成从当前目录跑 (skill 仓库下, 用相对路径) +cd "$(dirname "$0")" python3 scan_cn.py diff --git a/dividend-scanner/scripts/scan_hk.sh b/dividend-scanner/scripts/scan_hk.sh index b9ecef8..ffbf629 100755 --- a/dividend-scanner/scripts/scan_hk.sh +++ b/dividend-scanner/scripts/scan_hk.sh @@ -1,3 +1,3 @@ #!/bin/bash -cd /home/openclaw/.hermes/scripts +cd "$(dirname "$0")" python3 dca_scanner.py hk diff --git a/dividend-scanner/scripts/scan_us.sh b/dividend-scanner/scripts/scan_us.sh index 5e385ec..0ce2ca8 100755 --- a/dividend-scanner/scripts/scan_us.sh +++ b/dividend-scanner/scripts/scan_us.sh @@ -1,3 +1,3 @@ #!/bin/bash -cd /home/openclaw/.hermes/scripts +cd "$(dirname "$0")" python3 dca_scanner.py us diff --git a/lottery-hk/scripts/__pycache__/lottery.cpython-311.pyc b/lottery-hk/scripts/__pycache__/lottery.cpython-311.pyc deleted file mode 100644 index 9f593fc..0000000 Binary files a/lottery-hk/scripts/__pycache__/lottery.cpython-311.pyc and /dev/null differ