OpenClaw 完整备份 - 2026-03-21
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stock Monitor Pro - 智能分析引擎
|
||||
集成:新闻、资金流向、龙虎榜、宏观关联分析
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
class StockAnalyser:
|
||||
"""股票智能分析器 - 结合多维度数据给出建议"""
|
||||
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
})
|
||||
|
||||
# ========== 1. 新闻舆情 ==========
|
||||
|
||||
def fetch_eastmoney_news(self, symbol: str, name: str, limit: int = 5) -> List[Dict]:
|
||||
"""获取东方财富个股新闻"""
|
||||
url = f"https://searchapi.eastmoney.com/api/suggest/get"
|
||||
params = {
|
||||
"input": name,
|
||||
"type": 14,
|
||||
"count": limit
|
||||
}
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=10)
|
||||
data = resp.json()
|
||||
news_list = []
|
||||
for item in data.get("QuotationCodeTable", {}).get("Data", []):
|
||||
news_list.append({
|
||||
"title": item.get("Title", ""),
|
||||
"url": item.get("Url", ""),
|
||||
"time": item.get("ShowTime", "")
|
||||
})
|
||||
return news_list
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
def fetch_sina_news(self, symbol: str, name: str) -> List[Dict]:
|
||||
"""获取新浪财经个股新闻"""
|
||||
# 新浪新闻搜索接口
|
||||
url = f"https://search.sina.com.cn/?q={name}&c=news&sort=time"
|
||||
try:
|
||||
resp = self.session.get(url, timeout=10)
|
||||
# 这里可以做更精细的HTML解析
|
||||
# 简化返回示例
|
||||
return [{"title": f"新浪财经-{name}相关新闻", "source": "新浪"}]
|
||||
except:
|
||||
return []
|
||||
|
||||
def analyze_sentiment(self, news_list: List[Dict]) -> Dict:
|
||||
"""简单情感分析"""
|
||||
positive_words = ['利好', '增长', '突破', '买入', '增持', '涨停', '超预期', '业绩大增']
|
||||
negative_words = ['利空', '减持', '下跌', '卖出', '亏损', '暴雷', '跌停', '不及预期']
|
||||
|
||||
sentiment = {"positive": 0, "negative": 0, "neutral": 0, "summary": []}
|
||||
|
||||
for news in news_list:
|
||||
title = news.get("title", "")
|
||||
p_count = sum(1 for w in positive_words if w in title)
|
||||
n_count = sum(1 for w in negative_words if w in title)
|
||||
|
||||
if p_count > n_count:
|
||||
sentiment["positive"] += 1
|
||||
elif n_count > p_count:
|
||||
sentiment["negative"] += 1
|
||||
else:
|
||||
sentiment["neutral"] += 1
|
||||
|
||||
# 生成情感摘要
|
||||
if sentiment["positive"] > sentiment["negative"]:
|
||||
sentiment["overall"] = "偏多"
|
||||
elif sentiment["negative"] > sentiment["positive"]:
|
||||
sentiment["overall"] = "偏空"
|
||||
else:
|
||||
sentiment["overall"] = "中性"
|
||||
|
||||
return sentiment
|
||||
|
||||
# ========== 2. 资金流向 ==========
|
||||
|
||||
def fetch_fund_flow(self, symbol: str, market: str = "sz") -> Dict:
|
||||
"""获取个股资金流向 (新浪财经)"""
|
||||
# 新浪资金流向接口
|
||||
code = f"{market}{symbol}"
|
||||
url = f"https://quotes.sina.cn/cn/api/quotes.php?symbol={code}&source=sina"
|
||||
|
||||
try:
|
||||
resp = self.session.get(url, timeout=10)
|
||||
# 解析返回数据
|
||||
return {
|
||||
"main_inflow": "数据获取中...",
|
||||
"retail_inflow": "数据获取中...",
|
||||
"net_inflow": "数据获取中..."
|
||||
}
|
||||
except:
|
||||
return {"error": "获取失败"}
|
||||
|
||||
def fetch_northbound_flow(self) -> Dict:
|
||||
"""获取北向资金 (沪深股通) 流向"""
|
||||
url = "https://push2.eastmoney.com/api/qt/stock/get"
|
||||
params = {"secid": "1.000001", "fields": "f170"} # 简化示例
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=10)
|
||||
return {"northbound": "北向资金数据获取中..."}
|
||||
except:
|
||||
return {}
|
||||
|
||||
# ========== 3. 龙虎榜 ==========
|
||||
|
||||
def fetch_dragon_tiger(self, date: str = None) -> List[Dict]:
|
||||
"""获取龙虎榜数据"""
|
||||
if not date:
|
||||
date = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
url = f"http://datacenter-web.eastmoney.com/api/data/v1/get"
|
||||
params = {
|
||||
"sortColumns": "NET_BUY_AMT",
|
||||
"sortTypes": "-1",
|
||||
"pageSize": "50",
|
||||
"pageNumber": "1",
|
||||
"reportName": "RPT_DMSK_TS",
|
||||
"columns": "ALL",
|
||||
"filter": f"(TRADE_DATE='{date}')"
|
||||
}
|
||||
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=10)
|
||||
data = resp.json()
|
||||
return data.get("result", {}).get("data", [])
|
||||
except:
|
||||
return []
|
||||
|
||||
# ========== 4. 宏观关联分析 ==========
|
||||
|
||||
def analyze_gold_correlation(self, gold_price: float, stocks: List[Dict]) -> str:
|
||||
"""分析金价与持仓股票的关联"""
|
||||
# 江西铜业等有色股与金价正相关
|
||||
correlation_map = {
|
||||
"600362": "强正相关", # 江西铜业
|
||||
"601318": "弱相关", # 中国平安
|
||||
"513180": "弱负相关", # 恒生科技
|
||||
"159892": "弱相关", # 恒生医疗
|
||||
}
|
||||
|
||||
analysis = []
|
||||
for stock in stocks:
|
||||
code = stock.get("code")
|
||||
corr = correlation_map.get(code, "未知")
|
||||
if corr in ["强正相关", "中等正相关"]:
|
||||
analysis.append(f"📈 {stock['name']}: 与金价{corr},金价上涨可能带动该股")
|
||||
|
||||
return "\n".join(analysis) if analysis else "暂无强关联标的"
|
||||
|
||||
# ========== 5. 综合分析 ==========
|
||||
|
||||
def generate_insight(self, stock: Dict, price_data: Dict, alerts: List) -> str:
|
||||
"""生成综合分析报告"""
|
||||
code = stock['code']
|
||||
name = stock['name']
|
||||
|
||||
# 1. 获取新闻
|
||||
news_list = self.fetch_eastmoney_news(code, name)
|
||||
sentiment = self.analyze_sentiment(news_list)
|
||||
|
||||
# 2. 资金流向
|
||||
fund_flow = self.fetch_fund_flow(code, stock.get('market', 'sz'))
|
||||
|
||||
# 3. 构建报告
|
||||
report = f"""📊 <b>{name} ({code}) 深度分析</b>
|
||||
|
||||
💰 <b>价格异动:</b>
|
||||
• 当前: {price_data.get('price', 'N/A')} ({price_data.get('change_pct', 0):+.2f}%)
|
||||
• 触发: {', '.join([a[1] for a in alerts])}
|
||||
|
||||
📰 <b>舆情分析 ({sentiment.get('overall', '未知')}):</b>
|
||||
• 最近新闻: {len(news_list)} 条
|
||||
• 正面: {sentiment.get('positive', 0)} | 负面: {sentiment.get('negative', 0)}
|
||||
"""
|
||||
|
||||
# 添加最新新闻标题
|
||||
if news_list:
|
||||
report += "\n<b>最新动态:</b>\n"
|
||||
for n in news_list[:2]:
|
||||
report += f"• {n.get('title', '无标题')[:30]}...\n"
|
||||
|
||||
# 4. 给出建议
|
||||
suggestion = self._generate_suggestion(sentiment, alerts)
|
||||
report += f"\n💡 <b>Kimi建议:</b>\n{suggestion}"
|
||||
|
||||
return report
|
||||
|
||||
def _generate_suggestion(self, sentiment: Dict, alerts: List) -> str:
|
||||
"""基于数据生成建议"""
|
||||
alert_types = [a[0] for a in alerts]
|
||||
overall = sentiment.get("overall", "中性")
|
||||
|
||||
# 价格下跌 + 舆情偏空 = 谨慎
|
||||
if "below" in alert_types and overall == "偏空":
|
||||
return "⚠️ 价格跌破支撑位,且舆情偏空,建议观察等待,不急于抄底。"
|
||||
|
||||
# 价格下跌 + 舆情偏多 = 可能是机会
|
||||
if "below" in alert_types and overall == "偏多":
|
||||
return "🔍 价格下跌但舆情偏多,可能是情绪错杀,关注是否有反弹机会。"
|
||||
|
||||
# 价格突破 + 舆情偏多 = 确认趋势
|
||||
if "above" in alert_types and overall == "偏多":
|
||||
return "🚀 价格突破且舆情配合,趋势可能延续,可考虑顺势而为。"
|
||||
|
||||
# 大涨
|
||||
if "pct_up" in alert_types:
|
||||
return "📈 短期涨幅较大,注意获利了结风险。"
|
||||
|
||||
# 大跌
|
||||
if "pct_down" in alert_types:
|
||||
return "📉 短期跌幅较大,关注是否超跌反弹,但勿急于抄底。"
|
||||
|
||||
return "⏳ 建议保持观察,等待更明确信号。"
|
||||
|
||||
|
||||
# ========== 测试 ==========
|
||||
if __name__ == '__main__':
|
||||
analyser = StockAnalyser()
|
||||
|
||||
# 测试新闻抓取
|
||||
print("=== 新闻测试 ===")
|
||||
news = analyser.fetch_eastmoney_news("600362", "江西铜业")
|
||||
print(f"获取到 {len(news)} 条新闻")
|
||||
for n in news[:3]:
|
||||
print(f" - {n.get('title', 'N/A')[:40]}...")
|
||||
|
||||
# 测试情感分析
|
||||
print("\n=== 情感分析测试 ===")
|
||||
sentiment = analyser.analyze_sentiment(news)
|
||||
print(f"整体情绪: {sentiment.get('overall')}")
|
||||
print(f"正面: {sentiment.get('positive')}, 负面: {sentiment.get('negative')}")
|
||||
|
||||
# 测试金价关联
|
||||
print("\n=== 宏观关联测试 ===")
|
||||
stocks = [{"code": "600362", "name": "江西铜业"}]
|
||||
corr = analyser.analyze_gold_correlation(2743, stocks)
|
||||
print(corr)
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Stock Monitor 一键启动脚本
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG_DIR="$HOME/.stock_monitor"
|
||||
PID_FILE="$LOG_DIR/monitor.pid"
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
|
||||
echo "⚠️ 监控进程已在运行 (PID: $(cat $PID_FILE))"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 启动 Stock Monitor 后台进程..."
|
||||
mkdir -p "$LOG_DIR"
|
||||
nohup python3 "$SCRIPT_DIR/monitor_daemon.py" > "$LOG_DIR/monitor.log" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
echo "✅ 已启动 (PID: $!)"
|
||||
echo "📋 日志: $LOG_DIR/monitor.log"
|
||||
;;
|
||||
|
||||
stop)
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "🛑 停止监控进程 (PID: $PID)..."
|
||||
kill "$PID"
|
||||
rm "$PID_FILE"
|
||||
echo "✅ 已停止"
|
||||
else
|
||||
echo "⚠️ 进程不存在"
|
||||
rm "$PID_FILE"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ 没有运行中的进程"
|
||||
fi
|
||||
;;
|
||||
|
||||
status)
|
||||
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
|
||||
echo "✅ 监控运行中 (PID: $(cat $PID_FILE))"
|
||||
echo "📋 最近日志:"
|
||||
tail -5 "$LOG_DIR/monitor.log" 2>/dev/null || echo " 暂无日志"
|
||||
else
|
||||
echo "⏹️ 监控未运行"
|
||||
fi
|
||||
;;
|
||||
|
||||
log)
|
||||
tail -f "$LOG_DIR/monitor.log"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Stock Monitor 控制脚本"
|
||||
echo ""
|
||||
echo "用法: ./control.sh [start|stop|status|log]"
|
||||
echo ""
|
||||
echo " start - 启动后台监控"
|
||||
echo " stop - 停止监控"
|
||||
echo " status - 查看状态"
|
||||
echo " log - 查看实时日志"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
自选股监控预警工具 - OpenClaw集成版
|
||||
支持 A股、ETF 及 国际现货黄金 (伦敦金)
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# ============ 配置区 ============
|
||||
|
||||
# 监控列表 - 长期挂机通用配置
|
||||
# 注意: 伦敦金使用新浪hf_XAU接口,价格为 人民币/克 (约4800元/克 = $2740/盎司)
|
||||
#
|
||||
# 预警规则设计原则 (适合长期挂机):
|
||||
# 1. 成本百分比预警: 基于持仓成本设置 ±10%/±15% 预警,比固定价格更合理
|
||||
# 2. 单日涨跌幅预警:
|
||||
# - 个股 ±3%~5% (波动大)
|
||||
# - ETF ±1.5%~2.5% (波动小)
|
||||
# - 黄金 ±2%~3% (24H特殊)
|
||||
# 3. 防骚扰: 同类预警30分钟内只发一次
|
||||
|
||||
# 标的类型定义
|
||||
STOCK_TYPE = {
|
||||
"INDIVIDUAL": "individual", # 个股
|
||||
"ETF": "etf", # ETF
|
||||
"GOLD": "gold" # 黄金/贵金属
|
||||
}
|
||||
|
||||
WATCHLIST = [
|
||||
# ===== 用户自定义监控个股 =====
|
||||
{
|
||||
"code": "000630",
|
||||
"name": "铜陵有色",
|
||||
"market": "sz",
|
||||
"type": "individual",
|
||||
"cost": 7.00, # 参考成本价
|
||||
"alerts": {
|
||||
"change_pct_above": 5.0, # 日内涨超 5% 预警
|
||||
"change_pct_below": -5.0, # 日内跌超 5% 预警
|
||||
"volume_surge": 2.0 # 成交量异动
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "688313",
|
||||
"name": "仕佳光子",
|
||||
"market": "sh",
|
||||
"type": "individual",
|
||||
"cost": 15.00, # 参考成本价(待确认)
|
||||
"alerts": {
|
||||
"change_pct_above": 5.0, # 日内涨超 5% 预警
|
||||
"change_pct_below": -5.0, # 日内跌超 5% 预警
|
||||
"volume_surge": 2.0 # 成交量异动
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "600096",
|
||||
"name": "云天化",
|
||||
"market": "sh",
|
||||
"type": "individual",
|
||||
"cost": 42.00, # 老大确认的实际成本价
|
||||
"alerts": {
|
||||
"change_pct_above": 5.0, # 日内涨超 5% 预警
|
||||
"change_pct_below": -5.0, # 日内跌超 5% 预警
|
||||
"volume_surge": 2.0 # 成交量异动
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "002195",
|
||||
"name": "岩山科技",
|
||||
"market": "sz",
|
||||
"type": "individual",
|
||||
"cost": 10.68, # 200 股,成本 10.68 元
|
||||
"alerts": {
|
||||
"cost_pct_above": 5.0, # 盈利超 5% 快跑 (目标价 ¥11.21)
|
||||
"change_pct_above": 5.0, # 日内涨超 5% 预警
|
||||
"change_pct_below": -5.0, # 日内跌超 5% 预警
|
||||
"volume_surge": 2.0 # 成交量异动
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 智能频率配置
|
||||
SMART_SCHEDULE = {
|
||||
"market_open": {"hours": [(9, 30), (11, 30), (13, 0), (15, 0)], "interval": 300}, # 交易时间: 5分钟
|
||||
"after_hours": {"interval": 1800}, # 收盘后: 30分钟
|
||||
"night": {"hours": [(0, 0), (8, 0)], "interval": 3600}, # 凌晨: 1小时(仅伦敦金)
|
||||
}
|
||||
|
||||
# ============ 核心代码 ============
|
||||
|
||||
class StockAlert:
|
||||
def __init__(self):
|
||||
self.prev_data = {}
|
||||
self.alert_log = []
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"User-Agent": "Mozilla/5.0"})
|
||||
|
||||
def should_run_now(self):
|
||||
"""智能频率控制: 判断当前是否应该执行监控 (基于北京时间)"""
|
||||
# 服务器在纽约(EST),中国股市用北京时间(CST = EST + 13小时)
|
||||
from datetime import timedelta
|
||||
now = datetime.now() + timedelta(hours=13) # 转换成北京时间
|
||||
hour, minute = now.hour, now.minute
|
||||
time_val = hour * 100 + minute
|
||||
weekday = now.weekday()
|
||||
|
||||
# 周末只监控伦敦金
|
||||
if weekday >= 5: # 周六日
|
||||
return {"run": True, "mode": "weekend", "stocks": [s for s in WATCHLIST if s['market'] == 'fx']}
|
||||
|
||||
# 交易时间 (9:30-11:30, 13:00-15:00)
|
||||
morning_session = 930 <= time_val <= 1130
|
||||
afternoon_session = 1300 <= time_val <= 1500
|
||||
|
||||
if morning_session or afternoon_session:
|
||||
return {"run": True, "mode": "market", "stocks": WATCHLIST, "interval": 300}
|
||||
|
||||
# 午休 (11:30-13:00)
|
||||
if 1130 < time_val < 1300:
|
||||
return {"run": True, "mode": "lunch", "stocks": WATCHLIST, "interval": 600} # 10分钟
|
||||
|
||||
# 收盘后 (15:00-24:00)
|
||||
if 1500 <= time_val <= 2359:
|
||||
return {"run": True, "mode": "after_hours", "stocks": WATCHLIST, "interval": 1800} # 30分钟
|
||||
|
||||
# 凌晨 (0:00-9:30)
|
||||
if 0 <= time_val < 930:
|
||||
return {"run": True, "mode": "night", "stocks": [s for s in WATCHLIST if s['market'] == 'fx'], "interval": 3600} # 1小时
|
||||
|
||||
return {"run": False}
|
||||
|
||||
def fetch_eastmoney_kline(self, symbol, market):
|
||||
"""获取最新日K线数据 (收盘后也能获取收盘价)"""
|
||||
secid = f"{market}.{symbol}"
|
||||
url = "https://push2his.eastmoney.com/api/qt/stock/kline/get"
|
||||
params = {
|
||||
'secid': secid,
|
||||
'fields1': 'f1,f2,f3,f4,f5,f6',
|
||||
'fields2': 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
|
||||
'klt': '101', # 日线
|
||||
'fqt': '0',
|
||||
'end': '20500101',
|
||||
'lmt': '2' # 取最近2天,用于计算涨跌幅
|
||||
}
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=10)
|
||||
data = resp.json()
|
||||
klines = data.get('data', {}).get('klines', [])
|
||||
if len(klines) >= 1:
|
||||
# 格式: 日期,开盘,收盘,最高,最低,成交量,成交额,振幅,涨跌幅,涨跌额,换手率
|
||||
today = klines[-1].split(',')
|
||||
prev_close = float(today[2]) # 昨收
|
||||
if len(klines) >= 2:
|
||||
prev_close = float(klines[-2].split(',')[2]) # 前一天收盘
|
||||
return {
|
||||
'name': data.get('data', {}).get('name', symbol),
|
||||
'price': float(today[2]), # 收盘
|
||||
'prev_close': prev_close,
|
||||
'volume': int(float(today[5])),
|
||||
'amount': float(today[6]),
|
||||
'date': today[0],
|
||||
'time': '15:00:00'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"东财K线获取失败 {symbol}: {e}")
|
||||
return None
|
||||
|
||||
def fetch_volume_ma5(self, symbol, market):
|
||||
"""获取5日平均成交量"""
|
||||
secid = f"{market}.{symbol}"
|
||||
url = "https://push2his.eastmoney.com/api/qt/stock/kline/get"
|
||||
params = {
|
||||
'secid': secid,
|
||||
'fields1': 'f1,f2,f3,f4,f5,f6',
|
||||
'fields2': 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
|
||||
'klt': '101',
|
||||
'fqt': '0',
|
||||
'end': '20500101',
|
||||
'lmt': '6' # 取最近6天(今天+前5天)
|
||||
}
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=10)
|
||||
data = resp.json()
|
||||
klines = data.get('data', {}).get('klines', [])
|
||||
if len(klines) >= 2:
|
||||
# 计算前5日平均成交量(不含今天)
|
||||
volumes = []
|
||||
for k in klines[:-1]: # 排除最后一天(今天)
|
||||
p = k.split(',')
|
||||
volumes.append(float(p[5])) # 成交量
|
||||
return sum(volumes) / len(volumes) if volumes else 0
|
||||
except Exception as e:
|
||||
print(f"获取均量失败 {symbol}: {e}")
|
||||
return 0
|
||||
|
||||
def fetch_ma_data(self, symbol, market):
|
||||
"""获取均线数据 (MA5, MA10, MA20) 和 RSI"""
|
||||
secid = f"{market}.{symbol}"
|
||||
url = "https://push2his.eastmoney.com/api/qt/stock/kline/get"
|
||||
params = {
|
||||
'secid': secid,
|
||||
'fields1': 'f1,f2,f3,f4,f5,f6',
|
||||
'fields2': 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
|
||||
'klt': '101',
|
||||
'fqt': '0',
|
||||
'end': '20500101',
|
||||
'lmt': '30' # 取最近30天计算MA20和RSI
|
||||
}
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=10)
|
||||
data = resp.json()
|
||||
klines = data.get('data', {}).get('klines', [])
|
||||
if len(klines) >= 20:
|
||||
closes = []
|
||||
for k in klines:
|
||||
p = k.split(',')
|
||||
closes.append(float(p[2])) # 收盘价
|
||||
|
||||
# 计算均线
|
||||
ma5 = sum(closes[-5:]) / 5
|
||||
ma10 = sum(closes[-10:]) / 10
|
||||
ma20 = sum(closes[-20:]) / 20
|
||||
|
||||
# 判断均线趋势
|
||||
prev_ma5 = sum(closes[-6:-1]) / 5
|
||||
prev_ma10 = sum(closes[-11:-1]) / 10
|
||||
|
||||
# 计算RSI(14)
|
||||
rsi = self._calculate_rsi(closes, 14)
|
||||
|
||||
return {
|
||||
'MA5': ma5,
|
||||
'MA10': ma10,
|
||||
'MA20': ma20,
|
||||
'MA5_trend': 'up' if ma5 > prev_ma5 else 'down',
|
||||
'MA10_trend': 'up' if ma10 > prev_ma10 else 'down',
|
||||
'golden_cross': prev_ma5 <= prev_ma10 and ma5 > ma10,
|
||||
'death_cross': prev_ma5 >= prev_ma10 and ma5 < ma10,
|
||||
'RSI': rsi,
|
||||
'RSI_overbought': rsi > 70 if rsi else False,
|
||||
'RSI_oversold': rsi < 30 if rsi else False
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取均线失败 {symbol}: {e}")
|
||||
return None
|
||||
|
||||
def _calculate_rsi(self, closes, period=14):
|
||||
"""计算RSI指标"""
|
||||
if len(closes) < period + 1:
|
||||
return None
|
||||
|
||||
gains = []
|
||||
losses = []
|
||||
|
||||
for i in range(1, period + 1):
|
||||
change = closes[-i] - closes[-i-1]
|
||||
if change > 0:
|
||||
gains.append(change)
|
||||
losses.append(0)
|
||||
else:
|
||||
gains.append(0)
|
||||
losses.append(abs(change))
|
||||
|
||||
avg_gain = sum(gains) / period
|
||||
avg_loss = sum(losses) / period
|
||||
|
||||
if avg_loss == 0:
|
||||
return 100
|
||||
|
||||
rs = avg_gain / avg_loss
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
return round(rsi, 2)
|
||||
|
||||
def fetch_tencent_realtime(self, stocks):
|
||||
"""获取实时行情 (腾讯财经接口,更稳定)"""
|
||||
stock_list = [s for s in stocks if s['market'] != 'fx']
|
||||
fx_list = [s for s in stocks if s['market'] == 'fx']
|
||||
results = {}
|
||||
|
||||
# 1. A 股/ETF - 腾讯财经接口
|
||||
if stock_list:
|
||||
for stock in stock_list:
|
||||
code = f"{stock['market']}{stock['code']}"
|
||||
url = f"http://qt.gtimg.cn/q={code}"
|
||||
try:
|
||||
resp = self.session.get(url, timeout=5)
|
||||
resp.encoding = 'gbk'
|
||||
data = resp.text.strip()
|
||||
if '="' not in data:
|
||||
continue
|
||||
parts = data.strip('"').split('~')
|
||||
start_idx = 0
|
||||
for j, p in enumerate(parts):
|
||||
if p.isdigit() and len(p) == 2:
|
||||
start_idx = j
|
||||
break
|
||||
if len(parts) > start_idx + 20:
|
||||
current = float(parts[start_idx+3])
|
||||
prev_close = float(parts[start_idx+4])
|
||||
open_p = float(parts[start_idx+5])
|
||||
change = 0
|
||||
change_pct = 0
|
||||
high = 0
|
||||
low = 0
|
||||
for j in range(len(parts)-5):
|
||||
try:
|
||||
if parts[j].startswith('-') and '.' in parts[j]:
|
||||
v1 = float(parts[j])
|
||||
v2 = float(parts[j+1])
|
||||
if -10 < v2 < 10:
|
||||
change = v1
|
||||
change_pct = v2
|
||||
high = float(parts[j+2])
|
||||
low = float(parts[j+3])
|
||||
break
|
||||
except:
|
||||
continue
|
||||
results[stock['code']] = {
|
||||
'name': parts[start_idx+1],
|
||||
'price': current,
|
||||
'prev_close': prev_close,
|
||||
'open': open_p,
|
||||
'high': high,
|
||||
'low': low,
|
||||
'volume': 0,
|
||||
'amount': 0,
|
||||
'date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'time': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
print(f"✅ {stock['name']} ({stock['code']}): ¥{current:.2f}")
|
||||
except Exception as e:
|
||||
print(f"腾讯行情获取失败 {stock['code']}: {e}")
|
||||
|
||||
# 2. 伦敦金 (保留原逻辑)
|
||||
if fx_list:
|
||||
url = "https://hq.sinajs.cn/list=hf_XAU"
|
||||
try:
|
||||
resp = self.session.get(url, timeout=5)
|
||||
line = resp.text.strip()
|
||||
if '"' in line:
|
||||
data_str = line[line.index('"')+1 : line.rindex('"')]
|
||||
p = data_str.split(',')
|
||||
if len(p) >= 13:
|
||||
price = float(p[0])
|
||||
results['XAU'] = {
|
||||
'name': '伦敦金',
|
||||
'price': price,
|
||||
'prev_close': float(p[7]),
|
||||
'volume': 0, 'amount': 0,
|
||||
'date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'time': p[6]
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"伦敦金获取失败:{e}")
|
||||
|
||||
return results
|
||||
|
||||
def record_alert(self, code, icon):
|
||||
"""记录预警日志 (简化版)"""
|
||||
# 简单打印日志,实际可以写入文件
|
||||
print(f" 📝 预警记录:{code} - {icon}")
|
||||
|
||||
def check_alerts(self, stock_config, data):
|
||||
"""检查预警条件 (返回格式:[(icon, text), ...], level)"""
|
||||
alerts = []
|
||||
code = stock_config['code']
|
||||
cfg = stock_config.get('alerts', {})
|
||||
cost = stock_config.get('cost', 0)
|
||||
|
||||
price = data['price']
|
||||
prev_close = data['prev_close']
|
||||
change_pct = ((price - prev_close) / prev_close) * 100 if prev_close else 0
|
||||
|
||||
# 1. 成本百分比预警
|
||||
if cost > 0:
|
||||
if 'cost_pct_above' in cfg:
|
||||
threshold = cost * (1 + cfg['cost_pct_above'] / 100)
|
||||
if price >= threshold:
|
||||
alerts.append(("🎯", f"盈利 {cfg['cost_pct_above']}% (目标价 ¥{threshold:.2f})"))
|
||||
|
||||
if 'cost_pct_below' in cfg:
|
||||
threshold = cost * (1 + cfg['cost_pct_below'] / 100)
|
||||
if price <= threshold:
|
||||
alerts.append(("📉", f"亏损 {abs(cfg['cost_pct_below'])}% (止损价 ¥{threshold:.2f})"))
|
||||
|
||||
# 2. 日内涨跌幅预警
|
||||
if 'change_pct_above' in cfg and change_pct >= cfg['change_pct_above']:
|
||||
alerts.append(("📈", f"日内大涨 {change_pct:.2f}% (阈值 {cfg['change_pct_above']}%)"))
|
||||
|
||||
if 'change_pct_below' in cfg and change_pct <= cfg['change_pct_below']:
|
||||
alerts.append(("📉", f"日内大跌 {change_pct:.2f}% (阈值 {cfg['change_pct_below']}%)"))
|
||||
|
||||
# 确定预警级别
|
||||
if len(alerts) >= 3:
|
||||
level = "critical"
|
||||
elif len(alerts) >= 2:
|
||||
level = "warning"
|
||||
elif len(alerts) >= 1:
|
||||
level = "info"
|
||||
else:
|
||||
level = "none"
|
||||
|
||||
return alerts, level
|
||||
|
||||
def fetch_news(self, symbol):
|
||||
"""抓取个股最近新闻 (新浪/东财聚合) - 简化版"""
|
||||
try:
|
||||
# 使用东财个股新闻API
|
||||
url = f"https://emweb.securities.eastmoney.com/PC_HSF10/CompanySurvey/CompanySurveyAjax"
|
||||
params = {"code": symbol}
|
||||
resp = self.session.get(url, params=params, timeout=5)
|
||||
return ["新闻模块已就绪 (市场收盘中)"]
|
||||
except:
|
||||
return []
|
||||
|
||||
def run_once(self, smart_mode=True):
|
||||
"""执行监控 (支持智能频率)"""
|
||||
if smart_mode:
|
||||
schedule = self.should_run_now()
|
||||
if not schedule.get("run"):
|
||||
return []
|
||||
|
||||
stocks_to_check = schedule.get("stocks", WATCHLIST)
|
||||
mode = schedule.get("mode", "normal")
|
||||
|
||||
# 只在特定模式打印日志
|
||||
if mode in ["market", "weekend"]:
|
||||
print(f"[{datetime.now().strftime('%H:%M')}] {mode}模式扫描 {len(stocks_to_check)} 只标的...")
|
||||
else:
|
||||
stocks_to_check = WATCHLIST
|
||||
|
||||
data_map = self.fetch_tencent_realtime(stocks_to_check)
|
||||
triggered = []
|
||||
|
||||
for stock in stocks_to_check:
|
||||
code = stock['code']
|
||||
if code not in data_map: continue
|
||||
|
||||
data = data_map[code]
|
||||
|
||||
# 数据有效性检查
|
||||
if data['price'] <= 0 or data['prev_close'] <= 0:
|
||||
continue
|
||||
|
||||
alerts, level = self.check_alerts(stock, data)
|
||||
|
||||
if alerts:
|
||||
change_pct = (data['price'] - data['prev_close']) / data['prev_close'] * 100 if data['prev_close'] else 0
|
||||
|
||||
# 中国习惯: 红色=上涨, 绿色=下跌
|
||||
if change_pct > 0:
|
||||
color_emoji = "🔴" # 红涨
|
||||
elif change_pct < 0:
|
||||
color_emoji = "🟢" # 绿跌
|
||||
else:
|
||||
color_emoji = "⚪"
|
||||
|
||||
# 预警级别标识
|
||||
level_icons = {
|
||||
"critical": "🚨", # 紧急
|
||||
"warning": "⚠️", # 警告
|
||||
"info": "📢" # 提醒
|
||||
}
|
||||
level_icon = level_icons.get(level, "📢")
|
||||
level_text = {"critical": "【紧急】", "warning": "【警告】", "info": "【提醒】"}.get(level, "")
|
||||
|
||||
msg = f"<b>{level_icon} {level_text}{color_emoji} {stock['name']} ({code})</b>\n"
|
||||
msg += f"━━━━━━━━━━━━━━━━━━━━\n"
|
||||
msg += f"💰 当前价格: <b>{data['price']:.2f}</b> ({change_pct:+.2f}%)\n"
|
||||
|
||||
# 显示持仓盈亏
|
||||
cost = stock.get('cost', 0)
|
||||
if cost > 0:
|
||||
cost_change = (data['price'] - cost) / cost * 100
|
||||
profit_icon = "🔴+" if cost_change > 0 else "🟢"
|
||||
msg += f"📊 持仓成本: ¥{cost:.2f} | 盈亏: {profit_icon}{cost_change:.2f}%\n"
|
||||
|
||||
msg += f"\n🎯 触发预警 ({len(alerts)}项):\n"
|
||||
for _, text in alerts:
|
||||
msg += f" • {text}\n"
|
||||
self.record_alert(code, _)
|
||||
|
||||
# Pro版:集成智能分析
|
||||
try:
|
||||
from analyser import StockAnalyser
|
||||
analyser = StockAnalyser()
|
||||
insight = analyser.generate_insight(stock, {
|
||||
'price': data['price'],
|
||||
'change_pct': change_pct
|
||||
}, alerts)
|
||||
msg += f"\n{insight}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
triggered.append(msg)
|
||||
|
||||
return triggered
|
||||
|
||||
if __name__ == '__main__':
|
||||
monitor = StockAlert()
|
||||
for alert in monitor.run_once():
|
||||
print(alert)
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stock Monitor Daemon - 后台常驻进程
|
||||
自动运行监控,智能控制频率,支持 graceful shutdown
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# 设置日志
|
||||
log_dir = Path.home() / ".stock_monitor"
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_dir / "monitor.log"),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 导入监控类
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from monitor import StockAlert, WATCHLIST
|
||||
|
||||
class MonitorDaemon:
|
||||
def __init__(self):
|
||||
self.monitor = StockAlert()
|
||||
self.running = True
|
||||
self.last_run_time = 0
|
||||
|
||||
# 设置信号处理
|
||||
signal.signal(signal.SIGTERM, self.handle_shutdown)
|
||||
signal.signal(signal.SIGINT, self.handle_shutdown)
|
||||
|
||||
def handle_shutdown(self, signum, frame):
|
||||
"""优雅退出"""
|
||||
logger.info(f"收到信号 {signum},正在关闭...")
|
||||
self.running = False
|
||||
|
||||
def get_sleep_interval(self):
|
||||
"""根据当前时间获取睡眠间隔"""
|
||||
schedule = self.monitor.should_run_now()
|
||||
if not schedule.get("run"):
|
||||
# 如果当前不需要运行,计算到下次运行的时间
|
||||
now = datetime.now()
|
||||
hour = now.hour
|
||||
|
||||
# 凌晨时段,1小时后检查
|
||||
if 0 <= hour < 9:
|
||||
return 3600
|
||||
return 300 # 默认5分钟
|
||||
|
||||
return schedule.get("interval", 300)
|
||||
|
||||
def run(self):
|
||||
"""主循环"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("🚀 Stock Monitor Daemon 启动")
|
||||
logger.info(f"📋 监控标的: {len(WATCHLIST)} 只")
|
||||
logger.info("=" * 60)
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# 检查是否应该执行
|
||||
schedule = self.monitor.should_run_now()
|
||||
|
||||
if schedule.get("run"):
|
||||
mode = schedule.get("mode", "normal")
|
||||
stocks_count = len(schedule.get("stocks", []))
|
||||
logger.info(f"[{mode}] 扫描 {stocks_count} 只标的...")
|
||||
|
||||
# 执行监控
|
||||
alerts = self.monitor.run_once(smart_mode=False) # 已经判断过了
|
||||
|
||||
if alerts:
|
||||
logger.info(f"⚠️ 触发 {len(alerts)} 条预警")
|
||||
# 这里会通过 message 工具发送通知
|
||||
else:
|
||||
logger.debug("✅ 无预警")
|
||||
|
||||
self.last_run_time = time.time()
|
||||
|
||||
# 计算睡眠间隔
|
||||
sleep_interval = self.get_sleep_interval()
|
||||
logger.debug(f"下次检查: {sleep_interval} 秒后")
|
||||
|
||||
# 分段睡眠,方便及时响应退出信号
|
||||
slept = 0
|
||||
while slept < sleep_interval and self.running:
|
||||
time.sleep(1)
|
||||
slept += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"运行出错: {e}", exc_info=True)
|
||||
time.sleep(60) # 出错后等待1分钟重试
|
||||
|
||||
logger.info("👋 Daemon 已停止")
|
||||
|
||||
if __name__ == '__main__':
|
||||
daemon = MonitorDaemon()
|
||||
daemon.run()
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
铜陵有色专项监控 - 实时股价监控
|
||||
使用腾讯财经接口,确保数据准确性
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# ============ 配置 ============
|
||||
|
||||
WATCHLIST = [
|
||||
{
|
||||
"code": "000630",
|
||||
"name": "铜陵有色",
|
||||
"market": "sz",
|
||||
"alerts": {
|
||||
"change_pct_above": 5.0, # 涨超 5% 预警
|
||||
"change_pct_below": -5.0, # 跌超 5% 预警
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 企业微信配置
|
||||
WECOM_BOT_ID = "aibwl3AhnzfRPRTEZvBVwlB-vRD33yJdUVX"
|
||||
WECOM_SECRET = "1eUB2yd2R7bll6VjBQ5OGptJj2YiwutMUmACe9UGC7k"
|
||||
ALLOW_USERS = ["HouHuan", "WanMeiShengHuo", "XinNingXianGuoNaiChaKaFeiZhaJiHa"]
|
||||
|
||||
# ============ 核心功能 ============
|
||||
|
||||
def fetch_tencent_price(stock):
|
||||
"""腾讯财经接口获取实时股价"""
|
||||
code = f"{stock['market']}{stock['code']}"
|
||||
url = f"http://qt.gtimg.cn/q={code}"
|
||||
|
||||
try:
|
||||
resp = requests.get(url, timeout=5)
|
||||
resp.encoding = 'gbk'
|
||||
data = resp.text.strip()
|
||||
|
||||
# 解析:v_sz000630="51~名称~代码~现价~昨收~今开~...
|
||||
if '="' not in data:
|
||||
return None
|
||||
|
||||
parts = data.strip('"').split('~')
|
||||
|
||||
# 找到前缀位置
|
||||
start_idx = 0
|
||||
for i, p in enumerate(parts):
|
||||
if p.isdigit() and len(p) == 2:
|
||||
start_idx = i
|
||||
break
|
||||
|
||||
if len(parts) <= start_idx + 20:
|
||||
return None
|
||||
|
||||
# 正确解析索引
|
||||
name = parts[start_idx+1]
|
||||
code = parts[start_idx+2]
|
||||
current = float(parts[start_idx+3]) # 现价
|
||||
prev_close = float(parts[start_idx+4]) # 昨收
|
||||
open_p = float(parts[start_idx+5]) # 今开
|
||||
|
||||
# 找涨跌额、涨跌幅、最高、最低
|
||||
change = 0
|
||||
change_pct = 0
|
||||
high = 0
|
||||
low = 0
|
||||
|
||||
for i in range(len(parts)-5):
|
||||
try:
|
||||
if parts[i].startswith('-') and '.' in parts[i]:
|
||||
v1 = float(parts[i])
|
||||
v2 = float(parts[i+1])
|
||||
if -10 < v2 < 10: # 涨跌幅一般在 -10~10 之间
|
||||
change = v1
|
||||
change_pct = v2
|
||||
high = float(parts[i+2])
|
||||
low = float(parts[i+3])
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'code': code,
|
||||
'price': current,
|
||||
'prev_close': prev_close,
|
||||
'open': open_p,
|
||||
'high': high,
|
||||
'low': low,
|
||||
'change': change,
|
||||
'change_pct': change_pct,
|
||||
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"❌ 数据获取失败:{e}")
|
||||
return None
|
||||
|
||||
def check_alerts(stock, data):
|
||||
"""检查是否触发预警"""
|
||||
alerts = []
|
||||
cfg = stock.get('alerts', {})
|
||||
|
||||
change_pct = data['change_pct']
|
||||
|
||||
# 检查涨跌幅预警
|
||||
if 'change_pct_above' in cfg and change_pct >= cfg['change_pct_above']:
|
||||
alerts.append({
|
||||
'type': '🔴 大涨',
|
||||
'condition': f"涨幅超过 {cfg['change_pct_above']}%",
|
||||
'value': f"{change_pct:.2f}%"
|
||||
})
|
||||
|
||||
if 'change_pct_below' in cfg and change_pct <= cfg['change_pct_below']:
|
||||
alerts.append({
|
||||
'type': '🟢 大跌',
|
||||
'condition': f"跌幅超过 {abs(cfg['change_pct_below'])}%",
|
||||
'value': f"{change_pct:.2f}%"
|
||||
})
|
||||
|
||||
return alerts
|
||||
|
||||
def send_wecom_alert(stock_name, data, alerts):
|
||||
"""发送企业微信预警消息"""
|
||||
from gateway import send_message
|
||||
|
||||
# 组合消息
|
||||
alert_text = "\n".join([f" • {a['type']}: {a['condition']} ({a['value']})" for a in alerts])
|
||||
|
||||
message = f"""🚨【股价预警】{stock_name} ({data['code']})
|
||||
━━━━━━━━━━━━━━━━━━━━
|
||||
💰 当前价格:¥{data['price']:.2f} ({data['change_pct']:+.2f}%)
|
||||
|
||||
🎯 触发预警:
|
||||
{alert_text}
|
||||
|
||||
📊 详细数据:
|
||||
• 昨收:¥{data['prev_close']:.2f}
|
||||
• 今开:¥{data['open']:.2f}
|
||||
• 最高:¥{data['high']:.2f}
|
||||
• 最低:¥{data['low']:.2f}
|
||||
|
||||
⏰ 数据时间:{data['time']}
|
||||
|
||||
💡 建议关注后续走势,注意风险控制。
|
||||
"""
|
||||
|
||||
# 发送给所有允许的用户
|
||||
for user in ALLOW_USERS:
|
||||
try:
|
||||
send_message(
|
||||
channel="wecom",
|
||||
target=user,
|
||||
message=message
|
||||
)
|
||||
print(f"✅ 预警已发送给用户:{user}")
|
||||
except Exception as e:
|
||||
print(f"❌ 发送失败 {user}: {e}")
|
||||
|
||||
def monitor_once():
|
||||
"""执行一次监控"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"📊 铜陵有色监控 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
for stock in WATCHLIST:
|
||||
data = fetch_tencent_price(stock)
|
||||
|
||||
if not data:
|
||||
print(f"❌ {stock['name']} 数据获取失败")
|
||||
continue
|
||||
|
||||
print(f"✅ {stock['name']} ({stock['code']})")
|
||||
print(f" 现价:¥{data['price']:.2f} ({data['change_pct']:+.2f}%)")
|
||||
print(f" 昨收:¥{data['prev_close']:.2f}")
|
||||
print(f" 涨跌:¥{data['change']:.2f}")
|
||||
|
||||
# 检查预警
|
||||
alerts = check_alerts(stock, data)
|
||||
|
||||
if alerts:
|
||||
print(f" 🚨 触发 {len(alerts)} 条预警!")
|
||||
send_wecom_alert(stock['name'], data, alerts)
|
||||
else:
|
||||
print(f" ✅ 正常波动范围")
|
||||
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# ============ 主程序 ============
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 单次执行
|
||||
monitor_once()
|
||||
Reference in New Issue
Block a user