股票第六波: 宏观/消息面真实接入四维评分, 动态仓位(0.3-1.0), 数据刷新cron

This commit is contained in:
小唯 A06 2026-08-02 00:10:47 +08:00
parent 53f5d24c70
commit dfb8be201d
5 changed files with 432 additions and 9 deletions

View File

@ -159,10 +159,44 @@ def load_fundamental_scan():
return None
def load_macro_score():
"""读取宏观评分结果stock_macro.py 生成,--json 模式)"""
f = os.path.join(OUTPUT_DIR, "macro_score.json")
if not os.path.exists(f):
return None
try:
with open(f) as fp:
return json.load(fp)
except Exception:
return None
def load_sentiment_scan():
"""读取消息面情感扫描结果stock_sentiment.py 生成)"""
f = os.path.join(OUTPUT_DIR, "sentiment_scan.json")
if not os.path.exists(f):
return None
try:
with open(f) as fp:
return json.load(fp)
except Exception:
return None
def build_verdict(signals):
"""根据真实数据计算四维评分2026-08-01 增强:基本面接入真实数据)"""
"""根据真实数据计算四维评分2026-08-02 增强:宏观接入真实数据)"""
now = datetime.now()
macro = -1 # 宏观承压(保持判断,可后续接入宏观指标)
# 宏观面真实数据stock_macro.py 生成)
macro = 0
macro_reasons = []
macro_data = load_macro_score()
if macro_data:
macro = macro_data.get("macro", 0)
macro_reasons = macro_data.get("reasons", [])
else:
macro = -1 # 回退:无数据时保持保守判断
macro_reasons = ["宏观数据缺失,保守-1"]
# 基本面:取关注股票 PE/PB 平均,判断整体估值
fundamental = 0
@ -192,8 +226,15 @@ def build_verdict(signals):
elif below_count > above_count:
technical = -1
# 消息面:保持中性(暂无新闻接入
# 消息面:真实数据stock_sentiment.py 生成
message = 0
msg_reasons = []
senti = load_sentiment_scan()
if senti:
raw = senti.get("message_score", 0)
message = 1 if raw > 0.3 else (-1 if raw < -0.3 else 0)
if message != 0:
msg_reasons.append(f"市场消息面{raw:+.1f}")
total = macro + fundamental + technical + message
@ -212,7 +253,9 @@ def build_verdict(signals):
"message": message,
"total": total,
"decision": decision,
"macro_reasons": macro_reasons,
"fundamental_reasons": fund_reasons,
"message_reasons": msg_reasons,
}
def load_industry_momentum():
@ -236,13 +279,19 @@ def print_report(c):
print("=" * 50)
print("\n【四维评分】")
print(f" 宏观面: {verdict['macro']}(系统性压力)")
macro_line = "(承压)" if verdict['macro'] < 0 else ("(友好)" if verdict['macro'] > 0 else "(中性)")
print(f" 宏观面: {verdict['macro']}{macro_line}")
if verdict.get("macro_reasons"):
print(f" {' '.join(verdict['macro_reasons'][:2])}")
fund_line = "(稳健)" if verdict['fundamental'] > 0 else ("(偏弱)" if verdict['fundamental'] < 0 else "(中性)")
print(f" 基本面: {verdict['fundamental']}{fund_line}")
if verdict.get("fundamental_reasons"):
print(f" {' '.join(verdict['fundamental_reasons'])}")
print(f" 技术面: {verdict['technical']}(空头排列)")
print(f" 消息面: {verdict['message']}(中性)")
msg_line = "(利好)" if verdict['message'] > 0 else ("(利空)" if verdict['message'] < 0 else "(中性)")
print(f" 消息面: {verdict['message']}{msg_line}")
if verdict.get("message_reasons"):
print(f" {' '.join(verdict['message_reasons'])}")
print(f" 综合评分: {verdict['total']} → 决策:{verdict['decision']}")
# 行业动量版块2026-08-01 新增)

