Files
MoFin/scripts/divergence_detector.py
T
知微 9fef32413b refactor: 统一价格入口 mo_data.get_price() - 22个脚本移除自拉腾讯API
所有价格获取统一走 mo_data.get_price() / get_prices_batch():
  - 优先读 live_prices(DB) → 无/过期才调 stock_quote(API) → 自动写回DB
  - 22个脚本全部替换:branch_scanner chip_factors divergence_detector
    market_screener mo_provider mofin_collect monitor_300308 300308_monitor
    multi_timeframe refresh_macro_context stale_detector stale_push_wlin
    stock_profile strategy_evaluator strategy_lifecycle strategy_review
    strategy-staleness-check technical_analysis xiaoguo_signal_consumer
    collect_evaluation_data
2026-07-08 23:54:01 +08:00

317 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
divergence_detector.py — 跨市场背离监测器(no_agent)
每30分钟检测:
1. 科创50 vs 恒指 → 科技股超买/超卖信号
2. A/H 隐含溢价 → 内外资分歧度
3. 上证50 vs 创业板 → 风格轮动信号
4. 恒指 vs 国企指数 → 离岸市场情绪
5. 指数连涨/连跌天数 → 趋势延续/衰竭
输出:
- HIGH/medium divergence → 写入 signal_news (source=divergence_watch)
- 状态文件 macro_divergence_state.json
- no_agent: 有信号才出声
"""
import sys, json, re, datetime, os
from mo_data import get_price, get_prices_batch
from pathlib import Path
BASE = Path("/home/hmo/MoFin")
DATA = BASE / "data"
DB_PATH = DATA / "mofin.db"
STATE_PATH = DATA / "macro_divergence_state.json"
# ── 监测的指数 ──
INDEX_CODES = {
"上证指数": "sh000001",
"深证成指": "sz399001",
"创业板指": "sz399006",
"科创50": "sh000688",
"上证50": "sh000016",
"沪深300": "sh000300",
"恒生指数": "hkHSI",
"国企指数": "hkHSCEI",
}
# ── 背离阈值 ──
DIVERGENCE_STRONG = 5.0 # >5% → strong信号
DIVERGENCE_MODERATE = 3.0 # >3% → moderate信号
STREAK_DAYS = 3 # 连涨/连跌3天 → 信号
def fetch_indices():
"""获取所有指数实时数据(通过 mo_data.get_prices_batch"""
symbols = list(INDEX_CODES.values())
try:
raw = get_prices_batch(symbols)
if not raw:
return {}
except Exception as e:
print(f"[DIVERGE] 采集失败: {e}", file=sys.stderr)
return {}
indices = {}
for sym, (price, change_pct) in raw.items():
indices[sym] = {
"name": "",
"price": price,
"close": 0,
"change_pct": change_pct,
"high": 0,
"low": 0,
"timestamp": "",
}
return indices
def load_history():
"""从MACRO_CONTEXT_LOG加载前几天的指数数据"""
try:
import sqlite3
conn = sqlite3.connect(str(DB_PATH))
rows = conn.execute(
"SELECT indices, created_at FROM macro_context_log "
"WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 5"
).fetchall()
conn.close()
history = []
for row in rows:
idx_data = json.loads(row[0]) if row[0] else {}
history.append({
"indices": idx_data,
"timestamp": row[1],
})
return history
except Exception:
return []
def detect_divergences(indices, history):
"""
检测跨市场背离信号
返回: list of signal dicts {type, level, desc, pairs}
"""
signals = []
def get(name):
"""按中文名找指数"""
sym = INDEX_CODES.get(name)
if sym and sym in indices:
return indices[sym]
# 模糊匹配
for s, idx in indices.items():
if name in idx.get("name", ""):
return idx
return None
sh_comp = get("上证指数")
sz_comp = get("深证成指")
cyb = get("创业板指")
kc = get("科创50")
sz50 = get("上证50")
hs300 = get("沪深300")
hsi = get("恒生指数")
hscei = get("国企指数")
# ── 信号1: 科创50 vs 恒指(科技股vs国际资本)──
if kc and hsi:
kc_chg = kc["change_pct"]
hsi_chg = hsi["change_pct"]
divergence = abs(kc_chg - hsi_chg)
if divergence > DIVERGENCE_STRONG:
signals.append({
"type": "a_h_tech_divergence",
"level": "high",
"desc": f"科创50({kc_chg:+.1f}%) vs 恒指({hsi_chg:+.1f}%) 背离{divergence:.1f}个百分点",
"pairs": [kc, hsi],
"direction": "risk" if kc_chg > hsi_chg else "opportunity",
})
elif divergence > DIVERGENCE_MODERATE:
signals.append({
"type": "a_h_tech_divergence",
"level": "medium",
"desc": f"科创50({kc_chg:+.1f}%) vs 恒指({hsi_chg:+.1f}%) 背离{divergence:.1f}个百分点",
"pairs": [kc, hsi],
"direction": "risk" if kc_chg > hsi_chg else "opportunity",
})
# ── 信号2: 上证50 vs 创业板(价值vs成长)──
if sz50 and cyb:
sz50_chg = sz50["change_pct"]
cyb_chg = cyb["change_pct"]
divergence = abs(sz50_chg - cyb_chg)
if divergence > DIVERGENCE_STRONG:
direction = "opportunity" if sz50_chg > cyb_chg else "risk"
signals.append({
"type": "value_growth_divergence",
"level": "high",
"desc": f"上证50({sz50_chg:+.1f}%) vs 创业板({cyb_chg:+.1f}%) 背离{divergence:.1f}个百分点",
"pairs": [sz50, cyb],
"direction": direction,
})
elif divergence > DIVERGENCE_MODERATE:
direction = "opportunity" if sz50_chg > cyb_chg else "risk"
signals.append({
"type": "value_growth_divergence",
"level": "medium",
"desc": f"上证50({sz50_chg:+.1f}%) vs 创业板({cyb_chg:+.1f}%) 背离{divergence:.1f}个百分点",
"pairs": [sz50, cyb],
"direction": direction,
})
# ── 信号3: 恒指 vs 国企指数(国际资本流向)──
if hsi and hscei:
hsi_chg = hsi["change_pct"]
hscei_chg = hscei["change_pct"]
if hsi_chg < 0 and hscei_chg < hsi_chg:
# 国企跌得比恒指多 → 外资恐慌性卖出H股
signals.append({
"type": "hk_panic_selling",
"level": "high",
"desc": f"国企指数({hscei_chg:+.1f}%)跌幅大于恒指({hsi_chg:+.1f}%)→外资恐慌抛售H股",
"pairs": [hscei, hsi],
"direction": "risk",
})
# ── 信号4: A/H 价格背离(用历史数据检测趋势延续)──
if history and len(history) >= 2:
latest = history[0]["indices"]
prev = history[1]["indices"]
# 科创50连涨检测
if kc:
kc_now = kc["change_pct"]
kc_prev = prev.get("科创50", {}).get("change_pct", 0) if "科创50" in prev else 0
kc_yest = latest.get("科创50", {}).get("change_pct", 0)
# 检测连涨
if isinstance(kc_yest, (int, float)) and isinstance(kc_prev, (int, float)):
streak = 0
if kc_yest > 0: streak += 1
if kc_prev > 0: streak += 1
if kc_now > 0: streak += 1
if streak >= STREAK_DAYS and kc_now > 0:
signals.append({
"type": "tech_streak",
"level": "medium",
"desc": f"科创50连涨{streak}日({kc_prev:+.1f}%→{kc_yest:+.1f}%→{kc_now:+.1f}%)→超买风险",
"pairs": [kc],
"direction": "risk",
})
# ── 信号5: 大盘宽度 + 季节效应 ──
now = datetime.datetime.now()
is_month_end = now.day >= 25 # 月末最后一周
is_friday = now.weekday() == 4
# 总体判断
risk_count = sum(1 for s in signals if s["direction"] == "risk")
opp_count = sum(1 for s in signals if s["direction"] == "opportunity")
# 月末+周五叠加 → 脆弱性增强
if is_month_end and is_friday and risk_count >= 2:
signals.append({
"type": "time_window_risk",
"level": "high",
"desc": f"月末({now.day}日)+周五效应+{risk_count}个风险信号叠加→市场脆弱性高",
"pairs": [],
"direction": "risk",
})
elif is_month_end and risk_count >= 1:
signals.append({
"type": "time_window_risk",
"level": "medium",
"desc": f"月末窗口({now.day}日)+{risk_count}个风险信号→注意控制仓位",
"pairs": [],
"direction": "risk",
})
return signals
def write_state(signals, indices):
"""写入状态文件,供监控 cron 消费"""
levels = [s["level"] for s in signals]
highest = "high" if "high" in levels else ("medium" if "medium" in levels else "none")
directions = [s["direction"] for s in signals]
bias = "risk" if directions.count("risk") > directions.count("opportunity") else "opportunity"
state = {
"level": highest,
"bias": bias,
"signal_count": len(signals),
"signals": signals,
"indices": {k: v["change_pct"] for k, v in indices.items()},
"created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
STATE_PATH.write_text(json.dumps(state, ensure_ascii=False, indent=2))
def write_to_signal_news(signals):
"""HIGH signal → signal_news"""
if not signals:
return
import sqlite3
conn = sqlite3.connect(str(DB_PATH))
high_signals = [s for s in signals if s["level"] == "high"]
if high_signals:
for s in high_signals:
conn.execute(
"INSERT INTO signal_news (signal_id, sector, overall_sentiment, summary, key_articles, searched_stocks, source) VALUES (?, ?, ?, ?, ?, ?, ?)",
(0, "跨市场", f"背离-{s['direction'].upper()}", s["desc"], json.dumps(s, ensure_ascii=False), "", "divergence_watch")
)
conn.commit()
med_signals = [s for s in signals if s["level"] == "medium"]
if med_signals:
summary = "\n".join([s["desc"] for s in med_signals])
conn.execute(
"INSERT INTO signal_news (signal_id, sector, overall_sentiment, summary, key_articles, searched_stocks, source) VALUES (?, ?, ?, ?, ?, ?, ?)",
(0, "跨市场", "背离-MEDIUM", summary, json.dumps(med_signals, ensure_ascii=False), "", "divergence_watch")
)
conn.commit()
conn.close()
def print_report(signals, indices):
"""no_agent 输出"""
if not signals:
return # SILENT
# 有信号就输出
high = [s for s in signals if s["level"] == "high"]
med = [s for s in signals if s["level"] == "medium"]
lines = []
if high:
for s in high:
icon = "\u26a0\ufe0f" if s["direction"] == "risk" else "\u2b06\ufe0f"
lines.append(f"[DIVERGE] {icon} {s['level'].upper()} {s['type']}: {s['desc']}")
if med:
for s in med[:3]: # 最多3条
icon = "\u26a0\ufe0f" if s["direction"] == "risk" else "\u2b06\ufe0f"
lines.append(f"[DIVERGE] {icon} {s['level'].upper()} {s['type']}: {s['desc']}")
# 输出指数全景
idx_line = " | ".join([f"{n}: {indices.get(s, {}).get('change_pct', 0):+.1f}%" for n, s in INDEX_CODES.items() if s in indices])
lines.append(f"[DIVERGE] 指数全景: {idx_line}")
print("\n".join(lines))
def main():
indices = fetch_indices()
if not indices:
return
history = load_history()
signals = detect_divergences(indices, history)
# 写入 state + signal_news
write_state(signals, indices)
write_to_signal_news(signals)
# no_agent 输出
print_report(signals, indices)
if __name__ == "__main__":
main()