股票第六波: 宏观/消息面真实接入四维评分, 动态仓位(0.3-1.0), 数据刷新cron
This commit is contained in:
parent
53f5d24c70
commit
dfb8be201d
|
|
@ -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 新增)
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
@ -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¶m={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()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue