基于企业微信打造简单的量化信号股票盯盘系统

基于企业微信打造简单的量化信号股票盯盘系统

企业微信的实时推送

原理:

  • 注册企业微信,创建群机器人
  • 监控脚本调用Webhook接口实时推送

设置步骤:

  1. 下载企业微信APP,注册企业
  2. 创建一个群聊,添加"群机器人"(新版本叫消息推送)
  3. 复制机器人的Webhook地址
  4. 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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值