Files
Hermes-Skills/dividend-investing/scripts/dca_monitor.py
T
mike 913d1945d4 feat: 迁移 trading 相关脚本到 skill 仓库 (第2批: 🟡 中优先级)
【迁移内容】
- 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)
2026-07-24 11:42:13 +08:00

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