【迁移内容】
- dividend-investing/scripts/dca_monitor.py + dca_monitor_us.sh
- dividend-scanner/scripts/scan_cn.{py,sh} + scan_hk.sh + scan_us.sh
- okx-auto-position/scripts/signal_queue_retry.sh
- lottery-hk/scripts/verify_lottery.sh
- longbridge-cli/scripts/longport_http.py (公共模块)
【配套修改】
- 5 个 cron 任务 script 路径更新 (jobs.json):
- 8929ca09 (DCA港股上午) → dividend-investing/scripts/dca_monitor.py
- 9469c128 (DCA港股下午) → dividend-investing/scripts/dca_monitor.py
- 3a4bef2c (DCA美股凌晨) → dividend-investing/scripts/dca_monitor_us.sh
- a82a3ab0 (signal-queue-retry) → okx-auto-position/scripts/
- b4868849 (lottery-verify-result) → lottery-hk/scripts/
【删除】本地 ~/.hermes/scripts/{dca_monitor,scan_*,signal_queue_retry,verify_lottery,longport_http}
【未迁移】scan_cn/hk/us 仍需挂 cron, 暂不删 (走 scanner skill)
167 lines
5.9 KiB
Python
Executable File
167 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
DCA阶梯买入监控脚本
|
|
检查当前价格 vs 阶梯价位,触发时输出买入信号
|
|
"""
|
|
import os, sys, re, json
|
|
from datetime import datetime
|
|
|
|
# === 市场过滤参数 ===
|
|
market_filter = None
|
|
for arg in sys.argv[1:]:
|
|
if arg.startswith("--market="):
|
|
market_filter = arg.split("=")[1].upper() # HK / US / CN
|
|
|
|
# === Load env ===
|
|
env_vars = {}
|
|
with open(os.path.expanduser('~/.bashrc')) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith('export LONGBRIDGE_') or line.startswith('export LONGPORT_'):
|
|
parts = line.replace('export ', '').split('=', 1)
|
|
if len(parts) == 2:
|
|
env_vars[parts[0]] = parts[1]
|
|
# 2026-07-21 修复: 强制走海外域, 避免国内 socket 连不上
|
|
env_vars.setdefault('LONGPORT_HTTP_URL', 'https://openapi.longbridge.com')
|
|
env_vars.setdefault('LONGBRIDGE_HTTP_URL', 'https://openapi.longbridge.com')
|
|
env_vars.setdefault('LONGBRIDGE_REGION', 'ap')
|
|
env_vars.setdefault('LONGBRIDGE_TRADE_ENABLED', 'true')
|
|
for key, val in env_vars.items():
|
|
if '${' not in val:
|
|
os.environ[key] = val
|
|
for key, val in env_vars.items():
|
|
if '${' in val:
|
|
os.environ[key] = re.sub(r'\$\{(\w+)\}', lambda m: os.environ.get(m.group(1), ''), val)
|
|
|
|
from longport import openapi
|
|
import subprocess
|
|
|
|
cfg = openapi.Config.from_env()
|
|
# 不再使用 ctx.quote() (WSS 不稳定, 2026-07-21 改用 longport CLI HTTP 端点)
|
|
# ctx = openapi.QuoteContext(config=cfg)
|
|
|
|
# === Load positions ===
|
|
config_path = os.path.expanduser('~/.hermes/scripts/dca_positions.json')
|
|
with open(config_path) as f:
|
|
config = json.load(f)
|
|
|
|
positions = config['positions']
|
|
trigger_pct = config['alert_settings']['trigger_pct'] # 2% within ladder price
|
|
|
|
# === 股息率过滤:低于7%的标的跳过 ===
|
|
MIN_YIELD = 7.0
|
|
filtered_out = []
|
|
for sym in list(positions.keys()):
|
|
if positions[sym].get('yield', 0) < MIN_YIELD:
|
|
filtered_out.append(f"{sym}({positions[sym]['name']} {positions[sym]['yield']}%)")
|
|
del positions[sym]
|
|
|
|
# === 市场过滤 ===
|
|
if market_filter:
|
|
before = set(positions.keys())
|
|
positions = {k: v for k, v in positions.items() if v.get('market', '').upper() == market_filter}
|
|
skipped = before - set(positions.keys())
|
|
# if skipped:
|
|
# print(f"⏭️ 跳过非{market_filter}标的: {', '.join(skipped)}")
|
|
|
|
# === Get current prices (2026-07-21 改用 longport CLI 走 HTTP, 避免 WSS 不稳定) ===
|
|
all_symbols = list(positions.keys())
|
|
quotes = {}
|
|
import re
|
|
for sym in all_symbols:
|
|
try:
|
|
result = subprocess.run(
|
|
['proxychains4', '-f', '/home/openclaw/.proxychains/proxychains.conf',
|
|
'/home/openclaw/.local/bin/longbridge', '--profile', 'lb_real',
|
|
'quote', sym],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
m = re.search(r'│\s*' + re.escape(sym) + r'\s*│\s*([\d.]+)\s*│', result.stdout)
|
|
if m:
|
|
quotes[sym] = float(m.group(1))
|
|
except Exception:
|
|
pass
|
|
|
|
# === Check ladder triggers ===
|
|
alerts = []
|
|
summary_lines = []
|
|
|
|
for sym, pos in positions.items():
|
|
current_price = quotes.get(sym)
|
|
if current_price is None:
|
|
continue
|
|
|
|
name = pos['name']
|
|
market = pos['market']
|
|
flag = "🇭🇰" if market == "HK" else "🇺🇸"
|
|
|
|
for ladder in pos['ladder']:
|
|
tier = ladder['tier']
|
|
target = ladder['price']
|
|
alloc = ladder['alloc_pct']
|
|
status = ladder['status']
|
|
|
|
if status == 'done':
|
|
continue
|
|
|
|
# Check if price is within trigger range (at or below target)
|
|
if target > 0 and current_price <= target * (1 + trigger_pct / 100):
|
|
pct_diff = (current_price - target) / target * 100
|
|
action = "🟢 到价可买" if current_price <= target else "🟡 接近目标"
|
|
alerts.append({
|
|
'symbol': sym,
|
|
'name': name,
|
|
'flag': flag,
|
|
'tier': tier,
|
|
'target': target,
|
|
'current': current_price,
|
|
'pct_diff': pct_diff,
|
|
'alloc': alloc,
|
|
'yield': pos['yield'],
|
|
'action': action,
|
|
})
|
|
|
|
# Always add to summary
|
|
nearest = min(pos['ladder'], key=lambda l: abs(l['price'] - current_price) if l['status'] != 'done' and l['price'] > 0 else 9999)
|
|
gap_pct = (current_price - nearest['price']) / nearest['price'] * 100 if nearest['price'] > 0 else 0
|
|
summary_lines.append(f"{flag} {sym} {name}: 现价{current_price} → 最近档{nearest['price']}(T{nearest['tier']}) 差{gap_pct:+.1f}% 股息{pos['yield']}% [{pos.get('div_freq', '未知')}]")
|
|
|
|
# === Output ===
|
|
now = datetime.now().strftime('%Y-%m-%d %H:%M')
|
|
|
|
if alerts:
|
|
# Sort by urgency (closest to target first)
|
|
alerts.sort(key=lambda a: a['pct_diff'])
|
|
|
|
lines = [f"🔔 DCA买入信号 [{now}]", ""]
|
|
|
|
for a in alerts:
|
|
if a['pct_diff'] <= 0:
|
|
emoji = "🚨"
|
|
tag = "已触达"
|
|
else:
|
|
emoji = "🟡"
|
|
tag = f"差{a['pct_diff']:.1f}%"
|
|
|
|
lines.append(f"{emoji} {a['flag']} {a['symbol']} {a['name']}")
|
|
lines.append(f" 第{a['tier']}档目标: {a['target']} 现价: {a['current']} {tag}")
|
|
lines.append(f" 建议仓位: {a['alloc']}% 股息率: {a['yield']}%")
|
|
lines.append("")
|
|
|
|
lines.append("━━━━━━━━━━━━━")
|
|
lines.append("📋 全部监控标的:")
|
|
for s in summary_lines:
|
|
lines.append(f" {s}")
|
|
|
|
print("\n".join(lines))
|
|
else:
|
|
# No alerts - silent (empty output = no notification sent)
|
|
# 2026-07-21: 让 cron always 输出 summary (即使没 alert, 让你看到 status)
|
|
lines = [f"📊 DCA {market_filter} 监控 [{now}] (无买入信号)", ""]
|
|
lines.append("━━━━━━━━━━━━━")
|
|
lines.append("📋 全部监控标的:")
|
|
for s in summary_lines:
|
|
lines.append(f" {s}")
|
|
print("\n".join(lines))
|