#!/usr/bin/env python3 """ 小唯消息面情感评分 — 东方财富公告 + 关键词情感词典 ================================================== 抓取关注股票最新公告标题,用情感词典打分。 评分规则: 利空词(减持/亏损/违规/立案/质押/诉讼/退市/警示/下跌/处罚)→ -1 利好词(增持/回购/分红/中标/增长/突破/合作/扩产/盈利/上调/签约)→ +1 中性 → 0 综合多个公告后 clip 到 [-1, 1] 用法: python3 stock_sentiment.py # 输出情感评分 python3 stock_sentiment.py --json # JSON 输出(供矛盾周报引用) """ import json, sys, subprocess, shlex, os, re from datetime import datetime from pathlib import Path OUTPUT = Path.home() / ".hermes" / "stock_backtest" OUTPUT.mkdir(exist_ok=True) # 关注股票(代码, 名称, 行业) WATCHED = [ ("000858", "五粮液", "白酒"), ("600519", "贵州茅台", "白酒"), ("000568", "泸州老窖", "白酒"), ("002304", "洋河股份", "白酒"), ("600036", "招商银行", "银行"), ("601318", "中国平安", "保险"), ("000001", "平安银行", "银行"), ("300750", "宁德时代", "新能源"), ("002594", "比亚迪", "新能源"), ("002415", "海康威视", "科技"), ("601088", "中国神华", "煤炭"), ("688981", "中芯国际", "半导体"), ("600030", "中信证券", "证券"), ("000333", "美的集团", "家电"), ] # 情感词典 NEGATIVE_WORDS = [ "减持", "亏损", "违规", "立案", "质押", "诉讼", "退市", "警示", "下跌", "处罚", "风险", "终止", "暂停", "下滑", "恶化", "逾期", "冻结", "查封", "调查", "降级", "下调", "失败", "延期", "变卖", ] POSITIVE_WORDS = [ "增持", "回购", "分红", "中标", "增长", "突破", "合作", "扩产", "盈利", "上调", "签约", "创新高", "预增", "扭亏", "获批", "落地", "推出", "发布", "投资", "签订", "完成", "超预期", "翻倍", "新签订单", ] # 中性词(公告常见但无方向性) NEUTRAL_WORDS = ["会议", "报告", "公告", "章程", "制度", "通知", "更正", "说明"] def get_url(url, timeout=8): env = dict(os.environ) for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]: env.pop(k, None) cmd = f"curl -s --max-time {timeout} --compressed {shlex.quote(url)}" try: r = subprocess.run(cmd, shell=True, capture_output=True, timeout=timeout + 2, env=env) return r.stdout.decode("utf-8", errors="ignore") except Exception: return "" def get_announcements(code, limit=5): """东方财富个股公告(标题列表)""" url = (f"https://np-anotice-stock.eastmoney.com/api/security/ann" f"?sr=-1&page_size={limit}&page_index=1&ann_type=A" f"&client_source=web&stock_list={code}&f_node=0&s_node=0") text = get_url(url) if not text or len(text) < 50: return [] try: data = json.loads(text) items = data.get("data", {}).get("list", []) titles = [] for item in items: title = item.get("title", "").strip() # 清理 HTML 标签 title = re.sub(r"<[^>]+>", "", title) if title: titles.append(title) return titles except Exception: return [] def score_title(title): """单条标题情感评分:返回 (score, matched)""" pos_hits = [w for w in POSITIVE_WORDS if w in title] neg_hits = [w for w in NEGATIVE_WORDS if w in title] if pos_hits and not neg_hits: return 1, pos_hits if neg_hits and not pos_hits: return -1, neg_hits if pos_hits and neg_hits: return 0, pos_hits + neg_hits # 混合中性 return 0, [] def scan_sentiment(): """扫描所有关注股票,返回情感评分""" results = [] print("=" * 60) print(f"小唯消息面情感扫描 {datetime.now().strftime('%Y-%m-%d %H:%M')}") print("=" * 60) for code, name, industry in WATCHED: titles = get_announcements(code, limit=5) if not titles: print(f" ⚠️ {name}: 无公告数据") results.append({"code": code, "name": name, "industry": industry, "score": 0, "titles": [], "reasons": ["无公告"]}) continue scores = [score_title(t) for t in titles] # 有实质方向的最新公告优先(取最近3条的平均) recent = [s for s, _ in scores[:3]] avg = sum(recent) / len(recent) if recent else 0 final = max(-1, min(1, avg)) hits = [] for t, (s, words) in zip(titles[:3], scores[:3]): if words: emoji = "🔴" if s < 0 else ("🟢" if s > 0 else "⚪") hits.append(f"{emoji}{t[:35]}") results.append({"code": code, "name": name, "industry": industry, "score": final, "titles": titles[:3], "hits": hits, "reasons": hits}) emoji = "🔴" if final < 0 else ("🟢" if final > 0 else "⚪") print(f" {emoji} {name}({code}) [{industry}] 情感{final:+.0f}") # 汇总:全市场情感(消息面评分) valid = [r for r in results if r.get("titles")] if valid: avg_all = sum(r["score"] for r in valid) / len(valid) message = max(-1, min(1, avg_all)) else: message = 0 print(f"\n📊 市场消息面综合评分: {message:+.0f}") out = OUTPUT / "sentiment_scan.json" with open(out, "w", encoding="utf-8") as f: json.dump({"generated": datetime.now().strftime("%Y-%m-%d %H:%M"), "message_score": message, "stocks": results}, f, ensure_ascii=False, indent=2) print(f"📁 已保存: {out}") if "--json" in sys.argv: return message return results if __name__ == "__main__": scan_sentiment()