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)
This commit is contained in:
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user