slug: stock-price-alert-cn name: stock-price-alert description: "股票涨跌自动监控提醒 v1.0.1 — 修复quick_add TypeError、优化逐股查询(不再全市场拉取)、新增5分钟去重机制。设置价格/涨跌幅阈值,A股实时监控,突破时自动推送提醒。支持多股票同时监控、批量配置、历史提醒记录。" version: 1.0.1 trigger:
- 股票监控
- 股价提醒
- 涨跌提醒
- 价格预警
- 股票预警
- 股价监控
- 设置提醒
- 股票通知
- 突破提醒
- 添加监控
- 我的监控
- 查看提醒 category:
- 数据分析 tags:
- 股票
- 监控
- 提醒
- A股
- 实时行情 displayName: "股票涨跌自动监控提醒" summary: "股票涨跌自动监控提醒 — 设置价格/涨跌幅阈值,A 股实时监控,突破时自动推送提醒。支持多股票同时监控、批量配置、历史提醒记录、5 分钟去重。"
股票涨跌自动监控提醒 v1.0.1
触发条件
当用户提到 股票监控/股价提醒/涨跌提醒/价格预警/股票预警/股价监控/设置提醒/股票通知/突破提醒/添加监控/查看提醒 时自动加载。
快速决策树
用户请求股票监控
├── 添加监控股票?→ [A] 设置监控 + 价格/涨跌幅阈值
├── 查看当前监控?→ [B] 列出所有监控 + 当前价格状态
├── 手动检查触发?→ [C] 轮询所有监控 → 输出触发清单
├── 删除/修改监控?→ [D] 管理监控列表
└── 查看历史提醒?→ [E] 提醒记录查询
依赖安装
pip install akshare pandas -q
akshare 免费无需 API Key,数据来自东方财富等公开接口。
工作流 A:添加监控
A1: 添加价格阈值监控
import json, os
WATCHLIST_FILE = os.path.expanduser("~/.hermes/stock_watchlist.json")
def load_watchlist():
if os.path.exists(WATCHLIST_FILE):
with open(WATCHLIST_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return []
def save_watchlist(data):
with open(WATCHLIST_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def add_price_alert(stock_code, stock_name="", upper_price=None, lower_price=None,
upper_pct=None, lower_pct=None, note=""):
"""
添加股票价格监控
stock_code: 股票代码,如 '000001' 或 '600519'
upper_price: 上涨到该价格时提醒(元)
lower_price: 下跌到该价格时提醒(元)
upper_pct: 涨幅超过该百分比时提醒(如 5 表示 5%)
lower_pct: 跌幅超过该百分比时提醒(如 -3 表示跌 3%)
"""
watchlist = load_watchlist()
# 检查是否已存在
code_clean = stock_code.replace('sh', '').replace('sz', '').replace('.', '')
for item in watchlist:
if item['code'] == code_clean:
# 更新已有监控
if upper_price: item['upper_price'] = upper_price
if lower_price: item['lower_price'] = lower_price
if upper_pct is not None: item['upper_pct'] = upper_pct
if lower_pct is not None: item['lower_pct'] = lower_pct
if note: item['note'] = note
save_watchlist(watchlist)
return {"action": "updated", "code": code_clean, "name": item.get('name', '')}
# 新建监控
entry = {
"code": code_clean,
"name": stock_name or code_clean,
"upper_price": upper_price,
"lower_price": lower_price,
"upper_pct": upper_pct,
"lower_pct": lower_pct,
"note": note,
"created_at": __import__('datetime').datetime.now().isoformat(),
"last_price": None,
"last_check": None,
}
watchlist.append(entry)
save_watchlist(watchlist)
return {"action": "added", "code": code_clean, "name": stock_name or code_clean}
# 示例用法
# add_price_alert("000001", "平安银行", upper_price=12.50, lower_price=10.00, note="测试")
# add_price_alert("600519", "贵州茅台", upper_pct=5, lower_pct=-3)
A2: 快速添加(交互式)
def quick_add(stock_code, threshold_str):
"""
快速添加监控
threshold_str 格式:
">15.5" — 突破15.5元提醒
"<10.0" — 跌破10元提醒
"12~15" — 12-15元区间监控
"+5%" — 涨5%提醒
"-3%" — 跌3%提醒
">15.5,-5%" — 组合条件
"""
import re
upper_price, lower_price = None, None
upper_pct, lower_pct = None, None
parts = threshold_str.replace(',', ',').split(',')
for part in parts:
part = part.strip()
# 价格阈值
m = re.match(r'>(\d+\.?\d*)', part)
if m: upper_price = float(m.group(1))
m = re.match(r'<(\d+\.?\d*)', part)
if m: lower_price = float(m.group(1))
m = re.match(r'(\d+\.?\d*)~(\d+\.?\d*)', part)
if m:
lower_price = float(m.group(1))
upper_price = float(m.group(2))
# 百分比阈值
m = re.match(r'\+(\d+\.?\d*)%', part)
if m: upper_pct = float(m.group(1))
m = re.match(r'\-(\d+\.?\d*)%', part)
if m: lower_pct = -float(m.group(1))
return add_price_alert(stock_code,
upper_price=upper_price, lower_price=lower_price,
upper_pct=upper_pct, lower_pct=lower_pct)
# 示例
# quick_add("000001", ">12.5,<10.0")
# quick_add("600519", "+5%,-3%")
工作流 B:查看监控列表
def list_watchlist():
"""列出所有监控及当前价格状态"""
watchlist = load_watchlist()
if not watchlist:
return "当前没有监控中的股票。使用「添加监控」开始。\n\n示例:监控 000001 12~15元"
# 获取实时价格
prices = get_realtime_prices([w['code'] for w in watchlist])
lines = ["## 📊 股票监控列表\n"]
lines.append(f"共 {len(watchlist)} 只股票\n")
for w in watchlist:
code = w['code']
name = w.get('name', code)
current = prices.get(code, {}).get('price')
change_pct = prices.get(code, {}).get('change_pct')
lines.append(f"### {name}({code})")
if current:
arrow = "🔺" if (change_pct or 0) > 0 else ("🔻" if (change_pct or 0) < 0 else "➖")
lines.append(f" 现价: {arrow} {current:.2f}元 ({change_pct:+.2f}%)")
else:
lines.append(f" 现价: 获取中...")
conditions = []
if w.get('upper_price'):
conditions.append(f"涨到 {w['upper_price']}元")
if current and current >= w['upper_price']:
conditions[-1] += " 🚨已触发"
if w.get('lower_price'):
conditions.append(f"跌到 {w['lower_price']}元")
if current and current <= w['lower_price']:
conditions[-1] += " 🚨已触发"
if w.get('upper_pct'):
conditions.append(f"涨幅超 {w['upper_pct']}%")
if w.get('lower_pct'):
conditions.append(f"跌幅超 {abs(w['lower_pct'])}%")
if w.get('note'):
conditions.append(f"备注: {w['note']}")
for c in conditions:
lines.append(f" - {c}")
lines.append("")
return '\n'.join(lines)
工作流 C:检查并触发提醒
C1: 获取实时价格
def get_realtime_prices(codes):
"""批量获取股票实时价格(逐股查询东方财富API,避免全市场拉取)"""
import urllib.request, json
result = {}
for code in codes:
try:
market = "1" if code.startswith('6') else "0"
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid={market}.{code}&fields=f43,f44,f45,f46,f47,f48,f50,f51,f52,f57,f58,f60,f169,f170"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
resp = urllib.request.urlopen(req, timeout=10)
data = json.loads(resp.read())
d = data.get('data', {})
if d:
result[code] = {
'price': d.get('f43', 0) / 100 if d.get('f43') else None,
'change_pct': d.get('f170', 0) / 100 if d.get('f170') else None,
'high': d.get('f44', 0) / 100 if d.get('f44') else None,
'low': d.get('f45', 0) / 100 if d.get('f45') else None,
'volume': d.get('f47', 0),
'name': d.get('f58', code),
}
except Exception as e:
result[code] = {'price': None, 'error': str(e)}
return result
def get_realtime_prices_bulk(codes):
"""备选:通过 akshare 全市场拉取(适合监控5+只股票时减少请求数)"""
try:
import akshare as ak
df = ak.stock_zh_a_spot_em()
result = {}
for code in codes:
row = df[df['代码'] == code]
if not row.empty:
r = row.iloc[0]
result[code] = {
'price': float(r['最新价']),
'change_pct': float(r['涨跌幅']),
'high': float(r['最高']),
'low': float(r['最低']),
'volume': float(r['成交量']),
'name': r['名称'],
}
return result
except Exception as e:
print(f"akshare批量获取失败: {e},回退到逐股查询...")
return get_realtime_prices(codes)
C2: 检查提醒触发
import os, json
from datetime import datetime
ALERT_LOG_FILE = os.path.expanduser("~/.hermes/stock_alerts.json")
def load_alerts_log():
if os.path.exists(ALERT_LOG_FILE):
with open(ALERT_LOG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return []
def save_alerts_log(data):
with open(ALERT_LOG_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def check_alerts():
"""检查所有监控,返回触发的提醒列表(v1.0.1 新增去重:同股票同类型5分钟内不重复)"""
watchlist = load_watchlist()
if not watchlist:
return []
codes = [w['code'] for w in watchlist]
prices = get_realtime_prices(codes)
alerts_log = load_alerts_log()
triggered = []
# 去重窗口:5分钟内同股票同类型不重复
DEDUP_WINDOW = 300 # 秒
now_ts = datetime.now().timestamp()
for w in watchlist:
code = w['code']
current = prices.get(code, {})
price = current.get('price')
change_pct = current.get('change_pct')
if price is None:
continue
alerts = []
# 价格突破检查
if w.get('upper_price') and price >= w['upper_price']:
alerts.append(("upper_price", f"🔺 突破上限 {w['upper_price']}元(现价 {price:.2f})"))
if w.get('lower_price') and price <= w['lower_price']:
alerts.append(("lower_price", f"🔻 跌破下限 {w['lower_price']}元(现价 {price:.2f})"))
if w.get('upper_pct') and change_pct and change_pct >= w['upper_pct']:
alerts.append(("upper_pct", f"📈 涨幅 {change_pct:+.2f}% 超阈值 {w['upper_pct']}%"))
if w.get('lower_pct') and change_pct and change_pct <= w['lower_pct']:
alerts.append(("lower_pct", f"📉 跌幅 {change_pct:+.2f}% 超阈值 {w['lower_pct']}%"))
if alerts:
# 去重:检查最近记录中是否有相同股票+类型
for alert_type, alert_msg in alerts:
is_dup = False
for past in alerts_log:
past_ts = datetime.fromisoformat(past['time']).timestamp()
if (now_ts - past_ts) < DEDUP_WINDOW and past['code'] == code:
# 检查同一类型是否已在窗口内触发
for past_alert in past.get('alerts', []):
if alert_type in past_alert:
is_dup = True
break
if is_dup:
break
if is_dup:
continue # 跳过重复提醒
# 非重复 → 记录
record = {
"code": code,
"name": w.get('name', code),
"price": price,
"change_pct": change_pct,
"alerts": [alert_msg],
"time": datetime.now().isoformat(),
}
triggered.append(record)
alerts_log.append(record)
# 更新最后价格
w['last_price'] = price
w['last_check'] = datetime.now().isoformat()
save_watchlist(watchlist)
if triggered:
save_alerts_log(alerts_log)
return triggered
def format_alerts(triggered):
"""格式化提醒输出"""
if not triggered:
return "✅ 所有监控正常,无触发提醒。"
lines = ["## 🚨 股票提醒触发!\n"]
for r in triggered:
name = r['name']
code = r['code']
price = r['price']
change = r.get('change_pct', 0)
arrow = "🔺" if (change or 0) > 0 else "🔻"
lines.append(f"### {name}({code}){arrow} {price:.2f}元 ({change:+.2f}%)")
for a in r['alerts']:
lines.append(f" - {a}")
lines.append(f" *触发时间: {r['time']}*")
lines.append("")
return '\n'.join(lines)
C3: 一键检查
def run_check():
"""执行一次完整检查并输出结果"""
triggered = check_alerts()
return format_alerts(triggered)
# 使用:
# print(run_check())
工作流 D:管理监控
def remove_alert(stock_code):
"""删除某只股票的监控"""
code_clean = stock_code.replace('sh', '').replace('sz', '').replace('.', '')
watchlist = load_watchlist()
new_list = [w for w in watchlist if w['code'] != code_clean]
if len(new_list) == len(watchlist):
return f"未找到 {code_clean} 的监控"
save_watchlist(new_list)
return f"已删除 {code_clean} 的监控(剩余 {len(new_list)} 只)"
def clear_all():
"""清空所有监控"""
save_watchlist([])
return "已清空所有监控"
def modify_alert(stock_code, **kwargs):
"""修改监控 — 使用方法同 add_price_alert,会更新已有条目"""
return add_price_alert(stock_code, **kwargs)
工作流 E:历史提醒
def alert_history(days=7):
"""查看最近的提醒记录"""
alerts = load_alerts_log()
if not alerts:
return "暂无提醒记录。"
cutoff = datetime.now().timestamp() - days * 86400
recent = [a for a in alerts if datetime.fromisoformat(a['time']).timestamp() > cutoff]
if not recent:
return f"最近 {days} 天无提醒记录。"
lines = [f"## 📋 最近 {days} 天提醒记录\n"]
lines.append(f"共 {len(recent)} 条触发记录\n")
for r in reversed(recent):
name = r['name']
code = r['code']
lines.append(f"- **{r['time'][:16]}** | {name}({code}) | {r['price']:.2f}元")
for a in r['alerts']:
lines.append(f" {a}")
return '\n'.join(lines)
定时自动检查(推荐设置)
设置定时任务,每 5 分钟检查一次(仅交易时段 9:30-15:00):
# 创建检查脚本
cat > ~/.hermes/scripts/stock_check.py << 'EOF'
"""股票监控定时检查"""
import json, os, sys
sys.path.insert(0, os.path.dirname(__file__))
from datetime import datetime
# 判断是否交易时段
now = datetime.now()
weekday = now.weekday() # 0=Mon, 6=Sun
hour, minute = now.hour, now.minute
is_trading = (weekday < 5 and (
(hour == 9 and minute >= 30) or
(hour == 10) or (hour == 11 and minute <= 30) or
(hour >= 13 and hour < 15)
))
if not is_trading:
# 非交易时段静默
sys.exit(0)
# 执行检查
WATCHLIST_FILE = os.path.expanduser("~/.hermes/stock_watchlist.json")
if not os.path.exists(WATCHLIST_FILE):
sys.exit(0)
# 简化的价格获取
import urllib.request
def get_price(code):
market = "1" if code.startswith('6') else "0"
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid={market}.{code}&fields=f43,f170"
try:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
resp = urllib.request.urlopen(req, timeout=5)
data = json.loads(resp.read()).get('data', {})
return data.get('f43', 0) / 100, data.get('f170', 0) / 100
except:
return None, None
with open(WATCHLIST_FILE, 'r', encoding='utf-8') as f:
watchlist = json.load(f)
triggered = []
for w in watchlist:
price, change = get_price(w['code'])
if price is None:
continue
alerts = []
if w.get('upper_price') and price >= w['upper_price']:
alerts.append(f"🔺突破{w['upper_price']}元→现价{price:.2f}")
if w.get('lower_price') and price <= w['lower_price']:
alerts.append(f"🔻跌破{w['lower_price']}元→现价{price:.2f}")
if w.get('upper_pct') and change and change >= w['upper_pct']:
alerts.append(f"📈涨幅{change:+.2f}%超阈值{w['upper_pct']}%")
if w.get('lower_pct') and change and change <= w['lower_pct']:
alerts.append(f"📉跌幅{change:+.2f}%超阈值{w['lower_pct']}%")
if alerts:
triggered.append(f"{w.get('name',w['code'])}({w['code']}) {price:.2f}元 {change:+.2f}%\n" + "\n".join(alerts))
if triggered:
print("🚨 股票提醒触发!\n\n" + "\n\n".join(triggered))
EOF
# 创建 cron 任务(交易时段每5分钟)
hermes cron create "*/5 9-11,13-14 * * 1-5" --script stock_check.py --no-agent
常见使用场景
场景1:设置价格区间监控
用户:帮我监控平安银行,12到15元区间
→ 调用 quick_add("000001", "12~15")
场景2:涨跌幅提醒
用户:贵州茅台涨3%或跌2%提醒我
→ 调用 quick_add("600519", "+3%,-2%")
场景3:查看所有监控
用户:我有哪些股票在监控?
→ 调用 list_watchlist()
场景4:手动检查
用户:现在检查一下股票有没有触发提醒
→ 调用 run_check()
数据文件
| 文件 | 用途 |
|------|------|
| ~/.hermes/stock_watchlist.json | 监控列表配置 |
| ~/.hermes/stock_alerts.json | 历史提醒记录 |
错误速查
| 错误 | 原因 | 解决 |
|------|------|------|
| 获取行情失败 | 网络问题或非交易时段 | 用备用API或稍后重试 |
| akshare未安装 | 缺少依赖 | pip install akshare pandas |
| 代码无效 | 股票代码格式错误 | 使用6位纯数字代码 |
| 无数据返回 | 非交易时段 | 交易日9:30-15:00可用 |
微信扫一扫