163 lines
5.8 KiB
Python
163 lines
5.8 KiB
Python
#!/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()
|