123 lines
4.3 KiB
Python
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()
|