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