feat: 迁移 trading scripts 第3批 (scan_*/dca_scanner + 清理)
【迁移】
- dividend-scanner/scripts/scan_cn.sh 改 cd 路径 (原来硬编码 /home/openclaw/scripts)
- dividend-scanner/scripts/scan_hk.sh / scan_us.sh 同改
- dividend-scanner/scripts/dca_scanner.py (scan_hk/us 调它)
- 删除本地 ~/.hermes/scripts/{scan_cn,scan_hk,scan_us,scan_cn.py,dca_scanner}.*
【cron 路径】
- 84c46e13 (港股高息扫描) → dividend-scanner/scripts/scan_hk.sh
- 89a3e7a6 (美股高息扫描) → dividend-scanner/scripts/scan_us.sh
- 5d62894c (A股高息扫描) → dividend-scanner/scripts/scan_cn.sh
【清理】git rm --cached lottery-hk/scripts/__pycache__/lottery.cpython-311.pyc (不该 commit 编译产物)
+ .gitignore 加 __pycache__/
【保留本地 ~/.hermes/scripts/】(非 trading)
- analyze_cmb.sh / memory_check.sh / sync_qdrant_memory.sh / cleanup_disk.sh (Hermes 平台工具)
- hk_set_sl_now.sh / okx_trade.sh / merge_hype_shorts.sh / rgti_*.py (一次性工具)
- longbridge_hosts_*.sh / update_longbridge_token.sh (长桥 host 维护)
- ths_query.py / intraday_cost_check.py / lottery_analyze.sh / lb_test.py
- check-gateway-health.sh / clear-telegram-session.sh / mcp_auth_helper.py / mihomo_watchdog.sh / push_to_qq.sh / verify_lb_token.py
- stocks/, crypto/ (子目录, 工具集合)
- dca_positions.json / rgti_alert_state.json / rgti_t_state.json (数据文件)
【未迁移】crypto/ stocks/ 子目录 (待清理, 不紧急)
【测试】scan_cn.sh 从新位置跑通
This commit is contained in:
Executable
+242
@@ -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]")
|
||||
Reference in New Issue
Block a user