企业微信的实时推送
原理:
- 注册企业微信,创建群机器人
- 监控脚本调用Webhook接口实时推送
设置步骤:
- 下载企业微信APP,注册企业
- 创建一个群聊,添加"群机器人"(新版本叫消息推送)
- 复制机器人的Webhook地址
- Webhook地址y配置到监控脚本
优点: 真正实时推送到手机
缺点: 需要注册企业微信
系统实现
说明:
- 本系统只是初步搭建,仅供参考,各个信号的发出及买卖点均不实用,不构成投资建议,切勿按信号进行投资!
- 具体信号策略的研究请根据专业知识进行研发
#!/usr/bin/env python3
"""
持仓实时监控系统 - 量化买卖点版 v2.0
监控条件:
1. 股价涨跌幅 >= ±2%
2. 成交量较前5日均量放大 >= 30%
3. 量化信号:RSI/MACD/均线突破/量价背离
推送方式:企业微信群机器人 Webhook
"""
import requests
import json
import time
import os
from datetime import datetime, timedelta
from collections import deque
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
# 设置工作目录
WORK_DIR = "c:\\Users\\User\\work"
os.chdir(WORK_DIR)
# 企业微信群机器人 Webhook
WECHAT_WEBHOOK = "你复制的Webhook地址"
# 持仓列表
PORTFOLIO = {
'601899': {'name': '紫金矿业', 'type': 'stock'},
'513180': {'name': '恒生科技ETF', 'type': 'ETF'},
'你的股票代号': {'name': '你的股票名', 'type': 'stock/ETF'},
}
# 监控阈值
PRICE_THRESHOLD = 2.0 # 涨跌幅阈值 ±2%
VOLUME_THRESHOLD = 1.3 # 成交量放大阈值 30%
# 量化信号阈值
RSI_OVERSOLD = 30 # RSI超卖线
RSI_OVERBOUGHT = 70 # RSI超买线
MACD_THRESHOLD = 0.001 # MACD信号线阈值
MA_SHORT = 5 # 短期均线
MA_LONG = 20 # 长期均线
# 存储历史数据
history_data = {code: deque(maxlen=50) for code in PORTFOLIO} # 增加到50条用于计算指标
last_alert_time = {}
last_signal_time = {} # 记录上次信号时间,避免重复推送
@dataclass
class Signal:
"""量化信号结构"""
type: str # 'buy' | 'sell' | 'watch'
strength: str # 'strong' | 'weak' | 'neutral'
indicator: str # 'RSI' | 'MACD' | 'MA' | 'VOLUME' | 'PRICE'
message: str
price: float
timestamp: datetime = field(default_factory=datetime.now)
def log(message):
"""记录日志"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_msg = f"[{timestamp}] {message}"
print(log_msg)
with open("monitor.log", "a", encoding="utf-8") as f:
f.write(log_msg + "\n")
def get_stock_data(code):
"""获取股票实时数据(东方财富接口)这个容易封IP,可以换其他的"""
try:
if code.startswith('0') or code.startswith('3'):
secid = f"0.{code}"
else:
secid = f"1.{code}"
url = "https://push2.eastmoney.com/api/qt/stock/get"
params = {
'secid': secid,
'fields': 'f43,f44,f45,f46,f47,f48,f57,f58,f60,f170,f169,f168,f171,f50,f51,f52'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
response = requests.get(url, params=params, headers=headers, timeout=10)
data = response.json()
if data.get('data'):
stock = data['data']
price = float(stock.get('f43', 0)) / 100 if stock.get('f43') else 0
prev_close = float(stock.get('f60', 0)) / 100 if stock.get('f60') else 0
volume = int(stock.get('f47', 0))
change_pct = float(stock.get('f170', 0)) / 100 if stock.get('f170') else 0
change_amount = float(stock.get('f169', 0)) / 100 if stock.get('f169') else 0
turnover = float(stock.get('f168', 0)) / 100 if stock.get('f168') else 0 # 换手率
high = float(stock.get('f44', 0)) / 100 if stock.get('f44') else 0
low = float(stock.get('f45', 0)) / 100 if stock.get('f45') else 0
open_price = float(stock.get('f46', 0)) / 100 if stock.get('f46') else 0
return {
'code': code,
'name': stock.get('f58', PORTFOLIO[code]['name']),
'price': price,
'prev_close': prev_close,
'volume': volume,
'change_pct': change_pct,
'change_amount': change_amount,
'turnover': turnover,
'high': high,
'low': low,
'open': open_price
}
return None
except Exception as e:
log(f"获取 {code} 数据失败: {e}")
return None
def calculate_rsi(prices: List[float], period: int = 14) -> Optional[float]:
"""计算RSI指标"""
if len(prices) < period + 1:
return None
gains = []
losses = []
for i in range(1, len(prices)):
change = prices[i] - prices[i-1]
if change > 0:
gains.append(change)
losses.append(0)
else:
gains.append(0)
losses.append(abs(change))
if len(gains) < period:
return None
avg_gain = sum(gains[-period:]) / period
avg_loss = sum(losses[-period:]) / period
if avg_loss == 0:
return 100
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi
def calculate_ma(prices: List[float], period: int) -> Optional[float]:
"""计算移动平均线"""
if len(prices) < period:
return None
return sum(prices[-period:]) / period
def calculate_macd(prices: List[float]) -> Tuple[Optional[float], Optional[float], Optional[float]]:
"""计算MACD指标 (MACD, Signal, Histogram)"""
if len(prices) < 35:
return None, None, None
# 计算EMA12和EMA26
ema12 = [prices[0]]
ema26 = [prices[0]]
for i in range(1, len(prices)):
ema12.append(prices[i] * 0.1538 + ema12[-1] * 0.8462) # 2/(12+1)
ema26.append(prices[i] * 0.0741 + ema26[-1] * 0.9259) # 2/(26+1)
# MACD Line = EMA12 - EMA26
macd_line = [ema12[i] - ema26[i] for i in range(len(prices))]
# Signal Line = EMA9 of MACD
signal_line = [macd_line[0]]
for i in range(1, len(macd_line)):
signal_line.append(macd_line[i] * 0.2 + signal_line[-1] * 0.8) # 2/(9+1)
macd = macd_line[-1]
signal = signal_line[-1]
histogram = macd - signal
return macd, signal, histogram
def detect_volume_price_divergence(history: deque) -> Optional[Signal]:
"""检测量价背离信号"""
if len(history) < 10:
return None
data_list = list(history)
recent = data_list[-5:]
previous = data_list[-10:-5]
# 价格变化
recent_price_change = (recent[-1]['price'] - recent[0]['price']) / recent[0]['price']
prev_price_change = (previous[-1]['price'] - previous[0]['price']) / previous[0]['price']
# 成交量变化
recent_avg_vol = sum(d['volume'] for d in recent) / len(recent)
prev_avg_vol = sum(d['volume'] for d in previous) / len(previous)
vol_ratio = recent_avg_vol / prev_avg_vol if prev_avg_vol > 0 else 1
# 量价背离:价涨量缩(卖出信号)或价跌量缩(买入信号)
if recent_price_change > 0.01 and vol_ratio < 0.8:
return Signal(
type='sell',
strength='weak',
indicator='VOLUME',
message=f'⚠️ 量价背离:价格上涨但成交量萎缩 {((1-vol_ratio)*100):.1f}%',
price=recent[-1]['price']
)
elif recent_price_change < -0.01 and vol_ratio < 0.8:
return Signal(
type='buy',
strength='weak',
indicator='VOLUME',
message=f'💡 缩量回调:价格下跌但成交量萎缩 {((1-vol_ratio)*100):.1f}%',
price=recent[-1]['price']
)
return None
def analyze_technical_signals(code: str, data: dict, history: deque) -> List[Signal]:
"""分析技术面信号,返回买卖信号列表"""
signals = []
if len(history) < 20:
return signals
data_list = list(history)
prices = [d['price'] for d in data_list]
volumes = [d['volume'] for d in data_list]
# 1. RSI 超买超卖信号
rsi = calculate_rsi(prices)
if rsi is not None:
if rsi < RSI_OVERSOLD:
signals.append(Signal(
type='buy',
strength='strong' if rsi < 20 else 'weak',
indicator='RSI',
message=f'RSI超卖: {rsi:.1f}(低于{RSI_OVERSOLD})',
price=data['price']
))
elif rsi > RSI_OVERBOUGHT:
signals.append(Signal(
type='sell',
strength='strong' if rsi > 80 else 'weak',
indicator='RSI',
message=f'RSI超买: {rsi:.1f}(高于{RSI_OVERBOUGHT})',
price=data['price']
))
# 2. MACD 金叉死叉信号
macd, signal, hist = calculate_macd(prices)
if macd is not None and signal is not None:
prev_macd, prev_signal, _ = calculate_macd(prices[:-1])
if prev_macd is not None and prev_signal is not None:
# 金叉:MACD从下向上穿过Signal
if prev_macd < prev_signal and macd > signal:
signals.append(Signal(
type='buy',
strength='strong' if hist > 0.01 else 'weak',
indicator='MACD',
message=f'MACD金叉(柱状图:{hist:.4f})',
price=data['price']
))
# 死叉:MACD从上向下穿过Signal
elif prev_macd > prev_signal and macd < signal:
signals.append(Signal(
type='sell',
strength='strong' if hist < -0.01 else 'weak',
indicator='MACD',
message=f'MACD死叉(柱状图:{hist:.4f})',
price=data['price']
))
# 3. 均线突破信号
ma5 = calculate_ma(prices, MA_SHORT)
ma20 = calculate_ma(prices, MA_LONG)
if ma5 is not None and ma20 is not None:
prev_prices = prices[:-1]
prev_ma5 = calculate_ma(prev_prices, MA_SHORT)
prev_ma20 = calculate_ma(prev_prices, MA_LONG)
if prev_ma5 is not None and prev_ma20 is not None:
# 金叉:短期均线上穿长期均线
if prev_ma5 < prev_ma20 and ma5 > ma20:
signals.append(Signal(
type='buy',
strength='strong',
indicator='MA',
message=f'均线金叉:MA{MA_SHORT}({ma5:.3f})上穿MA{MA_LONG}({ma20:.3f})',
price=data['price']
))
# 死叉:短期均线下穿长期均线
elif prev_ma5 > prev_ma20 and ma5 < ma20:
signals.append(Signal(
type='sell',
strength='strong',
indicator='MA',
message=f'均线死叉:MA{MA_SHORT}({ma5:.3f})下穿MA{MA_LONG}({ma20:.3f})',
price=data['price']
))
# 价格突破均线
elif data['price'] > ma5 * 1.02 and data_list[-2]['price'] <= ma5 * 1.02:
signals.append(Signal(
type='buy',
strength='weak',
indicator='MA',
message=f'价格突破MA{MA_SHORT}:{data["price"]:.3f} > {ma5:.3f}',
price=data['price']
))
# 4. 量价背离
divergence = detect_volume_price_divergence(history)
if divergence:
signals.append(divergence)
return signals
def send_wechat_alert(content):
"""发送企业微信群机器人消息(markdown格式)"""
try:
payload = {
"msgtype": "markdown",
"markdown": {
"content": content
}
}
response = requests.post(WECHAT_WEBHOOK, json=payload, timeout=10)
result = response.json()
if result.get("errcode") == 0:
log(f"[推送成功] {content[:40].strip()}...")
else:
log(f"[推送失败] errcode={result.get('errcode')} errmsg={result.get('errmsg')}")
# 本地日志备份
with open("alerts.log", "a", encoding="utf-8") as f:
f.write(f"[{datetime.now().strftime('%H:%M:%S')}] {content[:80]}\n")
return result.get("errcode") == 0
except Exception as e:
log(f"推送异常: {e}")
return False
def build_price_alert(data):
"""构建涨跌报警消息(Markdown)"""
change_pct = data['change_pct']
direction = "大涨 🚀" if change_pct > 0 else "大跌 ⚠️"
# A股习惯:涨=红色,跌=绿色
color = "red" if change_pct > 0 else "green"
sign = "+" if change_pct > 0 else ""
return (
f"## 📊 持仓异动 | {data['name']} {direction}\n"
f"> **股票:**{data['name']}({data['code']})\n"
f"> **现价:**<font color=\"{color}\">{data['price']:.3f} 元</font>\n"
f"> **涨跌幅:**<font color=\"{color}\">{sign}{change_pct:.2f}%</font>\n"
f"> **时间:**{datetime.now().strftime('%H:%M:%S')}"
)
def build_volume_alert(data, volume_ratio):
"""构建放量报警消息(Markdown)"""
change_pct = data['change_pct']
sign = "+" if change_pct > 0 else ""
# A股习惯:涨=红色,跌=绿色
color = "red" if change_pct > 0 else "green"
return (
f"## 🔥 放量异动 | {data['name']}\n"
f"> **股票:**{data['name']}({data['code']})\n"
f"> **现价:**{data['price']:.3f} 元\n"
f"> **涨跌幅:**<font color=\"{color}\">{sign}{change_pct:.2f}%</font>\n"
f"> **当前成交量:**{data['volume']:,} 手\n"
f"> **较均量放大:**<font color=\"warning\">{(volume_ratio - 1) * 100:.1f}%</font>\n"
f"> **时间:**{datetime.now().strftime('%H:%M:%S')}"
)
def build_signal_alert(data: dict, signal: Signal) -> str:
"""构建量化信号报警消息"""
# 根据信号类型和强度选择图标和颜色
if signal.type == 'buy':
if signal.strength == 'strong':
emoji = "🟢"
title = f"强烈买入信号 | {data['name']}"
color = "info"
else:
emoji = "🟡"
title = f"买入信号 | {data['name']}"
color = "info"
elif signal.type == 'sell':
if signal.strength == 'strong':
emoji = "🔴"
title = f"强烈卖出信号 | {data['name']}"
color = "warning"
else:
emoji = "🟠"
title = f"卖出信号 | {data['name']}"
color = "warning"
else:
emoji = "⚪"
title = f"关注信号 | {data['name']}"
color = "comment"
# 构建建议
if signal.type == 'buy':
suggestion = "建议关注买入机会"
if signal.strength == 'strong':
suggestion = "建议积极买入,设置止损"
elif signal.type == 'sell':
suggestion = "建议减仓观望"
if signal.strength == 'strong':
suggestion = "建议果断卖出,锁定利润/止损"
else:
suggestion = "建议继续观察"
return (
f"## {emoji} {title}\n"
f"> **信号类型:**{signal.indicator} {signal.type.upper()}\n"
f"> **信号强度:**{'强' if signal.strength == 'strong' else '弱' if signal.strength == 'weak' else '中性'}\n"
f"> **触发条件:**{signal.message}\n"
f"> **当前价格:**<font color=\"{color}\">{data['price']:.3f} 元</font>\n"
f"> **操作建议:**{suggestion}\n"
f"> **时间:**{datetime.now().strftime('%H:%M:%S')}"
)
def check_alerts(code, data):
"""检查是否触发报警条件(包括量化信号)"""
alerts = []
now = datetime.now()
# 检查涨跌幅
change_pct = data['change_pct']
if abs(change_pct) >= PRICE_THRESHOLD:
last_time = last_alert_time.get(f"{code}_price")
if not last_time or (now - last_time) > timedelta(minutes=10):
alerts.append(build_price_alert(data))
last_alert_time[f"{code}_price"] = now
# 检查成交量
history = history_data[code]
if len(history) >= 3 and data['volume'] > 0:
volumes = [h['volume'] for h in history if isinstance(h, dict)]
if len(volumes) >= 3:
avg_volume = sum(volumes) / len(volumes)
if avg_volume > 0 and data['volume'] > avg_volume * VOLUME_THRESHOLD:
last_time = last_alert_time.get(f"{code}_volume")
if not last_time or (now - last_time) > timedelta(minutes=10):
volume_ratio = data['volume'] / avg_volume
alerts.append(build_volume_alert(data, volume_ratio))
last_alert_time[f"{code}_volume"] = now
# 检查量化信号
signals = analyze_technical_signals(code, data, history)
for signal in signals:
signal_key = f"{code}_{signal.indicator}_{signal.type}"
last_time = last_signal_time.get(signal_key)
# 同一信号30分钟内只推送一次
if not last_time or (now - last_time) > timedelta(minutes=30):
alerts.append(build_signal_alert(data, signal))
last_signal_time[signal_key] = now
log(f"[量化信号] {data['name']} - {signal.indicator} {signal.type}")
# 更新历史数据(存储完整数据用于指标计算)
history.append(data)
return alerts
def is_trading_time():
"""判断是否为交易时间"""
now = datetime.now()
if now.weekday() >= 5:
return False
time_str = now.strftime("%H:%M")
return ("09:30" <= time_str <= "11:30") or ("13:00" <= time_str <= "15:00")
def init_history():
"""初始化历史数据"""
log("正在初始化历史数据...")
for code in PORTFOLIO:
data = get_stock_data(code)
if data and data['volume'] > 0:
history_data[code].append(data)
time.sleep(0.3)
log("初始化完成")
def run_monitor():
"""主监控循环"""
log("=" * 50)
log("持仓监控系统启动(量化买卖点版 v2.0)")
log(f"监控标的: {len(PORTFOLIO)} 只")
log(f"报警阈值: 涨跌>=±{PRICE_THRESHOLD}%, 放量>={int((VOLUME_THRESHOLD-1)*100)}%")
log("量化信号: RSI/MACD/均线/量价背离")
log("=" * 50)
# 发送启动通知
startup_msg = (
"## ✅ 持仓监控系统已启动(量化版)\n"
f"> **监控标的:**{len(PORTFOLIO)} 只\n"
f"> **涨跌阈值:**±{PRICE_THRESHOLD}%\n"
f"> **放量阈值:**+{int((VOLUME_THRESHOLD-1)*100)}%\n"
"> **量化信号:**\n"
"> • RSI超买超卖(30/70)\n"
"> • MACD金叉死叉\n"
"> • 均线突破(5日/20日)\n"
"> • 量价背离\n"
f"> **扫描频率:**每30秒\n"
f"> **启动时间:**{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
"> **监控标的:**恒生科技ETF / 紫金矿业 / 你的股票名 / 游戏ETF"
)
send_wechat_alert(startup_msg)
init_history()
scan_count = 0
while True:
try:
if is_trading_time():
scan_count += 1
log(f"第 {scan_count} 轮扫描...")
for code in PORTFOLIO:
data = get_stock_data(code)
if data:
for alert_msg in check_alerts(code, data):
send_wechat_alert(alert_msg)
time.sleep(0.5)
time.sleep(30)
else:
if scan_count > 0:
log("非交易时间,监控暂停等待...")
scan_count = 0
time.sleep(60)
except KeyboardInterrupt:
log("监控已手动停止")
send_wechat_alert("## ⛔ 持仓监控系统已停止\n> 如需重新启动,请运行 start_monitor.bat")
break
except Exception as e:
log(f"监控异常: {e}")
time.sleep(60)
if __name__ == "__main__":
run_monitor()



被折叠的 条评论
为什么被折叠?



