xiaowei-system/scripts/stock_sentiment.py

163 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()