From 913d1945d4fd1ad345f65b884c83f7af186791cd Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 24 Jul 2026 11:42:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=20trading=20?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E8=84=9A=E6=9C=AC=E5=88=B0=20skill=20?= =?UTF-8?q?=E4=BB=93=E5=BA=93=20(=E7=AC=AC2=E6=89=B9:=20=F0=9F=9F=A1=20?= =?UTF-8?q?=E4=B8=AD=E4=BC=98=E5=85=88=E7=BA=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【迁移内容】 - 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) --- dividend-investing/scripts/dca_monitor.py | 166 +++++++++++++ dividend-investing/scripts/dca_monitor_us.sh | 3 + dividend-scanner/scripts/scan_cn.py | 127 ++++++++++ dividend-scanner/scripts/scan_cn.sh | 3 + dividend-scanner/scripts/scan_hk.sh | 3 + dividend-scanner/scripts/scan_us.sh | 3 + longbridge-cli/scripts/longport_http.py | 219 ++++++++++++++++++ lottery-hk/scripts/verify_lottery.sh | 96 ++++++++ .../scripts/signal_queue_retry.sh | 20 ++ 9 files changed, 640 insertions(+) create mode 100755 dividend-investing/scripts/dca_monitor.py create mode 100755 dividend-investing/scripts/dca_monitor_us.sh create mode 100755 dividend-scanner/scripts/scan_cn.py create mode 100755 dividend-scanner/scripts/scan_cn.sh create mode 100755 dividend-scanner/scripts/scan_hk.sh create mode 100755 dividend-scanner/scripts/scan_us.sh create mode 100755 longbridge-cli/scripts/longport_http.py create mode 100755 lottery-hk/scripts/verify_lottery.sh create mode 100755 okx-auto-position/scripts/signal_queue_retry.sh diff --git a/dividend-investing/scripts/dca_monitor.py b/dividend-investing/scripts/dca_monitor.py new file mode 100755 index 0000000..5ca0c0d --- /dev/null +++ b/dividend-investing/scripts/dca_monitor.py @@ -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)) diff --git a/dividend-investing/scripts/dca_monitor_us.sh b/dividend-investing/scripts/dca_monitor_us.sh new file mode 100755 index 0000000..8a9fd80 --- /dev/null +++ b/dividend-investing/scripts/dca_monitor_us.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /home/openclaw/.hermes/scripts +python3 dca_monitor.py --market=us diff --git a/dividend-scanner/scripts/scan_cn.py b/dividend-scanner/scripts/scan_cn.py new file mode 100755 index 0000000..7d93a08 --- /dev/null +++ b/dividend-scanner/scripts/scan_cn.py @@ -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)) diff --git a/dividend-scanner/scripts/scan_cn.sh b/dividend-scanner/scripts/scan_cn.sh new file mode 100755 index 0000000..1701ec1 --- /dev/null +++ b/dividend-scanner/scripts/scan_cn.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /home/openclaw/.hermes/scripts +python3 scan_cn.py diff --git a/dividend-scanner/scripts/scan_hk.sh b/dividend-scanner/scripts/scan_hk.sh new file mode 100755 index 0000000..b9ecef8 --- /dev/null +++ b/dividend-scanner/scripts/scan_hk.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /home/openclaw/.hermes/scripts +python3 dca_scanner.py hk diff --git a/dividend-scanner/scripts/scan_us.sh b/dividend-scanner/scripts/scan_us.sh new file mode 100755 index 0000000..5e385ec --- /dev/null +++ b/dividend-scanner/scripts/scan_us.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd /home/openclaw/.hermes/scripts +python3 dca_scanner.py us diff --git a/longbridge-cli/scripts/longport_http.py b/longbridge-cli/scripts/longport_http.py new file mode 100755 index 0000000..2857ab5 --- /dev/null +++ b/longbridge-cli/scripts/longport_http.py @@ -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") \ No newline at end of file diff --git a/lottery-hk/scripts/verify_lottery.sh b/lottery-hk/scripts/verify_lottery.sh new file mode 100755 index 0000000..a359cb8 --- /dev/null +++ b/lottery-hk/scripts/verify_lottery.sh @@ -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" diff --git a/okx-auto-position/scripts/signal_queue_retry.sh b/okx-auto-position/scripts/signal_queue_retry.sh new file mode 100755 index 0000000..ec5e92e --- /dev/null +++ b/okx-auto-position/scripts/signal_queue_retry.sh @@ -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