18
scripts/stock_data_refresh.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
# 股票数据刷新:宏观 + 基本面 + 情感 + 行业 + 因子 全量扫描
# 被 cron 调用(周一矛盾周报前 + 每日收盘后)
cd ~/.hermes/scripts || exit 1
echo "=== 宏观评分 ==="
python3 stock_macro.py --json 2>&1 | tail -2
echo ""
echo "=== 基本面扫描 ==="
python3 stock_fundamental.py 2>&1 | tail -3
echo ""
echo "=== 消息面情感 ==="
python3 stock_sentiment.py 2>&1 | tail -3
echo ""
echo "✅ 数据刷新完成 $(date '+%Y-%m-%d %H:%M')"

162
scripts/stock_macro.py Normal file
View File

@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
小唯宏观指标自动化 真实数据驱动四维评分的宏观维度
==================================================
获取上证/沪深300/创业板/原油计算宏观趋势评分
评分规则
上证 20日涨幅 > 0 +1大盘向上
上证 20日涨幅 < -3% -1大盘走弱
沪深300 20日涨幅 > 0 +1
原油 20日涨幅 > 10% -1通胀压力
综合sum clip [-1, 1]
用法
python3 stock_macro.py # 输出宏观评分
python3 stock_macro.py --json # JSON 输出(供矛盾周报引用)
"""
import json, sys, urllib.request
from datetime import datetime, timedelta
from pathlib import Path
OUTPUT = Path.home() / ".hermes" / "stock_backtest"
OUTPUT.mkdir(exist_ok=True)
def get_url(url, timeout=8):
import subprocess, shlex, os
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_url_gbk(url, timeout=8):
import subprocess, shlex, os
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("gbk", errors="ignore")
except Exception:
return ""
def get_kline_change(symbol, days=20):
"""获取指数/品种过去 N 日涨幅"""
today = datetime.now().strftime("%Y-%m-%d")
start = (datetime.now() - timedelta(days=days * 2)).strftime("%Y-%m-%d")
url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get"
f"?_var=kline_dayqfq&param={symbol},day,{start},{today},{days},qfq")
text = get_url(url)
if not text or len(text) < 50:
return None
try:
import re
json_str = re.sub(r"^[^=]+=", "", text, count=1)
data = json.loads(json_str)
key = list(data.get("data", {}).keys())
if not key:
return None
raw = data["data"][key[0]].get("qfqday") or data["data"][key[0]].get("day") or []
if len(raw) < 2:
return None
closes = [float(r[2]) for r in raw if len(r) > 2 and float(r[2]) > 0]
if len(closes) < 2:
return None
return (closes[-1] - closes[0]) / closes[0] * 100
except Exception:
return None
def get_macro_score():
"""
计算宏观评分
返回: {"macro": int, "details": {指标: 涨幅}, "reasons": [...]}
"""
# 上证指数
shanghai = get_kline_change("sh000001", days=20)
# 沪深300用 ETF 510300 代理)
hs300 = get_kline_change("sh510300", days=20)
# 创业板
chinext = get_kline_change("sz399006", days=20)
# 原油
oil = get_kline_change("hf_OIL", days=20)
score = 0
details = {}
reasons = []
if shanghai is not None:
details["上证20日"] = f"{shanghai:+.1f}%"
if shanghai > 0:
score += 1
reasons.append(f"上证20日{shanghai:+.1f}% → 大盘向上")
elif shanghai < -3:
score -= 1
reasons.append(f"上证20日{shanghai:+.1f}% → 大盘走弱")
else:
reasons.append(f"上证20日{shanghai:+.1f}% → 震荡")
if hs300 is not None:
details["沪深300"] = f"{hs300:+.1f}%"
if hs300 > 0:
score += 1
reasons.append(f"沪深300 {hs300:+.1f}% → 蓝筹强")
elif hs300 < -3:
score -= 1
reasons.append(f"沪深300 {hs300:+.1f}% → 蓝筹弱")
if chinext is not None:
details["创业板"] = f"{chinext:+.1f}%"
if chinext < -5:
score -= 1
reasons.append(f"创业板{chinext:+.1f}% → 成长弱")
if oil is not None:
details["原油20日"] = f"{oil:+.1f}%"
if oil > 10:
score -= 1
reasons.append(f"原油{oil:+.1f}% → 通胀压力")
elif oil < -10:
score += 1
reasons.append(f"原油{oil:+.1f}% → 通缩缓解")
# clip 到 [-1, 1]
macro = max(-1, min(1, score))
if macro == 0 and not reasons:
macro = 0
reasons.append("宏观数据获取不足,中性")
return {"macro": macro, "details": details, "reasons": reasons, "date": datetime.now().strftime("%Y-%m-%d")}
def main():
result = get_macro_score()
if "--json" in sys.argv:
out = OUTPUT / "macro_score.json"
with open(out, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(json.dumps(result, ensure_ascii=False))
print(f"📁 已保存: {out}")
return
print("=" * 50)
print(f"小唯宏观评分 {result['date']}")
print("=" * 50)
for k, v in result["details"].items():
print(f" {k}: {v}")
print(f"\n宏观评分: {result['macro']:+d}")
for r in result["reasons"]:
print(f"{r}")
if __name__ == "__main__":
main()

View File

@ -69,6 +69,38 @@ def save_account(acct):
json.dump(acct, f, ensure_ascii=False, indent=2)
def get_dynamic_alloc(industry):
"""
动态仓位2026-08-02 增强按行业动量强度调整
industry_scan.json 读取行业动量
动量 > 0 1.0全仓强势行业
动量 -20% ~ 0 0.6偏弱降仓
动量 -40% ~ -20% 0.5弱势半仓
动量 < -40% 0.3深度弱势轻仓
无数据回退弱势行业 0.5其他 1.0
"""
f = OUTPUT.parent / "industry_scan.json"
mom = None
if f.exists():
try:
scan = json.load(open(f))
mom_map = scan.get("industries", {}).get("avg_mom", {})
mom = mom_map.get(industry)
except Exception:
mom = None
if mom is None:
return 0.5 if industry in WEAK_INDUSTRIES else 1.0
if mom > 0:
return 1.0
if mom > -0.20:
return 0.6
if mom > -0.40:
return 0.5
return 0.3
def execute_signal(sig, dry_run=False):
"""
信号执行多账户路由版 v2 2026-08-01 回测优化
@ -96,21 +128,21 @@ def execute_signal(sig, dry_run=False):
if signal == "BUY" and not has_position:
if acct.get("blocked"):
return ("BLOCK", f"行业{industry}账户已锁定")
# 弱势行业半仓v2
# 动态仓位v3按行业动量强度调整2026-08-02
alloc = get_dynamic_alloc(industry)
is_weak = industry in WEAK_INDUSTRIES
alloc = 0.5 if is_weak else 1.0
shares = int(acct["current_capital"] * alloc // price) if price > 0 else 0
if shares <= 0:
return ("SKIP", "资金不足")
cost = shares * price
if dry_run:
return ("BUY", f"{industry}{'半仓' if is_weak else '全仓'}买入@{price:.2f} {shares}")
return ("BUY", f"{industry}{'半仓' if is_weak else '全仓'}买入@{price:.2f} {shares}(仓位{alloc:.0%})")
acct["positions"].append({"shares": shares, "avg_cost": price})
acct["current_capital"] -= cost
acct["last_signal"] = "买入"
acct["last_signal_date"] = datetime.now().strftime("%Y-%m-%d")
save_account(acct)
mode = "仓(弱势行业)" if is_weak else "全仓"
mode = f"{alloc:.0%}仓(弱势行业)" if is_weak else "全仓"
return ("BUY", f"{industry}{mode}买入{shares}股@{price:.2f} 剩余{acct['current_capital']:.0f}")
elif signal == "SELL" and has_position:

162
scripts/stock_sentiment.py Normal file
View File

@ -0,0 +1,162 @@
#!/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()