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))
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/openclaw/.hermes/scripts
|
||||||
|
python3 dca_monitor.py --market=us
|
||||||
Executable
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
A股高股息扫描器(LongPort取价 + 预设股息率)
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
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": "白酒"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scan_a_share():
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for sym, info in A_SHARE_POOL.items():
|
||||||
|
if info["yield"] < min_yield:
|
||||||
|
continue
|
||||||
|
q = all_quotes.get(sym)
|
||||||
|
if q and q["price"] > 0:
|
||||||
|
change_pct = (q["price"] - q["prev_close"]) / q["prev_close"] * 100 if q["prev_close"] else 0
|
||||||
|
price = q["price"]
|
||||||
|
else:
|
||||||
|
# LongPort没拿到价格,用预设
|
||||||
|
price = 0
|
||||||
|
change_pct = 0
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"symbol": sym, "name": info["name"],
|
||||||
|
"price": price, "change_pct": change_pct,
|
||||||
|
"yield": info["yield"], "freq": info["freq"],
|
||||||
|
"flag": "🇨🇳", "sector": info["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(results):
|
||||||
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
if not results:
|
||||||
|
return f"🇨🇳 A股高息扫描 | {now}\n\n暂无符合条件的标的(≥5%)"
|
||||||
|
|
||||||
|
lines = [f"🇨🇳 A股高息TOP | {now}", ""]
|
||||||
|
for i, r in enumerate(results, 1):
|
||||||
|
medal = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"][min(i-1, 9)]
|
||||||
|
lines.append(f"{medal} {r['name']} [{r['sector']}]")
|
||||||
|
lines.append(f" {r['symbol']}")
|
||||||
|
if r["price"] > 0:
|
||||||
|
chg = "📈" if r["change_pct"] >= 0 else "📉"
|
||||||
|
lines.append(f" 💰 现价: {r['price']:.2f} {chg} {r['change_pct']:+.1f}%")
|
||||||
|
else:
|
||||||
|
lines.append(f" 💰 价格: 盘后/未获取")
|
||||||
|
lines.append(f" 📊 股息率: {r['yield']:.1f}% 派息: {r['freq']}")
|
||||||
|
if r["price"] > 0:
|
||||||
|
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"📋 共扫描 {len(A_SHARE_POOL)} 只,筛出 {len(results)} 只")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
results = scan_a_share()
|
||||||
|
print(format_result(results))
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/openclaw/.hermes/scripts
|
||||||
|
python3 scan_cn.py
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/openclaw/.hermes/scripts
|
||||||
|
python3 dca_scanner.py hk
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/openclaw/.hermes/scripts
|
||||||
|
python3 dca_scanner.py us
|
||||||
Executable
+219
@@ -0,0 +1,219 @@
|
|||||||
|
"""
|
||||||
|
longport_http.py - 长桥 HTTP 公共模块 (替代 longport SDK WSS)
|
||||||
|
|
||||||
|
用户原话 2026-07-21: WSS 不稳定, 改用 HTTP 走 longport CLI 走 mihomo.
|
||||||
|
所有长桥脚本都应统一改用这个 module (避免每个脚本自己写 subprocess + 正则).
|
||||||
|
|
||||||
|
用法:
|
||||||
|
from longport_http import get_quote, get_quotes, submit_order, get_positions
|
||||||
|
|
||||||
|
设计:
|
||||||
|
- 所有函数返回 None / [] / {} 表示失败(不抛异常, 调用方自己检查)
|
||||||
|
- subprocess 走 proxychains4 走 mihomo (国内 VPS 走海外 WSS 必须)
|
||||||
|
- 单次调用超时 10 秒 (防止 cron 卡住)
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from typing import List, Dict, Optional, Union
|
||||||
|
|
||||||
|
# 路径
|
||||||
|
LONGBRIDGE_BIN = "/home/openclaw/.local/bin/longbridge"
|
||||||
|
PROXYCHAINS = "proxychains4"
|
||||||
|
PROXYCHAINS_CONF = "/home/openclaw/.proxychains/proxychains.conf"
|
||||||
|
PROFILE = "lb_real"
|
||||||
|
TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
|
def _run(*args) -> str:
|
||||||
|
"""底层调用: proxychains4 + longbridge CLI. 返回 stdout (失败返回空)."""
|
||||||
|
cmd = [PROXYCHAINS, "-f", PROXYCHAINS_CONF, LONGBRIDGE_BIN, "--profile", PROFILE, *args]
|
||||||
|
try:
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT)
|
||||||
|
# proxychains 诊断行混在 stdout (或 stderr) 里,统一过滤
|
||||||
|
combined = r.stdout + r.stderr
|
||||||
|
lines = [l for l in combined.splitlines() if not l.startswith("[proxychains]")]
|
||||||
|
if r.returncode != 0 and not lines:
|
||||||
|
return ""
|
||||||
|
return "\n".join(lines)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return ""
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_quote(symbol: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
拿 1 只票的实时报价 (替代 openapi.QuoteContext().quote([symbol]))
|
||||||
|
|
||||||
|
策略:
|
||||||
|
1. 先试 quote (实时, 但表格可能截断 A 股)
|
||||||
|
2. 失败则用 candlesticks day --count 1 (取收盘价)
|
||||||
|
|
||||||
|
返回: {"symbol": "NVDA.US", "price": 123.45, "change_pct": 1.2} 或 None
|
||||||
|
"""
|
||||||
|
out = _run("quote", symbol)
|
||||||
|
if out:
|
||||||
|
# 表格: │ NVDA.US │ 856.61 │ +1.20% │ ...
|
||||||
|
# 表格列宽限制会截断长 symbol: │ 600519… │ 1308.0… │
|
||||||
|
symbol_trunc = symbol[:7] + "…"
|
||||||
|
for cand in [symbol, symbol_trunc]:
|
||||||
|
m = re.search(
|
||||||
|
r"│\s*" + re.escape(cand) + r"\s*│\s*([\d.]+)\s*│\s*([+\-\d.%]+)\s*│",
|
||||||
|
out
|
||||||
|
)
|
||||||
|
if m:
|
||||||
|
price_str = m.group(1)
|
||||||
|
# 如果是 1308.0… 这种截断, candlesticks 取完整价
|
||||||
|
if "…" in price_str or len(price_str) < 4:
|
||||||
|
break
|
||||||
|
price = float(price_str)
|
||||||
|
change_pct = float(m.group(2).rstrip("%"))
|
||||||
|
return {"symbol": symbol, "price": price, "change_pct": change_pct}
|
||||||
|
|
||||||
|
# fallback: candlesticks 拿日线收盘价
|
||||||
|
cs_out = _run("candlesticks", symbol, "day", "--count", "1")
|
||||||
|
if cs_out:
|
||||||
|
# 表格: │ 2026-07-21 00:00 │ 1338.980 │ 1344.700 │ 1296.870 │ 1308.000 │ 77,148 │
|
||||||
|
m = re.search(r"│\s*[\d\-]+\s*[\d:\s]*│\s*([\d.]+)\s*│\s*([\d.]+)\s*│\s*([\d.]+)\s*│\s*([\d.]+)\s*│", cs_out)
|
||||||
|
if m:
|
||||||
|
close = float(m.group(4))
|
||||||
|
return {"symbol": symbol, "price": close, "change_pct": 0, "source": "candlestick_close"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_quotes(symbols: List[str]) -> Dict[str, Dict]:
|
||||||
|
"""
|
||||||
|
批量拿报价 (替代 openapi.QuoteContext().quote(batch))
|
||||||
|
|
||||||
|
返回: {"NVDA.US": {"price": 123, "change_pct": 1.2}, ...}
|
||||||
|
失败的 symbol 不会出现在结果里
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for sym in symbols:
|
||||||
|
q = get_quote(sym)
|
||||||
|
if q:
|
||||||
|
result[sym] = q
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_positions() -> List[Dict]:
|
||||||
|
"""
|
||||||
|
查持仓 (替代 openapi.TradeContext().position_list)
|
||||||
|
|
||||||
|
返回: [{"symbol": "NVDA.US", "quantity": 10, "cost_price": 100, ...}, ...]
|
||||||
|
"""
|
||||||
|
out = _run("positions")
|
||||||
|
if not out:
|
||||||
|
return []
|
||||||
|
# 解析长桥表格 (只解析包含股票代码的行)
|
||||||
|
results = []
|
||||||
|
# 表格行格式: │ NVDA.US │ 10 │ 100.00 │ 856.00 │ ... │
|
||||||
|
pattern = re.compile(
|
||||||
|
r"│\s*([A-Z\d]{1,6}\.(US|HK|SH|SZ))\s*│\s*(\d+)\s*│\s*([\d.]+)\s*│"
|
||||||
|
)
|
||||||
|
for m in pattern.finditer(out):
|
||||||
|
results.append({
|
||||||
|
"symbol": m.group(1),
|
||||||
|
"market": m.group(2),
|
||||||
|
"quantity": int(m.group(3)),
|
||||||
|
"cost_price": float(m.group(4))
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def submit_order(
|
||||||
|
symbol: str,
|
||||||
|
side: str, # "buy" / "sell"
|
||||||
|
quantity: float,
|
||||||
|
order_type: str = "MO", # "MO" = 市价, "LO" = 限价
|
||||||
|
price: Optional[float] = None, # LO 必填
|
||||||
|
time_in_force: str = "Day"
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
下单 (替代 openapi.TradeContext().submit_order)
|
||||||
|
|
||||||
|
返回: {"order_id": "1234567890", "side": "buy", "quantity": 0.31, "price": 862.35}
|
||||||
|
或 None (失败)
|
||||||
|
"""
|
||||||
|
args = ["submit", symbol, side, "--qty", str(quantity),
|
||||||
|
"--order-type", order_type,
|
||||||
|
"--tif", time_in_force, "-y"]
|
||||||
|
if order_type == "LO" and price is not None:
|
||||||
|
args.extend(["--price", str(price)])
|
||||||
|
|
||||||
|
out = _run(*args)
|
||||||
|
if not out:
|
||||||
|
return None
|
||||||
|
# 长桥返回: 订单号 1234567890
|
||||||
|
m = re.search(r"订单号[::\s]*(\d+)", out)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"order_id": m.group(1),
|
||||||
|
"symbol": symbol,
|
||||||
|
"side": side,
|
||||||
|
"quantity": quantity,
|
||||||
|
"price": price
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_candlesticks(symbol: str, period: str = "day", count: int = 30) -> Optional[List[Dict]]:
|
||||||
|
"""
|
||||||
|
拿 K 线数据 (替代 openapi.QuoteContext().candlesticks)
|
||||||
|
|
||||||
|
period: 'day' | '5m' | '15m' | '1h' | '1m' 等
|
||||||
|
返回: [{"timestamp": "2026-07-21", "open": 100, "high": 105, "low": 99, "close": 103, "volume": 12345}, ...]
|
||||||
|
或 None (失败)
|
||||||
|
"""
|
||||||
|
out = _run("candlesticks", symbol, period, "--count", str(count))
|
||||||
|
if not out:
|
||||||
|
return None
|
||||||
|
# 表格格式: │ 时间 │ 开盘 │ 最高 │ 最低 │ 收盘 │ 成交量 │
|
||||||
|
# A 股时间: '2026-07-21 09:30' / '2026-07-21 00:00' (日线)
|
||||||
|
# 数字带千分位: '1,234,567'
|
||||||
|
results = []
|
||||||
|
pattern = re.compile(
|
||||||
|
r"│\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s*│"
|
||||||
|
r"\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│\s*([\d,.]+)\s*│"
|
||||||
|
)
|
||||||
|
def parse_num(s):
|
||||||
|
return float(s.replace(",", ""))
|
||||||
|
|
||||||
|
for m in pattern.finditer(out):
|
||||||
|
ts_str = m.group(1).replace(" ", "T")
|
||||||
|
# 日线时间格式: '2026-07-21T00:00'
|
||||||
|
if ts_str.endswith("T00:00") and "T" + m.group(1).split()[1] == ts_str:
|
||||||
|
ts_str = m.group(1).replace(" ", "T")
|
||||||
|
results.append({
|
||||||
|
"timestamp": ts_str,
|
||||||
|
"open": parse_num(m.group(2)),
|
||||||
|
"high": parse_num(m.group(3)),
|
||||||
|
"low": parse_num(m.group(4)),
|
||||||
|
"close": parse_num(m.group(5)),
|
||||||
|
"volume": parse_num(m.group(6)),
|
||||||
|
})
|
||||||
|
return results if results else None
|
||||||
|
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== 测试 longport_http 模块 ===\n")
|
||||||
|
|
||||||
|
# 1. 单只报价
|
||||||
|
print("1. get_quote('NVDA.US'):")
|
||||||
|
q = get_quote("NVDA.US")
|
||||||
|
print(f" {q}\n")
|
||||||
|
|
||||||
|
# 2. 批量报价
|
||||||
|
print("2. get_quotes(['NLY.US', 'HTGC.US', 'ARCC.US']):")
|
||||||
|
qs = get_quotes(["NLY.US", "HTGC.US", "ARCC.US"])
|
||||||
|
for sym, data in qs.items():
|
||||||
|
print(f" {sym}: ${data['price']} ({data['change_pct']:+.2f}%)\n")
|
||||||
|
|
||||||
|
# 3. 持仓
|
||||||
|
print("3. get_positions():")
|
||||||
|
pos = get_positions()
|
||||||
|
for p in pos:
|
||||||
|
print(f" {p['symbol']}: {p['quantity']}股 @ ${p['cost_price']}\n")
|
||||||
|
print(f" (共 {len(pos)} 个持仓)\n")
|
||||||
Executable
+96
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 核验当前期预测 vs 实际开奖,记录命中率
|
||||||
|
# 22:30 北京时间(开奖21:30后1小时)跑
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DB="$HOME/.hermes/trading/lottery.db"
|
||||||
|
OUT_DIR="$HOME/.hermes/cron/output/lottery-verify"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
# 当前期号 - 优先代理,失败直连(VPS 出口有时被墙)
|
||||||
|
QI=$(curl -s --proxy http://127.0.0.1:7890 --max-time 8 "https://btc.tktk.app/data/v_xg.json" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('Qi',''))" 2>/dev/null)
|
||||||
|
if [ -z "$QI" ]; then
|
||||||
|
QI=$(curl -s --noproxy '*' --max-time 8 "https://btc.tktk.app/data/v_xg.json" 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('Qi',''))" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
if [ -z "$QI" ]; then
|
||||||
|
echo "❌ 无法获取当前期号(代理+直连都失败)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 实际开奖结果
|
||||||
|
ACTUAL=$(sqlite3 "$DB" "SELECT n1||','||n2||','||n3||','||n4||','||n5||','||n6||','||special FROM draws WHERE period='$QI';")
|
||||||
|
if [ -z "$ACTUAL" ]; then
|
||||||
|
echo "❌ 期号 $QI 还没入库开奖结果"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r A1 A2 A3 A4 A5 A6 AS <<< "$ACTUAL"
|
||||||
|
ACTUAL_FLAT="$A1 $A2 $A3 $A4 $A5 $A6"
|
||||||
|
ACTUAL_SPECIAL="$AS"
|
||||||
|
|
||||||
|
# 找对应的分析文件 - 看 Qi 是几(7-19/21的cron跑的就是 Qi-1 期预测)
|
||||||
|
QI_NUM=$((10#$QI))
|
||||||
|
PREV_PERIOD=$((QI_NUM - 1))
|
||||||
|
# 标题格式有几种:'077期热数据采集' / '热数据采集 · 077期' / '077期数据汇总'
|
||||||
|
# 简单找包含期号+期 的最近文件
|
||||||
|
ANALYSIS_FILE=$(grep -lE "${PREV_PERIOD}期数据汇总|${PREV_PERIOD}期热数据采集|热数据采集.*${PREV_PERIOD}期" "$HOME/.hermes/cron/output/5bec1f60f77f/"*.md 2>/dev/null | tail -1)
|
||||||
|
[ -z "$ANALYSIS_FILE" ] && ANALYSIS_FILE=$(grep -l "${PREV_PERIOD}期" "$HOME/.hermes/cron/output/5bec1f60f77f/"*.md 2>/dev/null | tail -1)
|
||||||
|
|
||||||
|
OUT_FILE="$OUT_DIR/$(date +%Y-%m-%d_%H-%M-%S)_${QI}.md"
|
||||||
|
{
|
||||||
|
echo "## 🎯 核验报告 | $QI期"
|
||||||
|
echo ""
|
||||||
|
echo "**开奖时间**: $(date +%Y-%m-%d) 21:30 北京"
|
||||||
|
echo "**核验时间**: $(date +%Y-%m-%d) 22:30 北京"
|
||||||
|
echo ""
|
||||||
|
echo "### 实际开奖"
|
||||||
|
echo "| 位置 | 号码 |"
|
||||||
|
echo "|------|------|"
|
||||||
|
echo "| 平码 | $A1 · $A2 · $A3 · $A4 · $A5 · $A6 |"
|
||||||
|
echo "| 特码 | **$AS** |"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -n "$ANALYSIS_FILE" ]; then
|
||||||
|
echo "### 分析文件"
|
||||||
|
echo "📄 $ANALYSIS_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "### 预测 vs 实际"
|
||||||
|
PREDICTED_SPECIAL=$(grep -o "特码重点.*\*\*[0-9]\+\*\*\|特码.*\*\*[0-9]\+\*\*" "$ANALYSIS_FILE" | head -1)
|
||||||
|
PREDICTED_FLAT=$(grep "平码优先" "$ANALYSIS_FILE" | head -1)
|
||||||
|
echo "**预测**: $PREDICTED_SPECIAL · $PREDICTED_FLAT"
|
||||||
|
echo ""
|
||||||
|
echo "### 命中分析"
|
||||||
|
|
||||||
|
HIT_SPECIAL="❌ 未中"
|
||||||
|
if echo "$PREDICTED_SPECIAL" | grep -q "\*\*$AS\*\*"; then
|
||||||
|
HIT_SPECIAL="✅ **特码命中**"
|
||||||
|
fi
|
||||||
|
echo "- 特码 $AS: $HIT_SPECIAL"
|
||||||
|
|
||||||
|
HIT_FLAT=$(echo "$ACTUAL_FLAT" | tr ' ' '\n' | while read n; do
|
||||||
|
if echo "$PREDICTED_FLAT" | grep -q "\*\*$n\*\*\|\b$n\b"; then
|
||||||
|
echo "✅ $n"
|
||||||
|
fi
|
||||||
|
done | tr '\n' ' ')
|
||||||
|
echo "- 平码命中: ${HIT_FLAT:-无}"
|
||||||
|
|
||||||
|
TOTAL_HIT=$(echo "$ACTUAL_FLAT" | tr ' ' '\n' | while read n; do
|
||||||
|
if grep -qE "\*\*$n\*\*| $n[、,,]|\b$n( |$)" "$ANALYSIS_FILE"; then
|
||||||
|
echo "1"
|
||||||
|
fi
|
||||||
|
done | wc -l)
|
||||||
|
echo ""
|
||||||
|
echo "**总命中**: 特码 + 平码 共 $((TOTAL_HIT+0)) / 7 球"
|
||||||
|
else
|
||||||
|
echo "### ⚠️ 未找到分析文件"
|
||||||
|
echo " 查找路径: $HOME/.hermes/cron/output/5bec1f60f77f/"
|
||||||
|
echo " 期号: $QI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "---"
|
||||||
|
echo "_生成时间: $(date '+%Y-%m-%d %H:%M:%S')_"
|
||||||
|
} > "$OUT_FILE"
|
||||||
|
|
||||||
|
cat "$OUT_FILE"
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 信号队列重试脚本 - 每5分钟检查一次待处理信号
|
||||||
|
# 由 signal-queue-retry cron job 调用
|
||||||
|
|
||||||
|
QUEUE_SCRIPT="$HOME/.hermes/skills/trading/okx-auto-position/scripts/signal_queue.py"
|
||||||
|
PROCESS_SCRIPT="$HOME/.hermes/skills/trading/okx-auto-position/scripts/process_signal.py"
|
||||||
|
DB="$HOME/.hermes/trading/signal_queue.db"
|
||||||
|
|
||||||
|
# 检查是否有待处理信号
|
||||||
|
if [ ! -f "$DB" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
PENDING=$(sqlite3 "$DB" "SELECT COUNT(*) FROM queue WHERE status IN ('pending','failed') AND retries < 3")
|
||||||
|
if [ "$PENDING" -eq 0 ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 有待处理信号,运行重试
|
||||||
|
python3 "$QUEUE_SCRIPT" retry
|
||||||
Reference in New Issue
Block a user