Files
MoFin/scripts/macro_signal_consumer.py
T

123 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""
macro_signal_consumer.py — 消费宏观风险信号,写入风险状态
no_agent 模式:有HIGH风险→输出风险摘要 | 无→静默
管道位置:
macro_risk_scanner (8:30/11:30) → signal_news(source=macro_watch) → 本脚本
macro_risk_state.json — 供所有监控 cron 读取
如果 HIGH → 推送到 Dad
"""
import sqlite3, json, os, sys, time
from pathlib import Path
from datetime import datetime
BASE = Path("/home/hmo/MoFin")
DATA = BASE / "data"
DB_PATH = DATA / "mofin.db"
STATE_PATH = DATA / "macro_risk_state.json"
def db_update(conn, sql, params, max_retries=3):
"""幂等DB更新,遇到锁自动重试"""
for attempt in range(max_retries):
try:
conn.execute(sql, params)
conn.commit()
return True
except sqlite3.OperationalError as e:
if "locked" in str(e).lower():
if attempt < max_retries - 1:
time.sleep(1)
continue
raise
print(f"[MACRO-CONSUMER] DB更新失败(持续锁): {sql}", file=sys.stderr)
return False
def main():
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.execute("PRAGMA busy_timeout=5000")
conn.row_factory = sqlite3.Row
# 读取未处理的 macro_watch 信号
rows = conn.execute(
"SELECT * FROM signal_news WHERE source='macro_watch' AND (processed=0 OR processed IS NULL)"
).fetchall()
if not rows:
# 无新信号时状态文件维持 15 分钟过期
try:
state = json.loads(STATE_PATH.read_text())
created = datetime.strptime(state.get("created_at", "2000-01-01"), "%Y-%m-%d %H:%M:%S")
if (datetime.now() - created).total_seconds() > 900: # 15 min
state["level"] = "none"
state["expired"] = True
STATE_PATH.write_text(json.dumps(state, ensure_ascii=False, indent=2))
except:
pass
conn.close()
return # SILENT
# 聚合风险等级(考虑修正覆盖信号)
levels = {"宏观-WATCH_HIGH": "high", "宏观-WATCH_MEDIUM": "medium", "宏观-WATCH_INFO": "info"}
highest = "info"
all_summaries = []
def _effective_level(sentiment, summary):
"""修正覆盖信号取修正后的级别,不按原始sentiment算"""
if "修正覆盖" in (summary or ""):
s = (summary or "").lower()
# 明确说零风险/正面/利好 → info
if any(kw in s for kw in ["零风险", "正面利好", "正面进展", "非风险", "利好"]):
return "info"
# 明确说MEDIUM → medium
if "medium" in s or "中风险" in s:
return "medium"
# 修正覆盖HIGH → 默认降为medium(不保留原始HIGH)
return "medium"
return levels.get(sentiment, "info")
for r in rows:
sentiment = r["overall_sentiment"]
lv = _effective_level(sentiment, r["summary"])
if lv == "high":
highest = "high"
elif lv == "medium" and highest != "high":
highest = "medium"
all_summaries.append({
"sentiment": sentiment,
"summary": r["summary"][:300],
"key_articles": r["key_articles"],
"created_at": r["created_at"],
})
# 写入风险状态文件
state = {
"level": highest,
"signals": all_summaries,
"signal_count": len(rows),
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"expired": False,
}
STATE_PATH.write_text(json.dumps(state, ensure_ascii=False, indent=2))
# 标记为已处理(含重试)
for r in rows:
db_update(conn, "UPDATE signal_news SET processed=1 WHERE id=?", (r["id"],))
conn.close()
# no_agent 输出(有 HIGH 才主动出声)
if highest == "high":
print(f"[MACRO-RISK] ⚠️ HIGH: {len(rows)}条高风险信号")
for s in all_summaries:
print(f" {s['summary'][:100]}")
elif highest == "medium":
if len(os.environ.get("MACRO_VERBOSE", "")) > 0:
print(f"[MACRO-RISK] MEDIUM: {len(rows)}条中风险信号")
# HIGH 信号会通过 no_agent 推送到 XMPP
if __name__ == "__main__":
main()